

Dual-Repo Deployment Guide (GitHub Pages + Vercel)
A GitHub Pages + Vercel dual-repo publishing tutorial: private source, public site, Actions auto-push.
Preface#
If deploying a personal blog with Vercel is a kind of “instant happiness,” then deploying with GitHub Pages feels more like an exercise that’s “closer to the underlying logic.” There isn’t as much platform magic making assumptions for you, and precisely because of that, you’ll understand more clearly: what the build output is, what the publishing repository is, why we separate source code from the site, and why some things should be committed while others should not.
This article records the whole process of turning my blog into “a private source repository + a public Pages publishing repository.” Like the previous post, this is both a note to myself and a step-by-step tutorial for everyone. But there are some prerequisites:
- You already have a blog project that can run locally;
- You want the source repository to stay private, but the site must be publicly accessible;
- You want to truly connect GitHub Pages and GitHub Actions end-to-end;
- You don’t want to manually copy
dist/every time—you want it to publish automatically after pushing code.
Part 1: First, understand what this solution is actually doing#
If this is your first time using GitHub Pages, it’s easy to mix up the “source repository” and the “publishing repository.” In a dual-repo deployment setup, they play completely different roles.
Why split into two repositories?#
Simply put, we want to satisfy two goals at the same time:
- Keep the source as private as possible: because blog source code may include theme configs, workflows, unpublished content, and sometimes experimental features.
- Keep the site public: because GitHub Pages is meant to be accessed by others.
Of course you can put everything into one repository, but you’ll keep struggling with questions like “should the source be public,” “should the build output be committed,” and “which folder does Pages read from.” The dual-repo approach gives clear responsibilities:
[username]/[repo1]: private source repository[username]/[username].github.io: public publishing repository
The former is for writing code, writing posts, and running the build; the latter is only for hosting the already-built static site.
What is the essence of this workflow?#
At its core, we only do three things:
- Run a static build in the private source repository and produce
dist/ - Use GitHub Actions to push
dist/to the root of the public repository - Let GitHub Pages publish from the public repository’s
main / (root)
Part 2: Before we start, what should we already have?#
Before continuing, it’s best to confirm the following prerequisites are met.
Repositories are ready#
- Private source repository:
[username]/[repo1] - Public publishing repository:
[username]/[username].github.io
The second repository must be public. This is important because a user-site GitHub Pages needs to be reliably publicly accessible, and using a public repository is the most hassle-free option.
The local project is ready#
At minimum, your blog project should satisfy:
pnpm buildsucceeds locally;- the project already supports building for GitHub Pages mode;
- build output goes to
dist/; - you know which repository is the source repo and which is the publishing repo.
If you haven’t done these yet, I recommend first following my blog getting started guide ↗ to get local builds working, and then continue with the automation below. Because Actions is essentially just running the same local workflow once again in the cloud.
Part 3: Generate a Deploy Key#
This step is critical—and it’s also the part where people most often get stuck in dual-repo publishing. But the principle is not complicated.
We need a “key” that allows the CI in the private source repository to legally push content to the public publishing repository. That key is an SSH Deploy Key. Sounds complex, but don’t worry—I’ll guide you step by step.
Generate a key pair locally#
Run this in any directory on your machine:
ssh-keygen -t ed25519 -C "pages-deploy" -f pages_deploy_key -N ""bashAfter it finishes, it will generate two files:
pages_deploy_key: private key, must never be leakedpages_deploy_key.pub: public key, can be added to GitHub repository settings
Put the public key and private key in the correct places#
The easiest part to mess up is not the operation itself, but “which key goes into which repository.”
- Put the public key into the public publishing repository (to allow write)
Go to the public repository: [username]/[username].github.io
Open: Settings to Deploy keys to Add deploy key
Then fill in:
Title: anything, e.g.deploy-from-OthersKey: paste the full content ofpages_deploy_key.pub- check
Allow write access
The purpose is clear: allow CI to use this key to push build output to the public repository.
- Put the private key into the private source repository’s Actions Secret
Go to the private repository: [username]/[repo1]
Open: Settings to Secrets and variables to Actions to New repository secret
Fill in:
Name:PAGES_DEPLOY_KEY(the name must be exactly this)Value: paste the full content of thepages_deploy_keyprivate key (including BEGIN / END)
Part 4: Enable GitHub Pages#
At this point, the publishing repository already has the ability to “receive pushed content.” Next, we need to tell GitHub Pages where to read these static files from.
Go to repository: [username]/[username].github.io
Open: Settings to Pages
Then set:
Source:Deploy from a branchBranch:mainFolder:/ (root)
After saving, the Pages URL will normally be:
https://[username].github.io/textPart 5: Automatic publishing from the source repository#
In your current source repository, this automated publishing workflow is actually already in place. In other words, we didn’t design the workflow from scratch; it has already been adapted into the following chain.
The auto-publish chain#
- You commit and push in the private source repository
- GitHub Actions is triggered
- Actions runs a static build with
DEPLOYMENT_PLATFORM=github - Build output is produced into
dist/ - The workflow uses the Deploy Key to push
dist/to the root of[username]/[username].github.ioon themainbranch - GitHub Pages reads static files from the root and refreshes the site
At this point, you’ve achieved GitHub Pages + Vercel dual-platform deployment for your personal blog. You can try opening the site via each platform’s URL. Enjoy your results—I believe it will feel fun and rewarding! Of course, from here on, building your blog’s content and polishing the UI will depend on your own hard work. Keep going and shape it into a space with your own style!
Summary#
Once you really walk through this entire workflow, you’ll realize GitHub Pages isn’t as complicated as it seems. What’s truly tricky is not a single command, but building the complete mental model the first time: who builds, who publishes, who provides public access, and who holds write permissions.
Once that model is in place, everything becomes smooth. You just write posts, tweak configs, and commit code as usual—the build and publishing are automatically handled by Actions.
More importantly, this dual-repo approach isn’t only for blogs. Any static-build website—product landing pages, portfolios, documentation sites, personal resumes—can reuse the same idea.
Keep the source for yourself; give the results to the world. That’s what I like most about this publishing method.