必须且只能出现一次,是屏幕阅读器跳转核心内容、搜索引擎提取主题的唯一语义锚点;重复或嵌套会导致辅助技术失效、seo降权及hydration mismatch。

写好 HTML 语义化标签不是“加分项”,而是前端工程师能稳定交付、不被线上 bug 反复咬住的底线。 它直接影响 DOM 结构可读性、无障碍支持是否生效、SEO 是否掉量,甚至影响框架组件嵌套时的渲染逻辑——这些都不是靠加个 aria-label 或临时 patch 就能绕开的。
为什么 <main></main> 必须且只能出现一次
浏览器和屏幕阅读器依赖 <main></main> 精确定位页面核心内容。若页面中误写多个 <main></main>(比如在组件库循环渲染时没做限制),会导致:
- 辅助技术只识别第一个
<main></main>,后续内容被忽略 - 搜索引擎可能降权,认为结构混乱、内容主次不明
- 某些 SSR 框架(如 Next.js)在 hydrate 阶段校验失败,触发 hydration mismatch 警告
实操建议:用 document.querySelector('main') 在控制台快速验证;组件内避免直接输出 <main></main>,改由父级统一包裹。
<nav></nav> 不等于“所有带链接的容器”
常见错误是把侧边栏菜单、页脚友情链接、文章内跳转锚点都塞进 <nav></nav>。W3C 明确定义:<nav></nav> 应仅用于“主要导航区块”,即用户用来在站点内大范围跳转的入口集合。
- 页脚的“关于我们/隐私政策”属于次要导航,更适合用
<footer></footer>内的普通<ul></ul> - 文章内的目录链接(
<a href="#section1"></a>)不属于导航,应放在<section></section>或<aside></aside>中 - React/Vue 组件中动态生成的面包屑(breadcrumb),即使含链接,也不该硬套
<nav></nav>,可用<ol class="breadcrumb"></ol>+aria-label="breadcrumb"
为什么 <section></section> 和 <article></article> 不能互换
二者语义差异直接决定内容如何被索引与朗读:
-
<article></article>表示独立、可分发的内容单元(如一篇博客、一条新闻、一个用户评论),自带隐式 heading 级别,会被 RSS 抓取、被屏幕阅读器标记为“可单独订阅” -
<section></section>是主题性分组,必须有显式标题(<h2></h2>~<h6></h6>),否则语义断裂;它不能脱离上下文独立存在 - 错误示例:把整页产品列表用一个
<section></section>包裹,而每个商品卡片却用<div> —— 正确做法是外层 <code><section></section>+ 每个卡片用<article></article>当框架自动注入 DOM 时,语义还能信得过吗
Vue 的
v-if、React 的条件渲染、Svelte 的 {#if} 都可能让语义结构在运行时坍塌。例如:- 用
<header></header>包裹 logo 和<nav></nav>,但<nav></nav>被条件隐藏后,<header></header>里只剩<h1></h1>—— 这违反了<header></header>应包含“引导性内容”的语义预期 - SSR 渲染出完整语义结构,CSR 后因状态变化删掉
<main></main>,导致首屏可访问性瞬间归零
实操建议:用 Lighthouse 的 Accessibility audit 扫描真实 DOM;在关键节点(如
<main></main>、<nav></nav>)加data-semantic="true"标记,便于 E2E 测试断言;避免用 display: none 切换语义容器,改用hidden属性或 aria-hidden。真正难的不是记住哪些标签叫什么,而是在组件抽象、状态驱动、服务端渲染交织的现代工程中,始终让 HTML 的骨架不松动——那根骨架一旦歪了,所有上层优化都会打滑。
- 用











