astro-html-kit
v0.3.0
Published
Astro integration and middleware to clean up your HTML.
Maintainers
Readme
astro-html-kit
Astro integration and middleware to clean up your HTML.
[!WARNING]
This project is under development. It should not be considered suitable for general use until a 1.0 release.
Overview
Astro renders clean HTML, but the final output often needs small fixes that are tedious to handle per-page: stripping .html suffixes from links, annotating external URLs, deduplicating IDs across repeated content, or trimming trailing whitespace.
astro-html-kit runs these transforms as Astro middleware. The point of collecting them in one place is efficiency: every HTML response is parsed into a DOM once, all handlers operate on that single parsed document, and it's serialized back to HTML once. No matter how many transforms you enable, the cost is one parse/serialize round-trip — not one per transform. String-level handlers then run on the serialized output. Even with no transforms enabled, the linkedom round-trip normalizes the HTML.
Some of these transforms (like fixing numeric IDs or deduplicating headings) could be implemented as rehype plugins, which would be more efficient since they'd run during Markdown/MDX compilation rather than on the final HTML. The trade-off is that rehype only covers the Markdown pipeline. Running as middleware means every Astro-rendered page is covered — .astro templates, framework components, and Markdown/MDX alike.
Built-in transforms:
annotateExternalLinks— Adddata-external-linkandrel="noopener noreferrer"to links pointing outside your site.addLinkPrefix— Prefix/_astro/asset paths inhref,src, andsrcsetattributes withBASE_URL.stripLinkSuffix— Remove.htmlfrom internal link hrefs (including before?queryand#hash).fixNumericIds— Prefix IDs that start with a digit (e.g.id="2024-updates"becomesid="id-2024-updates") to avoid issues with CSS selectors and JavaScript APIs. Also rewrites<a href="#…">fragments along withfor,headers, and the aria-* attributes that reference IDs.deduplicateIds— Append-2,-3, etc. to duplicate IDs on the page, matchinggithub-sluggerbehavior but page-wide.unwrapEmptyLinks— Replace<a>elements with empty, whitespace-only, or missinghrefwith their children.trimTrailingWhitespace— Trim trailing whitespace from each line of the HTML output.
All transforms are disabled by default and must be explicitly enabled.
Two ways to use astro-html-kit:
- As middleware (
astro-html-kit/middleware) — Full control over the handler pipeline, with support for custom DOM and string handlers. - As an integration (
astro-html-kit) — Zero-config convenience. Registers middleware automatically viaaddMiddleware. No custom handlers (functions can't be serialized), but all built-in transforms are available.
Getting started
Prerequisites
An Astro 6+ project.
Installation
pnpm add astro-html-kitIntegration setup
The simplest way to use astro-html-kit is as an Astro integration:
// In astro.config.ts
import htmlKit from 'astro-html-kit'
import { defineConfig } from 'astro/config'
export default defineConfig({
integrations: [
htmlKit({
annotateExternalLinks: true,
deduplicateIds: true,
fixNumericIds: true,
stripLinkSuffix: true,
trimTrailingWhitespace: true,
}),
],
site: 'https://example.com',
})Middleware setup
For custom handlers or finer control, use the middleware export directly in your src/middleware.ts:
// In src/middleware.ts
import { htmlKit } from 'astro-html-kit/middleware'
export const onRequest = htmlKit({
annotateExternalLinks: true,
stripLinkSuffix: true,
})Compose with other middleware using Astro's sequence:
// In src/middleware.ts
import { htmlKit } from 'astro-html-kit/middleware'
import { sequence } from 'astro:middleware'
export const onRequest = sequence(htmlKit({ annotateExternalLinks: true }), myOtherMiddleware)Configuration
All options apply to both the integration and middleware configs, except customDomHandler and customStringHandler which are middleware-only (functions can't be serialized through the integration's virtual module).
| Option | Type | Default | Description |
| ------------------------ | ------------------------------------------------------ | ------- | --------------------------------------------------------------------------------------------------------------- |
| addLinkPrefix | boolean | false | Prefix /_astro/ asset paths in href, src, and srcset with BASE_URL (read from import.meta.env). |
| annotateExternalLinks | boolean | false | Add data-external-link and rel to external links. |
| deduplicateIds | boolean | false | Append -2, -3, etc. to duplicate IDs. |
| fixNumericIds | boolean \| string | false | Prefix numeric IDs. true uses id-, a string sets a custom prefix (the trailing - is added automatically). |
| stripLinkSuffix | boolean | false | Remove .html from internal link hrefs. Requires site in config. |
| trimTrailingWhitespace | boolean | false | Trim trailing whitespace from each line of the HTML output. |
| unwrapEmptyLinks | boolean | false | Replace <a> elements with empty, whitespace-only, or missing href with their children. |
| customDomHandler | DomMiddlewareHandler \| DomMiddlewareHandler[] | none | Custom DOM transform(s). Middleware only — function handlers can't be serialized through the integration. |
| customStringHandler | StringMiddlewareHandler \| StringMiddlewareHandler[] | none | Custom string transform(s). Middleware only — function handlers can't be serialized through the integration. |
Custom handlers
DOM handlers
DOM handlers receive an Astro APIContext and a linkedom Document, and return a Document. The document is shared across the whole sequence, so handlers may mutate the input directly and return it — no copy is made between handlers. They run before serialization.
// In src/middleware.ts
import { defineDomMiddleware, htmlKit } from 'astro-html-kit/middleware'
const addTimestamp = defineDomMiddleware((_context, document) => {
document.body.dataset.rendered = new Date().toISOString()
return document
})
export const onRequest = htmlKit({
customDomHandler: addTimestamp,
})String handlers
String handlers receive the serialized HTML string after all DOM handlers have run. They're useful for text-level transformations that don't need a DOM tree.
// In src/middleware.ts
import { defineStringMiddleware, htmlKit } from 'astro-html-kit/middleware'
const addComment = defineStringMiddleware(
(_context, html) => `<!-- generated by astro-html-kit -->\n${html}`,
)
export const onRequest = htmlKit({
customStringHandler: addComment,
trimTrailingWhitespace: true,
})Execution order
- Built-in DOM handlers, in this fixed order:
annotateExternalLinks,addLinkPrefix,stripLinkSuffix,fixNumericIds,deduplicateIds,unwrapEmptyLinks - Custom DOM handlers
- Serialization via linkedom
toString() - Built-in string handlers (
trimTrailingWhitespace) - Custom string handlers
How it works
astro-html-kit middleware intercepts Astro's HTML responses after rendering. Only responses with content-type: text/html are processed — JSON, CSS, and other content types pass through untouched.
The HTML is parsed into a DOM via linkedom, a lightweight server-side DOM implementation. All DOM handlers share the same parsed document (one parse/serialize cycle, regardless of how many handlers run). After DOM transforms complete, the document is serialized back to HTML and passed through any string handlers.
This architecture means transforms apply to all Astro-rendered HTML regardless of source: .astro pages, Markdown, MDX, and framework components.
Exports
astro-html-kit (integration)
| Export | Description |
| -------------------------------- | --------------------------------------------------- |
| default (function) | htmlKit(config?) — Returns an AstroIntegration. |
| HtmlKitConfig (type) | Integration config type. |
| HtmlKitMiddlewareConfig (type) | Full middleware config type. |
| DomMiddlewareHandler (type) | DOM handler function signature. |
astro-html-kit/middleware
| Export | Description |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| htmlKit | Middleware factory. Returns an Astro MiddlewareHandler. |
| defineDomMiddleware | Identity function for typing DOM handlers. |
| defineStringMiddleware | Identity function for typing string handlers. |
| defineDomMiddlewareAsMiddleware | Wrap a single DOM handler as a standalone MiddlewareHandler. Useful when registering one transform via Astro's sequence() without invoking the full htmlKit pipeline. |
| domSequence | Lower-level API: compose DOM and string handlers into a MiddlewareHandler. |
| HtmlKitMiddlewareConfig (type) | Middleware config type. |
| DomMiddlewareHandler (type) | (context: APIContext, document: Document) => Document \| Promise<Document> |
| StringMiddlewareHandler (type) | (context: APIContext, html: string) => string \| Promise<string> |
| DomSequenceOptions (type) | Options for domSequence. |
Maintainers
Contributing
Issues are welcome and appreciated.
Please open an issue to discuss changes before submitting a pull request. Unsolicited PRs (especially AI-generated ones) are unlikely to be merged.
This repository uses @kitschpatrol/shared-config (via its ksc CLI) for linting and formatting, plus MDAT for readme placeholder expansion.
