上线
This commit is contained in:
parent
e96ae50b34
commit
15f7a83dc1
129
README.md
129
README.md
@ -1,25 +1,124 @@
|
|||||||
# Onlook Starter Template
|
# Seven - 多语言官网(Next.js 15)
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="app/favicon.ico" />
|
<img src="app/favicon.ico" />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
This is an [Onlook](https://onlook.com/) project set up with
|
基于 Next.js 15、Tailwind CSS 和 shadcn/ui 搭建的多语言企业官网模板,内置中/英/日/韩/繁体语言路由,统一页头/页尾布局与 SEO 元数据管理。
|
||||||
[Next.js](https://nextjs.org/), [TailwindCSS](https://tailwindcss.com/) and
|
|
||||||
[ShadCN](https://ui.shadcn.com).
|
|
||||||
|
|
||||||
## Getting Started
|
## 技术栈
|
||||||
|
|
||||||
First, run the development server:
|
- Next.js 15(App Router)
|
||||||
|
- React 18
|
||||||
|
- Tailwind CSS
|
||||||
|
- shadcn/ui
|
||||||
|
|
||||||
```bash
|
## 目录结构(关键项)
|
||||||
npm run dev
|
|
||||||
# or
|
```
|
||||||
yarn dev
|
app/
|
||||||
# or
|
[lang]/
|
||||||
pnpm dev
|
layout.tsx # 按语言生成 Metadata(需 await params)
|
||||||
# or
|
page.tsx # 首页入口(需 await params)
|
||||||
bun dev
|
products/page.tsx # 产品页(Client 组件)
|
||||||
|
solutions/page.tsx # 解决方案页(Client 组件)
|
||||||
|
contact/page.tsx # 联系我们(使用 PageLayout,表单仅在客户端渲染)
|
||||||
|
components/
|
||||||
|
PageLayout.tsx # 统一 TDK、Navbar、Footer 与内容容器
|
||||||
|
Navbar.tsx
|
||||||
|
Footer.tsx
|
||||||
|
TDK.tsx # 页面标题/描述/关键词
|
||||||
|
data/
|
||||||
|
content.ts # 站点通用文案(多语言)
|
||||||
|
pageContent.ts # 各页面文案(多语言)
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) in Onlook to see the result.
|
## 快速开始(Windows PowerShell)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 本地开发(默认 http://localhost:3000)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 生产构建
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 本地预览生产构建
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
|
## 环境要求
|
||||||
|
|
||||||
|
- Node.js ≥ 18.17(推荐 LTS)
|
||||||
|
- npm ≥ 9(或使用 pnpm/bun 亦可)
|
||||||
|
|
||||||
|
## 多语言与路由
|
||||||
|
|
||||||
|
- 路由格式:`/[lang]/...`,其中 `lang ∈ { en, zh-CN, zh-TW, ko, ja }`
|
||||||
|
- 入口页静态参数:`app/[lang]/page.tsx` 中的 `generateStaticParams()`
|
||||||
|
- 元数据生成:`app/[lang]/layout.tsx` 中的 `generateMetadata()` 会根据语言返回对应 TDK
|
||||||
|
|
||||||
|
注意:Next.js 15 起,动态段的 `params` 在服务端为 Promise,必须 `await` 后再使用,例如:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
|
const { lang } = await params;
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SEO 与 TDK
|
||||||
|
|
||||||
|
- 统一在 `PageLayout` 内通过 `TDK` 组件注入 `title/description/keywords`
|
||||||
|
- `app/[lang]/layout.tsx` 中维护各语言默认元数据与 `alternates.languages`
|
||||||
|
|
||||||
|
## SSR/CSR 与 Hydration 注意事项
|
||||||
|
|
||||||
|
- 避免在 SSR 阶段使用不稳定输出(如 `Date.now()`、`Math.random()`)
|
||||||
|
- 对于仅在客户端可用的交互(如带自动填充的表单),采用“客户端渲染占位”策略:
|
||||||
|
- `contact/page.tsx` 中使用 `isClient` 状态在挂载后再渲染表单,避免浏览器扩展(如 LastPass)注入导致水合不一致
|
||||||
|
- 若你引入新的服务端函数,确保在使用 `params` 前执行 `await`
|
||||||
|
|
||||||
|
## 常见问题(FAQ)
|
||||||
|
|
||||||
|
- Hydration failed:
|
||||||
|
- 检查是否使用了未固定输出的变量;
|
||||||
|
- 检查是否有浏览器扩展注入 DOM(如密码管理器);
|
||||||
|
- 对仅客户端渲染的复杂表单使用 `isClient` 保护渲染;
|
||||||
|
- 移除不必要的 `suppressHydrationWarning`,定位并修复根因。
|
||||||
|
|
||||||
|
- Route used `params.lang`. `params` should be awaited:
|
||||||
|
- Next.js 15 的动态 API 是异步的,请调整类型为 `Promise<...>` 并 `await`。
|
||||||
|
|
||||||
|
## 开发脚本
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 代码检查(如已配置)
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# Tailwind 即时开发(随 dev 一起运行)
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署
|
||||||
|
|
||||||
|
- 推荐部署到 Vercel:
|
||||||
|
- Framework: Next.js
|
||||||
|
- Build Command: `npm run build`
|
||||||
|
- Output: `.next`
|
||||||
|
- Node Version: 使用项目/平台默认 LTS
|
||||||
|
- 也可自托管:
|
||||||
|
- 运行 `npm run build && npm run start`
|
||||||
|
|
||||||
|
## 贡献与代码风格
|
||||||
|
|
||||||
|
- 变量/函数命名清晰完整,避免缩写
|
||||||
|
- 避免深层嵌套,使用早返回
|
||||||
|
- TypeScript 明确标注导出 API 的类型
|
||||||
|
- 不捕获且吞掉错误;错误需被有意义处理
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
|
||||||
|
本仓库以 MIT 协议开源。根据业务需要可自由修改与商用。
|
||||||
|
|||||||
409
app/[lang]/contact/page.tsx
Normal file
409
app/[lang]/contact/page.tsx
Normal file
@ -0,0 +1,409 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import PageLayout from '../../../components/PageLayout';
|
||||||
|
import Icon from '../../../components/Icon';
|
||||||
|
import { content } from '../../../data/content';
|
||||||
|
import { contactContent } from '../../../data/pageContent';
|
||||||
|
|
||||||
|
export default function ContactPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const currentLang = typeof params.lang === 'string' ? params.lang : 'en';
|
||||||
|
|
||||||
|
// Form state
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
company: '',
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
});
|
||||||
|
const [formSubmitted, setFormSubmitted] = useState(false);
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
// Set client-side flag
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Validate language and redirect if invalid
|
||||||
|
useEffect(() => {
|
||||||
|
const supportedLangs = ['zh-CN', 'zh-TW', 'en', 'ko', 'ja'];
|
||||||
|
if (!supportedLangs.includes(currentLang)) {
|
||||||
|
router.push('/en/contact');
|
||||||
|
}
|
||||||
|
}, [currentLang, router]);
|
||||||
|
|
||||||
|
const currentContent = content[currentLang as keyof typeof content] || content.en;
|
||||||
|
const pageContent =
|
||||||
|
contactContent[currentLang as keyof typeof contactContent] || contactContent.en;
|
||||||
|
|
||||||
|
const handleLanguageChange = (lang: string) => {
|
||||||
|
router.push(`/${lang}/contact`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>,
|
||||||
|
) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Here you would typically send the form data to your backend
|
||||||
|
console.log('Form submitted:', formData);
|
||||||
|
setFormSubmitted(true);
|
||||||
|
|
||||||
|
// Reset form after submission
|
||||||
|
setTimeout(() => {
|
||||||
|
setFormData({
|
||||||
|
name: '',
|
||||||
|
email: '',
|
||||||
|
phone: '',
|
||||||
|
company: '',
|
||||||
|
subject: '',
|
||||||
|
message: '',
|
||||||
|
});
|
||||||
|
setFormSubmitted(false);
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout
|
||||||
|
title={pageContent.title}
|
||||||
|
description={pageContent.description}
|
||||||
|
keywords={pageContent.keywords}
|
||||||
|
currentLang={currentLang}
|
||||||
|
currentContent={currentContent}
|
||||||
|
handleLanguageChange={handleLanguageChange}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">{pageContent.heading}</h1>
|
||||||
|
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||||
|
{pageContent.subheading}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-12">
|
||||||
|
{/* Contact Information */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-8">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Contact Information'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '联系信息'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '聯繫信息'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '연락처 정보'
|
||||||
|
: '連絡先情報'}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
<Icon name="map-pin" className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<h3 className="font-semibold text-gray-900">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Address'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '地址'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '地址'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '주소'
|
||||||
|
: '住所'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
{pageContent.contactInfo.address}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
<Icon name="phone" className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<h3 className="font-semibold text-gray-900">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Phone'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '电话'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '電話'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '전화'
|
||||||
|
: '電話'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">{pageContent.contactInfo.phone}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
<Icon name="mail" className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<h3 className="font-semibold text-gray-900">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Email'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '电子邮箱'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '電子郵箱'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '이메일'
|
||||||
|
: 'メール'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">{pageContent.contactInfo.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start">
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
<Icon name="clock" className="w-6 h-6 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<h3 className="font-semibold text-gray-900">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Business Hours'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '营业时间'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '營業時間'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '영업 시간'
|
||||||
|
: '営業時間'}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">{pageContent.contactInfo.hours}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Map Placeholder */}
|
||||||
|
<div className="mt-8 bg-gray-200 rounded-lg h-64 flex items-center justify-center">
|
||||||
|
<p className="text-gray-500">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Map will be displayed here'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '地图将显示在这里'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '地圖將顯示在這裡'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '지도가 여기에 표시됩니다'
|
||||||
|
: '地図がここに表示されます'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Form */}
|
||||||
|
<div className="bg-white rounded-lg shadow-lg p-8">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
||||||
|
{pageContent.formTitle}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{!isClient ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<div className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{pageContent.formFields.name.label}
|
||||||
|
</div>
|
||||||
|
<div className="w-full px-4 py-2 border border-gray-300 rounded-md bg-gray-100">
|
||||||
|
{pageContent.formFields.name.placeholder}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
{pageContent.formFields.email.label}
|
||||||
|
</div>
|
||||||
|
<div className="w-full px-4 py-2 border border-gray-300 rounded-md bg-gray-100">
|
||||||
|
{pageContent.formFields.email.placeholder}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<div className="text-gray-500">Loading form...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : formSubmitted ? (
|
||||||
|
<div className="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-6">
|
||||||
|
<strong className="font-bold">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Thank you!'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '谢谢!'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '謝謝!'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '감사합니다!'
|
||||||
|
: 'ありがとうございます!'}
|
||||||
|
</strong>
|
||||||
|
<span className="block sm:inline">
|
||||||
|
{' '}
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Your message has been sent. We will contact you soon.'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '您的消息已发送。我们将尽快与您联系。'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '您的訊息已發送。我們將盡快與您聯繫。'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '메시지가 전송되었습니다. 곧 연락 드리겠습니다.'
|
||||||
|
: 'メッセージが送信されました。すぐにご連絡いたします。'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="space-y-6"
|
||||||
|
>
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
{pageContent.formFields.name.label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
value={formData.name}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={pageContent.formFields.name.placeholder}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
{pageContent.formFields.email.label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
value={formData.email}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={pageContent.formFields.email.placeholder}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="phone"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
{pageContent.formFields.phone.label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="phone"
|
||||||
|
name="phone"
|
||||||
|
value={formData.phone}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={pageContent.formFields.phone.placeholder}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="company"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
{pageContent.formFields.company.label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="company"
|
||||||
|
name="company"
|
||||||
|
value={formData.company}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={pageContent.formFields.company.placeholder}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="subject"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
{pageContent.formFields.subject.label}
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="subject"
|
||||||
|
name="subject"
|
||||||
|
value={formData.subject}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
required
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
{pageContent.formFields.subject.placeholder}
|
||||||
|
</option>
|
||||||
|
{pageContent.formFields.subject.options.map(
|
||||||
|
(option, index) => (
|
||||||
|
<option key={index} value={option}>
|
||||||
|
{option}
|
||||||
|
</option>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="message"
|
||||||
|
className="block text-sm font-medium text-gray-700 mb-1"
|
||||||
|
>
|
||||||
|
{pageContent.formFields.message.label}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
name="message"
|
||||||
|
value={formData.message}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder={pageContent.formFields.message.placeholder}
|
||||||
|
required
|
||||||
|
rows={5}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-600 text-white px-6 py-3 rounded-md font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
{pageContent.formFields.submit}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
app/[lang]/layout.tsx
Normal file
66
app/[lang]/layout.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
interface LanguageLayoutProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
params: { lang: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ lang: string }>;
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
// Map of language-specific metadata
|
||||||
|
const metadataByLang: Record<string, Metadata> = {
|
||||||
|
en: {
|
||||||
|
title: 'AWS Cloud Services Partner - Professional Cloud Computing Solutions',
|
||||||
|
description:
|
||||||
|
'Providing comprehensive AWS cloud services including EC2, RDS, S3, AI/ML and more',
|
||||||
|
keywords: 'AWS, cloud computing, EC2, RDS, S3, cloud services, AI, machine learning',
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
title: 'AWS云服务合作伙伴 - 专业云计算解决方案',
|
||||||
|
description: '提供AWS云服务器、数据库、存储、AI/ML等全方位云计算服务',
|
||||||
|
keywords: 'AWS,云服务器,云计算,数据库,存储,AI,机器学习',
|
||||||
|
},
|
||||||
|
'zh-TW': {
|
||||||
|
title: 'AWS雲端服務合作夥伴 - 專業雲端運算解決方案',
|
||||||
|
description: '提供AWS雲端伺服器、資料庫、儲存、AI/ML等全方位雲端運算服務',
|
||||||
|
keywords: 'AWS,雲端伺服器,雲端運算,資料庫,儲存,AI,機器學習',
|
||||||
|
},
|
||||||
|
ko: {
|
||||||
|
title: 'AWS 클라우드 서비스 파트너 - 전문 클라우드 컴퓨팅 솔루션',
|
||||||
|
description: 'EC2, RDS, S3, AI/ML 등 포괄적인 AWS 클라우드 서비스 제공',
|
||||||
|
keywords: 'AWS,클라우드 컴퓨팅,EC2,RDS,S3,클라우드 서비스,AI,머신러닝',
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
title: 'AWSクラウドサービスパートナー - プロフェッショナルクラウドコンピューティングソリューション',
|
||||||
|
description: 'EC2、RDS、S3、AI/MLなど包括的なAWSクラウドサービスを提供',
|
||||||
|
keywords: 'AWS,クラウドコンピューティング,EC2,RDS,S3,クラウドサービス,AI,機械学習',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Await params before using its properties
|
||||||
|
const { lang } = await params;
|
||||||
|
|
||||||
|
// Get metadata for the current language or fall back to English
|
||||||
|
const metadata = metadataByLang[lang] || metadataByLang.en;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...metadata,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
en: '/en',
|
||||||
|
'zh-CN': '/zh-CN',
|
||||||
|
'zh-TW': '/zh-TW',
|
||||||
|
ko: '/ko',
|
||||||
|
ja: '/ja',
|
||||||
|
'x-default': '/en',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LanguageLayout({ children, params }: LanguageLayoutProps) {
|
||||||
|
return <>{children}</>;
|
||||||
|
}
|
||||||
13
app/[lang]/page.tsx
Normal file
13
app/[lang]/page.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import HomeClient from '../../components/HomeClient';
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const supportedLangs = ['en', 'zh-CN', 'zh-TW', 'ko', 'ja'];
|
||||||
|
return supportedLangs.map((lang) => ({
|
||||||
|
lang: lang,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Page({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
|
const { lang } = await params;
|
||||||
|
return <HomeClient lang={lang} />;
|
||||||
|
}
|
||||||
109
app/[lang]/products/page.tsx
Normal file
109
app/[lang]/products/page.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import PageLayout from '../../../components/PageLayout';
|
||||||
|
import Icon from '../../../components/Icon';
|
||||||
|
import { content } from '../../../data/content';
|
||||||
|
import { productsContent } from '../../../data/pageContent';
|
||||||
|
|
||||||
|
export default function ProductsPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const currentLang = typeof params.lang === 'string' ? params.lang : 'en';
|
||||||
|
|
||||||
|
// Validate language and redirect if invalid
|
||||||
|
useEffect(() => {
|
||||||
|
const supportedLangs = ['zh-CN', 'zh-TW', 'en', 'ko', 'ja'];
|
||||||
|
if (!supportedLangs.includes(currentLang)) {
|
||||||
|
router.push('/en/products');
|
||||||
|
}
|
||||||
|
}, [currentLang, router]);
|
||||||
|
|
||||||
|
const currentContent = content[currentLang as keyof typeof content] || content.en;
|
||||||
|
const pageContent =
|
||||||
|
productsContent[currentLang as keyof typeof productsContent] || productsContent.en;
|
||||||
|
|
||||||
|
const handleLanguageChange = (lang: string) => {
|
||||||
|
router.push(`/${lang}/products`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout
|
||||||
|
title={pageContent.title}
|
||||||
|
description={pageContent.description}
|
||||||
|
keywords={pageContent.keywords}
|
||||||
|
currentLang={currentLang}
|
||||||
|
currentContent={currentContent}
|
||||||
|
handleLanguageChange={handleLanguageChange}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">{pageContent.heading}</h1>
|
||||||
|
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||||
|
{pageContent.subheading}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Categories */}
|
||||||
|
<div className="space-y-16">
|
||||||
|
{pageContent.categories.map((category, index) => (
|
||||||
|
<div key={index} className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
|
<div className="bg-blue-600 text-white px-6 py-4">
|
||||||
|
<h2 className="text-2xl font-bold">{category.title}</h2>
|
||||||
|
<p className="mt-1">{category.description}</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid md:grid-cols-3 gap-6 p-6">
|
||||||
|
{category.products.map((product, productIndex) => (
|
||||||
|
<div
|
||||||
|
key={productIndex}
|
||||||
|
className="border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="w-12 h-12 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center mb-4">
|
||||||
|
<Icon name={product.icon} />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600">{product.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<div className="mt-16 bg-gradient-to-r from-blue-600 to-indigo-700 rounded-lg shadow-xl text-white p-8 text-center">
|
||||||
|
<h2 className="text-3xl font-bold mb-4">{currentContent.hero.cta}</h2>
|
||||||
|
<p className="text-lg mb-6 max-w-2xl mx-auto">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Contact our AWS experts to discuss your specific cloud computing needs and requirements.'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '联系我们的AWS专家,讨论您的特定云计算需求和要求。'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '聯繫我們的AWS專家,討論您的特定雲計算需求和要求。'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? 'AWS 전문가에게 문의하여 특정 클라우드 컴퓨팅 요구 사항에 대해 논의하세요.'
|
||||||
|
: '特定のクラウドコンピューティングのニーズと要件について、AWS専門家にお問い合わせください。'}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push(`/${currentLang}/contact`)}
|
||||||
|
className="bg-white text-blue-600 px-8 py-3 rounded-lg font-medium hover:bg-blue-50 transition-colors"
|
||||||
|
>
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Contact Us'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '联系我们'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '聯繫我們'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '문의하기'
|
||||||
|
: 'お問い合わせ'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
135
app/[lang]/solutions/page.tsx
Normal file
135
app/[lang]/solutions/page.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import PageLayout from '../../../components/PageLayout';
|
||||||
|
import Icon from '../../../components/Icon';
|
||||||
|
import { content } from '../../../data/content';
|
||||||
|
import { solutionsContent } from '../../../data/pageContent';
|
||||||
|
|
||||||
|
export default function SolutionsPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const params = useParams();
|
||||||
|
const currentLang = typeof params.lang === 'string' ? params.lang : 'en';
|
||||||
|
|
||||||
|
// Validate language and redirect if invalid
|
||||||
|
useEffect(() => {
|
||||||
|
const supportedLangs = ['zh-CN', 'zh-TW', 'en', 'ko', 'ja'];
|
||||||
|
if (!supportedLangs.includes(currentLang)) {
|
||||||
|
router.push('/en/solutions');
|
||||||
|
}
|
||||||
|
}, [currentLang, router]);
|
||||||
|
|
||||||
|
const currentContent = content[currentLang as keyof typeof content] || content.en;
|
||||||
|
const pageContent =
|
||||||
|
solutionsContent[currentLang as keyof typeof solutionsContent] || solutionsContent.en;
|
||||||
|
|
||||||
|
const handleLanguageChange = (lang: string) => {
|
||||||
|
router.push(`/${lang}/solutions`);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageLayout
|
||||||
|
title={pageContent.title}
|
||||||
|
description={pageContent.description}
|
||||||
|
keywords={pageContent.keywords}
|
||||||
|
currentLang={currentLang}
|
||||||
|
currentContent={currentContent}
|
||||||
|
handleLanguageChange={handleLanguageChange}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
{/* Page Header */}
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h1 className="text-4xl font-bold text-gray-900 mb-4">{pageContent.heading}</h1>
|
||||||
|
<p className="text-xl text-gray-600 max-w-3xl mx-auto">
|
||||||
|
{pageContent.subheading}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Industries Grid */}
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{pageContent.industries.map((industry, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-white rounded-lg shadow-lg overflow-hidden hover:shadow-xl transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="bg-gradient-to-r from-blue-600 to-indigo-700 p-6 text-white">
|
||||||
|
<div className="w-12 h-12 bg-white/20 rounded-lg flex items-center justify-center mb-4">
|
||||||
|
<Icon name={industry.icon} className="w-6 h-6 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold">{industry.name}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<p className="text-gray-600 mb-6">{industry.description}</p>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h4 className="font-semibold text-gray-900">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Key Benefits:'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '主要优势:'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '主要優勢:'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '주요 이점:'
|
||||||
|
: '主な利点:'}
|
||||||
|
</h4>
|
||||||
|
{industry.benefits.map((benefit, benefitIndex) => (
|
||||||
|
<div key={benefitIndex} className="flex items-start">
|
||||||
|
<div className="flex-shrink-0 mt-1">
|
||||||
|
<Icon
|
||||||
|
name="check"
|
||||||
|
className="w-5 h-5 text-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="ml-2 text-gray-600">{benefit}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<div className="mt-16 bg-gray-100 rounded-lg p-8 text-center">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Need a customized solution for your industry?'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '需要为您的行业定制解决方案?'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '需要為您的行業定制解決方案?'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '귀하의 산업에 맞는 맞춤형 솔루션이 필요하신가요?'
|
||||||
|
: 'あなたの業界向けのカスタマイズされたソリューションが必要ですか?'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-gray-600 mb-6 max-w-2xl mx-auto">
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Our AWS experts can help you design and implement a cloud solution tailored to your specific industry requirements.'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '我们的AWS专家可以帮助您设计和实施针对特定行业需求的云解决方案。'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '我們的AWS專家可以幫助您設計和實施針對特定行業需求的雲解決方案。'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '당사의 AWS 전문가가 귀하의 특정 산업 요구 사항에 맞게 설계된 클라우드 솔루션을 설계하고 구현하는 데 도움을 드릴 수 있습니다.'
|
||||||
|
: '当社のAWS専門家は、特定の業界要件に合わせたクラウドソリューションの設計と実装をお手伝いします。'}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => router.push(`/${currentLang}/contact`)}
|
||||||
|
className="bg-blue-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
{currentLang === 'en'
|
||||||
|
? 'Get in Touch'
|
||||||
|
: currentLang === 'zh-CN'
|
||||||
|
? '联系我们'
|
||||||
|
: currentLang === 'zh-TW'
|
||||||
|
? '聯繫我們'
|
||||||
|
: currentLang === 'ko'
|
||||||
|
? '연락하기'
|
||||||
|
: 'お問い合わせ'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
app/favicon.ico
Normal file
BIN
app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 175 KiB |
88
app/globals.css
Normal file
88
app/globals.css
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 0, 0, 0;
|
||||||
|
--background-start-rgb: 214, 219, 220;
|
||||||
|
--background-end-rgb: 255, 255, 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--foreground-rgb: 255, 255, 255;
|
||||||
|
--background-start-rgb: 0, 0, 0;
|
||||||
|
--background-end-rgb: 0, 0, 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-balance {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 0 0% 3.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 0 0% 3.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 0 0% 3.9%;
|
||||||
|
--primary: 0 0% 9%;
|
||||||
|
--primary-foreground: 0 0% 98%;
|
||||||
|
--secondary: 0 0% 96.1%;
|
||||||
|
--secondary-foreground: 0 0% 9%;
|
||||||
|
--muted: 0 0% 96.1%;
|
||||||
|
--muted-foreground: 0 0% 45.1%;
|
||||||
|
--accent: 0 0% 96.1%;
|
||||||
|
--accent-foreground: 0 0% 9%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 89.8%;
|
||||||
|
--input: 0 0% 89.8%;
|
||||||
|
--ring: 0 0% 3.9%;
|
||||||
|
--chart-1: 12 76% 61%;
|
||||||
|
--chart-2: 173 58% 39%;
|
||||||
|
--chart-3: 197 37% 24%;
|
||||||
|
--chart-4: 43 74% 66%;
|
||||||
|
--chart-5: 27 87% 67%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--background: 0 0% 3.9%;
|
||||||
|
--foreground: 0 0% 98%;
|
||||||
|
--card: 0 0% 3.9%;
|
||||||
|
--card-foreground: 0 0% 98%;
|
||||||
|
--popover: 0 0% 3.9%;
|
||||||
|
--popover-foreground: 0 0% 98%;
|
||||||
|
--primary: 0 0% 98%;
|
||||||
|
--primary-foreground: 0 0% 9%;
|
||||||
|
--secondary: 0 0% 14.9%;
|
||||||
|
--secondary-foreground: 0 0% 98%;
|
||||||
|
--muted: 0 0% 14.9%;
|
||||||
|
--muted-foreground: 0 0% 63.9%;
|
||||||
|
--accent: 0 0% 14.9%;
|
||||||
|
--accent-foreground: 0 0% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 0 0% 98%;
|
||||||
|
--border: 0 0% 14.9%;
|
||||||
|
--input: 0 0% 14.9%;
|
||||||
|
--ring: 0 0% 83.1%;
|
||||||
|
--chart-1: 220 70% 50%;
|
||||||
|
--chart-2: 160 60% 45%;
|
||||||
|
--chart-3: 30 80% 55%;
|
||||||
|
--chart-4: 280 65% 60%;
|
||||||
|
--chart-5: 340 75% 55%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
app/layout.tsx
Normal file
91
app/layout.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import './globals.css';
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { lang: string };
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
const domain = 'https://your-domain.com';
|
||||||
|
// Map of language-specific metadata
|
||||||
|
const metadataByLang: Record<string, Metadata> = {
|
||||||
|
en: {
|
||||||
|
title: 'AWS Cloud Services Partner - Professional Cloud Computing Solutions',
|
||||||
|
description:
|
||||||
|
'Providing comprehensive AWS cloud services including EC2, RDS, S3, AI/ML and more',
|
||||||
|
keywords: 'AWS, cloud computing, EC2, RDS, S3, cloud services, AI, machine learning',
|
||||||
|
},
|
||||||
|
'zh-CN': {
|
||||||
|
title: 'AWS云服务合作伙伴 - 专业云计算解决方案',
|
||||||
|
description: '提供AWS云服务器、数据库、存储、AI/ML等全方位云计算服务',
|
||||||
|
keywords: 'AWS,云服务器,云计算,数据库,存储,AI,机器学习',
|
||||||
|
},
|
||||||
|
'zh-TW': {
|
||||||
|
title: 'AWS雲端服務合作夥伴 - 專業雲端運算解決方案',
|
||||||
|
description: '提供AWS雲端伺服器、資料庫、儲存、AI/ML等全方位雲端運算服務',
|
||||||
|
keywords: 'AWS,雲端伺服器,雲端運算,資料庫,儲存,AI,機器學習',
|
||||||
|
},
|
||||||
|
ko: {
|
||||||
|
title: 'AWS 클라우드 서비스 파트너 - 전문 클라우드 컴퓨팅 솔루션',
|
||||||
|
description: 'EC2, RDS, S3, AI/ML 등 포괄적인 AWS 클라우드 서비스 제공',
|
||||||
|
keywords: 'AWS,클라우드 컴퓨팅,EC2,RDS,S3,클라우드 서비스,AI,머신러닝',
|
||||||
|
},
|
||||||
|
ja: {
|
||||||
|
title: 'AWSクラウドサービスパートナー - プロフェッショナルクラウドコンピューティングソリューション',
|
||||||
|
description: 'EC2、RDS、S3、AI/MLなど包括的なAWSクラウドサービスを提供',
|
||||||
|
keywords: 'AWS,クラウドコンピューティング,EC2,RDS,S3,クラウドサービス,AI,機械学習',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get metadata for the current language or fall back to English
|
||||||
|
const metadata = metadataByLang[params.lang] || metadataByLang.en;
|
||||||
|
|
||||||
|
return {
|
||||||
|
metadataBase: new URL(domain),
|
||||||
|
...metadata,
|
||||||
|
alternates: {
|
||||||
|
languages: {
|
||||||
|
en: '/en',
|
||||||
|
'zh-CN': '/zh-CN',
|
||||||
|
'zh-TW': '/zh-TW',
|
||||||
|
ko: '/ko',
|
||||||
|
ja: '/ja',
|
||||||
|
'x-default': '/en',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: metadata.title || undefined,
|
||||||
|
description: metadata.description || undefined,
|
||||||
|
url: `${domain}/${params.lang}`,
|
||||||
|
siteName: 'AWS Cloud Services Partner',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: '/og-image.png', // Must be an absolute URL
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: params.lang,
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
title: metadata.title || undefined,
|
||||||
|
description: metadata.description || undefined,
|
||||||
|
images: ['/og-image.png'], // Must be an absolute URL
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: Readonly<{ children: React.ReactNode; params: { lang: string } }>) {
|
||||||
|
return (
|
||||||
|
<html lang={params.lang}>
|
||||||
|
<body>
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
app/robots.ts
Normal file
14
app/robots.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
const domain = 'https://your-domain.com';
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
return {
|
||||||
|
rules: {
|
||||||
|
userAgent: '*',
|
||||||
|
allow: '/',
|
||||||
|
disallow: '/api/',
|
||||||
|
},
|
||||||
|
sitemap: `${domain}/sitemap.xml`,
|
||||||
|
};
|
||||||
|
}
|
||||||
18
app/sitemap.ts
Normal file
18
app/sitemap.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
const defaultLang = 'en';
|
||||||
|
const domain = 'https://your-domain.com';
|
||||||
|
const supportedLangs = ['en', 'zh-CN', 'zh-TW', 'ko', 'ja'];
|
||||||
|
|
||||||
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
|
const routes = ['', '/products', '/solutions', '/contact'];
|
||||||
|
|
||||||
|
const sitemapEntries = supportedLangs.flatMap((lang) => {
|
||||||
|
return routes.map((route) => ({
|
||||||
|
url: `${domain}/${lang}${route}`,
|
||||||
|
lastModified: new Date(),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
return sitemapEntries;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user