Configuration

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

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.

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 page

How 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:

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

OptionDefaultDescription
mode'warn''warn' logs and continues; 'error' fails the build — useful in CI
exclude[]Globs or regular expressions of hrefs to skip
checkFragmentsfalseAlso 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 lighthouse

sitelo 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.4s

Every 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.

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.

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 pagefind

1. 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:

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.'),
      ),
    ),
  )

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/pagefind
public/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.