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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user