Skip to content

perf(site): migrate markdown pipeline to Sätteri#1733

Open
decepulis wants to merge 5 commits into
mainfrom
claude/dreamy-lamport-per2ya
Open

perf(site): migrate markdown pipeline to Sätteri#1733
decepulis wants to merge 5 commits into
mainfrom
claude/dreamy-lamport-per2ya

Conversation

@decepulis

@decepulis decepulis commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Previously, Astro used unified to do its markdown processing. Now it suggests rust-based satteri.

This PR migrates our unified plugins to satteri and works around some new satteri idioms. We relied heavily on unified plugins for some MDX magic, so, this PR isn't trivial.

Visually, output is identical, though, so I think we succeeded.

The deets

Here's what's new

unified satteri notes
remarkReadingTime satteriReadingTime writes ctx.data.astro.frontmatter.{minutesRead,readingTimeMinutes}
remarkConditionalHeadings satteriConditionalHeadings writes frontmatter.conditionalHeadings
rehypePrepareCodeBlocks satteriCodeFrame see below
shikiTransformMetadata retired title read from fence meta in the wrap

Code blocks

Satteri runs at a different time than unified did, so we needed to apply code block styling differently. Here's how Claude would explain that:

Under Sätteri the Shiki highlight step rewrites each <pre> to raw HTML before HAST plugins run, so the old pre/code component overrides no longer fire for code blocks. Instead, satteriCodeFrame wraps each standalone fenced block in a new <CodeFrame> component at the MDAST stage (blocks inside an authored <TabsPanel> are left alone). CodeFrame.astro reuses the existing Tabs chrome + CopyButton, and the raw .astro-code <pre> gets its monospace font/size from a rule in shiki-transformers.css (previously supplied by MarkdownCode's codeBlock branch). Pre.astro is removed; MarkdownCode is simplified to inline-only.


Closes #1719.
Generated by Claude Code


Note

Medium Risk
Touches all MDX/Markdown rendering (TOC anchors, reading time, code frames); regressions would be site-wide but behavior is intended to be visually identical and covered by new plugin tests.

Overview
Switches the docs site from the unified (@astrojs/markdown-remark) Markdown processor to Sätteri via @astrojs/markdown-satteri, wiring three new MDAST plugins in astro.config.mjs: reading time, conditional TOC headings, and code-block framing.

Plugin parity: remarkReadingTime / remarkConditionalHeadings / rehypePrepareCodeBlocks and shikiTransformMetadata are removed and replaced by satteriReadingTime, satteriConditionalHeadings, and satteriCodeFrame, still writing the same remarkPluginFrontmatter fields (minutesRead, conditionalHeadings, etc.). Shiki notation transformers stay; shikiStripPreStyle replaces the title transformer and strips inline <pre> styles so CodeFrame owns chrome.

Code blocks: Because Sätteri emits highlighted <pre> as raw HTML before HAST/component overrides run, standalone fences are wrapped at MDAST in <CodeFrame> (Tabs UI + copy). Pre.astro is deleted; MarkdownCode is inline-only; .astro-code font rules move to shiki-transformers.css. Fences inside authored <TabsPanel> are not wrapped.

Deps & tests: Adds satteri; drops mdast-util-to-string and unist-util-visit. Vitest coverage exercises the new plugins under the Node environment (Sätteri native binding).

Reviewed by Cursor Bugbot for commit 6faed93. Bugbot is set up for automated code reviews on this repo. Configure here.

Flip `markdown.processor` from the `unified()` parity bridge to Astro 7's
native Rust processor, Sätteri, and port the custom transforms to Sätteri
MDAST plugins:

- satteriReadingTime / satteriConditionalHeadings: factories writing
  `ctx.data.astro.frontmatter.*` (surfaced unchanged as
  `render().remarkPluginFrontmatter`).
- satteriCodeFrame: wraps standalone fenced code in a new `<CodeFrame>`
  component, since under Sätteri the Shiki highlight step rewrites `<pre>`
  to raw HTML before HAST plugins run, so the old `pre`/`code` overrides no
  longer fire. CodeFrame reuses the existing Tabs chrome + CopyButton; the
  raw `.astro-code` <pre> gets its monospace font/size from a CSS rule.
  Title is read from fence meta, retiring shikiTransformMetadata.

Shiki themes/langs/notation transformers carry over via `shikiConfig`
(processor-independent); GFM + SmartyPants stay on by Astro defaults.
Drops @astrojs/markdown-remark, mdast-util-to-string, unist-util-visit;
adds satteri + @astrojs/markdown-satteri. Includes unit tests for the
three MDAST plugins and updates site/CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fACaXD786B35idFc1yUhW
@netlify

netlify Bot commented Jun 23, 2026

Copy link
Copy Markdown

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit 6faed93
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a3c2c98f027dd0008996615
😎 Deploy Preview https://deploy-preview-1733--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel

vercel Bot commented Jun 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Jun 24, 2026 7:14pm

Request Review

@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Initial
/video (default) 44.32 kB
/video (default + hls) 183.79 kB
/video (minimal) 43.98 kB
/video (minimal + hls) 183.63 kB
/audio (default) 37.83 kB
/audio (minimal) 36.61 kB
/background 4.20 kB
Media (10)
Entry Initial
/media/background-video 1.14 kB
/media/container 1.72 kB
/media/dash-video 214.74 kB
/media/hls-video 141.27 kB
/media/mux-audio 163.91 kB
/media/mux-video 163.68 kB
/media/native-hls-video 9.05 kB
/media/simple-hls-audio-only 17.15 kB
/media/simple-hls-video 18.69 kB
/media/vimeo-video 12.31 kB
Players (5)
Entry Initial
/video/player 8.07 kB
/audio/player 5.38 kB
/background/player 3.92 kB
/live-video/player 7.64 kB
/live-audio/player 5.39 kB
Skins (30)
Entry Type Initial
/video/minimal-skin.css css 5.45 kB
/video/skin.css css 5.43 kB
/video/minimal-skin js 44.01 kB
/video/minimal-skin.tailwind js 44.58 kB
/video/skin js 44.28 kB
/video/skin.tailwind js 44.98 kB
/audio/minimal-skin.css css 3.60 kB
/audio/skin.css css 3.53 kB
/audio/minimal-skin js 36.62 kB
/audio/minimal-skin.tailwind js 37.04 kB
/audio/skin js 37.80 kB
/audio/skin.tailwind js 38.25 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.45 kB
/live-video/skin.css css 5.43 kB
/live-video/minimal-skin js 43.12 kB
/live-video/minimal-skin.tailwind js 43.58 kB
/live-video/skin js 43.08 kB
/live-video/skin.tailwind js 43.58 kB
/live-audio/minimal-skin.css css 3.60 kB
/live-audio/skin.css css 3.53 kB
/live-audio/minimal-skin js 29.73 kB
/live-audio/minimal-skin.tailwind js 29.20 kB
/live-audio/skin js 31.04 kB
/live-audio/skin.tailwind js 30.66 kB
/global.css css 176 B
/shared.css css 88 B
/tailwind.css css 228 B
/skin-element js 1.44 kB
UI Components (38)
Entry Initial
/ui/airplay-button 2.29 kB
/ui/alert-dialog 2.45 kB
/ui/alert-dialog-close 2.15 kB
/ui/alert-dialog-description 2.18 kB
/ui/alert-dialog-title 2.17 kB
/ui/buffering-indicator 2.24 kB
/ui/captions-button 2.37 kB
/ui/captions-radio-group 2.73 kB
/ui/cast-button 2.25 kB
/ui/compounds 2.85 kB
/ui/controls 2.58 kB
/ui/error-dialog 2.61 kB
/ui/fullscreen-button 2.29 kB
/ui/hotkey 2.31 kB
/ui/menu 2.63 kB
/ui/mute-button 2.27 kB
/ui/pip-button 2.28 kB
/ui/play-button 2.28 kB
/ui/playback-rate-button 2.36 kB
/ui/playback-rate-radio-group 2.78 kB
/ui/popover 2.64 kB
/ui/poster 2.14 kB
/ui/quality-radio-group 2.75 kB
/ui/seek-button 2.27 kB
/ui/seek-indicator 2.29 kB
/ui/seek-indicator-value 465 B
/ui/slider 2.62 kB
/ui/status-announcer 2.28 kB
/ui/status-indicator 2.36 kB
/ui/status-indicator-value 467 B
/ui/thumbnail 2.09 kB
/ui/time 2.60 kB
/ui/time-slider 2.64 kB
/ui/tooltip 2.59 kB
/ui/volume-indicator 2.36 kB
/ui/volume-indicator-fill 410 B
/ui/volume-indicator-value 426 B
/ui/volume-slider 2.64 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Initial
/video (default) 36.83 kB
/video (default + hls) 175.18 kB
/video (minimal) 36.91 kB
/video (minimal + hls) 175.13 kB
/audio (default) 29.97 kB
/audio (minimal) 30.04 kB
/background 754 B
Media (9)
Entry Initial
/media/background-video 575 B
/media/dash-video 213.24 kB
/media/hls-video 139.80 kB
/media/mux-audio 162.26 kB
/media/mux-video 162.26 kB
/media/native-hls-video 7.39 kB
/media/simple-hls-audio-only 15.56 kB
/media/simple-hls-video 17.15 kB
/media/vimeo-video 10.58 kB
Skins (27)
Entry Type Initial
/tailwind.css css 228 B
/video/minimal-skin.css css 5.37 kB
/video/skin.css css 5.34 kB
/video/minimal-skin js 36.81 kB
/video/minimal-skin.tailwind js 42.58 kB
/video/skin js 36.73 kB
/video/skin.tailwind js 42.51 kB
/audio/minimal-skin.css css 3.47 kB
/audio/skin.css css 3.39 kB
/audio/minimal-skin js 29.98 kB
/audio/minimal-skin.tailwind js 31.76 kB
/audio/skin js 29.91 kB
/audio/skin.tailwind js 33.75 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.37 kB
/live-video/skin.css css 5.34 kB
/live-video/minimal-skin js 32.56 kB
/live-video/minimal-skin.tailwind js 38.22 kB
/live-video/skin js 32.58 kB
/live-video/skin.tailwind js 38.24 kB
/live-audio/minimal-skin.css css 3.47 kB
/live-audio/skin.css css 3.39 kB
/live-audio/minimal-skin js 21.74 kB
/live-audio/minimal-skin.tailwind js 24.64 kB
/live-audio/skin js 21.78 kB
/live-audio/skin.tailwind js 24.78 kB
UI Components (32)
Entry Initial
/ui/airplay-button 2.23 kB
/ui/alert-dialog 2.20 kB
/ui/buffering-indicator 2.10 kB
/ui/captions-button 2.16 kB
/ui/captions-radio-group 2.09 kB
/ui/cast-button 2.19 kB
/ui/controls 2.05 kB
/ui/error-dialog 2.24 kB
/ui/fullscreen-button 2.26 kB
/ui/gesture 2.25 kB
/ui/hotkey 2.27 kB
/ui/live-button 2.12 kB
/ui/menu 2.43 kB
/ui/mute-button 2.24 kB
/ui/pip-button 2.26 kB
/ui/play-button 2.19 kB
/ui/playback-rate 2.06 kB
/ui/playback-rate-button 2.25 kB
/ui/popover 2.64 kB
/ui/poster 2.09 kB
/ui/quality 2.10 kB
/ui/seek-button 2.20 kB
/ui/seek-indicator 2.19 kB
/ui/slider 2.26 kB
/ui/status-announcer 2.11 kB
/ui/status-indicator 2.15 kB
/ui/thumbnail 2.00 kB
/ui/time 2.05 kB
/ui/time-slider 2.29 kB
/ui/tooltip 2.59 kB
/ui/volume-indicator 2.18 kB
/ui/volume-slider 2.30 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (14)
Entry Initial
. 9.08 kB
/dom 17.02 kB
/dom/media/custom-media-element 2.09 kB
/dom/media/dash 209.07 kB
/dom/media/google-cast 4.04 kB
/dom/media/hls 135.63 kB
/dom/media/media-host 1.25 kB
/dom/media/media-played-ranges 576 B
/dom/media/mux 151.26 kB
/dom/media/native-hls 3.05 kB
/dom/media/simple-hls 16.47 kB
/dom/media/simple-hls-audio-only 14.92 kB
/dom/media/vimeo 9.86 kB
/media/predicate 573 B
🏷️ @videojs/element — no changes
Entries (2)
Entry Initial
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Initial
. 1.39 kB
/html 696 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Initial
/array 104 B
/dom 2.26 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 231 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (4)
Entry Initial
. 4.45 kB
/dom 6.33 kB
/hls 15.37 kB
/background-video 12.85 kB

ℹ️ How to interpret

JS sizes are initial static graph totals (minified + brotli). Lazy dynamic chunks are shown separately when present.

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current initial sizes.

`astro check` is stricter than the project's tsgo typecheck and flagged
type-only issues the latter missed:

- Cast the parsed reference JSON to the model's parameter type (the old
  `.js` plugin was unchecked) and pass `partOrder ?? undefined`.
- Type the code-frame ancestor walk var to admit `undefined`.
- Seed the full `data.astro` shape (incl. localImagePaths/remoteImagePaths)
  in the plugin tests to match the markdown-satteri DataMap augmentation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fACaXD786B35idFc1yUhW
The Sätteri migration added `.astro-code { font-size: var(--text-code) }`
to restore monospace sizing for raw highlighted `<pre>`. But `--text-code`
is an `em`, and the React `Code` components (`shared.pre` = "shiki
astro-code") also put `text-code` on their inner `<code>`. Sizing the
`<pre>` as well compounded the two `em`s (0.875 × 0.875), shrinking that
code to ~12.25px next to the 14px MDX blocks.

Move the size onto `.astro-code code` so it applies to the code element
once, matching the pre-Sätteri output where MarkdownCode sized `<code>`
directly. Verified all code blocks render uniformly at 14px.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fACaXD786B35idFc1yUhW
… control

Astro's Shiki highlighter writes an inline style on each highlighted
`<pre>` (theme background/color plus `overflow-x: auto`). Pre-Sätteri,
`Pre.astro` deliberately discarded that style so the surrounding frame
owned the background and scrolling — Shiki highlighted the text, the
container was ours. Under Sätteri the pre is slotted as raw HTML, so the
inline style survived: code blocks gained their own scroll box inset
inside the frame padding instead of scrolling at the container.

Add a Shiki `pre` transformer that deletes the inline style. Astro appends
its built-in transformer first, so ours runs last and removes the
fully-assembled style. Token colors live on the inner spans and are
unaffected; the React `Code` components already rebuild their own `<pre>`,
so only the markdown/MDX path needed this. Verified the built `<pre>` now
matches main (class + tabindex + data-language, no style).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fACaXD786B35idFc1yUhW

@decepulis decepulis left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly just nits about what goes in the comments. please address and then mark as ready for review.

Comment thread site/src/components/typography/CodeFrame.astro
Comment thread site/src/components/typography/MarkdownCode.astro Outdated
Comment thread site/src/styles/shiki-transformers.css Outdated
Comment thread site/src/styles/shiki-transformers.css Outdated
Comment thread site/src/utils/satteriCodeFrame.ts
Comment thread site/astro.config.mjs Outdated
Comment thread site/CLAUDE.md

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file requires a larger cleanup, but I'm considering that out of scope for this PR

Address review: comments should explain what the code does or why it's
unusual for future maintainers, not narrate the migration. Remove
references to the prior remark/rehype plugins, `Pre.astro`, and the old
processor across the Sätteri plugins, `CodeFrame`, `MarkdownCode`, the
shiki-transformers CSS, and the Astro config; tighten the wording.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fACaXD786B35idFc1yUhW
@decepulis decepulis marked this pull request as ready for review June 24, 2026 19:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Chore: Migrate Site Markdown Pipeline to Sätteri for Build Speed

2 participants