Skip to content

Repository files navigation

appsite

One implementation of an App Store companion site, in eleven languages.

A small app on the store needs the same pages every time: what it is, support, a privacy policy, terms, and — for a German operator — an Impressum. In eleven languages that is 45 files. Writing them twice is how the second app ends up wearing the first app's navigation, and how a fix to the checker lands on one app and rots on the others.

This package holds the structure. Each app holds its own words.

What is in here, and what is not

Here (shared) In the app (owned)
Navigation, language switcher, hreflang The app's name, brand markup, icon
The document: <head>, assets, <main> Which pages exist, and any page the kit does not know about
Page blocks: hero, cards, showcase, note, lockup, stats Which blocks, in what order, with what copy
Legal-page structure and the standard Apple links Every sentence of the privacy policy and terms
The stylesheet Its palette, as token overrides
The checker, and App Store Connect's field limits What the listing is required to point at
The index of every app, at the site root The card it contributes to it

The split is not arbitrary. Structure is single-sourced because a bug in it is one bug. Prose is owned because a sentence that has to change for one app must not change for the others — a privacy policy is a promise about a particular program, and sharing the text would eventually make one of the promises false.

Use it

Add the kit as a submodule and write one config file:

git submodule add https://github.com/ikunin/appsite.git vendor/appsite
mkdir appstore && cp vendor/appsite/template/* appstore/

vendor/appsite and appstore/, in every repository, exactly. Both lowercase: Tools/ and tools/ are the same directory on a case-insensitive Mac and two different ones on Linux CI, a trap worth stepping around once rather than debugging per repo. And one name for the config everywhere means a recipe written here runs in any of these repositories unedited — three names for the same directory is a lookup table the docs have to carry forever.

site_config.py finds the kit by walking up to it, so nothing breaks if a repository does put it elsewhere. It should not.

site_config.py is the whole interface:

SITE = Site(
    chrome=Chrome(brand="Harbor&nbsp;Rush", icon="img/icon-512.png",
                  pages=PAGES, copyright="© Igor Kunin 2026"),
    out="site",
    metadata=os.path.join("fastlane", "metadata"),
    palette={"accent": "#1ec8b6"},
)

Then, in the app's Makefile:

site:
	python3 appstore/install_site_assets.py
	python3 appstore/make_site_translations.py
	python3 appstore/make_site_legal.py
	python3 appstore/make_site_impressum.py
	python3 appstore/make_site_card.py
	python3 appstore/check_site.py

Order matters at both ends: install_site_assets.py writes the stylesheet the pages reference, and check_site.py is the gate. An app with a page the kit does not know about adds its own generator between them.

check_site.py also measures every listing field against what App Store Connect accepts — name and subtitle at 30 characters, keywords at 100, promotional text at 170, descriptions and release notes at 4000. Connect rejects rather than truncates, and it does so at upload, so without this the first you hear of one extra character is a failed submission of a build that has already been made. Measured in every locale, because a translation is longer than its source more often than not: the overrun that prompted this was French, at 31 against 30.

The listing is the source of the pitch

The landing page's headline and opening paragraph are read from metadata/<locale>/subtitle.txt and description.txt — fastlane's layout, which App Store Connect's own tools also expect. Those words are already translated and already reviewed, and they are what a customer meets before they ever reach the site. Translating the same pitch twice guarantees the two drift.

The apps make the index

One Pages site serves every app, a directory each, and its root lists them: ikunin.github.io/apps/. That page is not a list anybody keeps up to date.

make_site_card.py writes site/app.json — the app's name, the App Store subtitle it already ships, its icon and its accent, every one of them read from site_config.py or the listing. Publishing copies site/ wholesale, so the card lands on the branch with the pages, and publish.py rebuilds the index from every card it finds there.

An app is on that page because it published. A fourth app needs no edit in this repository at all, and an app whose card is missing has simply not been rebuilt since this existed.

A card is an icon, a name and one slogan. Nothing on that page describes the apps as a group — they differ in what they collect, and a sentence true of two of them is a false statement about the third. Its own wording lives in portfolio_config.py.

python3 vendor/appsite/publish.py --index-only     # rebuild just the root

Translating a page

languages.py carries what is the same for every app: language names, navigation labels for the five standard pages, and the governing-language clause. A page kind the kit does not know needs its own labels on the Page — that is the seam that keeps "Songs" out of a site kit.

Every translated privacy and terms page ends with a line saying the English version governs, linking back to it. A promise that reads differently in two languages is a legal problem; this resolves it in the open rather than hiding it.

template/

A starting point to copy into an app, not a dependency. It holds every script the recipe above runs, so a new app's make site works before a word of its own copy exists — and then refuses to finish, naming what is missing.

File What you do with it
site_config.py Write it: brand, palette, Impressum, which pages exist
site_text_privacy.py, _support.py, _terms.py Rewrite the app-specific keys. Each file's header lists them
make_site_translations.py Write it: the landing page below the hero, in eleven languages
install_site_assets.py, make_site_legal.py, make_site_impressum.py, make_site_card.py, check_site.py Nothing. Copy and leave alone

The legal text tables come from a real shipped app, so most of the eleven-language work is already done — but each file's header lists the keys that describe that app and must be rewritten. Read them. A privacy policy inherited without reading is worse than no policy, and it is a promise about a program it was not written for.

make_site_translations.py carries English only, on purpose: there is nothing to inherit for a landing page, and a build that stops with no landing copy for: de, fr, … is a better instruction than ten paragraphs about another app.

Tests

python3 tests/test_appsite.py

The kit's own gate, when it was extracted, was that it re-rendered a shipped 46-page site byte-identically. That is the check to repeat after changing anything here: build the app's site, git diff, and expect nothing.

About

One implementation of an App Store companion site, in eleven languages. Structure shared; each app owns its words.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages