GitLab Pages Blog SEO Checklist: CI, Domains and Redirects
A practical GitLab Pages SEO checklist for blogs: the Pages job in .gitlab-ci.yml, unique domains and base URLs, custom domain DNS and HTTPS, the primary domain redirect, robots.txt and sitemap placement, _redirects rules, 404 pages, access control and a post-deploy curl check.
Blog on GitLab? See how Blogger Dog publishes into GitLab.
Why a GitLab Pages blog can deploy green and still go unseen
GitLab Pages is a good home for a blog. The site is plain static files, the build runs in the same pipeline as your code, HTTPS certificates are issued for you, and redirects are a text file in the repo. Nothing about the hosting holds a blog back in search.
What holds it back is the setup around the hosting. A project site can be reachable at three different addresses at once. A static site generator left with the wrong base path ships broken links and a sitemap for the wrong host. A robots.txt in a project subpath is never read. A renamed post returns a 404 because the redirect file never made it into the published folder. And a site whose access is limited to project members shows crawlers a login page.
This checklist covers the hosting layer: what to set in .gitlab-ci.yml, in Deploy > Pages, and in DNS. It is checked against the GitLab documentation as of November 2026 and uses GitLab.com values. The generator-level work (titles, canonical tags, structured data) lives in the stack checklists: Next.js, Astro, Hugo, Jekyll, Gatsby and Docusaurus.
1. Deploy from one Pages job on the default branch
A Pages deployment is a CI job marked with the pages keyword. Older guides tell you the job must be named pages; since GitLab 17.6 the name is yours to choose, and the Pages tutorial uses create-pages with pages: true.
By default Pages publishes a folder named public. If your generator writes somewhere else, point pages.publish at it instead of copying files around:
# .gitlab-ci.yml
create-pages:
stage: deploy
image: node:22
script:
- npm ci
- npm run build
pages:
publish: dist
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Three details matter here:
- The
rulesline. Without it, every branch push replaces the live site. Limiting the job to the default branch means only merged work is published. - Artifacts. The published folder is added to
artifacts:pathsfor you (since GitLab 17.10 for the defaultpublicfolder, and always forpages.publish), so the job needs noartifactsblock. - A real home page. The Pages settings reference says the root of the published folder must contain a non-empty
index.html, or the deploy job fails.
2. Know which address your site is served from
Before you set anything else, open Deploy > Pages and read the URL. On GitLab.com there are three possibilities, described in the default domain names reference:
| Setup | Address | Served from |
|---|---|---|
| Unique domain (default for new projects) | https://my-blog-123456.gitlab.io/ | Root of its own host |
| Project site, unique domain off | https://namespace.gitlab.io/my-blog | A subpath of a shared host |
| Custom domain | https://example.com/ | Root of your host |
New projects use a unique domain: the project name plus a six-character ID. When it is on, the namespace.gitlab.io/my-blog form answers with a 308 Permanent Redirect to the unique host.
For a blog that should rank, treat both gitlab.io forms as staging addresses. Links you earn point at a host you do not control, and moving later means redirecting every URL. Start on a custom domain (steps 4 to 6) and the rest of this list gets simpler.
3. Match the generator's base URL to that address
GitLab's docs put it plainly: generators expect to live at the root of a domain, not in a subdirectory. If your site is served from namespace.gitlab.io/my-blog, the generator has to be told about /my-blog, or stylesheets, internal links and the sitemap all point at the wrong place. The GitLab example is Jekyll:
# _config.yml for https://namespace.gitlab.io/my-blog
url: "https://namespace.gitlab.io"
baseurl: "/my-blog"
On a custom domain or a unique domain the site sits at the root, so the base path is empty and the URL is your real origin:
# _config.yml for https://example.com
url: "https://example.com"
baseurl: ""
The setting has a different name in each tool: baseURL in Hugo, site and base in Astro, url and baseUrl in Docusaurus, pathPrefix in Gatsby, basePath in Next.js. Whatever it is called, it feeds your canonical tags, Open Graph URLs and sitemap, so it must be the production origin. A blog that moved to a custom domain and still builds with a gitlab.io URL tells search engines the old address is the real one.
4. Add the custom domain and keep the verification record
In Deploy > Pages, select Add domain, enter the domain, and GitLab shows a verification code. The custom domains guide lists the DNS records for GitLab.com:
| Name | Type | Value |
|---|---|---|
example.com | A | 35.185.44.232 |
example.com | AAAA | 2600:1901:0:7b8a:: |
blog.example.com | CNAME or ALIAS | namespace.gitlab.io |
_gitlab-pages-verification-code.example.com | TXT | gitlab-pages-verification-code=<your code> |
Use the A or AAAA records for a root domain and the CNAME for a subdomain. Do not point a root domain at GitLab with an apex CNAME; the guide advises against it. Some DNS providers append the domain to the record name themselves, in which case you enter only _gitlab-pages-verification-code.
Two things catch people out. Verification can take up to 24 hours, and until it passes the domain returns a 404, so do not announce the URL the same minute. And the TXT record is not a one-time step: GitLab removes unverified domains after 7 days and re-verifies domains periodically, so deleting the record later can take your blog offline.
5. Turn on Force HTTPS
GitLab's Let's Encrypt integration obtains and renews the certificate for a custom domain automatically; leave the toggle on when you add the domain. Then, in Deploy > Pages, tick Force HTTPS (requires valid certificates) and save. From then on every http:// request gets a 301 to https://, which keeps you from having an HTTP copy and an HTTPS copy of each post in the index.
If the domain sits behind Cloudflare's proxy, the guide says to set the SSL mode to full rather than flexible.
6. Choose a primary domain so one host wins
After step 4 your blog answers on at least two hosts: the custom domain and the gitlab.io address. If you added both example.com and www.example.com, it answers on three. Each is a complete copy of the site.
Since GitLab 17.8 there is a setting for this. In Deploy > Pages, pick your custom domain in the Primary domain dropdown and save. The Pages documentation describes the result: all requests to the Pages site are redirected to the primary domain with a 308 Permanent Redirect. You need the Maintainer or Owner role, and the domain has to be added first.
This is worth doing even if your pages already carry canonical tags. Google's canonicalization guide ranks redirects first among its signals, ahead of rel="canonical" and far ahead of sitemap inclusion. Keep the canonical tags too, and make them absolute URLs on the primary host, which is what the same guide recommends.
7. Serve robots.txt and the sitemap from the host root
Google's robots.txt rules are strict about location: the file must be in the top-level directory of the host, and crawlers do not look for it in subdirectories. It also applies only to the exact protocol and host it is served from.
That has a direct consequence on GitLab Pages. A project site at namespace.gitlab.io/my-blog publishes its file at /my-blog/robots.txt, which no crawler reads. On a custom domain or a unique domain, the same file lands at the root and works. One more reason to finish steps 4 to 6 first.
Put robots.txt where your generator copies static files from (static/ in Hugo, public/ in Astro and Next.js, the project root in Jekyll) and reference the sitemap by its full URL:
User-agent: *
Allow: /
Sitemap: https://example.com/sitemap.xml
Then open the sitemap and read the first few <loc> values. They should start with the primary domain from step 6, not with a gitlab.io host. If they do not, go back to step 3.
8. Redirect renamed posts with _redirects
GitLab Pages reads a _redirects file from the root of the published folder. Each line is a source path, a target and an optional status; the default is 301.
# public/_redirects
/blog/old-post-title/ /blog/new-post-title/ 301
/2024/:month/:day/:slug /blog/:slug/ 301
/news/* /blog/:splat 301
The rules that trip people up:
- The file has to be in the published folder. Hugo copies it from
static/, Astro frompublic/. Jekyll skips files that start with an underscore, so addinclude: ["_redirects"]to_config.yml. - Files win. If a file still exists at the source path, Pages serves the file and ignores the rule. Delete the old page when you add the redirect.
- Paths only. Every rule starts with
/. Rules that match query parameters are not supported. - Project subpaths need the prefix. On
namespace.gitlab.io/my-blog, both sides of each rule start with/my-blog/. On a custom domain they do not. - Limits. By default the file may be 64 KB and 1,000 rules.
To check your work, open https://example.com/_redirects in a browser. Pages prints each rule and whether it is valid.
9. Ship a 404 page and link with the URL shape Pages serves
Put a 404.html in the root of the published folder. On a custom domain, Pages serves only /404.html for missing paths, so one file covers the whole blog. Give it a link to the blog index so a visitor from a dead link has somewhere to go.
URL shape matters too. Most generators write a post as blog/my-post/index.html. The settings reference documents how Pages answers: /blog/my-post/ serves the file, while /blog/my-post returns a 302 to the version with the slash. It works, but every internal link without the slash costs readers and crawlers an extra request, and a temporary redirect is a weaker hint than a direct link. Use the trailing-slash form in your internal links, canonical tags and sitemap so all three agree.
10. Keep the site public, then check the live URL
Pages has its own access control, separate from who can see the repository. Under Settings > General > Visibility, project features, permissions, the Pages dropdown must allow everyone. The access control page explains the options; with Only Project Members selected, crawlers get a sign-in redirect instead of your posts. A blog can also live in a private project and still be public, which is useful if drafts sit in the same repo.
After each deploy, check one real post instead of trusting the pipeline badge:
URL="https://example.com/blog/my-post/"
curl -s -o /dev/null -w "%{http_code}\n" "$URL"
curl -s "$URL" | grep -oE '<link rel="?canonical"? href="?[^" >]+' | head -1
curl -sI "http://example.com/blog/my-post/" | grep -iE '^(HTTP|location)'
curl -sI "https://my-blog-123456.gitlab.io/blog/my-post/" | grep -iE '^(HTTP|location)'
curl -s https://example.com/sitemap.xml | grep -c "$URL"
You want a 200, a canonical equal to the URL, the HTTP request and the gitlab.io request both redirecting to the primary domain, and the post present in the sitemap. A green pipeline only proves the files were uploaded. The same reasoning is behind live URL verification for GitHub blog publishing: a post counts when its public address serves it.
The checklist in one place
| Area | Where | What good looks like |
|---|---|---|
| Deploy job | .gitlab-ci.yml | One job with pages, limited to the default branch |
| Published folder | pages.publish or public/ | Contains a non-empty index.html |
| Address | Deploy > Pages | A custom domain, not a gitlab.io URL |
| Base URL | generator config | Production origin, empty base path on a custom domain |
| DNS | your DNS provider | A/AAAA or CNAME, plus the TXT record kept in place |
| HTTPS | Deploy > Pages | Let's Encrypt on, Force HTTPS ticked |
| One host | Primary domain | Every other host answers with a 308 |
| Robots and sitemap | published folder root | Served at the host root, sitemap lists primary-domain URLs |
| Renamed posts | _redirects | 301 rules, old files deleted, checked at /_redirects |
| Missing pages | 404.html | In the root, links back to the blog |
| Access | project settings | Pages visible to everyone |
| Verification | curl after deploy | 200, matching canonical, redirects in place |
FAQ
Is GitLab Pages good for SEO?
Yes. It serves static files over HTTPS with automatic certificates, and it supports redirects and custom 404 pages. The risks are in configuration: several hosts serving the same site, a wrong base URL, and files such as robots.txt or _redirects ending up in the wrong place.
Does the gitlab.io address redirect to my custom domain?
Only if you set it up. Adding a custom domain leaves the gitlab.io address serving the same pages. Choose your domain as the Primary domain in Deploy > Pages (GitLab 17.8 and later) and every other address answers with a 308 redirect to it.
Where does robots.txt go on GitLab Pages?
In the root of the published folder, so it is served at /robots.txt on your host. On a project site under namespace.gitlab.io/project, the file is served from a subpath and crawlers ignore it, which is one reason to use a custom domain.
Does the Pages job have to be called pages?
No. Since GitLab 17.6 any job with the pages keyword (pages: true, or a pages: block with options such as publish) is a Pages job. Older pipelines with a job named pages keep working.
Can I redirect www to the root domain?
Add both names as domains with their DNS and TXT records, then pick the one you want as the primary domain. GitLab's custom domains guide also describes doing it at the DNS provider with a Cloudflare forwarding rule, in which case only the root domain is added to GitLab.
Keep the posts coming
A correctly configured GitLab Pages blog still needs new posts to earn traffic. Blogger Dog researches and writes cited SEO and AEO posts and commits each one to your GitLab project as a single commit on the branch you deploy from, so your pipeline runs exactly as it does for your own pushes. It then checks the public URL and only counts the post once the page is live. The GitLab integration page covers how the connection works; it supports Next.js, Astro, Hugo, Jekyll, Gatsby and Docusaurus sites on gitlab.com.
You get 3 posts free, then plans start at $19/month. See pricing and plans.