首页
/ freeCodeCamp 语义化 HTML5 挑战深度解析:用 main 与区块标签构建可访问的网页骨架

freeCodeCamp 语义化 HTML5 挑战深度解析:用 main 与区块标签构建可访问的网页骨架

2026-09-06 19:00:53作者:农烁颖Land

本指南完整拆解 freeCodeCamp 开源课程库中 Basic HTML and HTML5 模块的 Introduction to HTML5 Elements(HTML5 元素入门)挑战,说明 HTML5 语义化标签如何让结构更易读、更利于搜索引擎优化(SEO)与无障碍访问,并通过源码级注释与逐条验收标准(hints)演示如何将一个 main 元素与两个 p 段落正确组织为一个可提交、可通过测试的页面结构。读完你可以独立完成该挑战、理解其底层校验逻辑,并能迁移到真实项目的语义化重构中。

挑战背景:它在课程体系中的位置

本挑战文件位于 curriculum/challenges/english/blocks/basic-html-and-html5/bad87fee1348bd9aecf08801.md,是 freeCodeCamp Responsive Web Design(响应式网页设计)课程认证路径中 Basic HTML and HTML5 区块的一环。

从区块结构元数据 curriculum/structure/blocks/basic-html-and-html5.json 可以看到,该区块共编排了 28 个挑战,本挑战(id:bad87fee1348bd9aecf08801)排在「Say Hello to HTML Elements」之后、位于区块中段的第 8 位,前序挑战已让你接触了 h2p、占位文本、注释等基础元素,本挑战则是从"单个标签的使用"过渡到"有语义的结构组织"的关键一步;而整个区块归属于 curriculum/structure/superblocks/responsive-web-design.json 定义的 superblock。

挑战文件采用 freeCodeCamp 课程挑战统一的 Markdown + YAML frontmatter 格式,其核心结构是五个由 # --xxx-- 标记分隔的章节:--description--(知识点讲解)、--instructions--(任务指令)、--hints--(验收断言)、--seed--(起始代码)、--solutions--(官方参考实现),这种格式由 curriculum/schema/challenge-schema.js 中的 Joi 校验器约束,保证仓库内每个挑战文件都符合可被构建工具解析的规范。

HTML5 语义元素:为什么"标签名字有意义"很重要

原文档的核心知识段落指出:HTML5 引入了更多描述性的 HTML 标签,包括:

  • main —— 页面独一无二的主体内容容器
  • header —— 页首 / 区块头部
  • footer —— 页脚 / 区块尾部
  • nav —— 导航区域
  • video —— 视频内容
  • article —— 独立成篇的文章内容
  • section —— 逻辑分段

这些标签与旧时代通篇 <div> 的做法本质区别在于"语义"二字:<div> 只表达"这里有一个块",而 <main><nav><article> 同时向程序宣告"这个块在页面里扮演什么角色"。文档给出了三点收益:

  1. 为 HTML 赋予描述性的结构 —— 开发者阅读代码时一眼可知每个区域的职责;
  2. 让 HTML 更容易阅读和维护 —— 语义边界即结构边界,层级关系更清晰;
  3. 助力 SEO 与可访问性(accessibility) —— 搜索引擎与辅助技术(如屏幕阅读器)可以理解页面重点区域。

其中文档特别强调:main 标签能帮助搜索引擎和其他开发者快速定位页面的主体内容。从语义规范角度理解,main 表示文档的主内容区域,每个页面通常只应出现一次,且应直接承载该页最核心、区别于其他页面共同元素(导航、页脚、侧栏)的内容。

官方示例代码

文档给出的标准用法是创建一个 main 元素,并在其中嵌套两个子元素:

<main>
  <h1>Hello World</h1>
  <p>Hello Paragraph</p>
</main>

这个例子演示了语义化标签的三种典型写法习惯:标签名小写、成对的开闭标签、按内容层级缩进嵌套——这些习惯也会成为后续 hint 正则校验的关注点。

任务指令拆解

--instructions-- 章节给出了本挑战要完成的两步操作,全部基于 CatPhotoApp 的示例页面:

第一步:创建第二个 p 元素,填入如下 Kitty Ipsum 文本:

Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.

第二步:创建一个 main 元素,并且把两个 p 元素嵌套进 main 中。

注意原文档的措辞细节:需要嵌套的是"两个 p 元素"——也就是说页面原本已有的第一个段落也要一并移入 main,而不是把新段落放在 main 外面。这一要求会被后续 hint 精确校验。

从起始代码到通关:分步实现

起始状态(seed)

本挑战的起始代码定义在 --seed----seed-contents-- 中:

<h2>CatPhotoApp</h2>

<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>

即页面上目前只有一个 h2 标题和一个现成的段落,尚无 main,也没有第二段 Kitty Ipsum 文字。

通关实现

对照官方 --solutions--,一次完整的正确提交如下:

<h2>CatPhotoApp</h2>
<main>
  <p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
  <p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>

关键点:h2 标题保持在 main 之外(它属于页面头部语境),main 的开标签位于第一个 p 之前、闭标签位于第二个 p 的闭标签之后,且两个段落全部位于 main 内部。缩进并非语义要求,但良好缩进能让嵌套关系一目了然。

验收标准逐条解读:7 条 hint 背后的校验逻辑

--hints-- 章节定义了本挑战的全部自动验收断言,理解它们就等于理解了该挑战的评分器视角。hints 内的校验代码以浏览器 DOM 为运行环境(其中的 document.querySelectorAll 等 API 由测试沙箱在运行时注入到用户代码构建出的页面中),逐条解析如下。

1. 页面应有 2 个 p 元素(含 Kitty Ipsum 文本)

assert.lengthOf(document.querySelectorAll('p'), 2);

querySelectorAll('p') 会统计整个文档中所有 <p> 标签,计数必须恰好为 2。若漏建第二个段落、或无意多写了段落都会失败。

2. 每个 p 元素都必须有闭标签

assert.match(code, /<\/p>/g);
assert.strictEqual(code.match(/<\/p>/g).length, code.match(/<p/g).length);

先断言代码中确实存在 </p>,再断言 </p> 的出现次数与 <p 的开标签次数严格相等——这保证没有任何段落处于"有头无尾"的未闭合状态。值得注意的是正则 /<p/g 并不会误伤 <p ...> 带属性的写法,但要求开标签写法形如 <p>

3. 第二个 p 元素应包含指定文本的开头

assert.match(document.querySelectorAll('p')[1].textContent, /Purr\s+jump\s+eat/gi);

DOM 中下标为 [1](第二个)的段落,其文本需匹配 Purr jump eat,其中 \s+ 容忍任意空白符、i 忽略大小写。这允许文字在不同排版下有细微差异,但必须以 "Purr jump eat" 开头。

4. 页面应恰好有 1 个 main 元素

assert.lengthOf(document.querySelectorAll('main'), 1);

呼应语义规范中的"每页一个主体区域"——多余或缺失 main 都会失败。

5. main 内应有且仅有 2 个 p 子元素

const main = document.querySelector('main');
const children = main.querySelectorAll("p");
assert.lengthOf(children, 2);

main.querySelectorAll("p") 只统计 main 后代中的段落,数量必须为 2,即两个段落都在 main 内部。

6. main 开标签必须位于第一个 p 之前

assert.match(code, /<main>\s*?<p>/g);

校验源码中 main 的开标签与紧随其后的第一个 <p> 之间只能有空白字符,确保第一个段落确实是 main 的第一个内容节点。

7. main 闭标签必须位于第二个 p 闭标签之后

assert.match(code, /<\/p>\s*?<\/main>/g);

与上一条首尾呼应,确认第二个段落闭合后 main 才闭合,两个段落被完整包裹在 main 中。

综合来看,这 7 条断言覆盖了"数量、闭合、文本内容、嵌套边界"四个维度,共同保证了最终结构严格符合语义化预期。

从源码看挑战文件的结构与校验机制

本挑战的 frontmatter 与 basic-html-and-html5.json 元数据一起,揭示了 freeCodeCamp 课程挑战是如何被定义、验证和渲染的:

  • id: bad87fee1348bd9aecf08801 —— 全局唯一标识,区块元数据通过它引用挑战并编排顺序(见上文区块 JSON 中该 id 的位置);
  • title / dashedName —— 展示标题与用于生成 URL 的 slug 形式;
  • challengeType: 0 —— 挑战类型编号。常量定义在 packages/shared/src/config/challenge-types.ts,其中 const html = 0; 表示本挑战属于纯 HTML 编码型挑战,其答题载体与校验环境面向 HTML 编辑器;
  • videoUrl / forumTopicId —— 关联讲解视频与论坛讨论帖的元数据字段;
  • --description-- / --instructions-- / --hints-- / --seed-- / --solutions-- —— 五个内容区块均有 Joi schema 约束(参见 curriculum/schema/challenge-schema.js),例如 --hints-- 中的每个断言必须是可执行的 JS 代码段、--seed-- 必须提供完整的可运行 HTML 等,这是该挑战文件能被 curriculum 相关测试与构建工具正确消费的前提。

延伸阅读:从 main 走向完整的语义化与可访问性

原文档末尾特别给出提示:"许多新的 HTML5 标签及其好处会在 Applied Accessibility(应用无障碍)部分继续展开。" 这条线索在仓库中有直接的落点——课程在 curriculum/challenges/english/blocks/applied-accessibility 区块继续围绕语义化与无障碍展开:例如其中的导航跳转链接(jump link)挑战会引导你用 main 之外的手段配合键盘用户跳转主体内容。由此可以看到本挑战并非孤立知识点,而是 freeCodeCamp 课程在"响应式网页设计"主题下语义化与可访问性主线的一部分。

将本挑战习得的技能迁移到实战中,你可以这样组织一个典型页面的骨架:

<body>
  <header>
    <nav>…站点导航…</nav>
  </header>
  <main>
    <article>
      <h1>文章标题</h1>
      <section>…章节一…</section>
      <section>…章节二…</section>
    </article>
  </main>
  <footer>…版权与联系方式…</footer>
</body>

常见错误与自查清单

基于上文 7 条 hint 的校验逻辑,提交前可以按以下清单自查:

  • 是否恰好创建了两个 p?新旧段落都要计入;
  • 两个 p 是否都写了 </p> 闭标签?
  • 第二个段落的文本是否以 Purr jump eat 开头(大小写不敏感、空白符无妨)?
  • 页面是否只有一个 <main>
  • 两个 p 是否都位于 <main>...</main> 内部,而 h2 保持在 main 之外?
  • <main> 是否紧邻第一个 <p> 之前、</main> 是否紧跟第二个 </p> 之后?

最稳妥的提交方式就是直接对照官方参考实现(--solutions-- 章节),在其基础上保持结构不变,即可保证 7 条断言全部通过。

<输出文章> (注:以上为完整文章正文,末尾标签用于结束包裹,实际交付时请按格式规范输出。) </输出文章>

Actually, let me reconsider the output. The final deliverable should just be the article wrapped in <输出文章></输出文章> tags. Let me rewrite cleanly.

<输出文章>

本指南完整拆解 freeCodeCamp 开源课程库 Basic HTML and HTML5 模块中的 Introduction to HTML5 Elements(HTML5 元素入门)挑战,讲解 HTML5 语义化标签如何让 HTML 结构更易读、更利于搜索引擎优化(SEO)与无障碍访问,并逐条剖析该挑战的验收断言(hints),带你掌握如何把两个段落正确组织进一个 main 元素并通过全部自动测试。读完你可以独立通关该挑战,并将语义化结构实践迁移到真实网页开发中。

挑战在课程体系中的位置

本挑战文件位于 curriculum/challenges/english/blocks/basic-html-and-html5/bad87fee1348bd9aecf08801.md,隶属 freeCodeCamp 的 Responsive Web Design 认证课程路径。从区块元数据 curriculum/structure/blocks/basic-html-and-html5.json 可见,Basic HTML and HTML5 区块共编排了 28 个挑战,本挑战(id bad87fee1348bd9aecf08801,标题 Introduction to HTML5 Elements)位于区块第 8 位,前序挑战已让学习者掌握 h2p、占位文本、注释等基础元素,本挑战是其进阶:从"单个标签的使用"过渡到"具有语义的结构组织"。

挑战文件的 frontmatter 中 challengeType: 0packages/shared/src/config/challenge-types.ts 中定义为 const html = 0,表示这是纯 HTML 编码型挑战。该文件与仓库内其他数千个挑战一样,遵循统一的 YAML frontmatter + --description--/--instructions--/--hints--/--seed--/--solutions-- 五段式结构,并由 curriculum/schema/challenge-schema.js 中的 Joi schema 严格校验格式合法性。

核心知识点:HTML5 语义化元素

文档指出:HTML5 引入了更多描述性的 HTML 标签,包括 mainheaderfooternavvideoarticlesection 等。这些标签为 HTML 赋予了"语义"层面上的含义。

使用语义标签能带来三方面收益:

  1. 为 HTML 提供描述性结构:开发者一眼即可看出页面中每个区块扮演的角色;
  2. 让 HTML 更容易阅读:语义边界即结构边界,嵌套关系与内容职责一目了然;
  3. 助力 SEO 与可访问性:搜索引擎和屏幕阅读器等辅助技术可以借此理解页面重点内容。

文档特别强调,main 标签能帮助搜索引擎和其他开发者快速定位页面主体内容main 元素在 HTML 规范中表示文档的主内容区域,它包裹的是页面中独一无二的核心内容,而非导航、页脚等各页面共享的区域。

官方示例

文档给出了 main 的标准用法——一个 main 元素内嵌套两个子元素:

<main>
  <h1>Hello World</h1>
  <p>Hello Paragraph</p>
</main>

该示例同时示范了语义化 HTML 的书写规范:标签名小写、成对书写开闭标签、通过缩进体现嵌套层级。

挑战任务详解

--instructions-- 中明确了本次要完成的两步操作(基于 CatPhotoApp 页面):

第一步:创建第二个 p 元素,内容为 Kitty Ipsum 文本:

Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.

第二步:创建一个 main 元素,并只把两个 p 元素嵌套到 main 内部。

注意"两个"这一关键词:既有代码中已有一个段落,需要把它也移入 main,使 main 成为两个段落的唯一父容器——这一点会在后面的验收断言中被精确验证。

从起始代码到通关

起始代码(seed)

--seed----seed-contents-- 给出了页面初始状态:

<h2>CatPhotoApp</h2>

<p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>

页面上只有一个 h2 标题和一个现成段落,尚无 main 元素、也没有第二段文本。

通关参考实现(solution)

对照 --solutions-- 中官方给出的完整答案:

<h2>CatPhotoApp</h2>
<main>
  <p>Kitty ipsum dolor sit amet, shed everywhere shed everywhere stretching attack your ankles chase the red dot, hairball run catnip eat the grass sniff.</p>
  <p>Purr jump eat the grass rip the couch scratched sunbathe, shed everywhere rip the couch sleep in the sink fluffy fur catnip scratched.</p>
</main>

结构要点:

  • h2 标题保持在 main 之外,因为标题不属于主内容区域的一部分(本挑战的断言不要求也禁止把 h2 移入);
  • main 的开标签位于第一个 p 之前,闭标签位于第二个 p 之后;
  • 两个段落必须同时位于 main 内部。

验收断言逐条解析:hint 背后的校验逻辑

--hints-- 中定义了 7 条自动校验断言。这些 JS 断言在页面加载后的 DOM 环境内执行(可直接调用 documentcode 等对象),逐条剖析如下。

1. 页面应恰好有 2 个 p 元素

assert.lengthOf(document.querySelectorAll('p'), 2);

通过 document.querySelectorAll('p') 统计整个文档中的段落标签,数量必须严格等于 2,多一个或少一个都会失败。

2. 每个 p 元素都必须带闭标签

assert.match(code, /<\/p>/g);
assert.strictEqual(code.match(/<\/p>/g).length, code.match(/<p/g).length);

先断言代码中存在 </p>,再断言 </p> 出现次数与 <p 开标签次数严格相等——保证每个段落都有与之配对的闭标签,杜绝"有头无尾"的写法。

3. 第二个 p 元素须包含新 Kitty Ipsum 文本的开头

assert.match(document.querySelectorAll('p')[1].textContent, /Purr\s+jump\s+eat/gi);

取 DOM 中下标为 1(即第二个)的段落,校验其文本匹配 Purr jump eat。正则中 \s+ 允许任意空白、i 忽略大小写、g 全局匹配,说明该断言对换行、缩进、大小写保持宽容,但段落必须以指定文本开头。

4. 页面应恰好有 1 个 main 元素

assert.lengthOf(document.querySelectorAll('main'), 1);

呼应语义规范中"一个页面只有一个主内容区"的原则,main 不能缺失也不可重复。

5. main 内应恰好有 2 个 p 子元素

const main = document.querySelector('main');
const children = main.querySelectorAll("p");
assert.lengthOf(children, 2);

这是判断嵌套正确性的核心断言:只统计 main 内部的段落后代,数量必须为 2,即两个段落都必须位于 main 之内。

6. main 开标签应位于第一个段落标签之前

assert.match(code, /<main>\s*?<p>/g);

对源码文本断言:<main> 后只允许空白字符就紧跟 <p>,确保第一个段落确实作为 main 的第一个内容子元素出现。

7. main 闭标签应位于第二个段落闭标签之后

assert.match(code, /<\/p>\s*?<\/main>/g);

与第 6 条首尾呼应:断言 </p> 后紧跟 </main>,从闭合边界上保证两个段落被完整包裹在 main 内部。

综合来看,7 条断言从数量(第 1、4 条)、闭合完整性(第 2 条)、文本内容(第 3 条)、嵌套结构(第 5、6、7 条)四个维度锁定了预期答案,这与课程自动评分系统的设计目标一致。

挑战文件的格式机制与校验保障

该挑战文件头部 frontmatter 中的字段含义如下:

字段 含义
id bad87fee1348bd9aecf08801 全局唯一标识,被区块元数据引用以确定顺序
title Introduction to HTML5 Elements 挑战显示名称
challengeType 0 挑战类型,0 对应 HTML 编码挑战(见 challenge-types.ts
videoUrl 关联讲解视频 教学视频地址字段
forumTopicId 301097 关联论坛讨论主题 ID
dashedName introduction-to-html5-elements 用于生成 URL 的短横线命名

正文五个区块分别承担不同职责:--description-- 承载知识点、--instructions-- 给出任务、--hints-- 定义验收断言、--seed-- 提供起始代码、--solutions-- 提供官方参考实现。这套结构通过 curriculum/schema/challenge-schema.js 中定义的 Joi schema 做静态校验,配合 curriculum 目录下的 schema 测试确保每个挑战文件(包括本文件)都格式合法、字段齐全,可被课程构建与运行系统稳定消费。

延伸:从 main 走向完整的语义化与可访问性

原文档在结尾注明:"许多新 HTML5 标签及其好处会在 Applied Accessibility 部分继续介绍。" 这在仓库中有直接落点——语义化与无障碍主题在 curriculum/challenges/english/blocks/applied-accessibility 区块中持续深入展开,其中包含使用跳过导航链接、利用 main 等语义元素辅助屏幕阅读器等进阶实践。

将本挑战的技能迁移到实战中,一个语义化的页面骨架大致如下:

<body>
  <header>
    <nav>站点导航</nav>
  </header>
  <main>
    <article>
      <h1>文章标题</h1>
      <section>章节一内容</section>
      <section>章节二内容</section>
    </article>
  </main>
  <footer>版权与联系信息</footer>
</body>

这里 header 包裹站点页眉、nav 声明导航、main 承载主体、article 组织独立成篇的内容、section 做逻辑分段、footer 收尾——每一个标签都在向浏览器、搜索引擎和辅助技术"自我介绍"其职责,这正是本挑战希望学习者建立的第一性认知。

常见错误自查清单

通关前可对照以下自查项:

  • 是否创建了恰好两个 p 元素(保留原有段落并新增第二段)?
  • 两个 p 是否都配齐了 </p> 闭标签?
  • 第二个段落的文本是否以 Purr jump eat 开头?
  • 页面是否只有一个 <main>
  • 两个 p 是否都被包含在 <main></main> 之间?
  • h2 是否保持在 main 之外(既不要求也无须移入)?

当你逐项确认并通过页面右侧的测试后,即可解锁后续挑战。将本文件的参考实现(--solutions-- 部分)保存在本地随时对照,也是排查嵌套错误的最高效手段。

登录后查看全文
热门项目推荐
相关项目推荐