Separate the three kinds of configuration

Conflating them is the root of most configuration trouble. There are three distinct categories, with different owners, different review requirements and different storage:

  • Product configuration. Things true of the site regardless of where it runs: the colour palette, the navigation structure, the component defaults. Version-controlled, reviewed like code.
  • Environment configuration. Things that differ per deployment: the canonical site URL, whether search engines may index, which endpoint the contact form posts to. Version-controlled, but per environment.
  • Secrets. Things that must never appear in a repository or in a browser. Held in a vault, injected at deployment, never on a static site at all.

Watch out

On a static site the first two categories are downloaded by the browser and are therefore public. The third category cannot exist. Any design that puts a secret into a JSON file the site fetches has published it.

Make configuration declarative and typed by convention

A configuration file that a non-developer can edit needs an obvious shape and a comment explaining what each value does. JSON has no comment syntax, so we use a reserved _notice key at the top of every file.

config/seo.json

{
  "_notice": "Environment-specific. siteUrl must match the deployed domain exactly, with no trailing slash.",
  "seo": {
    "siteUrl": "https://www.example.co.uk",
    "defaultTitle": "Northbridge Digital Ltd",
    "titleTemplate": "%s | Northbridge Digital Ltd",
    "robots": "index, follow"
  }
}

Never let a missing value break the page

Configuration is fetched at runtime, and fetches fail. A site that renders a blank header because one JSON file returned a 404 has converted a small problem into an outage.

Safe read with a fallback

export function read(source, path, fallback = '') {
  const value = path
    .split('.')
    .reduce((acc, key) => (acc && acc[key] !== undefined ? acc[key] : undefined), source);
  return value === undefined || value === null ? fallback : value;
}

// Usage: never throws, always renders something sensible.
const name = read(company, 'company.name', 'Our company');

The fallback is not laziness. It is the difference between a page with a slightly generic heading and a page with no heading at all.

One file per concern

A single large configuration file becomes a merge conflict magnet and an all-or-nothing permission decision. Splitting by concern lets a marketing colleague edit navigation without touching anything that governs privacy, and lets you review the two differently.

Configuration files, their category and typical owner
File Category Typical owner
company.json Product Company owner
theme.json Product Design
navigation.json Product Marketing
seo.json Environment Engineering
analytics.json Environment Engineering, with data protection sign-off
privacy.json Product Legal or data protection

Default to the safe value

Where a setting has a privacy, security or SEO consequence, the checked-in default should be the conservative one. Analytics disabled. Non-production environments noindex. Cookie categories off until consented.

The reason is asymmetry of failure. Forgetting to enable analytics in production costs you a fortnight of data. Forgetting to disable it in a test environment, or forgetting noindex on a staging site, is a data protection question and a duplicate content problem respectively. Choose the default whose failure mode is cheapest.

Note

Write the environment differences down in a single table in your documentation. Three values that differ between staging and production is a manageable fact; three values that differ and are undocumented is the beginning of an incident report.

Validate it in the pipeline

At minimum: every file parses, every required key is present, and the environment- specific values match the environment being deployed to. A twenty-line check that asserts seo.siteUrl equals the domain you are publishing to would have prevented more canonical-tag incidents than any amount of care.

Treat configuration changes as changes

The last habit is cultural. A pull request that alters one line of JSON deserves the same review as one that alters one line of JavaScript, because it can do the same damage. Organisations that exempt configuration from review are not saving time; they are deferring it until the outage.