Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 18 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
44 changes: 44 additions & 0 deletions contracts/icon.schema.json
Original file line number Diff line number Diff line change
@@ -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 }
}
}
37 changes: 30 additions & 7 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions public/mask-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
39 changes: 39 additions & 0 deletions src/data/icons.json
Original file line number Diff line number Diff line change
@@ -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."
}
]
}
101 changes: 63 additions & 38 deletions src/layouts/Base.astro
Original file line number Diff line number Diff line change
@@ -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/" },
Expand All @@ -39,29 +26,67 @@ const isCurrent = (href: string) =>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="description" content={description} />
<meta name="description" content={metadata.description} />
<meta name="robots" content={metadata.robots} />
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#07111d" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" sizes="any" />
<link rel="mask-icon" href="/mask-icon.svg" color="#77e6ff" />
{
Astro.props.noindex ? (
<meta name="robots" content="noindex, nofollow" />
metadata.canonicalUrl ? (
<link rel="canonical" href={metadata.canonicalUrl} />
) : null
}
<meta name="color-scheme" content="dark" />
<meta name="theme-color" content="#07111d" />
<link rel="canonical" href={canonical} />
<meta property="og:type" content="website" />
<meta property="og:site_name" content={site.title} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonical} />
<meta property="og:image" content={socialImageUrl} />
<meta property="og:image:alt" content={socialImageAlt} />
<meta property="og:image:width" content={String(socialImage.width)} />
<meta property="og:image:height" content={String(socialImage.height)} />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={socialImageUrl} />
<meta name="twitter:image:alt" content={socialImageAlt} />
{
metadata.openGraph ? (
<>
<meta property="og:type" content={metadata.openGraph.type} />
<meta property="og:site_name" content={site.title} />
<meta property="og:title" content={metadata.openGraph.title} />
<meta
property="og:description"
content={metadata.openGraph.description}
/>
<meta property="og:url" content={metadata.openGraph.url} />
<meta property="og:image" content={metadata.openGraph.image.url} />
<meta
property="og:image:alt"
content={metadata.openGraph.image.alt}
/>
<meta
property="og:image:width"
content={String(metadata.openGraph.image.width)}
/>
<meta
property="og:image:height"
content={String(metadata.openGraph.image.height)}
/>
</>
) : null
}
{
metadata.twitter ? (
<>
<meta name="twitter:card" content={metadata.twitter.card} />
<meta name="twitter:title" content={metadata.twitter.title} />
<meta
name="twitter:description"
content={metadata.twitter.description}
/>
<meta name="twitter:image" content={metadata.twitter.image.url} />
<meta name="twitter:image:alt" content={metadata.twitter.image.alt} />
</>
) : null
}
{
metadata.structuredData?.map((record) => (
<script
type="application/ld+json"
is:inline
set:html={serializeJsonLd(record)}
/>
))
}
{
Astro.props.preloadImage ? (
<link
Expand All @@ -72,7 +97,7 @@ const isCurrent = (href: string) =>
/>
) : null
}
<title>{title}</title>
<title>{metadata.title}</title>
</head>
<body>
<a class="skip-link" href="#content">Skip to content</a>
Expand Down
Loading