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:
SiteURLUpdatesMain blogwhy.web.idFull Astro Modular themeProfessional sitemwhidayat.github.io/blog.htmlLatest 10 posts auto-fetched