Next.jsReactTypeScripti18nAWSS3CloudFront

发布 Own Info App — 基于 Next.js (SSG) × S3 × CloudFront 的极简应用

Sloth255
Sloth255
·2 min read·415 words

应用概述

我发布了"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.htmlen.htmlja.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-functionpublish-function
    • 按需执行 CloudFront 缓存失效(/*

AWS 凭证、S3 存储桶、CloudFront Distribution ID 和 NEXT_PUBLIC_* 均配置为 Secrets/Vars。
手动通过 CLI 更新也是可行的,但基本交由 CI 处理。


总结

  • 本地化
  • 最小化 SEO
  • 最小化基础设施配置
  • CI/CD & IaC

在开始个人开发时,我想完整体验发布一个应用的全过程——哪怕是任何简单的东西——于是就做成了这个低成本的极简应用。
很高兴第一次使用了 Next.js。接下来,我将尝试使用 SSR 而非 SSG 部署到 Vercel。

欢迎体验我创建的应用 📱

https://own-info-app.sloth255.comhttps://own-info-app.sloth255.com