diff --git a/app/[locale]/about/page.tsx b/app/[locale]/about/page.tsx
index f4f6924..2cef614 100644
--- a/app/[locale]/about/page.tsx
+++ b/app/[locale]/about/page.tsx
@@ -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 (
<>
-
>
diff --git a/app/[locale]/animation-sources/[slug]/page.tsx b/app/[locale]/animation-sources/[slug]/page.tsx
index 4290393..98051f8 100644
--- a/app/[locale]/animation-sources/[slug]/page.tsx
+++ b/app/[locale]/animation-sources/[slug]/page.tsx
@@ -109,6 +109,7 @@ export default async function AnimationSourceDetailPage({
image={source.coverImage}
datePublished={source.date}
authorName={source.author}
+ locale={locale === "en" ? "en" : "tr"}
/>
>
diff --git a/app/[locale]/blog/[slug]/page.tsx b/app/[locale]/blog/[slug]/page.tsx
index 6f47762..ce7bfb2 100644
--- a/app/[locale]/blog/[slug]/page.tsx
+++ b/app/[locale]/blog/[slug]/page.tsx
@@ -90,6 +90,7 @@ export default async function BlogDetailPage({ params }: BlogDetailPageProps) {
image={post.coverImage}
datePublished={post.date}
authorName={post.author}
+ locale={locale as SiteLocale}
/>
>
diff --git a/app/[locale]/projects/page.tsx b/app/[locale]/projects/page.tsx
index 08405dd..95e0eed 100644
--- a/app/[locale]/projects/page.tsx
+++ b/app/[locale]/projects/page.tsx
@@ -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 ;
+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 (
+ <>
+
+
+ >
+ );
}
diff --git a/app/sitemap.ts b/app/sitemap.ts
index c3df4f2..c50b4c7 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -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>) {
+ 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("en", paths.en!);
+
+ return languages;
+}
export default async function sitemap(): Promise {
const [posts, animationSources] = await Promise.all([
@@ -12,89 +56,75 @@ export default async function sitemap(): Promise {
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();
+ 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();
+ 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];
diff --git a/components/json-ld.tsx b/components/json-ld.tsx
index dff54c6..f56f73d 100644
--- a/components/json-ld.tsx
+++ b/components/json-ld.tsx
@@ -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(/
+ );
+}
+
+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 (
+
+ );
+}
+
+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 (
+
);
}
@@ -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 (
);
}