文章

Astro 内容站:SiteHeader 组件、品牌区与响应式导航

把站点名、导航、语言切换、主题按钮和移动端菜单收敛到一个 Header 组件里,并用数据属性控制交互状态。

一个内容站做到现在这个阶段,Header 已经不是“上面放几个链接”那么简单了。当前仓库里的头部同时承担了这些职责:

  • 显示站点品牌
  • 承载主导航
  • 提供中英文切换入口
  • 提供主题切换按钮
  • 在移动端收成汉堡菜单

这些行为现在都落在 src/components/layout/SiteHeader.astro

组件输入先收敛成一组明确的 props

当前 Header 没有自己去读取全局状态,而是全部通过 props 接收:

type HeaderLink = {
  href: string;
  label: string;
};

type HeaderProps = {
  siteName?: string;
  homeHref?: string;
  navLinks?: HeaderLink[];
  localeSwitch?: HeaderLink | null;
  menuToggleLabel?: string;
  themeToggleLightLabel?: string;
  themeToggleDarkLabel?: string;
};

这一步很重要。因为品牌名、首页链接、中英按钮文案,其实都来自 site.ts,Header 只是最后的渲染层。

结构分成三块

Header 的 DOM 结构现在很稳定:

  1. 品牌区:logo + 站点名
  2. 右侧操作区:导航菜单、语言切换、主题按钮
  3. 移动端菜单按钮

主体结构如下:

<header class="site-header" data-site-header data-menu-open="false">
  <a class="site-brand" href={homeHref}>...</a>

  <div class="site-header-end">
    <div class="site-menu" id="site-menu" data-site-menu>
      <nav class="site-nav">
        {navLinks.map((link) => (
          <a href={link.href}>{link.label}</a>
        ))}
      </nav>
    </div>

    <div class="site-controls">...</div>
    <button class="site-menu-toggle" data-site-menu-toggle>...</button>
  </div>
</header>

这个拆法的价值在于:桌面端和移动端共用同一套 DOM,只通过 CSS 和少量脚本切换状态,不需要准备两份头部模板。

品牌区不是纯文本,而是一个完整入口

品牌区当前包含一个内联 SVG 标记和站点名:

  • 点击品牌即可返回首页
  • 图形标记不依赖额外图片资源
  • 站点名保持可配置,不写死在组件里

这类站点级元素放在 Header 内,比在各页面单独写一个“返回首页”更稳定,也更符合长期维护的结构。

移动端菜单状态用 data attribute 管理

Header 没有引入客户端框架状态,而是直接使用:

  • data-site-header
  • data-site-menu
  • data-site-menu-toggle
  • data-menu-open

脚本里只做最小状态切换:

const isOpen = () => header.dataset.menuOpen === "true";
const closeMenu = () => {
  header.dataset.menuOpen = "false";
  toggle.setAttribute("aria-expanded", "false");
};
const openMenu = () => {
  header.dataset.menuOpen = "true";
  toggle.setAttribute("aria-expanded", "true");
};

这样做有两个直接好处:

  • 状态既能被 JS 读,也能直接被 CSS 用来控制样式
  • Astro 站点不需要为了一个 Header 菜单再引入额外状态库

当前交互把几个细节补齐了

Header 脚本除了开关菜单,还处理了几个容易漏掉的动作:

  • 桌面宽度下强制关闭移动端菜单
  • 点击 Header 外部区域时关闭菜单
  • Escape 时关闭菜单并把焦点还给按钮
  • 点击菜单内链接后关闭菜单
  • 通过 menuReady 防止脚本重复初始化

这也是为什么当前脚本要监听:

  • click
  • keydown
  • matchMedia("(min-width: 761px)")

不是因为逻辑复杂,而是因为要把“可用”做完整。

响应式样式的关键点

LayoutStyles.astro 里,Header 的响应式处理有两层:

桌面端:

  • 导航与控制按钮同行显示
  • site-header-end 通过 margin-left: auto 推到右侧

移动端:

  • site-menu-toggle 显示
  • site-menu 变成绝对定位浮层
  • data-menu-open="true" 时控制浮层显隐和汉堡线条动画

这意味着菜单的展开不是重新渲染,而只是视觉状态变化,因此切换成本很低。

为什么语言切换和主题按钮留在顶栏,不跟着菜单一起藏

当前实现里,中英文切换和主题按钮始终保留在 site-controls 中,移动端也不被塞进折叠菜单。这样做的原因很简单:

  • 语言切换是全站级入口,不应该被再点一次菜单才能看见
  • 主题切换是轻操作,放在顶栏比藏进导航里更直觉
  • 即使菜单关闭,用户依然能快速执行这两个高频操作

这一步让 Header 不只是“导航栏”,而是真正的站点控制层。

这类实现为什么适合 Astro

因为 Astro 页面本来就是静态优先的。像 Header 这种只需要少量前端交互的部件,用一个内联脚本加数据属性就足够了:

  • 保持 HTML 直出
  • 不引入额外客户端包袱
  • 交互又不至于退化成完全不可用

所以当前这个 SiteHeader,本质上就是一个典型的 Astro 组件化做法:结构静态化,交互最小化,状态直接落在 DOM 上。

相关文章

Astro 内容站:PageHero 组件与页面头部统一

把列表页、首页、系列页和详情页里重复的标题区抽成 PageHero,让返回链接、eyebrow、标题和状态文案共用一套骨架。

2026-04-16

Astro 内容站:PageSectionHeader 组件与区块标题复用

把 section 标题、补充说明、右侧动作和分组计数统一到一个组件里,让首页、归档和索引列表共享稳定的区块头结构。

2026-04-16

Astro 内容站:组件组合重构、重复代码清理与文档同步

一次收口页面骨架、分页辅助、概览卡和旧文档漂移,把页面组合整理成更稳定的单组件文件结构。

2026-04-16

Astro 内容站:SummaryStatGrid 组件与归档总览卡

把归档页里重复的统计卡结构抽成 SummaryStatGrid,为总条目、文章数、章节数和最近更新时间提供统一的数值卡组件。

2026-04-16