ACCID Router and Linker Note
What changed today
ACCID gained a router/linker layer to get rid of the old LEVEL-based path mess. The important distinction is that this was not a base href solution. It was a clearer architecture where components name destinations, and a resolver turns those names into the correct links for the context where the page is being rendered.[1]
That shift matters because the old problem was never just “routing” in the framework sense. The real bug class was hardcoded geography: components, views, and runtime scripts baking in assumptions like ../../ or fixed depth, which only worked while files stayed frozen in one place.[1]
The core idea
A component should never emit a physical path. It should emit a destination name, usually a slug, and let a resolver compute the final href late.[1]
That means identity stays stable while geography becomes derived. A component knows spatial, grid, or an article slug; it does not know whether the current page is one level deep, two levels deep, static HTML, or a future React target.[1]
Why this is not base href
The base href approach loses because it is one global mutable assumption. It affects regular links, in-page anchors, JavaScript fetch paths, and form actions all at once, and the same exact files can need different correct bases depending on where they are mounted.[1]
A live counterexample already existed in ACCID: the same bytes could be correct under one mount and also correct under another, which means no single base href could honestly represent both. Relative resolution with a per-page resolver survives that move because the links are derived from where the page actually stands.[1]
The late-binding model
The new model is late binding for links. Components name destinations; the router decides where those destinations live; the linker resolves the correct relative path from the current page to the target page.[1]
That is why the system feels portable. The static target can bind link('spatial') to a relative href, while a React target can bind the exact same semantic destination to a Link to="..." style route without changing the component body.[1]
Router versus linker
The two jobs are related, but they are not the same:[1]
Piece Job Question it answers Router Decides where a page lives “What file or route corresponds to this slug?” [1] Linker Resolves how to reach that page from here “From this page, what href gets me there correctly?” [1]
The breakthrough was realizing that ACCID mostly needed a linker, not just a router. Routing decides where content lives; linking is what frees components from caring about depth.[1]
The portability contract
The summary in the notes is the key contract: the resolver signature is the portability contract. A component that asks for links by name can move between static export, live ACCID runtime, and future framework targets without rewriting its internal markup rules.[1]
That contract only works if it stays small and consistent. One link shape, one vocabulary, and no per-component dialects; otherwise the system fragments back into the same complexity it was trying to escape.[1]
The static slice proof
The first proof was a small static vertical slice around the views collection. It established three separate concerns in one readable file: data, routing, and link resolution.[1]
The important claim was this: change the route prefix in one place, rebuild the slice, and only href outputs should change. Component code should stay untouched, because depth is a routing decision, not a component concern.[1]
Why it felt right
Part of this architecture was genuinely new, and part of it was a convergence with patterns ACCID had already been reaching toward. The convergent half was the idea that identity and geography should bind late from one authority; the new half was injecting link closure into components so they no longer knew path style at all.[1]
That is the piece that makes the “same code, different target” claim real instead of aspirational. A component that names destinations rather than writing paths is finally outside the routing issue.[1]
The kinetic sculpture metaphor
The metaphor from the notes is excellent: relative linking is like a kinetic sculpture attached at the edges. When the whole structure moves deeper or shallower in the directory tree, the joints flex and the .. math changes, but the structure holds because the pieces are connected to each other, not bolted to a fixed wall.[1]
Absolute paths and baked depth behave like a sculpture bolted to the wall. Move the structure, and the bolt tears out because the relationship was fixed to the room rather than derived from the pieces.[1]
Build time versus runtime
The notes also clarified an important architectural split. build.php is not like accid-bridge.php; it is a build-time generator, not a runtime dependency.[1]
That means PHP can stay in the factory. The generator runs when export is requested, emits static HTML and assets, and the shipped product no longer requires PHP on the destination host.[1]
Two honest halves of the system
ACCID now reads more clearly when split into two halves:[1]
- Content/perspectives half: files are written, sidecar JSON is derived, and live views already consume those files.[1]
- Views/templates half: pages reinterpret and re-present the derived content, and these are the places where linking and data resolution must stay portable.[1]
This is why the linker belongs to views and template-level components, not to every content write operation. The filesystem is already the interface between the halves.[1]
Why no rebuild trigger is needed on content write
The notes settled this cleanly: the main views already fetch their content client-side at runtime from JSON files that the bridge already maintains. That means new content “falls into place” without rebuilding the view scaffolds every time a perspective changes.[1]
So the linker/builder only needs to run when scaffold structure changes, such as adding or changing views or altering link topology. It does not belong in the normal save path for content.[1]
The real place the bug was hiding
One of the biggest findings was that the worst depth bugs were not only in visible <a href> markup. They were also inside runtime JavaScript fetch strings, where paths like ../../ were hardcoded into data loading and URL construction.[1]
That means the linking fix has two flavors:
- build-time rewriting for authored markup and static links,[1]
- runtime base computation for client-side fetches and runtime-generated links.[1]
Both are saying the same thing: depth must be derived, never hardcoded.[1]
ACCIDBASE
The runtime answer described in the notes is ACCIDBASE, computed once from the current page location. It gives every runtime fetch and client-side navigation helper a reliable relative path back to the virtual project root, instead of repeating fragile ../../ assumptions in every view.[1]
The important refinement was that ACCIDBASE should be derived inside the same resolver block that already parses the URL for project identity. One parse, two outputs: identity and geography from the same authority.[1]
Components in the template system
The template system’s “pieces” are effectively ACCID’s own simple component model. They are not trying to become a heavy framework abstraction; they are small markup units that need correct data and correct linking injected around them.[1]
That means ZAPPs and promoted HTML pieces still fit perfectly. The missing layer is not replacing them; it is adding a portable link annotation and relinking pass so the same stored code can be re-resolved for different targets later.[1]
Source versus derived links
The notes phrase this cleanly: the link name is source-of-truth, and the href is a derived artifact. That puts links into the same doctrine ACCID already uses for content and indexes: preserve the meaningful source, regenerate the environment-specific derivative whenever needed.[1]
Under that model, stored markup can keep a currently valid href as a cache, while also carrying an internal annotation like “this points to view spatial” or “this points to article foo.” Re-rendering for another target becomes a link derivation pass, not a rewrite of component code.[1]
What this unlocks
This is the kernel of the multi-target export story. The same collection, the same pieces, and the same semantic links can survive across live ACCID pages, static export, and later framework emitters by swapping only the resolver behavior.[1]
That is why this work matters beyond fixing path bugs. It establishes a portability contract that lets ACCID stay understandable while still growing toward multiple publish targets.[1]
Practical doctrine
The cleanest way to remember the new rule set is:
- Components name destinations, never paths.[1]
- Routing decides where things live.[1]
- Linking decides how to reach them from here.[1]
- Runtime fetches also use derived geography, never hardcoded depth.[1]
base hrefis not the solution; late resolution is.[1]
Recommended wording for the project
A concise summary for the architecture is:
ACCID no longer solves pathing with LEVEL math or
base href. Components and template pieces name destinations semantically, and ACCID resolves those names late through a router/linker contract. Routing decides where pages live; linking decides how to reach them from the current page. The result is a system that is readable, portable, and able to target static export, live runtime, or future frameworks without rewriting component code.[1]
Code examples
The architecture gets easier to trust once it is shown as a few small functions instead of a big abstract rule. The examples below show the exact job split: route definition, relative-link resolution, component injection, and runtime base computation.[1]
Example 1: router decides where a slug lives
function route(string $slug): string {
$prefix = getenv('SLICEPREFIX') ?: 'views';
return $prefix . '/' . $slug . '/index.html';
}
How this works:[1]
- The input is a semantic destination such as
gridorspatial, not a path.[1] - The router is the only place that knows where that destination should live in the exported site tree.[1]
- Changing
SLICEPREFIXfrom''toviewsre-roots the collection without touching component code.[1] - That is the proof that depth is a routing decision, not a component concern.[1]
Example 2: linker resolves the path from this page to the target
function relpath(string $fromDir, string $toFile): string {
$from = array_values(array_filter(explode('/', $fromDir), 'strlen'));
$to = array_values(array_filter(explode('/', $toFile), 'strlen'));
while ($from && $to && $from[0] === $to[0]) {
array_shift($from);
array_shift($to);
}
$rel = str_repeat('../', count($from)) . implode('/', $to);
return $rel ?: './';
}
How this works:[1]
fromDiris the directory of the page currently being rendered.[1]toFileis the routed location of the destination page.[1]- The function removes the common path prefix first, which is the shared trunk of the tree.[1]
- It then climbs upward with
../for every remaining segment in the current page path.[1] - Finally, it descends into the target path.[1]
- The result is a correct relative href with no
base href, no leading slash, and no hardcoded site root assumption.[1]
Example 3: make the URL pretty for output
function pretty(string $href): string {
return preg_replace('~/index\.html$~', '', $href) ?: './';
}
How this works:[1]
- The router stores real file destinations like
views/spatial/index.html.[1] - The linker computes a real relative file path to that page.[1]
pretty()trims the finalindex.htmlso the browser sees folder-style URLs.[1]- This keeps the file model honest while still producing readable links.[1]
Example 4: the component never writes ../ itself
function navcomponent(array $views, string $current, callable $link, string $home): string {
$items = '<a href="' . $home . '">Home</a>';
foreach ($views as $v) {
$active = $v['slug'] === $current ? ' class="active"' : '';
$href = $link($v['slug']);
$items .= '<a href="' . $href . '"' . $active . '>' . $v['title'] . '</a>';
}
return '<nav class="accid-nav">' . $items . '</nav>';
}
How this works:[1]
- The component receives a
linkfunction from the outside.[1] - It only knows semantic destinations like
slugand visual properties liketitle.[1] - It never writes
../spatial,/views/spatial, or any other physical path shape.[1] - This is what makes the component portable: the linking policy is injected, not hardcoded.[1]
Example 5: bind the linker to the current page during emit
$route = route($v['slug']);
$fromDir = dirname($route);
$link = fn(string $slug) => pretty(relpath($fromDir, route($slug)));
$asset = fn(string $name) => relpath($fromDir, 'assets/' . $name);
$home = pretty(relpath($fromDir, 'index.html'));
How this works:[1]
- During emit, ACCID knows exactly which page it is currently building.[1]
- It uses that page location to create page-specific helper closures.[1]
- The same component receives different link behavior depending on where it is being rendered, but the component code itself stays unchanged.[1]
- This is the concrete implementation of “late binding.”[1]
Example 6: the runtime side with ACCIDBASE
var dir = location.pathname.split('/').filter(Boolean);
if (!location.pathname.endsWith('/')) dir.pop();
var depth = Math.max(dir.length - 1, 0);
window.ACCIDBASE = depth ? '../'.repeat(depth) : './';
How this works:[1]
- The page inspects its own URL path at runtime.[1]
- If the URL does not end with a slash, the current file-like segment is removed before depth is counted.[1]
depthbecomes “how many segments below project root am I?”[1]ACCIDBASEbecomes the relative path back to the project root from the current page.[1]- Runtime code can then stop hardcoding
../../and instead ask for the computed base once.[1]
Example 7: use ACCIDBASE in fetches instead of hardcoded depth
fetch(ACCIDBASE + window.devproject + '-accid-perspectives-index.json')
How this works:[1]
- The fetch no longer assumes the page sits exactly two levels deep.[1]
- The page derives its own geography first, then uses that geography to build the fetch path.[1]
- If the page moves deeper later, the fetch path changes automatically because
ACCIDBASEchanges automatically.[1] - That removes the main bug class that had been hiding in runtime JavaScript.[1]
End-to-end walkthrough
The easiest way to understand the flow is to trace one destination, such as spatial, from source to output.[1]
Static export path
- The data collection contains a view with slug
spatial.[1] - The router maps
spatialto a real file location such asviews/spatial/index.html.[1] - While emitting another page, such as
views/grid/index.html, the linker computes the relative path fromviews/grid/toviews/spatial/index.html.[1] pretty()trims the finalindex.html, leaving../spatial/.[1]- The component receives that final href through the injected
link()helper and outputs a normal anchor tag.[1] - The component never had to know where
spatiallived or how deepgridwas.[1]
Runtime view path
- A live page loads in the browser at some nested location.[1]
- The resolver block computes
window.devprojectandwindow.ACCIDBASEfrom that location using the same URL authority.[1] - Runtime code uses
ACCIDBASEto fetch shared JSON indexes and build internal navigational links.[1] - If the same view is moved deeper later, the runtime fetches still work because geography is recomputed instead of assumed.[1]
Why this gets us where we need to go
The reason this architecture is strong is that it removes geography from the wrong places. Components stop carrying depth rules, runtime scripts stop guessing ../../, and template pieces can be promoted or exported without being rewritten for every mount point.[1]
That is exactly how ACCID reaches the next stage: one understandable contract for naming destinations, one resolver for deciding where they live, and one linker for determining how to get there from here. The code stays small enough to reason about, but flexible enough to support static export, live runtime, and future framework emitters.[1]