This commit is contained in:
Your Name committed 2026-09-29 07:40:08 +07:00
1 parent 602eaa9260
commit c42dd4272f
30 files changed
+2024 -252

No files matched your search

+14 -2
View File
@@ -39,11 +39,23 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
const pageResult = await getCmsPageResult(slug, lang)
if (!pageResult.ok || !pageResult.data.isPublished) return { title: 'Page Not Found' }
const page = pageResult.data
const title = page.seoTitle || page.title
const description = page.seoDescription || page.title
const ogImage = page.ogImageUrl
return generateSeoMetadata({
title: `${pageResult.data.title}`,
description: `${pageResult.data.title}`,
title,
description,
path: `/${slug}`,
lang,
openGraph: ogImage
? {
title,
description,
images: [ogImage],
}
: undefined,
})
}
@@ -1,5 +1,24 @@
import type { Metadata } from 'next'
import Link from '@/components/LocalizedLink'
import { API_BASE } from '@/lib/api'
import { generateSeoMetadata } from '@/lib/seo'
import { getStoreSettings } from '@/lib/cms'
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params
const settings = await getStoreSettings(lang)
const brandName = settings?.brandName || ''
return generateSeoMetadata({
title: brandName ? `About ${brandName}` : 'About Us',
description: brandName
? `Learn about ${brandName} — our story, mission, and commitment to pure botanical craftsmanship.`
: 'Learn about our story, mission, and commitment to pure botanical craftsmanship.',
path: '/about',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
})
}
const PILLARS = [
{
@@ -7,7 +7,7 @@ import type { Category, Product } from '@homaroma/types'
import { getCategoryImage } from '@homaroma/types'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import ProductCard from '@/components/ProductCard'
import { type ApiResult, fetchJsonResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
import { type ApiResult, fetchJsonResult, getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
import { resolveLocalized } from '@/lib/localization'
@@ -25,14 +25,32 @@ async function getCategoryProducts(id: string, lang: string): Promise<ApiResult<
export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise<Metadata> {
const { id, lang } = await params
const result = await getCategoryProducts(id, lang)
const [result, settings] = await Promise.all([
getCategoryProducts(id, lang),
getStoreSettings(lang),
])
const categoryName = result.ok && result.data.category ? resolveLocalized(result.data.category.name, lang) : ''
const brandName = settings?.brandName || ''
const description = categoryName
? (brandName ? `Shop ${categoryName} products from ${brandName}.` : `Shop ${categoryName} products.`)
: (brandName ? `Shop ${brandName} category products.` : 'Shop category products.')
const category = result.ok ? result.data.category : undefined
const categoryImage = category?.imageUrl || (categoryName ? getCategoryImage(categoryName) : undefined) || settings?.defaultOgImage
return generateSeoMetadata({
title: categoryName ? `${categoryName}` : 'Category',
description: categoryName ? `Shop ${categoryName} products from .` : 'Shop category products.',
description,
path: `/category/${id}`,
lang,
openGraph: categoryImage
? {
title: categoryName || undefined,
description,
images: [categoryImage],
}
: undefined,
})
}
@@ -3,17 +3,24 @@ import { generateSeoMetadata } from '@/lib/seo'
import CheckoutClient from '@/components/CheckoutClient'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
import { getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
// lang is extracted from params
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const settings = await getStoreSettings(lang);
const brandName = settings?.brandName || '';
const description = brandName
? `ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ ${brandName}`
: 'ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ';
return generateSeoMetadata({
title: 'Checkout',
description: 'ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ ',
description,
path: '/checkout',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
});
}
+17 -3
View File
@@ -1,8 +1,22 @@
import type { Metadata } from 'next'
import { generateSeoMetadata } from '@/lib/seo'
import { getStoreSettings } from '@/lib/cms'
export const metadata: Metadata = {
title: 'Contact Us',
description: 'Get in touch with . We respond to all inquiries within 2 hours.',
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params
const settings = await getStoreSettings(lang)
const brandName = settings?.brandName || ''
const description = brandName
? `Get in touch with ${brandName}. We respond to all inquiries within 2 hours.`
: 'Get in touch with us. We respond to all inquiries within 2 hours.'
return generateSeoMetadata({
title: 'Contact Us',
description,
path: '/contact',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
})
}
export default function ContactLayout({ children }: { children: React.ReactNode }) {
+9 -2
View File
@@ -2,7 +2,7 @@ import Link from '@/components/LocalizedLink'
import { Ticket, ArrowRight } from 'lucide-react'
import { generateSeoMetadata } from '@/lib/seo'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import { getActiveCouponsResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
import { getActiveCouponsResult, getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
import { toneClass } from '@/lib/theme'
@@ -16,11 +16,18 @@ function formatDiscount(type: string, value: number) {
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const settings = await getStoreSettings(lang);
const brandName = settings?.brandName || '';
const description = brandName
? `รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก ${brandName} สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ`
: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษสำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ';
return generateSeoMetadata({
title: 'โปรโมชั่นและคูปองส่วนลด',
description: 'รวมรหัสส่วนลดและโปรโมชั่นพิเศษจาก สำหรับการสั่งซื้อน้ำหอมและผลิตภัณฑ์อโรม่าที่คุณชื่นชอบ',
description,
path: '/coupons',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
});
}
+17 -3
View File
@@ -1,8 +1,22 @@
import type { Metadata } from 'next'
import { generateSeoMetadata } from '@/lib/seo'
import { getStoreSettings } from '@/lib/cms'
export const metadata: Metadata = {
title: 'FAQ',
description: 'Everything you need to know about products, shipping, and care.',
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }): Promise<Metadata> {
const { lang } = await params
const settings = await getStoreSettings(lang)
const brandName = settings?.brandName || ''
const description = brandName
? `Everything you need to know about ${brandName} products, shipping, and care.`
: 'Everything you need to know about our products, shipping, and care.'
return generateSeoMetadata({
title: 'FAQ',
description,
path: '/faq',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
})
}
export default function FaqLayout({ children }: { children: React.ReactNode }) {
+10 -1
View File
@@ -13,14 +13,23 @@ export async function generateMetadata({ params }: { params: Promise<{ lang: str
const { lang } = await params;
const settings = await getStoreSettings(lang);
const brandName = settings?.brandName || '';
const description = settings?.defaultSeoDescription || settings?.footerDescription || 'Pure Origins, Artisan Craft, Zero Compromise';
const ogImage = settings?.defaultOgImage;
return {
title: {
template: `%s | ${brandName}`,
default: brandName,
},
description: 'Pure Origins, Artisan Craft, Zero Compromise',
description,
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000'),
openGraph: ogImage
? {
title: brandName,
description,
images: [ogImage],
}
: undefined,
};
}
@@ -7,7 +7,7 @@ import { CheckCircle2, Clock, PackageCheck, Truck } from 'lucide-react'
import { formatPrice } from '@homaroma/types'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import { getCategoriesResult, getCmsPageResult, getOrderResult } from '@/lib/cms'
import { getCategoriesResult, getCmsPageResult, getOrderResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
import { orderStatusBadgeClass, toneClass } from '@/lib/theme'
@@ -31,15 +31,24 @@ function statusLabel(status: string) {
export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise<Metadata> {
const { id, lang } = await params
const result = await getOrderResult(id)
const [result, settings] = await Promise.all([
getOrderResult(id),
getStoreSettings(lang),
])
if (!result.ok) return { title: 'Order Not Found' }
const brandName = settings?.brandName || ''
const description = brandName
? `ตรวจสอบสถานะคำสั่งซื้อ ${orderLabel(result.data.id)} จาก ${brandName}`
: `ตรวจสอบสถานะคำสั่งซื้อ ${orderLabel(result.data.id)}`
return generateSeoMetadata({
title: `Order ${orderLabel(result.data.id)}`,
description: `ตรวจสอบสถานะคำสั่งซื้อ ${orderLabel(result.data.id)} จาก `,
description,
path: `/order/${id}`,
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
})
}
+21 -4
View File
@@ -1,7 +1,7 @@
import type { Metadata } from 'next'
import { generateSeoMetadata } from '@/lib/seo'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import { getCategories, getCmsPage } from '@/lib/cms'
import { getCategories, getCmsPage, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
type Props = {
@@ -10,13 +10,30 @@ type Props = {
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { lang } = await params
const page = await getCmsPage('home', lang)
const [page, settings] = await Promise.all([
getCmsPage('home', lang),
getStoreSettings(lang),
])
const title = page?.seoTitle || page?.title || undefined
const description =
page?.seoDescription ||
settings?.defaultSeoDescription ||
(settings?.brandName ? `${settings.brandName} - Pure Origins, Artisan Craft` : 'Pure Origins, Artisan Craft')
const ogImage = page?.ogImageUrl || settings?.defaultOgImage
return generateSeoMetadata({
title: page?.title ? `${page.title}` : undefined,
description: 'Premium aromatherapy, home fragrance, and botanical scent products from .',
title,
description,
path: '',
lang,
openGraph: ogImage
? {
title: page?.seoTitle || page?.title || settings?.brandName || undefined,
description,
images: [ogImage],
}
: undefined,
})
}
@@ -11,7 +11,7 @@ import ProductGallery from '@/components/ProductGallery'
import ProductStickyPurchaseBar from '@/components/ProductStickyPurchaseBar'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import ProductCard from '@/components/ProductCard'
import { getCategoriesResult, getProduct, getProductDetailSectionsResult, getProductResult, getProductReviewsResult, getProductsResult, getStoreSettingsResult, type ProductReview } from '@/lib/cms'
import { getCategoriesResult, getProduct, getProductDetailSectionsResult, getProductResult, getProductReviewsResult, getProductsResult, getStoreSettings, getStoreSettingsResult, type ProductReview } from '@/lib/cms'
import { resolveCategoryName, resolveLocalized } from '@/lib/localization'
import { SUPPORTED_LOCALES } from '@/lib/locales'
import { extractIdFromSlug, generateProductSlug } from '@/lib/slugify'
@@ -352,24 +352,32 @@ function ProductReviewsSection({
export async function generateMetadata({ params }: { params: Promise<{ slug: string, lang: string }> }): Promise<Metadata> {
const { slug, lang } = await params
const id = extractIdFromSlug(slug)
const product = await getProduct(id, lang)
const [product, settings] = await Promise.all([
getProduct(id, lang),
getStoreSettings(lang),
])
if (!product) return { title: 'Product Not Found' }
const productName = resolveLocalized(product.name, lang) || product.brand.name
const brandName = product.brand?.name || settings?.brandName || ''
const productName = resolveLocalized(product.name, lang) || brandName
const categoryName = resolveCategoryName(product.category, lang)
const description = resolveLocalized(product.description, lang)
const rawDescription = resolveLocalized(product.description, lang)
const description =
rawDescription || (categoryName ? (brandName ? `${categoryName} from ${brandName}` : categoryName) : productName)
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://www.homaroma.com'
const customLanguages: Record<string, string> = {}
for (const l of SUPPORTED_LOCALES) {
const locName = resolveLocalized(product.name, l) || product.brand.name
const locName = resolveLocalized(product.name, l) || brandName
customLanguages[l] = `${baseUrl}/${l}/products/${generateProductSlug(locName, product.id)}`
}
const ogImage = product.images?.[0]?.url || settings?.defaultOgImage
return generateSeoMetadata({
title: `${productName} ${categoryName ? `| ${categoryName}` : ''}`,
description: description || `Premium ${categoryName} from .`,
description,
path: `/products/${slug}`,
lang,
customLanguages,
@@ -377,7 +385,7 @@ export async function generateMetadata({ params }: { params: Promise<{ slug: str
type: 'website',
title: productName,
description,
images: product.images?.[0]?.url ? [product.images[0].url] : [],
images: ogImage ? [ogImage] : [],
},
})
}
+9 -2
View File
@@ -3,17 +3,24 @@ import { generateSeoMetadata } from '@/lib/seo'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import ProfileClient from '@/components/ProfileClient'
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
import { getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
// lang is extracted from params
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const settings = await getStoreSettings(lang);
const brandName = settings?.brandName || '';
const description = brandName
? `จัดการโปรไฟล์สมาชิก ${brandName} ประวัติคำสั่งซื้อ คะแนนสะสม และสมุดที่อยู่จัดส่ง`
: 'จัดการโปรไฟล์สมาชิก ประวัติคำสั่งซื้อ คะแนนสะสม และสมุดที่อยู่จัดส่ง';
return generateSeoMetadata({
title: 'โปรไฟล์สมาชิก',
description: 'จัดการโปรไฟล์สมาชิก ประวัติคำสั่งซื้อ คะแนนสะสม และสมุดที่อยู่จัดส่ง',
description,
path: '/profile',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
});
}
+9 -2
View File
@@ -2,17 +2,24 @@ import Link from '@/components/LocalizedLink'
import { generateSeoMetadata } from '@/lib/seo'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import ProductCard from '@/components/ProductCard'
import { getCategoriesResult, getCmsPageResult, getProductsResult } from '@/lib/cms'
import { getCategoriesResult, getCmsPageResult, getProductsResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
import { resolveLocalized } from '@/lib/localization'
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const settings = await getStoreSettings(lang);
const brandName = settings?.brandName || '';
const description = brandName
? `Search ${brandName} products by scent, category, or keyword.`
: 'Search products by scent, category, or keyword.';
return generateSeoMetadata({
title: 'Search Products',
description: 'Search products by scent, category, or keyword.',
description,
path: '/search',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
});
}
+9 -2
View File
@@ -3,18 +3,25 @@ import { generateSeoMetadata } from '@/lib/seo'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import ProductCard from '@/components/ProductCard'
import ShopCouponBridge from '@/components/ShopCouponBridge'
import { getCategoriesResult, getCmsPageResult, getProductsResult } from '@/lib/cms'
import { getCategoriesResult, getCmsPageResult, getProductsResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
import { resolveLocalized } from '@/lib/localization'
// lang is extracted from params
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const settings = await getStoreSettings(lang);
const brandName = settings?.brandName || '';
const description = brandName
? `Browse ${brandName} aroma oils, reed diffusers, room sprays, and premium scent products.`
: 'Browse aroma oils, reed diffusers, room sprays, and premium scent products.';
return generateSeoMetadata({
title: 'Shop Aroma Products',
description: 'Browse aroma oils, reed diffusers, room sprays, and premium scent products.',
description,
path: '/shop',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
});
}
+9 -2
View File
@@ -3,17 +3,24 @@ import { generateSeoMetadata } from '@/lib/seo'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import TrackOrderClient from '@/components/TrackOrderClient'
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
import { getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
// lang is extracted from params
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const settings = await getStoreSettings(lang);
const brandName = settings?.brandName || '';
const description = brandName
? `ตรวจสอบสถานะคำสั่งซื้อ ${brandName} ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ`
: 'ตรวจสอบสถานะคำสั่งซื้อ ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ';
return generateSeoMetadata({
title: 'ติดตามคำสั่งซื้อ',
description: 'ตรวจสอบสถานะคำสั่งซื้อ ด้วยหมายเลขคำสั่งซื้อและเบอร์โทรศัพท์ที่ใช้สั่งซื้อ',
description,
path: '/track',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
});
}
@@ -3,17 +3,24 @@ import { generateSeoMetadata } from '@/lib/seo'
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
import WishlistClient from '@/components/WishlistClient'
import { getCategoriesResult, getCmsPageResult } from '@/lib/cms'
import { getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
import { splitCmsLayoutSections } from '@/lib/cms-sections'
// lang is extracted from params
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const settings = await getStoreSettings(lang);
const brandName = settings?.brandName || '';
const description = brandName
? `รายการสินค้าที่คุณบันทึกไว้ใน Wishlist ของ ${brandName}`
: 'รายการสินค้าที่คุณบันทึกไว้ใน Wishlist';
return generateSeoMetadata({
title: 'Wishlist',
description: 'รายการสินค้าที่คุณบันทึกไว้ใน Wishlist ของ ',
description,
path: '/wishlist',
lang,
openGraph: settings?.defaultOgImage ? { images: [settings.defaultOgImage] } : undefined,
});
}
+4
View File
@@ -82,6 +82,10 @@ export async function getCategoriesResult(lang = 'all'): Promise<ApiResult<Categ
return await fetchJsonResult<Category[]>(`/categories?lang=${lang}`, { next: { revalidate: 0 } })
}
export async function getStoreSettings(lang = 'all'): Promise<StoreSettings | null> {
return await fetchJson<StoreSettings>(`/settings?lang=${lang}`, { next: { revalidate: defaultRevalidate } })
}
export async function getStoreSettingsResult(lang = 'all'): Promise<ApiResult<StoreSettings>> {
return await fetchJsonResult<StoreSettings>(`/settings?lang=${lang}`, { next: { revalidate: defaultRevalidate } })
}