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
This commit is contained in:
Unchained
2026-04-09 08:04:35 +02:00
parent 9d07a60d3f
commit 9ab07ab01d
25 changed files with 4618 additions and 117 deletions

View File

@@ -1,12 +1,22 @@
import { readFile } from "fs/promises";
import { readFile, readdir } from "fs/promises";
import { join } from "path";
import type {
OilForConcernPage,
OilForConcernContent,
OilsTaxonomyData,
ConcernsTaxonomyData,
OilTaxonomy,
ConcernTaxonomy,
LocalizedString,
LocalizedSEOKeywords,
LocalizedString
} 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 },
@@ -30,37 +40,142 @@ export function getLocalizedKeywords(
return [...keywords.primary, ...keywords.secondary, ...keywords.longTail];
}
export async function getOilForConcernPage(slug: string): Promise<OilForConcernPage | null> {
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(DATA_DIR, "oil-for-concern", `${slug}.json`);
const filePath = join(CONTENT_DIR, `${oilId}-${concernId}.json`);
const content = await readFile(filePath, "utf-8");
return JSON.parse(content) as OilForConcernPage;
return JSON.parse(content) as OilForConcernContent;
} catch (error) {
console.error(`Failed to load oil-for-concern page: ${slug}`, error);
return null;
}
}
export async function getAllOilForConcernSlugs(): Promise<string[]> {
try {
const { readdir } = await import("fs/promises");
const dirPath = join(DATA_DIR, "oil-for-concern");
const files = await readdir(dirPath);
return files
.filter((file) => file.endsWith(".json"))
.map((file) => file.replace(".json", ""));
} catch (error) {
console.error("Failed to load oil-for-concern slugs:", error);
return [];
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 slugs = await getAllOilForConcernSlugs();
const pages = await Promise.all(
slugs.map((slug) => getOilForConcernPage(slug))
);
return pages.filter((page): page is OilForConcernPage => page !== null);
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 }>> {
@@ -79,9 +194,41 @@ export async function getAllSolutionSlugs(): Promise<Array<{ locale: string; slu
return result;
}
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 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);
}