Syncing a GitHub Pages Site with a Self-Hosted Astro Blog
I maintain two separate online presences: a GitHub Pages site (mwhidayat.github.io) and a self-hosted blog on a VPS. The VPS primarily serves the MVP of a SaaS product I’m working on, but since it still has room, I migrated my WordPress blog, why.web.id, there too.
At the same time, I wanted the blog content to also appear on my GitHub Pages site without extra effort.
The Architecture
The setup has three components:
- Astro Modular blog on a VPS (
why.web.id) — the primary writing platform - GitHub Pages site (
mwhidayat.github.io) — the professional landing page - blog.json — a lightweight JSON feed that bridges the two
Step 1: Generate a JSON Feed
During each Astro build, I generate a blog.json file containing the 10 most recent posts:
// generate-blog-json.mjs — runs after pnpm build
const posts = files.map(f => {
const content = fs.readFileSync(path.join(postsDir, f), "utf-8");
const title = content.match(/title:\s*["']([^"']+)/)?.[1] || "";
const date = content.match(/date:\s*(\S+)/)?.[1] || "";
const slug = f.replace(/^\d{4}-\d{2}-\d{2}-/, "").replace(".md", "");
return { title, date, url: "/" + slug + "/", tags };
});
This produces a clean JSON file hosted at https://why.web.id/blog.json:
[
{
"title": "Building a Machine Learning Model to Assess German Writing Proficiency",
"date": "2025-11-14T02:49:36Z",
"url": "/building-a-machine-learning-model-to-assess-german-writing-proficiency/",
"tags": ["Coding", "Languages"]
}
]
Step 2: Add CORS Headers
Since the GitHub Pages site fetches from a different domain, I added a CORS header in Nginx:
location /blog.json {
add_header Access-Control-Allow-Origin "*";
}
Step 3: Fetch and Render on GitHub Pages
On the GitHub Pages side, a simple blog.html page fetches the JSON feed and renders it:
<script>
fetch("https://why.web.id/blog.json")
.then(r => r.json())
.then(posts => {
let html = "";
posts.forEach(p => {
html += `<div class="post">
<h2><a href="https://why.web.id${p.url}">${p.title}</a></h2>
<div>${p.tags.map(t => `<span class="tag">${t}</span>`).join("")}</div>
</div>`;
});
document.getElementById("posts").innerHTML = html;
});
</script>
When a user visits mwhidayat.github.io/blog.html, the page fetches the latest posts from the VPS in real-time. No build step, no sync script, no duplicate content.
Deploy Pipeline
On every blog update, I run a single deploy script that builds the site, generates the JSON feed, and uploads everything to the VPS. The GitHub Pages site picks up the new posts automatically — no additional steps needed.
Results
Both sites now stay in sync effortlessly:
| Site | URL | Updates |
|---|---|---|
| Main blog | why.web.id | Full Astro Modular theme |
| Professional site | mwhidayat.github.io/blog.html | Latest 10 posts auto-fetched |