静态博客前端:布局与排版
· 前端技术 · 约 8 分钟阅读
更新于
用信息栏、正文与目录三列组织长文:宽屏分栏、窄屏单列,代码和表格只在局部滚动。
长文排版最怕三件事:正文行宽太宽,读到后面容易串行;代码和表格把整页撑出横向滚动;目录在手机上占掉半屏,正文还没开始就要先划过去。
这篇用普通 HTML、CSS 和少量 JavaScript 搭一套文章页骨架,解决的就是这三个问题。它不依赖框架,也不包含滚动高亮、搜索和 PJAX;接到具体主题时,按真实生成的 DOM 调整选择器就行。
同一份 DOM 在三种宽度下的列结构
先搭一个能读的骨架
目录在文档里放在正文之前,章节 ID 唯一且稳定。这样即使脚本没加载,静态目录仍然能跳转。折叠按钮先隐藏,避免脚本失败后留下一个点了没反应的控件。
右侧的目录栏先留空,等脚本确认正文里确实有章节,再把目录移进去。
<div class="article-layout"> <aside class="article-rail" aria-label="文章信息"> <dl> <div><dt>发布</dt><dd><time datetime="2026-09-04">2026年9月4日</time></dd></div> <div><dt>分类</dt><dd><a href="/categories/frontend/">前端技术</a></dd></div> <div><dt>阅读</dt><dd>约 7 分钟</dd></div> </dl> </aside>
<article class="article-main"> <header class="article-header"> <h1>一份最小文章页</h1> <p class="article-description">正文先成立,目录和辅助信息随后增强。</p> </header>
<nav class="article-toc" data-article-toc aria-label="文章目录"> <button type="button" data-toc-toggle aria-expanded="true" aria-controls="toc-list">本页目录</button> <ol id="toc-list"> <li><a href="#preparation">准备内容</a></li> <li><a href="#example">阅读示例</a></li> <li><a href="#data">数据表</a></li> </ol> </nav>
<div class="article-content"> <h2 id="preparation">准备内容</h2> <p>正文正常换行,长地址也不能撑开整个页面。</p> <h2 id="example">阅读示例</h2> <pre tabindex="0" aria-label="示例代码"><code>const message = '保留空格与换行规则';</code></pre> <h3 id="data">数据表</h3> <div class="table-scroll" tabindex="0" role="region" aria-label="示例数据表"> <table> <caption>示例参数</caption> <thead><tr><th scope="col">名称</th><th scope="col">说明</th></tr></thead> <tbody><tr><td>阅读列</td><td>超宽内容在自身容器内滚动</td></tr></tbody> </table> </div> </div> </article>
<aside class="article-aside" aria-label="文章目录"></aside> </div>
|
给正文一个可收缩的宽度
先限制整页容器,再限制正文行宽。1fr 的自动最小尺寸可能被长代码撑住,正文列要用 minmax(0, 1fr),Grid 子项也要允许缩小。
* { box-sizing: border-box; } body { margin: 0; }
.article-shell { width: min(1180px, calc(100% - 2rem)); margin-inline: auto; padding-block: 2.5rem; } .article-layout { display: grid; grid-template-columns: minmax(0, 760px); justify-content: center; gap: 2.5rem; } .article-main { min-width: 0; padding: 3rem 3.25rem; border-radius: 1.25rem; background: #f9f8f3; } .article-rail, .article-aside { display: none; } .article-content { max-width: 47.5rem; font: 1.0625rem/1.88 system-ui, sans-serif; } .article-content :is(h2, h3) { scroll-margin-top: 5.5rem; } .article-content img { max-width: 100%; height: auto; }
@media (min-width: 1024px) { .article-layout.has-toc { grid-template-columns: minmax(0, 1fr) 13.75rem; gap: 2rem; } } @media (min-width: 1200px) { .article-layout { grid-template-columns: 9.5rem minmax(0, 760px); gap: 1.875rem; } .article-layout.has-toc { grid-template-columns: 9.5rem minmax(0, 1fr) 12.25rem; justify-content: start; } .article-rail { display: block; position: sticky; top: 5.5rem; align-self: start; } .article-aside { display: block; } }
|
三列不是固定像素的总和:中间列用 minmax(0, 1fr),在 1200px 视口里仍然能收缩。信息栏只在最宽布局出现,日期和分类不会同时出现在标题下方和左侧栏。
中文正文可以从 16–18px、1.75–1.9 行高开始,再按字体和行宽微调。不要照搬某个“标准答案”;字体不同,合适的行宽也会变。
让代码和表格自己滚动
正文里的长地址要允许换行,代码要保留原始空格,表格则交给包装层滚动。三条规则分开写,互不干扰:
.article-content { overflow-wrap: anywhere; } .article-content pre, .table-scroll { max-width: 100%; overflow-x: auto; margin-block: 1.5rem; } .article-content pre { padding: 2.1rem 1.25rem 1.1rem; white-space: pre; overflow-wrap: normal; border-radius: .625rem; background: #eef1e8; font: .85rem/1.7 ui-monospace, monospace; } .table-scroll { border-block: 1px solid #d7ddd3; } .table-scroll table { width: max-content; min-width: 100%; border-collapse: collapse; } .table-scroll :is(th, td) { padding: .65rem .8rem; border-bottom: 1px solid #d7ddd3; text-align: start; }
|
这里有个容易踩的坑:Hexo 的代码高亮器会把代码包进一个 table。如果你给所有 table 统一写了 display: block; overflow-x: auto,再在 .highlight table 里改回 display: table; overflow: visible,窄屏时代码就会把整页撑宽。
正确做法是让高亮表格自己成为滚动容器:
.highlight { position: relative; max-width: 100%; min-width: 0; } .highlight table { display: block; max-width: 100%; overflow-x: auto; } .highlight table tbody { display: table; width: max-content; min-width: 100%; } .highlight pre { overflow: visible; max-width: none; }
|
代码块上方的空白用来放语言标签,不需要再用一条外框把代码包成第二张卡片。复制按钮可以绝对定位在代码块右上角,但不要让它跟着横向滚动跑出可视区。
Table 的 caption 也别忘了。它既是表格说明,也是无障碍读屏器最先读到的内容。
从真实标题生成目录
下面的脚本从正文的 h2、h3 生成目录,宽屏放进右侧栏,窄屏放回正文标题之后。它只读取 textContent,不改写可能被外部引用的标题 ID。
const layout = document.querySelector('.article-layout'); const toc = document.querySelector('[data-article-toc]'); const list = toc?.querySelector('[data-toc-list]'); const toggle = toc?.querySelector('[data-toc-toggle]'); const header = document.querySelector('.article-header'); const content = document.querySelector('.article-content'); const aside = document.querySelector('.article-aside');
if (toc && list && toggle && header && content && aside) { const headings = [...content.querySelectorAll('h2[id], h3[id]')]; list.replaceChildren(...headings.map((heading) => { const link = document.createElement('a'); link.href = '#' + encodeURIComponent(heading.id); link.textContent = heading.textContent; const item = document.createElement('li'); item.append(link); return item; })); toc.hidden = headings.length === 0;
const wide = matchMedia('(min-width: 1024px)'); const place = () => { if (wide.matches) aside.append(toc); else header.after(toc); }; place(); layout?.classList.toggle('has-toc', headings.length > 0);
toggle.addEventListener('click', () => { const open = toggle.getAttribute('aria-expanded') !== 'true'; toggle.setAttribute('aria-expanded', String(open)); list.hidden = !open; }); wide.addEventListener('change', place); }
|
has-toc 只在目录确实有内容时添加,否则宽屏会白白预留一列。按钮只控制移动端的折叠;宽屏目录始终展开。接到 PJAX 站点时,把这段封装成挂载函数,并在卸载时移除 matchMedia 监听。
元信息和浮动工具放哪里
发布日期、更新时间和阅读时长放在信息栏;标题区只保留标题、摘要和标签。这样宽屏不会在标题下方重复一组日期,窄屏也不需要额外折叠。
浮动播放器或返回顶部按钮要避开正文和目录的阅读区域。窄屏面板用 100dvh 限制高度,底部留出安全区;播放器收起时只占一个小胶囊,展开时也不该盖住整列目录。
用长内容验收
在示例页里放一条长 URL、一段长代码和一张十列表格,然后按下面的宽度检查:
- 1440px:信息栏、正文、目录三列;目录 sticky,正文行宽稳定。
- 1200px:三列仍然放得下,中间列没有被长代码撑破。
- 1024–1199px:信息栏收起,正文和目录两列;日期回到标题下方。
- 768px 和 390px:单列,目录在正文前折叠;页面本身不横向滚动。
- 键盘能展开目录、跟随锚点并进入滚动区域,焦点始终可见。
- 禁用 JavaScript 后,正文和静态目录仍然可读,链接仍然能跳转。
尺寸和 DOM 检查只能验证规则。字体观感、触控滚动和 sticky 的真实行为,还是要在浏览器里看一眼。
技术参考