Korte versie

Astro genereert van zichzelf al schone, snel ladende HTML. Toch blijft er in de gebouwde bestanden winst liggen: commentaar, overtollige witruimte, CMS-attributen die niemand nodig heeft en koppen zonder anker-id. Met een klein Node-script dat na astro build draait, verwerk je elke HTML-variant in één keer: verkleinen met html-minifier-terser, id's toevoegen aan h2- tot h4-koppen en opruimen wat overblijft.

Astro genereert van zichzelf al schone, snel ladende HTML. Toch blijft er in de gebouwde bestanden winst liggen: commentaar, overtollige witruimte, CMS-attributen die niemand nodig heeft en koppen zonder anker-id. Met een klein Node-script dat na astro build draait, verwerk je elke HTML-variant in één keer: verkleinen met html-minifier-terser, id’s toevoegen aan h2- tot h4-koppen en opruimen wat overblijft. Dit is een los voorbeeld voor statische Astro-projecten; de huidige site gebruikt niet dit script. Hieronder lees je hoe je het opzet, wat het aantoonbaar oplevert en waar je moet opletten.

Aan de slag

Zorg voordat je kunt beginnen dat je de volgende vereisten op je systeem hebt geïnstalleerd:

  • Node.js (versie 22.12.0 of hoger; geen oneven Node-versies)
  • Astro (een ondersteunde versie)

Afhankelijkheden installeren

Ons script is afhankelijk van drie pakketten: ‘globby’, ‘html-minifier-terser’ en ‘jsdom’. Installeer ze door het volgende commando uit te voeren:

sh
npm install --save-dev globby html-minifier-terser jsdom

Het process-html.mjs script

Maak een nieuw bestand met de naam process-html.mjs in de hoofdmap van je Astro project. De onderstaande code gebruikt Astro’s standaard statische uitvoermap dist. Als je een andere uitvoermap gebruikt, pas dan de path variabele aan.

js
import fs from 'node:fs/promises'
import { globby } from 'globby'
import { minify } from 'html-minifier-terser'
import { JSDOM } from 'jsdom'

// Find all HTML files in the output folder
const path = './dist'
const files = await globby(`${path}/**/*.html`)

await Promise.all(
  files.map(async (file) => {
    console.log('Processing file:', file)
    let html = await fs.readFile(file, 'utf-8')

    // Preserve existing IDs and add unique IDs to other h2, h3 and h4 elements
    const dom = new JSDOM(html)
    const headings = dom.window.document.querySelectorAll('h2, h3, h4')
    const usedIds = new Set(
      [...dom.window.document.querySelectorAll('[id]')].map((element) => element.id)
    )

    for (const heading of headings) {
      if (heading.id) continue

      const baseId = heading.textContent
        .normalize('NFKD')
        .replace(/\p{Mark}+/gu, '')
        .trim()
        .toLowerCase()
        .replace(/[^\p{Letter}\p{Number}]+/gu, '-')
        .replace(/^-|-$/g, '') || 'section'

      let id = baseId
      let suffix = 2
      while (usedIds.has(id)) id = `${baseId}-${suffix++}`

      heading.setAttribute('id', id)
      usedIds.add(id)
    }

    html = dom.serialize()

    // Minify the resulting HTML
    html = await minify(html, {
      removeComments: false,
      preserveLineBreaks: true,
      collapseWhitespace: true,
    })

    await fs.writeFile(file, html)
  })
)

Bijwerken van package.json

Werk je package.json bestand bij om het script uit te voeren na het bouwproces. Voeg && node process-html.mjs toe na astro build zoals weergegeven in het voorbeeld hieronder.

json
{
  "name": "astro-process-html",
  "type": "module",
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro build && node process-html.mjs",
    "preview": "astro preview",
    "astro": "astro"
  }
}

Wat levert het op in de praktijk?

Het bescheidenste deel van dit script is de minify-stap. Astro minified HTML zelf al standaard (compressHTML staat aan), dus een tweede minifier kan hoogstens nog resterende witruimte verkleinen. Dit voorbeeld bewaart comments om hydratatie niet te breken. Meet de bestandsgrootte vóór en na verwerking op je eigen build; zonder bewaard meetrapport is een exact winstpercentage niet te onderbouwen. Wie een dramatische besparing verwacht, komt bedrogen uit.

De mogelijke meerwaarde zit in gerichte transformaties. Bestaande heading-IDs blijven behouden; ontbrekende IDs worden uniek toegevoegd. Het voorbeeld verwijdert geen lege paragrafen of CMS-attributen. Daarvoor is aanvullende, geteste logica nodig. Dat is per pagina weinig, maar over honderden pagina’s wél schonere, kleinere bestanden en een consistente ankertoegankelijkheid, elke build opnieuw.

Valkuilen die je moet kennen

Bewaar HTML-comments wanneer je Svelte 5 of andere comment-gebaseerde hydratatie gebruikt. Daarom staat removeComments: false in dit voorbeeld. Zie de Svelte 5-migratiegids.

  • Zet het script in je buildpipeline, niet in je dagelijkse werk. Elke build overschrijft de outputmap volledig, dus een handmatig gedraaide run verdwijnt bij de volgende deploy. Vandaar de aanpassing in package.json hierboven.
  • Houd preserveLineBreaks: true aan. Zonder die optie klapt je hele HTML op één regel en wordt een diff of debugsessie op gebouwde bestanden nagenoeg onmogelijk.
  • jsdom parseert en serializeert elke pagina opnieuw. Dat is veilig voor gewone HTML, maar test je hydratatie (Svelte- of framework-islands) na elke uitbreiding van het script. Controleer interactieve componenten en hun ankers na elke wijziging.
  • collapseWhitespace kan in randgevallen een zichtbare spatie rond inline elementen weghalen. Controleer je design na de eerste run, zeker rond buttons en iconen die naast tekst staan.

Alternatief: de astro:build:done-integratie

Wie dit netter vindt dan een los npm-script, pakt dezelfde logica in een Astro-integratie. Die draait op de astro:build:done-hook en krijgt de uitvoermap mee, zodat je geen paden hoeft te hardcoden. Je wint herbruikbaarheid (een integratie stop je in een package) en verliest een beetje eenvoud: voor één site is een los script prima, voor meerdere projecten loont de integratie.

Dit is alleen het integratieskelet, geen tweede volledig werkend script. Voeg de bestandsverwerking hierboven toe en zet de URL dir om met fileURLToPath uit node:url voordat je hem als globby-pad gebruikt. Gebruik dit alternatief niet tegelijk met het npm-postbuildscript.

js
// astro.config.mjs
import { defineConfig } from 'astro/config'

function processHtml() {
  return {
    name: 'process-html',
    hooks: {
      'astro:build:done': async ({ dir }) => {
        // hier dezelfde globby- en minify-logica als in process-html.mjs,
        // maar dan over alle .html-bestanden in de uitvoermap dir
      },
    },
  }
}

export default defineConfig({
  integrations: [processHtml()],
})

Beide routes komen op hetzelfde neer: na de build over alle HTML-bestanden heen en elke pagina verwerken. Kies wat past bij hoe je projecten beheert.

Conclusie

Met een handjevol regels na de build krijg je vaste heading-ankers, schonere links en iets kleinere HTML-bestanden, elke deploy opnieuw. De extra besparing kan klein zijn omdat Astro zelf al HTML comprimeert. Gebruik het script voor een concrete transformatie en meet de winst op je eigen build.

Wil je verder met snelheid, lees dan meer over de laadtijd van je website. En wil je dat dit soort pipelines iemand anders dagelijks onderhoudt, kijk dan bij websiteonderhoud of neem contact op voor vragen. Meer configuratieopties vind je in de html-minifier-terser referentiegids.

Liever een volledig begeleid traject voor je Astro-project? Bekijk dan Astro website laten maken.