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:

  1. 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.”
  2. 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 as output, .next, …).
  3. 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?

  1. Read from the repository: for this, we use a predefined action (actions/checkout).
  2. Prepare Node: we use another predefined action (actions/setup-node) with the version we’ve configured (in strategy->matrix->node-version).
  3. Install Yarn: you can skip this step if you decide to use npm instead of yarn. Similarly, you’ll need to adapt it if you use pnpm.
  4. Generate the code: these are the “Install dependencies” and “Build the project” steps.
  5. I’ve added a step to check what is generated (ls -R dist), but it’s only necessary for debugging.
  6. 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
  • GIT_USER_NAME
    • you can find this with git config --global user.name
  • API_TOKEN_GITHUB
    • You’ll need a token with permissions for repo and workflow.
  • GIT_USER
    • your GitHub username (you can find it in the URL: https://github.com/<username>/<repo>/)
  • GIT_REPO
    • identifier of the public repo (you can find it in the URL: https://github.com/<username>/<repo>/)

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