Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
61f4c06
poc: add astro@6
colbywhite Feb 27, 2026
0109a76
deps: patch breadcrumbs, validator
colbywhite Mar 9, 2026
43beafa
fix: use correct slug
colbywhite Mar 9, 2026
f86ac29
fix: use correct import for zod
colbywhite Mar 9, 2026
d7426a1
fix: better infer enum
colbywhite Mar 9, 2026
4e2196c
refactor: use json schema to build frontmatter UI
colbywhite Mar 9, 2026
570a498
fix: use url type for schema
colbywhite Mar 9, 2026
afd38ad
fix: use looseObject instead of empty object
colbywhite Mar 9, 2026
b9a724e
refactor: let ts infer sidebar types
colbywhite Mar 9, 2026
3806b30
feat: rm strict enforcing of frontmatter enums
colbywhite Mar 9, 2026
3ab6835
fix: rm errors that don't occur in v6
colbywhite Mar 9, 2026
d28f981
lint: ignore vitest imports
colbywhite Mar 9, 2026
d428360
ci: bump the heap
colbywhite Mar 9, 2026
faeb6e2
lint: rm unneeded ts-ignore
colbywhite Mar 9, 2026
50e1fde
fix: correct expected zod test error
colbywhite Mar 9, 2026
be2b823
fix: properly declare images optional
colbywhite Mar 9, 2026
cabc674
fix: override products prop
colbywhite Mar 10, 2026
1ce5d42
fix: override preview_image prop
colbywhite Mar 10, 2026
db89850
fix: defend against product names
colbywhite Mar 10, 2026
6d8d97d
fix: pass changelog references so Head can handle it.
colbywhite Mar 11, 2026
3baf1ad
fix: simplify changlog head logic by using original entry
colbywhite Mar 11, 2026
e1a8e31
fix: ensure content type tag is in changelog pages
colbywhite Mar 11, 2026
357b37f
deps: graduate pass the beta versions
colbywhite Mar 12, 2026
c578ac1
fix: exclude non-http links from validation
colbywhite Mar 12, 2026
4083cd6
fix: exclude separators in non-http links
colbywhite Mar 12, 2026
c910d40
fix: replace :root with :global()
colbywhite Mar 12, 2026
fd23166
fix: better handle slashes in vscode, chrome urls
colbywhite Mar 12, 2026
7cdabfa
fix: explictly set global styles for title
colbywhite Mar 12, 2026
f15675f
fix: explictly set global on breadcrumb styles
colbywhite Mar 12, 2026
3caa701
deps: consume starlight-links-validator patch
colbywhite Mar 13, 2026
1319f9e
deps: another version bump
colbywhite Mar 16, 2026
7a07c16
Fix merge conflict
kodster28 Mar 17, 2026
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
174 changes: 174 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Astro v6 + Starlight Beta — Migration POC

> **Reference links:**
>
> - Starlight beta: [withastro/starlight#3644]
> - Astro Docs example: [withastro/docs#13230]
> - [Astro migration guide]
> - [Zod 4 migration guide]
> - [Cloudflare adapter migration guide]

[withastro/starlight#3644]: https://github.com/withastro/starlight/pull/3644
[withastro/docs#13230]: https://github.com/withastro/docs/pull/13230
[Astro Migration guide]: https://v6.docs.astro.build/en/guides/upgrade-to/v6/
[Zod 4 migration guide]: https://zod.dev/v4/changelog
[Cloudflare adapter migration guide]: https://v6.docs.astro.build/en/guides/integrations-guide/cloudflare/#upgrading-to-v13-and-astro-6
[STARLIGHT_CUSTOMIZATIONS.md]: ./STARLIGHT_CUSTOMIZATIONS.md

## High-risk items and unknowns

| Item | Risk | Status |
|-------------------------------------------------------|--------|---------------------------------------------------------------|
| **`Page.astro` Vite alias** | HIGH | Unknown — We're coupled to low-level private implementation |
| **`patch-package` hunks against Starlight internals** | HIGH | Unknown — We're coupled to low-level private implementation |
| **`starlight-links-validator` Zod 4 incompatibility** | HIGH | Confirmed broken upstream |
| **Community Starlight plugins vs. beta** | MEDIUM | Unknown — must investigate each |
| **Vitest compatibility** | LOW | Must stay on Vitest v3.2.x, but we don't have a lot of tests. |
| **Zod 4** | LOW | Will result in warnings, not errors |

## Background notes

- We do **not** use the `@astrojs/cloudflare` adapter.
The site is compiled to static HTML and served by a custom Cloudflare Worker defined in `worker/index.ts`.
The [Cloudflare adapter migration guide] does not apply here.
- We have **six layers of Starlight customization** documented in [STARLIGHT_CUSTOMIZATIONS.md].
This should be read beforehand.
The most invasive is `patch-package`, which directly modifies Starlight files in `node_modules` after every install.
- We use several **community Starlight plugins** alongside the official ones.

## Version targets

Targets are determined by [withastro/docs#13230].
That is the north star.

| Package | Current | Target |
| ------------------------------ | -------- |--------------------------------------------------------|
| `astro` | `5.13.7` | `6.0.0-beta.8` |
| `@astrojs/starlight` | `0.36.0` | `https://pkg.pr.new/@astrojs/starlight@3644` |
| `@astrojs/starlight-docsearch` | `0.6.0` | `https://pkg.pr.new/@astrojs/starlight-docsearch@3644` |
| `@astrojs/starlight-tailwind` | `4.0.1` | `https://pkg.pr.new/@astrojs/starlight-tailwind@3644` |
| `@astrojs/check` | `0.9.4` | `0.9.7-beta.1` |
| `@astrojs/react` | `4.2.5` | ? |
| `@astrojs/sitemap` | `3.5.1` | `3.6.1-beta.3` |
| `@astrojs/rss` | `4.0.12` | ? |

The Starlight packages use `pkg.pr.new` URLs.
These are preview builds cut directly from the PR branch, not published to npm.

## Can any of this be done in a separate PR first?

Must be done in the final big-bang PR:

- `astro/zod` (the Astro 6 replacement for `astro:schema`) does not exist in Astro 5. The
18-file import rename must happen in the same commit as the version bump.
- `experimental.contentIntellisense` is still behind the experimental flag in Astro 5; it
can only be removed once Astro 6 is installed.
- All `patch-package` patches are pinned to exact package version strings and must be
regenerated against whatever versions the new packages install as.
- Zod 4 cannot be upgraded independently — it is bundled by Astro and upgrades with it.

## Things to ignore

Both of these are deprecated in Zod 4 but not removed. The old names still work at runtime; the only consequence is TypeScript deprecation warnings.

| Usage | Deprecated in favor of |
| ---------------------------- | ------------------------------- |
| `.catch((ctx) => ctx.input)` | `ctx.value` |
| `.describe("...")` | `.meta({ description: "..." })` |

---

## Migration steps

### Step 1 — Update versions

```shell
npx @astrojs/upgrade beta
```

This likely takes care of most Astro packages.
Starlight package must be manually set to the PR.

```shell
npm install \
@astrojs/starlight@https://pkg.pr.new/@astrojs/starlight@3644 \
@astrojs/starlight-docsearch@https://pkg.pr.new/@astrojs/starlight-docsearch@3644 \
@astrojs/starlight-tailwind@https://pkg.pr.new/@astrojs/starlight-tailwind@3644
```

> **Note on Vitest:** `vitest` must stay at `3.2.x`. `vitest.workspace.ts` uses
> `getViteConfig()` from `astro/config`, which Astro 6 only supports with Vitest v3.2.
> Vitest v4 is not yet supported.

### Step 2 — Regenerate patch-package patches ⚠️ HIGH RISK

See `STARLIGHT_CUSTOMIZATIONS.md` for full context on what each patch does.
It's likely these patches don't work out the box for the new Starlight beta.

Open question: Why did we do this?
This makes _every_ upgrade nearly impossible.

### Step 3 — Update `astro:schema` imports to `astro/zod`

All 18 files in `src/schemas/` import `z` from `"astro:schema"`.
Astro 6 deprecates this virtual module in favor of `"astro/zod"`.

```diff
-import { z } from "astro:schema";
+import { z } from "astro/zod";
```

Note: The [zod code mod] is likely useful.

Open question: Is there an Astro-specific code mod? Should there be?

Without a code mod:

```shell
find src/schemas -name "*.ts" | xargs sed -i '' 's|from "astro:schema"|from "astro/zod"|g'
```

[zod code mod]: https://github.com/nicoespeon/zod-v3-to-v4

### Step 4 — Remove `experimental.contentIntellisense` from `astro.config.ts`

Content Intellisense was behind an experimental flag in Astro 5 and is stable by default in Astro 6.
The flag will either be silently ignored or cause a type error.

In `astro.config.ts`, remove:

```ts
experimental: {
contentIntellisense: true,
},
```

### Step 5 — Investigate the `Page.astro` Vite alias ⚠️ HIGH RISK

**File:** `astro.config.ts`, `vite.resolve.alias` block

We shim Starlight's non-overridable `Page.astro` at the Vite bundler level:

```ts
vite: {
resolve: {
alias: {
"./Page.astro": fileURLToPath(...),
"../components/Page.astro": fileURLToPath(...),
},
},
},
```

Open question: Can we even keep doing such a low-level override?
This likely wasn't exposed for a reason.

### Step 6 — Check community Starlight plugins

| Plugin | Current version | Status |
| ---------------------------- | --------------- | -------------------------------------------- |
| `starlight-links-validator` | `0.17.2` | **Confirmed broken** — Zod 4 incompatibility |
| `starlight-image-zoom` | `0.13.0` | ? |
| `starlight-scroll-to-top` | `0.4.0` | ? |
| `starlight-package-managers` | `0.11.0` | ? |
| `starlight-showcases` | `0.3.0` | ? |
181 changes: 181 additions & 0 deletions STARLIGHT_CUSTOMIZATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Starlight Customizations

This doc explains every way we've extended or patched Starlight. There are six distinct layers, ordered roughly from most to least invasive.

## 1. `patch-package` — Direct node_modules patches

**Trigger:** `postinstall` in `package.json` runs `npx patch-package` after every `npm install`.

Patch files live in `patches/`. These are the nuclear option — used when Starlight gives us no other extension point.

### `patches/@astrojs+starlight+0.36.0.patch`

Three hunks against three different files:

**`components/SidebarSublist.astro`** — Sidebar icons

Starlight has no native API for per-item icons in the sidebar. We inject `~/components/SidebarIcon.astro` (Lottie animations) before each sidebar label:

```diff
-<span>{entry.label}</span>
+<span>{entry.icon && <SidebarIcon {...entry.icon} />}{entry.label}</span>
```

The `icon` property comes from sidebar entry `attrs` set in our sidebar generation logic.

**`integrations/remark-rehype-utils.ts`** — Remark/rehype pipeline scope

Starlight's remark/rehype processing originally bails out for any file not inside the `docs` collection. We widen the guard to also process `partials/` and `changelog/`:

```diff
-if (!normalizePath(file.path).startsWith(docsCollectionPath)) return false;
+if (
+ !normalizePath(file.path).startsWith(docsCollectionPath) &&
+ !normalizePath(file.path).includes("/src/content/partials/") &&
+ !normalizePath(file.path).includes("/src/content/changelog/")
+) return false;
```

Without this, heading transforms, tab components, etc. don't run inside partials or changelogs.

**`user-components/Tabs.astro`** — Injectable icon component

Makes the `Icon` component inside `<Tabs>` replaceable via prop, so callers (e.g. `TypeScriptExample`) can swap in a custom icon renderer:

```diff
+IconComponent?: typeof Icon;
-const { syncKey } = Astro.props;
+const { syncKey, IconComponent = Icon } = Astro.props;
...
-{icon && <Icon name={icon} />}
+{icon && <IconComponent name={icon} />}
```

### `patches/@astrojs+starlight-docsearch+0.6.0.patch`

**`DocSearch.astro`** — Keyboard shortcut hint UI

We disable the `/` shortcut in DocSearch (re-routing it to the sidebar search input instead). The original button always rendered a `/` slash icon. The patch makes the hint adaptive:

- Default: renders styled `<kbd>`-style key badges (`Ctrl K`)
- Only shows the slash SVG icon when `/` is enabled AND `Ctrl/Cmd+K` is disabled

The slash icon CSS is moved from a static `<style>` block into a JS conditional that runs after `docsearch()` initializes and reads `options.keyboardShortcuts`.

**`index.ts`** — Schema extension

Adds `keyboardShortcuts` to the DocSearch Zod config schema so the above is type-safe:

```ts
keyboardShortcuts: z.object({
"Ctrl/Cmd+K": z.boolean().optional(),
"/": z.boolean().optional(),
}).optional();
```

> **Upgrade note:** Both patches are pinned to exact package versions (`starlight@0.36.0`, `starlight-docsearch@0.6.0`). Upgrading either package requires regenerating the patch with `npx patch-package @astrojs/starlight` after manually re-applying the changes.



## 2. Vite alias — `Page.astro` (non-overridable slot)

**Location:** `astro.config.ts` → `vite.resolve.alias`

Starlight doesn't expose `Page.astro` in its official `components:` override map. We shim it at the Vite bundler level:

```ts
vite: {
resolve: {
alias: {
"./Page.astro": fileURLToPath(new URL("./src/components/overrides/Page.astro", import.meta.url)),
"../components/Page.astro": fileURLToPath(new URL("./src/components/overrides/Page.astro", import.meta.url)),
},
},
},
```

Both alias paths are needed because different internal Starlight files import `Page.astro` using different relative depths.

**What `src/components/overrides/Page.astro` does:**

It wraps Starlight's original `Page.astro` and mutates the `starlightRoute` locals before passing them through:

- **ToC regeneration** — calls `generateTableOfContents(html)` against the fully-rendered HTML instead of using Starlight's AST-derived ToC. This means our custom heading transforms (slugs, shift-headings) are reflected correctly.
- **Sidebar replacement** — calls `getSidebar(Astro)` to build a product-specific sidebar, replacing Starlight's static autogenerated one.
- **Pagination** — flattens the custom sidebar and computes prev/next from it, respecting `data-group-label` attrs for section labels.
- **Description generation** — if `frontmatter.description` is set, renders it through Markdown. If not, extracts a description from the rendered HTML.
- **`lastUpdated` suppression** — sets `data.lastUpdated = undefined` when a `reviewed` date is present, so only one recency signal shows in the UI.
- **Full-width layout** — injects `--sl-content-width: 67.5rem` on pages without a ToC.

## 3. Starlight `components:` overrides

Registered in `astro.config.ts` under `starlight({ components: { ... } })`.
All files are in `src/components/overrides/`.

These are overrides that use the official Starlight `components:` API.
See **[STARLIGHT_OVERRIDES.md](./STARLIGHT_OVERRIDES.md)**.

## 4. Starlight `plugins:`

Registered in `astro.config.ts` under `starlight({ plugins: [...] })`.

| Plugin | When active | Notes |
| ------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `starlightLinksValidator` | `RUN_LINK_CHECK=true` only | Validates internal links at build time. Long exclude list for dynamic routes, API paths, wildcard patterns. `errorOnInvalidHashes` and `errorOnLocalLinks` both disabled. |
| `starlightDocSearch` | Always | Algolia DocSearch via `clientOptionsModule: "./src/plugins/docsearch/index.ts"` — sets app ID/key, rewrites result URLs to current origin, adds "View all results" footer, disables the `/` keyboard shortcut. |
| `starlightImageZoom` | Always | Enables the zoom functionality consumed by `MarkdownContent.astro`. |
| `starlightScrollToTop` | Always | Custom double-chevron SVG path, progress ring (white), tooltip "Back to top", hidden on homepage. |


## 5. Starlight `routeMiddleware`

**File:** `src/plugins/starlight/route-data.ts`
**Registered:** `astro.config.ts` → `starlight({ routeMiddleware: "..." })`

Runs on every route via `defineRouteMiddleware`. Validates and normalizes the `tags` frontmatter array:

1. For each tag, searches the `allowedTags` allowlist (from `src/schemas/tags.ts`) for a case-insensitive match against `label` or any `variants`.
2. If found, replaces the raw tag string with the canonical `label`.
3. If not found, throws a build error with a link to the style guide.

This is why you can write `javascript` or `JavaScript` in frontmatter and it gets normalized to the canonical casing before it hits the UI or search index.


## 6. Expressive Code plugins

**File:** `ec.config.mjs`

Starlight's code block rendering runs through Expressive Code. We register six plugins:

| Plugin | File | Purpose |
| --------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| `pluginWorkersPlayground` | `src/plugins/expressive-code/workers-playground.js` | Adds "Open in Workers Playground" button to eligible code blocks |
| `pluginOutputFrame` | `src/plugins/expressive-code/output-frame.js` | Custom "output" frame type for terminal output blocks |
| `pluginDefaultTitles` | `src/plugins/expressive-code/default-titles.js` | Auto-assigns language-based titles (e.g. "TypeScript") to blocks without an explicit `title=` |
| `pluginCollapsibleSections` | `@expressive-code/plugin-collapsible-sections` | Upstream plugin enabling `collapse` ranges in code blocks |
| `pluginGraphqlApiExplorer` | `src/plugins/expressive-code/graphql-api-explorer.js` | Adds "Open in GraphQL Explorer" button to `graphql` blocks |
| `pluginLineNumbers` | `@expressive-code/plugin-line-numbers` | Upstream plugin; line numbers are off by default (`showLineNumbers: false`) |

Other config:

- **Themes** — Cloudflare's custom `solarflare-theme` dark and light VSCode themes
- **`curl` alias** — `curl` blocks are highlighted as `sh`
- **`extractFileNameFromCode: false`** — disables auto-extracting titles from code comments
- **Style overrides** — 1px border radius 0.25rem, adjusted text marker luminance for light/dark


## Remark/Rehype pipeline

These aren't Starlight-specific but affect how MDX content is processed. Registered in `astro.config.ts` → `markdown`:

| Plugin | File | Purpose |
| ------------------------ | ----------------------------------------- | --------------------------------------------------------------------------- |
| `remarkValidateImages` | `src/plugins/remark/validate-images` | Build-time validation that image paths resolve |
| `rehypeMermaid` | `src/plugins/rehype/mermaid.ts` | Converts `mermaid` code blocks to diagrams |
| `rehypeExternalLinks` | `src/plugins/rehype/external-links.ts` | Adds `target="_blank" rel="noopener"` to external links |
| `rehypeHeadingSlugs` | `src/plugins/rehype/heading-slugs.ts` | Custom slug generation for heading IDs |
| `rehypeAutolinkHeadings` | `src/plugins/rehype/autolink-headings.ts` | Injects anchor links next to headings (used by `MarkdownContent.astro` CSS) |
| `rehypeTitleFigure` | `rehype-title-figure` (upstream) | Wraps images with `title` attrs in `<figure>`/`<figcaption>` |
| `rehypeShiftHeadings` | `src/plugins/rehype/shift-headings.ts` | Shifts heading levels (e.g. H1→H2) for content that starts at H1 |

These run via Starlight's `markdown.headingLinks: false` (disabled — we handle anchor links ourselves via `rehypeAutolinkHeadings`).
Loading
Loading