Repository source: docs/plans/archive/forte_thread_selection_url_sync_step2_feature_description.md
Forte Thread Selection URL Sync — Step 2: Feature Description
Problem
Selecting a thread on Forte's board never updates the URL — only tag/sort changes do — so reloading or sharing a link loses the current thread selection. Worse, even the URLs that do carry a thread/reply today (permalinks, reply redirects) only restore that state via client-side JS after the page has already painted its default "nothing selected" view — so every one of those loads flashes the wrong state first.
User Stories
As a Forte board-view reader, I want reloading the page to restore both my tag filter and my selected thread, so I don't lose my place.
As anyone opening a Forte URL that names a tag, thread, and/or reply, I want the correct state visible in the very first paint, not flashed-in a moment later by JS.
As a Forte board-view reader, I want a deliberate click on a thread to be a real, back-able navigation, so pressing Back returns me to the thread I was just on.
As a Forte board-view reader browsing quickly with arrow keys or Prev/Next, I don't want every thread I pass through to pile up in my browser history.
As a reader who wants different history behavior than the default, I want that adjustable without waiting on a settings UI to ship.
Core Requirements
A thread-row click updates the URL (
selected={threadId}, tag preserved) viahistory.pushState, matching the existing tag/sort pattern — Back returns to the previous state.Arrow-key row navigation and Prev/Next toolbar stepping update the URL via
history.replaceStateby default — no new back-history entry per step (per approved Step 1, Option C).This click-vs-stepping split is a per-viewer
localStoragepreference, not a hardcoded rule, with three named values:"click-only"(default, used when unset or invalid) — click pushes, keyboard/button stepping replaces."always"— every selection change pushes, including keyboard/button stepping."never"— every selection change replaces, including clicks.No settings UI ships in this cycle; the value is only reachable by setting it directly (e.g. via devtools).
The board renders
selected=/created_post_id=server-side, the same way it already renderstag=: the requested thread's row and content article are marked selected/visible, its Reply button starts enabled (not the usual disabled-until-JS state), and the requested reply gets its highlight class — all directly in the initial HTML, mirroring the existing$selectedTag-drivenhiddencomputation inpaned_board_thread_list.php, not a new mechanism. This closes the flash for every URL carrying this state (permalinks and reply redirects included, not just new URL-sync traffic), by construction. Scroll position is the one thing this can't cover — the existing client-side scroll-into-view calls (list row and highlighted reply) still run after load and remain necessary; SSR only guarantees the content is already correct, not already scrolled to.selectedwins overtagon conflict: if the requested thread doesn't carry the requested tag (only reachable via a hand-edited URL, or a thread's tags changing after a link was shared — normal clicking/pushState never produces this, since a click can only target an already-visible, correctly-tagged row), the server keeps the thread selected and drops the tag filter to "All Threads" rather than the other way around.Invalid or unrecognized
selected=/created_post_id=values degrade to exactly today's default (no selection) — reusing the same validate-and-fallback patternresolveForteBoardTag()already applies totag=, not a new error path.Browser Back/Forward through pushed thread-selection states (no server round-trip involved) still needs client-side handling — the existing
popstatelistener only restores tag/sort today and must also restore thread selection.No change to tag/sort URL syncing, which already works today.
Shared Component Inventory
$selectedTag-driven server-sidehidden/visibility rendering (paned_board_thread_list.php) — pattern reused, extended with the equivalent forselected=/created_post_id=acrosspaned_board_thread_list.php,paned_board_content_pane.php, andpaned_thread_reply_tree.php.The board's client-side
initialSelected/initialCreatedPostIdrestore logic (paned_board_reader.js) — kept, not removed: harmless to re-apply over already-correct server-rendered state, and still the only mechanism available for thepopstate(no-reload) case.pushStateIfChanged/urlForState— extended: gains the current thread selection alongside tag/sort; a parallelreplaceStatepath added for the non-pushing case.This project's existing
localStorageread pattern (theme/thread-density prefs, try/catch-wrapped, silent fallback) — reused for the new history-mode preference.
Simple User Flow
Reader opens any Forte URL carrying
tag=/selected=/created_post_id=(typed, reloaded, or shared) — the correct tag, thread, and highlighted reply are all visible immediately, no flash.Reader clicks a thread — URL updates, tag preserved; Back will return to the prior state.
Reader arrow-keys or Prev/Next's through several threads — URL keeps reflecting the current thread without piling up history entries.
Reader presses Back after clicking a thread — the previous thread/tag state is restored without a full page reload.
A reader (or developer) who sets the
localStoragepreference to"always"or"never"gets that behavior instead, with no code changes.
Success Criteria
Opening
/forte?tag=X&selected=Y&created_post_id=Zdirectly (fresh load, not a same-session click) shows the correct tag, selected thread, highlighted reply, and an already-enabled Reply button in the first rendered response — verifiable without JS running.Opening a URL where
selected=Ydoesn't carry tagXshowsYselected under "All Threads," notYunselected under tagX.Opening a URL with a garbage or nonexistent
selected=/created_post_id=renders identically to a URL with neither present.Reloading
/forteafter selecting a thread restores that exact thread and tag filter.A thread click is a real back-history entry; Back returns to the previous thread/tag state without a server round-trip.
Rapid keyboard/button stepping through many threads does not create one history entry per step, by default.
Setting the
localStoragepreference to"always"or"never"measurably changes this behavior.Tag/sort URL syncing, and the existing permalink/reply-redirect flows, are completely unaffected in outcome (only when the correct state becomes visible changes).