Running an astro blog through org mode
I love Doom Emacs. By now, you should know that. Also quite fond of sharing my passion and findings with the world, hence blogging. When I started this project back in 2024, I used Doom Emacs to write articles and would publish them on the web with GoHugo. That was a lovely work-flow, since with a key-chord I could not only transform my .org file into markdown, but also sync it to the VPS, where it would be picked up by Hugo through a cron job.
There was, however, one thing that bugged me: I don’t understand Go and Go templates. This limited me in customizing the appearance of my site. Don’t get me wrong, the Hugo Themes are georgeous, but they felt too cookie cutter to me. The desire to stand out ate away at me.
A little hiatus
My profession is web development, and around last year I went all in on PayloadCMS. For the unaware, Payload is the content management system for NextJS. And since everything looked like a nail, the decision to rewrite the blog in PayloadCMS and just use the admin interface for content felt close. Even went so far to start a YouTube channel to document the creation of this new site.
However, after (checks notes) 17 episodes my motivation fades. Creating videos, producing them, uploading them and maintaining all this content was too much. After all, I’m a developer and therefore lazy af. Or put in more formal way: “The friction of producing the content was too high”.
I fell off.
The kindling of a new flame
But the spark never went away. I still want to share my passion with the world, just with less “friction”.
Reminiscing about the good ol times, when publishing content could be as easy as pressing some buttons, a new spark arose.
There must be some solution to my “cannot customize the template” situation.
With AI being so good nowadays, I asked the almighty what path forward I should choose. After all, I could dip my toes into Go…
But nay. There was an even better way to leverage all my web development skills and still keep using my established workflow with Doom Emacs, org-mode and ox-hugo.
Enter Astro.build
Astro wasn’t completely new to me. Here and there I heard tales of the blazingly fast performance of astro sites - with great developer experience, too. The question was just if there was a org-mode exporter for Astro.
But the AI schooled me and informed me that I could just keep using ox-hugo for the content management and use Astro as the visual representation on the web.
Vince-McMahon.jpg
Hol up. You’re telling me that I can do that? Yes. Yes you can.
It’s what you are looking at right now: a site build with Astro, TypeScript, React, TailwindCSS - in short tech I’m familiar with. But the article was still written in an org-mode file.
Let’s dive deeper into how it works.
Scaffolding Astro
The creation of the project was straightforward.
# Create the project (select "Empty" template)
pnpm create astro@latest my-blog
# Add integrations
cd opensource-odyssey-blog
pnpm astro add react tailwind
pnpm add -D @tailwindcss/typography
I initialized the Astro project and added the React/Tailwind integrations.
Using ox-hugo to export markdown to Astro
The key realization was that at the core, ox-hugo just transforms .org files into .md files and places them in some specified content/ directory. Fortunately, Astro also expects markdown files to live in such a directory. Granted, there is a little gotcha, in that Hugo expects the content/ directory to be at the root of the project, whereas Astro has it nested in src/content/, but by setting the HUGO_BASE_DIR to ./src/ that can be mitigated.
This is how the header of my .org file now looks.
#+TITLE: Opensource Odyssey Blog
#+AUTHOR: Alex
#+HUGO_BASE_DIR: ./src/
#+HUGO_FRONT_MATTER_FORMAT: yaml
#+STARTUP: indent
With this configuration it is as easy as pressing SPC m e H A and the entire .org file with the headlines as sub-trees are converted to .md and placed in the directory.
The rest is just Astro stuff.
Creating a src/pages/posts/[...slug].astro file that will render the articles
import { getCollection, render } from "astro:content"
import Layout from "@/layouts/Layout.astro"
export const prerender = true
export async function getStaticPaths() {
const posts = await getCollection("posts")
return posts.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}))
}
const { entry } = Astro.props
const { Content: PostContent } = await render(entry)
Adding more frontmatter to each headline
:PROPERTIES:
:EXPORT_FILE_NAME: running-astro-through-org-mode
:EXPORT_DATE: 2026-02-08
:EXPORT_DESCRIPTION: I love Doom Emacs, but I don't know Go. Soooo, I'm turning away from GoHugo and towards Astro.
:END:
And creating a src/content.config.ts to validate the frontmatter with zod.
import { defineCollection, z } from "astro:content"
const Post = defineCollection({
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
cover: z
.object({
image: z.string(),
alt: z.string().default("Post cover image"), // Default alt text if missing
})
.optional(),
}),
})
export const collections = { Post }
There is a little more to it, of course, but these are the essential parts. I am sure you can figure them out by yourself.
That’s it. It was that easy!
Summary about this journey
So I’m back - and in style! At the time of writing I sank about 5 hours into the entire process, from setting up the project to migrating my old articles to publishing everything on the web.
Finally I have full control over the look and feel of the site. There are more features that I’m envisioning, but these will come with time.
Astro is really, really amazing. You should check it out - maybe it fits a need that you have.
I’m very glad to have picked up another tool and not done everything in NextJS/PayloadCMS. This broadens my horizon, sharpens my skills and makes me a more versed developer.