document-publishing-architecture

youteacher_analyze is a Next.js App Router site whose whole job is to publish three business documents — a Business Plan, a Marketing Plan, and a Tech Playbook — through one shared shell. There is no CMS and no per-document layout code. Each document is just an ordered list of React chapter components handed to a single DocumentLayout.

One shell, three documents

The root route does nothing but forward the visitor. src/app/page.tsx is a two-line redirect('/businessplan'), so / always lands on the Business Plan as the default entry point. The three real routes are the App Router directories businessplan/, marketingplan/, and techplaybook/, each a page.tsx that returns the same thing in the same shape: a <DocumentLayout> with three props — currentDoc, title, subtitle — wrapping the document's chapters as children.

The only thing that differs between the three pages is the prop values and the list of chapter components inside. Business Plan passes currentDoc="business-plan" and nine chapters (Executive Summary through Appendix). Marketing Plan passes currentDoc="marketing-plan" and fourteen chapters. Tech Playbook passes currentDoc="tech-playbook" and five (PRD, Architecture, Agile Sprints, Quality Assurance, Deployment). The currentDoc prop is a fixed union of exactly those three string literals.

What DocumentLayout composes

DocumentLayout is a client component that assembles the page frame around whatever chapters it is given. It renders, in order:

  • a fixed-position DocumentSelector pinned near the top of the page, told which document is current;
  • a header block with the document's title and subtitle, followed by a static meta-info panel (project overview, team, date, status) that is hard-coded in the layout rather than passed per document;
  • a page body that places a TableOfContents sidebar next to the content region, and drops the chapter children into that content region.

Because the frame lives entirely in the layout, adding a chapter to a document means adding one import and one JSX line to that document's page.tsx. Nothing else has to know.

The content primitives: Chapter and Section

Documents are built from two tiny structural components. Chapter renders a <div> with the chapter's id, the class chapter, a data-type="chapter" marker, and an <h2> title above its children. Section renders a <section> with an optional id and an optional <h3> title above its children. Both are deliberately thin — they carry structure and identity, not content styling. This is what lets the table of contents work without any manual index (see below).

Table of contents built from the DOM, not a manifest

TableOfContents is the clever piece. It keeps no hand-maintained list of chapters. On mount it waits briefly for the DOM to settle, then walks every .chapter element, reads its <h2> as the chapter title and its nested <section> <h3>s as subsections, and builds the tree from what is actually rendered. That is exactly why Chapter and Section bother to emit stable ids and heading tags — the outline is a projection of the rendered document, so a chapter added in page.tsx appears in the sidebar automatically with no second place to update.

It also runs a scroll-spy: it measures each target's absolute offset, tracks which section the reader is currently in as they scroll, highlights the matching link, and expands the parent chapter when it (or one of its subsections) is active. Clicking a link smooth-scrolls to the target with a fixed top offset. The scroll handler is throttled through requestAnimationFrame so the spy stays cheap.

Switching between documents

DocumentSelector is the cross-document navigation. It holds a small static list mapping each document to its label, icon, and route path, marks the current one, and on selection calls the Next.js router to push the chosen path. So the three documents feel like tabs of one publication even though each is an independent App Router route.

Why this shape

The design trades a general content system for a very small, legible one: a document is an ordered list of components, the frame is one shared layout, and the navigation (both the in-document outline and the cross-document switcher) derives itself rather than being maintained by hand. The cost is that content lives in code (each chapter is a React component), not in data — fine for three curated planning documents, and the reason there is no editor, no database, and no per-document boilerplate.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →