ACCID Router & Linker

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]

PieceJobQuestion it answers
RouterDecides where a page lives“What file or route corresponds to this slug?” [1]
LinkerResolves 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]

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 href is not the solution; late resolution is.[1]

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]

  1. The input is a semantic destination such as grid or spatial, not a path.[1]
  2. The router is the only place that knows where that destination should live in the exported site tree.[1]
  3. Changing SLICEPREFIX from '' to views re-roots the collection without touching component code.[1]
  4. 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]

  1. fromDir is the directory of the page currently being rendered.[1]
  2. toFile is the routed location of the destination page.[1]
  3. The function removes the common path prefix first, which is the shared trunk of the tree.[1]
  4. It then climbs upward with ../ for every remaining segment in the current page path.[1]
  5. Finally, it descends into the target path.[1]
  6. 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]

  1. The router stores real file destinations like views/spatial/index.html.[1]
  2. The linker computes a real relative file path to that page.[1]
  3. pretty() trims the final index.html so the browser sees folder-style URLs.[1]
  4. 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]

  1. The component receives a link function from the outside.[1]
  2. It only knows semantic destinations like slug and visual properties like title.[1]
  3. It never writes ../spatial, /views/spatial, or any other physical path shape.[1]
  4. 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]

  1. During emit, ACCID knows exactly which page it is currently building.[1]
  2. It uses that page location to create page-specific helper closures.[1]
  3. The same component receives different link behavior depending on where it is being rendered, but the component code itself stays unchanged.[1]
  4. 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]

  1. The page inspects its own URL path at runtime.[1]
  2. If the URL does not end with a slash, the current file-like segment is removed before depth is counted.[1]
  3. depth becomes “how many segments below project root am I?”[1]
  4. ACCIDBASE becomes the relative path back to the project root from the current page.[1]
  5. 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]

  1. The fetch no longer assumes the page sits exactly two levels deep.[1]
  2. The page derives its own geography first, then uses that geography to build the fetch path.[1]
  3. If the page moves deeper later, the fetch path changes automatically because ACCIDBASE changes automatically.[1]
  4. 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

  1. The data collection contains a view with slug spatial.[1]
  2. The router maps spatial to a real file location such as views/spatial/index.html.[1]
  3. While emitting another page, such as views/grid/index.html, the linker computes the relative path from views/grid/ to views/spatial/index.html.[1]
  4. pretty() trims the final index.html, leaving ../spatial/.[1]
  5. The component receives that final href through the injected link() helper and outputs a normal anchor tag.[1]
  6. The component never had to know where spatial lived or how deep grid was.[1]

Runtime view path

  1. A live page loads in the browser at some nested location.[1]
  2. The resolver block computes window.devproject and window.ACCIDBASE from that location using the same URL authority.[1]
  3. Runtime code uses ACCIDBASE to fetch shared JSON indexes and build internal navigational links.[1]
  4. 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]