Integrate Prisma for content; enhance SEO, i18n, and deployment workflows

Co-authored-by: dennis <dennis@konkol.net>
This commit is contained in:
Cursor Agent
2026-01-12 15:27:35 +00:00
parent f1cc398248
commit 423a2af938
38 changed files with 757 additions and 629 deletions

View File

@@ -1,4 +1,5 @@
import { prisma } from "@/lib/prisma";
import type { Prisma } from "@prisma/client";
export async function getSiteSettings() {
return prisma.siteSettings.findUnique({ where: { id: 1 } });
@@ -55,14 +56,14 @@ export async function upsertContentByKey(opts: {
locale,
title: title ?? undefined,
slug: slug ?? undefined,
content: content as any, // JSON
content: content as Prisma.InputJsonValue, // JSON
metaDescription: metaDescription ?? undefined,
keywords: keywords ?? undefined,
},
update: {
title: title ?? undefined,
slug: slug ?? undefined,
content: content as any, // JSON
content: content as Prisma.InputJsonValue, // JSON
metaDescription: metaDescription ?? undefined,
keywords: keywords ?? undefined,
},

30
lib/seo.ts Normal file
View File

@@ -0,0 +1,30 @@
import { locales, type AppLocale } from "@/i18n/locales";
export function getBaseUrl(): string {
const raw =
process.env.NEXT_PUBLIC_BASE_URL ||
process.env.NEXTAUTH_URL || // fallback if ever added
"http://localhost:3000";
return raw.replace(/\/+$/, "");
}
export function toAbsoluteUrl(path: string): string {
const base = getBaseUrl();
const normalized = path.startsWith("/") ? path : `/${path}`;
return `${base}${normalized}`;
}
export function getLanguageAlternates(opts: {
/** Path without locale prefix, e.g. "/projects" or "/projects/my-slug" or "" */
pathWithoutLocale: string;
}): Record<AppLocale, string> {
const path = opts.pathWithoutLocale === "" ? "" : `/${opts.pathWithoutLocale}`.replace(/\/{2,}/g, "/");
const normalizedPath = path === "/" ? "" : path;
return locales.reduce((acc, l) => {
const url = toAbsoluteUrl(`/${l}${normalizedPath}`);
acc[l] = url;
return acc;
}, {} as Record<AppLocale, string>);
}

70
lib/sitemap.ts Normal file
View File

@@ -0,0 +1,70 @@
import { prisma } from "@/lib/prisma";
import { locales } from "@/i18n/locales";
import { getBaseUrl } from "@/lib/seo";
export type SitemapEntry = {
url: string;
lastModified: string;
changefreq?: "daily" | "weekly" | "monthly" | "yearly";
priority?: number;
};
export function generateSitemapXml(entries: SitemapEntry[]): string {
const xmlHeader = '<?xml version="1.0" encoding="UTF-8"?>';
const urlsetOpen = '<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">';
const urlsetClose = "</urlset>";
const urlEntries = entries
.map((e) => {
const changefreq = e.changefreq ?? "monthly";
const priority = typeof e.priority === "number" ? e.priority : 0.8;
return `
<url>
<loc>${e.url}</loc>
<lastmod>${e.lastModified}</lastmod>
<changefreq>${changefreq}</changefreq>
<priority>${priority.toFixed(1)}</priority>
</url>`;
})
.join("");
return `${xmlHeader}${urlsetOpen}${urlEntries}${urlsetClose}`;
}
export async function getSitemapEntries(): Promise<SitemapEntry[]> {
const baseUrl = getBaseUrl();
const nowIso = new Date().toISOString();
const staticPaths = ["", "/projects", "/legal-notice", "/privacy-policy"];
const staticEntries: SitemapEntry[] = locales.flatMap((locale) =>
staticPaths.map((p) => {
const path = p === "" ? `/${locale}` : `/${locale}${p}`;
return {
url: `${baseUrl}${path}`,
lastModified: nowIso,
changefreq: p === "" ? "weekly" : p === "/projects" ? "weekly" : "yearly",
priority: p === "" ? 1.0 : p === "/projects" ? 0.8 : 0.5,
};
}),
);
// Projects: for each project slug we publish per locale (same slug)
const projects = await prisma.project.findMany({
where: { published: true },
select: { slug: true, updatedAt: true },
orderBy: { updatedAt: "desc" },
});
const projectEntries: SitemapEntry[] = projects.flatMap((p) => {
const lastModified = (p.updatedAt ?? new Date()).toISOString();
return locales.map((locale) => ({
url: `${baseUrl}/${locale}/projects/${p.slug}`,
lastModified,
changefreq: "monthly",
priority: 0.7,
}));
});
return [...staticEntries, ...projectEntries];
}