Osama's Tech Blog
January 28, 2023

Deploy a Static Website with GitLab Pages

Posted on January 28, 2023  •  7 minutes  • 1461 words
Table of contents

What is a Static Website

When you have the raw .html, .js, and .css, you pretty much have a static website on your hands. If you look around on the web, you can see that you can use almost any language to render a webpage on the screen, and these usually include some form of templating and a fancy file extension that is something other than .html, .js, and .css. If these pages do not have the raw files, you can be sure that they will build them at some point. However, these can be called “dynamic” websites, and these are websites that do not have the raw .html, .js, and .css files right now, but they build them on a per request basis. A static website is when you have these files (the .html, .js, and .css files) right there in your folder before you deploy.

There are a lot of vendors you can use to host your static websites, and I’ll be focusing on GitLab, and how to deploy a website using the deployment pipeline functionality it offers.

The Barebones React Project

As a quick detour before we jump into GitLab, let me just set up a dummy project. I’ll use Vite to spin up a React app, and I’ll add React Router to it. After that, I’ll just delete everything and just keep a few test pages. You can see how this project is set up in the README file. After I remove the extra files from the src folder and add a few demo pages, this is what is left in the src folder (you can see it in GitLab ):

Src folder after removing extra files

The idea is that main.tsx has the route definitions in it, and the rest of the files are simply routes that we can navigate to in our app.

If you are looking at this and wondering how this is a static site even though there are obviously some .tsx files in there, it is because we have not built this site yet to generate the raw .html, .js, and .css files. Notice that while right now there are all kinds of file extensions in this project, after we run npm run build, the output will be the raw files we expect.

Files after build

This dist folder has only the files we need to consider this a static website. The .svg there is just for the logo image. All media assets (images, videos, etc…) will remain as is after the build.

Deploying the Project on GitLab Pages

The key ingredient we need is that dist folder, and it was generated after running npm run build. Now to deploy it on GitLab Pages as a static website, we need to generate that dist folder in a deployment pipeline. So let’s add a .gitlab-ci.yml file.

Usually we would split the build process from the deployment process. One practical reason to do this is so you can reuse whatever you built in multiple deployments. So for example you can build once then deploy to a preview environment, then once you’re satisfied that whatever is on the preview environment is ready to go, you can deploy the same build files to the production system. This would give you predictability, and you would be more confident that whatever you saw on the preview environment will look exactly the same when it is deployed to production.

So let’s start this off by adding these two stage:

# .gitlab-ci.yml
stages:
  - build
  - deploy

In the build stage, we want to generate the dist folder. Similar to how that was done on our local machine, we’ll do that in the deployment pipeline by adding a job to the build stage

web:build:
  stage: build
  image: node:latest
  script:
    - npm install
    - npm run build
    - echo '/* /index.html 200' > dist/_redirects
  artifacts:
    paths:
      - dist

Here’s a rundown of what each of these properties is doing:

Now, on to the deployment! Here is the job that deploys the website

pages:
  stage: deploy
  image: alpine:latest
  script:
    - rm -r public
    - mv ./dist ./public
  artifacts:
    paths:
      - public

The name of the job is important in this step. GitLab will only deploy your pages if the deploying job’s name is exactly equal to pages. With that in mind, here’s a rundown of what each of these properties is doing:

The complete .gitlab-ci.yml file looks like this:

# .gitlab-ci.yml
stages:
  - build
  - deploy

web:build:
  stage: build
  image: node:latest
  script:
    - npm install
    - npm run build
    - echo '/* /index.html 200' > dist/_redirects
  artifacts:
    paths:
      - dist

pages:
  stage: deploy
  image: alpine:latest
  script:
    - rm -r public
    - mv ./dist ./public
  artifacts:
    paths:
      - public

After that, you can access the page you deployed by going to the Settings -> Pages tab in your repository, then grab that URL and see the result.

Access GitLab page from settings

You can see the page that I deployed as part of this blog here . No, it’s not ugly, it’s simple, and simplicity is beautiful. Ok… in this case simplicity is ugly, but at least it makes it clear how you can deploy your own project!

Some Gotchas Specific to This Project

There are a few changes I needed to make so that the page can render correctly using this setup.

Remove the existing public folder in the page job

The naming conflict was causing issues. In the page job, I had to run rm -r public before running mv ./dist ./public to avoid this name conflict and have the dist folder be correctly renamed to public

Set the base url in vite to ./

If you look at the vite.config.ts file, you can see that I added the base property and set it like this:

export default defineConfig({
  base: './',
  plugins: [react()],
})

The reason this is necessary is to have the generated html file point to the js file like this

./assets/index-randomnumbers.js

instead of

/assets/index-randomnumbers.js

Notice the leading dot (.)? GitLab pages would have trouble loading the file without this change.

Set the basename in React Router

My page URL looks like this https://osamaelhariri.gitlab.io/TechBlog/, and this is the base URL. The /TechBlog is needed because in GitLab, you can have one page per repository. Since my repository is called TechBlog, the path to my page’s root is https://osamaelhariri.gitlab.io/TechBlog/. So since the base path includes /TechBlog, I configure React Router in the main.tsx file to take this into account:

const router = createBrowserRouter([
  // ...
], {
  basename: '/TechBlog'
});

Wrapping Up

There you have it. You own, glorious website. You can now share it with whoever you want over the internet, and they’ll be able to see it. One thing to note is that these pages could take around 30mins to an hour when you deploy it the first time. After the first time though, it gets updated quickly when you push changes to it.

Follow me

Let's get things done!