Baobaobaolin.com
date
entry
021
topic
tooling
rev
1

Your build config should not contain a file list

Multi-page bundlers want every HTML entry point enumerated. That list is maintained by a person, and forgetting to update it produces no error — the page just quietly never reaches the output.

The documented way to configure Vite (Rollup underneath) for multiple pages:

build: {
  rollupOptions: {
    input: {
      main:  resolve(__dirname, "index.html"),
      about: resolve(__dirname, "about/index.html"),
      post1: resolve(__dirname, "posts/foo/index.html"),
      post2: resolve(__dirname, "posts/bar/index.html"),
      // one more line per article
    }
  }
}

Entirely reasonable at three pages. At twenty it has become a list that must be updated every time content is added — and this kind of thing always fails the same way: missing an entry produces no error message. The build succeeds; it is simply one page short.

Worse, vite dev usually still serves that page locally — the dev server reads the filesystem rather than the input list — so development looks perfect and only the production output is missing it.

Let the filesystem be the source of truth

The entry points are already described by the directory structure. Rather than copying that into config, read it at build time:

function collectHtmlInputs() {
  const inputs = { main: resolve(root, "index.html") };

  const notFound = resolve(root, "404.html");
  if (existsSync(notFound)) inputs["not-found"] = notFound;

  ["", "en"].forEach((locale) => {
    const base = locale ? resolve(root, locale) : root;
    const prefix = locale ? `${locale}-` : "";

    if (locale) {
      const home = resolve(base, "index.html");
      if (existsSync(home)) inputs[`${prefix}home`] = home;
    }

    const about = resolve(base, "about", "index.html");
    if (existsSync(about)) inputs[`${prefix}about`] = about;

    const postsRoot = resolve(base, "posts");
    if (existsSync(postsRoot)) {
      readdirSync(postsRoot, { withFileTypes: true })
        .filter((e) => e.isDirectory())
        .forEach((e) => {
          const html = resolve(postsRoot, e.name, "index.html");
          if (existsSync(html)) inputs[`${prefix}post-${e.name}`] = html;
        });
    }
  });

  return inputs;
}

Publishing now means creating a directory with an index.html in it. The config does not move.

Three implementation details

Keys must be unique and stable. The Chinese and English versions share a slug, so the key needs a language prefix (post-foo versus en-post-foo). Collide and the later one silently overwrites the earlier, with no complaint from Rollup — another quiet failure.

Use existsSync rather than assuming. Finding a directory does not guarantee an index.html inside it; without that guard, one empty directory fails the whole build with a Rollup error that is not easy to trace back to its cause.

Loop over locales instead of copy-pasting. That ["", "en"] array is all you touch when a third language arrives. Same consideration as the other sync points on a bilingual site: a structure that can be duplicated will eventually disagree with its duplicate.

Where scanning stops being right

The risk of automatic discovery is discovering something you did not mean to publish — a half-written draft, a template, a backup.

The answer is a directory convention, not an exclusion list. Drafts live in drafts/, outside the scanned tree, and move into posts/ when finished. That beats maintaining exclusion rules inside the scanning logic — an exclusion list is itself a hand-maintained list, so you would only have relocated the problem.

The test is simple: "this is published" should be expressed by where the file lives, not by a line in a config file.

The same principle covers static assets. This site's build has a small plugin that copies assets/ into the output and places the English feed at dist/en/feed.xml — also a rule rather than a list. Adding an image changes no configuration.

The sync points that remain

This change took publishing an article from touching eight places to seven. The sitemap was later generated automatically too, bringing it to six. The remaining six are two articles, two listings and two feeds.

Ideally all of those would be generated from one source — extract each article's metadata and derive the listings, feeds and sitemap from it. I have not done that, because at this volume a check script still covers it.

The direction is clear though: every place a person has to remember to synchronise is a place someone eventually will not. Remove what you can, and put a check on the rest — in that order. Automate first; only then is a check worth writing.

If you remember one thing

Configuration should describe rules, not enumerate files. A list in a config file that grows with your content is a pending failure, because its correctness depends on someone remembering — and remembering has never been a reliable mechanism.

Revision history

  1. Sitemap generation also automated; remaining sync points updated to six
  2. First published