AkurAI Build
Menu

Public repository / master

AkurAI-Framework / site/frontend/spa.html

Latest change 3a1d4dc0df9425a910df82402300a2aeb40318ae - feat(frontend): client-side SPA router with View Transitions by Ólafur Búi Ólafsson

{% include "header" %}
    <main class="container" id="spa-main">
      <header class="page-head">
        <span class="eyebrow">Live demo</span>
        <h1>SPA router</h1>
        <p class="lede">The links below navigate <em>without</em> a full page reload. The
        router intercepts the click, fetches the next page, swaps this
        <code>&lt;main&gt;</code>, and updates the URL — animated with the View
        Transitions API. It's pure progressive enhancement: turn JS off and every
        link is an ordinary navigation. <a href="/docs/spa-router">Read how it works →</a></p>
      </header>

      <section class="split">
        <!-- What it does -->
        <article class="card">
          <header class="rt-head">
            <h3>What just happened</h3>
            <span id="spa-status" class="status">ready</span>
          </header>
          <p class="muted small">Click an in-site link. Watch the URL change and the
          content cross-fade — but notice the page never blanks out or re-runs the
          header. That's a client-side swap, not a reload.</p>
          <p class="muted small">Navigations handled this session:
          <strong id="spa-count">0</strong></p>
          <ul class="rt-log" id="spa-log" aria-live="polite"></ul>
        </article>

        <!-- Try it -->
        <article class="card">
          <header class="rt-head">
            <h3>Try it — instant navigation</h3>
          </header>
          <p class="muted small">These go through the SPA router on this page:</p>
          <nav class="btn-row" id="spa-links">
            <a class="btn btn-secondary" href="/">Home</a>
            <a class="btn btn-secondary" href="/components">Components</a>
            <a class="btn btn-secondary" href="/realtime">Realtime</a>
            <a class="btn btn-secondary" href="/reactivity">Reactivity</a>
          </nav>
          <p class="muted small">And these deliberately <em>opt out</em> — full
          navigation, as the rules dictate:</p>
          <nav class="btn-row">
            <a class="btn btn-secondary" href="https://github.com/olibuijr/AkurAI-Framework" rel="external">GitHub (external)</a>
            <a class="btn btn-secondary" href="/docs" data-no-spa>Docs (data-no-spa)</a>
          </nav>
        </article>
      </section>

      <section class="container cta">
        <h2>Smooth navigation, zero build step.</h2>
        <p class="lede">One native ES module, modern browser APIs, no dependencies.</p>
        <div class="btn-row center">
          <a class="btn btn-secondary" href="/docs/spa-router">SPA router docs</a>
          <a class="btn btn-secondary" href="https://github.com/olibuijr/AkurAI-Framework">Source on GitHub</a>
        </div>
      </section>
    </main>
    <!-- Opt in to the SPA router on this page only. -->
    <script type="module" src="/spa.js"></script>
    <script type="module">
      // Tiny on-page indicator so the demo is visibly "live". Re-binds after each
      // swap via the spa:navigated hook (the swapped <main> brings new elements).
      const start = location.href;
      let count = 0;
      document.addEventListener("spa:navigated", (e) => {
        count++;
        const status = document.getElementById("spa-status");
        const countEl = document.getElementById("spa-count");
        const log = document.getElementById("spa-log");
        if (status) {
          status.textContent = e.detail.mode === "pop" ? "back/forward" : "swapped";
          status.classList.add("ok");
        }
        if (countEl) countEl.textContent = String(count);
        if (log) {
          const li = document.createElement("li");
          const path = new URL(e.detail.url).pathname;
          li.textContent = `${e.detail.mode} → ${path}`;
          log.prepend(li);
          while (log.children.length > 8) log.lastChild.remove();
        }
        void start;
      });
    </script>
{% include "footer" %}