I made Astro build my .htaccess
file for server-side redirects with Apache.
With over a decade as Random Geekery, my site —
Look I’m gonna go straight to the code and do the explanation after. If you need this information, you need it now.
Put your generating logic in a GET
function exported from src/pages/ENDPOINT.js
.
import { getCollection } from "astro:content"
export async function GET(context) { const posts = await getCollection("posts")
const directiveText = posts.map((post) => post.data.aliases.map((alias) => `Redirect 301 ${alias} /post/${post.id}/`) ).flat().join("\n")
return new Response(directiveText)}
Astro didn’t seem src/pages/.htaccess.js
, possibly because it assumes hidden files should be ignored.
Instead I build an /htaccess
endpoint, and ensured my task runner renames the htaccess
file afterwards.
build: npm run build mv dist/htaccess dist/.htaccess
That’s all.
Okay now I can get back to rambling.
Why did I do this
With over a decade as Random Geekery, my site has seen multiple site generators and perennial reorganization ideas. I have a thousand redirects — 1,076 to be precise. Other people linked to my site at some point, and I changed the URL since. I still want them to get the post they were linking to!
Client-side redirects in Astro
You can configure redirects in Astro.
In SSG mode, Astro redirects are implemented as HTML files with <meta http-equiv="refresh">
.
I can specify them as a mapping of old permalinks to new permalinks in my config.
import { defineConfig } from "astro/config"
export default defineConfig({ redirects: { "/2020/04/indieweb-h-cards/": "/post/2020/04/indieweb-h-cards/", },})
Astro currently uses redirect
configuration to generate files like this on build.
<!doctype html><title>Redirecting to: https://randomgeekery.org/post/2020/04/indieweb-h-cards/</title><meta http-equiv="refresh" content="0;url=https://randomgeekery.org/post/2020/04/indieweb-h-cards"><meta name="robots" content="noindex"><link rel="canonical" href="https://randomgeekery.org/post/2020/04/indieweb-h-cards/"><body> <a href="https://randomgeekery.org/post/2020/04/indieweb-h-cards/"> Redirecting from <code>/2020/04/indieweb-h-cards/</code> to <code>https://randomgeekery.org/post/2020/04/indieweb-h-cards/</code> </a></body>
That tells the browser “immediately refresh your location to https://randomgeekery.org/post/2020/04/indieweb-h-cards/
,”
while also informing the user that they’re being redirected.
Just in case the browser isn’t cooperative.
This is what most static site generators do for redirects. It works. Sometimes it’s the only option available. Client-side redirects like this are not ideal, because they add work for the user agent — usually a desktop browser, but could be a bot or other Web-capable application.
- user agent fetches the initial URL
- Web server hands back
HTTP 200
and an HTML page containing the refresh meta - user agent parses the HTTP message and the HTML page, noting the refresh and new URL
- user agent (probably) updates its location, fetching the new URL
- for however long it takes to start reloading, you (might) see a link telling you this is a redirect
For you, the whole thing is pretty much instantaneous, assuming your system and network connection are up to par. But it’s still extra work. To make matters worse, it might even break the back button. I have wrestled with redirect accessibility before. Unsuccessfully, I might add.
Plus there’s the problem of an extra thousand lines in my config.
Each page has its permalink history in frontmatter, in the aliases
field.
That was mainly for Hugo, but I left that data in place in the Astro iteration.
I could generate my config on the fly to extract redirect metadata —
loading and parsing every page on my site before Astro loads and parses every page on my site.
Or I could add a separate preparation task which processes all my pages and creates a data file to be loaded by astro.config.mjs
—
but then I’d have to rebuild the data file routinely, just in case.
It’s easier to just let Astro help the server handle redirects.
Server-side redirects
Doing the redirect from the server speeds the exchange up considerably. Instead of returning a Web page to be parsed, processed, and possibly rendered, everything is handled at the HTTP layer.
HTTP/1.1 301 Moved PermanentlyLocation: https://randomgeekery.org/post/2020/04/indieweb-h-cards/index.html
The server responds to HTTP GET
requests for the old page with HTTP 301 to tell your user agent:
“That link is at this new location, and you can cache that, because it’ll be true forever.”
Well — as much as anything online can be “forever.”
The process now looks something like this:
- user agent fetches the initial URL
- Web server hands back the HTTP message
- user agent parses the HTTP message
- user agent (probably) updates its location, fetching the new URL
- for however long it takes to fetch, you (might) see a browser status bar update telling you this is a redirect
Honestly it has been a very long time since I saw step five for server-side redirects.
Server-side redirects with Astro SSG
So how do I set up server-side redirects? Well, you already saw the code.
Oh what the heck. We’ll go over it anyways.
Random Geekery is served by Apache on a shared host.
This Apache server processes server directives in a site-specific .htaccess
file, and enables redirect rules.
Meanwhile, Astro can build files for endpoints via a corresponding source file.
That file needs export a GET
function, reflecting to what is expected from HTTP GET
of that endpoint.
export async function GET(context) {}
As mentioned, permalink aliases are in post frontmatter. I need to access each of the posts for that. Since we are in Astro’s build process, I can get the collection that Astro already loaded.
import { getCollection } from "astro:content"
export async function GET(context) { const posts = await getCollection("posts")}
My .htaccess
file needs a redirect rule for each mapping of old URL to new URL.
1,075 variations of this, bundled into a single plain text file:
Redirect 301 /2020/04/11/indieweb-h-cards/ /post/2020/04/indieweb-h-cards
Since my schema defines the default for aliases
as a new empty array, I can be presumptuous.
Map over each post’s possibly empty list of aliases, generating lists of redirect rule strings.
Then I can flatten them into a single list and turn it into a single string.
export async function GET(context) { // ...
const directiveText = posts.map((post) => post.data.aliases.map((alias) => `Redirect 301 ${alias} /post/${post.id}/`) ).flat().join("\n")}
All Astro needs now from the GET
function is a Response
.
import { getCollection } from "astro:content"
export async function GET(context) { // ...
return new Response(directiveText)}
After npm run build
I move the generated file to its expected location.
mv dist/htaccess dist/.htaccess
A very good candidate for automation, since that’s easy to forget when doing the whole thing manually.
Wrap it up
Astro’s ability to generate arbitrary endpoints came in handy when I wanted to make more accessible redirects.
I need to build and transfer 1,075 fewer files than before adding .htaccess
to my site build process.
I had fun.
It’s all good.