落地页与应用的一轮实测修复:可移交的补丁源、少 3.5 MB 的应用、iOS Safari 上真正修好的页头 - #16
Merged
Conversation
…ir own files
marketing-patch.cjs was 1547 lines, of which 640 were a CSS stylesheet inside a
JS template literal and 74 were four languages of prose replacements inside a JS
object. Neither is code. Both now sit next to the patch as what they are:
marketing-corrections.css a stylesheet — lints, diffs and reads like one
marketing-copy-edits.json the copy edits, keyed by language
The shape matters because of where they are going. The generating side holds one
copy deck, a JSON file of {en, zh, ja, fr} strings; the edits are now a file that
can be merged into it rather than sentences to be lifted out of a build script.
The stylesheet is a file the design side can take as it is.
The patch reads both at build time and inlines them, so nothing the reader gets
changes: on all nine built pages the only diff is this stylesheet's own header
comment, rewritten because it told whoever found it to go and edit somewhere else.
One assertion is new. The sticky header paints a copy of the paper texture so its
bottom edge stops being a tone seam, and the original now lives in a different
file from the copy. So the build compares the two rather than trusting them: an
export that re-tuned its texture would otherwise leave the copy quietly
mismatched, and the seam would come back one shade smaller and much harder to
see. Verified by perturbing one gradient stop — the build fails and prints both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both stylesheets on these pages document themselves in prose — nearly every rule
records the measurement behind it, and the corrections file is 70% comment by
weight. That is the point of them and it belongs in the repo. It does not belong
in the bytes eight pages hand every reader: 28 KB per page, 23% of everything
downloaded, inside the <head>, ahead of anything that paints.
The prose stays in marketing-corrections.css and in marketing/, and the pages
ship without it. English / goes 119 KB -> 92 KB, and 30 KB -> 19 KB over the wire;
every page drops about a third of its compressed weight.
The stripper is quote-aware rather than /\/\*[\s\S]*?\*\//g, because the header
texture is an SVG data URL inside url("…") and a future rule could carry a string
with /* in it — a naive strip swallows everything to the next */ and takes live
rules with it. That failure is invisible: the page still builds and is simply
missing a rule. So it is checked two ways. Braces are counted inside the comments
as they are skipped, which makes "no rule was eaten" exact rather than a guess
about size; and the strip must be idempotent, which is the quote-aware way to ask
whether anything comment-shaped survived — a bare search for /* in the output
would fire on the legal content: "/*" the pass deliberately preserves.
Verified against the previous build with a second, independently written stripper:
on all eight pages the CSS rules are identical and the markup outside <style> is
byte-identical.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loregraph | be6a802 | Commit Preview URL Branch Preview URL |
Jul 30 2026, 10:20 AM |
Open Jane Eyre in the reader or the index and the view came down with the error boundary: "Cannot read properties of undefined (reading 'text')" and the same for 'id'. Both views read straight into the first passage and the first entity, and a book with neither has no first of either. This is not an edge case. 82 of the 85 works on the shelf have never been through the pipeline — the library is a manifest, and two of its books carry a real extracted graph. So two of the app's seven views crashed on 96% of its catalogue, and the crash was reachable in one click from the library grid. The Ask view had the same bug and already carries the fix: it renders an empty state instead of reading conv.q. This is that, shared, and it distinguishes two facts the reader cannot otherwise tell apart — a work that has not been extracted, and a work whose graph is still arriving. Split each view in two rather than returning early, because the guard has to run before the hooks: an early return past them changes the hook count between the empty render and the loaded one, which React rejects. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four things on the critical path that the app's first screen does not use. The landing page now makes "open the app" its primary button, so this is what that button cost: 12.5 MB of JavaScript and a 1.8 MB stylesheet, before a pixel. 1. A book's graph now arrives when the book is opened. data-exports.js was a generated 11 MB script — 90% of the bundle, parsed before the first paint — holding the full extracted graphs of two books out of 85. The app opens on the library grid, which draws neither. The generator now writes the shelf metadata (11 KB, still in the bundle, all the grid needs) and one file per book under assets/exports/, fetched by LG_LOAD_BOOK when that book becomes active. No loading state was needed: 82 of the 85 books have no graph at all and every view already renders them, so a book whose file is in flight looks exactly like one of those — which is also why the previous commit had to come first. Bundle: 12,467 KB -> 1,264 KB. 417 KB over the wire. Alice's graph is 0.5 MB gzipped and 西游记's is 2.5 MB, paid by whoever opens them. 2. Three webfont families, not seven. The request also asked for Noto Serif SC, TC, JP and KR at four weights each: 1828 @font-face rules, a 1.79 MB stylesheet (466 KB over the wire, 1.4 s) blocking render in the <head> — and not one element on either page named any of them. Verified in the browser: 1927 faces declared, one font file actually fetched. The app sets CJK in the platform serif, which is what .serif's own fallback has always done; the Noto names appeared only in the 3-D shelf's canvas font string, which now names platform serifs too. 1.79 MB -> 36 KB, 466 KB -> 2 KB over the wire. 3. three.js is fetched when a shelf renders. 670 KB in the <head> of every load for one mode of one view — and the library opens in grid mode. The flat CSS shelf covers the wait, which is the same thing it covers when WebGL is missing: one code path, no new state. 4. The library grid's 85 cover scans are lazy. They were 85 cross-origin requests to Wikimedia before the first paint, for 81 images nobody had scrolled to. The skeleton the grid already draws for a pending scan is what laziness looks like. DOMContentLoaded on a local server: 2113 ms -> 85 ms. Verified: every view for a demo book (pap), a never-extracted book (jane), the small export (alice) and the large one (xyj) — no crashes, the right empty states, and after the fetch the counts match the generator's own report exactly (477 entities, 1010 edges, 42 chunks for alice). Shelf mode loads three and renders its canvas; grid mode never requests it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every collage plate on the page is 1024×1024 or 768×1024, and five of the seven frames declare the ratio their plates actually are. Two did not, and object-fit: cover discards the difference without saying so. .work-card .img was 4/3 around a 3/4 plate. That is not a trim, it is a landscape window onto a portrait picture: the plate scaled up 1.78× and 22% cut off each end. The Botticelli sheet lost all three figures' heads and showed a band of drapery; the fan-shaped calligraphy was enlarged until it touched the frame edges. In both the mount, the margins and the registration marks — the reason these are plates and not stock photographs — were outside the frame. .lab-img was 4/5 around the same 3/4 shape. 6% of the height, which is exactly where the corner registration marks sit, on five plates in a row. Both frames now take the plates' ratio, so cover has nothing left to cut. The corpus cards get taller, which is what a portrait plate shown whole is. The fix depends on two things this repo does not control — the plates come from the design project and the frames from the exported stylesheet — so the build now reads each plate's real dimensions out of its WebP header and compares them with the ratio its frame declares. A re-export that changes either side fails with the arithmetic rather than cropping the artwork again in silence. Verified by putting 4/3 back: the build stops and prints "cover throws away 44% of its height". Not fixed, and recorded in marketing/README.md with the measurements: the five reading-room plates carry ink over 58 / 49 / 28 / 23 / 22% of their area, and no amount of CSS closes a 2.6× spread when two of the objects are landscape inside a portrait frame. Scaling them to match cuts the object rather than its mount. That belongs in the plate generator. Swept /, /zh, /ja, /fr at 305 to 1425 px: no horizontal scroll, nothing clipped, every plate frame 0.750. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three restatements, all structural, so the patch reads the shape rather than the
words and handles all four languages without knowing any of them.
The five reading-room cards each carried a status: "Shipped", "已上线", "実装済み",
"Livré" — one word, five times. A column whose every cell holds the same value says
nothing per row; it is a fact about the set. The cells go and the set's own
annotation takes it: "Five views" becomes "Five views, all built", in each
language's own word for shipped. Same claim, one fifth of the ink. If a future
export gives those five cards different statuses the column starts carrying
information, so the patch checks first and refuses rather than deleting it.
Each plate carried a chip — Text, Graph, Time, Index, Ask — 12px above a heading
reading Reader, Graph, Timeline, Index, Ask. Four are the same word twice. The other
two are nearly the same, which is worse than a duplicate: the reader has to work out
whether "Text" and "Reader" are one view or two. The heading names the view.
"eight passes, one gate" appeared three times — hero footer, call-to-action footer,
page footer — as an ornament in a metadata slot, never as a claim being made. The
page states the fact twice more where it is working: the hero's stat block ("8
passes, one of them a gate") and the pipeline section's heading. Six statements of
one fact on one page; the three ornaments go. Keyed off the hero footer's own copy,
so the words come from the page and not from a list in the build script.
And two type sizes that were one job at two values. Card body copy was 13px in the
reading room, 13.5px in the pipeline steps and capability cards, 14px in the corpus
cards — a half-pixel nobody reads as a distinction, recording only that four
sections were drawn at four different times. The columns are fixed, so unifying at
14px barely moves the measure: 32 characters a line to 31 in the narrowest card, 39
to 36 in the widest. The section lead was 16px in three sections and 17px in one.
Left alone, with the measurements in marketing/README.md: the 26-value gap scale.
Seven of those values are used once and sit beside a well-used neighbour, but
rounding a single-use 7px gap to 8px is a change no reader can see and seven more
values that must not drift. That is a design-token decision upstream.
Verified on /, /zh, /ja and /fr at 375 to 1425 px: zero badges, zero status cells,
zero taglines, the annotation carries the claim in all four languages, no horizontal
scroll, nothing clipped, nothing under 11px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The landing pages have a header, a footer and eight sections, and no main landmark at all — so a screen reader had nothing to jump to and a skip link had nothing to point at. Tabbing in meant eleven controls before the page's own h1, on each of the four pages, every time. The eight sections are now <main id='main' tabindex='-1'>, and a skip link sits in front of the header as the first tab stop, in the words the app already uses for its own. The tabindex is the part that makes it work: without it the browser scrolls to the target and leaves focus on the link, so the next Tab carries on through the header and the reader is back where they started. Wrapping is safe because the sections are plain blocks in normal flow and nothing in either stylesheet selects a direct child of .shell. And the corpus section's two 46px arrow buttons go the way of the filter pills: no handler anywhere in the bundle, nothing to page through — the cards they sit under are a three-column grid, not a scroller — and the only two controls on the page with no accessible name, so a screen reader announced "button, button" and a keyboard user tabbed onto two dead ends. Labelling them would have been labelling nothing. The patch refuses to remove them if a handler ever appears. Verified: clicking the skip link focuses <main>, so the next Tab starts inside the content; the target lands below the sticky header rather than behind it; and no ring is drawn round the whole page. Across all four languages: 56 links and no unnamed link, no unnamed button, no image without alt text, three language links each carrying hreflang and lang, and the heading outline h1 then h2/h3 with no skips. Checked and already right, recorded in marketing/README.md so it is not re-investigated: prefers-reduced-motion is honoured in the stylesheet and again in the reveal script, and the focus ring is the browser's own — nothing sets outline: none. Measuring that ring with a scripted .focus() reports "none" and is wrong, because :focus-visible does not match a programmatic focus. Forced-colours mode is named as untested rather than claimed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sticky header fills itself with the paper colour, so at rest its bottom edge was a hard tone seam — flat paper above, textured ground below. The fix I shipped for that gave it a copy of the ground's three layers with background-attachment: fixed, on the reasoning that this anchors them to the viewport the way body::before's are. That reasoning was wrong, and Chrome hid it. .nav carries transform: translateY(0) and will-change: transform for its show-on-scroll-up slide, which makes it a containing block, and inside one `fixed` attachment is defined to resolve against the element rather than the viewport. WebKit does exactly that: measured on an iPhone 16 Pro simulator, the two radial gradients were laid out across the header's own 1206x255 box, so the header ran 6 levels LIGHTER than the ground at the left edge and 5 levels DARKER at the right — the seam replaced by a gradient running the wrong way across the page. So the header takes the ground's colour rather than its recipe. #eae2cc is the mean of 300 samples of pure ground down the page's margins on that phone. The ground's own gradients swing about 4 levels either side of it, so a flat fill sits inside the variation the eye already reads as texture: worst step across the boundary goes from 6.2 levels to 4.5, and the reversal is gone. Losing the grain inside an 85px band costs nothing — the grain is one level. The texture-copy assertion goes with it. It was guarding a mechanism that does not work, and one fewer painted layer needs no guard. Also checked on that phone, and working: the fade that says the navigation row scrolls sideways (ink darkness 18.7 to 221.3 over the last 40px, exactly as declared), the punctuation tucked against the inline code chips, the plate frames, the CJK adaptation on /zh with all five nav links fitting, the reading room and the quick start. Firefox and Android are still untested and marketing/README.md says so. Reported separately: the Claude Code iOS Simulator integration refuses to start on this machine and wants `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`, which needs a password. These measurements were taken by driving simctl directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
七个提交,按主题分开。全部改动都在浏览器和 iPhone 16 Pro 模拟器里量过。
一、补丁的内容变成可移交的源文件。 原来 1547 行里有 640 行是塞在 JS 模板字符串里的样式表、74 行是四语整句替换。现在各自成文件:
marketing-corrections.css和marketing-copy-edits.json。构建时读入内联,读者拿到的字节没变(九个页面里唯一差别是这份样式表自己的头部注释)。文案那份和生成端的文案表同形,可以直接合并过去。二、页面不再把 28 KB 施工注释发给读者。 两份样式表几乎每条规则都记着量出来的数,修正样式表 70% 重量是注释。英文首页 119 KB → 92 KB,压缩后 30 KB → 19 KB。剥离器认引号、查幂等,不是一个会吞掉活规则的正则。
三、应用启动前要下 12.5 MB。 落地页现在把「打开应用」做成首要按钮,那个按钮的代价是 12.5 MB JavaScript 加一份 1.8 MB 样式表。四件事:
本地 DOMContentLoaded:2113 ms → 85 ms。
四、两个视图在任何没有抽取结果的书上都会崩。 打开《简·爱》的原文阅读或索引,视图直接被错误边界接走。书架上 85 部作品有 82 部没跑过流水线,也就是七个视图里有两个在 96% 的书目上崩溃,而且从书架点一下就能碰到。现在给出空状态,并且区分「没抽取过」和「图谱还在路上」。
五、两个图版画框的比例不对。 每张图版是 1024×1024 或 768×1024,七个画框里五个声明的比例是对的。
.work-card .img是 4/3 套在 3/4 的图版上——不是裁掉一点,是横构图的窗口套竖构图的画:图版放大 1.78 倍,上下各切掉 22%,波提切利那张三个人的头全没了,只剩一片衣褶。.lab-img是 4/5 套同样的 3/4,切掉 6% 的高度,正好是四角定位标记所在的位置。现在画框取图版自己的比例。构建时从 WebP 文件头读真实尺寸跟画框比,改动任何一边都会让构建失败并打出算式。六、页面不再把三件事说不止一次。 五张卡片各带一个相同的状态词(已上线/Shipped/実装済み/Livré)——一列里每格都一样的值,那是关于整组的事实,现在移到那组自己的注解上;每张图版上的小签(Text/Graph/Time/Index/Ask)跟它下面 12px 的标题(Reader/Graph/Timeline/Index/Ask)四个字面重复、两个只是相近,后者比重复更糟;「eight passes, one gate」作为装饰出现三次,而页面另有两处在真正陈述这件事。另外统一了两处字号:卡片正文原来 13/13.5/14 三个值,段首导语 16 和 17。
七、键盘现在有办法越过页头。 页面有 header、footer 和八个 section,没有 main 地标,所以读屏没有可跳的目标、跳转链接没有可指的地方——Tab 进来要按过十一个控件才到 h1,四个页面每次都是。八个 section 现在是
<main id='main' tabindex='-1'>,跳转链接放在页头之前。语料区那两个 46px 箭头按钮跟筛选药丸一样处理掉了:整包里没有处理函数,底下是三列网格不是滚动条,而且是页面上唯一两个没有可读名称的控件。八、页头的纹理复制方式是错的,iOS Safari 说了出来。 上一版给页头复制了一份纸纹并用
background-attachment: fixed,想让它跟body::before锚在同一个视口上。这个推理是错的,而 Chrome 把它遮住了:.nav带着transform: translateY(0)和will-change: transform,这让它成为包含块,而在包含块内部fixed附着按规范是相对元素而不是视口解析的。WebKit 就是这么做的——在 iPhone 16 Pro 上量出来,两道径向渐变被铺在页头自己的 1206×255 盒子里,页头在左边比地色亮 6 级、在右边暗 5 级,接缝变成了一道方向相反的渐变。现在页头取地色的颜色而不是它的配方(#eae2cc,页面边距上 300 个纯地色采样的均值),边界最大落差从 6.2 级降到 4.5 级,而地色自身的渐变本来就有 ±4 级的起伏。在 iPhone 上一并验过没问题的:导航行侧滑的渐隐提示、贴在行内代码块后面的标点、图版画框、中文页的 CJK 适配、阅读室和上手两节。Firefox 和安卓仍未测,README 里写明了。
需要你处理一件事: Claude Code 的 iOS 模拟器集成在这台机器上起不来,它要求先执行
这条命令需要密码,我不能执行。上面这些 iOS 上的测量是我直接调
simctl拿到的,不是走那套集成。🤖 Generated with Claude Code