# Day 8: History, Navigation, Search Routes, and Progressive Enhancement

**Onboarding track:** Alpine.js + htmx, co-equal architecture  
**Verified baseline:** Alpine.js 3.15.12 and htmx 2.0.10 (stable)  
**Verification date:** 2026-07-19  
**Compatibility boundary:** htmx 4.0.0-beta5 is a prerelease line and is not the syntax baseline for these lessons.

## Purpose

Build interfaces whose meaningful state survives direct visits, refresh, sharing, and browser history while Alpine remains responsible only for disposable presentation state.

## How to use this document

Budget a working day. Read the lessons in order, type the examples instead of copying them blindly, perform every mini-task, and finish the exit exercise without looking at the answer notes. The examples use a fictional server-rendered operations application, but the ownership rules are framework-neutral.

A successful day ends with three artifacts:

1. working markup or a small runnable page;
2. written ownership notes for each state value and DOM region;
3. answers to the comprehension check.

## Learning outcomes

You will be able to:

- decide which state belongs in the URL;
- use GET forms, `hx-push-url`, `hx-replace-url`, and `hx-boost`;
- make pushed URLs work as complete direct visits;
- keep local Alpine shell state separate from route state;
- define caching, title, focus, scroll, analytics, and history-restoration policy;
- test enhanced and non-enhanced navigation.

## 1. URL ownership

Put state in the URL when users should be able to:

- bookmark it;
- share it;
- refresh it;
- open it in a new tab;
- navigate back and forward;
- recover it after a session interruption.

Examples:

- search query;
- pagination;
- applied filters;
- selected record;
- remote tab;
- server workflow step.

Keep ephemeral presentation out of the URL:

- dropdown open/closed;
- local menu focus;
- sidebar disclosure;
- temporary unsaved selection;
- density preference unless product requirements say it is shareable.

## 2. Search GET form

```html
<form
    action="/tickets"
    method="get"
    hx-get="/tickets"
    hx-target="#ticket-results"
    hx-push-url="true"
    hx-sync="this:replace"
>
    <label>
        Search
        <input type="search" name="q" value="printer">
    </label>

    <label>
        Status
        <select name="status">
            <option value="">Any</option>
            <option value="open" selected>Open</option>
        </select>
    </label>

    <button type="submit">Search</button>
</form>

<section id="ticket-results">
    <!-- result representation for /tickets?q=printer&status=open -->
</section>
```

The URL is canonical for the applied query. The controls are its editable representation. htmx improves replacement scope.

## 3. `hx-push-url` versus `hx-replace-url`

Use `hx-push-url="true"` when the user has moved to a meaningful new location.

Examples:

- submitted search;
- page 2;
- selected remote record;
- navigable tab.

Use `hx-replace-url="true"` when updating the current location should not create another Back step.

Examples:

- canonicalizing a query;
- replacing a temporary route state;
- some continuously applied filters, if product history policy says Back should skip intermediate states.

Do not choose based only on convenience. Define expected Back-button behavior.

## 4. Direct-visit contract

Every pushed URL must work when:

1. pasted into a new tab;
2. refreshed;
3. opened without htmx;
4. restored after an htmx history cache miss;
5. requested by a crawler or accessibility tool that follows links.

A server may return:

- a complete page for a normal request;
- a fragment for `HX-Request: true`.

Both must represent the same route state.

## 5. Cache variation

If one URL has full-page and fragment variants, shared caches must not mix them.

A common response header:

```http
Vary: HX-Request
```

Also consider:

- authentication and personalization;
- `ETag` per representation;
- history restoration requests;
- CDN configuration;
- fragment-specific cache controls.

Do not use `HX-Request` as authorization. It only influences representation selection.

## 6. Remote tabs

Use Alpine-only tabs when all panels are already present and selection is temporary.

Use route-backed tabs when the selected panel:

- loads server content;
- is bookmarkable;
- changes authorization context;
- deserves browser history.

```html
<nav aria-label="Account settings">
    <a
        href="/account/profile"
        hx-get="/account/profile"
        hx-target="#settings-panel"
        hx-push-url="true"
    >
        Profile
    </a>

    <a
        href="/account/security"
        hx-get="/account/security"
        hx-target="#settings-panel"
        hx-push-url="true"
    >
        Security
    </a>
</nav>

<section id="settings-panel">
    <!-- canonical selected route representation -->
</section>
```

Alpine may manage keyboard focus or a local transition class. It must not maintain a contradictory selected route.

## 7. Pagination

```html
<a
    href="/orders?page=3"
    hx-get="/orders?page=3"
    hx-target="#orders"
    hx-push-url="true"
>
    Page 3
</a>
```

The server should render:

- result rows;
- current page;
- updated pagination links;
- document title in the complete-page representation;
- any needed OOB title or summary policy for fragments.

Do not append pages when the URL implies a single canonical page unless the product deliberately implements an infinite-feed route model.

## 8. `hx-boost`

`hx-boost` enhances ordinary links and forms while preserving their real `href` and `action`.

```html
<main hx-boost="true">
    <a href="/reports">Reports</a>
    <a href="/customers">Customers</a>
</main>
```

Before boosting a broad region, define policy for:

- target and swap;
- document title;
- focus after navigation;
- scroll position;
- analytics;
- global Alpine shells;
- page-specific initialization;
- history restoration;
- sensitive content in history snapshots;
- full-page error behavior.

Boosting is not a substitute for valid routes or complete pages.

## 9. Stable Alpine shells across navigation

A site-level shell may remain outside the boosted target:

```html
<body x-data="{ sidebarOpen: false }">
    <header>...</header>

    <aside x-cloak x-show="sidebarOpen">
        ...
    </aside>

    <main
        id="main-content"
        hx-boost="true"
        hx-target="#main-content"
        hx-select="#main-content"
        hx-swap="outerHTML"
    >
        <!-- current page main -->
    </main>
</body>
```

This is a powerful shape, but it requires testing:

- does the shell state still make sense on the new route?
- does the returned main have the same ID?
- are titles updated?
- does focus move to the new main heading?
- are route-specific scripts initialized and cleaned up?
- does history restore the correct representation?

State should survive navigation only when its meaning survives navigation.

## 10. Sensitive history

htmx history may snapshot content in browser storage. Use the project’s history policy for sensitive pages.

htmx 2 provides `hx-history="false"` to prevent a page snapshot from being stored, causing restoration to request the server instead.

Examples that deserve review:

- health or financial data;
- private messages;
- temporary authentication content;
- pages containing secrets or one-time codes.

Browser storage is not a place for Alpine-owned secrets either.

## 11. Focus and scroll policy

After a partial navigation:

- move focus to the new main heading or a stable landmark when appropriate;
- preserve focus for small in-place updates;
- avoid unexpected scroll jumps;
- allow the browser’s normal link and history behavior where possible;
- announce meaningful content changes.

A narrow server event can indicate `page-content-installed`, or a global integration listener can apply one documented navigation policy. Avoid feature-specific logic in a generic handler.

## 12. History testing matrix

Test each route with:

| Scenario | Expected |
| --- | --- |
| direct visit | complete usable page |
| htmx click | correct partial replacement |
| refresh | same route state |
| Back | previous meaningful route |
| Forward | restored next route |
| new tab | complete page |
| history cache miss | server restores route |
| JavaScript disabled | link/form works |
| expired session | safe authorization path |
| slow response | no stale route/content mismatch |

## 13. Progressive enhancement SOP

1. define route and complete page;
2. implement real links/forms;
3. render server success and error states;
4. add htmx target and swap;
5. add URL push/replace behavior;
6. configure cache variants;
7. add Alpine only for local ergonomics;
8. define title, focus, scroll, and analytics;
9. test history and direct visits;
10. review sensitive snapshot policy.

## 14. Day lab: ticket explorer

Build:

- `/tickets?q=&status=&page=`;
- a complete direct page;
- a fragment result path using the same route;
- search form with URL updates;
- pagination links;
- route-backed ticket details;
- a local Alpine filter disclosure;
- a local density preference;
- Back/Forward behavior;
- a focus policy after route changes.

Record whether each interaction pushes or replaces history and why.

## Mini-tasks

### Task A: route or local?

Classify:

- open sidebar;
- selected remote account tab;
- result density;
- page number;
- unsaved note;
- selected report date range;
- open menu;
- current customer ID.

### Task B: repair fake routing

Replace an Alpine store that owns `currentPage` and fetches JSON with real URLs and htmx history.

### Task C: direct-visit test

Take every pushed URL in a sample page and write the complete-page response it requires.

### Task D: cache policy

Document headers for a route that returns full and fragment representations.

## Comprehension check

1. What kind of state belongs in the URL?
2. When should `hx-replace-url` be used?
3. What must every pushed URL support?
4. Why might `Vary: HX-Request` be needed?
5. What may Alpine own in a route-backed tab component?
6. What policy must precede broad `hx-boost`?
7. Why can stable shell state become wrong after navigation?
8. What does `hx-history="false"` address?
9. Why is direct navigation a server responsibility?
10. What must Back and Forward tests verify?

## Cheat sheet

```text
BOOKMARKABLE / SHAREABLE      -> URL
NEW MEANINGFUL LOCATION       -> hx-push-url
UPDATE CURRENT LOCATION       -> hx-replace-url
COMPLETE DIRECT ROUTE         -> server page
PARTIAL ENHANCEMENT           -> htmx target
LOCAL DISCLOSURE / DENSITY    -> Alpine
FULL + FRAGMENT SAME URL      -> cache variation policy
SENSITIVE PAGE                -> history snapshot review
```

## Exit exercise

Design a route-backed analytics explorer with date range, segment, page, and selected report in the URL. Keep chart display options and panel disclosure local. Specify complete-page, fragment, cache, title, focus, scroll, and history behavior.

## Exit criteria

You pass when any meaningful application location can be copied, refreshed, and restored without relying on an Alpine store.

## Official reference shelf

Use these as the authority when a project version or behavior is uncertain:

- Alpine.js documentation: https://alpinejs.dev/
- Alpine.js directives: https://alpinejs.dev/directives/
- Alpine.js `Alpine.data()` lifecycle and `destroy()`: https://alpinejs.dev/globals/alpine-data
- htmx 2.x documentation: https://htmx.org/docs/
- htmx 2.x reference: https://htmx.org/reference/
- htmx 2.x events: https://htmx.org/events/
- htmx 4 migration guide: https://four.htmx.org/migration-guide-htmx-4/

> Version note: These exercises use htmx 2.x event names such as `htmx:afterSwap` and `htmx:afterSettle`. htmx 4 changes event naming and several defaults. Do not mechanically port the examples without the migration guide.
