feat(seo): add localized route metadata
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 />;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user