diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml
index 77ccbb7..71d51fc 100644
--- a/.github/workflows/deploy-pages.yml
+++ b/.github/workflows/deploy-pages.yml
@@ -18,7 +18,10 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
+ # The sitemap dates come from git history, so fetch all of it.
- uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 8c28436..3bb2a13 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -40,7 +40,10 @@ jobs:
workspaces:
runs-on: ubuntu-latest
steps:
+ # The site tests read sitemap dates from git history, so fetch all of it.
- uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
diff --git a/AGENTS.md b/AGENTS.md
index a28af6e..7f26fc3 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -55,7 +55,9 @@ Four places, or it does not build: the shell at `apps/site//index.html`,
the body at `apps/site/src/pages/.ts`, an entry in `tools/prerender.ts`,
and an input in `vite.config.ts`. Add the path to `INDEXABLE_PATHS` in
`tools/gen.mjs` as well — that one array feeds both the sitemap and the check
-that robots.txt never blocks a URL the sitemap advertises.
+that robots.txt never blocks a URL the sitemap advertises. Also add the page's
+source files to `PAGE_SOURCES`. The sitemap `` comes from their git
+history.
Three more lists are hardcoded, and a page missing from them ships with **no
test failing** — the silent gap, so add the page to all three: `PAGES` in
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c571193..be79613 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -40,10 +40,10 @@ place ([Versioning](docs/spec/07-versioning.md)); layout follows
`category` narrowed to how a brewer brews, and `pour-over-machine`, `drip` and
`capsule` added to it. An all-in-one carries more than one role, which one
member could not express. Registry data: nothing bumps.
-- **Thirty-three gear entries and twenty-six varietals.** The gear baskets
- collapse from eight per-dose slugs to four families, the dose moving to
- `variant`. Every varietal entry now carries a `kind` and the `species` epithet
- it is sold as, both optional.
+- **Thirty-three gear entries and twenty-six varietals.** The four per-dose
+ basket slugs become two families, with the dose in `variant`. Every varietal
+ entry now carries a `kind` and the `species` epithet it is sold as, both
+ optional.
### Changed
@@ -54,8 +54,21 @@ place ([Versioning](docs/spec/07-versioning.md)); layout follows
authoring schema enforces the omission for registered ids and leaves it alone
for an id the registry does not carry, which still needs the fallback.
- **`gearLabel` in `@coffeejson/core` resolves a known id** against the
- registry's own labels and renders `variant` beside it, rather than reading the
- display string off the document. The bundled map is keyed by language tag.
+ registry's own labels and renders `variant` beside that label, rather than
+ reading the display string off the document. The bundled map is keyed by
+ language tag. For `custom` and an unknown id, it shows the producer's `label`
+ as written. Without a `label`, it shows `brand` and `model`, then `variant`.
+
+### Removed
+
+- **Four per-dose basket slugs and their aliases are retired** from the
+ [gear registry](docs/spec/06-vocabularies.md#gear-registry):
+ `ims-precision-18g` (`ims-precision-18`), `vst-precision-15g` (`vst-15g`),
+ `vst-precision-18g` (`vst-18g`) and `vst-precision-20g` (`vst-20g`). Emit
+ `ims-precision` or `vst-precision` with the dose in `variant`:
+ `{ "id": "vst-precision", "variant": "18 g" }`. The old slugs do not resolve
+ to a family, because the family does not identify the dose. A consumer
+ treats them as unknown ids.
### Fixed
diff --git a/apps/site/DESIGN.md b/apps/site/DESIGN.md
index 622cfe2..0554e27 100644
--- a/apps/site/DESIGN.md
+++ b/apps/site/DESIGN.md
@@ -174,6 +174,9 @@ with a 22% radius, the iOS mask, so a square export reads as the icon it is.
Implementations · Validator · Generate) over a 2px `on-surface` rule; the heavy rule is
what makes it a masthead rather than chrome. The current page is bold with
`aria-current`, not accent-colored. Brew-along strips it to the wordmark.
+- **Links footer** — one row of links in a fixed order (Home · Browse · Showcase · Spec ·
+ For AI agents · GitHub), separated by dots, above the license line. Only `siteFooter`
+ builds it. Spec links to GitHub because the site has no page at `/docs/`.
- **Button** (`components.button`); **ghost** (`components.button-ghost`) is a modifier on
it with a primary border. Hover shades the fill 15% toward `on-surface`, instantly.
- **Chip** (`components.chip`, on-state `components.chip-on`) — facet filters and the
diff --git a/apps/site/src/lib/beans-body.ts b/apps/site/src/lib/beans-body.ts
index 46a6be3..cf927e1 100644
--- a/apps/site/src/lib/beans-body.ts
+++ b/apps/site/src/lib/beans-body.ts
@@ -2,8 +2,8 @@ import beans from "../generated/beans-index.json";
import {
CORRECTIONS,
LICENSE_SITE,
- licenseLine,
QUOTED_PROSE,
+ siteFooter,
} from "./footer.mjs";
import { siteHeader } from "./site-header.mjs";
import { esc, slugify } from "./text.mjs";
@@ -76,10 +76,5 @@ export const beansBody = (selected: string | null = null): string => {
${shown.length ? shown.map(card).join("") : `
No bag matches that roaster.
`}
- `;
+ ${siteFooter(LICENSE_SITE, QUOTED_PROSE, CORRECTIONS)}`;
};
diff --git a/apps/site/src/lib/footer.d.mts b/apps/site/src/lib/footer.d.mts
index 4678837..d5be896 100644
--- a/apps/site/src/lib/footer.d.mts
+++ b/apps/site/src/lib/footer.d.mts
@@ -12,3 +12,9 @@ export declare const CRAWLERS_UNCHANGED: string;
export declare function licenseLine(...clauses: string[]): string;
/** Joins the given license clauses with the privacy sentence, always last. */
export declare function footerHtml(...clauses: string[]): string;
+/** The spec on GitHub. */
+export declare const SPEC_URL: string;
+/** Footer links as `[href, label]`, in display order. */
+export declare const FOOTER_LINKS: readonly (readonly [string, string])[];
+/** The footer: the links row, then the license line. */
+export declare function siteFooter(...clauses: string[]): string;
diff --git a/apps/site/src/lib/footer.mjs b/apps/site/src/lib/footer.mjs
index 19687a8..2c161d1 100644
--- a/apps/site/src/lib/footer.mjs
+++ b/apps/site/src/lib/footer.mjs
@@ -3,6 +3,8 @@
// `tools/gen.mjs` is plain Node. A `.ts` module reaches only half the pages, and
// the sentence below is then hand-copied into the other half.
+import { GITHUB_URL } from "./site-header.mjs";
+
/** License line for the hand-written pages, which cover more than the corpus. */
export const LICENSE_SITE =
"Spec, schema and corpus: CC0 1.0 Universal — public domain.";
@@ -57,3 +59,34 @@ export const licenseLine = (...clauses) =>
*/
export const footerHtml = (...clauses) =>
``;
+
+/**
+ * The spec on GitHub. The site has no spec page at `/docs/`, so links for
+ * readers go here.
+ */
+export const SPEC_URL = `${GITHUB_URL}/tree/main/docs/spec`;
+
+/** The footer links, in display order. */
+export const FOOTER_LINKS = [
+ ["/", "Home"],
+ ["/recipes/", "Browse"],
+ ["/showcase/", "Showcase"],
+ [SPEC_URL, "Spec"],
+ ["/agents/", "For AI agents"],
+ [GITHUB_URL, "GitHub"],
+];
+
+/**
+ * The links row and the license line. Every page with this footer calls this
+ * function, so the pages cannot drift apart.
+ *
+ * @param {...string} clauses
+ * @returns {string}
+ */
+export const siteFooter = (...clauses) =>
+ ``;
diff --git a/apps/site/src/pages/implementations.ts b/apps/site/src/pages/implementations.ts
index c73ba39..6c1ba05 100644
--- a/apps/site/src/pages/implementations.ts
+++ b/apps/site/src/pages/implementations.ts
@@ -2,14 +2,13 @@
// implement this. Where the format is used is `/showcase`. A page headed "built
// with" listing one app reads as a claim.
-import { LICENSE_SITE, licenseLine, PACKAGES } from "../lib/footer.mjs";
+import { LICENSE_SITE, PACKAGES, siteFooter } from "../lib/footer.mjs";
import { siteHeader } from "../lib/site-header.mjs";
// The site serves the docs only as raw Markdown at their exact paths, so a page
// links the rendered copy on GitHub, the way the landing and showcase pages do.
const REPO = "https://github.com/coffeejson-org/coffeejson";
const GUIDE = `${REPO}/blob/main/docs/integration-guide.md`;
-const SPEC = `${REPO}/blob/main/docs/README.md`;
/** The implementations body. Prerendered — see the note on `landingBody`. */
export const implementationsBody = (): string => `
@@ -62,9 +61,4 @@ export const implementationsBody = (): string => `
and it appears on the showcase. No approval step,
nothing to sign.