Kush Digital SolutionsRequest a quote

Lab — concept build

Anchor: cinnabar-on-paper

The shop sign reads downward.

A seal-carving shop in Taipei, set the way a Chinese page is set: columns running top to bottom and right to left, punctuation in its vertical form, two-digit numbers standing upright inside the column. The typeface that draws it is a Traditional Chinese face weighing 15.94 MB whole. This shop downloads 79.9 KB of it.

Not client work. 一方印鋪 does not exist: the shop, its location, its opening hours and every sentence attributed to it are invented, nothing on those pages can be bought, and no message reaches anyone. The typeface figures are the exception and they are not invented — every byte count on this piece is measured by scripts/build-seal-shop-font.mjs and printed from the file that script writes.

01 — One declaration gets you columns. It does not get you typesetting.

writing-mode: vertical-rl turns a paragraph on its side and stacks the lines right to left. That part is free, and it is the part every article about vertical CJK stops at. Everything a Chinese reader would actually notice is downstream of it.

Punctuation moves. In vertical setting a full stop sits in the top right of its character cell rather than the middle, and the brackets 「」 rotate. The substitution that does this is an OpenType feature called vert, applied by the browser automatically — and dropped by default by every subsetting tool, because nobody named it. A page can be fast, valid, correctly marked up, and announce in its first line that whoever built it does not read Chinese.

Numbers stand up. A Latin digit in vertical Han text is rotated 90° by text-orientation: mixed, which is right for a long run — a URL, an email address — and wrong for a number. Up to four digits are set upright, side by side, sharing one character cell.text-combine-upright: all does it. Which runs get it is an editorial judgement rather than a pattern (a year does, a phone number does not), so the runs are marked in the copy itself.

Breaking is stricter. Chinese forbids certain characters at the start of a line and others at the end; line-break: strict is the rule set that enforces the full list, and the default is looser. word-break: break-all — the reflex fix for overflowing Latin — buys nothing here and damages the Latin it was added for.

Writing mode, and nothing else

印面常見 12、15、18 公釐見方,也可以照你手上的舊印配。

With 縱中橫

印面常見 121518 公釐見方,也可以照你手上的舊印配。

The same sentence from the shop, twice. On the left, writing-mode and nothing else: 12, 15 and 18 are lying on their sides, because that is what happens to a Latin digit in vertical Han text unless something says otherwise. On the right, the three runs marked and combined. It is the cheapest possible fix and it is the difference between a number and a piece of English on its side.

The punctuation is not in that comparison, and the reason is a measurement rather than an omission. To show 。 and 「 」 in the wrong place this page would have to switch vert off, and it cannot: HarfBuzz applies the feature as part of vertical shaping and ignores a font-feature-settings that tries to disable it. Measured, by screenshotting 「 at 80px and reading the ink's bounding box out of the pixels — vertical x23–71 y52–77, horizontal x52–77 y8–56, so the bracket does rotate; with the feature "off", byte-identical to "on". So the guarantee that a subset kept its vertical forms is a build check rather than something a page can demonstrate: the subsetter reads the feature back out of the output font's own tables, and a unit test ties that record to the shipped file by SHA-256.

02 — 15.94 MB of typeface, or 79.9 KB

A Latin webfont is a hundred-odd glyphs; you can ship it whole and nobody notices. A Traditional Chinese face has to be able to draw every character a reader might meet, so Noto Serif CJK TC maps 44,777 codepoints and the source file is 23.40 MB. That is the reason most Chinese sites pick one of two bad options: ship an enormous download, or set font-family: sans-serif and accept whatever face the device has — PMingLiU on one machine, a Songti on another, a Simplified face on a third. Neither is a decision. Both are a decision not to make one.

This shop sets 257 distinct Han characters and CJK marks. Cut to those, plus Basic Latin, the same face is 79.9 KB — 0.49% of what it would weigh on the wire uncut.

Measured 2026-08-09 by scripts/build-seal-shop-font.mjs
Source face, as published (OTF)24,541,904 bytes23.40 MB
The same face whole, as a webfont (WOFF2)16,712,768 bytes15.94 MB
Subset to this shop's characters (WOFF2)81,804 bytes79.9 KB
Codepoints in the shipped file353of 44,777

Both "before" figures are here because only one of them is the honest comparison. Setting 79.9 KB against a 23.40 MB desktop OTF would count the compression a browser gets for free — WOFF2 is Brotli-compressed and every browser that can use a webfont can read it. The number a site would actually put on the wire if it shipped this face whole is the middle row, and that is the one the percentage is against.

A subset font is a font with 44,500 glyphs deleted, which is a wonderful thing to ship and a terrible thing to leave unguarded.Add a character to a sentence, forget to re-run the subsetter, and everything builds, every test passes, and a reader in Taipei gets one character from a different typeface in the middle of a word. No build and no screenshot taken on a developer's machine can see it — a Mac has Songti TC installed and substitutes it silently. So the shop's copy lives in one module, the subsetter derives the glyph set from that module, and it records the codepoints the OUTPUT font actually contains along with the file's SHA-256. A unit test re-derives the required characters and fails if any is missing or if the committed file is not the one the manifest describes.

Licence: Noto Serif CJK TC Regular, SIL Open Font License 1.1 — which permits subsetting and redistribution. The licence text ships beside the font at /fonts/seal-shop/OFL.txt.

03 — The switcher lands on the page you were reading

The most common failure in multilingual web work is not a translation error. It is a language switcher that drops you on the homepage. You are three pages in, you switch to English, and your place is gone — and a search engine sees the same thing: every page in one language pointing its alternate at the other language's front door, so the reciprocal signal hreflang needs in order to cluster the two versions never forms. That costs rankings, quietly, on every page.

Here the two versions of a page are modelled as a pair with a shared key, and the switcher resolves the other member of the pair. The two locales deliberately do not share a slug — /lab/seal-shop/zh-hant/keyin against /lab/seal-shop/en/commissions — because localised URLs are correct practice and are exactly what breaks a switcher built by swapping the locale segment. Swap this one and you get a URL that does not exist, which is what makes the end-to-end test able to fail rather than able to pass by luck.

The annotation is reciprocal and self-referential: every document in the set lists every document in the set, itself included, plus an x-default. A cluster where each page names only the other is discarded whole rather than treated as partial, which is why so much hreflang on the web does nothing at all. These are the tags the front page of the shop emits:

The hreflang set on /lab/seal-shop/zh-hant
zh-Hant/lab/seal-shop/zh-hant
en/lab/seal-shop/en
x-default/lab/seal-shop/en

x-default points at the English page. That is not a claim that English is the primary version — the Chinese is, and this piece is about it. It is the instruction for a reader whose language is neither, and leaving it out means a search engine picks for itself.

The four routes

04 — Notes

Most sites that need Chinese take one of two bad options. They ship a full CJK typeface, which is fifteen to twenty megabytes, and the page arrives when it arrives. Or they set font-family: sans-serif and accept whatever the device has — PMingLiU on one machine, a Songti on another, a Simplified face on a third — which is why so much of the Chinese web looks typographically unfinished next to its English equivalent. Neither is a design decision. Both are a decision not to make one.

Cut the face to the characters the page actually sets and the trade disappears. This shop sets 257 distinct Han characters and marks; the same Noto Serif CJK TC that weighs 15.94 MB whole weighs 80 KB cut to them. That is not a rounding down of a big number — it is the difference between a typeface being possible on a Chinese site and not.

The vertical setting is the other half, and it is the half a build cannot check. writing-mode: vertical-rl is one declaration and gets you columns running the right way; what it does not get you is punctuation. In vertical Chinese a full stop sits in the top right of its character cell, not the middle, and the substitution that puts it there is an OpenType feature called vert — which a subsetter drops by default, silently, because nobody named it. A page can be subset, fast, valid, and still announce in its first line that whoever built it does not read Chinese.

The language switcher is the third thing, and it is the one that costs rankings rather than credibility. Most bilingual sites send you to the homepage when you switch, losing your place and — more expensively — failing to form the reciprocal annotation that tells a search engine the two versions are the same page. The two locales here deliberately use different slugs, so a switcher built by swapping the URL segment would land on a 404 rather than quietly appearing to work.

The English half is a translation, not a mirror. It sets horizontally, because English has no vertical tradition, no vertical punctuation forms and no 縱中橫, and a page that kept the setting and changed only the words would be a costume.

Measured build time: 1 hr 18 min.