perf: optimize public content caching

This commit is contained in:
Codex
2026-04-30 10:29:40 +08:00
parent d4fb7e0973
commit 3da316fed9
24 changed files with 778 additions and 275 deletions
+7 -2
View File
@@ -3,6 +3,7 @@ import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { AboutContactTabs } from '@/components/site/about-contact-tabs'
import { PageHero } from '@/components/site/page-hero'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { buildAbsoluteUrl, buildLocaleAlternates, extractRichTextPlainText, trimDescription } from '@/lib/seo'
import { normalizeUploadUrl } from '@/lib/upload-url'
@@ -15,13 +16,17 @@ function getAboutPageTitle(rawTitle: string | null | undefined) {
return normalized
}
async function getAboutPageData(locale: Locale) {
const getAboutPageData = cacheCmsQuery(
['about-page-global'],
async (locale: Locale) => {
const payload = await getPayloadClient()
return payload.findGlobal({
slug: 'aboutPage',
locale,
})
}
},
)
export async function generateMetadata({
params,
+6 -2
View File
@@ -4,6 +4,7 @@ import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { AboutContactTabs } from '@/components/site/about-contact-tabs'
import { PageHero } from '@/components/site/page-hero'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { buildAbsoluteUrl, buildLocaleAlternates } from '@/lib/seo'
import { normalizeUploadUrl } from '@/lib/upload-url'
@@ -158,7 +159,9 @@ function normalizeContactGroups(groups: unknown, locale: Locale) {
return normalized.length > 0 ? normalized : getDefaultContactGroups(locale)
}
async function getContactUsPageData(locale: Locale) {
const getContactUsPageData = cacheCmsQuery(
['contact-us-page-global'],
async (locale: Locale) => {
const payload = await getPayloadClient()
return payload.findGlobal({
@@ -166,7 +169,8 @@ async function getContactUsPageData(locale: Locale) {
locale,
slug: 'contactUsPage',
})
}
},
)
export async function generateMetadata({
params,
@@ -5,6 +5,7 @@ import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { SiteIcon } from '@/components/site/site-icons'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { buildLocaleAlternates, extractRichTextPlainText, trimDescription } from '@/lib/seo'
import { renderRichText } from '@/lib/render-rich-text'
@@ -122,7 +123,7 @@ function hasRichTextContent(value: Download['notes'] | Download['changelog'] | n
return Boolean(extractRichTextPlainText(value))
}
async function getDownloadProductData(slug: string, locale: Locale) {
async function getDownloadProductDataUncached(slug: string, locale: Locale) {
const payload = await getPayloadClient()
const products = await payload.find({
collection: 'products',
@@ -231,6 +232,11 @@ async function getDownloadProductData(slug: string, locale: Locale) {
}
}
const getDownloadProductData = cacheCmsQuery<
[string, Locale],
Awaited<ReturnType<typeof getDownloadProductDataUncached>>
>(['download-product-data'], getDownloadProductDataUncached)
function getRelatedProducts(download: Download) {
const multi = Array.isArray((download as Download & { products?: DownloadProductReference[] }).products)
? ((download as Download & { products?: DownloadProductReference[] }).products as DownloadProductReference[])
+12 -3
View File
@@ -4,6 +4,7 @@ import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { SiteIcon } from '@/components/site/site-icons'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { isTemplateProduct } from '@/lib/product-template-navigation'
import { buildAbsoluteUrl, buildLocaleAlternates, extractRichTextPlainText } from '@/lib/seo'
@@ -224,7 +225,7 @@ async function getSeedDownloadGroups(locale: Locale): Promise<DownloadGroup[]> {
.sort((left, right) => left.productLabel.localeCompare(right.productLabel))
}
async function getDownloadsPageData(locale: Locale) {
async function getDownloadsPageDataUncached(locale: Locale) {
const payload = await getPayloadClient()
const downloadsResult = await payload.find({
collection: 'downloads',
@@ -337,7 +338,14 @@ async function getDownloadsPageData(locale: Locale) {
return getSeedDownloadGroups(locale)
}
async function getDownloadsPageSettings(locale: Locale): Promise<DownloadsPageSettings> {
const getDownloadsPageData = cacheCmsQuery<[Locale], Awaited<ReturnType<typeof getDownloadsPageDataUncached>>>(
['downloads-page-data'],
getDownloadsPageDataUncached,
)
const getDownloadsPageSettings = cacheCmsQuery<[Locale], DownloadsPageSettings>(
['downloads-page-global'],
async (locale) => {
const payload = await getPayloadClient()
return (await payload.findGlobal({
@@ -345,7 +353,8 @@ async function getDownloadsPageSettings(locale: Locale): Promise<DownloadsPageSe
locale,
slug: 'downloadsPage',
})) as DownloadsPageSettings
}
},
)
function getRelatedProducts(download: Download) {
const multi = Array.isArray((download as Download & { products?: DownloadProductReference[] }).products)
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { notFound } from 'next/navigation'
import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { renderRichText } from '@/lib/render-rich-text'
import { buildAbsoluteUrl, buildLocaleAlternates, extractRichTextPlainText, trimDescription } from '@/lib/seo'
@@ -21,7 +22,9 @@ function formatPublishedAt(value: string | null | undefined, locale: Locale) {
}).format(new Date(value))
}
async function getNewsArticle(locale: Locale, slug: string) {
const getNewsArticle = cacheCmsQuery(
['news-article'],
async (locale: Locale, slug: string) => {
const payload = await getPayloadClient()
const result = await payload.find({
collection: 'news',
@@ -45,7 +48,8 @@ async function getNewsArticle(locale: Locale, slug: string) {
})
return result.docs[0] ?? null
}
},
)
function getRelatedProduct(product: number | Product) {
return typeof product === 'object' ? product : null
+11 -4
View File
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { getBlogPosts } from '@/lib/blog-api'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { buildAbsoluteUrl, buildLocaleAlternates } from '@/lib/seo'
import { normalizeUploadUrl } from '@/lib/upload-url'
@@ -31,7 +32,9 @@ type NewsListItem = {
title: string
}
async function getNewsPageSettings() {
const getNewsPageSettings = cacheCmsQuery(
['news-page-global'],
async () => {
const payload = await getPayloadClient()
try {
@@ -41,9 +44,12 @@ async function getNewsPageSettings() {
} catch {
return { newsSource: 'externalBlog' as const }
}
}
},
)
async function getLocalNewsPage(locale: Locale, page: number) {
const getLocalNewsPage = cacheCmsQuery(
['local-news-page'],
async (locale: Locale, page: number) => {
const payload = await getPayloadClient()
return payload.find({
@@ -59,7 +65,8 @@ async function getLocalNewsPage(locale: Locale, page: number) {
},
},
})
}
},
)
function getCoverImageUrl(article: News) {
if (!article.coverImage || typeof article.coverImage === 'number') return null
+19 -7
View File
@@ -9,6 +9,7 @@ import { HomeNews } from '@/components/home/home-news'
import { HomeRecognition } from '@/components/home/home-recognition'
import { HomeStatement } from '@/components/home/home-statement'
import { getBlogPosts } from '@/lib/blog-api'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getSiteChromeSettings } from '@/lib/global-settings'
import { getPayloadClient } from '@/lib/payload'
import { VISIBLE_PRODUCT_STATUS } from '@/lib/product-status'
@@ -110,16 +111,22 @@ function normalizeHomepageModuleOrder(items: Homepage['moduleOrder']): HomepageM
return [...new Set([...ordered, ...validModules])]
}
async function getHomepageData(locale: Locale) {
const getHomepageData = cacheCmsQuery<[Locale], Homepage>(
['homepage-global'],
async (locale) => {
const payload = await getPayloadClient()
return payload.findGlobal({
depth: 2,
slug: 'homepage',
locale,
})
}
}) as Promise<Homepage>
},
)
async function getPublishedProducts(locale: Locale) {
const getPublishedProducts = cacheCmsQuery<[Locale], Product[]>(
['published-products'],
async (locale) => {
const payload = await getPayloadClient()
const published = await payload.find({
collection: 'products',
@@ -139,8 +146,10 @@ async function getPublishedProducts(locale: Locale) {
if (published.docs.length > 0) {
return published.docs
}
return []
}
},
)
function getTemplateProductHrefs(products: Product[], locale: Locale) {
return products
@@ -148,7 +157,9 @@ function getTemplateProductHrefs(products: Product[], locale: Locale) {
.map((product) => `/${locale}/products/${product.slug}`)
}
async function getLatestLocalNews(locale: Locale) {
const getLatestLocalNews = cacheCmsQuery<[Locale], News[]>(
['latest-local-news'],
async (locale) => {
const payload = await getPayloadClient()
const result = await payload.find({
collection: 'news',
@@ -164,7 +175,8 @@ async function getLatestLocalNews(locale: Locale) {
})
return result.docs
}
},
)
function getLocalNewsHighlights(items: Homepage['newsHighlights']) {
return (items || []).filter((item): item is News => typeof item === 'object')
@@ -3,6 +3,7 @@ import { notFound } from 'next/navigation'
import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { ALL_PRODUCTS_FALLBACK } from '@/lib/fallback-data'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { ACCESSIBLE_PRODUCT_STATUSES } from '@/lib/product-status'
import { getProductSeriesDefault } from '@/lib/product-series-defaults'
@@ -336,7 +337,7 @@ function ProductSpecificationsSection({
)
}
async function getProductPageData(slug: string, locale: Locale) {
async function getProductPageDataUncached(slug: string, locale: Locale) {
const payload = await getPayloadClient()
const products = await payload.find({
@@ -455,6 +456,11 @@ async function getProductPageData(slug: string, locale: Locale) {
return { downloads, news: relatedNews, overviewMedia, product, reviews: reviews.docs, videos: videos.docs }
}
const getProductPageData = cacheCmsQuery<[string, Locale], Awaited<ReturnType<typeof getProductPageDataUncached>>>(
['product-page-data'],
getProductPageDataUncached,
)
export async function generateMetadata({
params,
}: {
+45 -19
View File
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { getProductSeriesDefault } from '@/lib/product-series-defaults'
import { VISIBLE_PRODUCT_STATUS } from '@/lib/product-status'
@@ -100,7 +101,7 @@ function normalizeCategory(category: number | ProductCategory | null | undefined
return getDefaultSeriesCopy(getCategoryKeyFromSlug(slug), locale)
}
async function getProductsPageData(locale: Locale) {
async function getProductsPageDataUncached(locale: Locale) {
const payload = await getPayloadClient()
const result = await payload.find({
collection: 'products',
@@ -162,6 +163,43 @@ async function getProductsPageData(locale: Locale) {
}))
}
const getProductsPageData = cacheCmsQuery<[Locale], Awaited<ReturnType<typeof getProductsPageDataUncached>>>(
['products-page-data'],
getProductsPageDataUncached,
)
const getProductsPageSettings = cacheCmsQuery(
['products-page-global'],
async (locale: Locale) => {
const payload = await getPayloadClient()
return payload.findGlobal({
depth: 1,
slug: 'productsPage',
locale,
})
},
)
const getProductCategories = cacheCmsQuery<[Locale], ProductCategory[]>(
['product-categories'],
async (locale) => {
const payload = await getPayloadClient()
const categories = await payload.find({
collection: 'productCategories',
depth: 0,
limit: 50,
locale,
sort: 'order',
where: {
and: [{ docLocale: { equals: locale } }, { name: { exists: true } }],
},
})
return categories.docs
},
)
export async function generateMetadata({
params,
}: {
@@ -187,25 +225,13 @@ export default async function ProductsPage({
const { locale } = await params
setRequestLocale(locale)
const products = await getProductsPageData(locale)
const payload = await getPayloadClient()
const page = await payload.findGlobal({
depth: 1,
slug: 'productsPage',
locale,
})
const categories = await payload.find({
collection: 'productCategories',
depth: 0,
limit: 50,
locale,
sort: 'order',
where: {
and: [{ docLocale: { equals: locale } }, { name: { exists: true } }],
},
})
const [products, page, categories] = await Promise.all([
getProductsPageData(locale),
getProductsPageSettings(locale),
getProductCategories(locale),
])
const categoryMap = new Map(
categories.docs.map((category) => [category.slug, normalizeCategory(category, category.slug, locale)]),
categories.map((category) => [category.slug, normalizeCategory(category, category.slug, locale)]),
)
const groups = new Map<string, { series: ProductSeries; items: ProductCard[] }>()
+13 -2
View File
@@ -4,6 +4,7 @@ import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { VideoModalGrid, type VideoModalGridItem } from '@/components/site/video-modal-grid'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { buildAbsoluteUrl, buildLocaleAlternates, trimDescription } from '@/lib/seo'
import { normalizeUploadUrl } from '@/lib/upload-url'
@@ -58,7 +59,7 @@ function truncateSourceName(value: string | null | undefined) {
return chars.length > 20 ? `${chars.slice(0, 20).join('')}...` : source
}
async function getReviewsPageData(locale: Locale) {
async function getReviewsPageDataUncached(locale: Locale) {
const payload = await getPayloadClient()
const [videos, reviews] = await Promise.all([
payload.find({
@@ -95,7 +96,12 @@ async function getReviewsPageData(locale: Locale) {
}
}
async function getReviewsPageSettings(locale: Locale) {
const getReviewsPageData = cacheCmsQuery<[Locale], Awaited<ReturnType<typeof getReviewsPageDataUncached>>>(
['reviews-page-data'],
getReviewsPageDataUncached,
)
async function getReviewsPageSettingsUncached(locale: Locale) {
const payload = await getPayloadClient()
const fallback = {
description:
@@ -141,6 +147,11 @@ async function getReviewsPageSettings(locale: Locale) {
}
}
const getReviewsPageSettings = cacheCmsQuery<[Locale], Awaited<ReturnType<typeof getReviewsPageSettingsUncached>>>(
['reviews-page-global'],
getReviewsPageSettingsUncached,
)
export async function generateMetadata({
params,
}: {
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { VISIBLE_PRODUCT_STATUS } from '@/lib/product-status'
import { renderRichText } from '@/lib/render-rich-text'
@@ -13,7 +14,7 @@ import { SupportContactForm } from './support-contact-form'
export const revalidate = 300
async function getContactPageData(locale: Locale) {
async function getContactPageDataUncached(locale: Locale) {
const payload = await getPayloadClient()
const [page, products] = await Promise.all([
@@ -40,6 +41,11 @@ async function getContactPageData(locale: Locale) {
return { page, products: products.docs }
}
const getContactPageData = cacheCmsQuery<[Locale], Awaited<ReturnType<typeof getContactPageDataUncached>>>(
['support-contact-page-data'],
getContactPageDataUncached,
)
function getContactPageTitle(rawTitle: string | null | undefined, locale: Locale) {
const normalized = rawTitle?.trim()
if (!normalized) return ''
@@ -2,6 +2,7 @@ import type { Metadata } from 'next'
import Link from 'next/link'
import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { buildAbsoluteUrl, buildLocaleAlternates, extractRichTextPlainText } from '@/lib/seo'
import { normalizeUploadUrl } from '@/lib/upload-url'
@@ -125,7 +126,9 @@ function getFaqHeroImageUrl(settings: FaqPageSettings) {
return normalizeUploadUrl(settings.hero?.imageUrl?.trim() || null)
}
async function getFaqPageSettings(locale: Locale): Promise<FaqPageSettings> {
const getFaqPageSettings = cacheCmsQuery<[Locale], FaqPageSettings>(
['faq-page-global'],
async (locale) => {
const payload = await getPayloadClient()
try {
@@ -138,9 +141,10 @@ async function getFaqPageSettings(locale: Locale): Promise<FaqPageSettings> {
} catch {
return {}
}
}
},
)
async function getFaqPageData(locale: Locale) {
async function getFaqPageDataUncached(locale: Locale) {
const payload = await getPayloadClient()
const result = await payload.find({
collection: 'faqs',
@@ -164,6 +168,11 @@ async function getFaqPageData(locale: Locale) {
return [...groups.entries()].map(([group, faqs]) => ({ faqs, group }))
}
const getFaqPageData = cacheCmsQuery<[Locale], Awaited<ReturnType<typeof getFaqPageDataUncached>>>(
['faq-page-data'],
getFaqPageDataUncached,
)
export async function generateMetadata({
params,
}: {
+6 -2
View File
@@ -4,6 +4,7 @@ import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { SiteIcon } from '@/components/site/site-icons'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { renderRichText } from '@/lib/render-rich-text'
import { buildAbsoluteUrl, buildLocaleAlternates, extractRichTextPlainText, trimDescription } from '@/lib/seo'
@@ -17,14 +18,17 @@ type SupportEntry = NonNullable<SupportPageData['primaryEntries']>[number] & {
iconKey?: string | null
}
async function getSupportPageData(locale: Locale) {
const getSupportPageData = cacheCmsQuery(
['support-page-global'],
async (locale: Locale) => {
const payload = await getPayloadClient()
return payload.findGlobal({
slug: 'supportPage',
locale,
})
}
},
)
function getSupportPageTitle(rawTitle: string | null | undefined, locale: Locale) {
const normalized = rawTitle?.trim()
@@ -3,6 +3,7 @@ import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { VideoModalGrid, type VideoModalGridItem } from '@/components/site/video-modal-grid'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { buildAbsoluteUrl, buildLocaleAlternates, trimDescription } from '@/lib/seo'
import { normalizeUploadUrl } from '@/lib/upload-url'
@@ -54,7 +55,7 @@ const FALLBACK_VIDEOS: TutorialVideo[] = [
},
]
async function getTutorialPageData(locale: Locale) {
async function getTutorialPageDataUncached(locale: Locale) {
const payload = await getPayloadClient()
const [page, videos] = await Promise.all([
payload.findGlobal({
@@ -87,6 +88,11 @@ async function getTutorialPageData(locale: Locale) {
}
}
const getTutorialPageData = cacheCmsQuery<[Locale], Awaited<ReturnType<typeof getTutorialPageDataUncached>>>(
['support-tutorial-page-data'],
getTutorialPageDataUncached,
)
function getTutorialTitle(tutorial: TutorialSettings | null | undefined, locale: Locale) {
return tutorial?.hero?.title?.trim() || (locale === 'zh' ? '视频教程' : 'Video Tutorials')
}
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { renderRichText } from '@/lib/render-rich-text'
import { buildLocaleAlternates, extractRichTextPlainText, trimDescription } from '@/lib/seo'
@@ -11,14 +12,17 @@ import type { WarrantyPage as WarrantyPageType } from '@/payload-types'
export const revalidate = 300
async function getWarrantyPageData(locale: Locale) {
const getWarrantyPageData = cacheCmsQuery(
['warranty-page-global'],
async (locale: Locale) => {
const payload = await getPayloadClient()
return payload.findGlobal({
slug: 'warrantyPage',
locale,
})
}
},
)
function getWarrantyPageTitle(rawTitle: string | null | undefined, locale: Locale) {
const normalized = rawTitle?.trim()
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { setRequestLocale } from 'next-intl/server'
import type { Locale } from '@/i18n/config'
import { PageHero } from '@/components/site/page-hero'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { buildAbsoluteUrl, buildLocaleAlternates } from '@/lib/seo'
import { normalizeUploadUrl } from '@/lib/upload-url'
@@ -292,7 +293,7 @@ function getMapPoints(dealers: Dealer[]) {
return [...grouped.values()].sort((left, right) => left.region.localeCompare(right.region))
}
async function getDealersPageSettings(locale: Locale) {
async function getDealersPageSettingsUncached(locale: Locale) {
const payload = await getPayloadClient()
try {
@@ -348,7 +349,12 @@ async function getDealersPageSettings(locale: Locale) {
}
}
async function getDealersPageData(locale: Locale) {
const getDealersPageSettings = cacheCmsQuery<
[Locale],
Awaited<ReturnType<typeof getDealersPageSettingsUncached>>
>(['dealers-page-global'], getDealersPageSettingsUncached)
async function getDealersPageDataUncached(locale: Locale) {
const payload = await getPayloadClient()
const result = await payload.find({
collection: 'dealers',
@@ -381,6 +387,11 @@ async function getDealersPageData(locale: Locale) {
}
}
const getDealersPageData = cacheCmsQuery<[Locale], Awaited<ReturnType<typeof getDealersPageDataUncached>>>(
['dealers-page-data'],
getDealersPageDataUncached,
)
function DealerTabs({
activeTab,
locale,
@@ -677,8 +688,10 @@ export default async function WhereToBuyPage({
const query = await searchParams
setRequestLocale(locale)
const { dealerDocs, dealerGroups, storeDocs } = await getDealersPageData(locale)
const settings = await getDealersPageSettings(locale)
const [{ dealerDocs, dealerGroups, storeDocs }, settings] = await Promise.all([
getDealersPageData(locale),
getDealersPageSettings(locale),
])
const hasDealers = dealerDocs.length > 0
const hasStores = storeDocs.length > 0
const shouldShowTabs = hasDealers && hasStores
@@ -1,7 +1,8 @@
'use server'
import { revalidatePath } from 'next/cache'
import { revalidatePath, revalidateTag } from 'next/cache'
import { redirect } from 'next/navigation'
import { CMS_CACHE_TAG } from '@/lib/cms-cache'
const CACHE_PATHS = [
'/sitemap.xml',
@@ -45,6 +46,8 @@ function normalizeReturnPath(value: FormDataEntryValue | null) {
export async function clearFrontendCache(formData: FormData) {
const returnTo = normalizeReturnPath(formData.get('returnTo'))
revalidateTag(CMS_CACHE_TAG)
for (const path of CACHE_PATHS) {
revalidatePath(path)
}
@@ -59,6 +62,7 @@ export async function clearFrontendCache(formData: FormData) {
export async function refreshSitemapCache(formData: FormData) {
const returnTo = normalizeReturnPath(formData.get('returnTo'))
revalidateTag(CMS_CACHE_TAG)
revalidatePath('/sitemap.xml')
redirect(`${returnTo}${returnTo.includes('?') ? '&' : '?'}sitemap=refreshed`)
+10 -1
View File
@@ -1,9 +1,18 @@
import type { MetadataRoute } from 'next'
import { getSiteUrl } from '@/lib/seo'
function isPublicEversoloHost(siteUrl: string) {
try {
const hostname = new URL(siteUrl).hostname.toLowerCase()
return hostname === 'eversolo.com' || hostname.endsWith('.eversolo.com')
} catch {
return false
}
}
export default function robots(): MetadataRoute.Robots {
const siteUrl = getSiteUrl()
const isProduction = /^https?:\/\/(www\.)?eversolo\./i.test(siteUrl)
const isProduction = isPublicEversoloHost(siteUrl)
return {
host: siteUrl,
+14
View File
@@ -0,0 +1,14 @@
import { unstable_cache } from 'next/cache'
export const CMS_CACHE_TAG = 'cms-public-content'
export const PUBLIC_REVALIDATE_SECONDS = 300
export function cacheCmsQuery<Args extends unknown[], Result>(
keyParts: string[],
callback: (...args: Args) => Promise<Result>,
): (...args: Args) => Promise<Result> {
return unstable_cache(callback, ['cms', ...keyParts], {
revalidate: PUBLIC_REVALIDATE_SECONDS,
tags: [CMS_CACHE_TAG],
}) as (...args: Args) => Promise<Result>
}
+6 -2
View File
@@ -1,4 +1,5 @@
import type { Locale } from '@/i18n/config'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { normalizeUploadUrl } from '@/lib/upload-url'
import type { GlobalSetting, Media } from '@/payload-types'
@@ -187,7 +188,9 @@ function ensureReviewsFooterLink(columns: FooterColumn[], locale: Locale): Foote
)
}
export async function getSiteChromeSettings(locale: Locale): Promise<SiteChromeSettings> {
export const getSiteChromeSettings = cacheCmsQuery<[Locale], SiteChromeSettings>(
['site-chrome-settings'],
async (locale) => {
const payload = await getPayloadClient()
const settings = (await payload.findGlobal({
slug: 'globalSettings',
@@ -246,4 +249,5 @@ export async function getSiteChromeSettings(locale: Locale): Promise<SiteChromeS
siteName: cmsValue(settings.site?.name, fallback.siteName),
siteTagline: cmsValue(settings.site?.tagline, fallback.siteTagline),
}
}
},
)
+6 -2
View File
@@ -1,4 +1,5 @@
import type { Locale } from '@/i18n/config'
import { cacheCmsQuery } from '@/lib/cms-cache'
import { getPayloadClient } from '@/lib/payload'
import { getProductSeriesDefault } from '@/lib/product-series-defaults'
import { VISIBLE_PRODUCT_STATUS } from '@/lib/product-status'
@@ -74,7 +75,9 @@ function getCategorySeries(
}
}
export async function getSiteProductMenuData(locale: Locale): Promise<SiteProductMenuSeries[]> {
export const getSiteProductMenuData = cacheCmsQuery<[Locale], SiteProductMenuSeries[]>(
['site-product-menu'],
async (locale) => {
const payload = await getPayloadClient()
const result = await payload.find({
collection: 'products',
@@ -120,4 +123,5 @@ export async function getSiteProductMenuData(locale: Locale): Promise<SiteProduc
products: group.products.sort((left, right) => left.order - right.order),
}))
.sort((left, right) => left.order - right.order)
}
},
)
+171
View File
@@ -0,0 +1,171 @@
import { createReadStream } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path'
import { Readable } from 'node:stream'
const IMMUTABLE_CACHE_CONTROL = 'public, max-age=31536000, immutable'
type ServeStaticFileOptions = {
contentDisposition?: (filePath: string) => string | null
contentType: (filePath: string) => string
headOnly?: boolean
parts: string[]
request: Request
root: string
}
type ByteRange = {
end: number
start: number
}
async function resolveExistingFile(root: string, requestedPath: string) {
const normalizedRoot = path.resolve(root)
const resolved = path.resolve(path.join(root, requestedPath))
if (!resolved.startsWith(`${normalizedRoot}${path.sep}`) && resolved !== normalizedRoot) {
return null
}
try {
await fs.access(resolved)
return resolved
} catch {
const dir = path.dirname(resolved)
const target = path.basename(resolved).toLowerCase()
try {
const entries = await fs.readdir(dir)
const match = entries.find((entry) => entry.toLowerCase() === target)
return match ? path.join(dir, match) : null
} catch {
return null
}
}
}
function parseRangeHeader(value: string | null, size: number): ByteRange | 'invalid' | null {
if (!value) return null
const match = /^bytes=(\d*)-(\d*)$/.exec(value.trim())
if (!match) return 'invalid'
const [, rawStart, rawEnd] = match
if (!rawStart && !rawEnd) return 'invalid'
if (!rawStart) {
const suffixLength = Number(rawEnd)
if (!Number.isFinite(suffixLength) || suffixLength <= 0) return 'invalid'
return {
end: size - 1,
start: Math.max(size - suffixLength, 0),
}
}
const start = Number(rawStart)
const end = rawEnd ? Number(rawEnd) : size - 1
if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start || start >= size) {
return 'invalid'
}
return {
end: Math.min(end, size - 1),
start,
}
}
function toWebStream(filePath: string, range?: ByteRange) {
const stream = createReadStream(filePath, range)
return Readable.toWeb(stream) as ReadableStream<Uint8Array>
}
function createBaseHeaders(options: {
contentDisposition?: string | null
contentType: string
etag: string
lastModified: string
}) {
const headers = new Headers({
'Accept-Ranges': 'bytes',
'Cache-Control': IMMUTABLE_CACHE_CONTROL,
'Content-Type': options.contentType,
ETag: options.etag,
'Last-Modified': options.lastModified,
})
if (options.contentDisposition) {
headers.set('Content-Disposition', options.contentDisposition)
}
return headers
}
function isFresh(request: Request, etag: string, mtimeMs: number) {
const ifNoneMatch = request.headers.get('if-none-match')
if (ifNoneMatch?.split(',').map((value) => value.trim()).includes(etag)) return true
const ifModifiedSince = request.headers.get('if-modified-since')
if (!ifModifiedSince) return false
const since = Date.parse(ifModifiedSince)
return Number.isFinite(since) && since >= Math.floor(mtimeMs)
}
export async function serveStaticFile({
contentDisposition,
contentType,
headOnly = false,
parts,
request,
root,
}: ServeStaticFileOptions) {
const requestedPath = parts.join('/')
const filePath = await resolveExistingFile(root, requestedPath)
if (!filePath) return new Response('Not Found', { status: 404 })
const stats = await fs.stat(filePath)
if (!stats.isFile()) return new Response('Not Found', { status: 404 })
const etag = `W/"${stats.size}-${Math.floor(stats.mtimeMs)}"`
const lastModified = stats.mtime.toUTCString()
const headers = createBaseHeaders({
contentDisposition: contentDisposition?.(filePath),
contentType: contentType(filePath),
etag,
lastModified,
})
const range = parseRangeHeader(request.headers.get('range'), stats.size)
if (range === 'invalid') {
return new Response(null, {
headers: {
'Content-Range': `bytes */${stats.size}`,
},
status: 416,
})
}
if (!range && isFresh(request, etag, stats.mtimeMs)) {
return new Response(null, {
headers,
status: 304,
})
}
if (range) {
headers.set('Content-Length', String(range.end - range.start + 1))
headers.set('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`)
return new Response(headOnly ? null : toWebStream(filePath, range), {
headers,
status: 206,
})
}
headers.set('Content-Length', String(stats.size))
return new Response(headOnly ? null : toWebStream(filePath), {
headers,
status: 200,
})
}
@@ -0,0 +1,159 @@
import { MigrateDownArgs, MigrateUpArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
DO $$
BEGIN
IF to_regclass('public.products') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'products' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'products' AND column_name = 'status')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'products' AND column_name = 'order')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "products_doc_locale_status_order_idx" ON "products" USING btree ("doc_locale", "status", "order")';
END IF;
IF to_regclass('public.products') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'products' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'products' AND column_name = 'model')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "products_doc_locale_model_idx" ON "products" USING btree ("doc_locale", "model")';
END IF;
IF to_regclass('public.products_locales') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'products_locales' AND column_name = '_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'products_locales' AND column_name = 'slug')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'products_locales' AND column_name = '_parent_id')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "products_locales_locale_slug_parent_idx" ON "products_locales" USING btree ("_locale", "slug", "_parent_id")';
END IF;
IF to_regclass('public.product_categories') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_categories' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_categories' AND column_name = 'order')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "product_categories_doc_locale_order_idx" ON "product_categories" USING btree ("doc_locale", "order")';
END IF;
IF to_regclass('public.downloads') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'downloads' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'downloads' AND column_name = 'status')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'downloads' AND column_name = 'visibility_show_in_downloads_center')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'downloads' AND column_name = 'release_date')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "downloads_doc_locale_status_visibility_release_idx" ON "downloads" USING btree ("doc_locale", "status", "visibility_show_in_downloads_center", "release_date" DESC)';
END IF;
IF to_regclass('public.downloads_rels') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'downloads_rels' AND column_name = 'products_id')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'downloads_rels' AND column_name = 'parent_id')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "downloads_rels_products_parent_idx" ON "downloads_rels" USING btree ("products_id", "parent_id")';
END IF;
IF to_regclass('public.news') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'news' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'news' AND column_name = 'published_at')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'news' AND column_name = 'updated_at')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "news_doc_locale_published_idx" ON "news" USING btree ("doc_locale", "published_at" DESC, "updated_at" DESC)';
END IF;
IF to_regclass('public.news_locales') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'news_locales' AND column_name = '_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'news_locales' AND column_name = 'slug')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'news_locales' AND column_name = '_parent_id')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "news_locales_locale_slug_parent_idx" ON "news_locales" USING btree ("_locale", "slug", "_parent_id")';
END IF;
IF to_regclass('public.news_rels') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'news_rels' AND column_name = 'products_id')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'news_rels' AND column_name = 'parent_id')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "news_rels_products_parent_idx" ON "news_rels" USING btree ("products_id", "parent_id")';
END IF;
IF to_regclass('public.dealers') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'dealers' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'dealers' AND column_name = 'is_authorized')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'dealers' AND column_name = 'order')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "dealers_doc_locale_authorized_order_idx" ON "dealers" USING btree ("doc_locale", "is_authorized", "order")';
END IF;
IF to_regclass('public.faqs') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'faqs' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'faqs' AND column_name = 'group')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'faqs' AND column_name = 'order')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "faqs_doc_locale_group_order_idx" ON "faqs" USING btree ("doc_locale", "group", "order")';
END IF;
IF to_regclass('public.faqs_locales') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'faqs_locales' AND column_name = '_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'faqs_locales' AND column_name = 'question')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'faqs_locales' AND column_name = '_parent_id')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "faqs_locales_locale_question_parent_idx" ON "faqs_locales" USING btree ("_locale", "question", "_parent_id")';
END IF;
IF to_regclass('public.product_videos') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_videos' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_videos' AND column_name = 'product_id')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_videos' AND column_name = 'status')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_videos' AND column_name = 'sort_order')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_videos' AND column_name = 'published_at')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "product_videos_doc_locale_product_status_sort_idx" ON "product_videos" USING btree ("doc_locale", "product_id", "status", "sort_order", "published_at" DESC)';
END IF;
IF to_regclass('public.product_reviews') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_reviews' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_reviews' AND column_name = 'product_id')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_reviews' AND column_name = 'status')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_reviews' AND column_name = 'sort_order')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'product_reviews' AND column_name = 'published_at')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "product_reviews_doc_locale_product_status_sort_idx" ON "product_reviews" USING btree ("doc_locale", "product_id", "status", "sort_order", "published_at" DESC)';
END IF;
IF to_regclass('public.support_tutorial_videos') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'support_tutorial_videos' AND column_name = 'doc_locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'support_tutorial_videos' AND column_name = 'status')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'support_tutorial_videos' AND column_name = 'sort_order')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'support_tutorial_videos' AND column_name = 'published_at')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "support_tutorial_videos_doc_locale_status_sort_idx" ON "support_tutorial_videos" USING btree ("doc_locale", "status", "sort_order", "published_at" DESC)';
END IF;
IF to_regclass('public.redirects') IS NOT NULL
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'redirects' AND column_name = 'enabled')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'redirects' AND column_name = 'locale')
AND EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'redirects' AND column_name = 'from')
THEN
EXECUTE 'CREATE INDEX IF NOT EXISTS "redirects_enabled_locale_from_idx" ON "redirects" USING btree ("enabled", "locale", "from")';
END IF;
END $$;
`)
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
DROP INDEX IF EXISTS "redirects_enabled_locale_from_idx";
DROP INDEX IF EXISTS "support_tutorial_videos_doc_locale_status_sort_idx";
DROP INDEX IF EXISTS "product_reviews_doc_locale_product_status_sort_idx";
DROP INDEX IF EXISTS "product_videos_doc_locale_product_status_sort_idx";
DROP INDEX IF EXISTS "faqs_locales_locale_question_parent_idx";
DROP INDEX IF EXISTS "faqs_doc_locale_group_order_idx";
DROP INDEX IF EXISTS "dealers_doc_locale_authorized_order_idx";
DROP INDEX IF EXISTS "news_rels_products_parent_idx";
DROP INDEX IF EXISTS "news_locales_locale_slug_parent_idx";
DROP INDEX IF EXISTS "news_doc_locale_published_idx";
DROP INDEX IF EXISTS "downloads_rels_products_parent_idx";
DROP INDEX IF EXISTS "downloads_doc_locale_status_visibility_release_idx";
DROP INDEX IF EXISTS "product_categories_doc_locale_order_idx";
DROP INDEX IF EXISTS "products_locales_locale_slug_parent_idx";
DROP INDEX IF EXISTS "products_doc_locale_model_idx";
DROP INDEX IF EXISTS "products_doc_locale_status_order_idx";
`)
}
+6
View File
@@ -40,6 +40,7 @@ import * as migration_20260428_043000_add_sitemap_settings from './20260428_0430
import * as migration_20260428_044000_add_homepage_hero_slide_visibility_overlay from './20260428_044000_add_homepage_hero_slide_visibility_overlay';
import * as migration_20260428_045000_add_faq_page_global from './20260428_045000_add_faq_page_global';
import * as migration_20260428_045500_add_faq_page_support_path from './20260428_045500_add_faq_page_support_path';
import * as migration_20260429_070000_add_public_query_indexes from './20260429_070000_add_public_query_indexes';
export const migrations = [
{
@@ -252,4 +253,9 @@ export const migrations = [
down: migration_20260428_045500_add_faq_page_support_path.down,
name: '20260428_045500_add_faq_page_support_path'
},
{
up: migration_20260429_070000_add_public_query_indexes.up,
down: migration_20260429_070000_add_public_query_indexes.down,
name: '20260429_070000_add_public_query_indexes'
},
];