SPEC_ANSWERHUB_SHELL.md
AnswerHub — foundations and page frame — spec
What this covers: making AnswerHub read as a finished web page rather than a working scaffold — Chinese typography, a shared page shell, real form labels, a theme control, consistent product vocabulary — plus the family-wide parts of that which apply to SiaoHub as well.
For what is actually built, deployed or reachable, see
ClaudeDocs/HANDOFF.md — status deliberately does not live here.
This spec does not change the family's visual direction. Colors,
tone, the sage accent and the Notion/Linear restraint all stay exactly
as ClaudeDesign/APP_FAMILY.md records them. It changes the things
underneath that: what font renders Chinese, what frame a page sits in,
whether a form field has a label.
It sits alongside SPEC_ANSWERHUB_LIVE.md and reorders part of it: the
browser walk in that spec now happens after this work, against the
rebuilt interface, so the owner is asked for their time once rather than
twice. It supersedes nothing.
No issue tracker exists for this project, so this spec is filed as a
ClaudeDocs/SPEC_* document — the same precedent SPEC_APPS.md
recorded.
Problem Statement
The owner looked at AnswerHub in a browser for the first time and said it plainly: this is not what a normal modern web page looks like.
That judgement is correct, and it is not a matter of taste. Ten specific things are wrong, all of them measurable rather than aesthetic:
Nothing anywhere specifies a Chinese font. The product is written entirely in Traditional Chinese, and the font stack names Geist — loaded with the Latin subset only — followed by the generic system sans. None of those contain a single Han glyph. Every Chinese character on the site is being rendered by whatever the browser falls back to, which differs per operating system and was never chosen by anyone. The Chinese text — which is all the text — is the one part of the page nobody has designed.
The page has no frame. Nine files each hand-write the same magic
number to subtract a header's height from the viewport. Five different
maximum widths are in use across the product. Four pages — settings,
profile, upload and bulk upload — center a small card vertically in an
otherwise empty screen, which is what a scaffold looks like, not a page.
There is no footer anywhere, so content simply stops. And there is no
link from anywhere in AnswerHub back to apps.siao.ai or siao.ai: a
visitor who arrives is stranded.
Forms have no labels. Every field is identified by its placeholder alone, so the label vanishes the moment the user types, and a screen reader has nothing to announce. One raw, unstyled file input survives — the last of the bare HTML form elements the restyle set out to remove.
Smaller things that add up. The document declares lang="en" for an
entirely Chinese product. Every page in the product shares one <title>,
so every browser tab reads "apps.siao.ai". Emoji stand in for icons
while lucide-react sits installed and completely unused, which means
the icons render differently on every platform. There are no
route-level loading, error or not-found states at all, which is the
direct cause of "I clicked and nothing happened" and "it went blank".
Dark mode follows the operating system only, with no control — and the
CSS carries a .dark class that nothing in the application ever
applies, so it is dead code. A CSS variable is defined in terms of
itself and therefore resolves to nothing.
The product cannot decide what it calls things. Pages written before the rename say 作業 (homework); pages written after say 解答 (answer); the homepage manages both 解答 and 答案 in a single sentence. The product is named 答案庫.
The same Chinese-font gap exists in SiaoHub, for a different reason. Its own interface is entirely English, so its chrome is unaffected — but everything it renders comes from Forgejo: repository descriptions, README bodies, file contents, much of it Chinese. Its font stack names only Latin faces too. SiaoHub also has no footer element.
Solution
Fix the foundations and rebuild the page frame, without touching the visual direction.
Chinese gets a font stack that names actual Chinese fonts — the platform faces, in a deliberate order, in both applications. No download, no external request, and no possibility of a missing glyph. The alternative of self-hosting a subsetted face is specifically rejected, and this project has already reasoned its way there once: SiaoHub's preview-card renderer records that subsetting CJK trades a guaranteed correct render for an occasional broken one, in exactly the cases — a rare surname, a technical term — most likely to matter. AnswerHub renders text its users type, so it can never know the character set in advance. The marketing homepage can subset because its copy is fixed and a test catches missing glyphs when the copy changes; AnswerHub has neither property.
One shared page shell replaces nine improvisations. Header, a
content region with a settled width, and a footer — decided once, used
by every page. Pages align from the top and carry a real page heading.
The four floating-card pages become pages. The footer carries the way
back to apps.siao.ai and siao.ai, which is also the fix for
AnswerHub being an island.
Every form field gets a visible label above it, using a shadcn
Label — the first genuinely-needed new primitive since the restyle,
which is the bar APP_FAMILY.md sets for adding one. The last bare file
input gets wrapped.
Dark mode gets a control, adopting SiaoHub's existing mechanism rather than inventing a second one, so the two applications behave identically and the dead class becomes live.
The product settles on 解答, matching its own name, and retires 作業 as the Lwopan-era leftover it is.
Everything is verified, not eyeballed: accessibility checks across every page, and visual snapshots of the pages that carry the most layout decisions, in both themes.
User Stories
Reading Chinese
- As a Traditional Chinese reader, I want the Chinese text to be rendered by a font somebody chose, so that the product does not look different on my phone than on my laptop for no reason.
- As a Traditional Chinese reader, I want every character I type to render, including rare ones, so that a name or a technical term never comes out as a blank box.
- As a reader on a slow connection, I want the Chinese text to cost no download, so that reading is not gated on a font transfer.
- As the owner, I want no font requests to a third party, so that nobody is tracked page-by-page as a side effect of typography.
- As a SiaoHub visitor, I want Chinese repository descriptions and README bodies to render in a chosen font too, so that the content the product exists to display is not the part nobody styled.
- As a screen reader user, I want the document to declare the language it is actually written in, so that my reader pronounces it correctly.
Moving through the product
- As a visitor, I want every page to share one frame, so that moving between them does not feel like moving between separate websites.
- As a visitor, I want pages to begin at the top of the screen, so that a page reads as a page rather than as a lone card in empty space.
- As a visitor, I want every page to announce what it is with a heading, so that I know where I am without inferring it from the content.
- As a visitor, I want the content column to be the same width everywhere it should be, so that the product does not appear to change shape as I navigate.
- As a visitor, I want a footer, so that the page has an ending rather than stopping mid-air.
- As a visitor who arrived at AnswerHub, I want a way back to
apps.siao.aiandsiao.ai, so that I am not stranded inside one application. - As a visitor on a phone, I want the same frame to work at that width, so that the product is not desktop-only by accident.
- As a developer adding a page later, I want the frame to come for free, so that a new page is consistent by default rather than by remembering.
Filling in forms
- As a user filling in a form, I want each field labelled above it, so that I can still see what a field is for after I start typing.
- As a screen reader user, I want each label bound to its field, so that my reader announces what I am editing.
- As a user, I want the file picker to look like part of the product, so that one control does not betray that the rest was styled and it was not.
- As a user, I want placeholder text to give me an example rather than repeat the label, so that it earns the space it takes.
- As a user who submits something invalid, I want to be told what is wrong, so that I can fix it rather than guess.
Waiting, failing, and getting lost
- As a user, I want a page that is loading to say so, so that I do not conclude my click did nothing and click again.
- As a user, I want a page that failed to say so, so that a blank screen is not the error message.
- As a user, I want a wrong URL to give me a page that looks like the product and offers me a way back, so that a typo does not feel like the site is broken.
Light and dark
- As a user, I want to choose light or dark myself, so that I am not bound to whatever my operating system is set to.
- As a user, I want my choice remembered, so that I do not re-pick it every visit.
- As a user, I want the page to render in my chosen theme immediately, so that I never see the wrong theme flash before the right one.
- As a user of both applications in the family, I want the theme control to work the same way in each, so that it is one product with one behaviour.
- As a developer, I want exactly one theming mechanism in the family, so that there is not a second one to keep in sync.
Reading the interface
- As a visitor, I want the product to use one word for the thing it stores, so that I do not wonder whether 作業 and 解答 are different things.
- As a visitor, I want that word to match the product's own name, so that the naming is coherent.
- As a visitor, I want icons that look the same on every device, so that the interface does not change character between my phone and my laptop.
- As a visitor, I want each page's browser tab to name that page, so that I can find it among a dozen open tabs.
- As a visitor sharing a link, I want the page to describe itself, so that what I paste is not identical for every page in the product.
Keeping it this way
- As a developer, I want a page that hand-writes its own height arithmetic to fail the test suite, so that the frame cannot be quietly bypassed.
- As a developer, I want a bare HTML form element to fail the test suite, so that the last one removed stays removed.
- As a developer, I want an emoji used as an icon to fail the test suite, so that platform-dependent glyphs do not creep back.
- As a developer, I want a page outside the shared shell to fail the test suite, so that consistency is enforced by machinery rather than by review attention.
- As the owner, I want accessibility checked on every page rather than one, so that the label work is verified rather than asserted.
- As the owner, I want the pages carrying the most layout decisions snapshotted in both themes, so that a later change that breaks them is caught rather than discovered.
- As the owner, I want the new conventions written into the design reference, so that the next application in the family inherits them instead of re-deciding.
Implementation Decisions
Chinese renders through a platform font stack, naming the Traditional Chinese faces each operating system actually ships, in a deliberate order, with the Latin face kept ahead of them so interface chrome, numerals and code keep the family's existing Latin identity. Applied to both AnswerHub and SiaoHub, because both render Chinese — AnswerHub in its own interface, SiaoHub in the content it fetches from Forgejo.
Self-hosted CJK is rejected, and so is a font CDN. The subsetting argument is settled prior art in this codebase (see the preview-card renderer's own reasoning) and the CDN question is settled by the homepage's standing "no font CDN" decision. The cost accepted in exchange is that glyph shapes differ slightly between platforms, which is the right trade for a functional tool and would not be for the marketing site.
One shared page shell. A single component owns the header, the content region's width and padding, the page heading slot, and the footer. Every page under AnswerHub renders through it. This is the one new architectural seam in this spec, and it is what makes the rest testable: the magic viewport arithmetic, the five different maximum widths and the four vertically-centered cards all disappear because none of them is a per-page decision any more.
Two content widths, not five — one for reading and listing, a narrower one for single-column forms. Both live in the shell; pages choose between them rather than inventing a number.
Pages align from the top and carry a heading. The vertically centered card is retired as a layout.
The footer is one line — product name, and links back to
apps.siao.aiandsiao.ai. Deliberately not a multi-column footer: the pages it would link to (about, terms, privacy) do not exist, and this project's standing decision is not to build placeholder pages. SiaoHub gains an equivalent footer, since it has none either.Form fields get a visible
<label>above them, bound to the field, via a newly added shadcnLabel. Adding it clears the barAPP_FAMILY.mdsets — a real page in current scope needs it. Existing placeholders are demoted to examples or removed where they merely repeated the label. The remaining bare file input is wrapped.Route-level loading, error and not-found states are added and use the shared shell, so a failure still looks like the product.
Theming adopts SiaoHub's existing mechanism — a stored preference, an attribute on the document element, and an inline initialiser that runs before paint so the wrong theme never flashes — rather than a second mechanism beside it. AnswerHub's current dark-mode class selector, which nothing ever applies, is reconciled to it. The control lives in the header. The inline initialiser is the same one the handoff already names as the reason there is no CSP; this does not make that worse, and does not make it better.
Product vocabulary settles on 解答. 作業 is retired everywhere it appears in the interface, and the homepage's stray 答案 goes with it. This is copy only — no route, identifier, database column or test file name is renamed, consistent with the standing deferral of that mechanical pass.
Icons come from
lucide-react, already a dependency and currently unused. Emoji are removed from the interface. Usage stays restrained, per the family's tone.AnswerHub declares Traditional Chinese as its document language. SiaoHub stays English: its interface genuinely is.
Per-page titles and descriptions replace the single application- wide title. The nav homepage keeps its existing title.
The self-referential font custom property is fixed, and the duplicate base-layer declarations that made the font resolution ambiguous are collapsed.
The structural guard grows four rules, each one policing something this work actually removed: no page writing its own viewport-height arithmetic, no bare HTML form element, no emoji in interface markup, and every AnswerHub page rendering through the shell. Deliberately not wider than that — a guard that also polices spacing and color would become a style cop that blocks work for aesthetic reasons, which is a different job from the one this guard does.
This work lands before the browser walk described in
SPEC_ANSWERHUB_LIVE.md, and is deployed before it, so the owner walks the finished interface once instead of walking a scaffold and then walking it again.
Testing Decisions
A good test here asserts what a visitor can observe — what a page renders, what a control does when used, what a checker reports — never which component produced it or what class it carries. This is the seam the existing suite already uses.
Prior art is the existing Playwright suite in this repository, and its one existing accessibility test, which already demonstrates the pattern to extend.
The shared shell is verified through pages, not in isolation. Asserting the footer renders on each page proves the shell is applied; asserting the shell component renders a footer would prove only that the component works while a page bypassing it went unnoticed. That distinction is the same one this project learned when a correct visibility filter coexisted with a page that never applied it.
Accessibility runs on every AnswerHub page, not one. This is the primary verification for the label work: an unlabelled field is precisely what the checker reports, so the fix and its test are the same assertion. Violations must be empty, not merely fewer.
Visual snapshots cover four pages in both themes — the homepage, a submission detail page, a form page and a profile page — chosen because they carry the layout decisions this spec makes. Not every page: a snapshot suite large enough to be a chore is a snapshot suite that gets updated without being looked at, which is worse than none.
Snapshot coverage is honest about what it proves. The suite runs against a development server in a single browser, so a snapshot catches a regression on the machine that runs it and does not constitute a cross-platform guarantee. Recorded here rather than implied by the word "snapshot".
The four structural rules are pinned the way the existing ones are — each rule must be demonstrated to flag a genuine offender, not merely to pass. A guard whose matcher has quietly stopped matching is indistinguishable from a guard that is satisfied, and this repository has now been bitten by that twice in one session.
Vocabulary is asserted where a user would see it: the interface must not render the retired term. This is a real assertion rather than a lint, because the point is what a visitor reads.
The theme control is tested as a round trip — choose a theme, the document reflects it; reload, it is still chosen. Testing that a click handler fires would test the implementation.
The done bar is typecheck, unit, end-to-end and a production build, in both repositories. SiaoHub's suite is substantially larger, and its numbers are re-measured rather than cited from the handoff, which was already found stale once this session.
Out of Scope
- The visual direction itself. Colors, the sage accent, the
restrained tone and the component conventions in
ClaudeDesign/APP_FAMILY.mdare not reopened. - Aesthetic refinement — spacing, weight, typographic rhythm, micro- interaction polish. A separate round, deliberately, so this one can finish.
- A logo. Still the open design decision it has always been.
- The browser walk, the identity work and credential rotation —
those belong to
SPEC_ANSWERHUB_LIVE.mdand remain blocked on the owner. - Public reachability — no DNS, tunnel or certificate work.
- Renaming routes, identifiers or test files to match the product name.
- Blog and knowledge base. Unreachable from navigation; not restyled, not brought into the shell beyond what comes for free.
- SiaoHub's own layout. It receives the Chinese font stack and a footer, nothing else. Its interface language stays English and its page structure is untouched.
- Live search results.
Further Notes
The owner's complaint was aesthetic in form and structural in substance. "This does not look like a normal modern web page" turned out to decompose into ten specific, checkable defects, of which the largest — no Chinese font at all, on an entirely Chinese product — was invisible in every code review this product has had. Worth remembering the next time a vague visual complaint arrives: look before deciding it is taste.
The Chinese text was rendering by accident. No stylesheet named a Chinese font; the characters appeared because browsers fall through to a default CJK face when the named fonts have no glyph. Everything looked fine on the owner's Mac, which is exactly the condition under which this kind of defect survives indefinitely.
The subsetting question was already answered elsewhere in this codebase, in a comment on a server-side font loader, for a different surface, with reasoning that transfers exactly. It was found by looking rather than reasoned from scratch. When a decision feels familiar, it is worth checking whether this project has made it before — the reasoning is usually better than a fresh attempt and the consistency is free.
apps-siao-aistill has no designated remote, so nothing here can be pushed to a shared location. SiaoHub additionally cannot reach Forgejo at all while the credential is outstanding, so any change to it deploys the same way the last one did — by bundle over SSH. Both are recorded as constraints on this work, not problems it solves.