From c5661ec255a1456e6daabb524876aefe21ffe02c Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Mon, 21 Sep 2026 19:25:32 +1000 Subject: [PATCH] Use browser navigation for reliable production page links --- .oxlintrc.json | 1 + app/page.tsx | 7 +++---- app/rules/coverage/page.tsx | 3 +-- components/bribes.tsx | 3 +-- components/draw-piles.tsx | 3 +-- components/ecaz-loyalty.tsx | 3 +-- components/faction-inspector.tsx | 7 +++---- components/force-inspector.tsx | 3 +-- components/game-table.tsx | 13 ++++++------- components/hand-browser.tsx | 3 +-- components/introduction.tsx | 7 +++---- components/revealed-battle.tsx | 3 +-- components/rules-reference.tsx | 13 ++++++------- components/truthtrance.tsx | 3 +-- docs/TRUENAS.md | 10 ++++++++++ 15 files changed, 40 insertions(+), 42 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 2e386fb5..9b23ada5 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -35,6 +35,7 @@ ], "rules": { "no-array-constructor": "error", + "nextjs/no-html-link-for-pages": "off", "no-var": "error", "prefer-const": "error", "prefer-rest-params": "error", diff --git a/app/page.tsx b/app/page.tsx index 5a6ac215..d5da75f6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,5 +1,4 @@ 'use client'; -import Link from 'next/link'; import { useState, useEffect, useCallback, useRef } from 'react'; import { TableTalk } from '@/components/table-talk'; import { GameTable } from '@/components/game-table'; @@ -914,11 +913,11 @@ export default function Home() {
Multiplayer table{' '} - Learn to play - + Learn to play + Rulebook - +
diff --git a/app/rules/coverage/page.tsx b/app/rules/coverage/page.tsx index f41bffea..d46d71a7 100644 --- a/app/rules/coverage/page.tsx +++ b/app/rules/coverage/page.tsx @@ -1,8 +1,7 @@ -import Link from 'next/link'; export default function Rules() { return (
- ← Return to the table + ← Return to the table

Rules coverage ledger

This project targets classic Gale Force Nine Dune, the advanced game, diff --git a/components/bribes.tsx b/components/bribes.tsx index 1b692972..e1399396 100644 --- a/components/bribes.tsx +++ b/components/bribes.tsx @@ -1,6 +1,5 @@ 'use client'; import { useId, useState } from 'react'; -import Link from 'next/link'; import type { Action, GameView } from '@/game/engine'; import { bribeAction } from '@/game/bribe-options'; import { faction } from '@/game/catalog'; @@ -47,7 +46,7 @@ export function Bribes({ game: g, act, busy }: {

{recipient ? `You may pay up to ${recipient.maximum} spice to this recipient.` : `Uncommitted spice: ${options.available}. Choose a recipient to see the payment limit.`}

{recipient?.limitation ?

{recipient.limitation}

: null} {recipient && !options.blocked && !action ? {recipient.maximum < 1 ? 'No spice is available for this payment.' : `Enter a whole number from 1 to ${recipient.maximum}.`} : null} -

Eligible Bureaucrats may divert two spice to the Bank. AI opponents can receive spice, but do not negotiate deals yet. Bribe rules

+

Eligible Bureaucrats may divert two spice to the Bank. AI opponents can receive spice, but do not negotiate deals yet. Bribe rules

); } diff --git a/components/draw-piles.tsx b/components/draw-piles.tsx index c4772941..dcd876e9 100644 --- a/components/draw-piles.tsx +++ b/components/draw-piles.tsx @@ -1,4 +1,3 @@ -import Link from 'next/link'; /** Counts only: this component never receives deck contents or private cards. */ export function DrawPiles({ piles }: { @@ -19,7 +18,7 @@ export function DrawPiles({ piles }: {

Draw piles exclude hands, auction cards and discards. {(piles.treachery === 0 || piles.spice === 0) && ' An empty pile may be replenished from eligible discards.'} - {' '}About these piles + {' '}About these piles

); diff --git a/components/ecaz-loyalty.tsx b/components/ecaz-loyalty.tsx index 7c2dc655..a698d9bf 100644 --- a/components/ecaz-loyalty.tsx +++ b/components/ecaz-loyalty.tsx @@ -1,4 +1,3 @@ -import Link from 'next/link'; import { leaders } from '@/game/cards'; import type { GameView } from '@/game/engine'; import { LeaderInspector } from './leader-inspector'; @@ -16,7 +15,7 @@ export function EcazLoyaltyCard({ loyalty }: { loyalty: GameView['ecazLoyalty']

This Ecaz Traitor Card was randomly set aside face up before dealing. It stays outside the deck for this game and cannot be drawn as a Traitor or Face Dancer. Its leader remains in play under the normal leader rules.

- Loyalty rules + Loyalty rules
; diff --git a/components/faction-inspector.tsx b/components/faction-inspector.tsx index 7242bdbd..5018e9f3 100644 --- a/components/faction-inspector.tsx +++ b/components/faction-inspector.tsx @@ -2,7 +2,6 @@ import { useState } from 'react'; import Image from 'next/image'; -import Link from 'next/link'; import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'; import { Eye, X } from 'lucide-react'; import { FACTIONS, faction, type FactionId } from '@/game/catalog'; @@ -34,8 +33,8 @@ export function FactionSheet({ factionId, advanced }: { factionId: FactionId; ad } ; } @@ -64,7 +63,7 @@ export function FactionInspector({ factionId, advanced }: { factionId: FactionId {house.name} faction sheet Read public faction powers and alliance guidance without changing the game. - Faction sheet help + Faction sheet help diff --git a/components/force-inspector.tsx b/components/force-inspector.tsx index 2da624c4..7c9e4581 100644 --- a/components/force-inspector.tsx +++ b/components/force-inspector.tsx @@ -1,7 +1,6 @@ 'use client'; import { useState, type CSSProperties } from 'react'; -import Link from 'next/link'; import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'; import { Eye, Star, X } from 'lucide-react'; import { FACTIONS, faction, type FactionId } from '@/game/catalog'; @@ -64,7 +63,7 @@ export function ForceInventoryDetails({ player, worlds = null }: { player: Publi

Concealed No-Field

{inventory.marker.name}

Counts as one force for board presence until revealed. It is a separate marker, not an extra physical force counter. Its hidden force value is not shown here. Revelation draws from the reserves available then.

}

Inspecting counters does not move them or commit a Battle Plan. Spice support, temporary powers and leaders may change battle strength without adding counters.

- Force counter guide + Force counter guide ; } diff --git a/components/game-table.tsx b/components/game-table.tsx index 9e3f8410..b3a29ada 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -137,7 +137,6 @@ import { import { validBattleSlotPair } from '@/game/battle-card-slots'; import { stoneBurnerPlanBlock } from '@/game/stone-burner'; import { TECH_TOKENS, ownedTech } from '@/game/tech-tokens'; -import Link from 'next/link'; import { canUseAsKaramaRole, canUseAsTruthtranceRole, @@ -2627,9 +2626,9 @@ export function GameTable({ comparison. Other weapon effects and traitor precedence still apply.

- + Read Stone Burner rules - + {actionButton('Kill both leaders', { type: 'decision', event: g.decision.event, @@ -3286,13 +3285,13 @@ export function GameTable({
What happens in this phase?

{PHASE_HELP[g.phase]}

- Open phase guide ↗ - +
@@ -5529,9 +5528,9 @@ export function GameTable({ several special cards, optional faction reactions, ally support, auction deadlines, and a printed-board connectivity audit.

- + Open the rules coverage ledger - + )} diff --git a/components/hand-browser.tsx b/components/hand-browser.tsx index 07c95acd..4274d505 100644 --- a/components/hand-browser.tsx +++ b/components/hand-browser.tsx @@ -1,6 +1,5 @@ 'use client'; import { Fragment, useId, useState, type ReactNode } from 'react'; -import Link from 'next/link'; import type { Card } from '@/game/cards'; import { browseHand, type HandCategory, type HandSort } from '@/game/hand-browsing'; import { Button } from './ui/button'; @@ -48,7 +47,7 @@ export function HandBrowser({ cards, empty, children }: {
{shown.length} of {cards.length} {cards.length === 1 ? 'card' : 'cards'} shown {filtered && } - Hand browsing help + Hand browsing help
diff --git a/components/introduction.tsx b/components/introduction.tsx index d052fe15..09066ef4 100644 --- a/components/introduction.tsx +++ b/components/introduction.tsx @@ -1,6 +1,5 @@ 'use client'; -import Link from 'next/link'; import { useEffect, useRef, useState, useSyncExternalStore } from 'react'; import { BattleWheel } from './battle-wheel'; import { CardInspector, CardRules } from './card-inspector'; @@ -25,7 +24,7 @@ export function Introduction() { const hydrated = useSyncExternalStore(subscribeHydration, clientSnapshot, serverSnapshot); return
- ← Return to Arrakis + ← Return to Arrakis

LEARN TO PLAY · BASIC GAME

Your first steps on Arrakis

Practice the Basic phases, from the storm and spice blow to alliances, resources, bidding, movement, battle and collection, then take a seat at a real table. These are separate examples; they do not change any saved game.

@@ -164,11 +163,11 @@ function IntroductionLessons() {
  • Use Protect your saved seat and keep its recovery kit private. An invitation identifies the room; the kit restores your seat after cookie loss.
  • Refresh restores a live game through its saved seat. If a request has an uncertain result, use the offered retry rather than repeating the game action.

    -
    Create or join a game →Explore the full reference →
    + }
    - Read the related rules + Read the related rules {state.step < INTRODUCTION_STEPS.length - 1 && }
    diff --git a/components/revealed-battle.tsx b/components/revealed-battle.tsx index b3b5970f..9c2059bf 100644 --- a/components/revealed-battle.tsx +++ b/components/revealed-battle.tsx @@ -1,5 +1,4 @@ import type { GameView } from '@/game/engine'; -import Link from 'next/link'; import { faction } from '@/game/catalog'; import { leaderStrengthLabel } from '@/game/cards'; import { isStoneBurner } from '@/game/battle-cards'; @@ -37,7 +36,7 @@ export function RevealedBattle({ game }: { game: GameView }) {

    Revealed battle plans

    Return to battle decisions -

    Both plans are public. Inspect any played component; use the decision controls to resolve the battle. Component guide · Battle rules

    +

    Both plans are public. Inspect any played component; use the decision controls to resolve the battle. Component guide · Battle rules

    {[battle.attacker, battle.defender].map((id) => { const plan = battle.plans[id]; diff --git a/components/rules-reference.tsx b/components/rules-reference.tsx index a32771f2..c9061f57 100644 --- a/components/rules-reference.tsx +++ b/components/rules-reference.tsx @@ -1,5 +1,4 @@ 'use client'; -import Link from 'next/link'; import { StrongholdCardGallery } from './stronghold-cards'; import { HomeworldCardGallery } from './homeworld-cards'; import { NexusCardGallery } from './nexus-cards'; @@ -94,7 +93,7 @@ function RulesReferenceContent() { return (
    - ← Return to Arrakis + ← Return to Arrakis THE MENTAT’S REFERENCE @@ -107,17 +106,17 @@ function RulesReferenceContent() { Find a phase, card or faction. Follow the action guides, inspect examples and move directly between related rules.

    -

    New to Dune? Try the interactive introduction →

    +

    New to Dune? Try the interactive introduction →

    This reference is growing with the game. Advanced play and expansion rules are unfinished.{' '} - + View the implementation ledger - + {' · '} - + Open feature checklists - +

    diff --git a/components/truthtrance.tsx b/components/truthtrance.tsx index e767150a..fd5266b4 100644 --- a/components/truthtrance.tsx +++ b/components/truthtrance.tsx @@ -7,7 +7,6 @@ import { ForceCountFields, forceCountInputError } from './truthtrance-force-coun import { isKnowledgeFact, knowledgeFactInputError } from '@/game/truthtrance-knowledge'; import type { PlanClaim } from '@/game/battle-promises'; import { useId, useState } from 'react'; -import Link from 'next/link'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { CHEAP_HERO_TRAITOR } from '@/game/traitors'; @@ -130,7 +129,7 @@ export function Truthtrance({

    Truthtrance

    - Question rules · The table + Question rules · The table resumes its previous decision after these questions.

    {w.stage === 'priority' ? ( diff --git a/docs/TRUENAS.md b/docs/TRUENAS.md index 015675c2..655e103b 100644 --- a/docs/TRUENAS.md +++ b/docs/TRUENAS.md @@ -52,6 +52,16 @@ public hosting remains separately gated. HTTP does not encrypt cookies or seat recovery kits. Browser operation IDs now use cryptographic `getRandomValues`, which also works at LAN HTTP origins where `randomUUID` is unavailable. +## Production navigation + +Use native anchors for app page links. The pinned Vinext/Vite production build +renames navigation exports while its Link runtime dynamically looks up their +original names, breaking clicks and prefetch despite successful HTTP responses. +Native navigation keeps query strings, rule anchors, browser history and saved +seat cookies. The Next.js lint preference for client-side Link is disabled for +this intentional choice. Verify page links in the production browser when +changing framework versions; development navigation alone does not cover it. + ## Update and preserve games ### Existing NGINX HTTPS termination