应用概述
我发布了"Own Info App",这是一个小型 Web 应用,可以快速查看网络信息(例如全局 IP 地址)以及浏览器/设备信息,并立即进行复制/分享。
https://x.com/Sloth255000/status/1986792593569763395- 技术栈(部分):Next.js 15(App Router)、React 19、TypeScript、Tailwind CSS
- 分发:S3 + CloudFront(静态导出)
- 多语言:英语/日语/中文/韩语/德语/法语/西班牙语/葡萄牙语
- 分析:Firebase Analytics
页面跳转和 UI 尽可能保持简洁。打开后,"复制"或"分享"所需信息,然后关闭——就这么简单。
技术栈与设计要点
1) Next.js 15 App Router × 静态导出
- 使用
output: 'export'完全静态输出(SSG) - 输出文件为扁平结构,
index.html、en.html、ja.html等直接位于 out 目录下
这种配置使 CDN 分发和缓存变得简单,无需动态服务器或 SSR,实现了快速响应的应用体验。
2) 使用 CloudFront Functions 进行 URL 重写
使用 CloudFront Functions(cloudfront-js-2.0)实现轻量级 URL → 实际文件的映射。
/→/index.html/ja→/ja.html(末尾斜杠同样处理)/contact、/privacy、/licenses→ 对应的*.html文件- 带有扩展名的 URI(.svg、.js、.css 等)不经重写直接通过
- 不支持的语言重定向至
/en/_not-found
3) i18n 与路由
- 支持语言:
en, ja, zh, ko, de, fr, es, pt - 翻译文件分离至
src/locales/*.json,App Router 为每个语言进行 SSG 生成 - 根路径
/渲染默认语言(en)版本,等同于 SSR,供 SEO 和爬虫使用;对于浏览器访问,仅一次性根据navigator.languages客户端重定向至对应语言页面
4) 每个语言的 SEO 与 OGP/Twitter Cards
- 为每个页面实现
generateMetadata(),以正确输出每种语言对应的"canonical / og:url / og:image / og:locale" - 为每种语言准备了 OGP 图片(
public/og-*.png),图片中的描述文字来自翻译文本 - 在服务端组件中(
application/ld+json)为每个语言输出结构化数据(JSON-LD),inLanguage中明确指定了全部 8 种语言
// 示例:结构化数据输出(节选)
export default function StructuredData({ title, description, siteUrl }: Props) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'WebApplication',
name: title,
description,
url: siteUrl || '/',
applicationCategory: 'UtilityApplication',
operatingSystem: 'Web Browser',
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
inLanguage: [
{ '@type': 'Language', name: 'English', alternateName: 'en' },
{ '@type': 'Language', name: 'Japanese', alternateName: 'ja' },
{ '@type': 'Language', name: 'Chinese', alternateName: 'zh' },
{ '@type': 'Language', name: 'Korean', alternateName: 'ko' },
{ '@type': 'Language', name: 'German', alternateName: 'de' },
{ '@type': 'Language', name: 'French', alternateName: 'fr' },
{ '@type': 'Language', name: 'Spanish', alternateName: 'es' },
{ '@type': 'Language', name: 'Portuguese', alternateName: 'pt' },
],
};
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />;
}
5) 每个语言正确设置 <html lang>
在 App Router 中,存在"根布局的 <html> 对所有页面公用"的限制,导致构建时 <html lang="en"> 保持固定。
为此,我在语言布局和根页面中都引入了一个"在客户端覆盖 <html lang>"的轻量组件,在实际使用时更新为正确的语言标签。
'use client'
import { useEffect } from 'react'
export default function HtmlLangSetterClient({ lang }: { lang: string }) {
useEffect(() => {
document.documentElement.setAttribute('lang', lang)
}, [lang])
return null
}
在将构建时静态 HTML 与用户实际看到的 DOM 差异保持最小的同时,实现了静态导出约束下的实用方案。
部署/运维
基础设施
- S3(静态托管)+ CloudFront(源站为 S3 的 Website 端点)
- CloudFront Functions 单独部署(SAM 模板用于资源创建,代码更新由 CI 侧处理)
CI/CD(GitHub Actions)
- 在推送至
main分支时触发:- Next.js 构建(静态导出)
- S3 同步
/_next/static使用max-age=31536000, immutable- HTML 使用
max-age=300, must-revalidate
- CloudFront Function 代码更新(
update-function→publish-function) - 按需执行 CloudFront 缓存失效(
/*)
AWS 凭证、S3 存储桶、CloudFront Distribution ID 和 NEXT_PUBLIC_* 均配置为 Secrets/Vars。
手动通过 CLI 更新也是可行的,但基本交由 CI 处理。
总结
- 本地化
- 最小化 SEO
- 最小化基础设施配置
- CI/CD & IaC
在开始个人开发时,我想完整体验发布一个应用的全过程——哪怕是任何简单的东西——于是就做成了这个低成本的极简应用。
很高兴第一次使用了 Next.js。接下来,我将尝试使用 SSR 而非 SSG 部署到 Vercel。
