
How to set up redirects in Astro?
To set up redirects in Astro, add a redirects object to astro.config.mjs that maps old paths to new ones, such as "/old-about": "/about". That covers permanent moves, dynamic patterns like "/blog/[...slug]": "/articles/[...slug]" and external URLs. For redirects that depend on the request, such as sending logged-out visitors to a login page, return Astro.redirect("/login") from an on-demand page, or call redirect() from an endpoint or middleware. How a configured redirect is delivered depends on your output: a static site without an adapter gets small HTML pages with a meta refresh, while a server or supported adapter sends real HTTP redirects with status codes.
Redirects matter whenever URLs change: a site migration, a renamed section, a short link. Get them wrong and you lose search rankings and frustrate visitors with 404s. In this article you'll learn how to configure static, dynamic and external redirects, how to choose status codes, what Astro actually outputs in static and server builds, how to redirect dynamically from pages, endpoints and middleware, and a priority behaviour in Astro 7.4.2 that differs from what the docs describe.
Configured Redirects
The redirects option is a map from the old path to the new destination:
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
redirects: {
"/old-about": "/about",
"/blog/[...slug]": "/articles/[...slug]",
"/twitter": "https://example.com/@me",
"/sale": { status: 302, destination: "/offers" },
},
});
Each entry is one of three kinds:
- Internal:
"/old-about": "/about"points one path at another page on your site. - Dynamic:
"/blog/[...slug]": "/articles/[...slug]"uses route parameters, so every URL under/blog/maps to the matching URL under/articles/. - External: any destination starting with
httporhttpsthat can be parsed as a URL sends visitors to another site.
The value can be a string, which uses the default status, or an object with status and destination when you want a specific code.
Rules for Dynamic Redirects
Dynamic redirects follow the same bracket syntax as file-based routing. Both sides must use the same parameters:
// allowed: same parameter on both sides
"/blog/[...slug]": "/articles/[...slug]",
"/docs/[page]": "/guides/[page]",
// not allowed: static source, dynamic destination
// "/article": "/blog/[...slug]",
The config reference also notes that trailing slash variants such as mapping /product1/ to /product1 aren't supported. Trailing slashes are handled by the trailingSlash option instead.
In static mode, a dynamic redirect only produces files for paths the destination route actually builds. My /articles/[...slug].astro page returned two paths from getStaticPaths(), hello and 2026/news, and the build created matching redirect pages at /blog/hello/ and /blog/2026/news/. If you're new to that pattern, see rest parameters for catch-all routes.
What Astro Outputs in a Static Build
Without an adapter, there's no server to send a redirect status, so Astro writes an HTML file for each redirect. I built the config above and the redirect for /old-about became dist/old-about/index.html:
<!doctype html><title>Redirecting to: /about</title><meta http-equiv="refresh" content="0;url=/about"><meta name="robots" content="noindex"><link rel="canonical" href="/about"><body><a href="/about">Redirecting from <code>/old-about/</code> to <code>/about</code></a></body>
The page does four things:
- Meta refresh:
content="0;url=/about"sends the browser on immediately. - noindex: asks search engines not to index the redirect page itself.
- Canonical link: points search engines at the destination.
- A fallback link: for the rare client that ignores meta refresh.
The browser receives a 200 response followed by a client-side redirect, not a 301. The status code you set in the config isn't used. Search engines generally follow meta refresh redirects, but a real HTTP redirect is the stronger signal, which is why server-side redirects are worth it for important moves.
Real Redirects on a Static Site
You have two ways to get proper status codes while keeping a static site:
- Use an adapter: the docs note that supported adapters write the host's own redirect configuration instead of HTML pages. The configured
statusis then respected. - Use your host's redirect file: hosts such as Netlify and Cloudflare Pages read a
_redirectsfile from the output folder. Put it inpublic/and Astro copies it intodist/unchanged.
# public/_redirects
/old-about /about 301
/sale /offers 302
Check your host's documentation for the exact syntax it supports.
Redirects with an Adapter
With an adapter, the server handles configured redirects at request time. I added the Node adapter to the same config:
// astro.config.mjs
import { defineConfig } from "astro/config";
import node from "@astrojs/node";
export default defineConfig({
adapter: node({ mode: "standalone" }),
redirects: {
"/old-about": "/about",
"/blog/[...slug]": "/articles/[...slug]",
"/twitter": "https://example.com/@me",
"/sale": { status: 302, destination: "/offers" },
},
});
The build no longer wrote redirect HTML files. Testing the running server with curl -s -o /dev/null -w '%{http_code} %{redirect_url}':
/old-about 301 http://localhost:4331/about
/sale 302 http://localhost:4331/offers
/blog/2026/news 301 http://localhost:4331/articles/2026/news
/twitter 301 https://example.com/@me
And for a POST request to the same redirect:
curl -s -X POST -o /dev/null -w '%{http_code}\n' http://localhost:4331/old-about
308
That's the documented behaviour: GET requests get 301 by default, and other methods get 308, which tells the client to repeat the request with the same method and body.
Choosing a Status Code
| Status | Meaning | Use it for |
|---|---|---|
301 | Moved permanently | Renamed pages, site migrations (default for GET) |
308 | Permanent, keep the method | Permanent moves of form or API URLs (default for other methods) |
302 | Found (temporary) | Short-term campaigns, A/B tests |
307 | Temporary, keep the method | Temporary moves where the method matters |
Use permanent codes when the old URL is gone for good, so search engines move their ranking signals to the new URL. Use temporary codes when you plan to switch back.
Dynamic Redirects with Astro.redirect()
Configured redirects are fixed. When the decision depends on the request, such as a cookie, a header or a database lookup, redirect from the page itself. This needs on-demand rendering:
---
// src/pages/account.astro
export const prerender = false;
const session = Astro.cookies.get("session")?.value;
if (!session) {
return Astro.redirect("/login");
}
---
<h1>Your account</h1>
Against the built server:
/account (no cookie) 302 http://localhost:4331/login
/account (session=abc) 200 <h1>Your account</h1>
Astro.redirect() defaults to 302 for on-demand routes, and you can pass a second argument to change it, for example Astro.redirect("/new", 301). Two rules apply:
- Return it: you must
returnthe result. Calling it without returning does nothing. - Pages only, not components: because Astro streams HTML on demand, the redirect must happen in the page's frontmatter, not inside a child component, where the response may already have started.
In a prerendered page, Astro.redirect() produces the same meta refresh HTML as a configured redirect and ignores the status code. For real login flows, see how to protect routes in Astro.
Redirecting from an Endpoint
Endpoints receive a redirect() helper on their context. That makes them a neat way to build short links:
// src/pages/go/[id].ts
import type { APIRoute } from "astro";
export const prerender = false;
const links: Record<string, string> = {
docs: "https://docs.astro.build/",
repo: "https://github.com/withastro/astro",
};
export const GET: APIRoute = ({ params, redirect }) => {
const target = links[params.id ?? ""];
if (!target) {
return new Response("Unknown link", { status: 404 });
}
return redirect(target, 307);
};
/go/docs 307 https://docs.astro.build/
/go/zzz 404
Endpoints are covered in full in how to build API endpoints in Astro.
Redirecting from Middleware
To redirect a whole group of URLs with your own logic, use middleware. The context object has the same redirect() helper:
// src/middleware.ts
import { defineMiddleware } from "astro:middleware";
export const onRequest = defineMiddleware((context, next) => {
if (context.url.pathname.startsWith("/legacy-shop")) {
return context.redirect("/offers", 301);
}
return next();
});
/legacy-shop/item 301 http://localhost:4331/offers
There's no legacy-shop page at all; the middleware answers before routing reaches a page. Middleware runs for on-demand requests in production, and for prerendered pages only at build time, so use it for redirects on server-rendered sites. Astro middleware explains how it fits into the request pipeline.
In Astro 7, redirects are also a stage in the default request pipeline, which you can reorder with a custom src/fetch.ts file using the redirects() handler from astro/fetch. Most sites don't need that; the config and middleware cover nearly every case.
Redirects and Existing Pages
The Astro routing docs state that configured redirects follow the same priority rules as file-based routes and that file-based routes take precedence over redirects, so /old-page shouldn't redirect if src/pages/old-page.astro exists.
My test on Astro 7.4.2 didn't match that. I added "/clash": "/about" alongside a real src/pages/clash.astro page:
- Static build:
dist/clash/index.htmlcontained the redirect HTML, not the page. - Dev server:
/clashreturned301to/about. - Node adapter:
/clashreturned301to/about.
In every case, the redirect won. Whichever behaviour is intended, the safe approach is simple: don't define a redirect for a path that still has a page. Delete or rename the old page when you add its redirect. The general priority rules are covered in route priority in Astro.
Migration Checklist
When you move content, work through these steps:
- List every old URL: export them from your analytics, sitemap or old CMS.
- Map patterns, not just pages: one dynamic rule like
/blog/[...slug]can replace hundreds of entries. - Pick the right delivery: use an adapter or your host's redirect file if you want real
301responses. - Remove the old pages: avoid a page and a redirect on the same path.
- Update internal links: link straight to the new URLs instead of relying on redirects.
- Test the production build: check status codes with
curl -Iagainstastro previewor your deployed site.
FAQ: Redirects in Astro
In the redirects option of astro.config.mjs, as a map from the old path to the new path or URL. For request-dependent redirects, use Astro.redirect() in an on-demand page, or redirect() in an endpoint or middleware.
With an adapter, yes: GET requests get 301 by default and other methods get 308. A static build without an adapter writes HTML pages with a meta refresh instead, so no status code is sent.
Yes. Any destination that starts with http or https and is a valid URL works, for example "/twitter": "https://example.com/@me".
Use a dynamic redirect with a rest parameter on both sides, such as "/blog/[...slug]": "/articles/[...slug]". The parameter names must match on both sides.
It uses 302 by default for on-demand routes. Pass a second argument, such as Astro.redirect("/new", 301), to change it. Remember to return the result from the page frontmatter.
Avoid it. The docs say the page should win, but in my Astro 7.4.2 tests the redirect took over in the static build, the dev server and the Node adapter. Remove the old page when you add the redirect.
Conclusion
Astro gives you redirects at three levels. The redirects config handles permanent moves, folder-wide patterns and external links in one place. Astro.redirect() and the redirect() helper handle decisions that depend on the request, in pages, endpoints and middleware. Together they cover everything from a renamed page to a full site migration.
The detail that matters most is delivery. A static build without an adapter writes meta refresh pages and ignores status codes, while an adapter or your host's redirect file sends real 301 and 302 responses. Choose the delivery that fits your host, keep pages and redirects from overlapping, and always test the production build with curl before you ship.


