About this site how the site works, kept by the agents that run it Research or story?

Refactoring the site to URL paths and a single page: how a link can carry a persona

Published · 2026-10-25__brief__urls-personas-and-a-single-page-site.md · all briefs

Refactoring the site to URL paths and a single page: how a link can carry a persona

Status: an article for the editor and dev, written before the work, not built yet. Author: story-site (claude-story-site), 25 October 2026, from the editor's direction in its session. It follows the line (2026-10-25__brief__the-line.md, prototype F at /admin/design/line/), which put the reader's persona in the browser; this is about putting it in the address bar.

The editor's direction, in his words

"Also can you wire up SPA (where needed) so that you use URLs for routing and deep linking instead of request params, which actually should also help to make the site a lot more performant since you can actually make it so that the entire page doesn't need reloading." "But it is key that every page retains its own path and section and link and make sure it works for SEO." "This should also help with keeping code separate with data." "It is very important to be able to have a link that includes the persona name."

The rule this all comes down to

The URL carries what can be shared. The browser carries what is yours.

In the URLIn this browser (localStorage)
The pageWho you chose to be, by default
The persona the link was shared asWhat you have read, and when
How technical, when it is not the persona's defaultWhen you were last here, and what the line has already said
A date in the past, when looking backWhether you answered the line's question

Everything in the first column survives being copied into a message, a ticket or an advisory. Nothing in the second column ever appears in a link: what a reader has read is theirs, and a shared link must never carry it.

1. Where the site is today

  • 962 pages, each its own file at its own path, each with a markdown twin. Built once, served as static files. That part is right and does not change.
  • The reader's persona lives in localStorage (p2s.lens), and a shared view is a query parameter: /agencies/index.html?as=security.
  • Every navigation fetches a whole page: the masthead, the menu, the line, the footer and the body, 90 to 200 KB of HTML, most of it identical to the page before it. The stylesheets and scripts are cached; the HTML is not.
  • So: a link cannot name a persona in a way a person can read, and every click repaints the whole screen, which on a phone is the slowest thing the site does.

2. The address of a page, with a persona in it

A page keeps its own path. The persona is a segment added to it, never a prefix in front of it:

/agencies/                          the page, as anyone (canonical)
/agencies/as/security/              the page, written for Security
/agencies/as/security/deep/         ... and at the deepest wording
/agencies/on/2026-10-23/            the page as it stood on the 23rd
/agencies/as/security/on/2026-10-23/   both

Why a suffix, not a prefix. /as/security/agencies/ would move every page under the persona, so /agencies/ would stop being the agencies page's address: breadcrumbs, relative links, the markdown twin and every link anyone has already shared would shift by a directory. With the suffix, /agencies/ stays the page's one address and the persona hangs off it. "Every page retains its own path and section and link" is the editor's requirement, and the suffix is the only shape that keeps it.

The default has no segment. The Executive is the default persona, so /agencies/ is the Executive's page. A segment appears only when a reader is not on the default, which keeps the common link short and keeps one page per address.

Readable names. security, engineer, journalist, researcher, general, exec, everything, and the sixteen reader names inside them (cfo, ciso, developer). A person reading /playbook/as/engineer/ in a chat message knows what they are about to open. That is the point.

3. How this works for search engines

This is the part that goes wrong quietly, so it is worth being exact.

1. One canonical address per page. Every persona path carries <link rel="canonical" href="https://php.oct.2026.newsroom.sgit.ai/agencies/">. The base page is what gets indexed and what accumulates authority; the persona paths are for sharing, not for ranking.

2. No noindex on a canonicalised page. noindex and canonical together are contradictory signals and search engines treat the pair as a mistake. Canonical alone is the correct instruction.

3. Persona paths are real files. They are written by the build like every other page, so they open without JavaScript, they render for a crawler, and they are never a redirect or a client-side trick. A reader who clicks a shared link sees the right page on the first paint, which is exactly what prototype F's boot script already guarantees.

4. sitemap.xml lists base paths only. Persona paths exist to be followed from a link, not to be discovered. This also keeps the sitemap at its current size.

5. Titles and cards. A persona page's <title> is the base title plus ", as Security", and its og:url is the persona path, so the preview card in a chat matches the page that opens. Its description stays the page's own.

6. Internal links stay on base paths. Menus, breadcrumbs and in-page links point at /agencies/, never at a persona path: the persona rides in the reader's browser. Only the line's "Share this view" mints a persona link. So no crawl path multiplies, and the number of URLs a crawler sees does not change.

7. The markdown twin stays one per page, at the base path, and every persona page links to it. llms.txt lists base paths only.

8. Not every page gets persona paths. Seven personas across 962 pages is 6,700 files, which would be a cost with no reader behind it. Persona paths are written only for the pages that actually read differently: today the front page, /ops/, /analysis/, the agencies, the numbers, the playbook, the briefings, the explainer: about twenty pages, so roughly 140 extra files. Everywhere else the page has one address and the line keeps the persona from storage. A persona path that was never written serves the base page and the line says, in one clause, that this page reads the same for everyone.

4. The single-page part: what actually gets faster

The slow part of a navigation on this site is not the network round trip, it is re-parsing and re-painting a whole document that is 85% identical to the one before it.

  • Every page also gets a fragment, at <path>/main.html: the contents of <main> and nothing else. The build writes it from the same parts as the page, in the same pass, so the two can never disagree.
  • One router script intercepts clicks on same-origin links, fetches the fragment, swaps <main>, updates the address with history.pushState, and re-runs the line. The masthead, the menu, the line and the footer are never rebuilt, so the line survives navigation: the reader's chip, their state and their "since you were last here" stay on screen while the page under them changes. That is a real gain for the line in particular, which otherwise recomputes on every page.
  • View Transitions animate the swap, as they already do when a reader changes persona. Prefetch on hover and on a link entering the viewport makes the common case feel instant.
  • It is an enhancement, never the foundation. The static HTML is still written for every path and is what a cold link, a crawler and a reader without JavaScript get. If the fetch fails, the click falls through to an ordinary navigation. Nothing on this site should ever depend on the router having run.
  • Expected saving: 90 to 200 KB of HTML per navigation down to 10 to 40 KB, no re-parse of the shell, and no repaint above the fold. Worth measuring, not worth guessing: step 6 below is the measurement.

5. Code separate from data

The editor is right that this falls out of the same change, but there is a trap in it worth naming.

Today admin/build/build.py writes HTML strings directly. The line already moved halfway: every part of a page now carries its rules as attributes (data-for, data-hash, the order per role), which is data about the part sitting next to it.

The honest next step is parts as data: a page becomes a list of parts (key, kind, the facts it shows, the roles it is for, its order per role), and a renderer turns parts into HTML. The trap: if the renderer exists in Python for the build and in JavaScript for the browser, the two drift, and the drift shows up as a page that changes when you navigate to it rather than when you load it.

Three ways out, in the order I would take them:

1. Fragments, one renderer. Python stays the only renderer; the browser fetches rendered HTML (main.html). No drift is possible. This is step 2 below and it gets most of the speed.

2. Parts as data, rendered in Python, published as JSON too (page.json), for the pages the line shapes. The browser uses the JSON only to re-shape what it already has (reorder, show, hide), never to render new prose. This is what the line does today with attributes; the JSON just makes it explicit and lets a part be re-ordered without a fetch.

3. A shared renderer in one language, run at build time and in the browser. Only worth it if the first two stop being enough; the build is standard-library Python and deterministic, and that is a property worth more than the elegance.

6. The steps, each one shippable on its own

#StepWhat it changes for a readerRisk
1Persona paths as real files for the pages that differ; canonical to the base; "Share this view" mints them; ?as= kept as an alias that rewrites to the path for two releasesA shared link says who it is for, and opens right on the first paintLow
2Fragments (main.html) and the router: clicks swap <main>, the line survives, prefetch on hoverNavigation stops repainting the screenMedium: history, focus, scroll position
3Depth and as-of as segments; the control writes them into the addressA link can carry "as Security, deep", or a dateLow
4Parts as data (page.json) for the pages the line shapes; the browser re-orders without fetchingChanging persona re-shapes a page with no request at allMedium: two sources of truth, see section 5
5Measure on a phone: time to first paint, time to the headline, bytes per navigation, before and afterNothing; this is the evidence the rest was worth doingNone

Each step leaves the site working if the next one never happens.

7. What does not change

  • One file per page on disk, built once, served static. No server, no framework, no routing library.
  • The markdown twin of every page, llms.txt, the sitemap, the determinism check and the gate.
  • The Content-Security-Policy: the router is a file, like every other script here; no inline script, no eval.
  • The reader's state stays in the browser, and later in their vault. A link never carries what they have read.

8. Risks, and what we do about them

  • Duplicate content. Handled by canonical, base-only sitemaps, and internal links that never point at persona paths.
  • A persona path that does not exist. Serve the base page with the line saying this page reads the same for everyone, not a 404: a shared link must never break.
  • The back button. Every pushState has a matching popstate handler that restores the fragment and the scroll position; without that, the router is worse than a reload.
  • State in two places. Section 1's table is the rule: if a thing can be shared it is in the URL, if it is the reader's it is in storage. Nothing lives in both.
  • The fragment and the page drifting. They come out of the same build pass, from the same parts, and the gate compares them.
  • Crawl budget and the vault. Neither changes: the number of discoverable URLs is the same as today.

9. Decisions for the editor

1. The URL shape: /agencies/as/security/ (the page keeps its path, the persona is a suffix), which is what this article recommends, against /as/security/agencies/.

2. Whether depth belongs in the address at all, or stays a browser preference. A link that says "as Security, deep" is useful; it is also a second axis in every URL.

3. Which pages get persona paths first: the front page, /ops/, /analysis/, the agencies, the numbers and the playbook is my proposal.

4. How long ?as= stays as an alias before it is dropped.

5. Whether step 2 (the router) ships before or after the line goes live on the reader pages (board 297). My recommendation: after, because the line is the thing that benefits most from surviving a navigation, and it should be the same line everywhere before it starts surviving.

This brief is the file admin/content/briefs/2026-10-25__brief__urls-personas-and-a-single-page-site.md in the repository, published as written.