Astro supports redirects in three places. The redirects option in astro.config.mjs is the portable choice. On a static site with no adapter, it produces HTML pages with a meta refresh rather than true HTTP redirects. For real 301 responses on a static site, add a _redirects file to public/ on hosts that support it (Netlify and Cloudflare Pages). Inside on-demand pages, return Astro.redirect('/new/', 301).
Key Takeaways
redirectsin config works everywhere, but on static builds it’s a meta refresh with no status code.- With an adapter, Astro can pass config redirects to the host as real HTTP redirects.
public/_redirectsgives true 301s on Netlify and Cloudflare Pages without an adapter.- Use
Astro.redirect()for runtime decisions, such as auth checks.
1. The redirects config option
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
redirects: {
'/old-about/': '/about/',
'/blog/[slug]/': '/articles/[slug]/',
'/promo/': { status: 302, destination: '/pricing/' },
},
});
Dynamic segments work when the pattern matches on both sides. /blog/[slug]/ redirects every post that exists as an /articles/[slug]/ page.
What gets built depends on your setup:
- Static, no adapter: Astro writes an HTML file at the old path with
<meta http-equiv="refresh">and a canonical link to the destination. Browsers follow it; search engines generally treat it as a redirect, but there’s no301status. - With an adapter: adapters for Netlify, Vercel and Cloudflare can turn config redirects into the host’s native redirect rules, so they return real status codes.
2. Host redirect files
For a static site that needs real 301s, use the host’s own format.
Netlify and Cloudflare Pages read _redirects from the output root. Put it in public/ and Astro copies it to dist/:
# public/_redirects
/old-about/ /about/ 301
/blog/old-post/ /blog/new-post/ 301
/docs/* /guides/:splat 301
Cloudflare Pages also reads _headers the same way. Rules are matched top to bottom, so put specific rules before wildcards.
Vercel uses vercel.json:
{
"redirects": [
{ "source": "/old-about/", "destination": "/about/", "permanent": true }
]
}
GitHub Pages has no server-side redirects. Use the config option and accept meta refreshes.
3. Runtime redirects
On a page rendered on demand, return Astro.redirect() from the frontmatter:
---
export const prerender = false;
const user = Astro.locals.user;
if (!user) return Astro.redirect('/login/', 302);
---
In endpoints and middleware, use context.redirect() or return a Response with a Location header. The default status is 302. Use 301 or 308 only for moves that are truly permanent, since browsers cache them hard.
Which to choose
| Situation | Use |
|---|---|
| Renamed a few pages, any host | redirects in config |
| Static site on Netlify or Cloudflare, SEO matters | public/_redirects |
| Site uses an adapter | redirects in config (adapter emits host rules) |
| Redirect depends on cookies or user | Astro.redirect() |
| Hundreds of legacy URLs | Host redirect file, generated by a script |
Avoid redirect chains
Each hop costs a round trip, and crawlers may stop following after a few. When a page moves twice, update the first rule to point at the final destination. Also keep the trailing slash consistent on both sides so a redirect doesn’t land on a URL that redirects again; the trailing slash tip explains how trailingSlash affects this.
The Astro routing guide covers how redirects fit with route priority, and the Cloudflare deploy docs show where _redirects lives in a theme. The full option reference is in the Astro routing docs.
FAQ
Does the Astro redirects option return a 301?
Only with an adapter that supports it. On a static build without an adapter, it generates a meta refresh page, which has no HTTP status code.
Where do I put a _redirects file in Astro?
In public/. Astro copies it to the root of dist/, where Netlify and Cloudflare Pages read it.