Interestingly, it is possible to post from any branch, just not from any job.

To do this, I needed to make two changes:

  1. I need to know the current state of the published data
  2. I need to change directory based on the current branch

GitLab has the ability to cache folders. Generally this is used to speed up builds by caching downloaded drivers. There is no reasons I could not use this to store the public folder. This way when I make changes to staging, I will remember the state of the root application:

cache:
  paths:
  - public

The next trick would be to publish pages to the appropriate folder, depending on current branch being built. To do this, we can look to GitLab CI/CD Environment Variables; in particular:

  • CI_COMMIT_REF_SLUG: The current branch
  • CI_DEFAULT_BRANCH: the default branch (master)

Knowing these two values, we can do a bit of bash to determine the correct place to write the content to.

pages:
  stage: deploy
  script:
  - dir="$CI_COMMIT_REF_SLUG"
  - if [ "$CI_COMMIT_REF_SLUG" == "$CI_DEFAULT_BRANCH" ]; then dir=""; fi;
  - dir="public/$dir"
  - echo "Deploying to $dir"
  - mkdir -p dir
  artifacts:
    paths:
    - public
  only:
  - staging
  - master

Don't forget to limit pages to only staging and master.

WARNING

I'm not satisfied with this.

I think it would be better to maintain the cache somewhere completely different and copy them in at a later stage, but completely re-writing the public folder each time.

The current solution will build cruft over time, but the basic idea is sound.

Answer from Jefferey Cave on Stack Overflow
Top answer
1 of 2
5

Interestingly, it is possible to post from any branch, just not from any job.

To do this, I needed to make two changes:

  1. I need to know the current state of the published data
  2. I need to change directory based on the current branch

GitLab has the ability to cache folders. Generally this is used to speed up builds by caching downloaded drivers. There is no reasons I could not use this to store the public folder. This way when I make changes to staging, I will remember the state of the root application:

cache:
  paths:
  - public

The next trick would be to publish pages to the appropriate folder, depending on current branch being built. To do this, we can look to GitLab CI/CD Environment Variables; in particular:

  • CI_COMMIT_REF_SLUG: The current branch
  • CI_DEFAULT_BRANCH: the default branch (master)

Knowing these two values, we can do a bit of bash to determine the correct place to write the content to.

pages:
  stage: deploy
  script:
  - dir="$CI_COMMIT_REF_SLUG"
  - if [ "$CI_COMMIT_REF_SLUG" == "$CI_DEFAULT_BRANCH" ]; then dir=""; fi;
  - dir="public/$dir"
  - echo "Deploying to $dir"
  - mkdir -p dir
  artifacts:
    paths:
    - public
  only:
  - staging
  - master

Don't forget to limit pages to only staging and master.

WARNING

I'm not satisfied with this.

I think it would be better to maintain the cache somewhere completely different and copy them in at a later stage, but completely re-writing the public folder each time.

The current solution will build cruft over time, but the basic idea is sound.

2 of 2
0

You can only publish changes to GitLab pages through your master branch, just as you describe. The only thing that GitLab pages does though, is to put files in the public folder in the job called pages. These files can be whatever files that you want though, as long as you manage to get them to this folder through the GitLab job.

You could try something like this:

pages:
  ...
  script:
    - mkdir -p public
    - cp -r www public
    - git checkout origin/staging
    - mkdir -p public/staging
    - cp -r www public/staging

I haven't tested this, so please let me know if it doesn't work!

If you run a GitLab job, it usually has all of the git history of your repo. There are settings that changes this though, both in git and in GitLab, so you have to make sure that you always get all of your git history to the pages job. If you have a folder that hasn't been added to git, like public, git should not change anything in it when you checkout another branch.

I think that you should also be able to set up the GitLab pages job with a schedule, so that the pages job is run even if only the staging branch has been updated, but not the master branch.

🌐
DEV Community
dev.to › zenika › gitlab-pages-preview-the-no-compromise-hack-to-serve-per-branch-pages-5599
🦊 GitLab Pages per Branch: The No-Compromise Hack to Serve Preview Pages - DEV Community
March 20, 2026 - -f public/$MAIN_BRANCH_PATH/index.html ]; then echo -e "💥\e[91;1m Unable to retrieve $CI_DEFAULT_BRANCH generated files from cache ; please regenerate $CI_DEFAULT_BRANCH files first\e[0m" exit 1 fi - rm -rf public/$CURRENT_CONTENT_PATH || true # remove last version of current branch script: - ./generate-my-html.sh --output public/$CURRENT_CONTENT_PATH || true # insert here your code that generates documentation - cd public/$EPHEMERAL_BRANCHES_PATH - tree -d -H '.' -L 1 --noreport --charset utf-8 -T "Versions" -o index.html # generate a root HTML listing all previews for easier access enviro
Discussions

git - Deploying GitLab pages for different branches - Stack Overflow
I am deploying my React app using GitLab Pages, and it works well. Here is my gitlab-ci.yml: # Using the node alpine image to build the React app image: node:alpine # Announce the URL as per CRA d... More on stackoverflow.com
🌐 stackoverflow.com
GitLab one page per branch - Stack Overflow
I have a library with multiple Git branches where I would like to generate an HTML documentation for every branch. Unfortunately, even making sub-directories named like their branch, the pipeline o... More on stackoverflow.com
🌐 stackoverflow.com
October 30, 2018
Pages for branches
Hello, I developed a website and I put it on GitLab. Then I used a template name ‘HTML’ for my “.gitlab-ci.yml”. Now I create a branche from master. In this branche I would to try what I want and create a page but not the same than the master one. How can I do that ? More on forum.gitlab.com
🌐 forum.gitlab.com
0
1
March 24, 2017
git - Deploying GitLab pages for different branches using Hugo framework? - Stack Overflow
An alternative approach would be to use netlify.com to deploy your website. It is very easy to deploy multiple branches there (netlify works with GitHub, GitLab, etc.) ... Below solution, from GitLab Pages per branch : the no-compromise hack to serve preview pages, works with Hugo or any generating ... More on stackoverflow.com
🌐 stackoverflow.com
🌐
Reddit
reddit.com › r/gitlab › per-branch gitlab pages
r/gitlab on Reddit: Per-branch GitLab Pages
June 30, 2019 -

Hey, so I haven't actually been a HPC systems engineer in years so apologies if this is quite a basic question... :)

Does anyone have experience setting up GitLab Pages to deploy per branch? The use case would be preview builds for merge requests. I have Pages working in a GitLab CI script for a master branch in a test repo, and I have preview builds linked from GitLab MRs ('View app' button) deploying to an S3 bucket with Jenkins totally separately. It would be ideal if we could just use GitLab CI for everything.

Any help appreciated; apologies if I haven't been clear anywhere and happy to try to clarify if needed.

Top answer
1 of 6
48

I've had success using the browsable artifacts for this purpose. In your example, you would create a job for your develop branch and set the PUBLIC_URL to the path on gitlab.io where the job's artifacts are published:

develop:
    artifacts:
        paths:
          - public

    environment:
        name: Develop
        url: "https://$CI_PROJECT_NAMESPACE.gitlab.io/-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/public/index.html"

    script: |
        # whatever

    stage: deploy

    variables:
        PUBLIC_URL: "/-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/public"

Setting the environment as indicated produces a »Review app« link in relevant merge requests, allowing you to get to the artifacts with a single click.

Note: if your repository is in a subgroup, you need to insert the subgroup name in two places above above between /-/ and $CI_PROJECT_NAME for the resulting URLs to work.

2 of 6
35

It is possible to keep several pages published for different pipelines/branches.

To do that you need to copy your pages content (basically test report, or whatever needs to be published) to specific unique directory in public folder. For example, the name of the directory can be the id of pipeline (CI_PIPELINE_ID). So the path to pages sources would be like public/$CI_PIPELINE_ID/.

Then the whole public folder should be defined as artifacts with specific unique name (here again "$CI_PIPELINE_ID" can be used).

Unique name for artifacts is needed to not override the artifacts with the next pipeline execution (if name is not specified, the default name will be taken https://docs.gitlab.com/ee/ci/yaml/#artifactsname).

Then you can access the published report via the link:

https://yourGitlab/yourNamespace/yourProjectName/{CI_PIPELINE_ID}/index.html

, that means you can access all your saved reports by changing the pipeline id.

My example:

stages:
  - publish

cache:
  # Required to keep artifacts from old builds, e.g. from master
  paths:
    - public

pages:
  stage: publish
  script:
    - mkdir -p public/$CI_PIPELINE_ID
    - cp target/site/allure-maven-plugin/* public/$CI_PIPELINE_ID/ -R
  artifacts:
    name: "$CI_PIPELINE_ID"
    paths:
      - public
    expire_in: 5 days
  when: always
🌐
GitHub
gist.github.com › donaldpipowitch › 2590b20520b2cf6ae01aab4f7b55f8fa
Use GitLab Pages to deploy a Storybook per branch · GitHub
🥳 Every job needs a script, but this job was just created to configure an environment." environment: name: storybook/$CI_COMMIT_REF_SLUG url: https://your-orga.gitlab.io/your-group/your-project/$CI_COMMIT_REF_SLUG/storybook/ on_stop: remove STORYBOOK only: - branches remove-storybook: stage: deployment cache: key: 'my-storybook' paths: - public script: - rm -rf "public/$CI_COMMIT_REF_SLUG/storybook" when: manual variables: GIT_STRATEGY: none # needed to prevent "Couldn't find remote ref" error environment: name: storybook/$CI_COMMIT_REF_SLUG action: stop # stage: pages (the stage name is cu
🌐
GitLab
gitlab.com › carlos fonseca › pages-per-branch
Carlos Fonseca / pages-per-branch · GitLab
May 24, 2022 - pages-per-branch · Project information · README · GitLab Pages · Created on · May 24, 2022 · Loading
🌐
GitLab
docs.gitlab.com › gitlab docs › use gitlab › deploy and release your application › gitlab pages › settings
GitLab Pages settings | GitLab Docs
Remember that GitLab Pages are by default branch/tag independent, and their deployment relies solely on what you specify in .gitlab-ci.yml.
🌐
Gitlab
k33g.gitlab.io › articles › 2020-07-23-GITLAB-PAGES-EN.html
🇬🇧 Every GitLab Page deserves a real CI/CD | K33G's website
So, it means that you can deploy a GitLab Page from a feature branch, without impacting your production page, in a new temporary environment 🤩
Find elsewhere
🌐
GitLab
docs.gitlab.com › gitlab docs › use gitlab › deploy and release your application › gitlab pages › parallel deployments
GitLab Pages parallel deployments | GitLab Docs
pages: stage: deploy script: - echo "Pages accessible through ${CI_PAGES_URL}" pages: # specifies that this is a Pages job and publishes the default public directory path_prefix: "$CI_COMMIT_BRANCH" ... Is converted to lowercase. Can contain numbers (0-9), letters (a-z), and periods (.).
Top answer
1 of 2
1

Using cache is the right direction, but the solution needs some side features to be fully satisfying.

With below solution, from GitLab Pages per branch : the no-compromise hack to serve preview pages , you can have these features :

  • main content is exposed on $CI_PAGES_URL and the path is configurable with $CURRENT_CONTENT_PATH
  • Per-branch preview content is exposed on $CI_PAGES_URL/preview, with a homepage to easily navigate to branches content
  • Path to root preview folder is configurable with $EPHEMERAL_BRANCHES_PATH variable to hide preview content by obfuscation
  • Generated pages are associated with environments to take advantage of auto-cleaning on branch deletion
  • To avoid disturbing already existing environments, pages environment are placed under a pages folder
  • If main content has not been generated in current cache, or if the cache has been deleted, an error is triggered, to avoid accidental deletion
  • Deletion job can be triggered manually with any cache path as input, to clean outdated data
  • Code can safely be added to an existing project pipeline without causing trouble with already existing jobs
  • The workflow:rules can be deleted if you already have your own, or updated to match your flow
  • The job must be named pages and the artifact must be a public folder to be deployed to GitLab Pages (or you can use the pages:publish keyword)
workflow:
  rules: # disable tag pipelines and duplicate MR pipelines
    - if: $CI_COMMIT_BRANCH

variables:
  EPHEMERAL_BRANCHES_PATH: preview # subpath to ephemeral branches content for preview, anything will work

pages:
  stage: build
  image: alpine:3.18
  cache:
    key: gitlab-pages
    paths: [public]
  before_script:
    # default available 'tree' app in alpine image does not work as intended
    - apk add tree
    # CURRENT_CONTENT_PATH is defined in rules, different between main branch and ephemeral branches
    - mkdir -p public/$CURRENT_CONTENT_PATH && ls public/$CURRENT_CONTENT_PATH/..
    - | # avoid deleting main branch content when cache has been erased
      if [ "$CI_COMMIT_BRANCH" != "$CI_DEFAULT_BRANCH" ] && [ ! -d public/$CI_DEFAULT_BRANCH ]; then
        echo -e "💥\e[91;1m Unable to retrieve $CI_DEFAULT_BRANCH generated files from cache ; please regenerate $CI_DEFAULT_BRANCH files first\e[0m"
        exit 1
      fi
    - rm -rf public/$CURRENT_CONTENT_PATH || true # remove last version of current branch
  script:
    - ./generate-my-html.sh --output build-docs || true # insert here your code that generates documentation
    - mv --verbose build-docs public/$CURRENT_CONTENT_PATH
    - cd public/$EPHEMERAL_BRANCHES_PATH
    - tree -d -H '.' -L 1 --noreport --charset utf-8 -T "Versions" -o index.html # generate a root HTML listing all previews for easier access
  environment:
    name: pages/$CI_COMMIT_BRANCH
    action: start
    url: $CI_PAGES_URL/$CURRENT_CONTENT_PATH
    on_stop: pages-clean-preview
  rules:
    # 'main branch' is exposed at GitLab Pages root
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
      variables:
        CURRENT_CONTENT_PATH: "."
    # other (short-lived) branches generation are exposed in 'EPHEMERAL_BRANCHES_PATH/branch-name-sanitized' sub path
    - variables:
        CURRENT_CONTENT_PATH: $EPHEMERAL_BRANCHES_PATH/$CI_COMMIT_REF_SLUG
  artifacts:
    paths: [public]
    expire_in: 1h

pages-clean-preview:
  stage: build
  image: alpine:3.18
  cache:
    key: gitlab-pages
    paths: [public]
  variables:
    GIT_STRATEGY: none # git files not available after branch deletion
    FOLDER_TO_DELETE: preview/$CI_COMMIT_BRANCH # an indirection to allow arbirtraty deletion when launching this job
  script:
    - rm -rf public/$FOLDER_TO_DELETE
  environment:
    name: pages/$CI_COMMIT_BRANCH
    action: stop
  rules:
    - if: $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
      when: manual
      allow_failure: true
2 of 2
0

Try prepending the following to your .gitlab-ci.yml:

cache:
  key: "$CI_JOB_NAME"
  paths:
    - public

Adding a cache step will tell GitLab to keep the specified paths across builds—each new build will see what the previous ones have produced. The key element causes GitLab to keep the cache across branches, rather than keep a separate branch for each cache.

Of course this has the side effect that your pipelines will no longer start off with a fresh public dir—rather, it will contain whatever the last pipeline left in there. Everything any of your pipelines ever places in public will stay there until explicitly deleted or overwritten—you will need to handle that in some way if the set of artifacs changes between builds.

(Haven’t tried this myself yet, but this is how I understand it works.)

🌐
GitLab
about.gitlab.com › blog › product › gitlab pages features review apps and multiple website deployment
GitLab Pages features review apps and multiple website deployment
April 9, 2025 - The below CI config will not only create a pages deployment when there is a commit to the default branch, but also for any commit to branches named v1, v2, or v3.
🌐
GitLab
forum.gitlab.com › how to use gitlab
Pages for branches - How to Use GitLab - GitLab Forum
March 24, 2017 - Hello, I developed a website and I put it on GitLab. Then I used a template name ‘HTML’ for my “.gitlab-ci.yml”. Now I create a branche from master. In this branche I would to try what I want and create a page but not t…
🌐
GitLab
gitlab.com › gitlab.org › #16208
Multiple version Pages support (#16208) · Issues · GitLab.org / GitLab · GitLab
Description Is it possible to have multiple GitLab Pages generated based on branches or tags? For example, have the...
🌐
Getlektor
getlektor.com › docs › deployment › glpages
GitLab Pages | Documentation | Lektor Static Content Management System
There are two types of GitLab pages: user and project pages. User pages are hosted at <username>.gitlab.io and project pages at <username>.gitlab.io/<project>. There can only be one user page and the repository for it needs to be named <username>.gitlab.io. The branch does not matter.
🌐
University of Toronto
microfluidics.utoronto.ca › help › help
Introduction · Pages · Project · User · Help · GitLab
GitLab Pages offers configuration options to customize your static site's deployment and presentation. With Pages settings, you can: Serve custom error pages for 403 and 404 responses. Configure URL redirects through _redirects files. Deploy pages from any branch using CI/CD rules.
🌐
Stack Overflow
stackoverflow.com › questions › 64242072 › deploying-gitlab-pages-for-different-branches-using-hugo-framework
git - Deploying GitLab pages for different branches using Hugo framework? - Stack Overflow
The job must be named pages and the artifact must be a public folder to be deployed to GitLab Pages (or you can use the pages:publish keyword) workflow: rules: # disable tag pipelines and duplicate MR pipelines - if: $CI_COMMIT_BRANCH variables: EPHEMERAL_BRANCHES_PATH: preview # subpath to ephemeral branches content for preview, anything will work pages: stage: build image: alpine:3.18 cache: key: gitlab-pages paths: [public] before_script: # default available 'tree' app in alpine image does not work as intended - apk add tree # CURRENT_CONTENT_PATH is defined in rules, different between main branch and ephemeral branches - mkdir -p public/$CURRENT_CONTENT_PATH && ls public/$CURRENT_CONTENT_PATH/.. - | # avoid deleting main branch content when cache has been erased if [ "$CI_COMMIT_BRANCH" != "$CI_DEFAULT_BRANCH" ] && [ !
🌐
GitLab
forum.gitlab.com › gitlab ci/cd
Modfy the deployed GitlabPages in CI rather than replacing them - GitLab CI/CD - GitLab Forum
December 3, 2021 - I have a repo with testcases that are run using CI. The testreport (a static HTML Site) gets deployed to pages. Since the testcases differ on each branch I wanted to have reports for multiple branches availiable. I thought the runner automatically gets the currently deployed pages folder, so I used / by placing the site in subfolders inside the pages folder.
🌐
GitLab
forum.gitlab.com › gitlab ci/cd
With GitLab Pages and CI/CD, how do you build different branches to different (sub-)directories? - GitLab CI/CD - GitLab Forum
August 6, 2020 - [This is a duplicate of https://stackoverflow.com/questions/63281252/with-gitlab-pages-and-ci-cd-how-do-you-build-different-branches-to-different-s, where I’ll also post the solution if one exists/materializes :grimacing…
Top answer
1 of 1
2

Only one site is supported on these platforms. You cannot have separate sites for separate branches.

The only way you might do this without some sort of cache/artifact retrieval (as you mentioned is another option in your question) is to build all your branches at once when publishing your Pages site.

How exactly you do that depends on a lot of factors, including what tool(s) you're using to build your site and if they are context-dependent -- but it might look something like this in GitLab

pages:
  # fetch the whole repo
  # this logic can change if you're on a detached head, like an MR
  # so we account for that here
  before_script: | 
        if [[ -n "$CI_COMMIT_BRANCH" ]]; then  # branch pipelines
            git remote set-url origin "https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
            git fetch origin
            git checkout $CI_COMMIT_BRANCH
        fi
        if [[ -n "$CI_MERGE_REQUEST_IID" ]]; then  # MR pipelines
            git remote set-url origin "https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_MERGE_REQUEST_SOURCE_PROJECT_PATH}.git"
            git fetch origin
        fi
  script: |
        mkdir public
        branches=()
        # ref: https://stackoverflow.com/a/3847586/5747944
        eval "$(git for-each-ref --shell --format='branches+=(%(refname))' refs/heads/)"
        for branch in "${branches[@]}"; do
            git checkout "$branch"
            # build each branch and output to public directory
            # YOU implement this
            make build "$branch" -o "public/${branch}"
        done
  artifacts:
    paths:
      - public
  environment: # ensure outdated jobs are skipped
    name: pages # https://docs.gitlab.com/ee/ci/environments/deployment_safety.html#skip-outdated-deployment-jobs
🌐
GitLab
gitlab.com › gitlab.org › gitlab foss › #35141
Multiple version Pages support (#35141) · Issues · GitLab.org / GitLab FOSS · GitLab
October 16, 2016 - Description Is it possible to have multiple GitLab Pages generated based on branches or tags? For example, have the...