Redirect System
Introduction
ReeWeb's redirect system maps old or external URLs to new destinations. Redirects are declared in config/redirects.ts and emitted by the SSG script to two outputs:
dist/_redirects- a Cloudflare-compatible redirect rules file (with and without trailing slash variants)- HTML stubs - per-redirect
dist/{from}/index.htmlpages with meta-refresh tags (fallback for non-Cloudflare hosts and local preview)
The system runs in two phases inside the SSG script, catching configuration errors early and ensuring no collisions with generated content.
Declaring Redirects
Edit config/redirects.ts:
export const redirects: Redirect[] = [
{ from: "/resume", to: "/files/resume-2026-v3.pdf" },
{ from: "/old-page", to: "/new-page", status: 301 },
{ from: "/talk", to: "https://youtube.com/watch?v=...", status: 302 },
];
Each redirect entry has:
| Field | Required | Type | Default | Description |
|---|---|---|---|---|
from | ✅ | string | - | Source path. Must start with /, no file extensions |
to | ✅ | string | - | Target path or URL. Internal paths are validated during static site generation |
status | ❌ | number | 301 | HTTP status: 301 (permanent) or 302 (temporary) |
Path Rules
from Rules
- Must start with
/- absolute paths only - Last segment must not contain a
.- file extensions are not allowed (use pretty URLs only) - Must be unique - duplicate
frompaths are rejected - Trailing slashes are normalized for uniqueness comparison
Valid:
/resume
/old-about-page
/blog-old/2024
Invalid (will fail validation):
old-about-page ← doesn't start with /
/resume.pdf ← contains file extension
to Rules
- Must be a non-empty string
- Can be an internal path (
/new-page,/files/resume.pdf) - Can be an external URL (
https://example.com/page) - Internal targets are validated for existence in
dist/after the SSG script completes
How It Works: Dual Output
Every redirect produces two artifacts:
1. _redirects File (Edge-Level)
The dist/_redirects file contains Cloudflare-compatible redirect rules. Each redirect produces two lines - one with and one without trailing slash - so both /resume and /resume/ redirect correctly:
/resume /files/resume-2026-v3.pdf 301
/resume/ /files/resume-2026-v3.pdf 301
/old-page /new-page 301
/old-page/ /new-page 301
On Cloudflare Pages, these rules are applied at the edge before any HTML is served - zero round-trips for redirect traffic.
2. HTML Stub (Fallback)
Each redirect also generates an HTML stub at dist/{from}/index.html with:
- A
<meta http-equiv="refresh">tag for instant redirect - A
<link rel="canonical">pointing to the target (internal targets only) - A visible link to the target for users whose browser doesn't support meta-refresh
<!DOCTYPE html>
<meta charset="utf-8" />
<title>Redirecting…</title>
<meta http-equiv="refresh" content="0;url=/files/resume-2026-v3.pdf" />
<link rel="canonical" href="/files/resume-2026-v3.pdf" />
<p>Redirecting to <a href="/files/resume-2026-v3.pdf">/files/resume-2026-v3.pdf</a>…</p>
This ensures redirects work on any static host, not just Cloudflare Pages.
Two-Phase Validation
The script validates redirects in two phases to catch errors early and prevent silent footguns.
Phase 1: Schema Validation (Before SSG)
Runs immediately, before any rendering work. Validates:
redirectsis an array- Each entry is an object with
fromandtostrings fromstarts with/and has no file extensiontois non-emptystatusis301or302(if present)- No duplicate
frompaths (trailing slashes normalized)
If any check fails, the script stops with a clear error message pointing to the specific entry.
Phase 2: Collision & Target Validation (After SSG)
Runs after all pages are rendered and static assets are copied to dist/. Checks:
- Collision with generated pages - does the
frompath match a rendered page URL? - Collision with static assets - does the
frompath match a file that was copied todist/? - Target existence - does the internal
topath resolve to an actual file indist/?
If any check fails, the script stops with a specific error message. External URL targets (https://...) are not fetched or validated during static site generation.
Redirects and Localized Routes
Redirects use literal from paths - there is no per-language fan-out. A redirect from /resume redirects /resume regardless of the active language. If you need language-specific redirects, add separate entries:
export const redirects: Redirect[] = [
{ from: "/resume", to: "/files/resume-2026-v3.pdf" },
{ from: "/en/resume", to: "/en/files/resume-2026-v3.pdf" },
];
Common Patterns
Permanent Redirect (301)
{ from: "/old-blog-post", to: "/blog/new-blog-post", status: 301 }
Default status is 301, so you can omit status for permanent redirects.
Temporary Redirect (302)
{ from: "/summer-sale", to: "/products/summer", status: 302 }
Use 302 for time-limited promotions, event links, or any redirect you plan to revert.
External Redirect
{ from: "/github", to: "https://github.com/reepolee/reeweb", status: 302 }
External targets are not validated during static site generation. Use 302 for external links so you're not permanently committing to the destination.
File Download Redirect
{ from: "/resume", to: "/files/resume-2026-v3.pdf" }
The from path has no file extension (pretty URL). The target can point to a file - the script will verify it exists.