diff --git a/PROVENANCE.md b/PROVENANCE.md
index a6b984e..650c53a 100644
--- a/PROVENANCE.md
+++ b/PROVENANCE.md
@@ -23,6 +23,13 @@ immutable Git blob object IDs, source and published digests, published
dimensions, transformations, license, notices, and alternative text in
`src/data/media.json`.
+The crystal favicon and pinned-tab mask under `public/` are new MIT-licensed
+vector artwork authored for website issue #27 by Zoey Rose with Codex
+implementation assistance. Their exact source paths, Git blob object IDs,
+SHA-256 digests, dimensions, transformations, purposes, and notices are closed
+records in `src/data/icons.json`; the checked-in SVG is both retained source and
+published file.
+
The synthetic download and media records under `tools/` are contract-only test
data. They do not name or contain a real release or asset and are never
published by Astro. `src/data/downloads.json` remains empty until exact
diff --git a/README.md b/README.md
index 4ea8610..21b4816 100644
--- a/README.md
+++ b/README.md
@@ -53,6 +53,20 @@ from `public/media/`; their exact source Git blob object IDs, digests,
dimensions, transformations, licenses, notices, and alternative text are
recorded in `src/data/media.json`.
+Every indexable route constructs a closed, typed page identity through
+`src/lib/metadata.ts`: one unique title and description, canonical URL, robots
+policy, internally consistent Open Graph/Twitter fields, preview image
+dimensions, and alternative text. `atrinik-now` is the documented sitewide
+preview fallback until issue #22 supplies approved replacement artwork; routes
+can select a more relevant record from the same validated media catalog. The
+homepage also emits the canonical Atrinik `WebSite` identity as safely
+serialized inert JSON-LD. The 404 is noindex and deliberately has no canonical,
+social-preview, or structured identity.
+
+The new crystal favicon and pinned-tab mask are compact repository-authored SVG
+files. Their closed authorship, license, source, hash, Git blob, dimensions,
+transformation, purpose, and notice records live in `src/data/icons.json`.
+
## Development
Use Node 24.18.1 and npm 11.16.0:
@@ -67,8 +81,10 @@ npm run deploy:dry-run
The build writes a self-contained static site to `dist/`. Validation rejects
unproven media, mutable download coordinates, missing attribution or alt text,
unsafe links, repository-authored client JavaScript, broken internal links, and
-page-weight budget violations. Generated release evidence belongs under ignored
-`build/`.
+page-weight budget violations. It permits only inert, locally serialized
+`type="application/ld+json"` data blocks; executable scripts, script sources,
+event handlers, and JavaScript assets remain forbidden. Generated release
+evidence belongs under ignored `build/`.
`src/data/downloads.json` is a reviewed immutable catalog, not a live release
feed. Its closed schema keeps a release repository separate from an artifact
diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index 27ced8b..b0e684d 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -26,3 +26,7 @@ lockfile.
Linked game packages and authored content are not distributed by this source
repository and retain their own exact licenses and notices.
+
+The favicon and pinned-tab mask are new MIT-licensed repository artwork, not
+third-party game assets. Their authorship and immutable provenance bindings are
+recorded in `src/data/icons.json` and `PROVENANCE.md`.
diff --git a/contracts/icon.schema.json b/contracts/icon.schema.json
new file mode 100644
index 0000000..16a0afe
--- /dev/null
+++ b/contracts/icon.schema.json
@@ -0,0 +1,44 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://atrinik.org/contracts/icon.schema.json",
+ "title": "Atrinik website icon provenance record",
+ "type": "object",
+ "additionalProperties": false,
+ "required": [
+ "id",
+ "publicPath",
+ "sourceRepository",
+ "sourcePath",
+ "sourceRevision",
+ "sourceSha256",
+ "publishedSha256",
+ "width",
+ "height",
+ "author",
+ "license",
+ "transformations",
+ "purpose",
+ "notice"
+ ],
+ "properties": {
+ "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
+ "publicPath": { "enum": ["/favicon.svg", "/mask-icon.svg"] },
+ "sourceRepository": { "const": "atrinik/website" },
+ "sourcePath": { "type": "string", "pattern": "^public/[a-z-]+\\.svg$" },
+ "sourceRevision": { "type": "string", "pattern": "^[0-9a-f]{40}$" },
+ "sourceSha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
+ "publishedSha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
+ "width": { "const": 64 },
+ "height": { "const": 64 },
+ "author": { "type": "string", "minLength": 1, "maxLength": 200 },
+ "license": { "const": "MIT" },
+ "transformations": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 10,
+ "items": { "type": "string", "minLength": 1, "maxLength": 300 }
+ },
+ "purpose": { "type": "string", "minLength": 1, "maxLength": 300 },
+ "notice": { "type": "string", "minLength": 1, "maxLength": 500 }
+ }
+}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 11fc770..0df485c 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -1,12 +1,25 @@
# Static website architecture
Astro compiles typed local data and `.astro` templates to static HTML and CSS.
-No browser JavaScript or server runtime is emitted by the build. `src/data` is
-the schema-validated structured metadata and catalog input, while `.astro`
-templates contain authored page prose. Closed validators reject unknown
-download/media fields and unsafe coordinates before rendering. Cloudflare can
-transform a deployed response after this build boundary; provider-injected
-security or performance code is not part of `dist/` and is audited separately.
+No browser JavaScript or server runtime is emitted by the build. The only
+permitted `script` element is inert `type="application/ld+json"` data generated
+locally with `<`, `>`, `&`, and JavaScript line separators escaped before raw
+HTML insertion. `src/data` is the schema-validated catalog input, the typed
+metadata factory owns page identity, and `.astro` templates contain authored
+page prose. Closed validators reject unknown download/media/icon fields and
+unsafe coordinates before rendering. Cloudflare can transform a deployed
+response after this build boundary; provider-injected security or performance
+code is not part of `dist/` and is audited separately.
+
+Every indexable page supplies an explicit metadata object with a unique title,
+description, canonical route, index policy, and matched Open Graph/Twitter
+identity. Social images resolve only through `src/data/media.json`, including
+their canonical local URL, dimensions, and alternative text. The temporary
+`atrinik-now` concept image is the explicit sitewide fallback pending issue #22;
+pages may choose a more relevant proven catalog record. The homepage alone owns
+the canonical `WebSite` JSON-LD record for `https://atrinik.org/` and its two
+verified Atrinik GitHub identities. The 404 emits no canonical, preview, or
+structured identity and retains `noindex, nofollow`.
Downloads remain in their owning GitHub releases. Catalog schema version 2
separates the release repository from the artifact's logical role and marks at
@@ -32,6 +45,13 @@ exact license, transformations, alt text, and notice. Same-repository sources
are digest-checked from a traversal-safe path. The website never imports an
asset tree by implication.
+The two SVG site icons use a separate closed catalog because they are
+repository-native vector interface artwork rather than page media. Each record
+binds the exact checked-in source/published bytes to a Git blob object ID and
+SHA-256, 64×64 view box, author, MIT license, transformation, purpose, and
+notice. Source validation rejects SVG scripts, event attributes, and external
+references; built pages must link both canonical local files.
+
The executable validator checks the JSON Schema's locally expressible field
sets, patterns, formats, bounds, constants, primary-artifact restrictions, and
archive suffix rules on every run, without adding a general-purpose runtime
@@ -43,7 +63,10 @@ the reviewed Classic release. Tests exercise valid and adversarial forms and
contract changes must update both representations and their fixtures together.
`public/_headers` supplies a no-script CSP and browser hardening for every
-static response. The built-output validator additionally enforces at most 16
+static response. Inert JSON-LD does not relax `script-src 'none'`. The
+built-output validator rejects every other script element or script attribute,
+parses the JSON-LD, compares it with visible metadata and canonical identity,
+and additionally enforces at most 16
generated files, 900,000 bytes total, 140,000 aggregate HTML bytes, 40,000
aggregate CSS bytes, 700,000 aggregate raster image bytes, and zero JavaScript.
Published image filenames are content-addressed for immutable caching, and
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..b8ce07b
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1,8 @@
+
diff --git a/public/mask-icon.svg b/public/mask-icon.svg
new file mode 100644
index 0000000..96bc7f2
--- /dev/null
+++ b/public/mask-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/src/data/icons.json b/src/data/icons.json
new file mode 100644
index 0000000..a47af6d
--- /dev/null
+++ b/src/data/icons.json
@@ -0,0 +1,39 @@
+{
+ "schemaVersion": 1,
+ "entries": [
+ {
+ "id": "atrinik-favicon",
+ "publicPath": "/favicon.svg",
+ "sourceRepository": "atrinik/website",
+ "sourcePath": "public/favicon.svg",
+ "sourceRevision": "b8ce07b4d6eb9d134268b1f13fba95290f6bb5c2",
+ "sourceSha256": "8b5a901b27d14d2c57bcd844febea646e026cb7ccb34a611837212a2da18ef45",
+ "publishedSha256": "8b5a901b27d14d2c57bcd844febea646e026cb7ccb34a611837212a2da18ef45",
+ "width": 64,
+ "height": 64,
+ "author": "Zoey Rose with Codex implementation assistance",
+ "license": "MIT",
+ "transformations": ["Authored directly as a compact SVG favicon."],
+ "purpose": "Browser favicon depicting the Atrinik crystal mark.",
+ "notice": "New repository-native vector artwork created for atrinik/website issue #27."
+ },
+ {
+ "id": "atrinik-mask-icon",
+ "publicPath": "/mask-icon.svg",
+ "sourceRepository": "atrinik/website",
+ "sourcePath": "public/mask-icon.svg",
+ "sourceRevision": "96bc7f293ea2a809ac45cdb0094cb82862cb2dde",
+ "sourceSha256": "84a60770868f27e7d1ac169d90c62f9293bd57e2b238117a699673f2e92535a2",
+ "publishedSha256": "84a60770868f27e7d1ac169d90c62f9293bd57e2b238117a699673f2e92535a2",
+ "width": 64,
+ "height": 64,
+ "author": "Zoey Rose with Codex implementation assistance",
+ "license": "MIT",
+ "transformations": [
+ "Simplified from the new favicon geometry into a monochrome SVG mask."
+ ],
+ "purpose": "Pinned-tab mask icon depicting the Atrinik crystal mark.",
+ "notice": "New repository-native vector artwork created for atrinik/website issue #27."
+ }
+ ]
+}
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
index 18bf9c3..dc91e4e 100644
--- a/src/layouts/Base.astro
+++ b/src/layouts/Base.astro
@@ -1,29 +1,16 @@
---
import site from "../data/site.json";
-import media from "../data/media.json";
import "../styles/global.css";
+import type { PageMetadata } from "../lib/metadata";
+import { serializeJsonLd } from "../lib/metadata";
interface Props {
- title?: string;
- description?: string;
- canonicalPath?: string;
+ metadata: PageMetadata;
preloadImage?: string;
- noindex?: boolean;
}
-const title = Astro.props.title
- ? `${Astro.props.title} · ${site.title}`
- : site.title;
-const description = Astro.props.description ?? site.description;
-const canonical = new URL(
- Astro.props.canonicalPath ?? Astro.url.pathname,
- site.canonicalOrigin,
-);
+const { metadata } = Astro.props;
const currentPath = Astro.url.pathname;
-const socialImage = media.entries.find((entry) => entry.id === "atrinik-now");
-if (!socialImage) throw new Error("missing social media record: atrinik-now");
-const socialImageUrl = new URL(socialImage.publicPath, site.canonicalOrigin);
-const socialImageAlt = `Temporary OpenAI-generated website concept artwork: ${socialImage.alt}`;
const navigation = [
{ label: "Home", href: "/" },
{ label: "About", href: "/about/" },
@@ -39,29 +26,67 @@ const isCurrent = (href: string) =>
diff --git a/src/pages/about.astro b/src/pages/about.astro
index 026bc1b..107d643 100644
--- a/src/pages/about.astro
+++ b/src/pages/about.astro
@@ -1,11 +1,17 @@
---
import Base from "../layouts/Base.astro";
+import { indexablePage } from "../lib/metadata";
+
+const metadata = indexablePage({
+ title: "About Atrinik: Human imagination and open development",
+ description:
+ "Atrinik began in 2009 as a human-developed open-source world. Learn how human creative direction, agentic software work, and direct contributions shape it today.",
+ canonicalPath: "/about/",
+ socialImageId: "atrinik-then",
+});
---
-
+
About Atrinik
Human imagination. Agentic engineering.
diff --git a/src/pages/downloads.astro b/src/pages/downloads.astro
index fea7f20..cb322d7 100644
--- a/src/pages/downloads.astro
+++ b/src/pages/downloads.astro
@@ -4,14 +4,18 @@ import DownloadCatalog from "../components/DownloadCatalog.astro";
import DownloadPrimaryAction from "../components/DownloadPrimaryAction.astro";
import downloads from "../data/downloads.json";
import type { DownloadCatalog as DownloadCatalogType } from "../lib/downloads";
+import { indexablePage } from "../lib/metadata";
const catalog: DownloadCatalogType = downloads;
+const metadata = indexablePage({
+ title: "Atrinik Classic downloads and release status",
+ description:
+ "Find the maintained Atrinik Classic release path, verify available packages, and understand why the replacement foundations are not yet a complete game download.",
+ canonicalPath: "/downloads/",
+});
---
-
+