Files
manoon-headless/src/lib/programmatic-seo/dataLoader.ts
Unchained 9ab07ab01d feat: implement centralized taxonomy for programmatic SEO
- Create taxonomy system with oils.json (5 oils) and concerns.json (9 concerns)
- Migrate 10 content files to new data/content/oil-for-concern/ structure
- Add scripts: generate-urls.js, validate-taxonomy.js, migrate-content.js
- Update dataLoader.ts to use centralized taxonomy
- Generate 40 URLs (10 pairs × 4 languages)
- Create sitemap-programmatic.xml for SEO
- Update by-oil and by-concern directory pages
2026-04-09 08:04:35 +02:00

235 lines
8.1 KiB
TypeScript

import { readFile, readdir } from "fs/promises";
import { join } from "path";
import type {
OilForConcernPage,
OilForConcernContent,
OilsTaxonomyData,
ConcernsTaxonomyData,
OilTaxonomy,
ConcernTaxonomy,
LocalizedString,
LocalizedSEOKeywords,
} from "./types";
const DATA_DIR = join(process.cwd(), "data");
const TAXONOMY_DIR = join(DATA_DIR, "taxonomy");
const CONTENT_DIR = join(DATA_DIR, "content", "oil-for-concern");
let oilsTaxonomyCache: OilsTaxonomyData | null = null;
let concernsTaxonomyCache: ConcernsTaxonomyData | null = null;
export function getLocalizedString(
localized: { sr: string; en: string; de: string; fr: string },
locale: string
): string {
return localized[locale as keyof typeof localized] || localized.en;
}
export function getLocalizedArray(
localized: { sr: string[]; en: string[]; de: string[]; fr: string[] },
locale: string
): string[] {
return localized[locale as keyof typeof localized] || localized.en;
}
export function getLocalizedKeywords(
seoKeywords: LocalizedSEOKeywords,
locale: string
): string[] {
const keywords = seoKeywords[locale as keyof LocalizedSEOKeywords] || seoKeywords.en;
return [...keywords.primary, ...keywords.secondary, ...keywords.longTail];
}
async function getOilsTaxonomy(): Promise<OilsTaxonomyData> {
if (oilsTaxonomyCache) return oilsTaxonomyCache;
const filePath = join(TAXONOMY_DIR, "oils.json");
const content = await readFile(filePath, "utf-8");
oilsTaxonomyCache = JSON.parse(content) as OilsTaxonomyData;
return oilsTaxonomyCache;
}
async function getConcernsTaxonomy(): Promise<ConcernsTaxonomyData> {
if (concernsTaxonomyCache) return concernsTaxonomyCache;
const filePath = join(TAXONOMY_DIR, "concerns.json");
const content = await readFile(filePath, "utf-8");
concernsTaxonomyCache = JSON.parse(content) as ConcernsTaxonomyData;
return concernsTaxonomyCache;
}
async function getOilForConcernContent(oilId: string, concernId: string): Promise<OilForConcernContent | null> {
try {
const filePath = join(CONTENT_DIR, `${oilId}-${concernId}.json`);
const content = await readFile(filePath, "utf-8");
return JSON.parse(content) as OilForConcernContent;
} catch (error) {
return null;
}
}
function generatePageSlug(oilSlug: string, concernSlug: string, locale: string): string {
if (locale === "sr") {
return `${oilSlug}-za-${concernSlug}`;
}
return `${oilSlug}-for-${concernSlug}`;
}
function generateLocalizedSlugs(oil: OilTaxonomy, concern: ConcernTaxonomy): LocalizedString {
return {
sr: generatePageSlug(oil.slug.sr, concern.slug.sr, "sr"),
en: generatePageSlug(oil.slug.en, concern.slug.en, "en"),
de: generatePageSlug(oil.slug.de, concern.slug.de, "de"),
fr: generatePageSlug(oil.slug.fr, concern.slug.fr, "fr"),
};
}
function generatePageTitle(oil: OilTaxonomy, concern: ConcernTaxonomy): LocalizedString {
return {
sr: `Najbolje ${oil.name.sr.toLowerCase()} za ${concern.name.sr.toLowerCase()}`,
en: `Best ${oil.name.en} for ${concern.name.en}`,
de: `Bestes ${oil.name.de} für ${concern.name.de}`,
fr: `Meilleure ${oil.name.fr.toLowerCase()} pour ${concern.name.fr.toLowerCase()}`,
};
}
function generateMetaTitle(oil: OilTaxonomy, concern: ConcernTaxonomy): LocalizedString {
return {
sr: `Najbolje ${oil.name.sr.toLowerCase()} za ${concern.name.sr.toLowerCase()} | Prirodno rešenje | ManoonOils`,
en: `Best ${oil.name.en} for ${concern.name.en} | Natural Solution | ManoonOils`,
de: `Bestes ${oil.name.de} für ${concern.name.de} | Natürliche Lösung | ManoonOils`,
fr: `Meilleure ${oil.name.fr.toLowerCase()} pour ${concern.name.fr.toLowerCase()} | Solution naturelle | ManoonOils`,
};
}
function generateMetaDescription(oil: OilTaxonomy, concern: ConcernTaxonomy): LocalizedString {
return {
sr: `Otkrijte zašto je ${oil.name.sr.toLowerCase()} efikasno za ${concern.name.sr.toLowerCase()}. ${oil.shortDescription.sr}`,
en: `Discover why ${oil.name.en} is effective for ${concern.name.en}. ${oil.shortDescription.en}`,
de: `Entdecken Sie, warum ${oil.name.de} effektiv für ${concern.name.de} ist. ${oil.shortDescription.de}`,
fr: `Découvrez pourquoi ${oil.name.fr} est efficace pour ${concern.name.fr}. ${oil.shortDescription.fr}`,
};
}
async function buildOilForConcernPage(
oil: OilTaxonomy,
concern: ConcernTaxonomy,
content: OilForConcernContent
): Promise<OilForConcernPage> {
const localizedSlugs = generateLocalizedSlugs(oil, concern);
const pageTitle = generatePageTitle(oil, concern);
const metaTitle = generateMetaTitle(oil, concern);
const metaDescription = generateMetaDescription(oil, concern);
return {
slug: localizedSlugs.en,
localizedSlugs,
oilSlug: oil.slug.en,
concernSlug: concern.slug.en,
oilId: oil.id,
concernId: concern.id,
pageTitle,
metaTitle,
metaDescription,
oilName: oil.name,
concernName: concern.name,
whyThisWorks: content.content.whyThisWorks,
keyBenefits: content.content.keyBenefits,
howToApply: content.content.howToApply,
expectedResults: content.content.expectedResults,
timeframe: content.content.timeframe,
complementaryIngredients: content.metadata.complementaryIngredients,
productsToShow: content.metadata.productsToShow,
customerResults: content.metadata.customerResults,
faqs: content.metadata.faqs,
seoKeywords: content.metadata.seoKeywords,
relatedPages: content.metadata.relatedPages,
};
}
export async function getAllOilForConcernPages(): Promise<OilForConcernPage[]> {
const [oilsTaxonomy, concernsTaxonomy] = await Promise.all([
getOilsTaxonomy(),
getConcernsTaxonomy(),
]);
const pages: OilForConcernPage[] = [];
for (const oil of Object.values(oilsTaxonomy.oils)) {
for (const concernId of oil.concerns) {
const concern = concernsTaxonomy.concerns[concernId];
if (!concern) continue;
const content = await getOilForConcernContent(oil.id, concernId);
if (!content) continue;
const page = await buildOilForConcernPage(oil, concern, content);
pages.push(page);
}
}
return pages;
}
export async function getOilForConcernPageBySlug(slug: string, locale: string): Promise<OilForConcernPage | null> {
const pages = await getAllOilForConcernPages();
return pages.find(page =>
page.localizedSlugs[locale as "sr" | "en" | "de" | "fr"] === slug
) || null;
}
export async function getAllSolutionSlugs(): Promise<Array<{ locale: string; slug: string }>> {
const pages = await getAllOilForConcernPages();
const result: Array<{ locale: string; slug: string }> = [];
for (const page of pages) {
for (const locale of ["sr", "en", "de", "fr"] as const) {
result.push({
locale,
slug: page.localizedSlugs[locale]
});
}
}
return result;
}
export async function getOilForConcernPage(slug: string): Promise<OilForConcernPage | null> {
return getOilForConcernPageBySlug(slug, "en");
}
export async function getAllOilForConcernSlugs(): Promise<string[]> {
const pages = await getAllOilForConcernPages();
return pages.map(page => page.slug);
}
export async function getAllOils(): Promise<OilTaxonomy[]> {
const taxonomy = await getOilsTaxonomy();
return Object.values(taxonomy.oils);
}
export async function getAllConcerns(): Promise<ConcernTaxonomy[]> {
const taxonomy = await getConcernsTaxonomy();
return Object.values(taxonomy.concerns);
}
export async function getOilById(id: string): Promise<OilTaxonomy | null> {
const taxonomy = await getOilsTaxonomy();
return taxonomy.oils[id] || null;
}
export async function getConcernById(id: string): Promise<ConcernTaxonomy | null> {
const taxonomy = await getConcernsTaxonomy();
return taxonomy.concerns[id] || null;
}
export async function getPagesByOil(oilId: string): Promise<OilForConcernPage[]> {
const pages = await getAllOilForConcernPages();
return pages.filter(page => page.oilId === oilId);
}
export async function getPagesByConcern(concernId: string): Promise<OilForConcernPage[]> {
const pages = await getAllOilForConcernPages();
return pages.filter(page => page.concernId === concernId);
}