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",