diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 51054bad..110899a5 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- uses: j178/prek-action@v1
@@ -27,7 +27,7 @@ jobs:
shell: bash
steps:
- name: Check out
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Set up the environment
uses: ./.github/actions/setup-python-env
@@ -46,21 +46,37 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- - name: Set up pnpm
- uses: pnpm/action-setup@v4
+ - name: Set up Node and pnpm
+ uses: pnpm/setup@v2
with:
version: 11.21.0
-
- - name: Set up Node
- uses: actions/setup-node@v6
- with:
- node-version-file: website/.node-version
- cache: pnpm
+ runtime: node@24.19.0
+ package-json-file: website/package.json
+ cache: true
cache-dependency-path: website/pnpm-lock.yaml
+ install: false
- - name: Check if documentation can be built
+ - name: Install website dependencies
+ run: pnpm --dir website install --frozen-lockfile
+
+ - name: Validate dependency health
run: |
- pnpm --dir website install --frozen-lockfile
- pnpm --dir website build
+ pnpm --dir website peers check
+ pnpm --dir website audit --audit-level=low
+
+ - name: Type-check the website
+ run: pnpm --dir website check
+
+ - name: Build the website
+ run: pnpm --dir website build
+
+ - name: Verify routes, search, feeds, and metadata
+ run: pnpm --dir website verify:build
+
+ - name: Validate the Cloudflare deployment
+ run: pnpm --dir website verify:deploy
+
+ - name: Smoke-test the Cloudflare preview
+ run: pnpm --dir website test:preview
diff --git a/website/AGENTS.md b/website/AGENTS.md
index ba80481b..88a38ac3 100644
--- a/website/AGENTS.md
+++ b/website/AGENTS.md
@@ -25,10 +25,10 @@ Site URL: `https://bub.build`
| Styling | **Tailwind CSS v4** via `@tailwindcss/vite` + `@astrojs/starlight-tailwind` |
| Component lib | shadcn/ui conventions (base-vega style) |
| Animations | `motion` (formerly Framer Motion) |
-| Code blocks | `astro-expressive-code` |
+| Code blocks | Astro's Shiki renderer |
| Fonts | Outfit Variable (sans), JetBrains Mono Variable (mono) |
| Icons | **@lucide/astro** — see Icon section below |
-| Type checking | TypeScript strict mode |
+| Type checking | TypeScript strict mode + `astro check` |
---
@@ -39,19 +39,20 @@ Site URL: `https://bub.build`
```bash
pnpm install # install deps
pnpm dev # dev server
+pnpm check # Astro + TypeScript diagnostics
pnpm build # production build
+pnpm verify:build # validate routes, search, feeds, SEO, and OG assets
pnpm preview # preview production build
+pnpm verify:deploy # dry-run the assets-only Cloudflare deployment
+pnpm test:preview # smoke-test the Cloudflare runtime
```
## Image Handling
-- Prefer the standard `Image` component from `astro:assets` for local raster assets used by pages and shared UI.
-- Do not replace local logo or illustration components with raw `
` just to work around Cloudflare adapter behavior.
-- The repository intentionally splits image handling in `astro.config.mjs`:
- - `BUB_ASTRO_IMAGE_MODE=dev`: Cloudflare image service, so `make docs` works under Node without `cloudflare:workers` import failures.
- - `BUB_ASTRO_IMAGE_MODE=build`: compile-time optimization with passthrough runtime, so `make docs-test`, `make docs-preview`, and production serve stable built asset URLs.
-- `Makefile` is the source of truth for that mode selection. `process.argv` command detection only exists as a fallback for direct `pnpm dev` / `pnpm build` usage inside `website/`.
-- If you change adapter image settings, re-verify all three paths: `make docs`, `make docs-test`, and `make docs-preview`.
+- Prefer Astro's `Image` component from `astro:assets` for local raster assets used by pages and shared UI.
+- All routes are prerendered, so Astro optimizes local images at build time with Sharp.
+- Keep the final output static unless a feature genuinely requires request-time state. Adding an SSR route also requires an adapter, a Worker deployment review, and new runtime smoke tests.
+- Re-verify `make docs`, `make docs-test`, and `make docs-preview` after changing image or output settings.
---
@@ -67,7 +68,7 @@ website/
├── src/
│ ├── content.config.ts # Content collections: docs (Starlight), i18n (Starlight), posts (blog)
│ ├── components/ # shared Astro components
-│ │ ├── ui/ # primitives (Icon, SectionHeading)
+│ │ ├── ui/ # primitives (Icon, PostCard, ThemeToggle, etc.)
│ │ ├── NavBar.astro
│ │ ├── Footer.astro
│ │ ├── Hero.astro
@@ -75,9 +76,7 @@ website/
│ │ ├── HookIntro.astro
│ │ ├── TapeModel.astro
│ │ ├── Testimonials.astro
-│ │ ├── Contributors.astro
-│ │ ├── PostCard.astro
-│ │ └── ThemeToggle.astro
+│ │ └── Contributors.astro
│ ├── content/
│ │ ├── docs/ # [Zone 1] Starlight content collection
│ │ │ ├── docs/ # EN docs — maps to /docs/… routes
@@ -95,25 +94,25 @@ website/
│ │ └── posts/ # [Zone 3] Blog posts
│ │ ├── en/ # English posts
│ │ └── zh-cn/ # Chinese posts
+│ ├── data/
+│ │ └── landing-page.ts # Single source of truth for landing-page copy (both locales)
│ ├── i18n/ # [Zone 2] Custom page i18n module (NOT for Starlight docs)
│ │ ├── ui.ts # Flat-key UI strings: nav, footer, 404, posts, site meta
│ │ ├── utils.ts # getLangFromUrl, useTranslations, getNavProps, etc.
-│ │ └── landing-page.ts # Single source of truth for all landing-page copy (both locales)
│ ├── layouts/
│ │ ├── BaseLayout.astro # Shared HTML shell (head, nav, footer, scripts)
│ │ ├── LandingLayout.astro
│ │ ├── PostLayout.astro
│ │ └── PostListLayout.astro
│ ├── pages/
-│ │ ├── 404.astro # 404 page (custom page, uses Zone 2 i18n)
-│ │ └── [...locale]/ # Dynamic locale routing — generates EN (root) + ZH-CN variants
+│ │ └── [...locale]/ # Static locale routing — prerenders EN (root) + ZH-CN variants
+│ │ ├── 404.astro # Localized Cloudflare 404 sources
│ │ ├── index.astro # Landing page (both locales via getStaticPaths)
│ │ └── posts/
│ │ ├── index.astro # Post list (both locales via getStaticPaths)
│ │ └── [slug].astro # Single post (both locales via getStaticPaths)
│ └── styles/
│ └── global.css # Tailwind v4 + Starlight bridge + CSS custom properties
-└── DESIGN.md # Visual design guide
```
---
@@ -129,7 +128,7 @@ The site has **three distinct i18n zones**. Each zone has its own translation me
| Zone | Pages | i18n mechanism | String source |
|------|-------|---------------|---------------|
| **1 — Starlight docs** | `/docs/…`, `/zh-cn/docs/…` (all under `src/content/docs/`) | Starlight built-in i18n | `src/content/i18n/{locale}.json` + sidebar `translations` in config |
-| **2 — Custom pages** | Landing (`/`, `/zh-cn/`), 404 | Project's own `src/i18n/` module | `src/i18n/ui.ts` + `src/i18n/landing-page.ts` |
+| **2 — Custom pages** | Landing (`/`, `/zh-cn/`), 404 | Project i18n + typed landing data | `src/i18n/ui.ts` + `src/data/landing-page.ts` |
| **3 — Blog** | `/posts/…`, `/zh-cn/posts/…` | Content collection + project `src/i18n/` | Post markdown in `src/content/posts/{locale}/`, UI strings in `ui.ts` |
### Zone 1 — Starlight docs i18n
@@ -195,7 +194,7 @@ export function getStaticPaths() {
// → generates /zh-cn/ (zh-cn)
```
-**Never duplicate a page file per locale** — use this pattern instead. The 404 page is the only exception (lives at `src/pages/404.astro` root because Astro requires it there).
+**Never duplicate a page file per locale** — use this pattern instead. The shared `[...locale]/404.astro` emits both 404 variants; the Chinese output is relocated to `dist/zh-cn/404.html` after the build so Cloudflare can serve the nearest localized 404.
| File | Purpose |
|--------------------|---------|
@@ -215,8 +214,8 @@ Blog posts are a **content collection** (`src/content/posts/{locale}/`) rendered
|-------|----------|
| Put Starlight UI overrides in `src/content/i18n/` | Duplicate Starlight keys into `src/i18n/ui.ts` |
| Put nav/footer/404/post-list strings in `src/i18n/ui.ts` | Put custom page strings in `src/content/i18n/` |
-| Put landing-page structured copy in `src/i18n/landing-page.ts` | Put landing text in `ui.ts` or Starlight i18n |
-| Use `[...locale]` rest-param pages for custom pages | Duplicate page files per locale (e.g., `zh-cn/posts/`) |
+| Put landing-page structured copy in `src/data/landing-page.ts` | Put landing text in `ui.ts` or Starlight i18n |
+| Use `[...locale]` rest-param pages for custom pages | Duplicate page files per locale |
| Use `translations` on sidebar items in `astro.config.mjs` | Create separate sidebar translation files for 2 locales |
| Use BCP-47 tags (`zh-CN`) in Starlight config/i18n files | Use URL slugs (`zh-cn`) in Starlight i18n JSON filenames |
| Use URL slugs (`zh-cn`) in directory paths and page routes | Use BCP-47 tags in directory/route paths |
@@ -369,13 +368,12 @@ Starlight ships with a blue accent (hue 224/234) and blue-tinted grays. The main
3. Starlight bridge: @import '@astrojs/starlight-tailwind';
4. Tailwind layers: @import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/utilities.css' layer(utilities);
-5. Animation utilities: @import "tw-animate-css"; (unlayered — @utility can't nest)
-6. @theme inline { … } — fonts, Starlight color scales, site design tokens, radius
-7. :root { … } — raw light tokens (unlayered)
-8. .dark, [data-theme="dark"] { … } — raw dark tokens (unlayered)
-9. :root { --sl-font/color overrides } — unlayered to beat bridge @layer utilities
-10. .dark, [data-theme="dark"] { --sl-color-* overrides }
-11. @layer base { … } — Tailwind preflight + site base resets (lowest priority)
+5. @theme inline { … } — fonts, Starlight color scales, site design tokens, radius
+6. :root { … } — raw light tokens (unlayered)
+7. .dark, [data-theme="dark"] { … } — raw dark tokens (unlayered)
+8. :root { --sl-font/color overrides } — unlayered to beat bridge @layer utilities
+9. .dark, [data-theme="dark"] { --sl-color-* overrides }
+10. @layer base { … } — site base resets (lowest priority)
```
**Why this order matters:**
@@ -383,19 +381,17 @@ Starlight ships with a blue accent (hue 224/234) and blue-tinted grays. The main
| Layer | Contains | Priority |
|---|---|---|
| `@layer base` | Site resets (`* { border-border }`, body bg/text, etc.) | Lowest |
-| `@layer starlight` | Starlight + expressive-code styles | Overrides `base` |
+| `@layer starlight` | Starlight component styles | Overrides `base` |
| `@layer theme` | Tailwind theme variables | Overrides `starlight` |
| `@layer utilities` | Tailwind utilities | Overrides `theme` |
| Unlayered CSS | Raw tokens, `--sl-font-*` / `--sl-color-*` overrides, `@media` queries | Highest |
**Key rules:**
-- **NEVER use `@import "tailwindcss"`** — it brings in the full Preflight reset that conflicts with Starlight and astro-expressive-code. Only import `tailwindcss/theme.css` and `tailwindcss/utilities.css` in their proper layers.
+- **NEVER use `@import "tailwindcss"`** — it brings in the full Preflight reset that conflicts with Starlight content styles. Only import `tailwindcss/theme.css` and `tailwindcss/utilities.css` in their proper layers.
- **Define Starlight colors via `@theme` scales** — `--color-accent-50` through `--color-accent-950` and `--color-gray-50` through `--color-gray-950`. The bridge reads these and generates `--sl-color-*` in `@layer utilities`.
- **Override `--sl-*` colors and fonts manually (unlayered)** — the bridge's auto-mapped values don't produce the right contrast for the monochrome theme. Unlayered `:root` / `.dark, [data-theme="dark"]` blocks with explicit `--sl-color-*` values win over the bridge's `@layer utilities` output.
-- **Import `tailwindcss/preflight.css` in `@layer base`** — restores box-sizing, link resets, and other base styles that the split Tailwind import omits. Because `base` is the lowest layer, EC and Starlight styles still override it.
-- **`tw-animate-css` must be imported unlayered** — it contains `@utility` directives that cannot be nested inside `@layer`.
-- **The `@layer base` `*` reset is safe** — because EC styles live in `@layer starlight.components` (higher priority), they always win.
+- **The `@layer base` `*` reset is safe** — Starlight styles live in a higher-priority layer and always win.
**Starlight color scales (in `@theme`):**
@@ -414,11 +410,13 @@ Both scales use pure neutral oklch values (0 chroma) to match the site's monochr
2. To change Starlight's accent color → update `--color-accent-*` values in `@theme inline`.
3. To change fonts → update `--font-sans` / `--font-mono` in `@theme inline` AND `--sl-font` / `--sl-font-mono` in the unlayered `:root` block.
4. To add site-specific overrides that should beat Starlight → put them **unlayered** (outside any `@layer`).
-5. To add base resets that Starlight/EC can override → put them in `@layer base`.
+5. To add base resets that Starlight can override → put them in `@layer base`.
-**Overriding expressive-code on custom pages (e.g., Hero):**
+**Styling Astro's `` component on custom pages (e.g., Hero):**
-When using `` from `astro-expressive-code` outside Starlight docs, override EC's CSS custom properties in the component's scoped `