Skip to content

appfair/appland

Repository files navigation

appland

Generic Astro template that turns an appindex.json publication document into a localized landing page (or a localized catalog of landing pages), with platform-specific content (iOS / Android), screenshots, permissions, and per-locale store badges.

appindex.json is expected to conform to the App Fair appindex schema — JSON Schema at https://appfair.org/schemas/appindex/v1.json.

How sites use it

The template lives in a vendored site/appland/ directory next to a small site/siteinfo.yaml config and the site/appindex.json data file:

your-repo/
└── site/
    ├── siteinfo.yaml      # template config (title, host, theme, …)
    ├── appindex.json      # generated by your release pipeline
    └── appland/           # this template, dropped in unmodified
        ├── src/
        └── …

At build time the template reads ../siteinfo.yaml, follows the appindex field to find appindex.json, and generates the static site into site/appland/dist/.

For a CI example, netskip.app is built from https://github.com/Net-Skip/Net-Skip/releases/latest/download/appindex.json via this workflow.

Single-app vs. multi-app mode

The template auto-selects between two render modes based on the number of entries in appindex.json's apps[] array:

Single-app mode (apps.length === 1)

The classic per-app landing page, e.g. https://netskip.app. Generated routes:

Route Renders
/ Default-locale landing page, with browser-language auto-redirect to /<locale>/.
/<locale>/ One per locale present in appindex.json.
/sitemap-*.xml Generated by @astrojs/sitemap.
/robots.txt Dynamic.

Multi-app mode (apps.length > 1)

Catalog mode, e.g. https://appfair.net aggregating every App Fair app into one site. Generated routes:

Route Renders
/ Default-locale catalog index, with auto-redirect.
/<locale>/ Catalog index in that locale (one card per app).
/<locale>/apps/<slug>/ Per-app landing page, identical to single-app mode but with the language-picker hopping to the same app in other locales.

In multi-app mode the locale list is the union of every app's locales — each app falls back gracefully when rendered in a locale it doesn't have content for (the existing xx-YY → xx → en-US → en → first resolution applies). Slugs come from each app's name field, which by convention matches the GitHub repo name.

What stays generic

  • appindex.json is the single source of truth for app content; the template contains no app-specific assumptions.
  • Locales are discovered dynamically by walking every localized field in the document. The default locale is en-US if present, then en, then whatever appears first.
  • The platform picker hides itself when only one platform is published.
  • Asset URLs are resolved against each app's source.assets, so screenshots and icons load from wherever the publishing pipeline puts them.
  • Store badges are localized: <locale> versions ship under public/badges/<locale>/{apple-app-store,google-play-store}.svg. The set of locales mirrors skipstone's normalizeLocaleApple() and normalizeLocaleGoogle() outputs (so an appindex written by skipstone always finds a matching badge); per-store fallback is exact → language-only → en so URLs never 404.
  • App descriptions and release notes can carry the small subset of HTML Google Play permits (<b>, <i>, <u>, <br>, <a>); a whitelist-based sanitizer renders those as real markup and escapes everything else as visible text. Plain-text Apple-style descriptions pass through unchanged.

Develop

cd site/appland
npm install
npm run dev          # http://localhost:4321

Build

npm run build        # outputs to site/appland/dist/
npm run preview      # local preview of the built site
npm run check        # astro check (TypeScript + Astro diagnostics)

Customize

Edit site/siteinfo.yaml. Recognized fields:

Field Required Description
title yes Site name (used in <title>, OpenGraph, header, multi-app catalog hero).
host yes Canonical site URL (sitemap, OpenGraph, canonical).
appindex yes Path to the publication document, relative to siteinfo.yaml.
tagline Subtitle below the title in the multi-app catalog hero, and <meta description> fallback.
accentColor Brand accent (any CSS colour). Defaults to #3B82F6.
defaultTheme light | dark | system. Defaults to system.
defaultPlatform ios | android. Defaults to ios. Hidden when only one platform is published.
showSourceLink Show source-repo link in the footer. Default true.
showStoreBadges Show App Store / Play Store badges. Default true.
showPermissions Show the permissions section. Default true.
showDependencyCount Show the SBOM dependency count. Default true.
footer Footer line; {year} is interpolated; the description sanitizer's HTML subset (notably <a>) is supported.
analyticsScript Optional analytics script URL.
analyticsDomain data-domain attribute for the analytics script.
socialImage OpenGraph card image. Falls back to feature-graphic, then app icon.
pagefind When true, runs Pagefind over the built site and adds a search bar to the header. Default false.

Localizable. Fields marked † accept either a plain string or a locale-keyed map, so e.g.

title:
  en:      'The App Fair Project'
  de:      'Das App-Fair-Projekt'
  zh-Hans: 'App Fair 项目'

renders the right text in each locale's page. Lookup uses the same exact → language-only → en-US → en → first fallback as the appindex's own localized fields, so partial coverage is fine.

Helper scripts

  • scripts/download-badges.mjs — refreshes the localized App Store / Play Store SVG badges under public/badges/<locale>/. Run when the upstream badge artwork changes.

    node scripts/download-badges.mjs
  • scripts/release-bump.sh — see below.

Releasing

Sites consume the template by checking it out at a tag in their build workflow, e.g.:

- uses: actions/checkout@v6
  with:
    repository: appfair/appland
    ref: v1                           # floating major-version tag
    path: site/appland

To cut a new release of the template, use scripts/release-bump.sh from the repo root. It bumps the latest v<major>.<minor>.<patch> tag, creates a GitHub release with auto-generated notes, and force-updates the floating v<major> tag so consumers pinning to v1 immediately pick up the release.

scripts/release-bump.sh                 # patch (default): v1.2.3 → v1.2.4
scripts/release-bump.sh --bump=minor    # v1.2.3 → v1.3.0
scripts/release-bump.sh --bump major    # v1.2.3 → v2.0.0

If no v*.*.* tag exists yet, the script treats the current version as v1.0.0 (so the first patch becomes v1.0.1, first minor v1.1.0, first major v2.0.0).

After computing the next version, the script shows the repo, branch, HEAD commit, and prompts:

Do you want to tag and release v1.2.4 and update the v1 tag? [y/N]

On y it runs:

  1. git tag -a v1.2.4 -m "Release v1.2.4"
  2. git push origin v1.2.4
  3. gh release create v1.2.4 --generate-notes --title v1.2.4
  4. git tag -f v1 v1.2.4
  5. git push --force origin v1

Pre-flight checks: working tree must be clean, gh must be on PATH, remote tags are fetched first, and the script aborts if the computed tag already exists locally. Each step is echoed before running so the operation is auditable from the terminal.