Static HTML / Node.js generators / sourced JSON data / SVG drawings

semiatic answers one question: what does this symbol mean? You find the symbol by its shape, not its name, and you land on a page that gives the plain meaning, what to do about it, and where that meaning comes from. The oven dial page is a good example: pick “a fan” or “straight lines” and you get the oven functions drawn that way, each with the makers whose manuals confirm it. This page is about how that is built, and what happened in the first weeks after it went live.

  • Pages: plain HTML files, no backend, no build step on the server; 1,034 URLs in the sitemap today
  • Data: JSON files under tools/data/, 51 of them, at least one per symbol family
  • Sources: 707 entries in those files carry their own list of sources, 2,353 citations pointing at 466 distinct URLs
  • Generators: 48 tools/build-<family>.cjs scripts, one per family, each with a --check mode
  • Tests: 66 test files, including a page contract every HTML file must pass

Every number on this page was read on 2026-10-09: repository facts from the semiatic repository at commit a0f81a42 (read only, nothing in it was changed), visitor counts from the estate’s GoatCounter export generated that day, and Search Console figures from its export for 2026-09-09 to 2026-10-07.

Every symbol is a row of data with its sources

The site’s rule is in its README: draw every symbol ourselves, never copy standard artwork or brand logos, and make “no safety claim beyond the standard’s own wording”. Every meaning is written from a source that is named on the page, with the date it was checked. So the source of truth is not the HTML; it is a data file per family, and every symbol entry carries the references it rests on.

A car dashboard light is the clearest case, because two regulations define the symbols: UN Regulation No. 121, used across Europe, and FMVSS 101 in the United States. The entry for the battery or charging light records which table of each regulation names it and what colour each requires (tools/data/dashboard.json, lines 375 to 387):

"rows": [
  {
    "reg": "fmvss101",
    "ref": "Table 1, “Electrical charge” — telltale and indicator",
    "colour": null,
    "colour_note": "no colour required"
  },
  {
    "reg": "r121",
    "ref": "Table, item 12 “Electrical charging condition” — tell-tale",
    "colour": "red"
  }
],

The page built from it says what a driver needs: the charging system is not charging the battery while the engine runs, switch off loads you do not need, and get help. It also says what the light does not mean (a faulty battery, by itself), and points you to the manual for whatever else the lamp reports on your car. Laundry care labels work the same way against ISO 3758, with a separate note for the North American dots that stand for the temperature numbers.

When no standard exists: two makers or no page

Oven dial icons have no published standard at all. Every maker draws its own, and the same drawing can mean different things on different brands. The oven data file says so in its own standard field, and the generator refuses to publish a symbol on one maker’s word. In validate() (tools/build-oven.cjs, line 428):

    if (makersOf(s).length < 2) throw new Error(`${s.slug}: needs sources from at least 2 different makers`);

A symbol is only published when at least two different makers’ own guides show it with that meaning. Each source records the maker, the URL, what that page shows and the date it was checked, and the build stops if any of the four is missing. In practice most symbols have more than two: “top and bottom heat” cites 11 makers’ guides, and the fan with top and bottom heat cites 4. The oven family holds 26 symbols grouped under 13 shapes, and the hub page’s picker filters them by shape in the browser.

The same evidence habit shows up in the kitchenware symbols: no law or public standard defines the coil symbol for induction pans, so the page lists how four different makers describe it and then says what it does not promise.

Drawings are code, not artwork

The symbol drawings are not drawn in an editor; the generators build them. The oven generator, for one, composes every icon from a handful of primitives on a 100 by 100 grid, stroked in currentColor, so the same drawing follows the light and dark themes. An oven icon is a list of parts from a table like this (tools/build-oven.cjs, lines 94 to 96):

  frame: () => '<rect x="8" y="8" width="84" height="84" rx="10"/>',
  top: () => '<path d="M22 22 H78"/>',
  bottom: () => '<path d="M22 78 H78"/>',

“Top and bottom heat” is frame plus top plus bottom; the grill is frame plus a zigzag. No maker’s or standard’s artwork is copied.

Until 2026-10-01 those drawings were inline SVG inside each page, which meant search engines could not show them as images. Commit a0f81a42 (T-SEMI.103) moved every named drawing into a file of its own under html/img/<family>/ and shows it through an ordinary image tag. Its commit message records the scale: 8530 named drawings, 3186 files, 889 of 1035 pages changed. An image file cannot inherit the page’s colours, so each file carries its own small style block with a light-theme and a dark-theme ink colour.

One generator per family, and a check mode that holds the bytes

Each family has one script, tools/build-<family>.cjs, that reads its data file and writes that family’s pages plus the shared html/sitemap.xml. The pieces every generator shares (the page head with its canonical link and description, the header, the analytics snippet, the sitemap) live in tools/lib/site.cjs. Its header explains the trick that keeps 48 generators from fighting over one sitemap: every generator emits the identical file, “so each one’s —check holds the others to it”.

node tools/build-oven.cjs --check writes nothing; it rebuilds the family in memory and fails if any file on disk differs from what it would generate. A hand edit to a built page, or a data change nobody rebuilt, turns that check red.

The contract every page passes

test/pages-contract.test.cjs walks every .html file under html/ and holds each one to the same rules: a canonical link pointing at its own URL, a non-empty title, a site icon that exists, a place in the sitemap (and no sitemap entry without a file), and a description long enough to be useful (line 48):

  if (!desc || desc[1].trim().length < 50) fail.push(`${rel}: meta description missing or under 50 chars`);

A second test, test/sibling-overlap.test.cjs, exists for the failure a generated site is most prone to: two pages that say the same thing under different URLs. It takes the main text of every pair of sibling pages, cuts it into overlapping 5-word runs, and fails any pair whose overlap (the Jaccard index) is above the ceiling (lines 15 to 16):

const CEILING = 0.85;
const K = 5;

Another fails the build if a page loads anything from another host beyond the analytics script, and each family has its own test of its pages against its data.

The first week: from a placeholder to a crowded site

The repository’s history shows how fast it grew. The scaffold on 2026-09-22 had one URL in its sitemap: a placeholder front page. The real laundry picker landed just after midnight UTC as commit 3f30bf94 (T-SEMI.1). Counting the URLs in html/sitemap.xml at the last commit of each day (UTC):

End of dayURLs in the sitemap
2026-09-23145
2026-09-24203
2026-09-25325
2026-09-26381
2026-09-27450
2026-09-28641
2026-09-291,212
2026-09-301,034

The end-of-day figure for 2026-09-29 hides the peak. That day had 59 commits, and at commit 1254bbb5 the sitemap held 1,776 URLs. Many were one page per light, per beep or per icon on a single kind of device.

Too many pages: folding short ones back into their charts

That same day the site’s owner wrote that it “might get over cluttered with time”. The answer in the history is a run of commits that made the site smaller. The same evening, robot-vacuum, thermostat, coffee-machine and smoke-alarm lights were regrouped as one page per brand, machine or maker instead of one page per light. Commit bd0a0588 (T-SEMI.95) withdrew two families that had been started after the owner asked for no new ones, removing 45 pages, and a test now keeps them out. Commit 6653fe2a (T-SEMI.99) folded 74 thin phone and smartwatch icon pages into 2. Then the two that give this section its name:

  • Laundry (a1eb3b23, T-SEMI.101): the 27 laundry pages with fewer than 300 words of main text became sections of the one laundry chart, each keeping its own anchor; the other 23 stayed as pages of their own.
  • Electrical marks and energy labels (e31d242e, T-SEMI.102): 18 short pages became sections of their two charts, 11 on the electrical-marks chart and 7 on the energy-label chart.

Short pages that answered one small question were folded into the chart they belonged to, so the chart answers it as a section. By the end of 2026-09-30 the sitemap was down to 1,034 URLs, where it still is.

What search engines and assistants sent

The estate’s visitor export has a section for arrivals from search engines and AI assistants over the last 7 days. Its rows for semiatic add up to 104 arrivals on 39 pages, and that is a floor, not a total: the list stops at 200 rows across all sites, so pages with a single arrival fell off the end. The pages at the top:

PageSearch and assistant arrivals, last 7 days
/oven/7
/oven/fan-top-bottom/6
/dashboard/dipped-beam-indicator/5
/oven/brand/gorenje/5
/oven/top-bottom-heat/4
/oven/pyrolytic/4

The oven family alone accounts for 30 of the 104, on 7 pages. A share of that came from an assistant rather than a search box: the export’s table of where AI assistants land lists five semiatic pages for the same 7 days, 24 arrivals in all, every one from chatgpt.com. They are the dipped-beam light and four of the oven pages above. Two of the listed arrivals landed on the 404 page.

Search Console tells the other half. For 2026-09-09 to 2026-10-07 Google showed semiatic pages 9,488 times and they were clicked 33 times, at an average position of 20.5. Most of those impressions went to answers that were seen and passed over: the page on a thermometer’s “Lo” reading was shown 329 times at an average position of 10 without a click, and the shipping orientation-arrows page 276 times at 8.3. The single most-shown query, 70 impressions at 9.3, asks which way the arrows on a box should point when loading it; another wording of the same question, with “select the best answer, then click submit” still attached, had 30 more. The query table covers 3,601 of the 9,488 impressions (38%), because Google withholds rare queries.

For scale, the same export counts 269 semiatic pageviews in the last 7 days. Those are pageviews, every visit included, not search arrivals.

What it cannot do

  • It does not replace the manual or the standard. Dashboard pages send you to your car’s manual where makes differ, and oven pages say plainly that the same drawing can mean different things on different brands. The laundry pages note that the full standards are sold by their publishers; the site writes meanings from the public sources it cites.
  • Folded pages are gone, not redirected. The repository holds no redirect for the URLs removed in the fold. /laundry/bleach-any/, one of the 27, answered 404 on 2026-10-09; its content is a section of the laundry chart now.
  • The visitor counts are floors. The 7-day list is capped at 200 rows across all sites, so semiatic’s total is at least 104, and the export cannot tell an owner’s own visit from a stranger’s.
  • Being shown is not being chosen. 9,488 impressions turned into 33 clicks. Most of the pages Google shows are passed over.
  • No answer is measured. The numbers above count arrivals and impressions; nothing here measures whether a visitor got their answer.
Static SiteNode.jsSVGProgrammatic SEOTestingData

← Back to Projects

Back to the top