feat(cms): add storage based website content management
This commit is contained in:
parent
7d5689cfa4
commit
12d31f5468
31 changed files with 1347 additions and 158 deletions
176
lib/content/defaults.ts
Normal file
176
lib/content/defaults.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import type { ContentDocuments, SeoContent } from "./types";
|
||||
|
||||
const now = "2026-01-01T00:00:00.000Z";
|
||||
|
||||
function seo(title: string, description: string, path: string): SeoContent {
|
||||
return {
|
||||
metaTitle: title,
|
||||
metaDescription: description,
|
||||
keywords: "Funktechnik Schubert, Funkgeräte Reparatur, CB Funk Service, Amateurfunk Service, Funkgeräte Abgleich, Messtechnik",
|
||||
openGraphTitle: title,
|
||||
openGraphDescription: description,
|
||||
socialImage: "/funktechnik_schubert_logo.jpg",
|
||||
canonicalUrl: path,
|
||||
};
|
||||
}
|
||||
|
||||
export const defaultContent: ContentDocuments = {
|
||||
home: {
|
||||
eyebrow: "Funktechnik, Messtechnik, Werkstattservice",
|
||||
title: "Funktechnik Schubert",
|
||||
subtitle: "Service, Reparatur und Diagnose für Funkgeräte, Messtechnik und Kommunikationstechnik.",
|
||||
heroText: "Von CB-Funk und Amateurfunk bis zur professionellen Funktechnik: Wir unterstützen bei Fehlersuche, Abgleich, Modulationsproblemen, Frequenzproblemen und technischer Dokumentation.",
|
||||
intro: "Von der ersten Fehlerbeschreibung bis zur dokumentierten Prüfung: Funktechnik Schubert verbindet technische Erfahrung, geeignete Messmittel und strukturierte Kommunikation.",
|
||||
paragraphs: [],
|
||||
cta: {
|
||||
text: "Direkte technische Einschätzung statt anonymer Abwicklung",
|
||||
primary: { label: "Reparatur anfragen", href: "/reparatur" },
|
||||
secondary: { label: "Leistungen ansehen", href: "/leistungen" },
|
||||
},
|
||||
listTitle: "Technische Schwerpunkte",
|
||||
list: ["CB-Funk", "Amateurfunk", "Messtechnik", "Abgleich", "Diagnose", "Service Manuals"],
|
||||
badges: ["CB-Funk", "Amateurfunk", "Messtechnik", "Abgleich", "Diagnose", "Service Manuals"],
|
||||
features: [
|
||||
{ title: "Funkgeräte-Service", text: "Sichtprüfung, Funktionsprüfung und technische Beurteilung für CB-Funk, Amateurfunk, Betriebsfunk sowie ältere und moderne Geräte." },
|
||||
{ title: "Fehlersuche & Reparatur", text: "Systematische Diagnose bei Empfangsproblemen, Sendeproblemen, PLL-Fehlern, Frequenzabweichungen und Audio-/NF-Störungen." },
|
||||
{ title: "Abgleich & Messtechnik", text: "Frequenzabgleich, Hub- und Modulationsprüfung, Sendeleistung, Empfindlichkeit und RF-Messung mit geeigneter Messtechnik." },
|
||||
],
|
||||
services: [
|
||||
{ title: "Funkgeräte-Service", text: "Prüfung und technische Einschätzung für CB-Funk, Amateurfunk und Kommunikationstechnik." },
|
||||
{ title: "Messtechnik", text: "Messungen mit geeigneter Werkstattpraxis und nachvollziehbarer Dokumentation." },
|
||||
{ title: "Serviceunterlagen", text: "Einordnung von Schaltplänen, Service Manuals und Abgleichhinweisen." },
|
||||
],
|
||||
promise: {
|
||||
eyebrow: "Persönlicher Support",
|
||||
title: "Direkte technische Einschätzung statt anonymer Abwicklung",
|
||||
text: "Viele Fehlerbilder lassen sich bereits mit einer klaren Beschreibung und den richtigen Gerätedaten eingrenzen.",
|
||||
bullets: [
|
||||
"Technischer Fokus auf Funkgeräte und Kommunikationstechnik",
|
||||
"Nachvollziehbare Diagnose mit Blick auf Messwerte und Fehlerbild",
|
||||
"Sauberer Umgang mit Service Manuals, Schaltplänen und Abgleichanleitungen",
|
||||
"Klare Rückmeldung zu Zustand, Aufwand und sinnvollen nächsten Schritten",
|
||||
],
|
||||
button: { label: "Reparaturformular öffnen", href: "/reparatur" },
|
||||
},
|
||||
footerText: "Service und technische Unterstützung für Funkgeräte, Messtechnik und Kommunikationstechnik.",
|
||||
faq: [],
|
||||
seo: seo("Funktechnik Schubert", "Service, Reparatur, Diagnose und Abgleich von Funktechnik, CB-Funk, Amateurfunk und Kommunikationstechnik.", "/"),
|
||||
updatedAt: now,
|
||||
},
|
||||
services: {
|
||||
eyebrow: "Leistungen",
|
||||
title: "Service für Funkgeräte und Kommunikationstechnik",
|
||||
subtitle: "Technische Unterstützung für Geräte, bei denen Diagnose, Messtechnik, Erfahrung und saubere Dokumentation zählen.",
|
||||
heroText: "Leistungen für private und professionelle Funktechnik mit klarem Fokus auf Diagnose, Abgleich und Reparatur.",
|
||||
intro: "Die Leistungen können im Adminbereich sortiert, erweitert und ein- oder ausgeblendet werden.",
|
||||
paragraphs: [],
|
||||
cta: { text: "Reparaturfall vorbereiten", primary: { label: "Reparatur anfragen", href: "/reparatur" }, secondary: { label: "Kontakt aufnehmen", href: "/kontakt" } },
|
||||
listTitle: "Leistungen",
|
||||
list: [],
|
||||
services: [
|
||||
{ title: "Funkgeräte-Service", description: "Service für CB-Funk, Amateurfunk, Betriebsfunk sowie ältere und moderne Funkgeräte.", icon: "radio", sortOrder: 10, visible: true },
|
||||
{ title: "Fehlersuche & Reparatur", description: "Strukturierte Analyse bei Modulationsproblemen, Frequenzabweichung, PLL-Problemen und Empfangs- oder Sendeproblemen.", icon: "tools", sortOrder: 20, visible: true },
|
||||
{ title: "Abgleich & Prüfung", description: "Frequenzabgleich, Hub- und Modulationsprüfung, Sendeleistung, Empfindlichkeit und Funktionsprüfung.", icon: "meter", sortOrder: 30, visible: true },
|
||||
{ title: "Messtechnik & Diagnose", description: "Systematische Fehlersuche mit Oszilloskop, Frequenzzähler, Signalgenerator, RF-Messung und Modulationsmessung.", icon: "scope", sortOrder: 40, visible: true },
|
||||
{ title: "Serviceunterlagen", description: "Einordnung von Schaltplänen, Service Manuals, Abgleichanleitungen und Reparaturhinweisen.", icon: "manual", sortOrder: 50, visible: true },
|
||||
],
|
||||
faq: [],
|
||||
seo: seo("Leistungen", "Funkgeräte-Service, Diagnose, Abgleich und Dokumentation für Kommunikationstechnik.", "/leistungen"),
|
||||
updatedAt: now,
|
||||
},
|
||||
"radio-service": {
|
||||
eyebrow: "Funkgeräte-Service",
|
||||
title: "Prüfung, Diagnose und Abgleich",
|
||||
subtitle: "Funkgeräte zeigen Fehler oft erst im Zusammenspiel von Empfang, Sendeteil, Versorgung, Antennenanpassung, Bedienung und Abgleich.",
|
||||
heroText: "Der Service betrachtet diese Zusammenhänge strukturiert und dokumentiert die technischen Schritte nachvollziehbar.",
|
||||
intro: "Eine gute Reparatur beginnt mit vollständigen Angaben zu Gerät, Fehlerbild und Vorgeschichte.",
|
||||
paragraphs: [],
|
||||
cta: { text: "Reparaturprozess starten", primary: { label: "Reparatur anfragen", href: "/reparatur" }, secondary: { label: "Kontakt", href: "/kontakt" } },
|
||||
listTitle: "Typische Fehlerbilder",
|
||||
list: [],
|
||||
brands: ["CB-Funk", "Amateurfunk", "Betriebsfunk"],
|
||||
symptoms: [
|
||||
"Kein oder schwacher Empfang",
|
||||
"Keine, leise oder verzerrte Modulation",
|
||||
"Frequenzversatz, PLL-Rasten oder instabile Kanäle",
|
||||
"Sendeprobleme, schwankende Leistung oder auffällige Erwärmung",
|
||||
"Audio-/NF-Probleme, Displayfehler oder Aussetzer durch Bedienelemente",
|
||||
"Unklare Vorarbeiten, fehlende Serviceunterlagen oder bereits geöffnete Geräte",
|
||||
],
|
||||
workflow: ["Anfrage", "Technische Sichtung", "Messung", "Abstimmung", "Reparatur oder Rückgabe"],
|
||||
measurements: ["Frequenz", "Sendeleistung", "Hub", "Modulation", "Empfindlichkeit"],
|
||||
alignment: "Abgleich erfolgt anhand technischer Unterlagen und sinnvoller Messpunkte.",
|
||||
repair: "Reparaturen werden vor Durchführung technisch eingeordnet und abgestimmt.",
|
||||
faq: [],
|
||||
seo: seo("Funkgeräte-Service", "Werkstattservice für CB-Funk, Amateurfunk und Funkgeräte-Abgleich.", "/funkgeraete-service"),
|
||||
updatedAt: now,
|
||||
},
|
||||
about: {
|
||||
eyebrow: "Über uns",
|
||||
title: "Technik verstehen, Fehler nachvollziehbar lösen",
|
||||
subtitle: "Funktechnik Schubert richtet sich an Kunden, die bei Funkgeräten und Kommunikationstechnik eine persönliche, technische Einschätzung suchen.",
|
||||
heroText: "Im Mittelpunkt stehen saubere Diagnose, transparente Kommunikation und respektvoller Umgang mit bestehenden Geräten.",
|
||||
intro: "Technischer Service lebt von Genauigkeit, Erfahrung und klarer Rückmeldung.",
|
||||
paragraphs: [
|
||||
{ title: "Arbeitsweise", text: "Im Mittelpunkt stehen saubere Diagnose, transparente Kommunikation und der respektvolle Umgang mit bestehenden Geräten und Serviceunterlagen." },
|
||||
{ title: "Fokus", text: "Der Schwerpunkt liegt auf Funktechnik, Elektronikservice, Abgleichfragen und technischer Unterstützung rund um Kommunikationstechnik." },
|
||||
],
|
||||
cta: { text: "Mehr zur Werkstatt", primary: { label: "Kontakt aufnehmen", href: "/kontakt" }, secondary: { label: "Leistungen", href: "/leistungen" } },
|
||||
listTitle: "Grundsätze",
|
||||
list: ["Nachvollziehbare technische Einschätzung", "Klare Kommunikation", "Sorgfältiger Umgang mit Geräten"],
|
||||
companyDescription: "Funktechnik Schubert bietet technischen Service für Funkgeräte, Messtechnik und Kommunikationstechnik.",
|
||||
workshopDescription: "Die Werkstatt arbeitet strukturiert mit Messmitteln, Dokumentation und technischer Prüfung.",
|
||||
philosophy: "Reparatur und Service sollen nachvollziehbar, ehrlich und technisch sauber bleiben.",
|
||||
faq: [],
|
||||
seo: seo("Über uns", "Funktechnik Schubert steht für technischen Service und klare Kommunikation.", "/ueber-uns"),
|
||||
updatedAt: now,
|
||||
},
|
||||
contact: {
|
||||
eyebrow: "Kontakt",
|
||||
title: "Kontakt aufnehmen",
|
||||
subtitle: "Beschreiben Sie kurz Ihr Anliegen. Für Reparaturen nutzen Sie idealerweise die strukturierte Reparaturannahme.",
|
||||
heroText: "Je genauer Gerät, Anliegen und gewünschte Unterstützung beschrieben werden, desto gezielter kann die Rückmeldung erfolgen.",
|
||||
intro: "Direkt und technisch: Nutzen Sie das Kontaktformular oder die Reparaturannahme.",
|
||||
paragraphs: [],
|
||||
cta: { text: "Anfrage senden", primary: { label: "Reparatur anfragen", href: "/reparatur" }, secondary: { label: "Leistungen", href: "/leistungen" } },
|
||||
listTitle: "Kontaktinformationen",
|
||||
list: [],
|
||||
phone: "",
|
||||
email: "",
|
||||
address: "",
|
||||
openingHours: "",
|
||||
googleMapsLink: "",
|
||||
socialMedia: [],
|
||||
faq: [],
|
||||
seo: seo("Kontakt", "Kontakt zu Funktechnik Schubert aufnehmen.", "/kontakt"),
|
||||
updatedAt: now,
|
||||
},
|
||||
settings: {
|
||||
siteName: "Funktechnik Schubert",
|
||||
logoUrl: "/funktechnik_schubert_logo.jpg",
|
||||
footerShortText: "Service und technische Unterstützung für Funkgeräte, Messtechnik und Kommunikationstechnik.",
|
||||
copyright: "© Funktechnik Schubert",
|
||||
footerLinks: [
|
||||
{ label: "Leistungen", href: "/leistungen" },
|
||||
{ label: "Funkgeräte-Service", href: "/funkgeraete-service" },
|
||||
{ label: "Reparatur", href: "/reparatur" },
|
||||
{ label: "Kontakt", href: "/kontakt" },
|
||||
],
|
||||
legalLinks: [
|
||||
{ label: "Impressum", href: "/impressum" },
|
||||
{ label: "Datenschutz", href: "/datenschutz" },
|
||||
],
|
||||
headerCta: { label: "Reparatur anfragen", href: "/reparatur" },
|
||||
seo: seo("Funktechnik Schubert", "Service, Reparatur, Diagnose und Abgleich von Funktechnik, CB-Funk, Amateurfunk und Kommunikationstechnik.", "/"),
|
||||
updatedAt: now,
|
||||
},
|
||||
};
|
||||
|
||||
export const contentFileNames = {
|
||||
home: "home.json",
|
||||
services: "services.json",
|
||||
"radio-service": "radio-service.json",
|
||||
about: "about.json",
|
||||
contact: "contact.json",
|
||||
settings: "settings.json",
|
||||
} as const;
|
||||
153
lib/content/service.ts
Normal file
153
lib/content/service.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { existsSync } from "fs";
|
||||
import { mkdir, readdir, readFile, rename, stat, unlink, writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import { storageDirectory } from "@/lib/runtime/config";
|
||||
import { listMediaFiles } from "@/lib/admin/media";
|
||||
import { contentFileNames, defaultContent } from "./defaults";
|
||||
import type { ContentDocuments, ContentKey, ContentSummary } from "./types";
|
||||
|
||||
export const contentDirectory = path.join(storageDirectory, "content");
|
||||
export const contentBackupDirectory = path.join(contentDirectory, "backups");
|
||||
export const contentVersion = "0.4.0";
|
||||
|
||||
const contentKeys = Object.keys(contentFileNames) as ContentKey[];
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function sanitizeString(value: string) {
|
||||
return value
|
||||
.replace(/<script[\s\S]*?>[\s\S]*?<\/script>/gi, "")
|
||||
.replace(/javascript:/gi, "")
|
||||
.replace(/[<>]/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function sanitizeValue(value: unknown): unknown {
|
||||
if (typeof value === "string") return sanitizeString(value);
|
||||
if (Array.isArray(value)) return value.map(sanitizeValue);
|
||||
if (!isRecord(value)) return value;
|
||||
|
||||
return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, sanitizeValue(entry)]));
|
||||
}
|
||||
|
||||
function mergeContent<T>(defaults: T, incoming: unknown): T {
|
||||
if (Array.isArray(defaults)) return Array.isArray(incoming) ? sanitizeValue(incoming) as T : defaults;
|
||||
if (!isRecord(defaults)) return sanitizeValue(incoming ?? defaults) as T;
|
||||
if (!isRecord(incoming)) return defaults;
|
||||
|
||||
const merged: Record<string, unknown> = { ...defaults };
|
||||
for (const [key, value] of Object.entries(defaults)) {
|
||||
merged[key] = mergeContent(value, incoming[key]);
|
||||
}
|
||||
return sanitizeValue(merged) as T;
|
||||
}
|
||||
|
||||
function filePathFor(key: ContentKey) {
|
||||
return path.join(contentDirectory, contentFileNames[key]);
|
||||
}
|
||||
|
||||
function backupStamp() {
|
||||
const date = new Date();
|
||||
const pad = (value: number) => String(value).padStart(2, "0");
|
||||
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}-${pad(date.getHours())}${pad(date.getMinutes())}`;
|
||||
}
|
||||
|
||||
async function pruneBackups(key: ContentKey) {
|
||||
const prefix = `${key}.`;
|
||||
const entries = await readdir(contentBackupDirectory).catch(() => []);
|
||||
const backups = await Promise.all(entries
|
||||
.filter((entry) => entry.startsWith(prefix) && entry.endsWith(".json"))
|
||||
.map(async (entry) => {
|
||||
const fullPath = path.join(contentBackupDirectory, entry);
|
||||
const fileStat = await stat(fullPath);
|
||||
return { entry, fullPath, modified: fileStat.mtimeMs };
|
||||
}));
|
||||
|
||||
const obsolete = backups.sort((left, right) => right.modified - left.modified).slice(20);
|
||||
await Promise.all(obsolete.map((entry) => unlink(entry.fullPath).catch(() => undefined)));
|
||||
}
|
||||
|
||||
async function ensureContentDocument<K extends ContentKey>(key: K) {
|
||||
await mkdir(contentDirectory, { recursive: true });
|
||||
await mkdir(contentBackupDirectory, { recursive: true });
|
||||
|
||||
const target = filePathFor(key);
|
||||
if (!existsSync(target)) {
|
||||
await writeFile(target, `${JSON.stringify(defaultContent[key], null, 2)}\n`, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureContentStorage() {
|
||||
await mkdir(contentDirectory, { recursive: true });
|
||||
await mkdir(contentBackupDirectory, { recursive: true });
|
||||
await Promise.all(contentKeys.map((key) => ensureContentDocument(key)));
|
||||
}
|
||||
|
||||
export async function getContent<K extends ContentKey>(key: K): Promise<ContentDocuments[K]> {
|
||||
await ensureContentDocument(key);
|
||||
|
||||
try {
|
||||
const raw = await readFile(filePathFor(key), "utf8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
return mergeContent(defaultContent[key], parsed);
|
||||
} catch {
|
||||
return defaultContent[key];
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllContent(): Promise<ContentDocuments> {
|
||||
const entries = await Promise.all(contentKeys.map(async (key) => [key, await getContent(key)] as const));
|
||||
return Object.fromEntries(entries) as ContentDocuments;
|
||||
}
|
||||
|
||||
export async function saveContent<K extends ContentKey>(key: K, content: ContentDocuments[K]) {
|
||||
await ensureContentDocument(key);
|
||||
const current = filePathFor(key);
|
||||
const backup = path.join(contentBackupDirectory, `${key}.${backupStamp()}.json`);
|
||||
|
||||
if (existsSync(current)) await rename(current, backup);
|
||||
|
||||
const next = mergeContent(defaultContent[key], {
|
||||
...content,
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
await writeFile(current, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
||||
await pruneBackups(key);
|
||||
return next;
|
||||
}
|
||||
|
||||
export async function getContentSummary(): Promise<ContentSummary> {
|
||||
await ensureContentStorage();
|
||||
const documents = await Promise.all(contentKeys.map(async (key) => {
|
||||
const content = await getContent(key);
|
||||
return {
|
||||
key,
|
||||
fileName: contentFileNames[key],
|
||||
title: "title" in content ? content.title : content.siteName,
|
||||
updatedAt: content.updatedAt,
|
||||
};
|
||||
}));
|
||||
const lastModified = documents
|
||||
.map((document) => document.updatedAt)
|
||||
.filter(Boolean)
|
||||
.sort()
|
||||
.at(-1) ?? null;
|
||||
|
||||
return {
|
||||
version: contentVersion,
|
||||
lastModified,
|
||||
pageCount: documents.length,
|
||||
documents,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getContentSystemStatus() {
|
||||
const [summary, media] = await Promise.all([getContentSummary(), listMediaFiles()]);
|
||||
return {
|
||||
...summary,
|
||||
mediaCount: media.length,
|
||||
};
|
||||
}
|
||||
128
lib/content/types.ts
Normal file
128
lib/content/types.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
export type SeoContent = {
|
||||
metaTitle: string;
|
||||
metaDescription: string;
|
||||
keywords: string;
|
||||
openGraphTitle: string;
|
||||
openGraphDescription: string;
|
||||
socialImage: string;
|
||||
canonicalUrl: string;
|
||||
};
|
||||
|
||||
export type LinkContent = {
|
||||
label: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export type TextBlockContent = {
|
||||
title: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export type FaqContent = {
|
||||
question: string;
|
||||
answer: string;
|
||||
};
|
||||
|
||||
export type ServiceContent = {
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
sortOrder: number;
|
||||
visible: boolean;
|
||||
};
|
||||
|
||||
export type PageContent = {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
heroText: string;
|
||||
intro: string;
|
||||
paragraphs: TextBlockContent[];
|
||||
cta: {
|
||||
text: string;
|
||||
primary: LinkContent;
|
||||
secondary: LinkContent;
|
||||
};
|
||||
listTitle: string;
|
||||
list: string[];
|
||||
faq: FaqContent[];
|
||||
seo: SeoContent;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type HomeContent = PageContent & {
|
||||
badges: string[];
|
||||
features: TextBlockContent[];
|
||||
services: TextBlockContent[];
|
||||
promise: {
|
||||
eyebrow: string;
|
||||
title: string;
|
||||
text: string;
|
||||
bullets: string[];
|
||||
button: LinkContent;
|
||||
};
|
||||
footerText: string;
|
||||
};
|
||||
|
||||
export type ServicesContent = PageContent & {
|
||||
services: ServiceContent[];
|
||||
};
|
||||
|
||||
export type RadioServiceContent = PageContent & {
|
||||
brands: string[];
|
||||
symptoms: string[];
|
||||
workflow: string[];
|
||||
measurements: string[];
|
||||
alignment: string;
|
||||
repair: string;
|
||||
};
|
||||
|
||||
export type AboutContent = PageContent & {
|
||||
companyDescription: string;
|
||||
workshopDescription: string;
|
||||
philosophy: string;
|
||||
};
|
||||
|
||||
export type ContactContent = PageContent & {
|
||||
phone: string;
|
||||
email: string;
|
||||
address: string;
|
||||
openingHours: string;
|
||||
googleMapsLink: string;
|
||||
socialMedia: LinkContent[];
|
||||
};
|
||||
|
||||
export type SettingsContent = {
|
||||
siteName: string;
|
||||
logoUrl: string;
|
||||
footerShortText: string;
|
||||
copyright: string;
|
||||
footerLinks: LinkContent[];
|
||||
legalLinks: LinkContent[];
|
||||
headerCta: LinkContent;
|
||||
seo: SeoContent;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type ContentDocuments = {
|
||||
home: HomeContent;
|
||||
services: ServicesContent;
|
||||
"radio-service": RadioServiceContent;
|
||||
about: AboutContent;
|
||||
contact: ContactContent;
|
||||
settings: SettingsContent;
|
||||
};
|
||||
|
||||
export type ContentKey = keyof ContentDocuments;
|
||||
|
||||
export type ContentSummary = {
|
||||
version: string;
|
||||
lastModified: string | null;
|
||||
pageCount: number;
|
||||
documents: Array<{
|
||||
key: ContentKey;
|
||||
fileName: string;
|
||||
title: string;
|
||||
updatedAt: string;
|
||||
}>;
|
||||
};
|
||||
|
|
@ -6,6 +6,8 @@ import { appVersion } from "./version";
|
|||
export const dataDirectory = path.join(process.cwd(), "data");
|
||||
export const storageDirectory = path.join(process.cwd(), "storage");
|
||||
export const configDirectory = path.join(storageDirectory, "config");
|
||||
export const contentDirectory = path.join(storageDirectory, "content");
|
||||
export const contentBackupDirectory = path.join(contentDirectory, "backups");
|
||||
export const uploadDirectory = path.join(storageDirectory, "uploads", "images");
|
||||
export const legacyPublicUploadDirectory = path.join(process.cwd(), "public", "uploads", "images");
|
||||
|
||||
|
|
@ -20,6 +22,7 @@ export function isDockerEnvironment() {
|
|||
export async function ensureRuntimeDirectories() {
|
||||
await mkdir(dataDirectory, { recursive: true });
|
||||
await mkdir(configDirectory, { recursive: true });
|
||||
await mkdir(contentBackupDirectory, { recursive: true });
|
||||
await mkdir(uploadDirectory, { recursive: true });
|
||||
await migrateLegacyUploads();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export const appVersion = "0.3.0";
|
||||
export const appVersion = "0.4.0";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue