Frameworks
vidimus reads the folder of HTML your generator writes, so the setup per generator comes down to four questions:
| Set | |
|---|---|
| where does the build go? | distDir |
| what is the production URL, with any base path? | siteUrl, the same value as the generator's own setting |
where do static files go, for _headers? | the generator's public or static folder |
| how does the host serve it? | optional: server.command to serve it with the host's tool, see Serving the build |
The security audit reads response headers from _headers at the root of the build (Netlify and Cloudflare Pages format, security.file). Every generator below copies a static folder into the build as is; put _headers there. The built-in server does not send those headers; use server.headers for headers the browser audits should see, or serve the build with wrangler pages dev or netlify dev through server.command, which do.
When the site lives under a path, the generator has to build with that prefix and siteUrl has to include it. See How it works.
A single-page app with client-side routing (React Router, Angular Router, Vue Router) builds one index.html and relies on the host to answer deep URLs with it. See Client-rendered apps below.
| Generator | Build | distDir | Base path setting | Static folder |
|---|---|---|---|---|
| Astro | astro build | dist | site, base | public/ |
| VitePress | vitepress build docs | docs/.vitepress/dist | base | docs/public/ |
| Hugo | hugo | public | baseURL | static/ |
| Eleventy | npx @11ty/eleventy | _site | pathPrefix | passthrough copy |
| Next.js | next build with output: 'export' | out | basePath | public/ |
| SvelteKit | vite build with adapter-static | build | kit.paths.base | static/ |
| Jekyll | jekyll build | _site | url, baseurl | any folder, plus include |
Astro
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
site: 'https://user.github.io',
base: '/project',
});// vidimus.config.ts
import { defineConfig } from 'vidimus';
export default defineConfig({
distDir: 'dist',
siteUrl: 'https://user.github.io/project',
});public/_headersends up indist/_headers.- Hashed assets are written to
_astro/. A cache header for them on the built-in server:server.headers: [{ match: '/_astro/', headers: { 'cache-control': 'public, max-age=31536000, immutable' } }]. Without^, the rule also matches under a base path. - With Astro's i18n routing and the default locale unprefixed, translations live under
/<locale>/, which is whatlocalesanddefaultLocaleexpect. astro previewserves the build on port 4321:npx vidimus --origin http://localhost:4321/project.
VitePress
VitePress builds into .vitepress/dist inside the docs folder. Keep the vidimus config next to it, and root becomes the docs folder, as in this repository's own docs/vidimus.config.json:
{
"$schema": "../node_modules/vidimus/schema.json",
"distDir": ".vitepress/dist",
"siteUrl": "https://user.github.io/project"
}npx vitepress build docs && npx vidimus -c docs/vidimus.config.jsonOr keep it at the project root with distDir: 'docs/.vitepress/dist'.
base: '/project/'in.vitepress/configsets the base path;sitemap.hostnamemakes VitePress writesitemap.xml.docs/public/_headersends up in the build.- Pages are written as
guide.html. WithcleanUrlsthey are linked as/guide, which the built-in server resolves toguide.html; your host has to do the same. vitepress preview docsserves the build on port 4173.
Hugo
# hugo.toml
baseURL = 'https://user.github.io/project/'
enableRobotsTXT = trueexport default defineConfig({
distDir: 'public',
siteUrl: 'https://user.github.io/project',
});static/_headersends up inpublic/_headers.- Hugo does not empty
public/before building, so pages you deleted stay there and are audited. Build withhugo --cleanDestinationDir, or deletepublic/first. - Hugo writes
robots.txtonly withenableRobotsTXT; theseoaudit checks it. - In a multilingual site, the default language is at the root unless
defaultContentLanguageInSubdiris set; setlocalesanddefaultLocaleto the language codes used in the paths. - Audit the built folder, not
hugo server: the development server renders differently and injects its live reload script.
Eleventy
// eleventy.config.js
export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy({ 'src/_headers': '_headers' });
}
export const config = {
dir: { input: 'src' },
pathPrefix: '/project/',
};export default defineConfig({
distDir: '_site',
siteUrl: 'https://user.github.io/project',
});- Eleventy only copies files it is told to; the passthrough copy puts
_headersat the root of_site. pathPrefixapplies only to URLs passed through theurlfilter or rewritten by the HTML<base>plugin. A hard-coded/about/stays unprefixed and breaks under the base path, andlinksreports it.
Next.js static export
// next.config.mjs
export default {
output: 'export',
basePath: '/project',
trailingSlash: true,
images: { unoptimized: true },
};export default defineConfig({
distDir: 'out',
siteUrl: 'https://user.github.io/project',
});next buildwrites the static site toout/, withpublic/_headerscopied in.trailingSlash: truewritesabout/index.html; without it pages areabout.html. Both are served.- The default image loader needs a server;
images.unoptimizedis required fornext/imagein an export.
SvelteKit with adapter-static
// svelte.config.js
import adapter from '@sveltejs/adapter-static';
export default {
kit: {
adapter: adapter(),
paths: { base: '/project' },
},
};// src/routes/+layout.js
export const prerender = true;
export const trailingSlash = 'always';export default defineConfig({
distDir: 'build',
siteUrl: 'https://user.github.io/project',
});- adapter-static writes to
build/by default (pagesoption);static/_headersis copied in. - Every route has to be prerendered;
prerender = truein the root layout does that. trailingSlash = 'always'writesabout/index.html, the default'never'writesabout.html.- A
fallbackpage for SPA mode is an empty shell without content; add its file to the top-levelexclude, unless it is404.html. Pointserver.fallbackat it so routes that are not prerendered load it, as they do on the host. vite previewserves the build on port 4173.
Jekyll
# _config.yml
url: https://user.github.io
baseurl: /project
include:
- _headersexport default defineConfig({
distDir: '_site',
siteUrl: 'https://user.github.io/project',
});bundle exec jekyll buildwrites_site/.- Jekyll skips files whose name starts with
_;includemakes it copy_headers. - Links need
relative_urlorabsolute_urlto get thebaseurlprefix; hard-coded root paths break under the base path. - GitHub Pages does not read
_headers, so the header checks cannot pass on a site hosted there. Setseverity: { security: 'warn' }to keep the other findings visible without failing.
Plain HTML
Point distDir at the folder you publish:
export default defineConfig({
distDir: 'site',
siteUrl: 'https://example.org',
});- Every
**/*.htmlunderdistDiris a page. Publishing from the project root (distDir: '.') also picks up HTML insidenode_modules/and vidimus' own reports in.vidimus/; keep the pages in their own folder, or addexclude: ['^node_modules/', '^\\.vidimus/']. - Put
_headersin that folder if your host reads it. - There is no build step, so the
no build outputerror meansdistDirpoints to the wrong place.
Client-rendered apps
A React, Angular, Vue or Solid app built without prerendering is one index.html, an empty <div id="root">, and scripts. Three settings make vidimus audit it like the site it becomes in the browser:
server.fallback: answer every page path withindex.html, as the host does.routes: the pages to audit, since the build has only one file. List them, read them from a sitemap, or crawl the rendered links.render: read the rendered DOM in thedistaudits and inlinks, and wait for the app in every browser audit.
export default defineConfig({
distDir: 'dist',
siteUrl: 'https://example.com',
server: { fallback: 'index.html' },
routes: { discover: 'crawl' },
render: { mode: 'on', waitFor: '#root > *' },
links: { notFound: { selector: '[data-page="not-found"]' } },
});render.waitFor is a CSS selector that exists once the app has rendered, 'networkidle' for apps that fetch before they render, or a number of milliseconds. links.notFound identifies the app's own not-found view, which the fallback answers with 200. See How it works for what each audit then reads.
React with Vite
vite buildwritesdist/; with a base path, build with--base /project/and setsiteUrlwith the same path.- React Router's
createBrowserRouterneeds the fallback.createHashRouterdoes not, but its routes all share one URL, so the server audits see only the first;links.notFoundstill opens every#/link. - React 19 hoists
<title>,<meta>and<link>rendered in a component into<head>, soseosees one title per route withrenderon. public/_headersis copied intodist/.
Angular
ng buildwritesdist/<project>/browser/; setoutputPathto{ "base": "dist", "browser": "" }inangular.jsonto drop thebrowser/level, or pointdistDirat it.--base-href /project/for a base path, withsiteUrlto match.- Keep SSR and prerendering off if you want the pure client-rendered build; turn on
outputMode: "static"with prerendered routes to get HTML per route instead, and dropserver.fallbackandroutes. - Component styles are inserted as
<style>elements at runtime. With a strict CSP they are blocked, andcspwithrenderon reports each of them. Keep styles in the global stylesheet, or allow them withautoCspand a nonce from a server. Also turn offinlineCritical: it adds an inline<style>and anonloadhandler toindex.html. TitleandMetaset the head per route;seoreads them withrenderon.
Vue with Vite
vite buildwritesdist/;baseinvite.config.tssets the base path.- Vue Router's
createWebHistoryneeds the fallback;createWebHashHistorydoes not, with the same limits as a hash router in React. @unhead/vuesets titles and meta tags at runtime, visible withrenderon.
SvelteKit SPA mode
- adapter-static with
fallback: '200.html'(or'index.html') and no prerendered routes. - Set
server.fallbackto the same file, and exclude it from the page list when it is notindex.html:exclude: ['^200\\.html$']. - With some routes prerendered and the rest client-rendered, the prerendered ones are files and need no
routesentry; list or crawl the others.
Hosts
The fallback in vidimus stands in for the host's rewrite. Match what yours does:
| Host | Rewrite | server.fallbackStatus |
|---|---|---|
| Netlify | /* /index.html 200 in _redirects | 200 |
| Vercel | "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] in vercel.json | 200 |
| Cloudflare Pages | automatic when there is no 404.html | 200 |
| GitHub Pages | none: deep links get 404.html, with a 404 status | 404, with server.fallback: '404.html' |
On GitHub Pages, deep links need a file: copy index.html to every route (about.html, docs/index.html) after the build, and each is served with a 200. The other workaround, a 404.html that redirects into the app, leaves a 404 status on every deep link for search engines and link checkers, and for vidimus with fallbackStatus: 404.
Prerender if you can
vidimus renders your app before auditing it; many crawlers, link previews and feed readers do not, and see the empty shell. A green seo run on a client-rendered app says the titles and descriptions are right once JavaScript has run, not that search engines will read them. Prerendering at build time gives every route its own HTML file, which vidimus, and everyone else, reads without a browser: vite-ssg or Vike for Vite apps, Angular's outputMode: "static", SvelteKit's prerender = true, React Router's prerender option.
The React and Angular flavors of these docs are client-rendered on purpose, audited in the same run as the others; see Eight flavors.
Other generators
Anything that writes a folder of HTML works: set distDir to that folder and siteUrl to the production URL. If the generator has a preview server that behaves like production, audit it with --origin; otherwise let vidimus serve the folder.