diff --git a/.gitignore b/.gitignore
index 94fd26027..0ccb0b53f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,9 +7,18 @@ dist/
.storybook-static/
coverage/
lib/
+# `lib/` above targets compiled build output, but is unanchored, so it also
+# swallowed the Astro site's hand-written src/lib/ — leaving that source
+# untracked and the package unbuildable from a fresh clone.
+!packages/pharos-site-astro/src/lib/
packages/**/package-lock.json
packages/pharos-site/public/
packages/pharos-site/.cache/
+packages/pharos-site-astro/.astro/
+# Brand-asset archives carried over from the Gatsby site's static/ directory.
+# Nothing in the source or the built HTML links to them, so they are kept out of
+# git history here rather than duplicating the ~7.4MB the Gatsby package tracks.
+packages/pharos-site-astro/public/files/**/*.zip
react-components/
packages/pharos/src/styles/**/*.ts
packages/pharos/src/styles/_variables.scss
diff --git a/.prettierignore b/.prettierignore
index 3d9fe87a4..bd30b828a 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -9,6 +9,19 @@ packages/pharos/custom-elements.json
packages/**/package-lock.json
packages/pharos-site/public/
packages/pharos-site/.cache/
+packages/pharos-site-astro/.astro/
+# Prettier is not safe on any MDX in this package. It rewrites MDX comment
+# delimiters ({/* */} -> {/_ _/}), which produces invalid JSX and fails the
+# build outright, and it reflows elements such as
/
/ onto several
+# lines. MDX treats a tag whose content spans multiple lines as Markdown and
+# wraps it in a generated
, so a purely cosmetic reflow silently adds
+# paragraph margins to the rendered page.
+#
+# This must match src/** and not just src/pages/**: the component pages moved
+# to src/content/ in the collection refactor, and a src/pages-only glob left
+# all 32 of them unprotected. {/* prettier-ignore */} is not an alternative --
+# prettier corrupts the marker's own delimiters before it can take effect.
+packages/pharos-site-astro/src/**/*.mdx
package.json
package-lock.json
.changeset/
diff --git a/.prettierrc.js b/.prettierrc.js
index a85491c96..ada48fddc 100644
--- a/.prettierrc.js
+++ b/.prettierrc.js
@@ -1,4 +1,7 @@
module.exports = {
+ // `.astro` has no built-in parser; without this prettier reports
+ // "No parser could be inferred" and silently formats none of them.
+ plugins: ['prettier-plugin-astro'],
printWidth: 100,
trailingComma: 'es5',
tabWidth: 2,
diff --git a/build-ignore.sh b/build-ignore.sh
index 68caa1308..160c99b3c 100644
--- a/build-ignore.sh
+++ b/build-ignore.sh
@@ -12,5 +12,5 @@ then
git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF packages/pharos/ .storybook/ package.json yarn.lock netlify.toml
elif [ "$SITE_NAME" == "pharos" ]
then
- git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF packages/pharos-site/ package.json yarn.lock netlify.toml
+ git diff --quiet $CACHED_COMMIT_REF $COMMIT_REF packages/pharos/ packages/pharos-site-astro/ package.json yarn.lock netlify.toml
fi
diff --git a/eslint.config.mjs b/eslint.config.mjs
index b837e2154..1634a428f 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -1,4 +1,5 @@
import js from '@eslint/js';
+import astroPlugin from 'eslint-plugin-astro';
import reactPlugin from 'eslint-plugin-react';
import prettierPlugin from 'eslint-plugin-prettier';
import storybookPlugin from 'eslint-plugin-storybook';
@@ -12,6 +13,7 @@ const globalIgnores = [
'packages/pharos/src/styles/**/*.ts',
'packages/pharos-site/public/',
'packages/pharos-site/.cache/',
+ 'packages/pharos-site-astro/.astro/',
'**/dist/',
'**/node_modules/',
'**/*.css.ts',
@@ -84,10 +86,30 @@ const tsxConfig = {
},
};
+const astroConfig = [
+ ...astroPlugin.configs.recommended,
+ ...astroPlugin.configs['jsx-a11y-recommended'],
+ {
+ files: ['**/*.astro'],
+ plugins: {
+ '@typescript-eslint': typeScriptEsLint,
+ },
+ rules: {
+ // TypeScript resolves identifiers itself, and `no-undef` cannot see
+ // type-only names, so it only fires falsely in frontmatter.
+ 'no-undef': 'off',
+ 'no-unused-vars': 'off',
+ '@typescript-eslint/no-unused-vars': ['error'],
+ '@typescript-eslint/no-explicit-any': ['error'],
+ },
+ },
+];
+
export default [
{ ignores: globalIgnores },
js.configs.recommended,
pharosConfig,
tsConfig,
tsxConfig,
+ ...astroConfig,
];
diff --git a/lint-staged.config.js b/lint-staged.config.js
index 3cbeb953a..fd6d53142 100644
--- a/lint-staged.config.js
+++ b/lint-staged.config.js
@@ -1,6 +1,7 @@
module.exports = {
'packages/pharos/assets/icons/*.svg': [`svglint --ci`],
'*.{ts,tsx,js,mjs}': ['eslint --fix'],
+ '*.astro': ['eslint --fix'],
'*.{scss,css}': ['stylelint --fix'],
'**/!(.changeset)/*.md': (filenames) =>
filenames.map((filename) => `yarn markdown-toc -i '${filename}'`),
diff --git a/netlify.toml b/netlify.toml
index 7de10e9e5..4247568bd 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -1,3 +1,11 @@
+[build]
+ command = "yarn site-astro:build"
+ publish = "packages/pharos-site-astro/dist"
+ ignore = "bash ./build-ignore.sh"
+
+[build.environment]
+ NODE_VERSION = "24"
+
[[redirects]]
from = "/storybook"
to = "https://pharos-storybooks.netlify.app/main/"
@@ -39,6 +47,3 @@
[headers.values]
Access-Control-Allow-Origin = "*"
X-Frame-Options = "SAMEORIGIN"
-
-[build]
- ignore = "bash ./build-ignore.sh"
diff --git a/package.json b/package.json
index e6086a214..fd93824f8 100644
--- a/package.json
+++ b/package.json
@@ -7,8 +7,9 @@
"build:tokens": "yarn workspace @ithaka/pharos build:tokens",
"analyze": "yarn workspace @ithaka/pharos analyze",
"format": "prettier packages/* --write",
- "lint": "yarn lint:lit-analyzer && yarn lint:eslint && yarn lint:styles && yarn lint:icons",
- "lint:eslint": "eslint 'packages/**/*.{ts,tsx,js,jsx,mjs}'",
+ "lint": "yarn lint:lit-analyzer && yarn lint:eslint && yarn lint:styles && yarn lint:icons && yarn lint:astro",
+ "lint:astro": "yarn workspace @ithaka/pharos-site-astro check",
+ "lint:eslint": "eslint 'packages/**/*.{ts,tsx,js,jsx,mjs,astro}'",
"lint:lit-analyzer": "lit-analyzer 'packages/*/src/**/!(*.css|*.test).ts' --strict --rules.no-missing-import off --rules.no-unknown-tag-name off",
"lint:styles": "stylelint 'packages/**/*.{scss,css}'",
"lint:icons": "svglint --ci packages/pharos/assets/icons/*.svg",
@@ -42,6 +43,13 @@
"site:develop": "yarn presite:develop && yarn workspace @ithaka/pharos-site develop",
"site:serve": "yarn workspace @ithaka/pharos-site serve",
"site:clean": "yarn workspace @ithaka/pharos-site clean",
+ "presite-astro:build": "yarn build:core",
+ "site-astro:build": "yarn presite-astro:build && yarn workspace @ithaka/pharos-site-astro build",
+ "presite-astro:develop": "yarn build:core",
+ "site-astro:develop": "yarn presite-astro:develop && yarn workspace @ithaka/pharos-site-astro develop",
+ "site-astro:serve": "yarn workspace @ithaka/pharos-site-astro preview",
+ "site-astro:check": "yarn workspace @ithaka/pharos-site-astro check",
+ "site-astro:clean": "yarn workspace @ithaka/pharos-site-astro clean",
"release": "changeset publish",
"prepare": "husky install"
},
@@ -80,10 +88,12 @@
"@typescript-eslint/parser": "^8.67.0",
"@vitejs/plugin-react": "^6.1.0",
"all-contributors-cli": "^6.26.1",
+ "astro-eslint-parser": "^3.1.0",
"concurrently": "^10.0.5",
"cssnano": "^8.0.7",
"eslint": "^10.9.0",
"eslint-config-prettier": "^10.1.8",
+ "eslint-plugin-astro": "^3.1.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-lit": "^2.3.1",
"eslint-plugin-prettier": "^5.5.6",
@@ -102,6 +112,7 @@
"postcss-media-minmax": "^5.0.0",
"postcss-preset-env": "^11.4.0",
"prettier": "^3.9.6",
+ "prettier-plugin-astro": "0.14.1",
"pretty-quick": "^4.2.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
@@ -113,6 +124,7 @@
"stylelint-scss": "^7.2.0",
"svglint": "^4.2.1",
"typescript": "^6.0.3",
+ "typescript-eslint": "^8.65.0",
"vite": "^8.2.2"
},
"resolutions": {
diff --git a/packages/pharos-site-astro/MODERNIZATION.md b/packages/pharos-site-astro/MODERNIZATION.md
new file mode 100644
index 000000000..df949df87
--- /dev/null
+++ b/packages/pharos-site-astro/MODERNIZATION.md
@@ -0,0 +1,935 @@
+# Astro site modernization backlog
+
+Work plan for making `packages/pharos-site-astro` follow Astro best practices.
+
+**Read this first if you are an agent picking this up cold.** It assumes no prior
+context. Read `README.md` in this package before touching anything — it documents
+several non-obvious constraints (minification-sensitive custom element
+registration, whitespace handling around inline elements, MDX paragraph
+wrapping) that will bite you otherwise.
+
+## Background
+
+This package is an Astro port of the Gatsby documentation site in
+`packages/pharos-site`, added as a candidate replacement for Gatsby. The two
+coexist so they can be compared side by side.
+
+The initial commit (`20b7ce2`) was a deliberate 1:1 reproduction of the Gatsby
+site's rendered output. It was validated against the **live production site** by
+diffing rendered text and full-page screenshots across all 62 routes. Parity was
+the acceptance test, so the port prioritized matching output over idiomatic
+Astro. Consequences that look like mistakes but are intentional:
+
+- Content was hand-written `.astro`, not Markdown. Item 5 below converted it:
+ 43 pages are MDX today, and the 20 that remain `.astro` are listed there with
+ the reason each stays.
+- Typos, odd spacing, and malformed inline styles from the Gatsby source are
+ reproduced verbatim.
+- 254 inline `style` attributes, because that is what the Gatsby components
+ emitted.
+
+This backlog undoes that where it no longer earns its keep.
+
+## The gating question
+
+**Is byte-for-byte parity with the Gatsby site still the acceptance test?**
+
+**Answered: no.** Pixel parity is retired. The standard is now "as close as
+reasonable" — match the design intent, and treat a small rendered difference as
+acceptable when it buys a real maintainability gain.
+
+Tier 2 and 3 are therefore unblocked. This does not license carelessness:
+measure before and after, keep the copy identical unless a change is a
+deliberate fix, and note anything that moves in the item's write-up.
+
+## Verifying your work
+
+```bash
+yarn site-astro:check # type-check .astro files
+yarn site-astro:build # full static build
+yarn site-astro:develop # dev server
+```
+
+Use `yarn`, never `npm`. Run these from the repo root.
+
+For Tier 1 work, the build output should be byte-identical before and after. A
+cheap way to confirm:
+
+```bash
+yarn site-astro:build && cp -r packages/pharos-site-astro/dist /tmp/dist-before
+# ...make changes...
+yarn site-astro:build && diff -r /tmp/dist-before packages/pharos-site-astro/dist
+```
+
+---
+
+## Tier 1 — parity-safe ✅ DONE
+
+All four items are complete. The build output was verified byte-identical to
+the pre-change `dist/` (`diff -r`, all 63 pages), and the 11 design-token pages
+were verified structurally identical to production — matching row counts, cell
+counts, and pixel width/height for every table.
+
+### 1. Delete the empty `src/content/` directory ✅
+
+Removed.
+
+### 2. Fix the positional coupling in `line-height.astro` ✅
+
+`fontSizeMap` is now keyed by token name rather than paired by array index, and
+throws on an unmapped token instead of silently rendering `undefined`. Adding,
+removing or reordering a line-height token can no longer shift every row onto
+the wrong font size.
+
+### 3. Consolidate the design-token tables ✅
+
+Added `src/components/TokenRows.astro`, which renders the shared
+`Token / Value / Example` `` and the row loop from a `rows` array. All
+11 pages use it. The example cell — the one genuine variation — is passed per
+row as an HTML string and emitted with `set:html`.
+
+Pages went from 624 to 453 lines (520 including the new component). Column
+widths are a prop because the pages do not agree on them (40/30, 33/33/36,
+40/40, and type-scale's four-column 25/20/25).
+
+The duplicated `comment?: string` interfaces were hoisted into
+`src/lib/tokenFormat.ts` as `CommentedToken`, plus `ScaleToken` for the two
+pages where `comment` is required.
+
+### 4. Add a nav-to-page assertion ✅
+
+`src/lib/assertNavigation.ts` fails the build in both directions: a
+`navigation.ts` entry with no matching page, and a page absent from
+`navigation.ts`. Called once from `Sidenav.astro`. Explicit ordering is
+preserved — the check only compares sets, it never derives order.
+
+Both directions were verified by deliberately introducing each failure.
+
+**Do not reach for `import.meta.glob` here.** The first attempt used it, and
+because the sidenav renders on every page, it made every page a style
+dependency of every other — each page's `
diff --git a/packages/pharos-site-astro/src/components/ColorCombos.astro b/packages/pharos-site-astro/src/components/ColorCombos.astro
new file mode 100644
index 000000000..bd4e3db8d
--- /dev/null
+++ b/packages/pharos-site-astro/src/components/ColorCombos.astro
@@ -0,0 +1,71 @@
+---
+/** Token lookups happen at build time, so no client JavaScript ships. */
+import tokens from '@ithaka/pharos/lib/styles/tokens';
+import CrossOut from '@components/CrossOut.astro';
+import { toTitleCase, toSlug } from '@lib/textConvert';
+
+interface Props {
+ colorNames: string[];
+ crossout?: boolean;
+}
+
+const { colorNames, crossout = false } = Astro.props;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const colors: Record = tokens.color;
+
+let width = '62px';
+switch (colorNames.length) {
+ case 3:
+ width = '54px';
+ break;
+ default:
+ break;
+}
+
+const getStyle = (name: string) => {
+ const normalizedName = toSlug(name);
+ let returnValue = colors[normalizedName].value;
+ if (!returnValue) {
+ returnValue = colors[normalizedName].base.value;
+ }
+
+ if (!returnValue) {
+ console.error(`Couldn't locate value for token color '${name}'`);
+ return '';
+ }
+
+ let returnStyle = `background-color: ${returnValue};`;
+ if (name === 'White') returnStyle += ' box-shadow: inset 0 0 0 1px #EAE8E1;';
+ return returnStyle;
+};
+
+const swatchStyle = (name: string) => `height: 100%; width: ${width}; ${getStyle(name)}`;
+
+let textDisplay = '';
+colorNames.forEach(
+ (name, i) => (textDisplay += `${name} ${i !== colorNames.length - 1 ? ' + ' : ''}`)
+);
+textDisplay = toTitleCase(textDisplay);
+---
+
+
diff --git a/packages/pharos-site-astro/src/components/CrossOut.astro b/packages/pharos-site-astro/src/components/CrossOut.astro
new file mode 100644
index 000000000..e4ac3f4a9
--- /dev/null
+++ b/packages/pharos-site-astro/src/components/CrossOut.astro
@@ -0,0 +1,45 @@
+---
+/**
+ * The diagonal line is sized from the rendered content, so the measurement runs
+ * in a small client-side script.
+ */
+---
+
+