Skip to main content

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

2 min 290 words

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:

  1. Astro Modular blog on a VPS (why.web.id) — the primary writing platform
  2. GitHub Pages site (mwhidayat.github.io) — the professional landing page
  3. 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:

SiteURLUpdates
Main blogwhy.web.idFull Astro Modular theme
Professional sitemwhidayat.github.io/blog.htmlLatest 10 posts auto-fetched