Astro 6.4 Makes You Pick Where Complexity Lives — Markdown Processor or Cloudflare Entrypoint
Sätteri can shave a minute off docs builds — but only if you accept a different plugin universe. The `cf()` helper only matters if you already own the fetch handler.
6 min read · May 29, 2026
#JavaScript #WebDevelopment #Astro #StaticSiteGenerator #CloudflareWorkers

Here's an uncomfortable truth about minor framework releases: the changelog reads like three bullet points, and the migration work hides in the assumptions. Astro 6.4 ships a pluggable markdown.processor, a Rust-based Sätteri option that the team says cut more than a minute off large docs builds, and cf() helpers for Cloudflare's experimental advanced routing. None of that is cosmetic. Each item moves complexity — from scattered config keys into an explicit processor object, from remark/rehype npm plugins into MDAST/HAST ports, from hand-rolled Worker wiring into adapter middleware.
If you maintain a forty-page marketing site, you can skim this and upgrade on a Friday. If you maintain Astro's docs-scale Markdown corpus or a Cloudflare Worker with a custom fetch entrypoint, 6.4 is a decision release, not a patch note.
Three features, two surfaces
Build time and runtime rarely share a release note, but Astro 6.4 ties them together anyway.
On the build side, Markdown stops being "whatever unified does behind markdown.remarkPlugins." It becomes markdown.processor: unified({...}) or markdown.processor: satteri({...}) — a single swap point for the entire .md / .mdx compile path Astro 6.4 release.
On the runtime side, if you adopted experimental advanced routing in 6.3, @astrojs/cloudflare now exports cf() so you don't re-implement SESSION KV injection, ASSETS serving, locals.cfContext, client IP from cf-connecting-ip, waitUntil, and prerendered error pages every time you write a custom handler Astro 6.4 release.
Classic adapter deployments without a owned entrypoint? The Markdown half still applies. The cf() half is optional noise until you opt into advanced routing.
That's the split most teams miss — they celebrate Sätteri speed while still hand-wiring bindings they don't need yet, or they cargo-cult cf() on a site that never left adapter-managed routing.
markdown.processor — swappable Markdown, not more knobs
Astro's Markdown story has always been unified under the hood — remark, rehype, thousands of plugins. 6.4 makes that explicit: the default processor is still unified(), and existing projects keep working without edits Astro 6.4 release.
What actually changes is where you configure plugins. The top-level keys markdown.remarkPlugins, markdown.rehypePlugins, markdown.remarkRehype, markdown.gfm, and markdown.smartypants still work today, but they're deprecated in favor of passing options into unified({...}), with removal planned for Astro 8.0 Astro 6.4 release. Not bikeshedding. Astro is drawing a line between framework defaults and the pipeline you own.
import { defineConfig } from 'astro/config';
import { unified } from '@astrojs/markdown-remark';
import remarkToc from 'remark-toc';
export default defineConfig({
markdown: {
processor: unified({
remarkPlugins: [remarkToc],
}),
},
});If your astro.config.mjs still lists remark plugins at the top level, you're on borrowed time. Move them under unified({}) now and you avoid a forced migration when 8.0 deletes the old surface.
The Processor Fork — unified() vs Sätteri()
The second processor is the headline: @astrojs/markdown-satteri, wrapping Sätteri — Markdown and MDX compiled in Rust Astro 6.4 release. Astro's own testing reports shaving over a minute off build times for large docs properties, including Astro's docs and Cloudflare's docs site Astro 6.4 release. Publisher benchmark, not your CI guarantee. The order of magnitude still matters. When Markdown compilation dominates astro build, processor choice is a build-SLO lever.
Install and flip:
import { defineConfig } from 'astro/config';
import { satteri } from '@astrojs/markdown-satteri';
export default defineConfig({
markdown: {
processor: satteri({
features: { directive: true },
}),
},
});Speed without remark is a different compiler, not a faster one.
The catch release posts underplay: Sätteri does not run remark or rehype plugins Astro 6.4 release. Switching processors replaces remark/rehype for both .md and .mdx unless you override the MDX integration separately Markdown in Astro. Your remark-toc, rehype-slug, and bespoke remark plugins that mutate file.data.astro.frontmatter simply stop applying. You either stay on unified(), port to Sätteri's mdastPlugins / hastPlugins, or split processors — Sätteri for .md, unified() for .mdx via mdx({ processor: unified({...}) }) when you need both Markdown in Astro.
Worked fork: who flips the switch?
Picture a Starlight docs monorepo — two thousand Markdown files, remark-toc for auto-TOCs, no exotic rehype transforms:
- Stay on
unified()if TOC generation or a single custom remark plugin is load-bearing. Speed is worthless if headings stop matching your sidebar logic. - Try Sätteri if builds are Markdown-bound and your plugin list is empty or port-able. Measure
astro buildbefore and after; you're optimizing CI minutes, not local dev comfort. - Split processors if MDX pages carry interactive components with remark dependencies while
.mdleaves are bulk content — configuremdx({ processor: unified({ remarkPlugins: [...] }) })andextendMarkdownConfig: falsewhen you need a hard boundary Markdown in Astro.
Sätteri is positioned as a future default in the release narrative Native Markdown RFC. That makes the inventory step urgent: list every remark/rehype plugin in config before someone flips the default in a major bump.
One more unified-only footgun: plugins that assume Astro-injected heading IDs must run after rehypeHeadingIds in the rehype stack — order matters when you consolidate under unified({ rehypePlugins: [...] }) Markdown in Astro. Sätteri users port that logic to hastPlugins instead of guessing.
cf() — what you're not wiring by hand
Advanced routing landed in 6.3 — Hono middleware, custom fetch handlers, you own the entrypoint Astro 6.3 release. 6.4's cf() is the Cloudflare adapter catching up to that model.
Custom fetch handler — import from @astrojs/cloudflare/fetch, run cf(state, env, ctx) before astro(state) so static assets and bindings exist when Astro handles the request:
import { astro, FetchState } from 'astro/fetch';
import { cf } from '@astrojs/cloudflare/fetch';
export default {
async fetch(request, env, ctx) {
const state = new FetchState(request);
const asset = await cf(state, env, ctx);
if (asset) return asset;
return astro(state);
},
};Hono — app.use(cf()) from @astrojs/cloudflare/hono before actions(), middleware(), pages(), and i18n() Astro 6.4 release.
This is not Astro.request.cf — that's request metadata (country, TLS fingerprint) on deployed routes per the Cloudflare integration guide. cf() is plumbing: SESSION KV for sessions, ASSETS for static output, Worker context hooks advanced routing expects you to wire on every cold start.
If you're still on adapter-managed routing with an auto-generated Wrangler file, skip cf() entirely. You're not failing to adopt 6.4 — you simply haven't bought the entrypoint complexity advanced routing requires.
The failure mode on the Worker side is subtler: teams add cf() but still read client IP from headers they set in dev, then wonder why production locals diverges. cf() exists to align dev and deploy binding shapes — use it as the single place SESSION and ASSETS get injected, not as decoration around an otherwise unchanged handler.
Wrangler optionalization from earlier 6.x releases still holds: simple sites without custom KV/D1 declarations can delete boilerplate wrangler.jsonc and let Astro generate defaults Cloudflare integration guide. Advanced routing is the opposite trade — more control, more entrypoint code, and cf() is the adapter admitting that hand-rolled wiring was getting copy-pasted.
Migration checklist (do this in order)
- Inventory remark/rehype plugins — grep
astro.configand any*.mjsplugin files. If the list is non-empty, default staysunified()until each plugin has a Sätteri port or replacement. - Move deprecated top-level markdown keys into
unified({...})— even if you never touch Sätteri, Astro 8.0 won't wait Astro 6.4 release. - Measure before switching processors — one timed
astro buildon main, one on a branch withsatteri(). Docs-scale repos only; small sites won't see the minute-level win. - Gate
cf()on routing mode — advanced routing enabled? Addcf()to fetch or Hono. Otherwise ignore until you migrate. - Upgrade —
npx @astrojs/upgradeor your package manager'sastro@latestpath Astro 6.4 release.
The actual takeaway
Astro 6.4 doesn't give you three free wins — it asks whether your pain is compile-time Markdown or runtime Worker wiring, then puts the switch in config where you can't pretend the old world still exists.
Your move: pick the surface that hurts, ignore the other until it does.
More in Build
Your Cache Hit Rate Looked Fine Until the Hour Mark
Redis did its job on every miss — your application just sent two hundred loaders to Postgres at once.
6 min · June 15, 2026
PHP Turns 31 — The History That Matters Is the Elephant
The version timeline is everywhere. The resume logger, the Usenet post, and the sideways doodle that became a mascot — that's the birthday story worth telling.
6 min · June 10, 2026
BullMQ Background Jobs That Survive Production
Retries with an error taxonomy, deduplication that survives cleanup, and a dead-letter queue someone actually inspects — not a five-minute `Queue` demo.
6 min · June 6, 2026