Configuration
On this page
Put plugin options and optional Vite settings in sitelo.config.js. No vite.config.js is required.
export default {
site: 'https://example.com',
rss: {
site: 'https://example.com',
title: 'My Blog',
description: 'Latest posts',
routePrefix: '/blog',
},
vite: {
publicDir: 'static',
build: {
emptyOutDir: true,
outDir: 'public',
},
server: {
port: 8888,
},
},
}Plugin options
pagesDir— default'src'pageExtensions— which suffixes count as pagescleanUrls— defaulttrue(/about/index.html)site— base URL; enablessitemap.xmlrss— RSS feed configpagefind—trueor options object; indexes the site with Pagefind aftersitelo build(requiresnpm install -D pagefind)images—trueor options object; optimizes images in dev and aftersitelo build(requiresnpm install -D sharp)missingAssets—'error'or'warn'linkCheck— dead internal links (see Link checking)lighthouse— Lighthouse audits of the build (see Lighthouse audits)generatedTypesDir— default'.sitelo/types'renderConcurrency/renderBatchSize— build parallelismbuildReport— defaulttrue; post-build summary of pages, output size, largest files and phase timings.falseto disable, or{ top }to change how many large files are listedpruneCss— defaultfalse;trueor{ keep }writes the sitelo/ui stylesheet with only the rules the built pages can matchdebug— verbose loggingdevToolbar— defaulttrue; setfalseto hide the dev-only toolbar (source file, params, island count, viewport toggle)devToolbarDocsUrl— Docs link in the toolbar (defaulthttps://sitelo.dev/docs)
Dev toolbar
While sitelo (dev) is running, a small bar at the bottom of each page shows the page file, params, and how many server islands are on the page. Use the viewport button to cycle Desktop / Tablet / Mobile preview (iframe, so media queries match), and Copy for a debug blob when filing issues. It never appears in sitelo build output.
// sitelo.config.js
export default {
devToolbar: false, // hide for everyone on this project
}Vite options
Anything under vite is merged into Vite’s config. CLI flags (e.g. --port) override both.
Sitelo builds set build.rollupOptions.checks.pluginTimings: false. Rolldown otherwise reports that plugin hooks dominate the build, which on a sitelo site is always true — generating the pages is the build — so it names the same plugin on every run. Set it to true under vite when profiling your own plugins.
Existing vite.config.js
Still supported — either Vite-only options, or full plugin control:
// Vite options only; sitelo still injects the plugin
export default {
publicDir: 'static',
server: { port: 8888 },
}// Register the plugin yourself
import htmlPages from 'sitelo'
export default {
plugins: [htmlPages({
site: 'https://example.com',
})],
}If the plugin is already in your Vite config, put plugin options there — not also as plugin options in sitelo.config.js (sitelo will error).
Sitemap & RSS
Set site to emit dist/sitemap.xml.
RSS:
export default {
rss: {
site: 'https://example.com',
title: 'My Blog',
description: 'Latest posts',
routePrefix: '/blog',
},
}Produces dist/rss.xml with an item for every page under routePrefix.
Link checking
Broken <script src> and stylesheet hrefs already fail the build (see missingAssets). linkCheck covers the other half: internal <a href> links that point at a page which does not exist.
export default {
linkCheck: true, // 'warn' (default), 'error', or an options object
}After sitelo build, every internal link in the output is resolved and anything with no page behind it is reported, grouped by the page it appears on:
[sitelo] 3 broken internal links
index.html
../escape -> escapes the output directory
/abuot -> no such page
/blog/missing-post -> no such pageHow links are resolved
The check runs against the emitted site, not the route table — so it accounts for cleanUrls, route groups, mapOutputPath, files copied from public/, and pages produced by dynamic routes. It also runs after image optimization and Pagefind, so it sees exactly what ships. A link is valid when a real file answers it, tried in the order a static host would:
/about→about, thenabout/index.html, thenabout.html/blog/→blog/index.html(a trailing slash only ever means a directory index)/→index.html
Relative links (../about) resolve against the page holding them, and one that climbs out of the output directory is reported. Query strings are ignored for resolution — /about?utm=x checks /about.
External links are never fetched. https://, protocol-relative //cdn.example.com, mailto:, tel: and other schemes are skipped entirely.
Options
| Option | Default | Description |
|---|---|---|
mode | 'warn' | 'warn' logs and continues; 'error' fails the build — useful in CI |
exclude | [] | Globs or regular expressions of hrefs to skip |
checkFragments | false | Also verify that #fragment targets exist in the linked page |
export default {
linkCheck: {
mode: 'error', // fail the build on a dead link
checkFragments: true, // also verify #fragment targets
exclude: ['/api/**', /^\/legacy\//],
},
}checkFragments is off by default because ids added by client-side JavaScript are not in the built HTML, and would be reported as missing. Both id and legacy anchor name attributes count as fragment targets.
Sites served from a base
If your site is deployed under a base (a GitHub Pages project site, say), a root-relative link that does not carry that base is reported. /about on a site served from /repo/ sends the browser to the host root, not into your site — resolving it against the output anyway would hide exactly the mistake worth catching. Use exclude when it is deliberate.
Lighthouse audits
sitelo lighthouse audits the finished build with Lighthouse. It is an optional peer dependency:
npm install -D lighthousesitelo serves dist/ the way sitelo preview does, points a headless Chrome at every page, and prints the scores:
[sitelo] lighthouse mobile - 3 pages
page perf a11y best seo
/ 98 100 100 100
/docs 95 100 100 100
/docs/routing 97 100 100 100
[sitelo] lighthouse audited 3 pages in 31.4sEvery page is audited at the URL the site links — /docs, never dist/docs.html — so cleanUrls, route groups and a base are all accounted for. Add thresholds and the report becomes a check: any score below its threshold fails the command.
export default {
lighthouse: {
exclude: ['404.html'], // the 404 page is rarely worth auditing
thresholds: {
performance: 90,
accessibility: 100,
'best-practices': 95,
seo: 100,
},
},
}Scores are written the way Lighthouse shows them (0–100); its own 0–1 fractions work too. Use mode: 'warn' to log instead of fail.
include/exclude— which pages are audited (globs or regular expressions)sample— audit this many random pages perincludepattern instead of all of themcategories—performance,accessibility,best-practices,seothresholds— the minimum score per categorymode—'error'(default) or'warn'formFactor—'mobile'(default),'desktop', or bothruns— repeat the audit and report the median scoreoutput/formats— save the full Lighthouse report for every pageflags/config— passed straight to Lighthouse, so anything its CLI accepts worksonBuild— audit at the end ofsitelo buildas well
export default {
lighthouse: {
formFactor: 'desktop', // the Lighthouse desktop preset
runs: 3, // three runs per page, median score
output: true, // full reports in .sitelo/lighthouse/
onBuild: true, // also audit at the end of sitelo build
flags: { throttlingMethod: 'provided' },
},
}Lighthouse drives a real Chrome: install one where the audit runs, or point CHROME_PATH at a binary. Performance scores move between runs, so use runs: 3 before pinning a threshold on them.
Pagefind search
Opt-in static search powered by Pagefind, an optional peer dependency. Install it when you want search, then enable indexing, mark the content, mount the UI, and run sitelo build.
npm install -D pagefind1. Enable indexing
Set pagefind: true. After sitelo build you get dist/pagefind/, and by default a copy in public/pagefind/ so the next sitelo (dev) or sitelo preview can serve /pagefind/ without rebuilding.
export default {
pagefind: true,
}2. Mark content and add a mount point
Put data-pagefind-body on the main content so nav/footer aren’t indexed. Leave an empty element for the search UI:
export default () => `
<html lang="en">
<head>
<title>My site</title>
<link rel="stylesheet" href="/styles.css">
<script type="module" src="/main.js"></script>
</head>
<body>
<header>
<a href="/">Home</a>
<div id="search"></div>
</header>
<main data-pagefind-body>
<h1>Hello</h1>
<p>Only this region is indexed.</p>
</main>
</body>
</html>
`import {
html, head, title, link, script, body, header, a, div, main, h1, p,
} from 'javascript-to-html'
export default () =>
html({ lang: 'en' },
head(
title('My site'),
link({ rel: 'stylesheet', href: '/styles.css' }),
script({ type: 'module', src: '/main.js' }),
),
body(
header(
a({ href: '/' }, 'Home'),
div({ id: 'search' }),
),
main({ 'data-pagefind-body': '' },
h1('Hello'),
p('Only this region is indexed.'),
),
),
)export default function Home() {
return (
<html lang="en">
<head>
<title>My site</title>
<link rel="stylesheet" href="/styles.css" />
<script type="module" src="/main.js" />
</head>
<body>
<header>
<a href="/">Home</a>
<div id="search" />
</header>
<main data-pagefind-body="">
<h1>Hello</h1>
<p>Only this region is indexed.</p>
</main>
</body>
</html>
)
}3. Mount the Pagefind UI
Load /pagefind/pagefind-ui.js and pagefind-ui.css from your client script (only after a build has produced the index):
async function initSearch() {
const mount = document.querySelector('#search')
if (!mount) return
// Index only exists after `sitelo build` (synced to public/pagefind by default)
try {
const probe = await fetch('/pagefind/pagefind-ui.js', { method: 'HEAD' })
if (!probe.ok) return
} catch {
return
}
const style = document.createElement('link')
style.rel = 'stylesheet'
style.href = '/pagefind/pagefind-ui.css'
document.head.appendChild(style)
await new Promise((resolve, reject) => {
const script = document.createElement('script')
script.src = '/pagefind/pagefind-ui.js'
script.onload = resolve
script.onerror = reject
document.body.appendChild(script)
})
new window.PagefindUI({
element: '#search',
showImages: false,
})
}
initSearch()4. Build and ignore the synced bundle
sitelo build
# then: sitelo preview — or sitelo (dev) using public/pagefindpublic/pagefind/Advanced options (syncPublic, glob, language, selectors, …) go on an object: pagefind: { syncPublic: false, glob: '**/*.html' }. Full UI options: pagefind.app.
404
Create src/404.ht.js for dist/404.html. Otherwise a clean default is generated.