# Mobile-First Web Performance Review — darcsweb
## Summary
The app gets the basics right: a viewport meta tag is set, a static stylesheet is served with a long cache window, and the CSS already has a mobile breakpoint. However, every HTML response is built by concatenating strict `Text` through `T.concat` and then round-tripped through `Data.Text.Lazy` to Scotty's `html` action, which defeats streaming and doubles allocations. HTML-escaping round-trips every byte through `String` (lazy `[Char]`) via the generated `HtmlPure.esc`, so on a 1 MiB diff view the CPU cost dominates TTFB. On mobile, the diff viewer forces horizontal scroll on long lines, tap targets (action links in repo/shortlog rows) are well under the 44 px minimum, and the semantic markup is almost entirely `div` + `table` with a single `<nav>`. Fixing escaping, adding `Cache-Control`/`ETag` on HTML responses, and tightening the tap targets would be the highest-leverage wins.
## Mobile rendering issues
- `src/DarcsWeb/Html.hs:85-88` — the "actions" links on the repo list (`log | tags`) are inline text links with `padding: var(--space-1) var(--space-2)` (static/style.css:370-375), which is roughly 20 px × 14 px. WCAG 2.5.5 / Apple HIG want ≥44 px. Same problem on the shortlog row at `src/DarcsWeb/Html.hs:174-176` ("diff") and on the tag list row at `src/DarcsWeb/Html.hs:278-280` ("details"). On a 360 px viewport these links are essentially unmissable-by-thumb.
- `src/DarcsWeb/Html.hs:241` — the patch diff is wrapped in a single `<pre class="diff">` with `white-space: pre` (static/style.css:631). Any line longer than the viewport triggers horizontal scroll of the diff block. On a phone, reviewing a patch with a path like `src/DarcsWeb/SomeLongModuleName.hs` or a 120-col source line means the user has to pan each line individually. Consider soft-wrapping with a toggle, or at least `overflow-wrap: anywhere` on `.diff-add`/`.diff-del` (the Haskell side should emit a container that CSS can target; today every line is a `<span class="diff-add">`, which is fine — the CSS, not the HTML, is the gap).
- `src/DarcsWeb/Html.hs:93-123` — the summary page has `<h1>` → renderCloneBlock → `<h2>Recent Activity</h2>` → `<h2>Tags</h2>`, but the clone block has no heading of its own, so a screen reader jumps "summary → recent activity" with the clone URL floating in between. Add `<h2>Clone</h2>` above the clone box, or at least `aria-labelledby`.
- `src/DarcsWeb/Html.hs:135` — the clone URL is a `<code>` with `user-select: all` (static/style.css:795). Mobile Safari and Chrome do not honor `user-select: all` as a one-tap select the way desktop does. On a phone the user has to long-press and drag the handles. Recommend rendering the URL inside `<input type="text" readonly value="…">` — it's one tap to focus, one "select all" on the context menu, and it keeps the existing copy-paste-without-shell-metachars invariant from the doc comment.
- `src/DarcsWeb/Html.hs:194-214` — the full-log entry uses `<div class="log-header">` with a flex row containing name + date. CSS flips to `flex-direction: column` at ≤768 px (style.css:899-902). That's fine, but on the server side the date string comes from `relativeDate` and can be "3 weeks ago" or "2026-04-21 08:12:34"; the column is not marked `<time datetime="…">`. Adding `<time datetime="2026-04-21T08:12:34Z">3 weeks ago</time>` would give screen readers and crawlers the machine-readable value "for free" and costs one small formatter. Same at `src/DarcsWeb/Html.hs:83`, `:165`, `:202`, `:230`, `:276`.
- `src/DarcsWeb/Html.hs:290-308` — the tree breadcrumb is a single `<div class="tree-path">` with `white-space: nowrap; overflow-x: auto` (style.css:708). For a deeply nested path on mobile, the user scrolls a narrow sliver of text horizontally just to see where they are. A `flex-wrap: wrap` breadcrumb would be more finger-friendly; the Haskell side doesn't have to change, just the CSS — noting here because the HTML element the CSS keys on is this one.
- `src/DarcsWeb/Html.hs:321` — the tree uses the raw UTF-8 byte sequence `\xf0\x9f\x93\x81` (📁) as the icon. On a phone without a color emoji font (older Android WebView, some Kindle) this renders as tofu. Consider an inline SVG or a CSS pseudo-element with fallback. Low priority, but listed because it is a rendering bug class.
- `src/DarcsWeb/Html.hs:361-364` — the blob view wraps file content in `<pre><code>` with `white-space: pre` (style.css:756). On mobile, a 120-column source line forces horizontal scroll of the entire pre block. At minimum allow a soft-wrap toggle; without it, any minified JS blob becomes unreadable on a phone.
- `app/Main.hs:33-38` — there is no `<link rel="icon">` in `renderPage`. Every mobile browser will issue a `/favicon.ico` request and get a 404 HTML page back (`notFound` at `app/Main.hs:340`). The 404 body is a full styled page, adding request latency and wasted bytes. Either serve `/favicon.ico` as a real static asset (already have `/static/darcs-logo.png`) or emit `<link rel="icon" href="/static/darcs-logo.png">` in `renderPage`.
- `src/DarcsWeb/Html.hs:35` — the viewport is `width=device-width, initial-scale=1`. Good. But consider also `viewport-fit=cover` so the header doesn't collide with the notch on iPhones. Minor.
- `src/DarcsWeb/Html.hs:40-42` — the header logo is a PNG served at a fixed `height: 32px` (26 px on mobile). Without `width=` / `height=` attributes on the `<img>`, the browser reserves no box during layout, so every page has a CLS (cumulative layout shift) jolt. Add explicit `width` / `height` attributes; the intrinsic image aspect ratio is probably known.
- `src/DarcsWeb/Html.hs:41` — `alt="darcs"` on the logo is fine, but the `<a>` around it already has `aria-label="darcs home"`, which means assistive tech announces "darcs home, darcs" — double announcement. Use `alt=""` on the image when the link has an aria-label.
## Performance issues
- `src/DarcsWeb/Html.hs:413-414` — `esc = T.pack . HtmlPure.esc . T.unpack`. Every HTML-escape unpacks `Text` into `String` (lazy `[Char]`), walks it through the generated Coq recursion at `gen/HtmlPure.hs:37-41` which allocates a `String` per input char, then re-packs to `Text`. For the patch detail page (diff + summary can easily be 1 MiB of escaped text) this is O(N) allocations with a huge constant. This will dominate TTFB for any non-trivial patch. A native `Text`-level escape using `T.replace` or a single-pass `Data.Text.Internal.Builder` is ~10–30× faster and cuts allocation by the same factor.
- `src/DarcsWeb/Html.hs:418-425` — `highlightDiff` does `T.concat . map highlightLine . T.lines`. `T.lines` allocates a list of lines, each `highlightLine` calls `esc` (which as noted above round-trips through String), then everything is `T.concat`'d. For a 1 MiB diff this is 2–3 full copies of the payload. A `Text.Lazy.Builder` that consumes one line and writes directly to the response would stream and halve the peak memory. Combined with the next finding, this is the single biggest CPU saving.
- `app/Main.hs:231, 249, 267, 275, 283, 295, 309, 323, 337, 342` — every handler calls `html $ TL.fromStrict $ render…`. `renderPage` builds a strict `Text` via `T.concat`; `TL.fromStrict` wraps it in a one-chunk lazy Text; Scotty then encodes to UTF-8 `ByteString` and hands it to Warp. Three copies of the page live in memory before the first byte hits the socket, and TTFB equals full-render time. Switching `renderPage` to return `TL.Text` (or a `Builder`) built from `<>` on `TL.Text` (or `TLB.Builder`) and using Scotty's `raw` with a streaming body, or at minimum `html` on a `TL.Text` you never converted to strict, removes the redundant copy and lets Warp send the first chunk while later chunks are still being built.
- `src/DarcsWeb/Html.hs:32-53` — `renderPage` takes `[Text]` and `T.concat`s. Every call site passes a single-element list (e.g. `renderShortLog` at line 149 passes `[body]`). The list wrapper is dead weight and pushes callers to pre-concat before calling. Either change the signature to `Text` and concat at one call site, or embrace builders.
- `src/DarcsWeb/Darcs.hs:106-131` — `getRepoPatches` reads the entire patch set on every request to `/repo/:name/shortlog`, `/log`, and even `/summary` (which also calls `listRepos` separately at `app/Main.hs:238`). There is no caching layer. A slow-connection mobile user refreshing the index page triggers a full patch-set read per repo. Even a simple in-process `IORef (Map FilePath (ModTime, [PatchSummary]))` with `_darcs/hashed_inventory`'s mtime as the key would cut repeated loads to O(1). Right now, on a repo with 500 patches, every page view pays the cost of 500 `extractPatchListing` calls.
- `app/Main.hs:236-249` — the summary handler does `listRepos` AND `getRepoPatches` AND `getRepoTags` — three full filesystem traversals of the same repo. `getRepoPatches` already returns everything needed to compute tags (filter `psIsTag`), but the handler calls `getRepoTags` separately, which re-opens the repo and re-reads all patches a second time. Combine the two calls: read once, partition.
- `src/DarcsWeb/Darcs.hs:98-104` — `readRepoDescription` uses `readFile` (which is `String`-based lazy I/O) and is called once per repo in `listRepos`. For a repos directory with many entries, this adds up. `Data.Text.IO.readFile` or `Data.ByteString.readFile` + `decodeUtf8'` is both faster and avoids the lazy-I/O handle leak when an exception interrupts `map`.
- `src/DarcsWeb/Darcs.hs:249` — `getRepoBlob` does `T.pack <$> Prelude.readFile fullPath`. Same issue as above. Worse: this path is for source files up to `maxBlobSize = 10 MiB`; round-tripping through `String` is ~30–50× slower than `Data.Text.IO.readFile` with explicit decode. Even when the file is 1 MB, the lazy-I/O handle stays alive until the last character is consumed.
- `app/Main.hs:228-231, 265-267` — `getCurrentTime` is invoked on every request, then passed to `relativeDate`, which re-parses every patch's date string with `parseTimeM`. On a 500-row shortlog this is 500 `parseTimeM` calls per request. Pre-parse dates once per patch (store `UTCTime` in `PatchSummary`, not `Text`) and format lazily.
- `app/Main.hs:217-215, 381` — HTML responses never set `Cache-Control` or `ETag`. `securityHeaders` adds 6 headers (good), but the repo list and summary don't change between most requests. Even `Cache-Control: private, max-age=30` on `/repo/:name/shortlog` would protect against the "user taps back → refetch" round trip on a slow mobile link. An `ETag: "W/<darcs-inventory-hash>"` + 304 response would be even better.
- `app/Main.hs:183-184` — `mimeType ".js" = "application/javascript"` is missing `charset=utf-8` (only `.css` has it). Modern browsers default to UTF-8 for JS, but explicit is better. Also `.svg` should be served with `charset=utf-8` so that SVG text elements render accentuated characters correctly.
- `app/Main.hs:344-380` — `serveStatic` canonicalizes the path on every request. That is one `realpath(2)` (+ `stat(2)`s) per static asset load. Given every HTML page references `/static/style.css` and `/static/darcs-logo.png`, that's two `canonicalizePath` calls per page view. An in-memory map from path → canonical path (populated lazily, with a size cap) would remove this per-request syscall.
- `app/Main.hs:378-380` — no `Content-Length` or `ETag` for the static file, so Warp's `sendfile` path works but the browser can't 304. `Cache-Control: public, max-age=86400, immutable` is a blunt hammer: `immutable` tells the browser never to revalidate, but there is no content-hash in the URL, so any change to `style.css` won't be picked up for 24 h. Either add a hash-in-query-string (`/static/style.css?v=<hash>`) at HTML emission time, or drop `immutable`.
- `app/Main.hs:205-215` — `securityHeaders` appends `secHeaders` via `(++)` on every response. For each response the WAI header list is rebuilt. Minor, but using `mapResponseHeaders (secHeaders ++)` is the same cost; the real win is to ensure `Content-Security-Policy` has `style-src 'self'` for the site's own stylesheet (which it does) and consider adding `Cross-Origin-Opener-Policy: same-origin` for isolation.
- `app/Main.hs:195-202` — `cspHeader` is a CAF, good. But it is built as `BC.pack $ CspPure.build_csp […]`, and `CspPure.join_directives` recurses through a `String` with `(++)` (gen/CspPure.hs:45-48) which is O(n²) for long directive values. With the current 6 short directives it doesn't matter, but if anyone adds a `script-src` with hashes or nonces this will quadratic-blow-up.
- `src/DarcsWeb/Html.hs:106-111` — `length recentPatches > 0` — use `not (null recentPatches)`. `length` on a list is O(n) and forces spine evaluation of the entire patch list just to decide whether to show a "more" link. Same pattern at line 118 (`length tags > 5` — this one needs the count, but `drop 5 tags` would tell you the same in O(5)).
- `darcsweb.cabal:28-35, 42-54` — the library depends on `text` but not `text-builder` or `bytestring-builder`. Switching the hot rendering path to `Data.Text.Lazy.Builder` would require no new dependency (it's in `text`), so that's a zero-cost unlock.
## Design & semantics
- `src/DarcsWeb/Html.hs:39-44` — the page header is a `<div class="page-header">` containing the logo link and a `<nav class="breadcrumbs">`. The wrapping container should be a `<header>` (HTML5 landmark). Similarly line 48 — `<div class="page-footer">` should be `<footer>`. Line 45 — `<div class="page-body">` should be `<main>`. This change is mechanical, three string replacements, and immediately improves screen-reader landmark navigation on mobile (which relies heavily on "jump to main").
- `src/DarcsWeb/Html.hs:58` — the index page passes an empty breadcrumbs string. That's fine, but the rendered HTML is `<nav class="breadcrumbs"></nav>` — an empty `<nav>` announced as an empty navigation region. Either omit the nav when breadcrumbs are empty, or fall back to a visible site title.
- `src/DarcsWeb/Html.hs:96, 143, 184, 220, 251, 288, 356, 386` — `breadcrumbs` is composed as a raw string starting with ` / `. This is a typographic separator baked into the content; it ought to be a CSS `::before` on the link. More importantly the leading " / " means the breadcrumb announced by a screen reader starts with "slash"; use `aria-current="page"` on the last item and let CSS add the visual separator.
- `src/DarcsWeb/Html.hs:60-63` — `<p class="empty">No repositories found.</p>` is styled (style.css:678) but conveys no status semantics. Use `<p role="status">` or `<div role="status" class="empty">` so assistive tech announces the empty state.
- `src/DarcsWeb/Html.hs:67-76` — the repo list uses a `<table>` with headings "Repository | Description | Last Change | Patches". On mobile (CSS line 831-839) the `<thead>` is hidden (`display: none`). Users on screen readers still hear the table as a table, but without headings they hear "column 1, column 2" for each cell. Either use semantic `<dl>` / `<article>` markup that survives the mobile re-stack, or set `scope="col"` on the `<th>`s (they are not set today) and add `aria-hidden="true"` on the thead's mobile hiding so assistive tech skips it. At `src/DarcsWeb/Html.hs:70`, the `<th>` elements have no `scope`.
- `src/DarcsWeb/Html.hs:164-178` — shortlog row: the two `<a>` elements point to the same URL (`/repo/NAME/patch/HASH`). One is the title link, one is a "diff" link. Duplicate targets waste keyboard tab stops and confuse assistive tech. Either drop the redundant link (title is enough) or make the second link point to a genuinely different view (e.g. raw diff, plain-text, downloadable).
- `src/DarcsWeb/Html.hs:193-213` — each full-log entry uses `<div class="log-entry">`. Semantically these are `<article>` elements with a `<header>` and a content body. Same for patch detail (`src/DarcsWeb/Html.hs:223`).
- `src/DarcsWeb/Html.hs:394-403` — `repoNavBar` emits a `<div class="repo-nav">` with a list of `<a>`. This is clearly a navigation region: use `<nav aria-label="Repository sections">` and put the links in a `<ul>`. The active link should carry `aria-current="page"`, not just `class="active"` (nav link generation at line 408).
- `src/DarcsWeb/Html.hs:199, 225` — `<span class="tag-badge">TAG</span>` uses a text-only badge. It's visible but its role is decorative in context; wrap it in `aria-hidden="true"` and also add visually-hidden "(tag)" after the patch name so screen-reader users know this entry is a tag. Alternately, render the badge as `<abbr title="Tag">TAG</abbr>`.
- `src/DarcsWeb/Html.hs:339-344` — the tree row icon is an emoji cell. Add `aria-hidden="true"` to the `<td class="tree-icon">` so screen readers don't announce "folder emoji" before every filename.
- `src/DarcsWeb/Html.hs:383-386` — `render404` renders `<h1>404 - Not Found</h1>`. The `<title>` is "Not Found", but there's no `status` role or `<main role="main">` landmark. Also the 404 response is served through `html` which encodes a full page; for machine callers (the clone endpoint), a plain `text/plain` response would save bytes.
- `src/DarcsWeb/Html.hs:43` — `<nav class="breadcrumbs">` inside the page header is nested inside another semantic region (header). Fine in HTML5, but give it `aria-label="Breadcrumb"` so assistive tech can pick it out from other `<nav>`s on the page.
- `src/DarcsWeb/Html.hs:29` — header links all resolve to PNG/brand styling. The PNG `darcs-logo.png` referenced in `renderPage` has no `srcset` for HiDPI displays. On retina/Android XHDPI the logo renders blurry. Add `srcset` or switch to an SVG.
## Quick wins
Ordered by impact-to-effort ratio.
1. **Stop round-tripping escape through `String`.** One-line `esc` change in `src/DarcsWeb/Html.hs:413-414` removes a massive allocation bottleneck on any page with substantial text (diff views, blobs).
```haskell
-- Before (src/DarcsWeb/Html.hs:413-414):
esc :: Text -> Text
esc = T.pack . HtmlPure.esc . T.unpack
-- After:
esc :: Text -> Text
esc = T.concatMap escChar
where
escChar '<' = "<"
escChar '>' = ">"
escChar '&' = "&"
escChar '"' = """
escChar '\'' = "'"
escChar c = T.singleton c
```
Keep `HtmlPure.esc` as the formally verified spec, and add a QuickCheck property in `test/Properties/Html.hs` that `esc` agrees with `T.pack . HtmlPure.esc . T.unpack` on arbitrary input. You lose nothing and gain 10–30× on the hot path.
2. **Switch rendering to `Data.Text.Lazy.Builder` and serve without re-conversion.** The current `renderPage` → strict `Text` → `TL.fromStrict` → `html` path at e.g. `app/Main.hs:231` copies the page three times. Minimum-diff version:
```haskell
-- In src/DarcsWeb/Html.hs, switch Text to TL.Text for the top-level return,
-- built via Data.Text.Lazy.Builder (TLB). Each helper returns TLB.Builder
-- and renderPage finalizes via TLB.toLazyText.
import qualified Data.Text.Lazy.Builder as TLB
import qualified Data.Text.Lazy as TL
renderPage :: Text -> TLB.Builder -> TLB.Builder -> TL.Text
renderPage title breadcrumbs body = TLB.toLazyText $
"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n…"
<> TLB.fromText (esc title) <> …
-- Then in app/Main.hs:231:
html $ renderRepoList now (cfgTitle cfg) repos -- no more TL.fromStrict
```
This alone turns the render pipeline into a lazy stream that Warp can start flushing immediately. Even if you keep strict `Text` outputs, dropping the `TL.fromStrict` by returning `TL.Text` directly from `render…` cuts one full copy.
3. **Add `<header>`/`<main>`/`<footer>` and `<nav aria-label="…">` everywhere.** Pure string replacement in `src/DarcsWeb/Html.hs:39, 45, 48, 43, 396`. No CSS change needed — the classes stay. Unlocks screen-reader landmark navigation on mobile (where it matters most because there's no mouse) and improves SEO — zero risk of regression.
```haskell
-- src/DarcsWeb/Html.hs:39-48
, "<header class=\"page-header\">\n"
…
, "<nav class=\"breadcrumbs\" aria-label=\"Breadcrumb\">", breadcrumbs, "</nav>\n"
, "</header>\n"
, "<main class=\"page-body\">\n"
…
, "</main>\n"
, "<footer class=\"page-footer\">\n"
```
And `repoNavBar` at line 395:
```haskell
repoNavBar repoName active = T.concat
[ "<nav class=\"repo-nav\" aria-label=\"Repository sections\">\n"
, …
, "</nav>\n"
]
```
And in `navLink'` at line 408, emit `aria-current="page"` alongside `class="active"`.
4. **Fix tap targets.** The row-action links are well below the 44 px minimum. Minimum diff: wrap them in a class that CSS can target with `min-height: 44px; display: inline-flex; align-items: center; padding: 10px 12px;`. Since `static/style.css` already has the class hooks, this is a CSS-only change, but the HTML must provide a target. At `src/DarcsWeb/Html.hs:86-88, 175-176, 279-280` the links are already inside `<td class="actions">`, so touch the CSS. (Out of scope per brief — note this is the single largest mobile-UX regression.)
5. **Serve a favicon.** Add one line to `renderPage` at `src/DarcsWeb/Html.hs:37`:
```haskell
, "<link rel=\"icon\" type=\"image/png\" href=\"/static/darcs-logo.png\">\n"
```
Kills the 404 refetch on every page load from mobile browsers.
6. **Pre-parse dates, don't re-parse on every render.** In `src/DarcsWeb/Types.hs:15`, change `psDate :: !Text` to `psDate :: !UTCTime` (or add a parallel `psDateParsed :: !UTCTime`). The parse happens once when the `PatchSummary` is constructed in `Darcs.hs:278` (move `parseTimeM` there), and `relativeDate` at `src/DarcsWeb/Html.hs:430` stops being a per-row parse. Saves 500 `parseTimeM` per shortlog request.
7. **Cache `listRepos` / `getRepoPatches` by inventory hash.** Add an `IORef` keyed on `_darcs/hashed_inventory` mtime at the `DarcsWebConfig` level; invalidate on change. Concrete sketch:
```haskell
-- src/DarcsWeb/Types.hs: add a cache field
, cfgPatchCache :: IORef (Map FilePath (UTCTime, [PatchSummary]))
-- src/DarcsWeb/Darcs.hs: new wrapper
getRepoPatchesCached :: IORef … -> FilePath -> IO [PatchSummary]
getRepoPatchesCached ref repoPath = do
mt <- getModificationTime (repoPath </> "_darcs/hashed_inventory")
m <- readIORef ref
case Map.lookup repoPath m of
Just (mt', ps) | mt' == mt -> pure ps
_ -> do ps <- getRepoPatches repoPath
atomicModifyIORef' ref (\m' -> (Map.insert repoPath (mt, ps) m', ()))
pure ps
```
Repos don't change often; mobile users refreshing the same repo on a slow link benefit enormously.
## Deeper restructuring (optional)
- **Stream diffs with `StreamingBody`**. Right now the full diff for a patch is materialized in memory (`src/DarcsWeb/Darcs.hs:267-272` `T.pack $ renderString $ showPatch ForDisplay p`, forced at `:143` with `evaluate (T.length (psDiff p))`) and then HTML-escaped whole (`src/DarcsWeb/Html.hs:241`). For a 50 MB diff (e.g. a vendored dependency), this blocks the main thread and pegs allocation. Switching to Warp's `responseStream` (accessible via Scotty's `raw` / `stream`), and writing the page header, then each diff line as it's produced by the darcs printer, would both halve memory and let the user see the first diff lines while the rest still streams. This requires a non-trivial refactor of `extractPatchFull` to be incremental rather than returning a final `Text`.
- **Drop the strict `Text` → lazy `Text` → `ByteString` triple-copy entirely**. Rewrite `renderPage` as a `Data.ByteString.Builder` over UTF-8-encoded segments, which Warp then converts to lazy `ByteString` with zero additional copy. Since all HTML templates are static ASCII except for user-supplied text (which gets escaped), this is a clean win; `esc` becomes `Text -> Builder`. Bigger payoff than #1 + #2 combined, but a larger diff; would also affect the test suite.
- **Introduce an HTTP caching layer driven by darcs inventory hash**. `darcs show repo --xml-output` exposes an inventory hash; use it as a strong ETag on `/` and every `/repo/:name/*` page. A mobile user pinching through history on a slow LTE link then fetches `304` for pages they've already visited instead of the full HTML. This pairs naturally with #7 in quick wins and makes sense to design together.
- **Replace the generated `HtmlPure` / `PathPure` `String`-based hot paths with `Text`-native extractors, keeping Coq as the spec.** The point of Coq-verified code is correctness. You can keep the Coq definitions and their proofs as the oracle, then implement the production versions against `Text` / `ByteString` and QuickCheck-test equivalence. That way the fast path is no longer bounded by the `String`-based extraction's allocator cost, and the verification story is unchanged. Affects `gen/HtmlPure.hs`, `gen/PathPure.hs`, and all callers in `src/DarcsWeb/Html.hs:414`, `src/DarcsWeb/Darcs.hs:186, 191`, and `app/Main.hs:411`.