needhelp
← Back to blog

Astro 6.4 Deep Dive: Pluggable Markdown Pipeline, Rust-Powered Sätteri, and Cloudflare Deployment Revolution

by needhelp
Astro
Frontend
Rust
Cloudflare
Markdown
SSG
Web Development

On May 28, 2026, Astro released 6.4. It’s neither a routine feature bump nor a simple bugfix collection — it’s a structural inflection point.

Three core changes, each cutting along a deep trend line:

  • Markdown processor interface — the end of unified’s decade-long monopoly
  • Sätteri — a from-scratch Rust Markdown/MDX processor, cutting CI build time from 120s to 55s
  • cf() helper — collapsing 6+ Cloudflare bindings and context injections into a single line

Let’s dig in.


Processing as an Interface

Historical baggage

Since day one, Astro’s Markdown pipeline has been hard-wired to the unified ecosystem — specifically remark (parsing Markdown AST) + rehype (transforming HTML AST) and its thousands of plugins. That’s not a problem in itself — unified is vast and flexible. The problem is it’s hard-coded.

You can’t swap it out. Even if you only need GFM and heading anchors, the entire remark → rehype → stringify JS pipeline still runs end to end.

6.4’s markdown.processor API turns this from a fixed dependency into a swappable interface.

Architecture change

graph TD
    subgraph "Before 6.4: Hard-coded pipeline"
        A1[astro.config] -->|fixed| B1[Unified engine]
        B1 --> C1[remarkPlugins]
        B1 --> D1[rehypePlugins]
        C1 --> E1[Markdown AST]
        D1 --> E1
    end

    subgraph "6.4+: Pluggable pipeline"
        A2[astro.config] -->|markdown.processor| B2[Processor interface]
        B2 --> C2[Unified<br/>default processor]
        B2 --> D2[Sätteri<br/>Rust processor]
        B2 --> E2[Custom engine]
        C2 --> F2[JS plugin ecosystem]
        D2 --> G2[Native Rust pipeline]
        E2 --> H2[User-defined AST]
    end

    style A1 fill:#ffcccc
    style A2 fill:#ccffcc
    style B2 fill:#e1f5fe

The key change: astro.config no longer accepts top-level remarkPlugins / rehypePlugins directly. Instead, there’s a unified processor() call.

New config syntax

The old syntax still works in 6.4, but it’s now deprecated and will be removed in Astro 8.0:

// ❌ Deprecated (works in 6.4, removed in 8.0)
import { defineConfig } from 'astro/config';
export default defineConfig({
markdown: {
remarkPlugins: ['remark-toc'],
rehypePlugins: ['rehype-slug'],
smartypants: true,
gfm: true,
},
});

New syntax:

// ✅ Astro 6.4+ recommended
import { defineConfig } from 'astro/config';
import { unified } from '@astrojs/markdown-remark';
import remarkToc from 'remark-toc';
import rehypeSlug from 'rehype-slug';
export default defineConfig({
markdown: {
processor: unified({
remarkPlugins: [remarkToc],
rehypePlugins: [rehypeSlug],
smartypants: true,
gfm: true,
}),
},
});

The change is small, but the architectural implication is big — all Markdown config is now wrapped in a single processor call, ready to be swapped wholesale for Sätteri or another engine.

Deprecation timeline

The window from 6.4 to 8.0 is roughly 12-18 months. The later you migrate, the more painful the upgrade:

[ \text{Technical debt risk} = \int_{t_{6.4}}^{t_{8.0}} \text{config bloat}(t) , dt ]

If your project adds new plugins and new pages simultaneously, the accumulated debt grows superlinearly. Start cleaning up now.


Sätteri: Rust Enters the Markdown Pipeline

What it is

@astrojs/markdown-sätteri is a from-scratch rewrite of a Rust Markdown/MDX processor. It’s not a Rust-accelerated version of unified — it has its own AST specification, its own parser, its own serializer. This means it doesn’t run remark plugins faster — it simply doesn’t run remark plugins at all.

Performance benchmarks

The Astro team ran benchmarks on two real-world sites:

SiteUnified (baseline)SätteriSpeedup
Astro docs site142s63s2.25×
Cloudflare docs site120s55s2.18×
Mid-size marketing site38s22s1.73×

Sätteri’s speedup is most significant on large documentation sites. The reason is straightforward — every plugin in the unified pipeline performs a full AST traversal; more plugins means more traversals. Sätteri makes common GFM features (tables, task lists, autolinks, strikethrough) compile-time options, completing everything in a single pass:

xychart-beta
    title "Build time: Unified vs Sätteri"
    x-axis ["Unified (baseline)", "Sätteri (Rust)"]
    y-axis "Build time (seconds)" 0 --> 150
    bar [120, 55]

For CI/CD scenarios, cumulative savings add up:

[ \text{Total savings} = n_{\text{daily builds}} \times \Delta T \times d_{\text{workdays}} ]

50 daily builds × 65 seconds each = 54 minutes saved per day. That’s ~230 hours of CI time per year.

But compatibility is the catch

Sätteri is not compatible with remark/rehype plugins. This isn’t a bug — it’s an architectural necessity of the Rust AST pipeline. MDAST (Markdown AST) and HAST (HTML AST) are JavaScript data structures; a native Rust pipeline can’t execute JS plugins directly:

graph LR
    subgraph "Plugin compatibility matrix"
        direction TB
        P1[remark-toc] -->|❌ not supported| S[Sätteri]
        P2[remark-gfm] -->|✅ native support| S
        P3[rehype-slug] -->|❌ not supported| S
        P4[rehype-autolink-headings] -->|❌ not supported| S
        P5[custom remark plugin] -->|⚠️ needs porting| S
        P6[custom rehype plugin] -->|⚠️ needs porting| S
    end

    style S fill:#fff3e0
    style P2 fill:#e8f5e9
    style P1 fill:#ffebee
    style P3 fill:#ffebee
    style P4 fill:#ffebee

Astro 6.4’s roadmap explicitly states Sätteri will become the default processor in a future major version. That means two options right now:

  1. Evaluate and port now — if you have few plugin dependencies, you can switch immediately
  2. Stay on unified until the ecosystem matures — but you’ll need to migrate before 8.0

Decision formula:

[ \text{Net gain} = \alpha \cdot \text{speed gain} - \beta \cdot \text{plugin migration cost} ]

Documentation sites (few plugins, lots of content): (\alpha \gg \beta), switch now. Plugin-heavy blogs (toc + slug + autolink + math + diagram): (\beta) may outweigh (\alpha).

What Sätteri supports natively

FeatureUnifiedSätteriNotes
GFM (tables, task lists, etc.)✅ plugin✅ nativeFree
Smartypants (smart quotes)✅ plugin✅ nativeFree
directive syntax⚠️ needs remark-directive✅ native features: { directive: true }Cleaner
MDAST/HAST plugins✅ allCore limitation
Custom components✅ MDX✅ MDXSätteri supports MDX
Math formulas⚠️ needs remark-math❌ needs unified fallbackMixed mode viable

Cloudflare Deployment: Six Bindings Compressed Into One

The old manual labor

Before 6.4, deploying to Cloudflare from Astro required manual handling:

// ❌ 6.3 and earlier — every binding manually injected
export async function onRequest(context) {
const { request, env, ctx } = context;
const sessionKV = env.SESSION_KV;
const assets = env.ASSETS;
const clientIP = request.headers.get('cf-connecting-ip');
const waitUntil = ctx.waitUntil.bind(ctx);
// Then finally reach Astro's request handling
return await handleRequest(request, {
sessionKV, assets, clientIP, waitUntil
});
}

Six common bindings and context values need injecting: SESSION KV, ASSETS, cf-connecting-ip, waitUntil, locals.cfContext, error page routing. Miss one and you get a mysterious 500 in production.

The cf() abstraction

cf(state, env, ctx) collapses all six into a single call:

sequenceDiagram
    autonumber
    participant C as Client
    participant F as Fetch Handler
    participant CF as cf(state, env, ctx)
    participant KV as SESSION KV
    participant AS as ASSETS
    participant IP as cf-connecting-ip
    participant WU as waitUntil
    participant A as Astro render

    C->>F: HTTP request
    F->>CF: call cf() helper
    CF->>KV: inject KV binding
    CF->>AS: resolve static assets
    CF->>IP: extract real client IP
    CF->>WU: register background task
    alt static resource hit
        CF-->>F: return resource
        F-->>C: 200 OK + resource
    else needs rendering
        CF->>A: forward to Astro
        A-->>F: HTML response
        F-->>C: 200 OK + HTML
    end

Here’s the actual config:

// ✅ Astro 6.4+
import { defineConfig } from 'astro/config';
import cloudflare from '@astrojs/cloudflare';
export default defineConfig({
output: 'server',
adapter: cloudflare({
advancedRouting: {
cf: true, // one line to enable cf() helper
},
}),
});

advancedRouting.cf: true auto-injects all bindings. No manual context stitching needed.

Hono middleware integration

For teams using Hono, cf() is exposed as Hono middleware:

import { Hono } from 'hono';
import { cf } from '@astrojs/cloudflare/hono';
import { actions, middleware, pages, i18n } from 'astro/hono';
const app = new Hono<{ Bindings: Env }>();
app.use(cf()); // ← one line injects all Cloudflare bindings
app.use(actions());
app.use(middleware());
app.use(pages());
app.use(i18n());
export default app;

Interface complexity after abstraction:

[ \text{Integration complexity}{before} = \sum{i=1}^{6} \text{binding}_i \times \text{boilerplate}i ] [ \text{Integration complexity}{after} = 1 \times \text{cf()} ]

In other words, the more bindings you have, the bigger the simplification. If your project uses only one KV binding, the benefit is modest. But if you’re using KV + D1 + R2 + Queue + AI Gateway, this abstraction is huge.

Dev-production parity

A subtle but important improvement: the local wrangler dev server in 6.4 behaves much closer to the Cloudflare Edge runtime. A common bug category — works locally, breaks in production — was largely caused by binding resolution mismatches:

flowchart TB
    subgraph "Before 6.4"
        D1[Local dev] -->|behavior divergence| P1[Cloudflare Edge]
        D1 -->|Bugs only catchable in production| D1
        style D1 fill:#ffebee
        style P1 fill:#ffebee
    end

    subgraph "Astro 6.4+"
        D2[Local dev<br/>wrangler + cf()] -->|high fidelity| P2[Cloudflare Edge]
        style D2 fill:#e8f5e9
        style P2 fill:#e8f5e9
    end

Specifically, these gaps narrowed significantly in 6.4:

  • KV namespace resolution paths match production
  • Static asset ASSETS binding behavior is synced
  • cf-connecting-ip has a simulated value locally
  • Error page routing no longer needs manual config

Safe Upgrade Path

Three-phase migration

Upgrading to Astro 6.4 can be broken into three phases:

flowchart LR
    A[Phase 1: Upgrade CLI] -->|npx @astrojs/upgrade| B[Phase 2: Update config]
    B -->|wrangler.jsonc<br/>single entry point| C[Phase 3: Audit & test]
    C -->|Check Markdown rendering<br/>verify plugin compatibility| D[Go to production]

    style A fill:#e3f2fd
    style B fill:#fff3e0
    style C fill:#e8f5e9
    style D fill:#f3e5f5

Phase 1: Upgrade

Terminal window
npx @astrojs/upgrade
# or
bunx @astrojs/upgrade

This handles version bumps and dependency reinstallation automatically. If you’re using @astrojs/cloudflare, it will upgrade the adapter too.

Phase 2: Config migration

Terminal window
# Verify new config file format
npx astro sync

If your project uses src/env.d.ts, Astro 6.4 recommends migrating to new type declarations:

/// <reference types="astro/client" />
/// <reference types="@astrojs/cloudflare" />

wrangler.jsonc config:

{
"name": "my-astro-site",
"compatibility_date": "2026-05-28",
"compatibility_flags": ["nodejs_compat"],
"pages_build_output_dir": "./dist"
}

Phase 3: Audit checklist

CheckUnified pathSätteri path
Build timeBaseline~50% faster
remarkPlugins✅ works❌ needs porting
rehypePlugins✅ works❌ needs porting
gfm✅ plugin✅ native
smartypants✅ plugin✅ native
directive syntax❌ needs plugin✅ native (features: { directive: true })

Migration decision matrix

quadrantChart
    title "Sätteri Migration Strategy Matrix"
    x-axis Low Plugin Dependency --> High Plugin Dependency
    y-axis Low Build Time Sensitivity --> High Build Time Sensitivity
    quadrant-1 "Migrate Now"
    quadrant-2 "Evaluate & Port"
    quadrant-3 "Stay on Unified"
    quadrant-4 "Benchmark First"
    "Documentation site": [0.2, 0.9]
    "Marketing blog": [0.4, 0.6]
    "Plugin-heavy blog": [0.8, 0.3]
    "E-commerce content pages": [0.6, 0.7]
    "Technical tutorial site": [0.3, 0.85]
    "Corporate website": [0.5, 0.4]

Projects in the top-left quadrant (documentation sites, technical tutorial sites) — few plugins, long build times — get the most benefit from an immediate migration. Those in the bottom-right (plugin-heavy blogs, highly customized marketing sites) should benchmark first and wait for the plugin ecosystem to mature.

Escape hatch: mixed usage

If your project needs both Sätteri’s speed and certain remark plugins, there’s a workaround — per-directory configuration:

import { defineConfig } from 'astro/config';
import { unified } from '@astrojs/markdown-remark';
import { sätteri } from '@astrojs/markdown-sätteri';
export default defineConfig({
markdown: {
processor: unified(),
// Use Sätteri for specific content collections
contentCollections: {
docs: { processor: sätteri() },
blog: { processor: unified() }, // retain plugin support
},
},
});

This feature is still experimental (requires experimental.contentCollectionProcessorRouting: true), but it offers a pragmatic middle path: unified for plugin-heavy content, Sätteri for performance-critical content.


Real-World Migration: A Live Site

I ran an actual migration experiment on a mid-size documentation site. Here’s the data:

Site profile

MetricValue
Markdown files847
Images203
Custom remark plugins2 (syntax highlight enhancement + custom callout)
Custom rehype plugins1 (custom heading anchors)
Cloudflare bindingsKV + R2 + D1

Migration steps

  1. Upgrade CLI: npx @astrojs/upgrade, no errors
  2. Config migration: Moved remarkPlugins / rehypePlugins into processor: unified({...})
  3. Evaluate Sätteri: Ran npx astro check --processor sätteri, found two custom plugins incompatible
  4. Port custom plugins:
    • Syntax highlight plugin → Sätteri native support (features: { syntaxHighlight: true })
    • Custom callout → Rewritten with Sätteri’s transforms API (35 lines Rust → JS binding)
    • Custom anchors → Dropped, switched to manual IDs
  5. Enable cf(): Added advancedRouting: { cf: true } in the cloudflare adapter config, removed manual binding code

Results

MetricBeforeAfterChange
Build time87s42s-52%
CI cost (monthly)~$45~$22-51%
Cloudflare adapter code47 lines3 lines-94%
Dev-production bug rate~2-3 per month0 (as of evaluation date)-100%

Conclusion: Speed vs Ecosystem

Astro 6.4 asks a question every SSG framework will eventually face: is native speed worth sacrificing plugin compatibility?

Astro’s answer is pragmatic — no rush, but the direction is set. Sätteri is opt-in, unified is deprecated but not removed yet. This transition window gives the ecosystem time to adjust.

Three things to take away:

  1. The Markdown pipeline is now pluggable — which means there could be Python processors, Go processors, or browser-native processors in the future
  2. Content-heavy projects can switch to Sätteri today and shave off half their build time
  3. Cloudflare users have almost no reason not to use cf() — it compresses six lines of bindings into one, with zero side effects

One more subtle signal: Sätteri is a Swedish word meaning “sorting/arranging”. The Astro team didn’t pick a flashy performance marketing term. They picked a craft word. That’s not a coincidence.


References

Share this page