你是不是也有过这种经历:在本地打开网站,排版漂亮得不行,一部署到 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。
六、注意事项清单
写到这里,我把自己踩过的坑和容易忽视的细节整理成一份清单:
baseURL末尾一定要带斜杠。缺少斜杠时,Hugo 拼接出来的资源链接会变成https://www.zhoulang.comcss/style.css,惨不忍睹。- 不要在 HTML 或 Markdown 里硬编码
https://用户名.gitlab.io/项目名/这样的完整路径。必须用 Hugo 的absURL或resources.Get。 - 更新 CSS 后,不要只相信眼睛。问问自己:这个页面上的 CSS 文件名变了吗?如果没变,很有可能是 CDN 或浏览器缓存。
- 使用
_headers文件时,要确认它最终出现在public目录的根下。在 Hugo 里,请放到static/_headers,而不是项目根目录。 - 绑定自定义域名后,如果页面样式还是乱,先看看浏览器地址栏里的协议是不是
https,再检查是不是被 CDN 缓存了旧的 HTML。可以考虑在浏览器无痕模式下打开。 - 如果你用了 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}'
- 每次切换域名或修改
baseURL后,记得把.gitlab-ci.yml里的构建缓存清掉,再重新部署,防止 GitLab 用了旧的构建产物。
七、总结
折腾到最后,你会发现静态站点的样式丢失,本质上就两件事:路径和缓存。路径错了,CSS 文件根本找不到;缓存没处理好,CSS 文件找到了,却是旧的。而自定义域名只是让这两个问题变得更显眼而已。
用 Hugo 部署 GitLab Pages 的正确姿势:设置好 baseURL,模板里使用 absURL 或 resources.Get 生成资源链接;给 CSS/JS 加上指纹哈希;通过 static/_headers 配置合理的缓存策略;绑定自定义域名后,及时更新 baseURL 并清一次 CDN 缓存。
你把这几步走顺了,以后不管怎么切换域名、怎么加 CDN,页面都会穿着整齐的衣服,体体面面地出现在访客面前。这不比天天给“裸奔”的页面遮遮掩掩舒服多了?
评论
围绕“用GitLab Pages部署静态站点时样式丢失?破解自定义域名与CDN缓存难题”参与讨论