We Upgraded Starwind Pro to Astro 7 and Cut the Full Build Pipeline by 64.7%
Starwind Pro's upgrade from Astro 6 to Astro 7 cut its full production pipeline from 3:23 to 1:12. This post covers the changes, the final HTML-minification replacement, and how we verified the app.


Moving Starwind Pro from Astro 6 to Astro 7 cut our local full production build pipeline from 3:23 to 1:12: a 64.7% reduction and a saving of over two minutes and per build!
That figure covers the whole production pipeline: the marketing site, docs, accounts, component browser, live previews, search index, and installable component registry. It is the result of the Starwind Pro upgrade project, including the changes with Astro 7, the Markdown pipeline, build dependencies, and HTML finishing step.
What changed
| Change | Why it mattered |
|---|---|
| Astro 7, Vite 8, and Cloudflare adapter v14 | The framework, bundler, and Worker adapter needed compatible versions. |
| A repository-owned MDX auto-import plugin | Docs authors could keep using the same components without adding imports to every page. |
| A replacement for PlayForm | We kept final HTML compression without its old dependency chain or redundant JavaScript minification. |
Prepare the Astro 6 app
Astro 7 moves Markdown and MDX to Sätteri. Our docs use MDX and make ten common components available without imports on every page. The existing auto-import integration did not support Sätteri, so we replaced it with a small repository-owned plugin. It detects the configured components used in a document, respects explicit imports, and inserts only the imports that are needed. Documentation authors keep the same workflow, while the implementation now lives in code we own and test.
Vite 8 resolves the shared stylesheet aliases used in five Astro components differently. We changed those imports to relative paths before the framework upgrade, keeping the styles unchanged and removing that resolver issue from the migration.
Before the upgrade, PlayForm ran at the end of every production build. It minified generated HTML and browser JavaScript, but also brought older Astro and Vite packages into the dependency graph. We removed its duplicate JavaScript-minification work and later replaced its HTML step with a smaller, faster custom integration.
Those preparation changes brought the build to 2:27 before Astro itself changed, about 27% below the original measurement.
Upgrade Astro, Vite, and Cloudflare together
Astro 7 changes the compiler, Markdown processor, and Vite version. We updated the framework with Vite 8, the Cloudflare adapter v14, the Astro MDX and React integrations, Expressive Code, and the Sätteri bridge.
We also set Astro’s compressHTML option explicitly to true. Astro 7 has a new default whitespace mode; this setting preserves the earlier HTML-aware behaviour for existing content. Astro documents the difference in its Astro 7 whitespace migration guide.
We moved from Cloudflare adapter v13 to v14 and removed configuration options that only the older adapter understood. Some account and registry routes run on Cloudflare Workers when a user requests them. A static build creates pages ahead of time; it cannot test those request-time routes. We started the app and made real requests through the public site, auth, error, registry, and preview paths.
Add full HTML minification
The Astro 7 build reduced build time while producing larger HTML than the old PlayForm output. Astro’s compressHTML setting handles whitespace safely; it does not try to remove every ordinary comment and redundant piece of markup.
CAUTION
Astro server-island comments are part of the rendered application, not disposable markup. A generic HTML minifier can remove them, so the custom integration protects and verifies them before writing the final output.
We tested PlayForm in HTML-only mode against a replacement built with minify-html. On the same 586 generated HTML files, PlayForm had a 34.24-second median. The replacement had a 2.77-second median and produced slightly smaller output.
The replacement is a small, repository-owned Astro integration that runs after Astro writes the HTML files. Before minifying, it protects Astro’s server-island markers and code samples whose whitespace must remain exact. After minifying, it verifies that the original content returned unchanged. The integration handles final HTML plus inline CSS and JavaScript; Vite continues to handle separately bundled client assets.
Full source: custom html-minify astro integration
import { readdir, readFile, writeFile } from "node:fs/promises";import { performance } from "node:perf_hooks";import { fileURLToPath } from "node:url";import path from "node:path";
import minifyHtml from "@minify-html/node";import type { AstroIntegration } from "astro";
const { minify } = minifyHtml;
const ASTRO_COMMENT_TOKEN_PREFIX = "{{STARWIND_PROTECTED_ASTRO_COMMENT_";const SOURCE_CODE_TOKEN_PREFIX = "{{STARWIND_PROTECTED_SOURCE_CODE_BLOCK_";const HTML_COMMENT_PATTERN = /<!--[\s\S]*?-->/g;const SOURCE_CODE_PATTERN = /<code\b(?=[^>]*\bdata-source-code(?:\s|=|>))[^>]*>[\s\S]*?<\/code\s*>/gi;const MINIFY_OPTIONS = { keep_closing_tags: true, keep_html_and_head_opening_tags: true, keep_input_type_text_attr: true, minify_css: true, minify_js: true, preserve_brace_template_syntax: true,} as const;
export interface HtmlMinifySummary { fileCount: number; beforeBytes: number; afterBytes: number; durationMs: number;}
function isAstroFrameworkComment(comment: string) { const body = comment.slice(4, -3).trim();
return /^\[if astro\]>/i.test(body) || /^astro(?::|$)/i.test(body);}
function countOccurrences(source: string, search: string) { return source.split(search).length - 1;}
function selectProtectionTokenPrefix(source: string, basePrefix: string) { let tokenPrefix = basePrefix; let suffix = 0;
while (source.includes(tokenPrefix)) { tokenPrefix = `${basePrefix}${suffix++}_`; }
return tokenPrefix;}
function protectAstroFrameworkComments(source: string) { const tokenPrefix = selectProtectionTokenPrefix( source, ASTRO_COMMENT_TOKEN_PREFIX, ); const comments: string[] = []; const protectedHtml = source.replace(HTML_COMMENT_PATTERN, (comment) => { if (!isAstroFrameworkComment(comment)) return comment;
const token = `${tokenPrefix}${comments.length}}}`; comments.push(comment); return token; });
return { comments, protectedHtml, tokenPrefix };}
function restoreAstroFrameworkComments( source: string, comments: readonly string[], tokenPrefix: string,) { let restoredHtml = source;
for (const [index, comment] of comments.entries()) { const token = `${tokenPrefix}${index}}}`; const tokenCount = countOccurrences(restoredHtml, token);
if (tokenCount !== 1) { throw new Error( `Rust HTML minification changed Astro framework comment token ${index}; expected it once, found ${tokenCount}.`, ); }
restoredHtml = restoredHtml.replace(token, comment); }
if (restoredHtml.includes(tokenPrefix)) { throw new Error( "Rust HTML minification left an unresolved Astro framework comment token.", ); }
const restoredComments = restoredHtml.match(HTML_COMMENT_PATTERN)?.filter(isAstroFrameworkComment) ?? []; if ( restoredComments.length !== comments.length || restoredComments.some((comment, index) => comment !== comments[index]) ) { throw new Error( "Rust HTML minification did not restore Astro framework comments exactly.", ); }
return restoredHtml;}
function protectSourceCodeBlocks(source: string) { const tokenPrefix = selectProtectionTokenPrefix( source, SOURCE_CODE_TOKEN_PREFIX, ); const blocks: string[] = []; const protectedHtml = source.replace(SOURCE_CODE_PATTERN, (block) => { const token = `${tokenPrefix}${blocks.length}}}`; blocks.push(block); return token; });
return { blocks, protectedHtml, tokenPrefix };}
function restoreSourceCodeBlocks( source: string, blocks: readonly string[], tokenPrefix: string,) { let restoredHtml = source;
for (const [index, block] of blocks.entries()) { const token = `${tokenPrefix}${index}}}`; const tokenCount = countOccurrences(restoredHtml, token);
if (tokenCount !== 1) { throw new Error( `Rust HTML minification changed source-code block token ${index}; expected it once, found ${tokenCount}.`, ); }
restoredHtml = restoredHtml.replace(token, block); }
if (restoredHtml.includes(tokenPrefix)) { throw new Error( "Rust HTML minification left an unresolved source-code block token.", ); }
return restoredHtml;}
async function findHtmlFiles(directory: string): Promise<string[]> { const entries = await readdir(directory, { withFileTypes: true }); const nestedFiles = await Promise.all( entries.map(async (entry) => { const entryPath = path.join(directory, entry.name);
if (entry.isDirectory()) return findHtmlFiles(entryPath); return entry.isFile() && entry.name.endsWith(".html") ? [entryPath] : []; }), );
return nestedFiles.flat().sort();}
/** * Minifies one generated HTML document while preserving Astro's framework control comments and * source-code examples exactly. */export function minifyHtmlDocument(source: string) { const { blocks, protectedHtml: sourceCodeProtectedHtml, tokenPrefix: sourceCodeTokenPrefix, } = protectSourceCodeBlocks(source); const { comments, protectedHtml, tokenPrefix: astroCommentTokenPrefix, } = protectAstroFrameworkComments(sourceCodeProtectedHtml); const minifiedHtml = minify( Buffer.from(protectedHtml), MINIFY_OPTIONS, ).toString("utf8"); const commentsRestoredHtml = restoreAstroFrameworkComments( minifiedHtml, comments, astroCommentTokenPrefix, );
return restoreSourceCodeBlocks( commentsRestoredHtml, blocks, sourceCodeTokenPrefix, );}
/** * Minifies every generated HTML file beneath an Astro output directory. */export async function minifyHtmlDirectory( directory: URL | string,): Promise<HtmlMinifySummary> { const directoryPath = directory instanceof URL ? fileURLToPath(directory) : directory; const files = await findHtmlFiles(directoryPath); const startedAt = performance.now(); let beforeBytes = 0; let afterBytes = 0;
await Promise.all( files.map(async (file) => { const source = await readFile(file, "utf8"); const minified = minifyHtmlDocument(source);
beforeBytes += Buffer.byteLength(source); afterBytes += Buffer.byteLength(minified); await writeFile(file, minified); }), );
return { fileCount: files.length, beforeBytes, afterBytes, durationMs: performance.now() - startedAt, };}
/** * Runs native Rust HTML, inline CSS, and inline JavaScript minification after Astro writes output. */export function htmlMinify(): AstroIntegration { return { name: "starwind-html-minify", hooks: { "astro:build:done": async ({ dir, logger }) => { const summary = await minifyHtmlDirectory(dir); const removedMiB = (summary.beforeBytes - summary.afterBytes) / 1024 / 1024;
logger.info( `Minified ${summary.fileCount} HTML files with Rust in ${(summary.durationMs / 1000).toFixed(2)}s, removing ${removedMiB.toFixed(2)} MiB.`, ); }, }, };}Starwind Pro upgrade results
| Production pipeline | Midpoint |
|---|---|
| Original Astro 6 application, including PlayForm | 3:23 |
| Prepared Astro 6 application | 2:27 |
| Current Astro 7 application, including the custom HTML pass | 1:12 |
INFO
The 64.7% result covers the full Starwind Pro upgrade: migration preparation, Astro 7 and Vite 8, the Cloudflare adapter update, and the final HTML pass.
The original and current values are midpoints of two clean builds of the same application workload. The prepared Astro 6 value is a single run, included because it shows that the preparation work cut about 27% from the build before we upgraded Astro itself. That stage combined the new MDX pipeline, CSS import fixes, and build-tool cleanup, so it is not a score for any one package.
The final Astro 7 build at 1:12 includes the HTML minification pass.
What we checked
Docs still render the necessary auto-imported components and exact code examples. The component browser, previews, sitemap, and search index remain available. Free registry responses remain public, Pro responses remain protected, and missing blocks return 404s. We also checked request-time Worker routes, including auth and error paths, rather than relying on the static build alone.
The resulting build
Starwind Pro now has an Astro 7 and Vite 8 stack, a current Cloudflare adapter, an owned MDX auto-import plugin, explicit whitespace behavior, and a faster HTML finishing pass with safeguards around the content it must preserve.
Thanks to the Astro team and the maintainers of the integrations we rely on! Astro 7 let us update Starwind Pro’s build without changing how the site or docs work, while cutting more than two minutes off our production-build time.