diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx
index 52e8fd2..f4f6924 100644
--- a/app/[locale]/about/page.tsx
+++ b/app/[locale]/about/page.tsx
@@ -1,5 +1,15 @@
import { AboutContent } from "@/components/about-content";
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() {
return (
diff --git a/app/[locale]/about/references/page.tsx b/app/[locale]/about/references/page.tsx
index ae8c172..7f84d33 100644
--- a/app/[locale]/about/references/page.tsx
+++ b/app/[locale]/about/references/page.tsx
@@ -1,4 +1,18 @@
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() {
return ;
diff --git a/app/[locale]/about/volunteer-community/page.tsx b/app/[locale]/about/volunteer-community/page.tsx
index 2f2e732..bc13e0a 100644
--- a/app/[locale]/about/volunteer-community/page.tsx
+++ b/app/[locale]/about/volunteer-community/page.tsx
@@ -1,4 +1,18 @@
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() {
return ;
diff --git a/app/[locale]/agenda/[slug]/page.tsx b/app/[locale]/agenda/[slug]/page.tsx
index dad854a..e0abb1f 100644
--- a/app/[locale]/agenda/[slug]/page.tsx
+++ b/app/[locale]/agenda/[slug]/page.tsx
@@ -2,10 +2,13 @@ import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { BlogDetailContent } from "@/components/blog-detail-content";
import { ArticleJsonLd } from "@/components/json-ld";
-import { getBlogDetailBySlug } from "@/data/blog-detail";
+import { getBlogDetailBySlug, getBlogTranslations } from "@/data/blog-detail";
import { isNewsletterCategory } from "@/data/blog";
-
-const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
+import {
+ createAlternates,
+ getLocalizedUrl,
+ type SiteLocale,
+} from "@/lib/seo";
type AgendaDetailPageProps = {
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ı" };
}
- 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 {
title: post.title,
description: post.excerpt,
- alternates: { canonical: url },
+ alternates: createAlternates(siteLocale, paths),
openGraph: {
title: post.title,
description: post.excerpt,
url,
type: "article",
+ locale: locale === "en" ? "en_US" : "tr_TR",
+ alternateLocale:
+ translations.length > 1
+ ? [locale === "en" ? "tr_TR" : "en_US"]
+ : undefined,
publishedTime: post.date,
authors: [post.author],
images: [
@@ -60,7 +76,10 @@ export default async function AgendaDetailPage({ params }: AgendaDetailPageProps
notFound();
}
- const url = `${SITE_URL}${locale === "en" ? "/en" : ""}/agenda/${post.slug}`;
+ const url = getLocalizedUrl(
+ locale as SiteLocale,
+ `/agenda/${post.slug}`,
+ );
return (
<>
diff --git a/app/[locale]/agenda/page.tsx b/app/[locale]/agenda/page.tsx
index b594dd6..d13efa7 100644
--- a/app/[locale]/agenda/page.tsx
+++ b/app/[locale]/agenda/page.tsx
@@ -2,6 +2,7 @@ import type { Metadata } from "next";
import { getTranslations } from "next-intl/server";
import { BlogContent } from "@/components/blog-content";
import { getAgendaPageData } from "@/data/blog";
+import { createPageMetadata } from "@/lib/seo";
type AgendaPageProps = {
params: Promise<{ locale: string }>;
@@ -15,10 +16,12 @@ export async function generateMetadata({ params }: AgendaPageProps): Promise;
@@ -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 {
title: post.title,
description: post.excerpt,
+ alternates: createAlternates(siteLocale, paths),
openGraph: {
title: post.title,
description: post.excerpt,
url,
type: "article",
+ locale: locale === "en" ? "en_US" : "tr_TR",
+ alternateLocale:
+ translations.length > 1
+ ? [locale === "en" ? "tr_TR" : "en_US"]
+ : undefined,
publishedTime: post.date,
authors: [post.author],
images: [
@@ -69,7 +86,7 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
;
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) {
const { locale } = await params;
const resolved = searchParams ? await searchParams : undefined;
diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx
index c53c031..00b5f18 100644
--- a/app/[locale]/contact/page.tsx
+++ b/app/[locale]/contact/page.tsx
@@ -1,4 +1,18 @@
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() {
return ;
diff --git a/app/[locale]/content/page.tsx b/app/[locale]/content/page.tsx
index b5158dd..46a10a3 100644
--- a/app/[locale]/content/page.tsx
+++ b/app/[locale]/content/page.tsx
@@ -2,6 +2,20 @@ import { ContentContent } from "@/components/content-content";
import { YOUTUBE_VIDEO_LINKS } from "@/data/youtube-videos";
import { X_JAVASCRIPT_ANATOMY_VIDEOS } from "@/data/x-videos";
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() {
const pdfFiles = await getPdfNotes();
diff --git a/app/[locale]/gallery/page.tsx b/app/[locale]/gallery/page.tsx
index c237691..c2340b5 100644
--- a/app/[locale]/gallery/page.tsx
+++ b/app/[locale]/gallery/page.tsx
@@ -1,11 +1,19 @@
import { GalleryContent } from "@/components/gallery-content";
import { GALLERY_IMAGES } from "@/data/gallery";
-import type { Metadata } from "next";
+import { getStaticPageMetadata } from "@/lib/seo";
-export const metadata: Metadata = {
- title: "Galeri",
- description: "Poyraz Avsever'in tasarımları, projeleri ve görsel içeriklerinden oluşan galeri portföyü.",
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getStaticPageMetadata({
+ locale,
+ page: "gallery",
+ path: "/gallery",
+ });
+}
export default function GalleryPage() {
return ;
diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx
index 33e823e..bbb06b3 100644
--- a/app/[locale]/layout.tsx
+++ b/app/[locale]/layout.tsx
@@ -30,9 +30,6 @@ export async function generateMetadata({
},
description: description,
applicationName: "Poyraz Avsever Portfolyo",
- alternates: {
- canonical: "/",
- },
icons: {
icon: [
{ url: "/favicon.ico", sizes: "any" },
@@ -47,7 +44,6 @@ export async function generateMetadata({
openGraph: {
type: "website",
locale: locale === "tr" ? "tr_TR" : "en_US",
- url: "https://poyrazavsever.com",
siteName: title,
title: title,
description: description,
diff --git a/app/[locale]/links/page.tsx b/app/[locale]/links/page.tsx
index b76da6b..4512a7a 100644
--- a/app/[locale]/links/page.tsx
+++ b/app/[locale]/links/page.tsx
@@ -1,5 +1,5 @@
-import type { Metadata } from "next";
import { LinksContent } from "@/components/links-content";
+import { getStaticPageMetadata } from "@/lib/seo";
type LinksPageProps = {
searchParams?: Promise<{
@@ -8,35 +8,14 @@ type LinksPageProps = {
}>;
};
-export const metadata: Metadata = {
- title: "Links",
- description:
- "Poyraz Avsever'in sosyal medya, portfolyo ve içerik bağlantılarına tek sayfadan ulaş.",
- alternates: {
- canonical: "/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"],
- },
-};
+export async function generateMetadata({
+ params,
+}: {
+ params: Promise<{ locale: string }>;
+}) {
+ const { locale } = await params;
+ return getStaticPageMetadata({ locale, page: "links", path: "/links" });
+}
function firstParam(value?: string | string[]) {
return Array.isArray(value) ? value[0] : value;
diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx
index 24cee3f..49bcde5 100644
--- a/app/[locale]/page.tsx
+++ b/app/[locale]/page.tsx
@@ -3,6 +3,21 @@ import { HomeProjectsSection } from "@/components/home-projects-section";
import { HomeVideosSection } from "@/components/home-videos-section";
import { ReferencesSection } from "@/components/references-section";
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({
params,
diff --git a/app/[locale]/projects/page.tsx b/app/[locale]/projects/page.tsx
index 7dc8864..08405dd 100644
--- a/app/[locale]/projects/page.tsx
+++ b/app/[locale]/projects/page.tsx
@@ -1,4 +1,18 @@
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() {
return ;
diff --git a/data/blog-detail.ts b/data/blog-detail.ts
index bd014e5..e7dacbc 100644
--- a/data/blog-detail.ts
+++ b/data/blog-detail.ts
@@ -62,12 +62,21 @@ export async function listBlogDetails(locale?: string): Promise {
}),
);
- const targetLang = locale || "tr";
return posts
- .filter((post) => post.lang === targetLang)
+ .filter((post) => !locale || post.lang === locale)
.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 {
const safeSlug = slug.trim().toLowerCase();
if (!safeSlug) return null;
diff --git a/lib/seo.ts b/lib/seo.ts
new file mode 100644
index 0000000..3a960f5
--- /dev/null
+++ b/lib/seo.ts
@@ -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>;
+
+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 = {};
+
+ 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,
+ });
+}
diff --git a/messages/en.json b/messages/en.json
index 891d0cb..7959e6d 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -126,6 +126,48 @@
"prev": "Previous",
"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": {
"title": "Poyraz Avsever | Portfolio",
"titleTemplate": "%s | Poyraz Avsever",
diff --git a/messages/tr.json b/messages/tr.json
index 6478c48..9465c5e 100644
--- a/messages/tr.json
+++ b/messages/tr.json
@@ -126,6 +126,48 @@
"prev": "Önceki",
"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": {
"title": "Poyraz Avsever | Portfolyo",
"titleTemplate": "%s | Poyraz Avsever",