Skip to content

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

Merged
decepulis merged 7 commits into
mainfrom
claude/dreamy-lamport-per2ya
Jul 2, 2026
Merged

perf(site): migrate markdown pipeline to Sätteri#1733
decepulis merged 7 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
This rewrites the central MDX/markdown build path (TOC, reading time, and code framing) for all docs and blog content; regressions would be visible sitewide, though behavior is intended to be pixel-identical and is covered by new plugin tests.

Overview
Switches the docs site from Astro’s unified() remark/rehype stack to @astrojs/markdown-satteri with native Sätteri MDAST plugins, aimed at faster builds while keeping the same rendered output.

Pipeline: astro.config.mjs now uses processor: satteri({ mdastPlugins: [...] }). The old remarkReadingTime, remarkConditionalHeadings, and rehypePrepareCodeBlocks (plus shikiTransformMetadata) are removed and replaced by satteriReadingTime, satteriConditionalHeadings, and satteriCodeFrame, still writing remarkPluginFrontmatter fields (minutesRead, readingTimeMinutes, conditionalHeadings).

Code blocks: Because Shiki turns <pre> into raw HTML before HAST/component overrides run, standalone fences are wrapped at MDAST in <CodeFrame> (Tabs chrome + copy). Pre.astro is deleted; MarkdownCode is inline-only; .astro-code typography moves to shiki-transformers.css. Fence title="..." is parsed in the MDAST plugin; shikiStripPreStyle drops inline styles on highlighted <pre>.

Deps/docs/tests: @astrojs/markdown-remark@astrojs/markdown-satteri + satteri; Vitest coverage for the three Sätteri plugins (node env). CLAUDE.md documents the new processor model.

Reviewed by Cursor Bugbot for commit bd8dbeb. 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 bd8dbeb
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/6a458caf38dfc70008920a53
😎 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 Jul 1, 2026 9:55pm

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) 46.22 kB
/video (default + hls) 185.88 kB
/video (minimal) 46.20 kB
/video (minimal + hls) 185.91 kB
/audio (default) 39.78 kB
/audio (minimal) 38.84 kB
/background 4.20 kB
Media (10)
Entry Initial
/media/background-video 1.14 kB
/media/container 1.72 kB
/media/dash-video 214.72 kB
/media/hlsjs-video 141.49 kB
/media/mux-audio 163.89 kB
/media/mux-video 163.96 kB
/media/native-hls-video 9.05 kB
/media/simple-hls-audio-only 17.15 kB
/media/simple-hls-video 18.72 kB
/media/vimeo-video 12.31 kB
Players (5)
Entry Initial
/video/player 8.22 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.65 kB
/video/skin.css css 5.61 kB
/video/minimal-skin js 46.15 kB
/video/minimal-skin.tailwind js 46.92 kB
/video/skin js 46.18 kB
/video/skin.tailwind js 46.96 kB
/audio/minimal-skin.css css 3.93 kB
/audio/skin.css css 3.82 kB
/audio/minimal-skin js 38.95 kB
/audio/minimal-skin.tailwind js 39.36 kB
/audio/skin js 39.78 kB
/audio/skin.tailwind js 40.27 kB
/background/skin.css css 133 B
/background/skin js 1.14 kB
/live-video/minimal-skin.css css 5.65 kB
/live-video/skin.css css 5.61 kB
/live-video/minimal-skin js 45.20 kB
/live-video/minimal-skin.tailwind js 45.73 kB
/live-video/skin js 44.77 kB
/live-video/skin.tailwind js 45.50 kB
/live-audio/minimal-skin.css css 3.93 kB
/live-audio/skin.css css 3.82 kB
/live-audio/minimal-skin js 31.54 kB
/live-audio/minimal-skin.tailwind js 30.98 kB
/live-audio/skin js 32.54 kB
/live-audio/skin.tailwind js 32.01 kB
/global.css css 176 B
/shared.css css 104 B
/tailwind.css css 161 B
/skin-element js 1.45 kB
UI Components (39)
Entry Initial
/ui/airplay-button 2.44 kB
/ui/alert-dialog 2.56 kB
/ui/alert-dialog-close 2.19 kB
/ui/alert-dialog-description 2.17 kB
/ui/alert-dialog-title 2.16 kB
/ui/audio-track-radio-group 2.85 kB
/ui/buffering-indicator 2.27 kB
/ui/captions-button 2.45 kB
/ui/captions-radio-group 2.83 kB
/ui/cast-button 2.41 kB
/ui/compounds 2.88 kB
/ui/controls 2.62 kB
/ui/error-dialog 2.65 kB
/ui/fullscreen-button 2.41 kB
/ui/hotkey 2.30 kB
/ui/menu 2.67 kB
/ui/mute-button 2.41 kB
/ui/pip-button 2.39 kB
/ui/play-button 2.45 kB
/ui/playback-rate-button 2.40 kB
/ui/playback-rate-radio-group 2.80 kB
/ui/popover 2.72 kB
/ui/poster 2.27 kB
/ui/quality-radio-group 2.80 kB
/ui/seek-button 2.34 kB
/ui/seek-indicator 2.37 kB
/ui/seek-indicator-value 446 B
/ui/slider 2.67 kB
/ui/status-announcer 2.30 kB
/ui/status-indicator 2.40 kB
/ui/status-indicator-value 411 B
/ui/thumbnail 2.19 kB
/ui/time 2.64 kB
/ui/time-slider 2.66 kB
/ui/tooltip 2.65 kB
/ui/volume-indicator 2.33 kB
/ui/volume-indicator-fill 416 B
/ui/volume-indicator-value 392 B
/ui/volume-slider 2.67 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Initial
/video (default) 38.31 kB
/video (default + hls) 176.58 kB
/video (minimal) 38.65 kB
/video (minimal + hls) 177.24 kB
/audio (default) 31.51 kB
/audio (minimal) 31.60 kB
/background 754 B
Media (9)
Entry Initial
/media/background-video 575 B
/media/dash-video 213.16 kB
/media/hlsjs-video 140.07 kB
/media/mux-audio 162.52 kB
/media/mux-video 162.57 kB
/media/native-hls-video 7.41 kB
/media/simple-hls-audio-only 15.56 kB
/media/simple-hls-video 17.14 kB
/media/vimeo-video 10.58 kB
Skins (27)
Entry Type Initial
/tailwind.css css 161 B
/video/minimal-skin.css css 5.55 kB
/video/skin.css css 5.52 kB
/video/minimal-skin js 38.57 kB
/video/minimal-skin.tailwind js 44.64 kB
/video/skin js 38.21 kB
/video/skin.tailwind js 44.31 kB
/audio/minimal-skin.css css 3.79 kB
/audio/skin.css css 3.68 kB
/audio/minimal-skin js 31.48 kB
/audio/minimal-skin.tailwind js 33.72 kB
/audio/skin js 31.46 kB
/audio/skin.tailwind js 35.63 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 5.55 kB
/live-video/skin.css css 5.52 kB
/live-video/minimal-skin js 33.37 kB
/live-video/minimal-skin.tailwind js 39.33 kB
/live-video/skin js 33.38 kB
/live-video/skin.tailwind js 39.35 kB
/live-audio/minimal-skin.css css 3.79 kB
/live-audio/skin.css css 3.68 kB
/live-audio/minimal-skin js 22.45 kB
/live-audio/minimal-skin.tailwind js 25.59 kB
/live-audio/skin js 22.52 kB
/live-audio/skin.tailwind js 25.65 kB
UI Components (33)
Entry Initial
/ui/airplay-button 2.20 kB
/ui/alert-dialog 2.16 kB
/ui/audio-track 2.18 kB
/ui/buffering-indicator 2.15 kB
/ui/captions-button 2.24 kB
/ui/captions-radio-group 2.10 kB
/ui/cast-button 2.18 kB
/ui/controls 2.05 kB
/ui/error-dialog 2.25 kB
/ui/fullscreen-button 2.19 kB
/ui/gesture 2.26 kB
/ui/hotkey 2.22 kB
/ui/live-button 2.13 kB
/ui/menu 2.29 kB
/ui/mute-button 2.21 kB
/ui/pip-button 2.20 kB
/ui/play-button 2.22 kB
/ui/playback-rate 2.02 kB
/ui/playback-rate-button 2.21 kB
/ui/popover 2.62 kB
/ui/poster 2.05 kB
/ui/quality 2.14 kB
/ui/seek-button 2.21 kB
/ui/seek-indicator 2.11 kB
/ui/slider 2.28 kB
/ui/status-announcer 2.09 kB
/ui/status-indicator 2.05 kB
/ui/thumbnail 2.03 kB
/ui/time 2.01 kB
/ui/time-slider 2.21 kB
/ui/tooltip 2.54 kB
/ui/volume-indicator 2.10 kB
/ui/volume-slider 2.24 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (14)
Entry Initial
. 9.40 kB
/dom 17.52 kB
/dom/media/custom-media-element 2.09 kB
/dom/media/dash 208.96 kB
/dom/media/google-cast 4.04 kB
/dom/media/hls-js 136.00 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.07 kB
/dom/media/simple-hls 16.49 kB
/dom/media/simple-hls-audio-only 14.92 kB
/dom/media/vimeo 9.86 kB
/media/predicate 563 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

@mihar-22 mihar-22 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ran through a variety of pages and everything looks pretty good. Not seeing anything concerning here. Thanks for the upgrade!

…-per2ya

# Conflicts:
#	site/src/utils/remarkConditionalHeadings.js

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 741a0d0. Configure here.

if (frameworks) heading.frameworks = frameworks;
if (styles) heading.styles = styles;

headings.push(heading);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Missing TOC slug collision handling

Medium Severity

The removed remark plugin reserved slugs when API reference headings were injected and re-slugged prose headings on collision. The Sätteri plugin always uses slugger.slug(text) with no reserved set, so injected entries like props can share a slug with a later markdown heading. TableOfContents keys rows by heading.slug, which duplicates keys and TOC anchors.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 741a0d0. Configure here.

@decepulis decepulis merged commit fb50e33 into main Jul 2, 2026
26 checks passed
@decepulis decepulis deleted the claude/dreamy-lamport-per2ya branch July 2, 2026 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants