Feat/new layout prototype - #4
Open
edwintantawi wants to merge 11 commits into
Open
Conversation
Counts words in the raw body at 200 wpm, memoized on file content beside the table-of-contents parse. Code samples are counted with prose rather than excluded: readers do spend time on them, usually more per line, so dropping them understates a code-heavy post more than counting them overstates a prose one — and one uniform rule keeps the number predictable for authors. Also lifts frontmatter stripping into a shared helper, which the table-of-contents parser had been doing inline.
Index pages rendering one or two items hide most layout problems, so the design needs dense content to be built and verified against. Fixtures are committed rather than local-only, so a fresh clone reproduces the same environment and regressions at volume stay reproducible. They must never ship. Rather than defining the fixture collections only in development — which would change the generated module's exports between modes and break type-checking against it — the collections always exist and only their include glob changes, becoming one no file can match in production. Fixture arrays are then simply empty there, so content.source can concatenate them unconditionally and no consumer needs an environment check. Also factors the three near-identical collection transforms into shared builders, rather than copy-pasting them a second time for the fixtures.
Routes their collection access through content.source so development renders against fixture content, and carries the new reading-time estimate onto every returned item.
Standalone posts and series parts are separate collections with separate routes, but readers discover them together: a series part tagged react is invisible under that tag unless the two are unioned, which effectively unpublishes most of a series. FeedItem is the shape both flatten to, discriminated by kind so consumers branch on it rather than assembling hrefs from strings — that keeps link building inside the router's type checking. Series roots are excluded on purpose: a root is a table of contents for its parts, not something read on its own, so including it would surface the same material twice. Sorting falls back to title on equal dates, so adding a file cannot reshuffle a list and make prerendered output differ between builds.
Three named widths drive every page: measure is the reading column at roughly 68 characters, breakout is what code, tables, and figures widen to inside an article, and shell frames the page with room for a table-of-contents rail from xl up. Tailwind derives max-w-* utilities from each, so pages reference the intent rather than a magic number. site.ts holds the constants that are neither translated nor per-environment, plus the absolute-URL and GitHub-edit helpers that canonicals, feeds, and article footers will each need.
…ontrols Every page now renders inside a shell: skip link, sticky masthead, a single main landmark, and a footer. Pages drop their own main element, since two would make the landmark ambiguous. Navigation collapses into a bottom drawer below md and sits inline above it. Both variants are always in the markup and switched with utility classes, never a media-query hook — the site is prerendered, so a JavaScript-chosen variant would bake one width's chrome into the HTML that every other width then corrects on hydration. The drawer's contents mount only while open, so the links never exist twice in the accessibility tree. The footer repeats the primary links on purpose. On small screens the header's copy lives inside a drawer that cannot open without JavaScript, so without them the site would have no reachable navigation in that case. The locale switcher is two plain anchors rather than a dropdown: a dropdown cannot open without JavaScript, and a client-side navigation would change the address bar while leaving the page in the previous language, since every string is resolved during the server render. Interface copy is translated into both locales.
Tags were rendered as badges linking nowhere. They now resolve to real pages spanning both collections, so a series part is discoverable by its topic rather than only through its series. Slugging is what merges Cloudflare and cloudflare into one page instead of two half-empty ones. A slug nobody uses is a 404 rather than an empty page: the tag does not exist, and rendering no-results for it would invite crawlers to index an unbounded set of empty URLs. Both pages are plain — they get their design in a later pass — but they are wired to real data, so the header's navigation has no dead ends.
Replaces the animated rail — morphing thumb, boundary dots, scroll-driven edge fades, separate tablet and mobile presentations — with a flat list of anchors and an active highlight. About a fifth of the code, and far less surface to prove WCAG conformance against. The list is flat rather than nested: screen readers announce nesting depth for every item, which is noise when the visual indent already says the same thing and the hierarchy is two levels deep at most. Active tracking observes headings against a band across the top of the viewport rather than the whole of it, which would mark every heading on screen as current at once. When nothing is in the band the reader is inside a section, so the last match is held instead of cleared — otherwise the highlight would be blank for most of the time spent reading. The entry parser is kept as-is; it was the data transform, not the rail.
Article pages need more than the document: where it sits in a sequence, what to read next, and where its source lives. Adjacency differs by kind and the difference is the point. A series part's neighbours are its author's ordering; a standalone post's are chronological, and its sequence excludes series parts — dropping a reader into the middle of an unrelated series is a worse suggestion than none. Relatedness is shared-tag count: blunt, but a signal the reader can verify at a glance. Items sharing nothing are omitted rather than padded in, so a post with unusual tags shows fewer suggestions or none. Collections now also expose the repository-relative source path, which is what an "edit this page" link needs.
The router treats a prefix match as active and stamps aria-current="page" on the result — and it spreads that after activeProps, so the value cannot be overridden, only avoided. Reading /series/x/y therefore announced the header's Series item, both /series breadcrumb crumbs, and the footer's Series link as the current page, alongside the crumb that genuinely was. Matching is now pinned to exact wherever a link is a destination rather than a statement about where the reader is. NavLink derives the header's section highlight separately and exposes it as data-section: "you are somewhere under here" is a visual hint, not a claim about the current page. Also moves focus to the content landmark on client-side navigation. A full page load resets focus and announces the new page; a route change did neither, so following "next article" was silent and the next Tab restarted from the top of the document (WCAG 2.4.3).
Both article routes now render the full reading experience: breadcrumbs, title block with byline and reading time, linked topics, the body, source and copy-link actions, previous/next, and read-next. Series parts add their position and the whole parts list. ArticleLayout frames it. The body column is the full breakout width and .article-column caps everything in it back to the reading measure, so code and tables widen by simply not being capped — no negative margins, nothing to clamp against a width CSS cannot know inside a grid. Wrappers are exempt from the cap rather than capped, since capping a wrapper traps its children. Rails appear where the arithmetic allows rather than at one shared breakpoint: the table of contents needs 15rem beside a 56rem column, which fits from xl; a series part wants a second rail opposite, and 15 + 56 + 15 plus gaps only fits from 2xl. Below each, a bottom-sheet disclosure. Both placements are always in the markup and switched with utility classes, so the prerendered HTML is correct at every width. Cards are text-first with no thumbnail: lists are for scanning, and a column of images would make the layout depend on every author shipping a good one, which a nullable thumbnail says they will not. FeedItemLink is the single place mapping a feed entry to a route, so nothing else assembles an href and every link stays inside the router's type checking. Verified at 390, 820, 1440, and 1700px in both themes.
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.
No description provided.