From caf9fceb7a930137d397b28a9373cd8ed0da3298 Mon Sep 17 00:00:00 2001 From: poyrazavsever Date: Mon, 31 Aug 2026 10:27:33 +0300 Subject: [PATCH] feat(seo): expand sitemap and structured data --- app/[locale]/about/page.tsx | 24 ++- app/[locale]/agenda/[slug]/page.tsx | 1 + .../animation-sources/[slug]/page.tsx | 1 + app/[locale]/blog/[slug]/page.tsx | 1 + app/[locale]/projects/page.tsx | 49 ++++- app/sitemap.ts | 200 ++++++++++-------- components/json-ld.tsx | 126 ++++++++++- 7 files changed, 304 insertions(+), 98 deletions(-) diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx index f4f6924..2cef614 100644 --- a/app/[locale]/about/page.tsx +++ b/app/[locale]/about/page.tsx @@ -1,6 +1,7 @@ import { AboutContent } from "@/components/about-content"; -import { PersonJsonLd } from "@/components/json-ld"; -import { getStaticPageMetadata } from "@/lib/seo"; +import { ProfilePageJsonLd } from "@/components/json-ld"; +import { getLocalizedUrl, getStaticPageMetadata } from "@/lib/seo"; +import { getTranslations } from "next-intl/server"; export async function generateMetadata({ params, @@ -11,10 +12,25 @@ export async function generateMetadata({ return getStaticPageMetadata({ locale, page: "about", path: "/about" }); } -export default function AboutPage() { +export default async function AboutPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const siteLocale = locale === "en" ? "en" : "tr"; + const t = await getTranslations({ + locale: siteLocale, + namespace: "Seo.about", + }); + return ( <> - diff --git a/app/[locale]/animation-sources/[slug]/page.tsx b/app/[locale]/animation-sources/[slug]/page.tsx index 4290393..98051f8 100644 --- a/app/[locale]/animation-sources/[slug]/page.tsx +++ b/app/[locale]/animation-sources/[slug]/page.tsx @@ -109,6 +109,7 @@ export default async function AnimationSourceDetailPage({ image={source.coverImage} datePublished={source.date} authorName={source.author} + locale={locale === "en" ? "en" : "tr"} /> diff --git a/app/[locale]/blog/[slug]/page.tsx b/app/[locale]/blog/[slug]/page.tsx index 6f47762..ce7bfb2 100644 --- a/app/[locale]/blog/[slug]/page.tsx +++ b/app/[locale]/blog/[slug]/page.tsx @@ -90,6 +90,7 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) { image={post.coverImage} datePublished={post.date} authorName={post.author} + locale={locale as SiteLocale} /> diff --git a/app/[locale]/projects/page.tsx b/app/[locale]/projects/page.tsx index 08405dd..95e0eed 100644 --- a/app/[locale]/projects/page.tsx +++ b/app/[locale]/projects/page.tsx @@ -1,5 +1,14 @@ import { ProjectsContent } from "@/components/projects-content"; -import { getStaticPageMetadata } from "@/lib/seo"; +import { ProjectsJsonLd } from "@/components/json-ld"; +import { + EXTENSIONS, + FIGMA_TEMPLATES, + MOBILE_APPS, + WEB_APPS, +} from "@/data/projects"; +import { getLocalizedValue } from "@/lib/locale"; +import { getLocalizedUrl, getStaticPageMetadata } from "@/lib/seo"; +import { getTranslations } from "next-intl/server"; export async function generateMetadata({ params, @@ -14,6 +23,40 @@ export async function generateMetadata({ }); } -export default function ProjectsPage() { - return ; +export default async function ProjectsPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + const siteLocale = locale === "en" ? "en" : "tr"; + const t = await getTranslations({ + locale: siteLocale, + namespace: "Seo.projects", + }); + const projects = [ + ...WEB_APPS, + ...MOBILE_APPS, + ...EXTENSIONS, + ...FIGMA_TEMPLATES, + ].map((project) => ({ + name: getLocalizedValue(project.title, siteLocale), + description: getLocalizedValue(project.description, siteLocale), + image: project.image, + url: project.href, + technologies: project.technologies, + })); + + return ( + <> + + + + ); } diff --git a/app/sitemap.ts b/app/sitemap.ts index c3df4f2..c50b4c7 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,10 +1,54 @@ -import { listBlogDetails } from "@/data/blog-detail"; -import { isNewsletterCategory } from "@/data/blog"; -import { listAnimationSources } from "@/data/animation-sources"; import type { MetadataRoute } from "next"; +import { listAnimationSources } from "@/data/animation-sources"; +import { isNewsletterCategory } from "@/data/blog"; +import { listBlogDetails } from "@/data/blog-detail"; +import { + getAbsoluteUrl, + getLocalizedUrl, + type SiteLocale, +} from "@/lib/seo"; -const SITE_URL = - process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com"; +const LOCALES: SiteLocale[] = ["tr", "en"]; + +const STATIC_ROUTES = [ + { path: "/", changeFrequency: "weekly", priority: 1 }, + { path: "/about", changeFrequency: "monthly", priority: 0.8 }, + { path: "/about/references", changeFrequency: "monthly", priority: 0.5 }, + { + path: "/about/volunteer-community", + changeFrequency: "monthly", + priority: 0.5, + }, + { path: "/blog", changeFrequency: "weekly", priority: 0.9 }, + { path: "/agenda", changeFrequency: "weekly", priority: 0.9 }, + { path: "/projects", changeFrequency: "monthly", priority: 0.8 }, + { path: "/content", changeFrequency: "weekly", priority: 0.7 }, + { path: "/gallery", changeFrequency: "monthly", priority: 0.6 }, + { path: "/contact", changeFrequency: "yearly", priority: 0.5 }, + { path: "/links", changeFrequency: "monthly", priority: 0.4 }, + { + path: "/animation-sources", + changeFrequency: "monthly", + priority: 0.7, + }, +] as const; + +function toValidDate(value: string) { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? undefined : date; +} + +function getLanguageLinks(paths: Partial>) { + const languages: Record = {}; + + if (paths.tr) languages["tr-TR"] = getLocalizedUrl("tr", paths.tr); + if (paths.en) languages["en-US"] = getLocalizedUrl("en", paths.en); + languages["x-default"] = paths.tr + ? getLocalizedUrl("tr", paths.tr) + : getLocalizedUrl("en", paths.en!); + + return languages; +} export default async function sitemap(): Promise { const [posts, animationSources] = await Promise.all([ @@ -12,89 +56,75 @@ export default async function sitemap(): Promise { listAnimationSources(), ]); - const staticRoutes: MetadataRoute.Sitemap = [ - { - url: SITE_URL, - lastModified: new Date(), - changeFrequency: "weekly", - priority: 1.0, - }, - { - url: `${SITE_URL}/about`, - lastModified: new Date(), - changeFrequency: "monthly", - priority: 0.8, - }, - { - url: `${SITE_URL}/about/references`, - lastModified: new Date(), - changeFrequency: "monthly", - priority: 0.5, - }, - { - url: `${SITE_URL}/about/volunteer-community`, - lastModified: new Date(), - changeFrequency: "monthly", - priority: 0.5, - }, - { - url: `${SITE_URL}/blog`, - lastModified: new Date(), - changeFrequency: "weekly", - priority: 0.9, - }, - { - url: `${SITE_URL}/agenda`, - lastModified: new Date(), - changeFrequency: "weekly", - priority: 0.9, - }, - { - url: `${SITE_URL}/projects`, - lastModified: new Date(), - changeFrequency: "monthly", - priority: 0.8, - }, - { - url: `${SITE_URL}/content`, - lastModified: new Date(), - changeFrequency: "weekly", - priority: 0.7, - }, - { - url: `${SITE_URL}/contact`, - lastModified: new Date(), - changeFrequency: "yearly", - priority: 0.5, - }, - { - url: `${SITE_URL}/links`, - lastModified: new Date(), - changeFrequency: "monthly", - priority: 0.4, - }, - { - url: `${SITE_URL}/animation-sources`, - lastModified: new Date(), - changeFrequency: "monthly", - priority: 0.7, - }, - ]; + const staticRoutes: MetadataRoute.Sitemap = STATIC_ROUTES.flatMap((route) => { + const paths = { tr: route.path, en: route.path }; + const languages = getLanguageLinks(paths); - const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => ({ - url: `${SITE_URL}/${isNewsletterCategory(post.category) ? "agenda" : "blog"}/${post.slug}`, - lastModified: post.date ? new Date(post.date) : new Date(), - changeFrequency: "monthly", - priority: 0.7, - })); + return LOCALES.map((locale) => ({ + url: getLocalizedUrl(locale, route.path), + changeFrequency: route.changeFrequency, + priority: route.priority, + alternates: { languages }, + })); + }); + + const postGroups = new Map(); + for (const post of posts) { + const key = `${post.category.toLocaleLowerCase()}:${post.coverImage}`; + postGroups.set(key, [...(postGroups.get(key) ?? []), post]); + } + + const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => { + const section = isNewsletterCategory(post.category) ? "agenda" : "blog"; + const key = `${post.category.toLocaleLowerCase()}:${post.coverImage}`; + const translations = postGroups.get(key) ?? [post]; + const paths = Object.fromEntries( + translations.map((translation) => [ + translation.lang, + `/${section}/${translation.slug}`, + ]), + ); + + return { + url: getLocalizedUrl(post.lang, `/${section}/${post.slug}`), + lastModified: toValidDate(post.date), + changeFrequency: "monthly", + priority: 0.7, + alternates: { languages: getLanguageLinks(paths) }, + images: [getAbsoluteUrl(post.coverImage)], + }; + }); + + const animationGroups = new Map(); + for (const source of animationSources) { + animationGroups.set(source.slug, [ + ...(animationGroups.get(source.slug) ?? []), + source, + ]); + } const animationSourceRoutes: MetadataRoute.Sitemap = animationSources.map( - (source) => ({ - url: `${SITE_URL}${source.lang === "en" ? "/en" : ""}/animation-sources/${source.slug}`, - lastModified: source.date ? new Date(source.date) : new Date(), - changeFrequency: "monthly", - priority: 0.6, - }), + (source) => { + const translations = animationGroups.get(source.slug) ?? [source]; + const paths = Object.fromEntries( + translations.map((translation) => [ + translation.lang, + `/animation-sources/${translation.slug}`, + ]), + ); + + return { + url: getLocalizedUrl( + source.lang, + `/animation-sources/${source.slug}`, + ), + lastModified: toValidDate(source.date), + changeFrequency: "monthly", + priority: 0.6, + alternates: { languages: getLanguageLinks(paths) }, + images: [getAbsoluteUrl(source.coverImage)], + }; + }, ); return [...staticRoutes, ...blogRoutes, ...animationSourceRoutes]; diff --git a/components/json-ld.tsx b/components/json-ld.tsx index dff54c6..f56f73d 100644 --- a/components/json-ld.tsx +++ b/components/json-ld.tsx @@ -1,5 +1,9 @@ const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com"; +function serializeJsonLd(data: object) { + return JSON.stringify(data).replace(/ + ); +} + +export type ProfilePageJsonLdProps = PersonJsonLdProps & { + pageUrl: string; + pageName: string; + description: string; + locale: "tr" | "en"; +}; + +export function ProfilePageJsonLd({ + pageUrl, + pageName, + description, + locale, + name, + url = SITE_URL, + image = `${SITE_URL}/logo/logo.webp`, + jobTitle = "Fullstack Developer", + sameAs = [], +}: ProfilePageJsonLdProps) { + const data = { + "@context": "https://schema.org", + "@type": "ProfilePage", + "@id": `${pageUrl}#profile-page`, + url: pageUrl, + name: pageName, + description, + inLanguage: locale === "tr" ? "tr-TR" : "en-US", + mainEntity: { + "@type": "Person", + "@id": `${SITE_URL}/#person`, + name, + url, + image, + jobTitle, + sameAs, + }, + }; + + return ( +