← Blog

Building a virtual garage for hobby projects

1,056 words Filed in: web development, vercel, static sites, GitHub Pages

I wanted my GitHub Pages hobby projects on-domain without a complicated workflow; Vercel rewrites turned out to be the cleanest path.

I've been building hobby projects for years. Normally I'll give the better ones a write-up here and a link to GitHub Pages, but the project itself lives at khawkins98.github.io/project-name/.

I wanted to bring them on-domain without the obvious headaches: dozens of subdomains is more infrastructure than any of these deserve, subdomains still confuse people, and copying build output into the main repo defeats the point of keeping them separate.

Could I just have my site do what GitHub Pages already does: proxy /repo-name/ to whatever GitHub is already serving?

  • Before: khawkins98.github.io/PDF-A-go-go
  • After: AllAboutKen.com/PDF-A-go-go

Same project, same deploy pipeline, better home.

See the garage

Keep reading if you want to see exactly how I wired it up.

tl;dr#

  • Vercel rewrites proxy /project-name/:path* to the GitHub Pages origin — no deploy changes required
  • A naive rewrite 404s on the project root; two redirects entries (bare path and trailing slash → /project-name/index.html) fix it
  • Each project keeps its GitHub Pages URL; a hostname-check script redirects those visitors to the canonical URL
  • CORS headers on proxied paths cover shared JS assets loaded via fetch()
  • A /garage/ index page on the main site ties all the projects together

What I didn't want to do#

The obvious path is a merge: monorepo, or move each project to Vercel as a separate deployment. Both mean real work per project: consolidated CI, shared dependency trees, combined issue trackers. Hobby projects earn their independence precisely because they're free to be messy and self-contained. The right fix is a smarter routing layer in front, not a restructuring underneath.

How the proxy works#

My site was already on Vercel and each project already served from https://khawkins98.github.io/repo-name/.

Vercel rewrite rules can proxy a path prefix to an external origin. Minimum viable vercel.json:

{
  "rewrites": [
    {
      "source": "/PDF-A-go-go/:path*",
      "destination": "https://khawkins98.github.io/PDF-A-go-go/:path*"
    }
  ]
}

Requests to allaboutken.com/PDF-A-go-go/anything forward to khawkins98.github.io/PDF-A-go-go/anything. The GitHub Pages deploy keeps working at its original URL unchanged.

This works for every path except the project root.

The trailing-slash problem#

Going to allaboutken.com/PDF-A-go-go/ still gave a 404, even with the rewrite in place. The reason is Vercel's processing order: filesystem matching (including a directory-index check for build/PDF-A-go-go/index.html) runs before rewrites. That file doesn't exist in the main site's build, so Vercel 404s before the rewrite ever fires.

Two redirects entries fix it. Redirects run before filesystem matching:

{
  "redirects": [
    { "source": "/PDF-A-go-go",  "destination": "/PDF-A-go-go/index.html", "permanent": false },
    { "source": "/PDF-A-go-go/", "destination": "/PDF-A-go-go/index.html", "permanent": false }
  ],
  "rewrites": [
    {
        "source": "/PDF-A-go-go/:path*",
        "destination": "https://khawkins98.github.io/PDF-A-go-go/:path*"
    }
  ]
}

Visiting /PDF-A-go-go/ now redirects (302) to /PDF-A-go-go/index.html. No directory-index ambiguity; the rewrite fires and proxies to GitHub Pages. The browser ends up at /PDF-A-go-go/index.html, where relative asset paths like ./app.js resolve correctly to /PDF-A-go-go/app.js.

Not burning the old address#

GitHub Pages keeps serving at khawkins98.github.io/project-name/. The Vercel setup is additive: the personal domain gains a route, the original URL stays live. Rolling back is two deleted lines in vercel.json.

For now I've only moved two projects across — PDF-A-go-go and pinment — while I confirm that existing embed links aren't affected. The redirect from khawkins98.github.io to the proxied URL could in theory break things for anyone who has embedded the old URL directly. Once it's clear nothing is breaking, adding the rest is a few lines in vercel.json.

But visitors arriving at the old URL should reach the canonical one. A small script at the top of each project's <head>:

<script>if(window.location.hostname==='khawkins98.github.io'){window.location.replace('https://www.allaboutken.com/PDF-A-go-go/')}</script>
<link rel="canonical" href="https://www.allaboutken.com/PDF-A-go-go/">

The hostname check is essential. Without it, the redirect fires on the proxied page too: allaboutken.com proxies to github.io, github.io redirects back to allaboutken.com, repeat. The check breaks the loop.

Don't forget: CORS for shared JS assets#

Pinment is a bookmarklet tool. Its install snippet builds a script URL from window.location.origin at runtime, so it always points to whichever domain the user is visiting from. Through the Vercel proxy, that becomes allaboutken.com/pinment/pinment-bookmarklet.js, which the rewrite proxies correctly to GitHub Pages.

<script src> tags don't trigger CORS, but fetch() does. A headers block in vercel.json covers it:

{
  "headers": [
    { "source": "/pinment/:path*", "headers": [{ "key": "Access-Control-Allow-Origin", "value": "*" }] }
  ]
}

Worth adding for any project that exposes public JS assets loaded programmatically from other pages.

The full configuration#

{
  "buildCommand": "yarn build",
  "outputDirectory": "build",
  "headers": [
    { "source": "/PDF-A-go-go/:path*", "headers": [{ "key": "Access-Control-Allow-Origin", "value": "*" }] },
    { "source": "/pinment/:path*", "headers": [{ "key": "Access-Control-Allow-Origin", "value": "*" }] }
  ],
  "redirects": [
    { "source": "/PDF-A-go-go",  "destination": "/PDF-A-go-go/index.html", "permanent": false },
    { "source": "/PDF-A-go-go/", "destination": "/PDF-A-go-go/index.html", "permanent": false },
    { "source": "/pinment",  "destination": "/pinment/index.html", "permanent": false },
    { "source": "/pinment/", "destination": "/pinment/index.html", "permanent": false }
  ],
  "rewrites": [
    { "source": "/PDF-A-go-go/:path*", "destination": "https://khawkins98.github.io/PDF-A-go-go/:path*" },
    { "source": "/pinment/:path*", "destination": "https://khawkins98.github.io/pinment/:path*" }
  ]
}

Adding another project#

  1. Confirm GitHub Pages is enabled and serving at USERNAME.github.io/repo-name/.
  2. Check asset paths: ./-relative or prefixed with the repo name. Bare /styles.css will break.
  3. Add two redirects entries and one rewrite to vercel.json.
  4. Add the hostname-check script and <link rel="canonical"> to the project's index.html.
  5. Add the project to the garage page.

For SPAs with client-side routing: the project needs a 404.html on GitHub Pages that loads index.html. The proxy can't route to paths the GH Pages deploy doesn't know about.

The full configuration and checklist live in vercel.json and docs/DEPLOYMENT.md. Nothing about the project deploys has to change: GitHub Pages stays the origin, Vercel becomes the routing layer. See what it looks like in practice at the garage. If you've proxied projects this way — or found a cleaner approach — I'd be glad to hear about it. The projects just get to live at home.

I think this is a clean way to bring web projects back home without adding server complexity.

Was this helpful? Tap to tell me if so.