你是不是也有过这种经历:在本地打开网站,排版漂亮得不行,一部署到 GitLab Pages 上,整个页面就像被人扒了衣服,光秃秃地躺着。文字还在,图片还在,就是 CSS 不见了。按下 F12 一看,满屏的红色报错,全是 404。其实这个问题不难破,今天咱们就用一个大白话版的教程,把这层窗户纸捅破。

一、先看一个让人抓狂的场景

1.1 症状描述

我朋友老周,用 GitLab Pages 搭了个个人博客。在本地用 Hugo 预览,样式、字体、间距都完美。他高高兴兴地把代码推上去,然后打开浏览器访问 https://zhoutest.gitlab.io/my-blog/,结果页面变成了一堆纯文字,排版全没影儿了。他截图给我看,我一眼就发现了问题:浏览器地址栏里访问的是 /my-blog/ 这个路径,可控制台里请求的 CSS 地址却是 /css/style.css,少了一层 /my-blog/

有的朋友还会说:“我换成了自定义域名 www.zhoulang.com,结果更惨,连图片都加载不出来了。” 这其实就是静态站点部署时最常见的两个坑:路径不对,缓存捣乱。

1.2 为什么本地好好的,一到线上就“裸奔”

在本地开发时,你的项目文件夹就是网站的“根”。你写一句 <link href="/css/style.css">,浏览器自然会去本地项目根目录下找 css/style.css,所以一切正常。

但 GitLab Pages 的项目站点不是这样的。假如你的用户名是 zhoutest,项目名是 my-blog,那么线上的真实地址是 https://zhoutest.gitlab.io/my-blog/。注意,/my-blog/ 并不是一个子目录,它就是你这个项目网站的“根”。如果你在 HTML 里写了以 / 开头的绝对路径,比如 /css/style.css,浏览器会傻乎乎地去请求 https://zhoutest.gitlab.io/css/style.css,这当然找不到。

你可以把 GitLab Pages 理解成一栋大楼,你的用户名是大楼,项目名是其中一个房间。绝对路径相当于你直接喊“去门卫室找快递”,但没说去哪个房间,快递员当然跑错地方。

二、破解第一个难题:路径不对

2.1 GitLab Pages 的地址规则

在动手改代码之前,你得先搞清楚自己的项目属于哪一种:

  • 用户网站:https://用户名.gitlab.io/
  • 项目网站:https://用户名.gitlab.io/项目名/
  • 自定义域名:https://你的域名/

不同的地址,对应的“根路径”是不一样的。尤其是项目网站,所有 CSS、JS、图片资源,都得放在项目名这个“根”下面。这就要求我们在写资源路径的时候,不能写死绝对路径,而是要动态地算出一个“当前正确的根地址”。

2.2 用 Hugo 正确设置 baseURL

如果你用的是 Hugo,那核心解决方案很简单:在配置文件里把 baseURL 设置对。baseURL 就是所有相对地址的“地基”。

比如你部署在项目网站下,就要写:

# Hugo 的配置文件 config.toml
# 注意末尾一定要带斜杠,这很关键
baseURL = "https://zhoutest.gitlab.io/my-blog/"
languageCode = "zh-cn"
title = "老周的博客"

等哪天你绑定了自定义域名 https://www.zhoulang.com,只需要把 baseURL 改成:

baseURL = "https://www.zhoulang.com/"

Hugo 会在构建时,把站点里所有的资源链接根据这个 baseURL 拼好。这样你就不用来回改代码里的路径了。

2.3 代码示例:Hugo 项目结构与配置

下面是一个标准的 Hugo 项目结构,我会在注释里标清楚每个地方的作用。

# 这是 Hugo 项目的目录结构
# 注意 assets 和 static 是两个容易混淆的目录
hugo-site/
├── config.toml              # Hugo 配置文件,设置 baseURL 的地方
├── assets/                  # 存放原始资源,比如 SCSS、JS,可被 Hugo 处理
│   └── css/
│       └── style.css        # 我们网站的样式表
├── content/                 # 写 Markdown 文章的地方
│   └── _index.md
├── layouts/                 # 模板文件,控制页面怎么渲染
│   ├── _default/
│   │   └── baseof.html      # 所有页面的基础模板
│   └── index.html           # 首页模板
├── static/                  # 静态文件,会被原封不动复制到 public 目录
│   └── _headers             # 用于配置 HTTP 响应头(后面会用到)
└── .gitlab-ci.yml           # GitLab CI 的构建部署配置

你的 config.toml 至少要有下面这几行:

# 站点根地址,部署到项目页面时必须带 /项目名/
baseURL = "https://zhoutest.gitlab.io/my-blog/"

# 站点标题,会在浏览器标签栏显示
title = "老周的技术小窝"

# 默认语言
languageCode = "zh-cn"

2.4 在模板中引用 CSS

在 Hugo 模板里,最安全的做法是使用 absURL 方法,它会自动基于 baseURL 生成完整的、带域名的地址。我们不要用简单的相对路径,因为相对路径是跟着当前页面 URL 变化的,如果某个文章页面的 URL 里多了一层,CSS 又找不到了。

来看 layouts/_default/baseof.html 这个文件,它是整个站点的骨架:

<!-- 这是 Hugo 的 baseof.html 模板 -->
<!-- 它会把内容插入到 main 这个占位符中 -->

<!DOCTYPE html>
<html lang="zh-cn">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ .Title }} - 老周的技术小窝</title>

    <!-- 用 absURL 把 css/style.css 拼成完整的地址 -->
    <!-- 无论 baseURL 是项目地址还是自定义域名,这里都会自动匹配 -->
    <link rel="stylesheet" href="{{ "css/style.css" | absURL }}">
</head>
<body>
    <header>
        <nav>
            <a href="{{ "/" | absURL }}">首页</a>
            <a href="{{ "/about/" | absURL }}">关于</a>
        </nav>
    </header>

    <!-- 页面主要内容区域 -->
    <main>
        {{ block "main" . }}{{ end }}
    </main>

    <footer>
        <p>© 2025 老周</p>
    </footer>
</body>
</html>

然后 layouts/index.html 可以这样写:

<!-- 这是首页特有的内容 -->
<!-- 它会嵌入到 baseof.html 的 main 区块中 -->
{{ define "main" }}
    <h1>欢迎来到老周的技术小窝</h1>
    <p>这里会写一些关于 GitLab Pages、Hugo 和折腾的记录。</p>
{{ end }}

这样改完之后,推送到 GitLab,Hugo 生成的 HTML 里,CSS 链接就会变成 https://zhoutest.gitlab.io/my-blog/css/style.css,样式自然就回来了。

三、破解第二个难题:CDN 缓存

3.1 CDN 缓存是怎么“捣乱”的

路径问题解决后,你可能又会遇到另一个“玄学”:明明已经更新了 CSS,为什么用户看到的还是旧样式?特别是你给网站套了一层 CDN(比如 Cloudflare、腾讯云 CDN)之后,样式经常在更新后莫名其妙地“丢”了。

CDN 就像小区门口的代收点。你的网站文件放在源站,CDN 节点会把一份文件放在自己那里。用户来取文件时,CDN 直接把自己存的那份给用户,速度快。问题在于:如果源站的 CSS 文件内容变了,但文件名还是 style.css,CDN 一看名字没变,就以为还是原来的文件,直接把旧的缓存丢给用户。结果你明明改了颜色,用户看到的还是一个月前的样式,甚至因为新旧样式混用,整个页面看起来就像没穿好衣服。

3.2 用带指纹的文件名绕过缓存

最经典的破解方法是给文件名加上“指纹”哈希。也就是说,只要文件内容变了,文件名就会跟着变。比如 style.css 变成 style.3d42a9.css。对 CDN 来说,这是一个新文件,自然就不会拿旧缓存来糊弄你。

在 Hugo 里,这个操作非常简单。原来的 style.css 放在 assets/css/ 目录下,模板里改成这样:

<!-- 使用 Hugo 的 resources.Get 获取资源 -->
<!-- 用 minify 压缩体积,再用 fingerprint 生成带哈希的新文件名 -->
{{ $style := resources.Get "css/style.css" | minify | fingerprint }}

<!-- 输出:/css/style.3d42a9.css -->
<link rel="stylesheet" href="{{ $style.RelPermalink }}">

然后你的站点里就不需要手动引用 css/style.css 了,Hugo 会自动生成一个带哈希的文件名,并且自动把它复制到 public/css/ 目录下。以后你更新 CSS 内容,哈希值就会变,文件名就变,CDN 和浏览器都没有理由继续用旧缓存。

3.3 用 _headers 设置缓存策略

光有文件名指纹还不够,我们还得让 CDN 知道:HTML 文件不要缓存,因为 HTML 里放着最新的 CSS 链接;而带指纹的 CSS、JS 资源可以放心大胆地缓存一年,因为文件名变一次,CDN 就会当新文件处理。

GitLab Pages 支持通过 public/_headers 文件来设置自定义 HTTP 响应头。如果你是 Hugo 站点,需要把这个文件放在项目的 static/ 目录下,因为 static/ 里的文件会被原样复制到 public/ 里面去。

来看 static/_headers 的内容:

# 所有 html 文件不缓存,保证每次访问都能拿到最新的页面结构
/*.html
  Cache-Control: no-cache

# 带指纹的 css 文件缓存一年
# 因为文件名带了哈希,所以不用担心更新不到
/css/*.css
  Cache-Control: public, max-age=31536000, immutable

# 带指纹的 js 文件也缓存一年
/js/*.js
  Cache-Control: public, max-age=31536000, immutable

写完这个文件,重新部署后,你用浏览器开发者工具看响应头,会发现 CSS 文件的 Cache-Control 变成了 max-age=31536000,而 HTML 文件则是 no-cache,这就非常理想了。

四、破解第三个难题:自定义域名的那些坑

4.1 域名解析与 Pages 绑定

自己买了个域名,比如 www.zhoulang.com,想绑到 GitLab Pages 上。操作不复杂:在 GitLab 项目里的 Settings → Pages 页面,输入域名,然后按照提示去域名服务商那里加一条 CNAME 记录,指向 zhoutest.gitlab.io。之后 GitLab 会帮你申请免费 HTTPS 证书,等个十几分钟就好了。

4.2 从默认域名换到自定义域名后,哪些资源会失效

这里最大的坑是:你的站点里如果有一些用“旧默认域名”拼出来的绝对地址,比如在文章里硬编码了 https://zhoutest.gitlab.io/my-blog/css/style.css,那换到自定义域名后,这些地址还是会去请求旧域名。

所以,不管你有没有自定义域名,我都强烈建议:不要在模板、文章里写死默认域名的 URL。老老实实让 Hugo 的 baseURL 来管理。绑定自定义域名后,把 config.toml 里的 baseURL 改成:

# 自定义域名上线后,baseURL 换成你自己的域名
baseURL = "https://www.zhoulang.com/"

然后重新部署,所有资源链接都会重新基于新域名生成,不会有多余的跳转。

4.3 实操:在 GitLab 上配置自定义域名

假设你已经买好域名,并且把 DNS 解析做好了。我们来走一遍流程。

第一步,打开 GitLab 项目页面,点击左侧菜单里的 Settings,然后选择 Pages

第二步,在 New Domain 下面,输入你的域名,比如 www.zhoulang.com,点 Create New Domain

第三步,GitLab 会给出一个 CNAME 记录值,你到你域名服务商的后台添加一条解析,主机记录填 www,记录类型选 CNAME,记录值填 zhoutest.gitlab.io,TTL 随意。

第四步,用命令行验证一下解析是否生效:

# 使用 dig 命令检查 CNAME 解析
# 如果没有 dig,可以直接访问 https://dns.google 来查询
dig +short www.zhoulang.com CNAME

如果输出类似 zhoutest.gitlab.io.,说明解析已经生效。等待 GitLab 颁发证书,之后就可以用 https://www.zhoulang.com 访问你的站点了。

五、应用场景与优缺点分析

5.1 什么时候适合用 GitLab Pages

GitLab Pages 最适合静态网站。比如个人博客、项目文档首页、产品介绍页、开源项目的落地页。特别是你的代码本身已经托管在 GitLab 上,那直接用 Pages 几乎是零成本。它不需要你买服务器,也不用装什么宝塔面板,代码一推,自动构建,自动上线。

但如果你需要会员注册、用户评论、数据库交互,那就不适合用 GitLab Pages 了。它是纯静态托管,没有后端运行环境。评论功能可以借助第三方服务,但核心业务逻辑还是得找别的去处。

5.2 用 GitLab Pages 的优势

  • 免费,而且内置 HTTPS 证书,不用自己花钱买。
  • 和 GitLab CI/CD 无缝结合,git push 之后自动执行构建和部署。
  • 支持自定义域名,而且可以绑定多个。
  • 没有复杂的服务器运维,不用担心被攻击、被挂马。

5.3 不能忽略的缺点

  • 只能托管静态文件,不能用 PHP、Python、Node.js 写接口。
  • 如果项目不是公开的,Pages 功能可能受限,很多用法要求项目可见性为公开。
  • 构建时间和产物大小有上限,超大站点可能构建失败。
  • 缓存问题需要自己控制,如果你不配置 _headers,CDN 或浏览器可能会一直保留旧 CSS。

六、注意事项清单

写到这里,我把自己踩过的坑和容易忽视的细节整理成一份清单:

  1. baseURL 末尾一定要带斜杠。缺少斜杠时,Hugo 拼接出来的资源链接会变成 https://www.zhoulang.comcss/style.css,惨不忍睹。
  2. 不要在 HTML 或 Markdown 里硬编码 https://用户名.gitlab.io/项目名/ 这样的完整路径。必须用 Hugo 的 absURLresources.Get
  3. 更新 CSS 后,不要只相信眼睛。问问自己:这个页面上的 CSS 文件名变了吗?如果没变,很有可能是 CDN 或浏览器缓存。
  4. 使用 _headers 文件时,要确认它最终出现在 public 目录的根下。在 Hugo 里,请放到 static/_headers,而不是项目根目录。
  5. 绑定自定义域名后,如果页面样式还是乱,先看看浏览器地址栏里的协议是不是 https,再检查是不是被 CDN 缓存了旧的 HTML。可以考虑在浏览器无痕模式下打开。
  6. 如果你用了 Cloudflare 这类 CDN,在调试阶段可以手动清除一次全部缓存,避免旧资源干扰判断。
# 以 Cloudflare 为例,用 API 清除全部缓存
# 请将 ZONE_ID 和 API_TOKEN 替换成真实值
curl -X POST "https://api.cloudflare.com/client/v4/zones/ZONE_ID/purge_cache" \
     -H "Authorization: Bearer API_TOKEN" \
     -H "Content-Type: application/json" \
     --data '{"purge_everything":true}'
  1. 每次切换域名或修改 baseURL 后,记得把 .gitlab-ci.yml 里的构建缓存清掉,再重新部署,防止 GitLab 用了旧的构建产物。

七、总结

折腾到最后,你会发现静态站点的样式丢失,本质上就两件事:路径和缓存。路径错了,CSS 文件根本找不到;缓存没处理好,CSS 文件找到了,却是旧的。而自定义域名只是让这两个问题变得更显眼而已。

用 Hugo 部署 GitLab Pages 的正确姿势:设置好 baseURL,模板里使用 absURLresources.Get 生成资源链接;给 CSS/JS 加上指纹哈希;通过 static/_headers 配置合理的缓存策略;绑定自定义域名后,及时更新 baseURL 并清一次 CDN 缓存。

你把这几步走顺了,以后不管怎么切换域名、怎么加 CDN,页面都会穿着整齐的衣服,体体面面地出现在访客面前。这不比天天给“裸奔”的页面遮遮掩掩舒服多了?