feat(seo): expand sitemap and structured data

This commit is contained in:
poyrazavsever
2026-08-31 10:27:33 +03:00
parent e2c33d6b29
commit caf9fceb7a
7 changed files with 304 additions and 98 deletions
+20 -4
View File
@@ -1,6 +1,7 @@
import { AboutContent } from "@/components/about-content";
import { PersonJsonLd } from "@/components/json-ld";
import { getStaticPageMetadata } from "@/lib/seo";
import { ProfilePageJsonLd } from "@/components/json-ld";
import { getLocalizedUrl, getStaticPageMetadata } from "@/lib/seo";
import { getTranslations } from "next-intl/server";
export async function generateMetadata({
params,
@@ -11,10 +12,25 @@ export async function generateMetadata({
return getStaticPageMetadata({ locale, page: "about", path: "/about" });
}
export default function AboutPage() {
export default async function AboutPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const siteLocale = locale === "en" ? "en" : "tr";
const t = await getTranslations({
locale: siteLocale,
namespace: "Seo.about",
});
return (
<>
<PersonJsonLd
<ProfilePageJsonLd
pageUrl={getLocalizedUrl(siteLocale, "/about")}
pageName={t("title")}
description={t("description")}
locale={siteLocale}
name="Poyraz Avsever"
jobTitle="Fullstack Developer"
sameAs={[
+1
View File
@@ -90,6 +90,7 @@ export default async function AgendaDetailPage({ params }: AgendaDetailPageProps
image={post.coverImage}
datePublished={post.date}
authorName={post.author}
locale={locale as SiteLocale}
/>
<BlogDetailContent post={post} section="agenda" />
</>
@@ -109,6 +109,7 @@ export default async function AnimationSourceDetailPage({
image={source.coverImage}
datePublished={source.date}
authorName={source.author}
locale={locale === "en" ? "en" : "tr"}
/>
<AnimationSourceDetailContent source={source} />
</>
+1
View File
@@ -90,6 +90,7 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
image={post.coverImage}
datePublished={post.date}
authorName={post.author}
locale={locale as SiteLocale}
/>
<BlogDetailContent post={post} />
</>
+46 -3
View File
@@ -1,5 +1,14 @@
import { ProjectsContent } from "@/components/projects-content";
import { getStaticPageMetadata } from "@/lib/seo";
import { ProjectsJsonLd } from "@/components/json-ld";
import {
EXTENSIONS,
FIGMA_TEMPLATES,
MOBILE_APPS,
WEB_APPS,
} from "@/data/projects";
import { getLocalizedValue } from "@/lib/locale";
import { getLocalizedUrl, getStaticPageMetadata } from "@/lib/seo";
import { getTranslations } from "next-intl/server";
export async function generateMetadata({
params,
@@ -14,6 +23,40 @@ export async function generateMetadata({
});
}
export default function ProjectsPage() {
return <ProjectsContent />;
export default async function ProjectsPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const siteLocale = locale === "en" ? "en" : "tr";
const t = await getTranslations({
locale: siteLocale,
namespace: "Seo.projects",
});
const projects = [
...WEB_APPS,
...MOBILE_APPS,
...EXTENSIONS,
...FIGMA_TEMPLATES,
].map((project) => ({
name: getLocalizedValue(project.title, siteLocale),
description: getLocalizedValue(project.description, siteLocale),
image: project.image,
url: project.href,
technologies: project.technologies,
}));
return (
<>
<ProjectsJsonLd
name={t("title")}
description={t("description")}
url={getLocalizedUrl(siteLocale, "/projects")}
locale={siteLocale}
projects={projects}
/>
<ProjectsContent />
</>
);
}
+115 -85
View File
@@ -1,10 +1,54 @@
import { listBlogDetails } from "@/data/blog-detail";
import { isNewsletterCategory } from "@/data/blog";
import { listAnimationSources } from "@/data/animation-sources";
import type { MetadataRoute } from "next";
import { listAnimationSources } from "@/data/animation-sources";
import { isNewsletterCategory } from "@/data/blog";
import { listBlogDetails } from "@/data/blog-detail";
import {
getAbsoluteUrl,
getLocalizedUrl,
type SiteLocale,
} from "@/lib/seo";
const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
const LOCALES: SiteLocale[] = ["tr", "en"];
const STATIC_ROUTES = [
{ path: "/", changeFrequency: "weekly", priority: 1 },
{ path: "/about", changeFrequency: "monthly", priority: 0.8 },
{ path: "/about/references", changeFrequency: "monthly", priority: 0.5 },
{
path: "/about/volunteer-community",
changeFrequency: "monthly",
priority: 0.5,
},
{ path: "/blog", changeFrequency: "weekly", priority: 0.9 },
{ path: "/agenda", changeFrequency: "weekly", priority: 0.9 },
{ path: "/projects", changeFrequency: "monthly", priority: 0.8 },
{ path: "/content", changeFrequency: "weekly", priority: 0.7 },
{ path: "/gallery", changeFrequency: "monthly", priority: 0.6 },
{ path: "/contact", changeFrequency: "yearly", priority: 0.5 },
{ path: "/links", changeFrequency: "monthly", priority: 0.4 },
{
path: "/animation-sources",
changeFrequency: "monthly",
priority: 0.7,
},
] as const;
function toValidDate(value: string) {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? undefined : date;
}
function getLanguageLinks(paths: Partial<Record<SiteLocale, string>>) {
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("en", paths.en!);
return languages;
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const [posts, animationSources] = await Promise.all([
@@ -12,89 +56,75 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
listAnimationSources(),
]);
const staticRoutes: MetadataRoute.Sitemap = [
{
url: SITE_URL,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 1.0,
},
{
url: `${SITE_URL}/about`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${SITE_URL}/about/references`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.5,
},
{
url: `${SITE_URL}/about/volunteer-community`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.5,
},
{
url: `${SITE_URL}/blog`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
url: `${SITE_URL}/agenda`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.9,
},
{
url: `${SITE_URL}/projects`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
{
url: `${SITE_URL}/content`,
lastModified: new Date(),
changeFrequency: "weekly",
priority: 0.7,
},
{
url: `${SITE_URL}/contact`,
lastModified: new Date(),
changeFrequency: "yearly",
priority: 0.5,
},
{
url: `${SITE_URL}/links`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.4,
},
{
url: `${SITE_URL}/animation-sources`,
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.7,
},
];
const staticRoutes: MetadataRoute.Sitemap = STATIC_ROUTES.flatMap((route) => {
const paths = { tr: route.path, en: route.path };
const languages = getLanguageLinks(paths);
const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${SITE_URL}/${isNewsletterCategory(post.category) ? "agenda" : "blog"}/${post.slug}`,
lastModified: post.date ? new Date(post.date) : new Date(),
changeFrequency: "monthly",
priority: 0.7,
}));
return LOCALES.map((locale) => ({
url: getLocalizedUrl(locale, route.path),
changeFrequency: route.changeFrequency,
priority: route.priority,
alternates: { languages },
}));
});
const postGroups = new Map<string, typeof posts>();
for (const post of posts) {
const key = `${post.category.toLocaleLowerCase()}:${post.coverImage}`;
postGroups.set(key, [...(postGroups.get(key) ?? []), post]);
}
const blogRoutes: MetadataRoute.Sitemap = posts.map((post) => {
const section = isNewsletterCategory(post.category) ? "agenda" : "blog";
const key = `${post.category.toLocaleLowerCase()}:${post.coverImage}`;
const translations = postGroups.get(key) ?? [post];
const paths = Object.fromEntries(
translations.map((translation) => [
translation.lang,
`/${section}/${translation.slug}`,
]),
);
return {
url: getLocalizedUrl(post.lang, `/${section}/${post.slug}`),
lastModified: toValidDate(post.date),
changeFrequency: "monthly",
priority: 0.7,
alternates: { languages: getLanguageLinks(paths) },
images: [getAbsoluteUrl(post.coverImage)],
};
});
const animationGroups = new Map<string, typeof animationSources>();
for (const source of animationSources) {
animationGroups.set(source.slug, [
...(animationGroups.get(source.slug) ?? []),
source,
]);
}
const animationSourceRoutes: MetadataRoute.Sitemap = animationSources.map(
(source) => ({
url: `${SITE_URL}${source.lang === "en" ? "/en" : ""}/animation-sources/${source.slug}`,
lastModified: source.date ? new Date(source.date) : new Date(),
changeFrequency: "monthly",
priority: 0.6,
}),
(source) => {
const translations = animationGroups.get(source.slug) ?? [source];
const paths = Object.fromEntries(
translations.map((translation) => [
translation.lang,
`/animation-sources/${translation.slug}`,
]),
);
return {
url: getLocalizedUrl(
source.lang,
`/animation-sources/${source.slug}`,
),
lastModified: toValidDate(source.date),
changeFrequency: "monthly",
priority: 0.6,
alternates: { languages: getLanguageLinks(paths) },
images: [getAbsoluteUrl(source.coverImage)],
};
},
);
return [...staticRoutes, ...blogRoutes, ...animationSourceRoutes];
+120 -6
View File
@@ -1,5 +1,9 @@
const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || "https://poyrazavsever.com";
function serializeJsonLd(data: object) {
return JSON.stringify(data).replace(/</g, "\\u003c");
}
export type PersonJsonLdProps = {
name: string;
url?: string;
@@ -28,7 +32,114 @@ export function PersonJsonLd({
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
);
}
export type ProfilePageJsonLdProps = PersonJsonLdProps & {
pageUrl: string;
pageName: string;
description: string;
locale: "tr" | "en";
};
export function ProfilePageJsonLd({
pageUrl,
pageName,
description,
locale,
name,
url = SITE_URL,
image = `${SITE_URL}/logo/logo.webp`,
jobTitle = "Fullstack Developer",
sameAs = [],
}: ProfilePageJsonLdProps) {
const data = {
"@context": "https://schema.org",
"@type": "ProfilePage",
"@id": `${pageUrl}#profile-page`,
url: pageUrl,
name: pageName,
description,
inLanguage: locale === "tr" ? "tr-TR" : "en-US",
mainEntity: {
"@type": "Person",
"@id": `${SITE_URL}/#person`,
name,
url,
image,
jobTitle,
sameAs,
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
);
}
export type ProjectsJsonLdItem = {
name: string;
description: string;
image: string;
url?: string;
technologies: string[];
};
export function ProjectsJsonLd({
name,
description,
url,
locale,
projects,
}: {
name: string;
description: string;
url: string;
locale: "tr" | "en";
projects: ProjectsJsonLdItem[];
}) {
const data = {
"@context": "https://schema.org",
"@type": "CollectionPage",
"@id": `${url}#projects`,
url,
name,
description,
inLanguage: locale === "tr" ? "tr-TR" : "en-US",
mainEntity: {
"@type": "ItemList",
numberOfItems: projects.length,
itemListElement: projects.map((project, index) => ({
"@type": "ListItem",
position: index + 1,
item: {
"@type": "CreativeWork",
name: project.name,
description: project.description,
image: project.image.startsWith("http")
? project.image
: `${SITE_URL}${project.image}`,
url: project.url,
keywords: project.technologies,
creator: {
"@type": "Person",
"@id": `${SITE_URL}/#person`,
name: "Poyraz Avsever",
},
},
})),
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
);
}
@@ -40,6 +151,7 @@ export type ArticleJsonLdProps = {
image: string;
datePublished: string;
authorName?: string;
locale?: "tr" | "en";
};
export function ArticleJsonLd({
@@ -49,6 +161,7 @@ export function ArticleJsonLd({
image,
datePublished,
authorName = "Poyraz Avsever",
locale = "tr",
}: ArticleJsonLdProps) {
const data = {
"@context": "https://schema.org",
@@ -56,8 +169,11 @@ export function ArticleJsonLd({
headline: title,
description,
url,
mainEntityOfPage: url,
image: image.startsWith("http") ? image : `${SITE_URL}${image}`,
datePublished,
dateModified: datePublished,
inLanguage: locale === "tr" ? "tr-TR" : "en-US",
author: {
"@type": "Person",
name: authorName,
@@ -66,17 +182,15 @@ export function ArticleJsonLd({
publisher: {
"@type": "Person",
name: authorName,
logo: {
"@type": "ImageObject",
url: `${SITE_URL}/logo/logo.webp`,
},
url: SITE_URL,
image: `${SITE_URL}/logo/logo.webp`,
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }}
/>
);
}