Skip to content

fix(site): stop two documents racing for the same index.html - #2

Merged
w0rxbend merged 3 commits into
mainfrom
fix/landing-page-collision
Aug 9, 2026
Merged

fix(site): stop two documents racing for the same index.html#2
w0rxbend merged 3 commits into
mainfrom
fix/landing-page-collision

Conversation

@w0rxbend

@w0rxbend w0rxbend commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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.html is 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 to index.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 called index — also rendered to index.html.

So two writers, one path. Laika renders documents in parallel, and nothing in Laika or in scripts/site.sh arbitrates between them, so which one "wins" is a race:

  • On the deploy that is live now, the shorter landing page was written over the front of the longer page without truncating the file first. The tail of the loser survived past the winner's closing </html>. The giveaway is that both the corrupt published file and a clean local build are exactly 19188 bytes — the same size, different content.
  • On a local build, the race went the other way: a perfectly clean index.html with the landing page silently missing altogether.

Both builds exited 0. Laika reported nothing. The Site workflow's existing safety check — "did out/site/html/index.html get 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 to index.html.

  • site/src/index.md is renamed to site/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's homeLink changes from Root / "index.md" to Root / "README". That is not a typo, and there is no such file in site/src. It is the path the re-inserted title document ends up at, README being 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, and Root / "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.md is deliberately not in laika.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.md opened 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 from laika.title in directory.conf and 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.sh now checks the staged input tree for a root index.md or README.md and 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 slow mdoc step, so it fails in about a second.

How to test it

Reproduce the bug as it stands on the published site:

curl -s https://worxbend.github.io/codeberg4s/ | grep -c '</html>'

That prints 2. A valid HTML document has one. To see where the seam is:

curl -s https://worxbend.github.io/codeberg4s/ | grep -n -E '<!DOCTYPE|</html>'

</html> at line 97, in a 271-line file.

Now build this branch and check the same thing:

./scripts/site.sh --clean
grep -c '</html>' out/site/html/index.html          # 1

Every page, not just the front page:

find out/site/html -name '*.html' -not -path '*/api/*' \
  -exec sh -c 'test "$(grep -c "</html>" "$1")" = 1 || echo "BAD: $1"' _ {} \;

No output means every page is well-formed.

Confirm the guard fires:

printf '# oops\n' > site/src/index.md
./scripts/site.sh          # fails at step 2 with an explanatory message
rm site/src/index.md

And to look at the result, ./scripts/site.sh --serve then open http://localhost:8080/.

Notes for reviewers

  • How the Helium behaviour was established. Not from the documentation, which does not spell this out. Laika 1.3.2's laika-io jar was unpacked and LandingPageGenerator disassembled to find which document name the theme looks for, where it re-inserts it, and what drives the hasCustomContent flag in landing.template.html that 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.
  • The race is gone by construction, not by timing. There is now one writer for index.html. This is not a fix that makes a collision less likely.
  • Nothing outside site/ and scripts/site.sh is touched. No library module, no test, no build definition.
  • Deliberately left alone, both open questions: the four teaser boxes and the "The four properties that matter" section state the same four things back to back — that is the summary-then-detail structure the existing comment in laika.scala describes, so the prose was not rewritten. And the banner's title colour (SiteInfo.primaryMedium, #a7c6d9 on 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

    • Prevented conflicting site files from producing duplicate landing pages during staging.
    • Added clear diagnostics directing contributors to use the designated landing-page filename.
  • Documentation

    • Updated site content guidance and navigation to reflect the dedicated landing page.
    • Clarified landing-page rendering behavior and file restrictions.
    • Removed the duplicate landing-page heading for cleaner rendered output.

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.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The site build now rejects conflicting index.md and README.md files, resolves the home link through Laika’s suffixless README title document, and aligns navigation, documentation, and landing-page content with landing-page.md.

Changes

Landing page flow

Layer / File(s) Summary
Landing page resolution and collision validation
scripts/site.sh, site/build/laika.scala
The staging script rejects root index.md and README.md files. Laika resolves the home link through the suffixless README title document.
Navigation and site content alignment
site/src/directory.conf, site/README.md, site/src/landing-page.md
The sidebar no longer lists index.md. Documentation identifies landing-page.md as the fixed landing-page filename. The duplicate top-level heading is removed from the landing page.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the site fix that prevents two documents from rendering to the same index.html file.
Description check ✅ Passed The description clearly explains the behavior change, rationale, implementation, commits, and verification steps, but omits the repository checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/landing-page-collision

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@w0rxbend
w0rxbend merged commit 99a7cc4 into main Aug 9, 2026
4 of 5 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6cb2ba8 and 9d7fa09.

📒 Files selected for processing (5)
  • scripts/site.sh
  • site/README.md
  • site/build/laika.scala
  • site/src/directory.conf
  • site/src/landing-page.md
💤 Files with no reviewable changes (1)
  • site/src/landing-page.md

Comment thread site/build/laika.scala
Comment on lines +214 to +218
// 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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/src is reserved.
  • site/README.md#L19-L19: state that only the root of src/ may not contain index.md or README.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant