diff --git a/.gitlab/ci/docs.gitlab-ci.yml b/.gitlab/ci/docs.gitlab-ci.yml
index c78f243ff62d3f5f2636d234544b873b1ed45303..c7b9c56c1a50da13a2b05340c637ecb146e37b4f 100644
--- a/.gitlab/ci/docs.gitlab-ci.yml
+++ b/.gitlab/ci/docs.gitlab-ci.yml
@@ -39,6 +39,41 @@ review-docs-cleanup:
   script:
     - ./scripts/trigger-build.rb docs cleanup
 
+.review-docs-hugo:
+  extends:
+    - .default-retry
+    - .docs:rules:review-docs
+  image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION}-alpine
+  stage: review
+  needs: []
+  variables:
+    GIT_DEPTH: 1
+    # By default, deploy the Review App using the `main` branch of the `gitlab-org/technical-writing-group/gitlab-docs-hugo` project
+    DOCS_BRANCH: main
+  environment:
+    name: review-docs/mr-${CI_MERGE_REQUEST_IID}-hugo
+    auto_stop_in: 2 weeks
+    url: https://new.docs.gitlab.com/upstream-review-mr-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}
+    on_stop: review-docs-hugo-cleanup
+  before_script:
+    - source ./scripts/utils.sh
+    - install_gitlab_gem
+
+# Deploy documentation review app by using GitLab Docs Hugo project (gitlab-org/technical-writing-group/gitlab-docs-hugo)
+review-docs-hugo-deploy:
+  extends: .review-docs-hugo
+  script:
+    - ./scripts/trigger-build.rb docs-hugo deploy
+
+# Cleanup remote environment of gitlab-org/technical-writing-group/gitlab-docs-hugo
+review-docs-hugo-cleanup:
+  extends: .review-docs-hugo
+  environment:
+    name: review-docs/mr-${CI_MERGE_REQUEST_IID}-hugo
+    action: stop
+  script:
+    - ./scripts/trigger-build.rb docs-hugo cleanup
+
 .docs-markdown-lint-image:
   # When updating the image version here, update it in /scripts/lint-doc.sh too.
   image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-docs/lint-markdown:alpine-3.20-vale-3.7.1-markdownlint2-0.14.0-lychee-0.15.1
diff --git a/doc/development/documentation/img/manual_build_docs_v14_6.png b/doc/development/documentation/img/manual_build_docs_v14_6.png
deleted file mode 100644
index 731bda3dd56a77b2170ce40059db027c0e64c5f5..0000000000000000000000000000000000000000
Binary files a/doc/development/documentation/img/manual_build_docs_v14_6.png and /dev/null differ
diff --git a/doc/development/documentation/review_apps.md b/doc/development/documentation/review_apps.md
index 6695359a4977745083363133810ce1f871d643d7..a56dcc8be1b023bdd9cd0227b54c3fe41b2ee4f7 100644
--- a/doc/development/documentation/review_apps.md
+++ b/doc/development/documentation/review_apps.md
@@ -7,144 +7,63 @@ description: Learn how documentation review apps work.
 
 # Documentation review apps
 
-If you're a GitLab team member and your merge request contains documentation changes, you can use a review app to preview
-how they would look if they were deployed to the [GitLab Docs site](https://docs.gitlab.com).
-
-Review apps are enabled for the following projects:
-
-- [GitLab](https://gitlab.com/gitlab-org/gitlab)
-- [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab)
-- [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner)
-- [GitLab Charts](https://gitlab.com/gitlab-org/charts/gitlab)
-- [GitLab Operator](https://gitlab.com/gitlab-org/cloud-native/gitlab-operator)
-
-Alternatively, check the [`gitlab-docs` development guide](https://gitlab.com/gitlab-org/gitlab-docs/blob/main/README.md#development-when-contributing-to-gitlab-documentation)
-or [the GDK documentation](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/main/doc/howto/gitlab_docs.md)
-to render and preview the documentation locally.
-
-## How to trigger a review app
-
-If a merge request has documentation changes, use the `review-docs-deploy` manual job
-to deploy the documentation review app for your merge request.
-
-![Manual trigger a documentation review app](img/manual_build_docs_v14_6.png)
-
-The `review-docs-deploy*` job triggers a cross project pipeline and builds the
-docs site with your changes. When the pipeline finishes, the review app URL
-appears in the merge request widget. Use the app to go to your changes.
-
-The `review-docs-cleanup` job is triggered automatically on merge. This job deletes the review app.
-
-You must have the Developer role for the project. Users without the Developer role, such
-as external contributors, cannot run the manual job. In that case, ask someone from
-the GitLab team to run the job.
-
-## Technical aspects
-
-If you want to know the in-depth details, here's what's really happening:
-
-1. You manually run the `review-docs-deploy` job in a merge request.
-1. The job downloads and runs the [`scripts/trigger-build.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/trigger-build.rb)
-   script with the `docs deploy` flag, which triggers the "Triggered from `gitlab-org/gitlab` 'review-docs-deploy' job"
-   pipeline trigger in the `gitlab-org/gitlab-docs` project for the `$DOCS_BRANCH` (defaults to `main`).
-1. The preview URL is shown both at the job output and in the merge request
-   widget. You also get the link to the remote pipeline.
-1. In the `gitlab-org/gitlab-docs` project, the pipeline is created and it
-   [skips most test jobs](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/d41ca9323f762132780d2d072f845d28817a5383/.gitlab/ci/rules.gitlab-ci.yml#L101-103)
-   to lower the build time.
-1. After the docs site is built, the HTML files are uploaded as artifacts to
-   a GCP bucket (see [issue `gitlab-com/gl-infra/reliability#11021`](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/11021)
-   for the implementation details).
-
-The following GitLab features are used among others:
-
-- [Manual jobs](../../ci/jobs/job_control.md#create-a-job-that-must-be-run-manually)
-- [Multi project pipelines](../../ci/pipelines/downstream_pipelines.md#multi-project-pipelines)
-- [Review apps](../../ci/review_apps/index.md)
-- [Artifacts](../../ci/yaml/index.md#artifacts)
-- [Merge request pipelines](../../ci/pipelines/merge_request_pipelines.md)
-
-## How to add a new documentation review app
-
-In case a documentation review app is missing from one of the documentation
-projects, you can use the following CI/CD template to add a manually triggered review app:
-
-```yaml
-# Set up documentation review apps
-# https://docs.gitlab.com/ee/development/documentation/review_apps.html
-.review-docs:
-  image: ruby:3.1-alpine
-  needs: []
-  before_script:
-    - gem install gitlab --no-doc
-    # We need to download the script rather than clone the repo since the
-    # review-docs-cleanup job will not be able to run when the branch gets
-    # deleted (when merging the MR).
-    - apk add --update openssl
-    - wget https://gitlab.com/gitlab-org/gitlab/-/raw/master/scripts/trigger-build.rb
-    - chmod 755 trigger-build.rb
-  variables:
-    GIT_STRATEGY: none
-    DOCS_REVIEW_APPS_DOMAIN: docs.gitlab-review.app
-    # By default, deploy the Review App using the `main` branch of the `gitlab-org/gitlab-docs` project
-    DOCS_BRANCH: main
-  when: manual
-  allow_failure: true
-
-# Trigger a docs build in gitlab-docs
-# Useful to preview the docs changes live
-# https://docs.gitlab.com/ee/development/documentation/index.html#previewing-the-changes-live
-review-docs-deploy:
-  extends:
-    - .review-docs
-  environment:
-    name: review-docs/mr-${CI_MERGE_REQUEST_IID}
-    # DOCS_REVIEW_APPS_DOMAIN and DOCS_GITLAB_REPO_SUFFIX are CI variables
-    # Discussion: https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/14236/diffs#note_40140693
-    auto_stop_in: 2 weeks
-    url: https://${DOCS_BRANCH}-${DOCS_GITLAB_REPO_SUFFIX}-${CI_MERGE_REQUEST_IID}.${DOCS_REVIEW_APPS_DOMAIN}/${DOCS_GITLAB_REPO_SUFFIX}
-    on_stop: review-docs-cleanup
-  script:
-  - ./trigger-build.rb docs deploy
-
-# Cleanup remote environment of gitlab-docs
-review-docs-cleanup:
-  extends:
-    - .review-docs
-  environment:
-    name: review-docs/mr-${CI_MERGE_REQUEST_IID}
-    action: stop
-  script:
-  - ./trigger-build.rb docs cleanup
-```
-
-You may need to add some rules when those jobs run, it depends on the project.
-You can find the current implementations:
-
-- [GitLab](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/docs.gitlab-ci.yml)
-- [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/ee8699658c8a7d4c635ad503ef0b825ac592dc4b/gitlab-ci-config/gitlab-com.yml#L367-391)
-- [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner/-/blob/main/.gitlab/ci/docs.gitlab-ci.yml)
-- [GitLab Charts](https://gitlab.com/gitlab-org/charts/gitlab/-/blob/aae7ee8d23a60d6025eec7d1a864ce244f21cd85/.gitlab-ci.yml#L629-679)
-- [GitLab Operator](https://gitlab.com/gitlab-org/cloud-native/gitlab-operator/-/blob/5fa29607cf9286b510148a8f5fef7595dca34186/.gitlab-ci.yml#L180-228)
-
-## Troubleshooting review apps
-
-### `NoSuchKey The specified key does not exist`
-
-If you see the following message in a review app, either the site is not
-yet deployed, or something went wrong with the downstream pipeline in `gitlab-docs`.
-
-```plaintext
-NoSuchKeyThe specified key does not exist.No such object: <URL>
-```
-
-In that case, you can:
-
-- Wait a few minutes and the review app should appear online.
-- Check the `review-docs-deploy` job's log and verify the URL. If the URL shown in the merge
-  request UI is different than the job log, try the one from the job log.
-- Check the status of the remote pipeline from the link in the merge request's job output.
-  If the pipeline failed or got stuck, GitLab team members can ask for help in the `#docs`
-  internal Slack channel. Contributors can ping a
-  [technical writer](https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments-to-devops-stages-and-groups)
-  in the merge request.
+GitLab team members can deploy a [review app](../../ci/review_apps/index.md) for merge requests with documentation
+changes. The review app helps you preview what the changes would look like if they were deployed to either:
+
+- The [GitLab Docs site](https://docs.gitlab.com).
+- The [new GitLab Docs site](https://new.docs.gitlab.com). The site is still in development.
+
+Review apps deployments are available for these projects:
+
+- [GitLab](https://gitlab.com/gitlab-org/gitlab) (configuration: <https://gitlab.com/gitlab-org/gitlab/-/blob/b4f30955e41aeab862c59f7102529e1a5a2659d1/.gitlab/ci/docs.gitlab-ci.yml#L1-40>)
+- [Omnibus GitLab](https://gitlab.com/gitlab-org/omnibus-gitlab) (configuration: <https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/bae935d36ea9296941c20233b637d780847c443a/gitlab-ci-config/gitlab-com.yml#L304-328>)
+- [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner) (configuration: <https://gitlab.com/gitlab-org/gitlab-runner/-/blob/69d2416333df4712cbd95d90214b10f100183df3/.gitlab/ci/docs.gitlab-ci.yml#L64-110>)
+- [GitLab Charts](https://gitlab.com/gitlab-org/charts/gitlab) (configuration: <https://gitlab.com/gitlab-org/charts/gitlab/-/blob/8222a7c3cf28d8ad3f454784a04cad8921b6638b/.gitlab/ci/review-docs.yml#L2-49>)
+- [GitLab Operator](https://gitlab.com/gitlab-org/cloud-native/gitlab-operator) (configuration: <https://gitlab.com/gitlab-org/cloud-native/gitlab-operator/-/blob/56200465a5c8f8857f3aef2c309bdf2ca9e4b672/.gitlab-ci.yml#L210-257>)
+
+## Deploy a review app and preview changes
+
+Prerequisites:
+
+- You must have the Developer role for the project. External contributors cannot run these jobs and
+should ask a GitLab team member to run the jobs for them.
+
+Merge requests with documentation changes have the following jobs available:
+
+- `review-docs-deploy`, which uses Nanoc static-site generation using
+  [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs).
+- `review-docs-hugo-deploy`: Optional. This review app is only for testing the Hugo static site generation from
+  [`gitlab-docs-hugo`](https://gitlab.com/gitlab-org/technical-writing-group/gitlab-docs-hugo),
+  which is still in development.
+
+To deploy a review app and preview changes:
+
+1. [Manually run](../../ci/jobs/job_control.md#run-a-manual-job) either (or both) of these jobs. These jobs trigger a
+   [multi project pipelines](../../ci/pipelines/downstream_pipelines.md#multi-project-pipelines), build the
+   documentation site with your changes, and deploy a site with your changes.
+1. When the pipeline finishes, select **View app** on either deployment to open a browser and review the
+   changes introduced by the merge request.
+
+The `review-docs-cleanup` and `review-docs-hugo-cleanup` jobs are triggered automatically on merge. These job delete
+the review app.
+
+## How documentation review apps work
+
+Documentation review apps follow this process:
+
+1. You manually run the `review-docs-deploy` or `review-docs-hugo-deploy` job in a merge request.
+1. The job downloads (if outside of `gitlab` project) and runs the
+   [`scripts/trigger-build.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/trigger-build.rb) script with
+   either:
+
+   - The `docs deploy` flag, which triggers a pipeline in the `gitlab-org/gitlab-docs` project.
+   - The `docs-hugo deploy` flag, which triggers a pipeline in the `gitlab-org/technical-writing-group/gitlab-docs-hugo`
+     project.
+
+   The `DOCS_BRANCH` environment variable determines which branch of either the `gitlab-org/gitlab-docs` project or the
+   `gitlab-org/technical-writing-group/gitlab-docs-hugo` project are used. If not set, the `main` branch is used.
+1. After the documentation preview site is built:
+   - For `nanoc` builds, the HTML files are uploaded as [artifacts](../../ci/yaml/index.md#artifacts) to a GCP bucket.
+     For implementation details, see
+     [issue `gitlab-com/gl-infra/reliability#11021`](https://gitlab.com/gitlab-com/gl-infra/reliability/-/issues/11021).
+   - For `hugo` builds, a [parallel deployment](../../user/project/pages/index.md#parallel-deployments) is deployed.
diff --git a/doc/development/geo.md b/doc/development/geo.md
index 2286ce872d2e7b54738a7e8e786a577f527f6353..bdfd63bd8cb3c3a996cc52176dbe0040e960086f 100644
--- a/doc/development/geo.md
+++ b/doc/development/geo.md
@@ -9,7 +9,7 @@ info: Any user with at least the Maintainer role can merge updates to this conte
 Geo connects GitLab instances together. One GitLab instance is
 designated as a **primary** site and can be run with multiple
 **secondary** sites. Geo orchestrates quite a few components that can be seen on
-the diagram below and are described in more detail within this document.
+the diagram below and are described in more detail in this document.
 
 ![Geo Architecture Diagram](../administration/geo/replication/img/geo_architecture_v13_8.png)
 
diff --git a/scripts/trigger-build.rb b/scripts/trigger-build.rb
index e37109630c1847fe3d81dd2283a83e4eba67789b..c8553a8271c17b90d7019b98f3fedc5b21543402 100755
--- a/scripts/trigger-build.rb
+++ b/scripts/trigger-build.rb
@@ -29,7 +29,7 @@ def self.variables_for_env_file(variables)
   class Base
     # Can be overridden
     STABLE_BRANCH_REGEX = /^[\d-]+-stable(-ee|-jh)?$/
-    def self.access_token
+    def access_token
       ENV['PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE']
     end
 
@@ -71,7 +71,7 @@ def simple_forwarded_variables
     def com_gitlab_client
       @com_gitlab_client ||= Gitlab.client(
         endpoint: endpoint,
-        private_token: self.class.access_token
+        private_token: access_token
       )
     end
 
@@ -246,8 +246,7 @@ def version_param_value(_version_file)
   # - https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/b44483f05c5e22628ba3b49ec4c7f8761c688af0/gitlab-ci-config/gitlab-com.yml#L199-224
   # - https://gitlab.com/gitlab-org/omnibus-gitlab/-/blob/b44483f05c5e22628ba3b49ec4c7f8761c688af0/gitlab-ci-config/gitlab-com.yml#L356-380
   class Docs < Base
-    def self.access_token
-      # Default to "DOCS_PROJECT_API_TOKEN" at https://gitlab.com/gitlab-org/gitlab-docs/-/settings/access_tokens
+    def access_token
       ENV['DOCS_PROJECT_API_TOKEN'] || super
     end
 
@@ -345,6 +344,36 @@ def display_success_message
     end
   end
 
+  class DocsHugo < Docs
+    def access_token
+      ENV['DOCS_HUGO_PROJECT_API_TOKEN'] || super
+    end
+
+    private
+
+    def downstream_environment
+      "upstream-review/mr-${CI_MERGE_REQUEST_IID}"
+    end
+
+    def review_slug
+      identifier = ENV['CI_MERGE_REQUEST_IID'] || ENV['CI_COMMIT_REF_SLUG']
+
+      "#{project_slug}-#{identifier}"
+    end
+
+    def downstream_project_path
+      ENV.fetch('DOCS_PROJECT_PATH', 'gitlab-org/technical-writing-group/gitlab-docs-hugo')
+    end
+
+    def trigger_token
+      ENV['DOCS_HUGO_TRIGGER_TOKEN']
+    end
+
+    def app_url
+      "https://new.docs.gitlab.com/upstream-review-mr-#{review_slug}/"
+    end
+  end
+
   class DatabaseTesting < Base
     IDENTIFIABLE_NOTE_TAG = 'gitlab-org/database-team/gitlab-com-database-testing:identifiable-note'
 
@@ -471,8 +500,8 @@ def status
   case ARGV[0]
   when 'gitlab-com-database-testing'
     Trigger::DatabaseTesting.new.invoke!
-  when 'docs'
-    docs_trigger = Trigger::Docs.new
+  when 'docs-hugo', 'docs'
+    docs_trigger = (ARGV[0] == 'docs-hugo' ? Trigger::DocsHugo : Trigger::Docs).new
 
     case ARGV[1]
     when 'deploy'
@@ -485,7 +514,9 @@ def status
     end
   else
     puts "Please provide a valid option:
-    omnibus - Triggers a pipeline that builds the omnibus-gitlab package
+    docs - Triggers a pipline that builds a documentation review app by using the gitlab-docs project
+    docs-hugo - Triggers a pipline that builds a documentation review app by using the gitlab-docs-hugo project
+    omnibus - Triggers a pipelines that builds the omnibus-gitlab package
     gitlab-com-database-testing - Triggers a pipeline that tests database changes on GitLab.com data"
   end
 end
diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb
index a1bedd19ed37b655d8a1b08cf122a78986369109..6a853fed7bceba3bb731b0f1801ba89588ec7f22 100644
--- a/spec/scripts/trigger-build_spec.rb
+++ b/spec/scripts/trigger-build_spec.rb
@@ -21,7 +21,8 @@
       'GITLAB_USER_NAME' => 'gitlab_user_name',
       'GITLAB_USER_LOGIN' => 'gitlab_user_login',
       'QA_IMAGE' => 'qa_image',
-      'DOCS_PROJECT_API_TOKEN' => nil
+      'DOCS_PROJECT_API_TOKEN' => nil,
+      'DOCS_HUGO_PROJECT_API_TOKEN' => nil
     }
   end
 
@@ -569,7 +570,7 @@ def ref_param_name
         end
 
         it 'returns the docs-specific access token' do
-          expect(described_class.access_token).to eq(docs_project_api_token)
+          expect(subject.access_token).to eq(docs_project_api_token)
         end
       end
 
@@ -579,7 +580,7 @@ def ref_param_name
         end
 
         it 'returns the default access token' do
-          expect(described_class.access_token).to eq(Trigger::Base.access_token)
+          expect(subject.access_token).to eq(Trigger::Base.new.access_token)
         end
       end
     end
@@ -665,6 +666,223 @@ def ref_param_name
     end
   end
 
+  describe Trigger::DocsHugo do
+    let(:downstream_project_path) { 'gitlab-org/technical-writing-group/gitlab-docs-hugo' }
+
+    describe '#variables' do
+      describe "BRANCH_CE" do
+        before do
+          stub_env('CI_PROJECT_PATH', 'gitlab-org/gitlab-foss')
+        end
+
+        context 'when CI_PROJECT_PATH is gitlab-org/gitlab-foss' do
+          it 'sets BRANCH_CE to CI_COMMIT_REF_NAME' do
+            expect(subject.variables['BRANCH_CE']).to eq(env['CI_COMMIT_REF_NAME'])
+          end
+        end
+      end
+
+      describe "BRANCH_EE" do
+        before do
+          stub_env('CI_PROJECT_PATH', 'gitlab-org/gitlab')
+        end
+
+        context 'when CI_PROJECT_PATH is gitlab-org/gitlab' do
+          it 'sets BRANCH_EE to CI_COMMIT_REF_NAME' do
+            expect(subject.variables['BRANCH_EE']).to eq(env['CI_COMMIT_REF_NAME'])
+          end
+        end
+      end
+
+      describe "BRANCH_RUNNER" do
+        before do
+          stub_env('CI_PROJECT_PATH', 'gitlab-org/gitlab-runner')
+        end
+
+        context 'when CI_PROJECT_PATH is gitlab-org/gitlab-runner' do
+          it 'sets BRANCH_RUNNER to CI_COMMIT_REF_NAME' do
+            expect(subject.variables['BRANCH_RUNNER']).to eq(env['CI_COMMIT_REF_NAME'])
+          end
+        end
+      end
+
+      describe "BRANCH_OMNIBUS" do
+        before do
+          stub_env('CI_PROJECT_PATH', 'gitlab-org/omnibus-gitlab')
+        end
+
+        context 'when CI_PROJECT_PATH is gitlab-org/omnibus-gitlab' do
+          it 'sets BRANCH_OMNIBUS to CI_COMMIT_REF_NAME' do
+            expect(subject.variables['BRANCH_OMNIBUS']).to eq(env['CI_COMMIT_REF_NAME'])
+          end
+        end
+      end
+
+      describe "BRANCH_CHARTS" do
+        before do
+          stub_env('CI_PROJECT_PATH', 'gitlab-org/charts/gitlab')
+        end
+
+        context 'when CI_PROJECT_PATH is gitlab-org/charts/gitlab' do
+          it 'sets BRANCH_CHARTS to CI_COMMIT_REF_NAME' do
+            expect(subject.variables['BRANCH_CHARTS']).to eq(env['CI_COMMIT_REF_NAME'])
+          end
+        end
+      end
+
+      describe "BRANCH_OPERATOR" do
+        before do
+          stub_env('CI_PROJECT_PATH', 'gitlab-org/cloud-native/gitlab-operator')
+        end
+
+        context 'when CI_PROJECT_PATH is gitlab-org/cloud-native/gitlab-operator' do
+          it 'sets BRANCH_OPERATOR to CI_COMMIT_REF_NAME' do
+            expect(subject.variables['BRANCH_OPERATOR']).to eq(env['CI_COMMIT_REF_NAME'])
+          end
+        end
+      end
+
+      describe "REVIEW_SLUG" do
+        before do
+          stub_env('CI_PROJECT_PATH', 'gitlab-org/gitlab-foss')
+        end
+
+        context 'when CI_MERGE_REQUEST_IID is set' do
+          it 'sets REVIEW_SLUG' do
+            expect(subject.variables['REVIEW_SLUG']).to eq("ce-#{env['CI_MERGE_REQUEST_IID']}")
+          end
+        end
+
+        context 'when CI_MERGE_REQUEST_IID is not set' do
+          before do
+            stub_env('CI_MERGE_REQUEST_IID', nil)
+          end
+
+          it 'sets REVIEW_SLUG' do
+            expect(subject.variables['REVIEW_SLUG']).to eq("ce-#{env['CI_COMMIT_REF_SLUG']}")
+          end
+        end
+      end
+    end
+
+    describe '.access_token' do
+      context 'when DOCS_HUGO_PROJECT_API_TOKEN is set' do
+        let(:docs_hugo_project_api_token) { 'docs_hugo_project_api_token' }
+
+        before do
+          stub_env('DOCS_HUGO_PROJECT_API_TOKEN', docs_hugo_project_api_token)
+        end
+
+        it 'returns the docs-specific access token' do
+          expect(subject.access_token).to eq(docs_hugo_project_api_token)
+        end
+      end
+
+      context 'when DOCS_HUGO_PROJECT_API_TOKEN is not set' do
+        before do
+          stub_env('DOCS_HUGO_PROJECT_API_TOKEN', nil)
+        end
+
+        it 'returns the default access token' do
+          expect(subject.access_token).to eq(Trigger::Base.new.access_token)
+        end
+      end
+    end
+
+    describe '#invoke!' do
+      let(:trigger_token) { 'docs_hugo_trigger_token' }
+      let(:ref) { 'main' }
+
+      let(:env) do
+        super().merge(
+          'CI_PROJECT_PATH' => 'gitlab-org/gitlab-foss',
+          'DOCS_HUGO_TRIGGER_TOKEN' => trigger_token
+        )
+      end
+
+      describe '#downstream_project_path' do
+        context 'when DOCS_PROJECT_PATH is set' do
+          let(:downstream_project_path) { 'docs_project_path' }
+
+          before do
+            stub_env('DOCS_PROJECT_PATH', downstream_project_path)
+          end
+
+          it 'triggers the pipeline on the correct project' do
+            expect_run_trigger_with_params
+
+            subject.invoke!
+          end
+        end
+      end
+
+      describe '#ref' do
+        context 'when DOCS_BRANCH is set' do
+          let(:ref) { 'docs_branch' }
+
+          before do
+            stub_env('DOCS_BRANCH', ref)
+          end
+
+          it 'triggers the pipeline on the correct ref' do
+            expect_run_trigger_with_params
+
+            subject.invoke!
+          end
+        end
+      end
+    end
+
+    describe '#cleanup!' do
+      let(:downstream_environment_response) { double('downstream_environment', id: 42) }
+      let(:downstream_environments_response) { [downstream_environment_response] }
+
+      before do
+        expect(com_gitlab_client).to receive(:environments)
+          .with(downstream_project_path, name: subject.__send__(:downstream_environment))
+          .and_return(downstream_environments_response)
+        expect(com_gitlab_client).to receive(:stop_environment)
+          .with(downstream_project_path, downstream_environment_response.id)
+          .and_return(downstream_environment_stopping_response)
+      end
+
+      context "when stopping the environment succeeds" do
+        let(:downstream_environment_stopping_response) { double('downstream_environment', state: 'stopped') }
+
+        it 'displays a success message' do
+          expect(subject).to receive(:puts)
+            .with("=> Downstream environment '#{subject.__send__(:downstream_environment)}' stopped.")
+
+          subject.cleanup!
+        end
+      end
+
+      context "when stopping the environment fails" do
+        let(:downstream_environment_stopping_response) { double('downstream_environment', state: 'running') }
+
+        it 'displays a failure message' do
+          expect(subject).to receive(:puts)
+            .with("=> Downstream environment '#{subject.__send__(:downstream_environment)}' failed to stop.")
+
+          subject.cleanup!
+        end
+      end
+    end
+
+    describe '#app_url' do
+      let(:review_slug) { 'ce-123' }
+
+      before do
+        allow(subject).to receive(:review_slug).and_return(review_slug)
+      end
+
+      it 'returns the correct app URL' do
+        expected_url = "https://new.docs.gitlab.com/upstream-review-mr-#{review_slug}/"
+        expect(subject.send(:app_url)).to eq(expected_url)
+      end
+    end
+  end
+
   describe Trigger::DatabaseTesting do
     describe '#variables' do
       it 'invokes the trigger with expected variables' do