WaterBaby Serres — 婴幼儿游泳学校
项目概述
WaterBaby Serres 在 Elpida Resort 的恒温室内泳池里,教四个月起的婴儿和十岁以下的儿童。这个网站要先对希腊家长说话,其次才是英语访客,但又不能变成两个网站。于是每一句面向访客的文字都在同一个文件里写了两遍,切换器只负责隐藏其中一份:每页一个网址,不重新加载,不发请求,没有第二套页面要同步,而且关掉 JavaScript 希腊语依然读得完整。六个页面撑起全部内容——首页、关于、两个课程、相册与联系——其中列出儿童的四个级别,从入门到高阶,还有家长需要准备的装备与证明文件,以及常见问答。GSAP 与 ScrollTrigger 放在本地而不是 CDN,所有动画都是叠加式的:CSS 不预先隐藏任何东西,脚本失败留下的是完整页面而不是空白页。首页的入场动画每次会话只放一遍,让一片水波沿视口退去,并由 CSS keyframes 自行收尾——即便 JavaScript 全数失效,也不会把访客困在遮罩后面。结构化数据覆盖课程、问答、相册和面包屑;llms.txt 为 AI 助手列出已核实的事实,并由测试与页面绑定,使两者不会走样。二十一个 Node 测试把一切钉住——页面结构、希腊语文案、跳转桩、加载动画的安全性质,以及每一块橙色表面的 WCAG 对比度。
挑战
双语网站通常意味着什么都要两份——两套页面、两个网址、两次“改了这边忘了那边”的机会。而这个站点由一位教练独自维护,每页多出一份副本是负担,不是功能。希腊语还必须是真正的默认语言,在任何脚本运行之前就能读,因为受众是本地家长。而游泳学校想要的那些点缀——入场动画、滚动显现、灯箱——恰恰是脚本在信号不好的手机上失败时,会留下一片空白的东西。
解决方案
两种语言都内联写在每个文件里,切换器只翻转一个 hidden 属性,于是只有一个页面要改、一个网址要分享、没有东西需要同步;语言偏好存在 localStorage,切换时派发一个自定义事件,相册会监听它。非文本内容——aria-label、占位符、meta 描述,乃至文档标题——通过成对的 data 属性在同一个函数里翻译,所以翻译过的页面不会留下没翻到的角落。动效按规矩只做叠加,并由测试强制执行:CSS 绝不允许预先隐藏参与动画的元素,因此 GSAP 加载失败只损失动画本身。入场遮罩以 hidden 出厂,只有 JavaScript 加上的类才会显示它,而它的退场是带 fill-mode forwards 的 CSS keyframe——脚本被关掉、被拦截或抛错,遮罩照样自行散去。库文件放在本地,CDN 挂掉不会连带把站点的动效一起带走;二十一个测试每一个都通过故意破坏它所保护的东西验证过。
图片集

首页——家长第一眼看到的,就是泳池本身

理念——让家长安心的三个数字

两个课程——从四个月到十岁

资历——四项家长可以核实的资格

装备与文件——每一次第一堂课之前都会被问到的问题

相册——照片的大小由画面决定,而不是由网格决定

灯箱——原生 dialog 元素,键盘优先

联系——周末课时,以及去泳池的路

平板上的首页——标题下方,两个清楚的下一步

Baby Swimming——家长要一起下水的课程

儿童游泳——小班教学,按每个孩子的节奏

故事——是谁、在什么时候把婴儿游泳带到塞雷斯

相册——每张照片都能放大,轻点或按方向键即可

手机上——全宽按钮,拇指一够就到

手机菜单——两种语言和电话号码都在里面

手机上的课程——先看年龄段

课表——周末上课,配合家庭生活

手机上的相册——一次一张,铺满屏宽

手机上的灯箱——每张照片都保留自己的说明

联系——三种方式,联系一所没有前台的学校