一个内容站做到现在这个阶段,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 结构现在很稳定:
- 品牌区:logo + 站点名
- 右侧操作区:导航菜单、语言切换、主题按钮
- 移动端菜单按钮
主体结构如下:
<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-headerdata-site-menudata-site-menu-toggledata-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防止脚本重复初始化
这也是为什么当前脚本要监听:
clickkeydownmatchMedia("(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 上。