← 返回文章

静态博客前端:布局与排版

更新于

用信息栏、正文与目录三列组织长文:宽屏分栏、窄屏单列,代码和表格只在局部滚动。

信息栏、正文纸面与目录三列,正文中留出一段可横向滚动的代码

长文排版最怕三件事:正文行宽太宽,读到后面容易串行;代码和表格把整页撑出横向滚动;目录在手机上占掉半屏,正文还没开始就要先划过去。

这篇用普通 HTML、CSS 和少量 JavaScript 搭一套文章页骨架,解决的就是这三个问题。它不依赖框架,也不包含滚动高亮、搜索和 PJAX;接到具体主题时,按真实生成的 DOM 调整选择器就行。

宽屏为信息栏、正文与目录三列;1024 至 1199 像素为正文与目录两列;窄屏收为单列,目录折叠。
同一份 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 的真实行为,还是要在浏览器里看一眼。

技术参考