feat: 实现联系表单提交与邮件通知功能
添加联系表单提交接口和邮件通知功能,支持从环境变量读取 SMTP 配置 重构 SEO 配置到 site.json,新增 robots.txt 和 sitemap.xml 生成 更新公司电话并添加 PM2 生产运行配置
This commit is contained in:
145
lib/seo.ts
Normal file
145
lib/seo.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
import type { Metadata, MetadataRoute } from "next"
|
||||
import siteData from "@/data/site.json"
|
||||
|
||||
type SiteSeoConfig = {
|
||||
siteUrl: string
|
||||
siteName: string
|
||||
defaultTitle: string
|
||||
titleTemplate: string
|
||||
description: string
|
||||
keywords: string[]
|
||||
canonicalPath: string
|
||||
locale?: string
|
||||
robots?: {
|
||||
index?: boolean
|
||||
follow?: boolean
|
||||
}
|
||||
openGraph?: {
|
||||
type?: "website" | "article"
|
||||
image?: string
|
||||
}
|
||||
twitter?: {
|
||||
card?: "summary" | "summary_large_image" | "app" | "player"
|
||||
site?: string
|
||||
creator?: string
|
||||
}
|
||||
verification?: {
|
||||
google?: string
|
||||
yandex?: string
|
||||
other?: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
const seo = siteData.seo as SiteSeoConfig
|
||||
|
||||
function nonEmpty(value?: string) {
|
||||
if (!value) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const trimmed = value.trim()
|
||||
|
||||
return trimmed ? trimmed : undefined
|
||||
}
|
||||
|
||||
function getMetadataBase() {
|
||||
return new URL(seo.siteUrl)
|
||||
}
|
||||
|
||||
function getCanonicalPath() {
|
||||
return nonEmpty(seo.canonicalPath) ?? "/"
|
||||
}
|
||||
|
||||
function getVerificationOther() {
|
||||
const otherEntries = Object.entries(seo.verification?.other ?? {}).filter(([, value]) => Boolean(nonEmpty(value)))
|
||||
|
||||
return otherEntries.length > 0 ? Object.fromEntries(otherEntries) : undefined
|
||||
}
|
||||
|
||||
function getOpenGraphImages() {
|
||||
const image = nonEmpty(seo.openGraph?.image)
|
||||
|
||||
return image ? [{ url: image }] : undefined
|
||||
}
|
||||
|
||||
export function buildSiteMetadata(): Metadata {
|
||||
const index = seo.robots?.index ?? true
|
||||
const follow = seo.robots?.follow ?? true
|
||||
const verificationOther = getVerificationOther()
|
||||
const canonicalPath = getCanonicalPath()
|
||||
const openGraphImages = getOpenGraphImages()
|
||||
|
||||
return {
|
||||
metadataBase: getMetadataBase(),
|
||||
applicationName: seo.siteName,
|
||||
title: {
|
||||
default: seo.defaultTitle,
|
||||
template: seo.titleTemplate,
|
||||
},
|
||||
description: seo.description,
|
||||
keywords: seo.keywords,
|
||||
authors: [{ name: siteData.company.name }],
|
||||
creator: siteData.company.name,
|
||||
publisher: siteData.company.name,
|
||||
alternates: {
|
||||
canonical: canonicalPath,
|
||||
},
|
||||
robots: {
|
||||
index,
|
||||
follow,
|
||||
googleBot: {
|
||||
index,
|
||||
follow,
|
||||
},
|
||||
},
|
||||
verification: {
|
||||
google: nonEmpty(seo.verification?.google),
|
||||
yandex: nonEmpty(seo.verification?.yandex),
|
||||
other: verificationOther,
|
||||
},
|
||||
openGraph: {
|
||||
title: seo.defaultTitle,
|
||||
description: seo.description,
|
||||
url: canonicalPath,
|
||||
siteName: seo.siteName,
|
||||
locale: nonEmpty(seo.locale),
|
||||
type: seo.openGraph?.type ?? "website",
|
||||
images: openGraphImages,
|
||||
},
|
||||
twitter: {
|
||||
card: seo.twitter?.card ?? "summary_large_image",
|
||||
title: seo.defaultTitle,
|
||||
description: seo.description,
|
||||
images: openGraphImages?.map((image) => image.url),
|
||||
site: nonEmpty(seo.twitter?.site),
|
||||
creator: nonEmpty(seo.twitter?.creator),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRobots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
],
|
||||
sitemap: `${seo.siteUrl.replace(/\/$/, "")}/sitemap.xml`,
|
||||
host: seo.siteUrl,
|
||||
}
|
||||
}
|
||||
|
||||
export function buildSitemap(): MetadataRoute.Sitemap {
|
||||
const canonicalPath = getCanonicalPath()
|
||||
const normalizedSiteUrl = seo.siteUrl.replace(/\/$/, "")
|
||||
|
||||
return [
|
||||
{
|
||||
url: `${normalizedSiteUrl}${canonicalPath}`,
|
||||
lastModified: new Date(),
|
||||
changeFrequency: "weekly",
|
||||
priority: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user