Skip to content

feat: add redirect() helper for loaders - #43

Merged
thinkter merged 1 commit into
mainfrom
fix/issue-15-loader-redirect
Jul 6, 2026
Merged

feat: add redirect() helper for loaders#43
thinkter merged 1 commit into
mainfrom
fix/issue-15-loader-redirect

Conversation

@thinkter

@thinkter thinkter commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a redirect(url, status?) helper that can be called from any load() function (layout or page) to redirect the browser to another URL
  • On cold (full-page) loads the server responds with the HTTP redirect status and Location header directly; the browser follows it before any HTML is rendered
  • On client-side soft-navigation the server sends an x-brandy-redirect response header and the client runtime calls location.assign(), replacing the current page at the destination URL
  • redirect() throws a RedirectError that bypasses error and not-found boundaries — it always propagates out without invoking any error.tsx or not-found.tsx
  • Adds a RedirectRenderedRoute variant (kind: "redirect") to the RenderedRoute union type
  • Documents the canonical loader-based auth pattern in the README and explicitly notes that a middleware system was deliberately not added

What this enables

The idiomatic auth pattern for protected subtrees is a redirect in the nearest layout loader:

// app/dashboard/layout.tsx
import { redirect } from "brandy"

export async function load({ request }) {
  const session = await getSession(request)
  if (!session) redirect("/login")
  return session.user
}

This covers every route under dashboard/ without repeating the guard in each page — the same scope that middleware would cover, with no separate execution context.

Test plan

  • redirect() in a layout loader returns { kind: "redirect" } from renderFullMatch (cold load)
  • redirect() in a page loader returns { kind: "redirect" } with the correct status code
  • redirect() in a layout loader returns { kind: "redirect" } from renderFragmentMatch (soft nav)
  • redirect() bypasses renderError error boundaries
  • Cold load through the full HTTP server returns 302 + Location header
  • Fragment navigation through the full HTTP server returns 302 + x-brandy-redirect header
  • All 83 pre-existing tests continue to pass (89 total with 6 new)
  • TypeScript typechecks cleanly

Fixes #15

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
brandy Ready Ready Preview, Comment Jul 6, 2026 5:30pm

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@thinkter, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b9f53bd-5301-44d0-aa75-3044aaa18534

📥 Commits

Reviewing files that changed from the base of the PR and between 8f6bdf1 and 4247138.

📒 Files selected for processing (8)
  • README.md
  • src/client/runtime.ts
  • src/core/control.ts
  • src/core/render.ts
  • src/core/types.ts
  • src/server.ts
  • tests/integration.test.ts
  • tests/loaders.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-15-loader-redirect

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Loaders can now call redirect(url, status?) to send the browser to a
different URL. On cold loads the server responds with the redirect
status + Location header directly. On client-side fragment navigation
the runtime receives an x-brandy-redirect response header and calls
location.assign() so the full page is replaced at the destination URL.

RedirectError bypasses error and not-found boundaries — it always
propagates out of loaders without invoking error.tsx or not-found.tsx.

The README documents the canonical loader-based auth pattern (redirect
in a layout loader) and explicitly notes that a middleware system was
deliberately not added.

Fixes #15

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@thinkter
thinkter force-pushed the fix/issue-15-loader-redirect branch from cbe1ebc to 4247138 Compare July 6, 2026 17:30
@thinkter
thinkter merged commit 2c2baab into main Jul 6, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No redirect() for loaders and no middleware concept; auth story undefined for dashboards/CRUD

1 participant