Links & navigation

Goribu provides a few components for enhanced navigation with sensible defaults.

On this page

Import Link from goribu. It renders an anchor that works before JavaScript loads:

Navigation links
import { Link } from "goribu";

// Keep normal anchor behavior such as opening a link in a new tab.
export default function Menu() {
  return (
    <nav>
      <Link href="/posts">Posts</Link>
      <Link href="/login">Sign in</Link>
      <Link href="/reports" prefetch={false}>Reports</Link>
    </nav>
  );
}
Navigation links
import { Link } from "goribu";

// Keep normal anchor behavior such as opening a link in a new tab.
export default function Menu() {
  return (
    <nav>
      <Link href="/posts">Posts</Link>
      <Link href="/login">Sign in</Link>
      <Link href="/reports" prefetch={false}>Reports</Link>
    </nav>
  );
}
  • History replacement: replace updates the current history entry instead of adding one.
  • Prefetch default: prefetch defaults to true. Hovering briefly or pressing an eligible link starts loading its destination.
  • Disable prefetch: Use prefetch={false} when prefetching is not useful or is costly.
  • Anchor props: Link accepts normal anchor props, including target, download, and event handlers.
  • Click cancellation: Your onClick runs first and can cancel navigation with preventDefault().

The NavLink is useful for a menus that need active url styling:

Active navigation
import { NavLink } from "goribu";

// Highlight the current section using pathname-based matching.
export default function SettingsMenu() {
  return (
    <nav>
      <NavLink
        href="/settings"
        className={({ isActive }) => isActive ? "active" : undefined}
      >
        Settings
      </NavLink>
      <NavLink href="/settings/profile" end>Profile</NavLink>
    </nav>
  );
}
Active navigation
import { NavLink } from "goribu";

// Highlight the current section using pathname-based matching.
export default function SettingsMenu() {
  return (
    <nav>
      <NavLink
        href="/settings"
        className={({ isActive }) => isActive ? "active" : undefined}
      >
        Settings
      </NavLink>
      <NavLink href="/settings/profile" end>Profile</NavLink>
    </nav>
  );
}
  • Active state: An active NavLink gets aria-current="page". Let NavLink own that attribute.
  • Path matching: Matching ignores query strings and hashes, respects segment boundaries and normally includes child paths.
  • Exact matching: end requires the exact pathname. The / pathname is always matched exactly.

Use navigate() when navigation follows an action with no meaningful anchor:

Event handler
import { navigate } from "goribu";

// Continue after an application-controlled event.
export async function continueToDashboard() {
  await navigate("/dashboard", { replace: true });
}
Event handler
import { navigate } from "goribu";

// Continue after an application-controlled event.
export async function continueToDashboard() {
  await navigate("/dashboard", { replace: true });
}
  • Return value: navigate() returns a Promise.
  • Link semantics: Prefer Link when users should be able to copy the URL, open another tab or navigate without JavaScript.

After changing data with your own fetch, navigate to the current URL to refresh a Worker-rendered page:

Event handler
await fetch(`/posts/${id}/like`, { method: "POST" });
await navigate(location.href);
Event handler
await fetch(`/posts/${id}/like`, { method: "POST" });
await navigate(location.href);
  • Worker-rendered refresh: Navigating to the current URL runs the page's GET handler and meta again, fetches fresh route data and updates the page in place.
  • State and history: A refresh with it of the current URL keeps component state and adds no history entry. replace is not needed.
  • Pre-rendered refresh: A prerendered page has fixed build-time data. Navigating to its current URL only scrolls to the top.
  • Form results: A Form result already updates the page itself.

Where navigation gets its data

When you follow a Link, call navigate or press Back/Forward in the browser, Goribu fetches the destination page's data and swaps it in without a full document load. Where that data comes from depends on how the destination route renders:

  • Prerendered pages are served from Static Assets in production. The Worker does not run. The page receives the same props and metadata that were saved at build time and changing the query string does not recompute them. This also applies to prefetch and to redirects that land on a static page.
  • Dynamic pages are requested from the Worker on every navigation, just like a document load. Middleware, GET handlers, cookies and request-scoped resources such as req.d1 all run as usual.

If a page must reflect the current request, such as the signed-in user or the query string, make it dynamic with a handler, middleware or prerender = false. See Rendering for the rules.

In development every navigation goes through the Worker, because build-time snapshots do not exist yet. Test static behavior with a production build.

In both cases, Goribu loads the destination's JavaScript and styles before it updates the screen and URL. The data request uses an internal endpoint under /_goribu/. Always navigate with Link, NavLink, or navigate instead of calling it directly. There is no router store or navigation hook to keep in sync.

Page state across navigation

Changing a query parameter keeps the pathname, so navigation updates the page in place and preserves its component state:

Browser event handler
import { navigate } from "goribu";

export async function showNotesTab() {
  const url = new URL(location.href);
  url.searchParams.set("tab", "notes");
  await navigate(url.href);
}
Browser event handler
import { navigate } from "goribu";

export async function showNotesTab() {
  const url = new URL(location.href);
  url.searchParams.set("tab", "notes");
  await navigate(url.href);
}
  • Same-path navigation: Query changes such as ?tab=notes, revisiting the current URL and in-place Form renders update the page in place, preserving component state and DOM.
  • State across paths: To keep state across different paths of the same route, put it in the URL or browser storage.

History and scrolling

  • History entries: Each navigation adds one history entry or replaces the current one with replace. A redirect chain records only its final destination.
  • Scrolling: A new page scrolls to its hash target or to the top. Back and Forward restore the saved scroll position.
  • Current URL: Navigating to the URL already on screen adds no history entry and scrolls to the top. A static page keeps its state without refetching while a dynamic page fetches fresh data.

Native behavior and fallbacks

  • Browser-handled links: Goribu handles only same-origin links. External links, downloads, target="_blank", modifier clicks and same-page #hash links behave as usual.
  • Full-page fallback: If a client-side transition can't complete, for example because the response is an API result or the app was redeployed, Goribu loads the page normally instead.
  • Overlapping navigations: The latest navigation wins. A slower earlier request never replaces it.