diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..7c35646 Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..9dcae12 Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/og-image.png b/public/og-image.png new file mode 100644 index 0000000..6962af5 Binary files /dev/null and b/public/og-image.png differ diff --git a/src/app/[locale]/(marketing)/blog/[slug]/page.tsx b/src/app/[locale]/(marketing)/blog/[slug]/page.tsx index 0b06e07..a9de43a 100644 --- a/src/app/[locale]/(marketing)/blog/[slug]/page.tsx +++ b/src/app/[locale]/(marketing)/blog/[slug]/page.tsx @@ -3,6 +3,7 @@ import { notFound } from "next/navigation"; import { getAllBlogs, getBlogModule } from "@/lib/blog"; import { locales, type Locale } from "@/i18n.config"; +import { generatePageMetadata } from "@/lib/metadata"; interface PageProps { params: Promise<{ @@ -32,18 +33,15 @@ export async function generateMetadata({ params }: PageProps): Promise } const { blog } = blogModule; - const metadata: Metadata = { + + return generatePageMetadata({ + locale, + path: `/blog/${slug}`, title: blog.title, description: blog.description, - }; - - if (blog.image) { - metadata.openGraph = { - images: [blog.image], - }; - } - - return metadata; + ogImage: blog.image, + ogType: "article", + }); } export default async function BlogPostPage({ params }: PageProps) { diff --git a/src/app/[locale]/(marketing)/tools/[slug]/page.tsx b/src/app/[locale]/(marketing)/tools/[slug]/page.tsx index 9f67fa9..21ef1ec 100644 --- a/src/app/[locale]/(marketing)/tools/[slug]/page.tsx +++ b/src/app/[locale]/(marketing)/tools/[slug]/page.tsx @@ -5,6 +5,7 @@ import { getLocale, getTranslations } from "next-intl/server"; import { ExternalLink, ArrowLeft, Tag } from "lucide-react"; import { Button } from "@/components/button"; import { SoftwareApplicationJsonLd } from "@/components/json-ld"; +import { generatePageMetadata } from "@/lib/metadata"; interface ToolDetailPageProps { params: Promise<{ @@ -30,10 +31,13 @@ export async function generateMetadata({ params }: ToolDetailPageProps) { ? tool.descriptionZh : tool.descriptionEn; - return { + return generatePageMetadata({ + locale, + path: `/tools/${slug}`, title: `${name} - ${t("title")}`, description: description || `${t("subtitle")} - ${name}`, - }; + ogImage: tool.coverImageUrl || tool.logoUrl || undefined, + }); } export default async function ToolDetailPage({ params }: ToolDetailPageProps) { diff --git a/src/app/[locale]/(marketing)/tools/page.tsx b/src/app/[locale]/(marketing)/tools/page.tsx index 8eb88d9..a0b9d34 100644 --- a/src/app/[locale]/(marketing)/tools/page.tsx +++ b/src/app/[locale]/(marketing)/tools/page.tsx @@ -2,13 +2,21 @@ import { getPublishedTools, getCategoriesWithCount } from "@/features/tools/acti import { ToolsList } from "@/features/tools/components/tools-list"; import { CollectionPageJsonLd } from "@/components/json-ld"; import { getLocale, getTranslations } from "next-intl/server"; +import { generatePageMetadata } from "@/lib/metadata"; -export async function generateMetadata() { - const t = await getTranslations("seo.tools"); - return { +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const t = await getTranslations({ locale, namespace: "seo.tools" }); + return generatePageMetadata({ + locale, + path: "/tools", title: t("title"), description: t("description"), - }; + }); } interface ToolsPageProps { diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts index bbfce04..4a5512d 100644 --- a/src/app/sitemap.ts +++ b/src/app/sitemap.ts @@ -1,12 +1,19 @@ import { MetadataRoute } from 'next' +import { eq } from 'drizzle-orm' -export default function sitemap(): MetadataRoute.Sitemap { - const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://toolso.ai' +// 每小时重新生成,保证新收录的工具能进入 sitemap +export const revalidate = 3600 +import { db } from '@/lib/db' +import { tool, category } from '@/lib/db/schema' +import { getAllBlogs } from '@/lib/blog' +import { locales } from '@/i18n.config' +import { absoluteUrl } from '@/lib/metadata' +export default async function sitemap(): Promise { // Static routes with SEO priority for AI tools directory const staticRoutes = [ { path: '', changeFreq: 'daily' as const, priority: 1.0 }, - { path: '/tools', changeFreq: 'daily' as const, priority: 0.95 }, // AI tools directory - high priority + { path: '/tools', changeFreq: 'daily' as const, priority: 0.95 }, { path: '/blog', changeFreq: 'weekly' as const, priority: 0.8 }, { path: '/contact', changeFreq: 'monthly' as const, priority: 0.5 }, { path: '/privacy', changeFreq: 'monthly' as const, priority: 0.3 }, @@ -14,26 +21,53 @@ export default function sitemap(): MetadataRoute.Sitemap { { path: '/cookies', changeFreq: 'monthly' as const, priority: 0.3 }, ] - // Generate sitemap entries for both languages - const locales = ['zh', 'en'] - const sitemapEntries: MetadataRoute.Sitemap = [] + const [tools, categories, blogs] = await Promise.all([ + db + .select({ slug: tool.slug, updatedAt: tool.updatedAt }) + .from(tool) + .where(eq(tool.status, 'published')), + db.select({ slug: category.slug, updatedAt: category.updatedAt }).from(category), + getAllBlogs(), + ]) - locales.forEach((locale) => { - staticRoutes.forEach((route) => { - sitemapEntries.push({ - url: `${baseUrl}/${locale}${route.path}`, - lastModified: new Date(), - changeFrequency: route.changeFreq, - priority: route.priority, - alternates: { - languages: { - zh: `${baseUrl}/zh${route.path}`, - en: `${baseUrl}/en${route.path}`, - }, + const dynamicRoutes = [ + ...tools.map((t) => ({ + path: `/tools/${t.slug}`, + changeFreq: 'weekly' as const, + priority: 0.9, + lastModified: t.updatedAt, + })), + ...categories.map((c) => ({ + path: `/category/${c.slug}`, + changeFreq: 'daily' as const, + priority: 0.85, + lastModified: c.updatedAt, + })), + ...blogs.map((b) => ({ + path: `/blog/${b.slug}`, + changeFreq: 'monthly' as const, + priority: 0.7, + lastModified: b.date ? new Date(b.date) : undefined, + })), + ] + + const allRoutes = [ + ...staticRoutes.map((r) => ({ ...r, lastModified: undefined as Date | undefined })), + ...dynamicRoutes, + ] + + return locales.flatMap((locale) => + allRoutes.map((route) => ({ + url: absoluteUrl(locale, route.path), + lastModified: route.lastModified ?? new Date(), + changeFrequency: route.changeFreq, + priority: route.priority, + alternates: { + languages: { + en: absoluteUrl('en', route.path), + zh: absoluteUrl('zh', route.path), }, - }) - }) - }) - - return sitemapEntries + }, + })) + ) } diff --git a/src/components/json-ld.tsx b/src/components/json-ld.tsx index 8374372..c29cda7 100644 --- a/src/components/json-ld.tsx +++ b/src/components/json-ld.tsx @@ -1,4 +1,3 @@ -import Script from 'next/script' interface WebSiteJsonLdProps { name: string @@ -30,10 +29,8 @@ export function WebSiteJsonLd({ name, description, url, locale }: WebSiteJsonLdP } return ( -