How to Deploy a Website on GitHub Pages
In the articles How to Create a Blog with Astro and How to Adapt Astro to TypeScript, I explained two ways to create a blog with Astro (with and without TypeScript). But a blog isn’t of much use if it isn’t deployed on a server that others can access.
In this article, I’ll show you how to deploy the website, specifically how to do it on GitHub Pages.
And if you’re interested in why I chose GitHub Pages, you can find out here.
Deploying the Website on a Server
As with any website based on JavaScript/TypeScript, we need to follow these steps:
- Check the website: lint, test, check, … and combinations of these and other commands that may vary depending on the framework and help uncover a good number of issues, errors, and “things to improve.”
- Build the website: build, an easy-to-remember command that generates the “usable” files in a folder generally called
dist
(although in some cases it may have a different name, such asoutput
,.next
, …). - Deploy the website: basically, copy the files generated by the
build
to the server (please, no FTP, use secure systems).
You can perform these steps manually… but it’s common to automate them.
In my case, I’ll do it with GitHub Actions, and I’ll publish on GitHub Pages. Additionally, I’ll keep the base code in a private repo and the GitHub Pages code in a public repo. Why? You can read about it here.
Step 1: Private Repository Setup
Is there anyone who doesn’t develop against a repo nowadays?
I assume not.
So the only thing you might be missing is that it’s a private repo so that any code you have that is specific to you is protected from prying eyes.
That said, this repo contains all the source code for your website generated with Astro and will be responsible for building the site and sending the resulting static files to a second public repository.
flowchart LR
A[Private Repo] --->|"build (CI)"| A
A --->|"copy (CI)"| B[Public Repo]
B --->|deploy| C[GitHub Pages]
Step 2: Set Up the GitHub Actions Workflow
We’re going to define a GitHub Actions workflow in the private repo. This workflow will handle building the project and uploading the generated files to the public repo.
To do this, you can create the workflow file with the following command from the repo’s root folder:
mkdir -p .github/workflows && touch .github/workflows/workflow-name.yml # in my case, I named it `build-and-store-website.yml
Then, you need to write the following code in that file:
name: Deploy statics on public repo
on:
push:
branches:
- main
paths:
- 'src/content/**'
- 'src/pages/**'
jobs:
build:
name: create-package
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Use Node.js 21
with:
node-version: ${{ matrix.node-version }}
- name: Install Yarn
run: npm install --global yarn
- name: Install dependencies with Yarn
run: yarn install
- name: Build the project with Yarn
run: yarn build
env:
CI: false
- run: ls -R dist
- name: Push built files to public repo
run: |
git config --global user.email "${{ secrets.GIT_USER_EMAIL }}"
git config --global user.name "${{ secrets.GIT_USER_NAME }}"
git clone https://${{ secrets.API_TOKEN_GITHUB }}@github.com/${{ secrets.GIT_USER }}/${{ secrets.GIT_REPO }}.git deploy_repo
rm -rf deploy_repo/*
cp -R dist/* deploy_repo/
cd deploy_repo
git add .
COMMIT_MESSAGE="Deploy new version - $(date -u +"%Y-%m-%d %H:%M:%S UTC")"
git commit -m "$COMMIT_MESSAGE"
git push origin main
env:
GITHUB_TOKEN: ${{ secrets.API_TOKEN_GITHUB }}
Note:
Although I try to keep articles up to date (especially the code you can copy), please check the code because there may be outdated versions.
When Is It Triggered?
In the file, we have a section (right after the name) with the on
key (which translates to run when the following happens
).
So, when do we want it to run?
When we make content changes that are ready for production.
I won’t go into explaining Git, so in short, this translates to:
When we push to the main branch that has changes in the src/content
and src/pages
folders.
Translated into YAML (the format used to define these workflows), it looks like this:
on:
push:
branches:
- main
paths:
- 'src/content/**'
- 'src/pages/**'
What Should It Do?
In addition to the trigger section, we have a jobs section, although this workflow only has one. This is because each job has its own environment, and it’s simpler to generate the website’s static files and copy them to the public repo than to define a job for each step and then share the artifacts between each job’s environment.
The important points to understand about this job are as follows:
runs-on
: which operating system the job will use for its execution (in this case, the latest version of Ubuntu).steps
: steps to complete the job.
So, what steps do we need to complete the job?
- Read from the repository: for this, we use a predefined action (
actions/checkout
). - Prepare Node: we use another predefined action (
actions/setup-node
) with the version we’ve configured (instrategy->matrix->node-version
). - Install Yarn: you can skip this step if you decide to use
npm
instead ofyarn
. Similarly, you’ll need to adapt it if you usepnpm
. - Generate the code: these are the “Install dependencies” and “Build the project” steps.
- I’ve added a step to check what is generated (
ls -R dist
), but it’s only necessary for debugging. - Finally, a “bare” step to copy the generated static files to the public repo.
jobs:
build:
name: create-package
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [21]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
name: Use Node.js 21
with:
node-version: ${{ matrix.node-version }}
- name: Install Yarn
run: npm install --global yarn
- name: Install dependencies with Yarn
run: yarn install
- name: Build the project with Yarn
run: yarn build
env:
CI: false
- run: ls -R dist
- name: Push built files to public repo
run: |
git config --global user.email "${{ secrets.GIT_USER_EMAIL }}"
git config --global user.name "${{ secrets.GIT_USER_NAME }}"
git clone https://${{ secrets.API_TOKEN_GITHUB }}@github.com/${{ secrets.GIT_USER }}/${{ secrets.GIT_REPO }}.git deploy_repo
rm -rf deploy_repo/*
cp -R dist/* deploy_repo/
cd deploy_repo
git add .
COMMIT_MESSAGE="Deploy new version - $(date -u +"%Y-%m-%d %H:%M:%S UTC")"
git commit -m "$COMMIT_MESSAGE"
git push origin main
env:
GITHUB_TOKEN: ${{ secrets.API_TOKEN_GITHUB }}
As you can see, all the parts that may vary depending on who you are or the repo you use are set as secrets
of the repo. This way, you can copy the content and only need to configure those secrets
for the workflow to work correctly.
To make this step easier, here’s a list of which variables you should configure in the secrets
:
GIT_USER_EMAIL
- you can find this with
git config --global user.email
- you can find this with
GIT_USER_NAME
- you can find this with
git config --global user.name
- you can find this with
API_TOKEN_GITHUB
- You’ll need a token with permissions for
repo
andworkflow
.
- You’ll need a token with permissions for
GIT_USER
- your GitHub username (you can find it in the URL:
https://github.com/<username>/<repo>/
)
- your GitHub username (you can find it in the URL:
GIT_REPO
- identifier of the public repo (you can find it in the URL:
https://github.com/<username>/<repo>/
)
- identifier of the public repo (you can find it in the URL:
The Devil is in the detail
Astro, like many other frameworks (in fact, more and more frameworks), has telemetry enabled by default (that is, it receives data about what you do with Astro to, at least in theory, improve the framework itself).
If you disagree with this practice, you need to modify the project’s package.json
to disable it.
Right now, your package.json
will look like this:
{
"name": "astro-blog-template",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^3.1.4",
"@astrojs/rss": "^4.0.7",
"@astrojs/sitemap": "^3.1.6",
"astro": "^4.14.5",
"@astrojs/check": "^0.9.3",
"typescript": "^5.5.4"
}
}
And after the change, it should look like this:
{
"name": "astro-blog-template",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro telemetry disable && astro check && astro build",
"preview": "astro preview",
"astro": "astro"
},
"dependencies": {
"@astrojs/mdx": "^3.1.4",
"@astrojs/rss": "^4.0.7",
"@astrojs/sitemap": "^3.1.6",
"astro": "^4.14.5",
"@astrojs/check": "^0.9.3",
"typescript": "^5.5.4"
}
}
Step 3: Public Repository Setup
The second repository is public and contains only the static files that will be published on GitHub Pages. In this case, we don’t need a specific workflow for publishing; it’s enough to enable GitHub Pages from the settings and select the main
branch as the source of the files to publish.
You will need to configure your custom domain (if you want to use one) and check the box to enforce the use of HTTPS.
A Little Trick
Thanks for making it this far. In upcoming articles, I’ll continue explaining how to create a custom template.
But if you don’t want to create one, you can use mine (the one you’ll have, more or less, if you follow all the steps in this series of articles) with the following command:
yarn create astro -- --template borjalofe/astro-blog-template