# koru documentation website

English and Spanish documentation for people creating Android apps with koru. Built with Docsify 4 and Docsify Themeable, with a soft lavender reading theme. Existing Docsify navigation, search and copy-code controls are reused. Phosphor duotone icons, fonts and application scripts are served locally. The sidebar links to the Ko-fi donation page.

## Develop

Requires Bun (tested with 1.2.13) and a fresh checkout of the repository:

```sh
cd website
bun install --frozen-lockfile
bun run dev
```

Visit http://localhost:4173/docs/en/ or http://localhost:4173/docs/es/. The site builds before serving. After editing Markdown, CSS or scripts, rebuild and refresh; this intentionally small server does not provide hot reload.

## Build and deploy

```sh
SITE_URL=https://koru.js.org/ bun run build
bun run preview
```

Replace the URL with the real public HTTPS domain and optional subpath. Deploy the contents of `dist/` at that path. Ordinary static directory indexes are sufficient: every localized route has its own `index.html` and `README.md`. Do not configure an SPA catch-all that turns missing URLs into HTTP 200. Serve `404.html` with HTTP 404.

Without `SITE_URL`, the build uses localhost and marks preview pages `noindex`. With a real HTTPS `SITE_URL`, it emits indexable HTML, absolute self-canonicals, reciprocal English/Spanish `hreflang`, Open Graph and Twitter large-image metadata (`assets/og-en.png`, `assets/og-es.png`, 1200×630), a JSON-LD `@graph` per page, `robots.txt` and a sitemap. No domain or ranking is fabricated. The public URL must be set correctly before deployment. When deploying under a subpath, merge the generated robots directives into the host’s domain-root `/robots.txt`; crawlers do not use a robots file inside a subdirectory. The sitemap can stay under the subpath.

## SEO

- **Sitemap** (`sitemap.xml`): both landings and every localized page, each with `xhtml:link` alternates (`en`, `es`, `x-default`). `lastmod` is the date of the last commit that touched the page's source (`git log -1 --format=%cI`; landings use `scripts/landing.ts`). Without git history the `lastmod` is omitted rather than invented, so CI checks out with `fetch-depth: 0`. Redirect and `noindex` pages are never listed.
- **Root `/`**: a redirect page to the English landing (`/en/`): canonical to `/en/`, `meta refresh` 0 and `location.replace`, with fallback links to `/en/` and `/es/` and no `meta robots`. It is not in the sitemap. Landings and documentation point their `x-default` at `/en/` and the English page respectively.
- **Old URLs** (`/<lang>/<slug>/`, `/es/inicio/`): canonical to the new URL, `meta refresh` 0 and `location.replace`, without `noindex`.
- **Structured data**: each page has a `@graph` with `WebSite` and `Organization`; documentation adds `BreadcrumbList` (Home, Documentation, page) and `TechArticle` with `dateModified`; landings add `SoftwareApplication` (`softwareVersion` is read from `const version` in `cmd/koru/main.go`). There are no ratings.
- **robots.txt** allows everything and lists the sitemap. The archive viewers under `original/` carry `noindex,follow`, which a crawler can only read if it is allowed to fetch them, so they are not disallowed. Raw `.md`, `.txt` and `.json` records cannot carry `noindex`.
- **Verification (optional)**: set `GOOGLE_SITE_VERIFICATION`, `BING_SITE_VERIFICATION` and/or `YANDEX_VERIFICATION` at build time to emit `google-site-verification`, `msvalidate.01` and `yandex-verification` meta tags on the landings. They are repository variables in CI.
- **IndexNow (optional)**: with `INDEXNOW_KEY` (`[a-zA-Z0-9-]{8,128}`, otherwise the build fails) the build writes `dist/<key>.txt`. The `notify` job in `pages.yml` then posts the sitemap URLs to `https://api.indexnow.org/indexnow` after deploying; it is `continue-on-error`. Google does not support IndexNow.
- `bun test` includes an SEO linter over a production build (unique titles and descriptions, one h1, canonicals, reciprocal hreflang, og:image files, JSON-LD, sitemap, legacy redirects, root redirect). Docsify scripts stay blocking on purpose: with `defer`, plugins can register after Docsify has started, and `test:browser` catches it.

The landing pages (`scripts/landing.ts`) live at `/en/` and `/es/`, the domain root redirects to `/en/`, and the documentation lives under `/docs/en/` and `/docs/es/` (old `/<lang>/<page>/` URLs redirect there); its copy lives in that file rather than in Markdown. Language switching in the documentation preserves the current document. The build also publishes `internal/config/koru.schema.json` at `/schema/koru.schema.json` for editors. Other languages can be added to `scripts/catalog.ts`, with matching content and UI translations. Missing content fails the build rather than silently using another language.

## llms.txt

The build generates `llms.txt` (English index), `es/llms.txt` and `llms-full.txt` (every English page concatenated as plain Markdown) from `scripts/catalog.ts` via `scripts/llms.ts`; none is written by hand. Links are absolute URLs to each page's `README.md`, and the build fails if one does not exist. Landing and documentation pages advertise it with `<link rel="alternate" type="text/plain" title="llms.txt">`, and the landing footer links to it.

Text records in `original/` have noindex HTML viewers, with download links preserving their original bytes. Example HTML/SVG sources are stored as `.txt` downloads so they cannot execute as another app on the documentation origin. This works on GitHub Pages, which cannot set custom response headers. `robots.txt` does not block the archive, so crawlers can read the `noindex` of its viewers.

See [DEPLOYMENT.md](DEPLOYMENT.md) for the Pages workflow and the live `koru.js.org` domain. Rebuild after changing the Pages custom domain so generated routes and canonical URLs use the new base path.

## Appearance

Use the appearance menu in the header to choose System, Light or Dark (Sistema, Claro u Oscuro in Spanish). System follows changes to the OS preference; explicit choices persist across pages, language switches and tabs. The theme initializes before CSS to avoid a light flash. If preference storage is unavailable, the menu still works for the current page. With JavaScript disabled, the site retains its light reading theme and hides the inactive control.

Long guides with at least three main sections include a collapsible, localized “On this page” index above the first section. It is part of the prerendered HTML for readers without JavaScript and is rebuilt after Docsify navigation so links always match the rendered headings.

## Content and provenance

English guides are in `content/en/`; original Spanish reference pages are read directly from the repository at build time unless a richer localized page exists in `content/es/`. New Spanish guides live in `content/es/`. The public navigation prioritizes first steps, app guides and reference. Architecture, DEX and JNI live under Inside koru. Historical reports keep their routes and are reachable through the archive. The build includes every Markdown file from `docs/`, plus the root README, engineering instructions, testing/native/example READMEs, raw results and example sources. `original/manifest.json` records SHA-256 hashes. Original documents retain their historical language and claims; the reading guides explicitly distinguish early DEX-only evidence from later APK execution.

When changing a reference, update the English counterpart. Add every new page to `scripts/catalog.ts`; both locales must be present. The build rejects unresolved relative documentation links. The tests check locale parity, coverage, source hashes and metadata. Run the build again after changing source docs or validation notes.

## Validate

```sh
bun run build
bun test
bun run test:browser
bun run format:check
```

Browser tests launch the installed `google-chrome-stable` with Playwright, rather than downloading a separate Chromium. Set `CHROME_PATH` if the executable is elsewhere. They start their own preview server; port 4183 must be available. Checks cover every document route, the landing pages, locale switching, locale-scoped search, copying, keyboard focus and section-index navigation, responsive overflow, axe accessibility, HTTP 404, no-JavaScript reading, local-only assets and metadata updates. Screenshots and the JSON report go to ignored `test-results/`. See [VALIDATION.md](VALIDATION.md) for recorded results.

Website tooling is separate from koru's Go compiler and never enters an APK build. Upstream licenses are copied into `dist/assets/licenses/`. See [DESIGN.md](DESIGN.md) for design decisions and official research references.

## Hero terminal recording

`assets/hero-demo.gif` is a real CLI run recorded from [scripts/hero.tape](scripts/hero.tape) with [Charm VHS](https://github.com/charmbracelet/vhs). It initializes a temporary project, builds a signed APK, and verifies it. The recording's private signing identity stays in the temporary directory; it is never a website asset. A static frame is served when the reader prefers reduced motion. To regenerate, compile `bin/koru`, install VHS and its `ttyd`/FFmpeg dependencies, then run from the repository root:

```sh
KORU_BIN="$PWD/bin/koru" vhs website/scripts/hero.tape
gifsicle --resize-width 960 -O3 --colors 128 website/assets/hero-demo.gif -o /tmp/koru-animation.gif
gifsicle --unoptimize /tmp/koru-animation.gif '#-1' -o /tmp/koru-final.gif
gifsicle --delay=220 /tmp/koru-final.gif --delay=8 /tmp/koru-animation.gif -o /tmp/koru-loop.gif
gifsicle -O3 --colors 128 /tmp/koru-loop.gif -o website/assets/hero-demo.gif
ffmpeg -y -i website/assets/hero-demo.gif -frames:v 1 website/assets/hero-poster.png
```

The first GIF frame holds the completed output briefly before replaying the command sequence. A `<picture>` source substitutes the still poster under `prefers-reduced-motion: reduce`. The same commands and representative output appear as accessible text below it. It is documentation media, never part of an APK.
