Ghost is a pleasant writing environment, but running a dynamic CMS and paying for hosting is more than every personal blog needs. The ghost-on-github-pages project takes a different route: write in a local Ghost installation, generate static files, and push those files to a public GitHub repository served by GitHub Pages.
This tutorial follows the project's stable v3.0.0 release. It covers the smallest useful path from an empty machine to a published static blog, then explains what the scripts actually do and where their boundaries are.
TL;DR
On macOS, Linux, or WSL, install Node.js LTS and wget, download the v3.0.0 release, and run:
chmod +x install.sh
./install.sh
The installer creates ~/.ghost, installs Ghost locally, starts it on port 2373, and optionally asks whether to publish immediately. After creating a post in Ghost Admin, publish changes with:
cd ~/.ghost
./deploy.sh
The result is a static site at https://USERNAME.github.io/REPOSITORY.
Prerequisites
The project's v3 requirements are deliberately small:
- macOS or Linux. Windows users can use WSL.
- Node.js LTS, documented for v18 or v20.
-
wget. - Git and a GitHub account.
- A public GitHub repository for the generated site.
You do not need Python for v3. The project replaced its Classic v2 Python-based publishing tool with gssg, a Node-based static-site generator. The exact Ghost and gssg versions are installed by the release scripts, so do not assume that a globally installed copy is equivalent to this workflow.
Check the prerequisites in a Unix-like terminal:
node --version
wget --version
git --version
Install Ghost locally
Download and extract the v3.0.0 archive, then enter the extracted directory. The release's install.sh checks for node, npm, and wget, creates ~/.ghost, installs ghost-cli@latest, and runs a local Ghost installation on port 2373.
Run it like this:
cd path/to/ghost-on-github-pages
chmod +x install.sh
./install.sh
The script may take several minutes because it downloads and configures Ghost. When it finishes, open:
http://localhost:2373/ghost
Create the administrator account, then make a small test post. The local editor is the source of truth for writing. The GitHub repository will contain the generated public output, not your local Ghost database or administrator interface.
If you want to postpone the first publish, use the documented option:
./install.sh --skip-deploy
That flag is useful when you want to finish local configuration before creating or selecting a GitHub destination.
Publish the static site
When the blog is ready, run the release's deploy script from the Ghost folder:
cd ~/.ghost
./deploy.sh
On the first run, the script asks for your GitHub username, repository name, and repository URL. It stores those settings in ~/.ghost/deploy.conf. For a project site named my-blog, the expected public URL is:
https://USERNAME.github.io/my-blog
For a user site named USERNAME.github.io, GitHub Pages uses the root URL instead.
The script first makes sure Ghost is running and sets the local site URL to http://localhost:2373. It then runs gssg, copies the project's small index.html wrapper when present, validates the generated static directory, and pushes the publish directory to the master and gh-pages branches. The repository's deployment guide documents the same lifecycle.
After the push, allow GitHub Pages time to build and serve the site. The project documentation says to wait about ten minutes, but the actual delay depends on GitHub's current Pages processing.
Verify the important failure boundary
Do not stop at a successful git push. Verify both the local generated files and the public URL:
cd ~/.ghost
./scripts/validate-static.sh static
The validator fails if generated files contain localhost:2368 or localhost:2373, or if they contain the known malformed JPEG suffixes .jpegg, .jpegpg, or .jpegjpg. The repository includes fixtures for both failure cases. Its validation test suite can be run from a checkout of the release:
./tests/run-validation-tests.sh
Then open https://USERNAME.github.io/REPOSITORY in a browser and check the home page, one post, one tag, and one image. A static site can look healthy on its home page while a post link or asset still points at localhost, so those deeper checks matter.
Why this works
The architecture is a simple publishing pipeline:
- Ghost provides the local editing and administration experience.
-
gssgturns the local Ghost site into static HTML, CSS, JavaScript, and assets. - The validator checks for two classes of broken output.
- Git pushes the generated directory to GitHub Pages branches.
That separation gives you a rich editor without exposing Ghost Admin to the public internet. It also makes the hosted result cheap and easy to inspect: the public repository contains the files visitors receive.
Limitations and security boundaries
This is a local publishing workflow, not a hosted Ghost service. You are responsible for keeping the local Ghost installation, Node.js, Git credentials, and administrator account secure. The public Pages repository should contain generated site output only. Never commit .env files, database exports, admin credentials, access tokens, or private drafts.
The deploy script uses force pushes for its master and gh-pages targets. That matches the project's intended publishing model, but it can overwrite history on those branches. Use a dedicated site repository, confirm the remote URL before the first publish, and do not point the script at a repository containing unrelated work.
The project documents macOS, Linux, and WSL, with Node.js 18 or 20 as the supported examples. Native Windows PowerShell is not the documented environment because the workflow depends on Bash commands and Unix tools. The repository's scripts also expect network access to install Ghost and generate the static site.
If the site still contains localhost links, consult the troubleshooting guide. If you are upgrading the old Classic v2 workflow, use the project's migration guide instead of copying v3 files into an existing Ghost folder.
FAQ
Is Ghost running publicly?
No. Ghost runs locally at http://localhost:2373. GitHub Pages serves the generated static output.
Do I need Python?
Not for v3. Python was part of the Classic v2 path.
Can I publish updates?
Yes. Start Ghost, edit or publish the post locally, then run cd ~/.ghost && ./deploy.sh again.
What happens if there are no changes?
The deploy script reports that there are no changes to publish. Make sure the post was saved or published in Ghost before trying again.
Takeaway
The useful idea is not merely hosting a blog for free. It is separating writing from delivery: Ghost handles local authoring, while gssg, the validator, and GitHub Pages handle a reviewable static artifact. Start with the v3.0.0 release, publish a test post, and verify a post, tag, and image before treating the site as ready.
Have you used a local CMS with a static publishing target? I would be interested in which part of the workflow you would automate next: preview builds, link checking, or branch protection.
AI assistance disclosure
AI assistance was used to organize this tutorial and improve wording. The commands, version references, workflow behavior, limitations, and validation claims were checked against the v3.0.0 release, its documentation, scripts, and fixture tests before publication.
Top comments (0)