feat(seo): add localized route metadata

This commit is contained in:
poyrazavsever
2026-08-31 10:23:53 +03:00
parent 87e6279dec
commit e2c33d6b29
20 changed files with 399 additions and 54 deletions
+10
View File
@@ -1,5 +1,15 @@
import { AboutContent } from "@/components/about-content"; import { AboutContent } from "@/components/about-content";
import { PersonJsonLd } from "@/components/json-ld"; import { PersonJsonLd } from "@/components/json-ld";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({ locale, page: "about", path: "/about" });
}
export default function AboutPage() { export default function AboutPage() {
return ( return (
+14
View File
@@ -1,4 +1,18 @@
import { ReferencesDetailContent } from "@/components/references-detail-content"; import { ReferencesDetailContent } from "@/components/references-detail-content";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "references",
path: "/about/references",
});
}
export default function AboutReferencesPage() { export default function AboutReferencesPage() {
return <ReferencesDetailContent />; return <ReferencesDetailContent />;
@@ -1,4 +1,18 @@
import { VolunteerCommunityContent } from "@/components/volunteer-community-content"; import { VolunteerCommunityContent } from "@/components/volunteer-community-content";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "volunteerCommunity",
path: "/about/volunteer-community",
});
}
export default function AboutVolunteerCommunityPage() { export default function AboutVolunteerCommunityPage() {
return <VolunteerCommunityContent />; return <VolunteerCommunityContent />;
+25 -6
View File
@@ -2,10 +2,13 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { BlogDetailContent } from "@/components/blog-detail-content"; import { BlogDetailContent } from "@/components/blog-detail-content";
import { ArticleJsonLd } from "@/components/json-ld"; import { ArticleJsonLd } from "@/components/json-ld";
import { getBlogDetailBySlug } from "@/data/blog-detail"; import { getBlogDetailBySlug, getBlogTranslations } from "@/data/blog-detail";
import { isNewsletterCategory } from "@/data/blog"; import { isNewsletterCategory } from "@/data/blog";
import {
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com"; createAlternates,
getLocalizedUrl,
type SiteLocale,
} from "@/lib/seo";
type AgendaDetailPageProps = { type AgendaDetailPageProps = {
params: Promise<{ locale: string; slug: string }>; params: Promise<{ locale: string; slug: string }>;
@@ -21,17 +24,30 @@ export async function generateMetadata({
return { title: locale === "en" ? "Agenda post not found" : "Gündem yazısı bulunamadı" }; return { title: locale === "en" ? "Agenda post not found" : "Gündem yazısı bulunamadı" };
} }
const url = `${SITE_URL}${locale === "en" ? "/en" : ""}/agenda/${post.slug}`; const siteLocale = locale as SiteLocale;
const translations = await getBlogTranslations(post);
const paths = Object.fromEntries(
translations.map((translation) => [
translation.lang,
`/agenda/${translation.slug}`,
]),
);
const url = getLocalizedUrl(siteLocale, `/agenda/${post.slug}`);
return { return {
title: post.title, title: post.title,
description: post.excerpt, description: post.excerpt,
alternates: { canonical: url }, alternates: createAlternates(siteLocale, paths),
openGraph: { openGraph: {
title: post.title, title: post.title,
description: post.excerpt, description: post.excerpt,
url, url,
type: "article", type: "article",
locale: locale === "en" ? "en_US" : "tr_TR",
alternateLocale:
translations.length > 1
? [locale === "en" ? "tr_TR" : "en_US"]
: undefined,
publishedTime: post.date, publishedTime: post.date,
authors: [post.author], authors: [post.author],
images: [ images: [
@@ -60,7 +76,10 @@ export default async function AgendaDetailPage({ params }: AgendaDetailPageProps
notFound(); notFound();
} }
const url = `${SITE_URL}${locale === "en" ? "/en" : ""}/agenda/${post.slug}`; const url = getLocalizedUrl(
locale as SiteLocale,
`/agenda/${post.slug}`,
);
return ( return (
<> <>
+5 -2
View File
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { getTranslations } from "next-intl/server"; import { getTranslations } from "next-intl/server";
import { BlogContent } from "@/components/blog-content"; import { BlogContent } from "@/components/blog-content";
import { getAgendaPageData } from "@/data/blog"; import { getAgendaPageData } from "@/data/blog";
import { createPageMetadata } from "@/lib/seo";
type AgendaPageProps = { type AgendaPageProps = {
params: Promise<{ locale: string }>; params: Promise<{ locale: string }>;
@@ -15,10 +16,12 @@ export async function generateMetadata({ params }: AgendaPageProps): Promise<Met
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: "Agenda" }); const t = await getTranslations({ locale, namespace: "Agenda" });
return { return createPageMetadata({
locale: locale === "en" ? "en" : "tr",
title: t("title"), title: t("title"),
description: t("description"), description: t("description"),
}; path: "/agenda",
});
} }
export default async function AgendaPage({ params, searchParams }: AgendaPageProps) { export default async function AgendaPage({ params, searchParams }: AgendaPageProps) {
@@ -52,6 +52,7 @@ export async function generateMetadata({
languages: { languages: {
"tr-TR": turkishUrl, "tr-TR": turkishUrl,
"en-US": englishUrl, "en-US": englishUrl,
"x-default": turkishUrl,
}, },
}, },
openGraph: { openGraph: {
+1
View File
@@ -32,6 +32,7 @@ export async function generateMetadata({
languages: { languages: {
"tr-TR": `${SITE_URL}/animation-sources`, "tr-TR": `${SITE_URL}/animation-sources`,
"en-US": `${SITE_URL}/en/animation-sources`, "en-US": `${SITE_URL}/en/animation-sources`,
"x-default": `${SITE_URL}/animation-sources`,
}, },
}, },
openGraph: { openGraph: {
+22 -5
View File
@@ -2,10 +2,13 @@ import { notFound, permanentRedirect } from "next/navigation";
import { Metadata } from "next"; import { Metadata } from "next";
import { BlogDetailContent } from "@/components/blog-detail-content"; import { BlogDetailContent } from "@/components/blog-detail-content";
import { ArticleJsonLd } from "@/components/json-ld"; import { ArticleJsonLd } from "@/components/json-ld";
import { getBlogDetailBySlug } from "@/data/blog-detail"; import { getBlogDetailBySlug, getBlogTranslations } from "@/data/blog-detail";
import { isNewsletterCategory } from "@/data/blog"; import { isNewsletterCategory } from "@/data/blog";
import {
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com"; createAlternates,
getLocalizedUrl,
type SiteLocale,
} from "@/lib/seo";
type BlogDetailPageProps = { type BlogDetailPageProps = {
params: Promise<{ locale: string; slug: string }>; params: Promise<{ locale: string; slug: string }>;
@@ -21,16 +24,30 @@ export async function generateMetadata({ params }: BlogDetailPageProps): Promise
}; };
} }
const url = `${SITE_URL}/blog/${post.slug}`; const siteLocale = locale as SiteLocale;
const translations = await getBlogTranslations(post);
const paths = Object.fromEntries(
translations.map((translation) => [
translation.lang,
`/blog/${translation.slug}`,
]),
);
const url = getLocalizedUrl(siteLocale, `/blog/${post.slug}`);
return { return {
title: post.title, title: post.title,
description: post.excerpt, description: post.excerpt,
alternates: createAlternates(siteLocale, paths),
openGraph: { openGraph: {
title: post.title, title: post.title,
description: post.excerpt, description: post.excerpt,
url, url,
type: "article", type: "article",
locale: locale === "en" ? "en_US" : "tr_TR",
alternateLocale:
translations.length > 1
? [locale === "en" ? "tr_TR" : "en_US"]
: undefined,
publishedTime: post.date, publishedTime: post.date,
authors: [post.author], authors: [post.author],
images: [ images: [
@@ -69,7 +86,7 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
<ArticleJsonLd <ArticleJsonLd
title={post.title} title={post.title}
description={post.excerpt} description={post.excerpt}
url={`${SITE_URL}/blog/${post.slug}`} url={getLocalizedUrl(locale as SiteLocale, `/blog/${post.slug}`)}
image={post.coverImage} image={post.coverImage}
datePublished={post.date} datePublished={post.date}
authorName={post.author} authorName={post.author}
+6
View File
@@ -1,11 +1,17 @@
import { BlogContent } from "@/components/blog-content"; import { BlogContent } from "@/components/blog-content";
import { getBlogPageData } from "@/data/blog"; import { getBlogPageData } from "@/data/blog";
import { getStaticPageMetadata } from "@/lib/seo";
type BlogPageProps = { type BlogPageProps = {
params: Promise<{ locale: string }>; params: Promise<{ locale: string }>;
searchParams?: Promise<{ page?: string | string[]; category?: string | string[]; search?: string | string[] }>; searchParams?: Promise<{ page?: string | string[]; category?: string | string[]; search?: string | string[] }>;
}; };
export async function generateMetadata({ params }: BlogPageProps) {
const { locale } = await params;
return getStaticPageMetadata({ locale, page: "blog", path: "/blog" });
}
export default async function BlogPage({ params, searchParams }: BlogPageProps) { export default async function BlogPage({ params, searchParams }: BlogPageProps) {
const { locale } = await params; const { locale } = await params;
const resolved = searchParams ? await searchParams : undefined; const resolved = searchParams ? await searchParams : undefined;
+14
View File
@@ -1,4 +1,18 @@
import { ContactContent } from "@/components/contact-content"; import { ContactContent } from "@/components/contact-content";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "contact",
path: "/contact",
});
}
export default function ContactPage() { export default function ContactPage() {
return <ContactContent />; return <ContactContent />;
+14
View File
@@ -2,6 +2,20 @@ import { ContentContent } from "@/components/content-content";
import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos"; import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos";
import { X_JAVASCRIPT_ANATOMY_VIDEOS } from "@/data/x-videos"; import { X_JAVASCRIPT_ANATOMY_VIDEOS } from "@/data/x-videos";
import { getPdfNotes } from "@/lib/content-page"; import { getPdfNotes } from "@/lib/content-page";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "content",
path: "/content",
});
}
export default async function ContentPage() { export default async function ContentPage() {
const pdfFiles = await getPdfNotes(); const pdfFiles = await getPdfNotes();
+13 -5
View File
@@ -1,11 +1,19 @@
import { GalleryContent } from "@/components/gallery-content"; import { GalleryContent } from "@/components/gallery-content";
import { GALLERY_IMAGES } from "@/data/gallery"; import { GALLERY_IMAGES } from "@/data/gallery";
import type { Metadata } from "next"; import { getStaticPageMetadata } from "@/lib/seo";
export const metadata: Metadata = { export async function generateMetadata({
title: "Galeri", params,
description: "Poyraz Avsever'in tasarımları, projeleri ve görsel içeriklerinden oluşan galeri portföyü.", }: {
}; params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "gallery",
path: "/gallery",
});
}
export default function GalleryPage() { export default function GalleryPage() {
return <GalleryContent images={GALLERY_IMAGES} />; return <GalleryContent images={GALLERY_IMAGES} />;
-4
View File
@@ -30,9 +30,6 @@ export async function generateMetadata({
}, },
description: description, description: description,
applicationName: "Poyraz Avsever Portfolyo", applicationName: "Poyraz Avsever Portfolyo",
alternates: {
canonical: "/",
},
icons: { icons: {
icon: [ icon: [
{ url: "/favicon.ico", sizes: "any" }, { url: "/favicon.ico", sizes: "any" },
@@ -47,7 +44,6 @@ export async function generateMetadata({
openGraph: { openGraph: {
type: "website", type: "website",
locale: locale === "tr" ? "tr_TR" : "en_US", locale: locale === "tr" ? "tr_TR" : "en_US",
url: "https://poyrazavsever.com",
siteName: title, siteName: title,
title: title, title: title,
description: description, description: description,
+9 -30
View File
@@ -1,5 +1,5 @@
import type { Metadata } from "next";
import { LinksContent } from "@/components/links-content"; import { LinksContent } from "@/components/links-content";
import { getStaticPageMetadata } from "@/lib/seo";
type LinksPageProps = { type LinksPageProps = {
searchParams?: Promise<{ searchParams?: Promise<{
@@ -8,35 +8,14 @@ type LinksPageProps = {
}>; }>;
}; };
export const metadata: Metadata = { export async function generateMetadata({
title: "Links", params,
description: }: {
"Poyraz Avsever'in sosyal medya, portfolyo ve içerik bağlantılarına tek sayfadan ulaş.", params: Promise<{ locale: string }>;
alternates: { }) {
canonical: "/links", const { locale } = await params;
}, return getStaticPageMetadata({ locale, page: "links", path: "/links" });
openGraph: { }
title: "Poyraz Avsever | Links",
description:
"Poyraz Avsever'in sosyal medya, portfolyo ve içerik bağlantılarına tek sayfadan ulaş.",
url: "https://poyrazavsever.com/links",
images: [
{
url: "/logo/cover.png",
width: 1536,
height: 512,
alt: "Poyraz Avsever Links sayfası kapak görseli",
},
],
},
twitter: {
card: "summary_large_image",
title: "Poyraz Avsever | Links",
description:
"Poyraz Avsever'in sosyal medya, portfolyo ve içerik bağlantılarına tek sayfadan ulaş.",
images: ["/logo/cover.png"],
},
};
function firstParam(value?: string | string[]) { function firstParam(value?: string | string[]) {
return Array.isArray(value) ? value[0] : value; return Array.isArray(value) ? value[0] : value;
+15
View File
@@ -3,6 +3,21 @@ import { HomeProjectsSection } from "@/components/home-projects-section";
import { HomeVideosSection } from "@/components/home-videos-section"; import { HomeVideosSection } from "@/components/home-videos-section";
import { ReferencesSection } from "@/components/references-section"; import { ReferencesSection } from "@/components/references-section";
import { getHomeBlogNews } from "@/data/blog"; import { getHomeBlogNews } from "@/data/blog";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "home",
path: "/",
absoluteTitle: true,
});
}
export default async function Home({ export default async function Home({
params, params,
+14
View File
@@ -1,4 +1,18 @@
import { ProjectsContent } from "@/components/projects-content"; import { ProjectsContent } from "@/components/projects-content";
import { getStaticPageMetadata } from "@/lib/seo";
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
return getStaticPageMetadata({
locale,
page: "projects",
path: "/projects",
});
}
export default function ProjectsPage() { export default function ProjectsPage() {
return <ProjectsContent />; return <ProjectsContent />;
+11 -2
View File
@@ -62,12 +62,21 @@ export async function listBlogDetails(locale?: string): Promise<BlogDetail[]> {
}), }),
); );
const targetLang = locale || "tr";
return posts return posts
.filter((post) => post.lang === targetLang) .filter((post) => !locale || post.lang === locale)
.sort((a, b) => a.slug.localeCompare(b.slug)); .sort((a, b) => a.slug.localeCompare(b.slug));
} }
export async function getBlogTranslations(post: BlogDetail) {
const posts = await listBlogDetails();
return posts.filter(
(candidate) =>
candidate.coverImage === post.coverImage &&
candidate.category.toLocaleLowerCase() === post.category.toLocaleLowerCase(),
);
}
export async function getBlogDetailBySlug(slug: string): Promise<BlogDetail | null> { export async function getBlogDetailBySlug(slug: string): Promise<BlogDetail | null> {
const safeSlug = slug.trim().toLowerCase(); const safeSlug = slug.trim().toLowerCase();
if (!safeSlug) return null; if (!safeSlug) return null;
+127
View File
@@ -0,0 +1,127 @@
import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
export const SITE_URL = (
process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com"
).replace(/\/$/, "");
export type SiteLocale = "tr" | "en";
type LocalePaths = Partial<Record<SiteLocale, string>>;
type StaticSeoPage =
| "home"
| "about"
| "references"
| "volunteerCommunity"
| "blog"
| "projects"
| "content"
| "gallery"
| "links"
| "contact";
function normalizePath(path: string) {
if (!path || path === "/") return "/";
return `/${path.replace(/^\/+|\/+$/g, "")}`;
}
export function getLocalizedPath(locale: SiteLocale, path: string) {
const normalizedPath = normalizePath(path);
if (locale === "tr") return normalizedPath;
return normalizedPath === "/" ? "/en" : `/en${normalizedPath}`;
}
export function getAbsoluteUrl(path: string) {
return new URL(path, `${SITE_URL}/`).toString();
}
export function getLocalizedUrl(locale: SiteLocale, path: string) {
return getAbsoluteUrl(getLocalizedPath(locale, path));
}
export function createAlternates(
locale: SiteLocale,
paths: LocalePaths,
): Metadata["alternates"] {
const currentPath = paths[locale];
if (!currentPath) return undefined;
const languages: Record<string, string> = {};
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(locale, currentPath);
return {
canonical: getLocalizedUrl(locale, currentPath),
languages,
};
}
export function createPageMetadata({
locale,
title,
description,
path,
absoluteTitle = false,
}: {
locale: SiteLocale;
title: string;
description: string;
path: string;
absoluteTitle?: boolean;
}): Metadata {
const url = getLocalizedUrl(locale, path);
return {
title: absoluteTitle ? { absolute: title } : title,
description,
alternates: createAlternates(locale, { tr: path, en: path }),
openGraph: {
type: "website",
locale: locale === "tr" ? "tr_TR" : "en_US",
alternateLocale: locale === "tr" ? ["en_US"] : ["tr_TR"],
url,
title,
description,
},
twitter: {
card: "summary_large_image",
title,
description,
creator: "@poyrazavsever",
},
};
}
export async function getStaticPageMetadata({
locale,
page,
path,
absoluteTitle = false,
}: {
locale: string;
page: StaticSeoPage;
path: string;
absoluteTitle?: boolean;
}) {
const siteLocale = locale === "en" ? "en" : "tr";
const t = await getTranslations({ locale: siteLocale, namespace: "Seo" });
return createPageMetadata({
locale: siteLocale,
title: t(`${page}.title`),
description: t(`${page}.description`),
path,
absoluteTitle,
});
}
+42
View File
@@ -126,6 +126,48 @@
"prev": "Previous", "prev": "Previous",
"next": "Next" "next": "Next"
}, },
"Seo": {
"home": {
"title": "Poyraz Avsever | Portfolio",
"description": "The portfolio of Poyraz Avsever, featuring full-stack products, AI systems, modern web projects, and practical software content."
},
"about": {
"title": "About",
"description": "Explore Poyraz Avsever's software journey, professional experience, education, certificates, and community work."
},
"references": {
"title": "References",
"description": "Read professional testimonials and references from people who have worked with Poyraz Avsever."
},
"volunteerCommunity": {
"title": "Volunteering and Community",
"description": "Explore Poyraz Avsever's volunteer work, technology community contributions, and event experience."
},
"blog": {
"title": "Blog",
"description": "Technical articles about frontend development, JavaScript, web architecture, user experience, and software engineering."
},
"projects": {
"title": "Projects",
"description": "Explore Poyraz Avsever's web applications, mobile products, open-source tools, browser extensions, and design work."
},
"content": {
"title": "Content",
"description": "Poyraz Avsever's YouTube videos, LinkedIn PDF notes, and JavaScript Anatomy video series."
},
"gallery": {
"title": "Gallery",
"description": "Selected images from Poyraz Avsever's events, presentations, projects, and community work."
},
"links": {
"title": "Links",
"description": "Access Poyraz Avsever's social profiles, projects, content, and essential links from one page."
},
"contact": {
"title": "Contact",
"description": "Contact Poyraz Avsever by email or social media for projects, collaborations, and professional opportunities."
}
},
"Metadata": { "Metadata": {
"title": "Poyraz Avsever | Portfolio", "title": "Poyraz Avsever | Portfolio",
"titleTemplate": "%s | Poyraz Avsever", "titleTemplate": "%s | Poyraz Avsever",
+42
View File
@@ -126,6 +126,48 @@
"prev": "Önceki", "prev": "Önceki",
"next": "Sonraki" "next": "Sonraki"
}, },
"Seo": {
"home": {
"title": "Poyraz Avsever | Portfolyo",
"description": "Full-stack ürünler, yapay zeka sistemleri, modern web projeleri ve uygulanabilir yazılım içerikleri üreten Poyraz Avsever'in portfolyosu."
},
"about": {
"title": "Hakkımda",
"description": "Poyraz Avsever'in yazılım yolculuğunu, deneyimini, eğitimini, sertifikalarını ve topluluk çalışmalarını keşfedin."
},
"references": {
"title": "Referanslar",
"description": "Birlikte çalıştığım kişilerin Poyraz Avsever hakkındaki profesyonel değerlendirmelerini ve referanslarını inceleyin."
},
"volunteerCommunity": {
"title": "Gönüllülük ve Topluluk",
"description": "Poyraz Avsever'in gönüllülük çalışmalarını, teknoloji topluluklarına katkılarını ve etkinlik deneyimlerini inceleyin."
},
"blog": {
"title": "Blog",
"description": "Frontend, JavaScript, web mimarisi, kullanıcı deneyimi ve yazılım geliştirme üzerine teknik yazılar."
},
"projects": {
"title": "Projeler",
"description": "Poyraz Avsever'in web uygulamaları, mobil ürünleri, açık kaynak araçları, tarayıcı eklentileri ve tasarım çalışmalarını keşfedin."
},
"content": {
"title": "İçerikler",
"description": "Poyraz Avsever'in YouTube videoları, LinkedIn PDF notları ve JavaScript Anatomisi video serisi."
},
"gallery": {
"title": "Galeri",
"description": "Poyraz Avsever'in etkinlik, sunum, proje ve topluluk çalışmalarından seçilmiş görseller."
},
"links": {
"title": "Bağlantılar",
"description": "Poyraz Avsever'in sosyal medya hesaplarına, projelerine, içeriklerine ve hızlı erişim bağlantılarına tek sayfadan ulaşın."
},
"contact": {
"title": "İletişim",
"description": "Proje, iş birliği ve profesyonel iletişim için Poyraz Avsever'e e-posta veya sosyal medya üzerinden ulaşın."
}
},
"Metadata": { "Metadata": {
"title": "Poyraz Avsever | Portfolyo", "title": "Poyraz Avsever | Portfolyo",
"titleTemplate": "%s | Poyraz Avsever", "titleTemplate": "%s | Poyraz Avsever",