My personal site started as hand-written HTML and CSS hosted on GitHub Pages.
That was the right decision for a profile. It became the wrong architecture for a publication.
Once I planned to publish guides, research, experiences and regularly updated articles, manually duplicating HTML pages would create inconsistent metadata, fragile navigation and unnecessary work.
I moved the site to Astro while keeping GitHub Pages as the host.
This guide explains the architecture and the exact implementation pattern.
Why Astro
Astro generates static HTML by default. That matters for a content-heavy personal site because pages are:
- fast;
- crawlable without client-side rendering;
- inexpensive to host;
- straightforward to cache; and
- portable between hosting providers.
Astro also provides content collections for structured Markdown. A schema can require every article to include a title, description, dates, category, tags and other metadata.
The architecture
Markdown articles
↓
Astro content collection
↓
Layouts + schema metadata
↓
Static HTML in /dist
↓
GitHub Actions
↓
GitHub Pages + custom domain
GitHub stores the source. GitHub Actions builds the site. GitHub Pages serves the generated files.
1. Create the Astro project
For a new project:
npm create astro@latest
For an existing repository, install Astro and initialise these core files:
npm install astro @astrojs/rss @astrojs/sitemap
A minimal package.json:
{
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview"
},
"dependencies": {
"@astrojs/rss": "^4",
"@astrojs/sitemap": "^3",
"astro": "^7"
}
}
Commit the generated lockfile. The official Astro deployment action uses it to detect the package manager.
2. Configure the custom domain
Set the canonical site URL in astro.config.mjs:
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://varunjain.info',
integrations: [sitemap()],
output: 'static',
trailingSlash: 'always'
});
Because this repository uses a custom domain, no repository-name base path is required.
Create public/CNAME:
varunjain.info
Astro copies files from public/ into the final build without processing them.
3. Define a content collection
Create src/content.config.ts:
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const foundersGuide = defineCollection({
loader: glob({
pattern: '**/*.{md,mdx}',
base: './src/content/founders-guide'
}),
schema: z.object({
title: z.string(),
description: z.string(),
publishedDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
category: z.string(),
tags: z.array(z.string()),
draft: z.boolean().default(false)
})
});
export const collections = { foundersGuide };
The schema prevents incomplete articles from silently reaching production.
4. Write articles in Markdown
Create a file such as:
src/content/founders-guide/own-your-personal-brand.md
Add frontmatter:
---
title: "How to Own Your Personal Brand"
description: "A practical system for building an independent professional asset."
publishedDate: 2026-07-23
category: "Personal Brand"
tags: ["personal brand", "owned media"]
draft: false
---
Everything below the frontmatter is regular Markdown.
5. Generate one route for every article
Create src/pages/founders-guide/[...id].astro:
---
import { getCollection, render } from 'astro:content';
import PostLayout from '../../layouts/PostLayout.astro';
export async function getStaticPaths() {
const posts = await getCollection(
'foundersGuide',
({ data }) => !data.draft
);
return posts.map((post) => ({
params: { id: post.id },
props: { post }
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
<PostLayout {...post.data} slug={post.id}>
<Content />
</PostLayout>
Astro generates a static page for every published entry during the build.
6. Add SEO once, in the layout
The layout should generate:
- title and meta description;
- canonical URL;
- Open Graph metadata;
- X card metadata;
- author information;
- publication and update dates;
BlogPostingstructured data; and- breadcrumb structured data.
Do this centrally. Do not manually copy metadata into every page template.
The article frontmatter supplies the page-specific values.
7. Generate an RSS feed
Install @astrojs/rss, then create src/pages/rss.xml.js.
The endpoint loads all non-draft articles and returns their titles, descriptions, publication dates and URLs. Readers and other systems can then follow new articles without depending on a social platform.
8. Generate a sitemap
The @astrojs/sitemap integration generates sitemap files during the production build using the site value from astro.config.mjs.
Reference it from public/robots.txt:
User-agent: *
Allow: /
Sitemap: https://varunjain.info/sitemap-index.xml
Submit the sitemap in Google Search Console and Bing Webmaster Tools after deployment.
9. Deploy with GitHub Actions
Create .github/workflows/deploy.yml:
name: Deploy Astro to GitHub Pages
on:
push:
branches: [master]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: withastro/action@v6
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v5
In the repository settings, change Pages → Build and deployment → Source to GitHub Actions.
The official Astro GitHub Pages guide recommends this deployment model.
10. Validate before merging
Run:
npm install
npm run build
The build should validate the content schema, type-check Astro components and generate every route.
Inspect the dist/ directory or run:
npm run preview
Then test:
- homepage;
- article index;
- individual articles;
- RSS feed;
- sitemap;
- canonical URLs;
- mobile layout; and
- the custom-domain CNAME.
What not to add yet
A static publication does not need a backend simply because dynamic features are possible.
Avoid adding:
- a user database;
- custom authentication;
- your own comments service;
- server-side rendering;
- an EC2 instance; or
- a complex headless CMS
until the content workflow genuinely requires them.
Comments, analytics, forms and search can initially use lightweight client-side or external services. The permanent assets remain the domain, URLs and Markdown source.
The operating workflow
My publishing workflow is now:
- Write or edit one Markdown file.
- Open a pull request.
- Validate the production build.
- Merge to
master. - Let GitHub Actions deploy.
- Submit or notify search systems.
- Distribute the article through social posts.
- Add useful reader questions back into the canonical page.
The result is not merely a prettier website. It is a publishing system that can accumulate hundreds of articles without turning maintenance into the main job.
Continue the argument
What did I miss?
Send a counterexample, question or first-hand experience. The strongest questions will be answered inside this article so the page becomes more useful over time.
Send a question