Integrate Prisma for content; enhance SEO, i18n, and deployment workflows
Co-authored-by: dennis <dennis@konkol.net>
This commit is contained in:
@@ -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
30
lib/seo.ts
Normal 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
70
lib/sitemap.ts
Normal 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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user