x
This commit is contained in:
1 parent
f7aa4d099f
commit
ab3e53c4da
33 files changed
+290
-69
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
|
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||||
@@ -38,9 +39,12 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
|
|||||||
const pageResult = await getCmsPageResult(slug, lang)
|
const pageResult = await getCmsPageResult(slug, lang)
|
||||||
if (!pageResult.ok || !pageResult.data.isPublished) return { title: 'Page Not Found | HomAroma' }
|
if (!pageResult.ok || !pageResult.data.isPublished) return { title: 'Page Not Found | HomAroma' }
|
||||||
|
|
||||||
return {
|
return generateSeoMetadata({
|
||||||
title: `${pageResult.data.title} | HomAroma`,
|
title: `${pageResult.data.title} | HomAroma`,
|
||||||
}
|
description: `HomAroma - ${pageResult.data.title}`,
|
||||||
|
path: `/${slug}`,
|
||||||
|
lang,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CmsCustomPage({ params }: { params: Promise<{ slug: string, lang: string }> }) {
|
export default async function CmsCustomPage({ params }: { params: Promise<{ slug: string, lang: string }> }) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Link from '@/components/LocalizedLink'
|
import Link from '@/components/LocalizedLink'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
|
|
||||||
const PILLARS = [
|
const PILLARS = [
|
||||||
{
|
{
|
||||||
@@ -25,9 +26,14 @@ const STATS = [
|
|||||||
{ value: '100%', label: 'Natural Extracts' },
|
{ value: '100%', label: 'Natural Extracts' },
|
||||||
]
|
]
|
||||||
|
|
||||||
export const metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
title: 'About Us | HomAroma',
|
const { lang } = await params;
|
||||||
description: 'Born from a passion for stillness. HomAroma began in 2019 as a Bangkok studio crafting botanical aroma blends.',
|
return generateSeoMetadata({
|
||||||
|
title: 'About Us | HomAroma',
|
||||||
|
description: 'Born from a passion for stillness. HomAroma began in 2019 as a Bangkok studio crafting botanical aroma blends.',
|
||||||
|
path: '/about',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from '@/components/LocalizedLink'
|
import Link from '@/components/LocalizedLink'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
import { ArrowLeft } from 'lucide-react'
|
import { ArrowLeft } from 'lucide-react'
|
||||||
import type { Article } from '@homaroma/types'
|
import type { Article } from '@homaroma/types'
|
||||||
|
|
||||||
@@ -13,20 +14,22 @@ async function getArticle(slug: string): Promise<Article | null> {
|
|||||||
} catch { return null }
|
} catch { return null }
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string, lang: string }> }): Promise<Metadata> {
|
||||||
const { slug } = await params
|
const { slug, lang } = await params
|
||||||
const article = await getArticle(slug)
|
const article = await getArticle(slug)
|
||||||
if (!article) return { title: 'Article Not Found | HomAroma' }
|
if (!article) return { title: 'Article Not Found | HomAroma' }
|
||||||
return {
|
return generateSeoMetadata({
|
||||||
title: `${article.title} | HomAroma Scent Guide`,
|
title: `${article.title} | HomAroma Scent Guide`,
|
||||||
description: article.content.replace(/<[^>]+>/g, '').slice(0, 160),
|
description: article.content.replace(/<[^>]+>/g, '').slice(0, 160),
|
||||||
|
path: `/article/${slug}`,
|
||||||
|
lang,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: article.title,
|
title: article.title,
|
||||||
images: article.coverImage ? [article.coverImage] : [],
|
images: article.coverImage ? [article.coverImage] : [],
|
||||||
type: 'article',
|
type: 'article',
|
||||||
publishedTime: article.createdAt,
|
publishedTime: article.createdAt,
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ArticleReaderPage({ params }: { params: Promise<{ slug: string }> }) {
|
export default async function ArticleReaderPage({ params }: { params: Promise<{ slug: string }> }) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from '@/components/LocalizedLink'
|
import Link from '@/components/LocalizedLink'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
import type { Category, Product } from '@homaroma/types'
|
import type { Category, Product } from '@homaroma/types'
|
||||||
import { getCategoryImage } from '@homaroma/types'
|
import { getCategoryImage } from '@homaroma/types'
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
@@ -26,10 +27,12 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
|||||||
const result = await getCategoryProducts(id, lang)
|
const result = await getCategoryProducts(id, lang)
|
||||||
const categoryName = result.ok && result.data.category ? resolveLocalized(result.data.category.name, lang) : ''
|
const categoryName = result.ok && result.data.category ? resolveLocalized(result.data.category.name, lang) : ''
|
||||||
|
|
||||||
return {
|
return generateSeoMetadata({
|
||||||
title: categoryName ? `${categoryName} | HomAroma` : 'Category | HomAroma',
|
title: categoryName ? `${categoryName} | HomAroma` : 'Category | HomAroma',
|
||||||
description: categoryName ? `Shop ${categoryName} products from HomAroma.` : 'Shop HomAroma category products.',
|
description: categoryName ? `Shop ${categoryName} products from HomAroma.` : 'Shop HomAroma category products.',
|
||||||
}
|
path: `/category/${id}`,
|
||||||
|
lang,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CategoryPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
export default async function CategoryPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { CreditCard } from 'lucide-react'
|
import { CreditCard } from 'lucide-react'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
|
|
||||||
import CheckoutClient from '@/components/CheckoutClient'
|
import CheckoutClient from '@/components/CheckoutClient'
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
@@ -6,9 +7,14 @@ import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
|||||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||||
|
|
||||||
// lang is extracted from params
|
// lang is extracted from params
|
||||||
export const metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
title: 'Checkout | HomAroma',
|
const { lang } = await params;
|
||||||
description: 'ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ HomAroma',
|
return generateSeoMetadata({
|
||||||
|
title: 'Checkout | HomAroma',
|
||||||
|
description: 'ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ HomAroma',
|
||||||
|
path: '/checkout',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CheckoutPage({ params }: { params: Promise<{ lang: string }> }) {
|
export default async function CheckoutPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from '@/components/LocalizedLink'
|
import Link from '@/components/LocalizedLink'
|
||||||
import { Ticket, ArrowRight } from 'lucide-react'
|
import { Ticket, ArrowRight } from 'lucide-react'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import { getActiveCouponsResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
import { getActiveCouponsResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||||
@@ -12,9 +13,14 @@ function formatDiscount(type: string, value: number) {
|
|||||||
return `${value}`
|
return `${value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
title: 'โปรโมชั่นและคูปองส่วนลด | HomAroma',
|
const { lang } = await params;
|
||||||
description: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก HomAroma สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ',
|
return generateSeoMetadata({
|
||||||
|
title: 'โปรโมชั่นและคูปองส่วนลด | HomAroma',
|
||||||
|
description: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก HomAroma สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ',
|
||||||
|
path: '/coupons',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function CouponsPage({ params }: { params: Promise<{ lang: string }> }) {
|
export default async function CouponsPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
import '../globals.css';
|
import '../globals.css';
|
||||||
import Navbar from '@/components/Navbar';
|
import Navbar from '@/components/Navbar';
|
||||||
import Footer from '@/components/Footer';
|
import Footer from '@/components/Footer';
|
||||||
@@ -6,6 +7,7 @@ import ClientProviders from '@/components/ClientProviders';
|
|||||||
import { API_BASE } from '@/lib/api';
|
import { API_BASE } from '@/lib/api';
|
||||||
import type { MegaMenuPayload } from '@/components/navigation/menuTypes';
|
import type { MegaMenuPayload } from '@/components/navigation/menuTypes';
|
||||||
import type { MenuItem, StoreSettings } from '@homaroma/types';
|
import type { MenuItem, StoreSettings } from '@homaroma/types';
|
||||||
|
import { isSupportedLocale } from '@/lib/locales';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'HomAroma',
|
title: 'HomAroma',
|
||||||
@@ -82,6 +84,10 @@ export default async function RootLayout({
|
|||||||
}) {
|
}) {
|
||||||
const { lang } = await params;
|
const { lang } = await params;
|
||||||
|
|
||||||
|
if (!isSupportedLocale(lang)) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
const [categories, initialMenu, footerMenus, settings] = await Promise.all([
|
const [categories, initialMenu, footerMenus, settings] = await Promise.all([
|
||||||
getCategories(lang),
|
getCategories(lang),
|
||||||
getMegaMenu(lang),
|
getMegaMenu(lang),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from '@/components/LocalizedLink'
|
import Link from '@/components/LocalizedLink'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
import { CheckCircle2, Clock, PackageCheck, Truck } from 'lucide-react'
|
import { CheckCircle2, Clock, PackageCheck, Truck } from 'lucide-react'
|
||||||
import { formatPrice } from '@homaroma/types'
|
import { formatPrice } from '@homaroma/types'
|
||||||
|
|
||||||
@@ -34,16 +35,18 @@ function statusTone(status: string) {
|
|||||||
return 'bg-gray-50 text-gray-700 border-gray-100'
|
return 'bg-gray-50 text-gray-700 border-gray-100'
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise<Metadata> {
|
||||||
const { id } = await params
|
const { id, lang } = await params
|
||||||
const result = await getOrderResult(id)
|
const result = await getOrderResult(id)
|
||||||
|
|
||||||
if (!result.ok) return { title: 'Order Not Found | HomAroma' }
|
if (!result.ok) return { title: 'Order Not Found | HomAroma' }
|
||||||
|
|
||||||
return {
|
return generateSeoMetadata({
|
||||||
title: `Order ${orderLabel(result.data.id)} | HomAroma`,
|
title: `Order ${orderLabel(result.data.id)} | HomAroma`,
|
||||||
description: `ตรวจสอบสถานะคำสั่งซื้อ ${orderLabel(result.data.id)} จาก HomAroma`,
|
description: `ตรวจสอบสถานะคำสั่งซื้อ ${orderLabel(result.data.id)} จาก HomAroma`,
|
||||||
}
|
path: `/order/${id}`,
|
||||||
|
lang,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function OrderStatusPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
export default async function OrderStatusPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import { getCategories, getCmsPage } from '@/lib/cms'
|
import { getCategories, getCmsPage } from '@/lib/cms'
|
||||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||||
@@ -11,10 +12,12 @@ export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
|||||||
const { lang } = await params
|
const { lang } = await params
|
||||||
const page = await getCmsPage('home', lang)
|
const page = await getCmsPage('home', lang)
|
||||||
|
|
||||||
return {
|
return generateSeoMetadata({
|
||||||
title: page?.title ? `${page.title} | HomAroma` : 'HomAroma',
|
title: page?.title ? `${page.title} | HomAroma` : 'HomAroma',
|
||||||
description: 'Premium aromatherapy, home fragrance, and botanical scent products from HomAroma.',
|
description: 'Premium aromatherapy, home fragrance, and botanical scent products from HomAroma.',
|
||||||
}
|
path: '',
|
||||||
|
lang,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function HomePage({ params }: Props) {
|
export default async function HomePage({ params }: Props) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Link from '@/components/LocalizedLink'
|
import Link from '@/components/LocalizedLink'
|
||||||
import { notFound } from 'next/navigation'
|
import { notFound } from 'next/navigation'
|
||||||
import type { Metadata } from 'next'
|
import type { Metadata } from 'next'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
import { getCategoryImage, type Product } from '@homaroma/types'
|
import { getCategoryImage, type Product } from '@homaroma/types'
|
||||||
import AddToCartSection from '@/components/AddToCartSection'
|
import AddToCartSection from '@/components/AddToCartSection'
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
@@ -25,15 +26,17 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
|||||||
const categoryName = resolveCategoryName(product.category, lang)
|
const categoryName = resolveCategoryName(product.category, lang)
|
||||||
const description = resolveLocalized(product.description, lang)
|
const description = resolveLocalized(product.description, lang)
|
||||||
|
|
||||||
return {
|
return generateSeoMetadata({
|
||||||
title: `${productName} ${categoryName ? `| ${categoryName}` : ''} | HomAroma`,
|
title: `${productName} ${categoryName ? `| ${categoryName}` : ''} | HomAroma`,
|
||||||
description: description || `Premium ${categoryName} from HomAroma.`,
|
description: description || `Premium ${categoryName} from HomAroma.`,
|
||||||
|
path: `/products/${id}`,
|
||||||
|
lang,
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: productName,
|
title: productName,
|
||||||
description,
|
description,
|
||||||
images: product.images?.[0]?.url ? [product.images[0].url] : [],
|
images: product.images?.[0]?.url ? [product.images[0].url] : [],
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ProductDetailPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
export default async function ProductDetailPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { User } from 'lucide-react'
|
import { User } from 'lucide-react'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
|
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import ProfileClient from '@/components/ProfileClient'
|
import ProfileClient from '@/components/ProfileClient'
|
||||||
@@ -6,9 +7,14 @@ import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
|||||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||||
|
|
||||||
// lang is extracted from params
|
// lang is extracted from params
|
||||||
export const metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
title: 'โปรไฟล์สมาชิก | HomAroma',
|
const { lang } = await params;
|
||||||
description: 'จัดการโปรไฟล์สมาชิก HomAroma ประวัติคำสั่งซื้อ คะแนนสะสม และสมุดที่อยู่จัดส่ง',
|
return generateSeoMetadata({
|
||||||
|
title: 'โปรไฟล์สมาชิก | HomAroma',
|
||||||
|
description: 'จัดการโปรไฟล์สมาชิก HomAroma ประวัติคำสั่งซื้อ คะแนนสะสม และสมุดที่อยู่จัดส่ง',
|
||||||
|
path: '/profile',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ProfilePage({ params }: { params: Promise<{ lang: string }> }) {
|
export default async function ProfilePage({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
import Link from '@/components/LocalizedLink'
|
import Link from '@/components/LocalizedLink'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import { getArticlesResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
import { getArticlesResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||||
|
|
||||||
// lang is extracted from params
|
// lang is extracted from params
|
||||||
export const metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
title: 'Scent Guide | HomAroma',
|
const { lang } = await params;
|
||||||
description: 'Explore our collection of aroma articles, guides, and sensory knowledge to find your perfect scent.',
|
return generateSeoMetadata({
|
||||||
|
title: 'Scent Guide | HomAroma',
|
||||||
|
description: 'Explore our collection of aroma articles, guides, and sensory knowledge to find your perfect scent.',
|
||||||
|
path: '/scent-guide',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ScentGuidePage({ params }: { params: Promise<{ lang: string }> }) {
|
export default async function ScentGuidePage({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
import Link from '@/components/LocalizedLink'
|
import Link from '@/components/LocalizedLink'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import ProductCard from '@/components/ProductCard'
|
import ProductCard from '@/components/ProductCard'
|
||||||
import { getCategoriesResult, getCmsPageResult, getProductsResult } from '@/lib/cms'
|
import { getCategoriesResult, getCmsPageResult, getProductsResult } from '@/lib/cms'
|
||||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||||
import { resolveLocalized } from '@/lib/localization'
|
import { resolveLocalized } from '@/lib/localization'
|
||||||
|
|
||||||
export const metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
title: 'Search Products | HomAroma',
|
const { lang } = await params;
|
||||||
description: 'Search HomAroma products by scent, category, or keyword.',
|
return generateSeoMetadata({
|
||||||
|
title: 'Search Products | HomAroma',
|
||||||
|
description: 'Search HomAroma products by scent, category, or keyword.',
|
||||||
|
path: '/search',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function SearchPage({ searchParams, params }: { searchParams: Promise<{ q?: string | string[] }>, params: Promise<{ lang: string }> }) {
|
export default async function SearchPage({ searchParams, params }: { searchParams: Promise<{ q?: string | string[] }>, params: Promise<{ lang: string }> }) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Link from '@/components/LocalizedLink'
|
import Link from '@/components/LocalizedLink'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import ProductCard from '@/components/ProductCard'
|
import ProductCard from '@/components/ProductCard'
|
||||||
import ShopCouponBridge from '@/components/ShopCouponBridge'
|
import ShopCouponBridge from '@/components/ShopCouponBridge'
|
||||||
@@ -7,9 +8,14 @@ import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
|||||||
import { resolveLocalized } from '@/lib/localization'
|
import { resolveLocalized } from '@/lib/localization'
|
||||||
|
|
||||||
// lang is extracted from params
|
// lang is extracted from params
|
||||||
export const metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
title: 'Shop Aroma Products | HomAroma',
|
const { lang } = await params;
|
||||||
description: 'Browse HomAroma aroma oils, reed diffusers, room sprays, and premium scent products.',
|
return generateSeoMetadata({
|
||||||
|
title: 'Shop Aroma Products | HomAroma',
|
||||||
|
description: 'Browse HomAroma aroma oils, reed diffusers, room sprays, and premium scent products.',
|
||||||
|
path: '/shop',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ShopPage({ searchParams, params }: { searchParams: Promise<{ coupon?: string | string[] }>, params: Promise<{ lang: string }> }) {
|
export default async function ShopPage({ searchParams, params }: { searchParams: Promise<{ coupon?: string | string[] }>, params: Promise<{ lang: string }> }) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MapPin } from 'lucide-react'
|
import { MapPin } from 'lucide-react'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
|
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import TrackOrderClient from '@/components/TrackOrderClient'
|
import TrackOrderClient from '@/components/TrackOrderClient'
|
||||||
@@ -6,9 +7,14 @@ import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
|||||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||||
|
|
||||||
// lang is extracted from params
|
// lang is extracted from params
|
||||||
export const metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
title: 'ติดตามคำสั่งซื้อ | HomAroma',
|
const { lang } = await params;
|
||||||
description: 'ตรวจสอบสถานะคำสั่งซื้อ HomAroma ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ',
|
return generateSeoMetadata({
|
||||||
|
title: 'ติดตามคำสั่งซื้อ | HomAroma',
|
||||||
|
description: 'ตรวจสอบสถานะคำสั่งซื้อ HomAroma ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ',
|
||||||
|
path: '/track',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function TrackPage({ params }: { params: Promise<{ lang: string }> }) {
|
export default async function TrackPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Heart } from 'lucide-react'
|
import { Heart } from 'lucide-react'
|
||||||
|
import { generateSeoMetadata } from '@/lib/seo'
|
||||||
|
|
||||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||||
import WishlistClient from '@/components/WishlistClient'
|
import WishlistClient from '@/components/WishlistClient'
|
||||||
@@ -6,9 +7,14 @@ import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
|||||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||||
|
|
||||||
// lang is extracted from params
|
// lang is extracted from params
|
||||||
export const metadata = {
|
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
title: 'Wishlist | HomAroma',
|
const { lang } = await params;
|
||||||
description: 'รายการสินค้าที่คุณบันทึกไว้ใน Wishlist ของ HomAroma',
|
return generateSeoMetadata({
|
||||||
|
title: 'Wishlist | HomAroma',
|
||||||
|
description: 'รายการสินค้าที่คุณบันทึกไว้ใน Wishlist ของ HomAroma',
|
||||||
|
path: '/wishlist',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function WishlistPage({ params }: { params: Promise<{ lang: string }> }) {
|
export default async function WishlistPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
|
|||||||
@@ -7,7 +7,16 @@ export default function robots(): MetadataRoute.Robots {
|
|||||||
rules: {
|
rules: {
|
||||||
userAgent: '*',
|
userAgent: '*',
|
||||||
allow: '/',
|
allow: '/',
|
||||||
disallow: ['/api/', '/_next/', '/profile/', '/checkout/', '/order/'],
|
disallow: [
|
||||||
|
'/api/',
|
||||||
|
'/_next/',
|
||||||
|
'/*/profile/',
|
||||||
|
'/*/checkout/',
|
||||||
|
'/*/order/',
|
||||||
|
'/profile/',
|
||||||
|
'/checkout/',
|
||||||
|
'/order/'
|
||||||
|
],
|
||||||
},
|
},
|
||||||
sitemap: `${baseUrl}/sitemap.xml`,
|
sitemap: `${baseUrl}/sitemap.xml`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from './LocalizedLink'
|
||||||
import { getCategoryImage, formatPrice, type Product } from '@homaroma/types'
|
import { getCategoryImage, formatPrice, type Product } from '@homaroma/types'
|
||||||
import { useCartStore } from '../store/cartStore'
|
import { useCartStore } from '../store/cartStore'
|
||||||
import { resolveCategoryName, resolveLocalized } from '../lib/localization'
|
import { resolveCategoryName, resolveLocalized } from '../lib/localization'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { X, ShoppingBag, Plus, Minus, Trash2 } from 'lucide-react'
|
import { X, ShoppingBag, Plus, Minus, Trash2 } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from './LocalizedLink'
|
||||||
import { useCartStore } from '../store/cartStore'
|
import { useCartStore } from '../store/cartStore'
|
||||||
import { formatPrice } from '@homaroma/types'
|
import { formatPrice } from '@homaroma/types'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from './LocalizedLink'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useEffect, useMemo, useState } from 'react'
|
import { useEffect, useMemo, useState } from 'react'
|
||||||
import { AlertCircle, CheckCircle2, Loader2, PackageCheck, ShoppingBag, Ticket, UserRound } from 'lucide-react'
|
import { AlertCircle, CheckCircle2, Loader2, PackageCheck, ShoppingBag, Ticket, UserRound } from 'lucide-react'
|
||||||
@@ -9,6 +9,8 @@ import { formatPrice, type CartItem, type CustomerAddress } from '@homaroma/type
|
|||||||
import { API_BASE } from '../lib/api'
|
import { API_BASE } from '../lib/api'
|
||||||
import { useAuthStore, type Customer } from '../store/authStore'
|
import { useAuthStore, type Customer } from '../store/authStore'
|
||||||
import { useCartStore } from '../store/cartStore'
|
import { useCartStore } from '../store/cartStore'
|
||||||
|
import { useLang } from '../hooks/useLang'
|
||||||
|
import { getPathWithLocale } from '../lib/locales'
|
||||||
|
|
||||||
type StoreSettings = {
|
type StoreSettings = {
|
||||||
shippingFee?: number
|
shippingFee?: number
|
||||||
@@ -115,6 +117,7 @@ function saveLocalOrder(orderId: number) {
|
|||||||
|
|
||||||
export default function CheckoutClient() {
|
export default function CheckoutClient() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const lang = useLang()
|
||||||
const { customer } = useAuthStore()
|
const { customer } = useAuthStore()
|
||||||
const { items, clearCart, getCartTotal } = useCartStore()
|
const { items, clearCart, getCartTotal } = useCartStore()
|
||||||
const [hydrated, setHydrated] = useState(false)
|
const [hydrated, setHydrated] = useState(false)
|
||||||
@@ -373,7 +376,7 @@ export default function CheckoutClient() {
|
|||||||
|
|
||||||
clearCart()
|
clearCart()
|
||||||
saveLocalOrder(order.id)
|
saveLocalOrder(order.id)
|
||||||
router.push(`/order/${order.id}`)
|
router.push(getPathWithLocale(`/order/${order.id}`, lang))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setSubmitError(error instanceof Error ? error.message : 'ไม่สามารถสร้างคำสั่งซื้อได้')
|
setSubmitError(error instanceof Error ? error.message : 'ไม่สามารถสร้างคำสั่งซื้อได้')
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function Footer({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const lang = useLang()
|
const lang = useLang()
|
||||||
const [mounted, setMounted] = useState(false)
|
const [mounted, setMounted] = useState(false)
|
||||||
useEffect(() => { setMounted(true) }, [])
|
useEffect(() => { queueMicrotask(() => setMounted(true)) }, [])
|
||||||
// Before mount: always 'th' to match SSR output → no hydration mismatch
|
// Before mount: always 'th' to match SSR output → no hydration mismatch
|
||||||
// After mount: real user language → correct content
|
// After mount: real user language → correct content
|
||||||
const effectiveLang = mounted ? lang : 'th'
|
const effectiveLang = mounted ? lang : 'th'
|
||||||
@@ -32,7 +32,7 @@ export default function Footer({
|
|||||||
const translateMenuLabel = (item: MenuItem): string => {
|
const translateMenuLabel = (item: MenuItem): string => {
|
||||||
// Use outer `lang` from closure — hooks cannot be called inside nested functions
|
// Use outer `lang` from closure — hooks cannot be called inside nested functions
|
||||||
if (effectiveLang !== 'en' && item.translations && item.translations.length > 0) {
|
if (effectiveLang !== 'en' && item.translations && item.translations.length > 0) {
|
||||||
const trans = item.translations.find((tr: any) => tr.langCode === effectiveLang)
|
const trans = item.translations.find((tr: { langCode: string; label: string }) => tr.langCode === effectiveLang)
|
||||||
if (trans?.label) return trans.label
|
if (trans?.label) return trans.label
|
||||||
}
|
}
|
||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function LocalizedLink({
|
|||||||
// Extract current locale from pathname
|
// Extract current locale from pathname
|
||||||
const currentLocale = useMemo(() => {
|
const currentLocale = useMemo(() => {
|
||||||
const segments = pathname.split('/').filter(Boolean);
|
const segments = pathname.split('/').filter(Boolean);
|
||||||
if (segments.length > 0 && SUPPORTED_LOCALES.includes(segments[0] as any)) {
|
if (segments.length > 0 && (SUPPORTED_LOCALES as readonly string[]).includes(segments[0])) {
|
||||||
return segments[0];
|
return segments[0];
|
||||||
}
|
}
|
||||||
return 'th'; // Fallback
|
return 'th'; // Fallback
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ const LANGUAGES = [
|
|||||||
{ code: 'ko', label: '한국어', short: 'KO' },
|
{ code: 'ko', label: '한국어', short: 'KO' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
function setLanguageCookie(code: string) {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
document.cookie = `homaroma_lang=${code}; path=/; max-age=31536000`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function createFallbackMenu(categories: Category[]): MegaMenuPayload[] {
|
function createFallbackMenu(categories: Category[]): MegaMenuPayload[] {
|
||||||
const categoryLinks = categories.map((category) => {
|
const categoryLinks = categories.map((category) => {
|
||||||
// category.name is a multilingual Record<lang,string> when fetched with lang=all
|
// category.name is a multilingual Record<lang,string> when fetched with lang=all
|
||||||
@@ -90,13 +96,13 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
|||||||
const changeLanguage = (code: string) => {
|
const changeLanguage = (code: string) => {
|
||||||
i18n.changeLanguage(code)
|
i18n.changeLanguage(code)
|
||||||
localStorage.setItem('homaroma_lang', code)
|
localStorage.setItem('homaroma_lang', code)
|
||||||
document.cookie = `homaroma_lang=${code}; path=/; max-age=31536000`
|
setLanguageCookie(code)
|
||||||
|
|
||||||
const currentPathWithoutLang = getPathWithoutLocale(pathname)
|
const currentPathWithoutLang = getPathWithoutLocale(pathname)
|
||||||
const newPath = getPathWithLocale(currentPathWithoutLang, code)
|
const newPath = getPathWithLocale(currentPathWithoutLang, code)
|
||||||
|
|
||||||
setLangSheetOpen(false)
|
setLangSheetOpen(false)
|
||||||
router.push(newPath)
|
router.push(newPath + window.location.search)
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginWithLine = () => {
|
const loginWithLine = () => {
|
||||||
@@ -105,7 +111,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
|||||||
maybeWindow.liff.login()
|
maybeWindow.liff.login()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
router.push('/profile')
|
router.push(getPathWithLocale('/profile', lang))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -163,7 +169,7 @@ export default function Navbar({ categories = [], initialMenu = [], settings = n
|
|||||||
const handleSearch = (e: React.FormEvent) => {
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
router.push(`/search?q=${encodeURIComponent(searchQuery.trim())}`)
|
router.push(getPathWithLocale(`/search?q=${encodeURIComponent(searchQuery.trim())}`, lang))
|
||||||
setSearchOpen(false)
|
setSearchOpen(false)
|
||||||
setMobileOpen(false)
|
setMobileOpen(false)
|
||||||
setSearchQuery('')
|
setSearchQuery('')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from './LocalizedLink'
|
||||||
import type { Product } from '@homaroma/types'
|
import type { Product } from '@homaroma/types'
|
||||||
import { getCategoryImage, formatPrice } from '@homaroma/types'
|
import { getCategoryImage, formatPrice } from '@homaroma/types'
|
||||||
import { resolveCategoryName, resolveLocalized } from '../lib/localization'
|
import { resolveCategoryName, resolveLocalized } from '../lib/localization'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from './LocalizedLink'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Award,
|
Award,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useLang } from '../hooks/useLang'
|
||||||
|
import { getPathWithLocale } from '../lib/locales'
|
||||||
|
|
||||||
type ShopCouponBridgeProps = {
|
type ShopCouponBridgeProps = {
|
||||||
coupon?: string
|
coupon?: string
|
||||||
@@ -9,13 +11,14 @@ type ShopCouponBridgeProps = {
|
|||||||
|
|
||||||
export default function ShopCouponBridge({ coupon }: ShopCouponBridgeProps) {
|
export default function ShopCouponBridge({ coupon }: ShopCouponBridgeProps) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const lang = useLang()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!coupon) return
|
if (!coupon) return
|
||||||
|
|
||||||
localStorage.setItem('homaroma_active_coupon', coupon)
|
localStorage.setItem('homaroma_active_coupon', coupon)
|
||||||
router.replace('/shop', { scroll: false })
|
router.replace(getPathWithLocale('/shop', lang), { scroll: false })
|
||||||
}, [coupon, router])
|
}, [coupon, router, lang])
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from './LocalizedLink'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { ChevronRight, Loader2, Package, Search } from 'lucide-react'
|
import { ChevronRight, Loader2, Package, Search } from 'lucide-react'
|
||||||
|
|
||||||
import { API_BASE } from '../lib/api'
|
import { API_BASE } from '../lib/api'
|
||||||
|
import { useLang } from '../hooks/useLang'
|
||||||
|
import { getPathWithLocale } from '../lib/locales'
|
||||||
|
|
||||||
type TrackResponse =
|
type TrackResponse =
|
||||||
| { success: true; orderId: number }
|
| { success: true; orderId: number }
|
||||||
@@ -30,6 +32,7 @@ function orderLabel(id: number) {
|
|||||||
|
|
||||||
export default function TrackOrderClient() {
|
export default function TrackOrderClient() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const lang = useLang()
|
||||||
const [orderId, setOrderId] = useState('')
|
const [orderId, setOrderId] = useState('')
|
||||||
const [phone, setPhone] = useState('')
|
const [phone, setPhone] = useState('')
|
||||||
const [localOrders, setLocalOrders] = useState<number[]>([])
|
const [localOrders, setLocalOrders] = useState<number[]>([])
|
||||||
@@ -75,7 +78,7 @@ export default function TrackOrderClient() {
|
|||||||
|
|
||||||
if (response.ok && 'success' in data && data.success) {
|
if (response.ok && 'success' in data && data.success) {
|
||||||
saveLocalOrder(data.orderId)
|
saveLocalOrder(data.orderId)
|
||||||
router.push(`/order/${data.orderId}`)
|
router.push(getPathWithLocale(`/order/${data.orderId}`, lang))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from './LocalizedLink'
|
||||||
import { Heart } from 'lucide-react'
|
import { Heart } from 'lucide-react'
|
||||||
|
|
||||||
import ProductCard from './ProductCard'
|
import ProductCard from './ProductCard'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from '../LocalizedLink'
|
||||||
import { Clock, Mail, MapPin, MessageCircle } from 'lucide-react'
|
import { Clock, Mail, MapPin, MessageCircle } from 'lucide-react'
|
||||||
import type { Category, PageSection, Product } from '@homaroma/types'
|
import type { Category, PageSection, Product } from '@homaroma/types'
|
||||||
import { getCategoryImage } from '@homaroma/types'
|
import { getCategoryImage } from '@homaroma/types'
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function useLang(): string {
|
|||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
// 1. Try to get it from route params
|
// 1. Try to get it from route params
|
||||||
if (params?.lang && typeof params.lang === 'string') {
|
if (params?.lang && typeof params.lang === 'string') {
|
||||||
if (SUPPORTED_LOCALES.includes(params.lang as any)) {
|
if ((SUPPORTED_LOCALES as readonly string[]).includes(params.lang)) {
|
||||||
return params.lang;
|
return params.lang;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ export function useLang(): string {
|
|||||||
// 2. Fallback: extract from pathname if params are not available yet
|
// 2. Fallback: extract from pathname if params are not available yet
|
||||||
if (pathname) {
|
if (pathname) {
|
||||||
const segments = pathname.split('/').filter(Boolean);
|
const segments = pathname.split('/').filter(Boolean);
|
||||||
if (segments.length > 0 && SUPPORTED_LOCALES.includes(segments[0] as any)) {
|
if (segments.length > 0 && (SUPPORTED_LOCALES as readonly string[]).includes(segments[0])) {
|
||||||
return segments[0];
|
return segments[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
import { SUPPORTED_LOCALES } from './locales';
|
||||||
|
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';
|
||||||
|
|
||||||
|
type SeoMetadataProps = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
path: string; // e.g. '/shop', '/category/1', or ''
|
||||||
|
lang: string;
|
||||||
|
openGraph?: Metadata['openGraph'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generateSeoMetadata({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
path,
|
||||||
|
lang,
|
||||||
|
openGraph,
|
||||||
|
}: SeoMetadataProps): Metadata {
|
||||||
|
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
||||||
|
const pathPart = cleanPath === '/' ? '' : cleanPath;
|
||||||
|
|
||||||
|
const languages: Record<string, string> = {};
|
||||||
|
SUPPORTED_LOCALES.forEach((locale) => {
|
||||||
|
languages[locale] = `${baseUrl}/${locale}${pathPart}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
alternates: {
|
||||||
|
canonical: `${baseUrl}/${lang}${pathPart}`,
|
||||||
|
languages,
|
||||||
|
},
|
||||||
|
openGraph,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -31,15 +31,15 @@ export function proxy(request: NextRequest) {
|
|||||||
const cookieLocale = request.cookies.get('homaroma_lang')?.value;
|
const cookieLocale = request.cookies.get('homaroma_lang')?.value;
|
||||||
let targetLocale = DEFAULT_LOCALE;
|
let targetLocale = DEFAULT_LOCALE;
|
||||||
|
|
||||||
if (cookieLocale && SUPPORTED_LOCALES.includes(cookieLocale as any)) {
|
if (cookieLocale && (SUPPORTED_LOCALES as readonly string[]).includes(cookieLocale)) {
|
||||||
targetLocale = cookieLocale as any;
|
targetLocale = cookieLocale as typeof SUPPORTED_LOCALES[number];
|
||||||
} else {
|
} else {
|
||||||
// 2. Check Accept-Language header
|
// 2. Check Accept-Language header
|
||||||
const acceptLang = request.headers.get('accept-language');
|
const acceptLang = request.headers.get('accept-language');
|
||||||
if (acceptLang) {
|
if (acceptLang) {
|
||||||
const preferred = acceptLang.split(',')[0].split('-')[0].toLowerCase();
|
const preferred = acceptLang.split(',')[0].split('-')[0].toLowerCase();
|
||||||
if (SUPPORTED_LOCALES.includes(preferred as any)) {
|
if ((SUPPORTED_LOCALES as readonly string[]).includes(preferred)) {
|
||||||
targetLocale = preferred as any;
|
targetLocale = preferred as typeof SUPPORTED_LOCALES[number];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
function walkDir(dir, callback) {
|
||||||
|
fs.readdirSync(dir).forEach(f => {
|
||||||
|
let dirPath = path.join(dir, f);
|
||||||
|
if (fs.statSync(dirPath).isDirectory()) {
|
||||||
|
walkDir(dirPath, callback);
|
||||||
|
} else {
|
||||||
|
callback(dirPath);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
walkDir('apps/website/src/app/[lang]', (filePath) => {
|
||||||
|
if (!filePath.endsWith('.tsx') && !filePath.endsWith('.ts')) return;
|
||||||
|
|
||||||
|
let content = fs.readFileSync(filePath, 'utf8');
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
// 1. Convert `export const metadata = { title: '...', description: '...' }` to generateMetadata
|
||||||
|
// We need to parse:
|
||||||
|
// export const metadata = {
|
||||||
|
// title: '...',
|
||||||
|
// description: '...',
|
||||||
|
// }
|
||||||
|
|
||||||
|
const metadataRegex = /export const metadata\s*(?::\s*Metadata\s*)?=\s*\{([\s\S]*?)\};?/g;
|
||||||
|
|
||||||
|
// Exclude layout.tsx because we'll keep its global metadata, and handle it separately.
|
||||||
|
if (filePath.endsWith('layout.tsx')) return;
|
||||||
|
|
||||||
|
content = content.replace(metadataRegex, (match, body) => {
|
||||||
|
changed = true;
|
||||||
|
|
||||||
|
// Extract title and description
|
||||||
|
const titleMatch = body.match(/title:\s*['"`](.*?)['"`]/);
|
||||||
|
const descMatch = body.match(/description:\s*['"`](.*?)['"`]/);
|
||||||
|
|
||||||
|
const title = titleMatch ? titleMatch[1] : '';
|
||||||
|
const desc = descMatch ? descMatch[1] : '';
|
||||||
|
|
||||||
|
// Determine the route path (e.g. /shop, /about, /checkout)
|
||||||
|
// from the filePath. e.g. apps/website/src/app/[lang]/shop/page.tsx -> /shop
|
||||||
|
let routePath = '';
|
||||||
|
const relPath = filePath.split('app/[lang]/')[1].replace('/page.tsx', '').replace('page.tsx', '');
|
||||||
|
if (relPath && !relPath.includes('[') && !relPath.includes(']')) {
|
||||||
|
routePath = '/' + relPath;
|
||||||
|
} else if (relPath.includes('[') && relPath.includes(']')) {
|
||||||
|
// Dynamic route, can't just use string replacement easily. We will leave it to be manually fixed or use params.
|
||||||
|
// But wait! If it's a dynamic route, it probably already has `generateMetadata`!
|
||||||
|
// Let's check if there are static metadata in dynamic routes.
|
||||||
|
// Wait, `/order/[id]/page.tsx` has `generateMetadata`.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relPath.includes('[')) {
|
||||||
|
// Just fallback to empty for dynamic segments if any static metadata exists
|
||||||
|
routePath = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||||
|
const { lang } = await params;
|
||||||
|
return generateSeoMetadata({
|
||||||
|
title: '${title}',
|
||||||
|
description: '${desc}',
|
||||||
|
path: '${routePath}',
|
||||||
|
lang,
|
||||||
|
});
|
||||||
|
}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
if (!content.includes('generateSeoMetadata')) {
|
||||||
|
// Insert import at the top
|
||||||
|
content = `import { generateSeoMetadata } from '@/lib/seo';\n` + content;
|
||||||
|
}
|
||||||
|
fs.writeFileSync(filePath, content, 'utf8');
|
||||||
|
console.log('Converted static metadata in', filePath);
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in new issue
Block a user