fix(site): stop two documents racing for the same index.html - #2
Conversation
The published landing page was a corrupt file holding two HTML documents: `</body></html>` appeared in the middle, followed by the tail of a second copy of the same page. Readers saw the title stamped over itself, raw SVG path coordinates as visible body text, and the sidebar dumped into the page as a bare bullet list. The cause is that two different documents both rendered to `index.html`. Helium's landing page is not a file anyone writes directly: the theme takes the content of a document named `landing-page`, lifts it out of the content tree, and re-inserts it as the *title document* of the root directory — and a directory's title document is what renders to `index.html`. Our own `site/src/index.md` rendered to `index.html` too. Laika renders documents in parallel and nothing arbitrates between two writers to one path, so the outcome was a coin flip. On the deploy that is live now, the shorter landing page was written over the front of the longer page without truncating it, leaving the tail behind — which is why both files are exactly 19188 bytes. Locally the race went the other way and produced a clean file with the landing page silently missing altogether. Both builds exited 0 and reported nothing. So: rename `site/src/index.md` to `site/src/landing-page.md`, leaving exactly one document that renders to `index.html`. Two consequences worth knowing before touching this again: - `homeLink` now points at `Root / "README"`, with no `.md` and no such file in `site/src`. That is the path the re-inserted title document ends up at, `README` being Laika's default title-document input name. `Root / "landing-page.md"` fails link validation, because by the time links resolve that document is gone from the tree; `Root / "README.md"` fails because the re-inserted document carries no suffix. Both were tried. The reasoning is in a comment, since a suffix-less path to a file that does not exist otherwise reads as a typo. - `landing-page.md` is deliberately absent from `laika.navigationOrder`. It is not a sidebar entry, because by then it is not in the tree.
The landing page rendered "codeberg4s" twice in a row: once as the theme's own header, at 48px on the blue gradient, and then again immediately underneath as an `<h1>` from the Markdown source. That was invisible while `index.md` was a normal page, because a page needs a title of its own. It became visible the moment the file started being rendered underneath a header that already carries the title. The heading is removed rather than reworded. The document's title comes from `laika.title` in `site/src/directory.conf`, and the browser tab title from the site metadata, so nothing depended on it.
The two-writers-to-index.html bug that produced a corrupt published landing page was silent from end to end: Laika reported nothing, the script exited 0, and the workflow's existing guard — "does out/site/html/index.html exist?" — was satisfied by the corrupt file. Nothing stops the same mistake being made again. Adding `index.md` back to `site/src`, or adding a `README.md` there, reintroduces it exactly, and neither filename looks wrong: both are the obvious name for a directory's front page in every other context. So check for them while staging, before mdoc runs, and fail with a message that names the file and says where its content belongs. Costs one loop over two filenames; the alternative is finding out from a reader.
📝 WalkthroughWalkthroughThe site build now rejects conflicting ChangesLanding page flow
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant SiteScript as scripts/site.sh
participant Laika
participant LandingPage as landing-page.md
SiteScript->>SiteScript: Validate staged input
SiteScript->>Laika: Build the site
Laika->>LandingPage: Resolve the suffixless README title document
LandingPage-->>Laika: Supply landing-page prose
Laika-->>SiteScript: Generate the landing page
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@site/build/laika.scala`:
- Around line 214-218: Clarify the collision documentation in
site/build/laika.scala at lines 214-218 to state that only the root of site/src
is reserved; nested index.md and README.md files remain valid. Apply the same
root-only wording in site/README.md at line 19 for src/, with no other changes.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7a3c9312-4cba-4fbb-8250-aff135ad0a5a
📒 Files selected for processing (5)
scripts/site.shsite/README.mdsite/build/laika.scalasite/src/directory.confsite/src/landing-page.md
💤 Files with no reviewable changes (1)
- site/src/landing-page.md
| // The consequence to keep in mind is that site/src must contain no `index.md` and no `README.md`. Either one | ||
| // would also render to `index.html`, and Laika renders documents in parallel — so the two writers race for the | ||
| // same file. That is not hypothetical: it is what produced a published `index.html` holding the landing page | ||
| // spliced on top of the tail of a second, differently-templated copy of the same page. scripts/site.sh fails | ||
| // the build if either file reappears. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Scope the collision rule to the root of site/src.
The implementation rejects only root index.md and README.md files. The current wording can be read recursively and can incorrectly prohibit valid nested README title documents.
- site/build/laika.scala#L214-L218: state that only the root of
site/srcis reserved. - site/README.md#L19-L19: state that only the root of
src/may not containindex.mdorREADME.md.
Proposed wording
- // site/src must contain no `index.md` and no `README.md`.
+ // The root of site/src must contain no `index.md` and no `README.md`.📍 Affects 2 files
site/build/laika.scala#L214-L218(this comment)site/README.md#L19-L19
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@site/build/laika.scala` around lines 214 - 218, Clarify the collision
documentation in site/build/laika.scala at lines 214-218 to state that only the
root of site/src is reserved; nested index.md and README.md files remain valid.
Apply the same root-only wording in site/README.md at line 19 for src/, with no
other changes.
What this does
The documentation site's front page — https://worxbend.github.io/codeberg4s/ — is currently a broken file, and this fixes it.
The published
index.htmlis not one web page. It is two, concatenated: a</body></html>sits in the middle of the file, and a second, differently-styled copy of the same page follows it. A browser renders the whole thing anyway, so what a reader sees is the site title stamped on top of itself, a paragraph of raw SVG path coordinates as visible body text, and the navigation sidebar dumped into the middle of the page as a bare bullet list.Why
Two different documents were both being rendered to the file
index.html.Some background, because none of this is obvious from the source. The site is built with Laika, and its "Helium" theme can generate a landing page — the big blue banner with the project name, the version, and the four teaser boxes. That landing page is not a file anyone writes. Helium builds it by taking whatever content lives in a document named
landing-page, lifting that document out of the site's content tree, and re-inserting it as the title document of the root directory. In Laika, a directory's title document is the one that renders toindex.html. That is how the landing page becomes the site's front page.Meanwhile this repository also had
site/src/index.md, which — being a page calledindex— also rendered toindex.html.So two writers, one path. Laika renders documents in parallel, and nothing in Laika or in
scripts/site.sharbitrates between them, so which one "wins" is a race:</html>. The giveaway is that both the corrupt published file and a clean local build are exactly 19188 bytes — the same size, different content.index.htmlwith the landing page silently missing altogether.Both builds exited
0. Laika reported nothing. TheSiteworkflow's existing safety check — "didout/site/html/index.htmlget created?" — was satisfied by the corrupt file. There was no signal anywhere.How it works
Three commits, one logical change each.
1.
fix(site)— leave exactly one document rendering toindex.html.site/src/index.mdis renamed tosite/src/landing-page.md, which is the filename Helium looks for. Its prose now renders below the banner and the teasers, where it was always meant to be.site/build/laika.scala: the top navigation'shomeLinkchanges fromRoot / "index.md"toRoot / "README". That is not a typo, and there is no such file insite/src. It is the path the re-inserted title document ends up at,READMEbeing Laika's default title-document input name. The two paths you would reach for first were both tried and both fail Laika's link validation:Root / "landing-page.md"fails because by the time links are resolved that document is no longer in the tree, andRoot / "README.md"fails because the re-inserted document carries no file suffix. A comment on the line explains this, since a suffix-less path to a nonexistent file otherwise reads as a mistake.site/src/directory.conf:landing-page.mdis deliberately not inlaika.navigationOrder. It is not a sidebar entry, because by the time the sidebar is built it is not in the tree.2.
style(site)— remove the heading that says the project name twice.index.mdopened with# codeberg4s. That was correct while it was an ordinary page. Rendered underneath a banner that already prints "codeberg4s" at 48px, it printed the same word twice in a row. The heading is deleted rather than reworded: the document title comes fromlaika.titleindirectory.confand the browser-tab title from the site metadata, so nothing depended on it.3.
build(site)— make the mistake impossible to repeat silently.scripts/site.shnow checks the staged input tree for a rootindex.mdorREADME.mdand fails the build if either is present, naming the file and saying where its content belongs. Both filenames are the obvious name for a directory's front page in almost every other tool, so this is an easy mistake to make twice. The check runs during staging, before the slowmdocstep, so it fails in about a second.How to test it
Reproduce the bug as it stands on the published site:
That prints
2. A valid HTML document has one. To see where the seam is:</html>at line 97, in a 271-line file.Now build this branch and check the same thing:
Every page, not just the front page:
No output means every page is well-formed.
Confirm the guard fires:
And to look at the result,
./scripts/site.sh --servethen open http://localhost:8080/.Notes for reviewers
laika-iojar was unpacked andLandingPageGeneratordisassembled to find which document name the theme looks for, where it re-inserts it, and what drives thehasCustomContentflag inlanding.template.htmlthat decides whether the prose is emitted at all. Each candidate fix was then rendered and inspected before being kept. Anyone changing this area should expect to do the same rather than reason from the API surface.index.html. This is not a fix that makes a collision less likely.site/andscripts/site.shis touched. No library module, no test, no build definition.laika.scaladescribes, so the prose was not rewritten. And the banner's title colour (SiteInfo.primaryMedium,#a7c6d9on the blue gradient) is legible but washed out; changing it would shift the whole theme palette, which is a separate decision.Summary by CodeRabbit
Bug Fixes
Documentation