Syncing a GitHub Pages Site with a Self-Hosted Astro Blog

· [object Object] · [object Object]

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:

&#x3C;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}&#x3C;/a>&#x3C;/h2> <div>${p.tags.map(t => &#x3C;span class="tag">${t}&#x3C;/span>).join("")}</div> </div>; }); document.getElementById("posts").innerHTML = html; }); &#x3C;/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