diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml
index 15e4b50..39818bc 100644
--- a/.github/actions/test/action.yml
+++ b/.github/actions/test/action.yml
@@ -3,17 +3,33 @@ description: >
Is this commit good. Nothing is published here; a failure means the site is
wrong, not that the pipeline is.
- Runs AFTER ../build, because everything it asserts is a property of ./dist. A
- static site has two ways to be broken that matter -- it did not build, and it
- built the wrong thing -- and only the second one needs a test.
+ Runs AFTER ../build. Everything it asserts about the site is a property of
+ ./dist, except the lint-and-format gate -- a source assertion that goes
+ first, because it needs nothing from ./dist and is cheaper than the ones
+ that do. A static site has two ways to be broken that matter -- it did not
+ build, and it built the wrong thing -- and only the second one needs a dist
+ assertion.
- The assertions are this site's own and differ from its siblings': how many
- pages it emits, and what else has to be on them. That is the part worth
- keeping per-repo.
+ The first step is the gate shared by every adopter; the rest are this
+ site's own and differ from its siblings': how many pages it emits, and what
+ else has to be on them. That is the part worth keeping per-repo.
runs:
using: composite
steps:
+ # Lint and format, against the biome.json copied verbatim from the
+ # archetype: one canonical configuration for every web repository, so
+ # style is a fleet property rather than a per-repo debate. The engine is
+ # @biomejs/biome, pinned as an exact devDependency in package.json and
+ # installed by ../build's frozen install, which runs before this action
+ # in the Test job -- `bun x` resolves that local binary, so the gate
+ # always runs the pinned version, never a resolved latest. A source
+ # assertion, so it goes first: it needs nothing from ./dist and is
+ # cheaper than the assertions that do.
+ - name: Lint and format are canonical
+ shell: bash
+ run: bun x biome check
+
# A build that emits nothing still "succeeds", so the artefacts are
# checked rather than the exit code. All three pages, because a broken
# route is invisible from the index alone.
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..64d5654
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,55 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json",
+ "vcs": {
+ "enabled": true,
+ "clientKind": "git",
+ "useIgnoreFile": true
+ },
+ "files": {
+ "includes": ["**", "!dist", "!.astro"]
+ },
+ "formatter": {
+ "enabled": true,
+ "indentStyle": "tab",
+ "lineWidth": 120
+ },
+ "linter": {
+ "enabled": true,
+ "rules": {
+ "recommended": true
+ }
+ },
+ "javascript": {
+ "formatter": {
+ "quoteStyle": "single",
+ "semicolons": "always",
+ "trailingCommas": "all"
+ }
+ },
+ "json": {
+ "formatter": {
+ "indentStyle": "space",
+ "indentWidth": 2
+ }
+ },
+ "assist": {
+ "enabled": true,
+ "actions": {
+ "source": {
+ "organizeImports": "on"
+ }
+ }
+ },
+ "overrides": [
+ {
+ "includes": ["**/*.astro"],
+ "linter": {
+ "rules": {
+ "correctness": {
+ "noUnusedVariables": "off"
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/bun.lock b/bun.lock
index 5eb4b99..e2e1828 100644
--- a/bun.lock
+++ b/bun.lock
@@ -10,6 +10,7 @@
},
"devDependencies": {
"@astrojs/check": "^0.9.10",
+ "@biomejs/biome": "2.5.14",
"@types/primer__octicons": "^19.21.0",
"typescript": "6",
},
@@ -64,6 +65,24 @@
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
+ "@biomejs/biome": ["@biomejs/biome@2.5.14", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.14", "@biomejs/cli-darwin-x64": "2.5.14", "@biomejs/cli-linux-arm64": "2.5.14", "@biomejs/cli-linux-arm64-musl": "2.5.14", "@biomejs/cli-linux-x64": "2.5.14", "@biomejs/cli-linux-x64-musl": "2.5.14", "@biomejs/cli-win32-arm64": "2.5.14", "@biomejs/cli-win32-x64": "2.5.14" }, "bin": { "biome": "bin/biome" } }, "sha512-0FabLIjd4M/dm8VFI86RMaLLdgepzbdfiAL2R8cr7V81OYYrP1w7Z73KfAwPK5h9SrEBXTzNG2y+mBwPo9xRnw=="],
+
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UnzaXO65L4tsZimFITFP2M121GyhDcWFrT3pL5ZJ5U4XcS/0L5VHYytVohdCf/gnDUFHgl2I9xnt5bV/J1kxHQ=="],
+
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-kiy8qA16K93J7uvFfWi4LgjqDpnRKyePAna6A0Y4jxyUga35SYpIZ2cNWlhy0lsfgqRenCpfymnJWEZ6mgpRgA=="],
+
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-vO/9BaU1n30CiFNLx49gMTMtbCAAqlo/EqFoG0BU3deQInTbJrmXSmTQ9e3FoDZIKQnvSLDVNL4lx1ci7y1T1Q=="],
+
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-SJ9PrZkBnnH9dHJDnxk34vKs0GB2dbsioaft6/hPhWJ7AHpwYH83Isnhj0FSRpFkGgpVfJ1lds23ApB2czUDLQ=="],
+
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-VHZRa7CCQBUWKxNwUvHJeuW03WFRgN5NWO//SDGOitc9QIeNyfA42V9zlKm+x82xFSG9Bzi5fi+hbhm9anO8yA=="],
+
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-2kI5PrMgW5dcEZYrstLPmUmCwkUwZY39rP4BN93Vxbwcs2O57LDQOktUZZYPvvspN5i0mhGr3TJtF5sU26NUWg=="],
+
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-pHgAFffmZtaYoxavEsWEYNvcA7NwOIjLyqw2HXyLbt16xYryX0L4fMV2u0G9ag6LNYPIoqWaWqzUcnc6rLGGXg=="],
+
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-oJWmBhoHsnhUKIke+0gXDX0mltJrWHA1UyHsrTlXwX0TL64ilVZAo+TYZm97baecV0esBdpzy3k96q+09JaZUQ=="],
+
"@bruits/satteri-darwin-arm64": ["@bruits/satteri-darwin-arm64@0.10.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA=="],
"@bruits/satteri-darwin-x64": ["@bruits/satteri-darwin-x64@0.10.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q=="],
diff --git a/package.json b/package.json
index b8197cb..a663dba 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
},
"devDependencies": {
"@astrojs/check": "^0.9.10",
+ "@biomejs/biome": "2.5.14",
"@types/primer__octicons": "^19.21.0",
"typescript": "6"
}
diff --git a/src/layouts/Full.astro b/src/layouts/Full.astro
index 56d1db5..5f09b4e 100644
--- a/src/layouts/Full.astro
+++ b/src/layouts/Full.astro
@@ -15,8 +15,7 @@ const {
} = Astro.props;
const canonical = new URL(Astro.url.pathname, 'https://avior.studio');
-const icon = (name: keyof typeof octicons, size = 16) =>
- octicons[name].toSVG({ width: size, height: size });
+const icon = (name: keyof typeof octicons, size = 16) => octicons[name].toSVG({ width: size, height: size });
const nav = [
{ href: '/devtools', label: 'Devtools' },
diff --git a/src/pages/devtools.astro b/src/pages/devtools.astro
index 0208a89..768eefb 100644
--- a/src/pages/devtools.astro
+++ b/src/pages/devtools.astro
@@ -1,10 +1,9 @@
---
-import Full from '../layouts/Full.astro';
-import Entry from '../components/Entry.astro';
import octicons from '@primer/octicons';
+import Entry from '../components/Entry.astro';
+import Full from '../layouts/Full.astro';
-const icon = (name: keyof typeof octicons, size = 16) =>
- octicons[name].toSVG({ width: size, height: size });
+const icon = (name: keyof typeof octicons, size = 16) => octicons[name].toSVG({ width: size, height: size });
// The addons GDAM was built to carry. Listed alphabetically rather than by
// importance: fourteen is enough that a reader needs to be able to find one,
@@ -54,7 +53,7 @@ const addons = [
{
name: 'gd-pocketbase',
tagline: 'PocketBase auth and session primitives',
- copy: 'Normalizes either a native {token, record} response or a backend proxy\'s {access_token, user} into one shape, then handles JWT expiry, persisted sessions and stable client IDs. Games keep HTTP transport and refresh policy; no superuser credential ever needs to reach the client.',
+ copy: "Normalizes either a native {token, record} response or a backend proxy's {access_token, user} into one shape, then handles JWT expiry, persisted sessions and stable client IDs. Games keep HTTP transport and refresh policy; no superuser credential ever needs to reach the client.",
},
{
name: 'gd-redis',
diff --git a/src/pages/games.astro b/src/pages/games.astro
index e59529e..bce27da 100644
--- a/src/pages/games.astro
+++ b/src/pages/games.astro
@@ -1,10 +1,9 @@
---
-import Full from '../layouts/Full.astro';
-import Entry from '../components/Entry.astro';
import octicons from '@primer/octicons';
+import Entry from '../components/Entry.astro';
+import Full from '../layouts/Full.astro';
-const icon = (name: keyof typeof octicons, size = 16) =>
- octicons[name].toSVG({ width: size, height: size });
+const icon = (name: keyof typeof octicons, size = 16) => octicons[name].toSVG({ width: size, height: size });
---
- octicons[name].toSVG({ width: size, height: size });
+const icon = (name: keyof typeof octicons, size = 16) => octicons[name].toSVG({ width: size, height: size });
// The two doors, and what is actually behind each one. Naming the contents
// here rather than saying "and more" means the index never promises a page
diff --git a/src/styles/global.css b/src/styles/global.css
index 0d27ea3..5277396 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -23,8 +23,8 @@
--bone: #f7f2e9;
--quiet: #a29d97;
--faint: #7c7772;
- --sans: system-ui, -apple-system, 'Segoe UI', Roboto, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif;
- --mono: ui-monospace, 'SF Mono', 'Cascadia Code', 'JetBrains Mono', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
+ --sans: system-ui, -apple-system, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif;
+ --mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", Menlo, Consolas, "DejaVu Sans Mono", monospace;
font-family: var(--sans);
background: var(--ink);
color: var(--paper);
@@ -32,10 +32,41 @@
color-scheme: dark;
}
-* { box-sizing: border-box; }
-html { scroll-behavior: smooth; background: var(--ink); }
-body { min-width: 320px; min-height: 100svh; margin: 0; background: var(--ink); text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; }
-button, input, textarea, select { font: inherit; }
-img { max-width: 100%; height: auto; }
-::selection { background: var(--bone); color: var(--ink); }
-@media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; transition-duration: .01ms !important; } }
+* {
+ box-sizing: border-box;
+}
+html {
+ scroll-behavior: smooth;
+ background: var(--ink);
+}
+body {
+ min-width: 320px;
+ min-height: 100svh;
+ margin: 0;
+ background: var(--ink);
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+}
+button,
+input,
+textarea,
+select {
+ font: inherit;
+}
+img {
+ max-width: 100%;
+ height: auto;
+}
+::selection {
+ background: var(--bone);
+ color: var(--ink);
+}
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ scroll-behavior: auto !important;
+ animation-duration: 0.01ms !important;
+ transition-duration: 0.01ms !important;
+ }
+}