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:
204
scripts/generate-urls.js
Normal file
204
scripts/generate-urls.js
Normal file
@@ -0,0 +1,204 @@
|
||||
const oils = require('../data/taxonomy/oils.json');
|
||||
const concerns = require('../data/taxonomy/concerns.json');
|
||||
|
||||
const LOCALES = ['sr', 'en', 'de', 'fr'];
|
||||
const DEFAULT_LOCALE = 'sr';
|
||||
|
||||
function generateUrl(oilId, concernId, locale) {
|
||||
const oil = oils.oils[oilId];
|
||||
const concern = concerns.concerns[concernId];
|
||||
|
||||
if (!oil || !concern) return null;
|
||||
|
||||
const localePrefix = locale === DEFAULT_LOCALE ? '' : `/${locale}`;
|
||||
const oilSlug = oil.slug[locale];
|
||||
const concernSlug = concern.slug[locale];
|
||||
|
||||
return {
|
||||
url: `${localePrefix}/solutions/${oilSlug}-for-${concernSlug}`,
|
||||
canonical: `https://manoonoils.com${localePrefix}/solutions/${oilSlug}-for-${concernSlug}`,
|
||||
locale,
|
||||
oil: {
|
||||
id: oilId,
|
||||
name: oil.name[locale],
|
||||
slug: oilSlug
|
||||
},
|
||||
concern: {
|
||||
id: concernId,
|
||||
name: concern.name[locale],
|
||||
slug: concernSlug
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function generateAllUrls() {
|
||||
const urls = [];
|
||||
const stats = {
|
||||
total: 0,
|
||||
byLocale: {},
|
||||
byOil: {},
|
||||
byConcern: {}
|
||||
};
|
||||
|
||||
LOCALES.forEach(l => stats.byLocale[l] = 0);
|
||||
Object.keys(oils.oils).forEach(o => stats.byOil[o] = 0);
|
||||
Object.keys(concerns.concerns).forEach(c => stats.byConcern[c] = 0);
|
||||
|
||||
for (const oilId of Object.keys(oils.oils)) {
|
||||
const oil = oils.oils[oilId];
|
||||
|
||||
for (const concernId of oil.concerns) {
|
||||
const concern = concerns.concerns[concernId];
|
||||
|
||||
if (!concern) {
|
||||
console.warn(`Warning: Concern ${concernId} not found for oil ${oilId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const locale of LOCALES) {
|
||||
const urlData = generateUrl(oilId, concernId, locale);
|
||||
if (urlData) {
|
||||
urls.push(urlData);
|
||||
stats.total++;
|
||||
stats.byLocale[locale]++;
|
||||
stats.byOil[oilId]++;
|
||||
stats.byConcern[concernId]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { urls, stats };
|
||||
}
|
||||
|
||||
function generateSitemap() {
|
||||
const { urls } = generateAllUrls();
|
||||
|
||||
let sitemap = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
||||
sitemap += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
|
||||
|
||||
for (const url of urls) {
|
||||
sitemap += ' <url>\n';
|
||||
sitemap += ` <loc>${url.canonical}</loc>\n`;
|
||||
sitemap += ' <changefreq>weekly</changefreq>\n';
|
||||
sitemap += ' <priority>0.8</priority>\n';
|
||||
sitemap += ` <xhtml:link rel="alternate" hreflang="${url.locale}" href="${url.canonical}" />\n`;
|
||||
sitemap += ' </url>\n';
|
||||
}
|
||||
|
||||
sitemap += '</urlset>';
|
||||
return sitemap;
|
||||
}
|
||||
|
||||
function generateUrlReport() {
|
||||
const { urls, stats } = generateAllUrls();
|
||||
|
||||
let report = '# Programmatic SEO URL Report\n\n';
|
||||
report += `Generated: ${new Date().toISOString()}\n\n`;
|
||||
|
||||
report += '## Summary\n\n';
|
||||
report += `- **Total URLs**: ${stats.total}\n`;
|
||||
report += `- **Languages**: ${LOCALES.join(', ')}\n`;
|
||||
report += `- **Oils**: ${Object.keys(oils.oils).length}\n`;
|
||||
report += `- **Concerns**: ${Object.keys(concerns.concerns).length}\n\n`;
|
||||
|
||||
report += '## URLs by Locale\n\n';
|
||||
for (const [locale, count] of Object.entries(stats.byLocale)) {
|
||||
report += `- **${locale.toUpperCase()}**: ${count} URLs\n`;
|
||||
}
|
||||
report += '\n';
|
||||
|
||||
report += '## URLs by Oil\n\n';
|
||||
for (const [oilId, count] of Object.entries(stats.byOil)) {
|
||||
const oil = oils.oils[oilId];
|
||||
report += `- **${oil.name.en}** (${oilId}): ${count} URLs\n`;
|
||||
}
|
||||
report += '\n';
|
||||
|
||||
report += '## URLs by Concern\n\n';
|
||||
for (const [concernId, count] of Object.entries(stats.byConcern)) {
|
||||
const concern = concerns.concerns[concernId];
|
||||
report += `- **${concern.name.en}** (${concernId}): ${count} URLs\n`;
|
||||
}
|
||||
report += '\n';
|
||||
|
||||
report += '## All Generated URLs\n\n';
|
||||
|
||||
const urlsByOil = {};
|
||||
for (const url of urls) {
|
||||
if (!urlsByOil[url.oil.id]) {
|
||||
urlsByOil[url.oil.id] = [];
|
||||
}
|
||||
urlsByOil[url.oil.id].push(url);
|
||||
}
|
||||
|
||||
for (const [oilId, oilUrls] of Object.entries(urlsByOil)) {
|
||||
const oil = oils.oils[oilId];
|
||||
report += `### ${oil.name.en}\n\n`;
|
||||
|
||||
const byConcern = {};
|
||||
for (const url of oilUrls) {
|
||||
if (!byConcern[url.concern.id]) {
|
||||
byConcern[url.concern.id] = [];
|
||||
}
|
||||
byConcern[url.concern.id].push(url);
|
||||
}
|
||||
|
||||
for (const [concernId, concernUrls] of Object.entries(byConcern)) {
|
||||
const concern = concerns.concerns[concernId];
|
||||
report += `#### ${concern.name.en}\n\n`;
|
||||
|
||||
for (const url of concernUrls) {
|
||||
report += `- ${url.locale.toUpperCase()}: \`${url.canonical}\`\n`;
|
||||
}
|
||||
report += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateUrl,
|
||||
generateAllUrls,
|
||||
generateSitemap,
|
||||
generateUrlReport,
|
||||
LOCALES,
|
||||
DEFAULT_LOCALE
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
const { urls, stats } = generateAllUrls();
|
||||
|
||||
console.log('\n=== PROGRAMMATIC SEO URL GENERATOR ===\n');
|
||||
console.log(`Total URLs Generated: ${stats.total}`);
|
||||
console.log('\nBy Locale:');
|
||||
for (const [locale, count] of Object.entries(stats.byLocale)) {
|
||||
console.log(` ${locale.toUpperCase()}: ${count}`);
|
||||
}
|
||||
console.log('\nBy Oil:');
|
||||
for (const [oilId, count] of Object.entries(stats.byOil)) {
|
||||
const oil = oils.oils[oilId];
|
||||
console.log(` ${oil.name.en}: ${count}`);
|
||||
}
|
||||
console.log('\nBy Concern:');
|
||||
for (const [concernId, count] of Object.entries(stats.byConcern)) {
|
||||
const concern = concerns.concerns[concernId];
|
||||
console.log(` ${concern.name.en}: ${count}`);
|
||||
}
|
||||
|
||||
console.log('\n=== SAMPLE URLS ===\n');
|
||||
const sampleUrls = urls.filter((_, i) => i < 12);
|
||||
for (const url of sampleUrls) {
|
||||
console.log(`${url.locale.toUpperCase()}: ${url.canonical}`);
|
||||
}
|
||||
|
||||
const fs = require('fs');
|
||||
const report = generateUrlReport();
|
||||
fs.writeFileSync('./url-report.md', report);
|
||||
console.log('\n✓ Full report saved to: url-report.md');
|
||||
|
||||
const sitemap = generateSitemap();
|
||||
fs.writeFileSync('./sitemap-programmatic.xml', sitemap);
|
||||
console.log('✓ Sitemap saved to: sitemap-programmatic.xml');
|
||||
}
|
||||
106
scripts/migrate-content.js
Normal file
106
scripts/migrate-content.js
Normal file
@@ -0,0 +1,106 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const oils = require('../data/taxonomy/oils.json');
|
||||
const concerns = require('../data/taxonomy/concerns.json');
|
||||
|
||||
const LOCALES = ['sr', 'en', 'de', 'fr'];
|
||||
|
||||
const legacyFiles = [
|
||||
{ file: 'najbolje-arganovo-ulje-za-bore.json', oil: 'argan-oil', concern: 'wrinkles' },
|
||||
{ file: 'najbolje-arganovo-ulje-za-suvu-kozu.json', oil: 'argan-oil', concern: 'dry-skin' },
|
||||
{ file: 'najbolje-arganovo-ulje-za-podocnjake.json', oil: 'argan-oil', concern: 'under-eye-bags' },
|
||||
{ file: 'najbolje-ulje-divlje-ruze-za-bore.json', oil: 'rosehip-oil', concern: 'wrinkles' },
|
||||
{ file: 'najbolje-ulje-divlje-ruze-za-tamne-pjege.json', oil: 'rosehip-oil', concern: 'dark-spots' },
|
||||
{ file: 'najbolje-ulje-divlje-ruze-za-oziljke-od-akni.json', oil: 'rosehip-oil', concern: 'acne-scars' },
|
||||
{ file: 'najbolje-jojoba-ulje-za-akne.json', oil: 'jojoba-oil', concern: 'acne' },
|
||||
{ file: 'najbolje-jojoba-ulje-za-masnu-kozu.json', oil: 'jojoba-oil', concern: 'oily-skin' },
|
||||
{ file: 'najbolje-ulje-pasjeg-trna-za-hiperpigmentaciju.json', oil: 'sea-buckthorn-oil', concern: 'hyperpigmentation' },
|
||||
{ file: 'najbolje-ulje-slatkog-badema-za-osetljivu-kozu.json', oil: 'sweet-almond-oil', concern: 'sensitive-skin' }
|
||||
];
|
||||
|
||||
function extractContent(oldData) {
|
||||
return {
|
||||
schema: {
|
||||
version: "1.0.0",
|
||||
type: "oil-for-concern",
|
||||
oilId: oldData.oilSlug,
|
||||
concernId: oldData.concernSlug
|
||||
},
|
||||
content: {
|
||||
whyThisWorks: oldData.whyThisWorks,
|
||||
keyBenefits: oldData.keyBenefits,
|
||||
howToApply: oldData.howToApply,
|
||||
expectedResults: oldData.expectedResults,
|
||||
timeframe: oldData.timeframe
|
||||
},
|
||||
metadata: {
|
||||
productsToShow: oldData.productsToShow || [],
|
||||
complementaryIngredients: oldData.complementaryIngredients || [],
|
||||
customerResults: (oldData.customerResults || []).map(r => ({
|
||||
quote: r.quote,
|
||||
name: r.name,
|
||||
age: r.age,
|
||||
skinType: r.skinType,
|
||||
timeframe: r.timeframe
|
||||
})),
|
||||
faqs: (oldData.faqs || []).map(f => ({
|
||||
question: f.question,
|
||||
answer: f.answer
|
||||
})),
|
||||
seoKeywords: oldData.seoKeywords || {},
|
||||
relatedPages: oldData.relatedPages || {
|
||||
otherOilsForSameConcern: [],
|
||||
sameOilForOtherConcerns: []
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function migrateFiles() {
|
||||
const sourceDir = path.join(__dirname, '../data/oil-for-concern');
|
||||
const targetDir = path.join(__dirname, '../data/content/oil-for-concern');
|
||||
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
let migrated = 0;
|
||||
let errors = [];
|
||||
|
||||
for (const mapping of legacyFiles) {
|
||||
const sourcePath = path.join(sourceDir, mapping.file);
|
||||
const targetFilename = `${mapping.oil}-${mapping.concern}.json`;
|
||||
const targetPath = path.join(targetDir, targetFilename);
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
errors.push(`Source file not found: ${mapping.file}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const oldData = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
|
||||
const newData = extractContent(oldData);
|
||||
|
||||
fs.writeFileSync(targetPath, JSON.stringify(newData, null, 2));
|
||||
console.log(`✓ Migrated: ${mapping.file} → ${targetFilename}`);
|
||||
migrated++;
|
||||
} catch (err) {
|
||||
errors.push(`Error migrating ${mapping.file}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\n=== MIGRATION COMPLETE ===`);
|
||||
console.log(`Migrated: ${migrated}/${legacyFiles.length} files`);
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.log(`\nErrors (${errors.length}):`);
|
||||
errors.forEach(e => console.log(` - ${e}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
migrateFiles();
|
||||
}
|
||||
|
||||
module.exports = { extractContent, migrateFiles };
|
||||
327
scripts/validate-taxonomy.js
Normal file
327
scripts/validate-taxonomy.js
Normal file
@@ -0,0 +1,327 @@
|
||||
const oils = require('../data/taxonomy/oils.json');
|
||||
const concerns = require('../data/taxonomy/concerns.json');
|
||||
|
||||
const LOCALES = ['sr', 'en', 'de', 'fr'];
|
||||
const DEFAULT_LOCALE = 'sr';
|
||||
|
||||
class TaxonomyValidator {
|
||||
constructor() {
|
||||
this.errors = [];
|
||||
this.warnings = [];
|
||||
this.stats = {
|
||||
oils: 0,
|
||||
concerns: 0,
|
||||
relationships: 0,
|
||||
checked: 0
|
||||
};
|
||||
}
|
||||
|
||||
validate() {
|
||||
console.log('\n=== TAXONOMY VALIDATION ===\n');
|
||||
|
||||
this.validateOils();
|
||||
this.validateConcerns();
|
||||
this.validateRelationships();
|
||||
this.validateSlugs();
|
||||
this.validateTranslations();
|
||||
this.validateCategories();
|
||||
|
||||
this.printResults();
|
||||
|
||||
return {
|
||||
valid: this.errors.length === 0,
|
||||
errors: this.errors,
|
||||
warnings: this.warnings,
|
||||
stats: this.stats
|
||||
};
|
||||
}
|
||||
|
||||
validateOils() {
|
||||
console.log('Validating oils...');
|
||||
const oilIds = Object.keys(oils.oils);
|
||||
this.stats.oils = oilIds.length;
|
||||
|
||||
for (const oilId of oilIds) {
|
||||
const oil = oils.oils[oilId];
|
||||
this.stats.checked++;
|
||||
|
||||
if (oil.id !== oilId) {
|
||||
this.errors.push(`Oil ID mismatch: ${oilId} vs ${oil.id}`);
|
||||
}
|
||||
|
||||
if (!oil.name || !oil.name[DEFAULT_LOCALE]) {
|
||||
this.errors.push(`Oil ${oilId} missing name for default locale`);
|
||||
}
|
||||
|
||||
if (!oil.slug || !oil.slug[DEFAULT_LOCALE]) {
|
||||
this.errors.push(`Oil ${oilId} missing slug for default locale`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(oil.concerns) || oil.concerns.length === 0) {
|
||||
this.warnings.push(`Oil ${oilId} has no concerns`);
|
||||
}
|
||||
|
||||
if (typeof oil.comedogenicRating !== 'number') {
|
||||
this.warnings.push(`Oil ${oilId} missing comedogenicRating`);
|
||||
}
|
||||
|
||||
LOCALES.forEach(locale => {
|
||||
if (!oil.name[locale]) {
|
||||
this.errors.push(`Oil ${oilId} missing name translation for ${locale}`);
|
||||
}
|
||||
if (!oil.slug[locale]) {
|
||||
this.errors.push(`Oil ${oilId} missing slug for ${locale}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
validateConcerns() {
|
||||
console.log('Validating concerns...');
|
||||
const concernIds = Object.keys(concerns.concerns);
|
||||
this.stats.concerns = concernIds.length;
|
||||
|
||||
for (const concernId of concernIds) {
|
||||
const concern = concerns.concerns[concernId];
|
||||
this.stats.checked++;
|
||||
|
||||
if (concern.id !== concernId) {
|
||||
this.errors.push(`Concern ID mismatch: ${concernId} vs ${concern.id}`);
|
||||
}
|
||||
|
||||
if (!concern.name || !concern.name[DEFAULT_LOCALE]) {
|
||||
this.errors.push(`Concern ${concernId} missing name for default locale`);
|
||||
}
|
||||
|
||||
if (!concern.slug || !concern.slug[DEFAULT_LOCALE]) {
|
||||
this.errors.push(`Concern ${concernId} missing slug for default locale`);
|
||||
}
|
||||
|
||||
if (!concern.category) {
|
||||
this.warnings.push(`Concern ${concernId} missing category`);
|
||||
}
|
||||
|
||||
if (!concerns.categories[concern.category]) {
|
||||
this.errors.push(`Concern ${concernId} has invalid category: ${concern.category}`);
|
||||
}
|
||||
|
||||
LOCALES.forEach(locale => {
|
||||
if (!concern.name[locale]) {
|
||||
this.errors.push(`Concern ${concernId} missing name translation for ${locale}`);
|
||||
}
|
||||
if (!concern.slug[locale]) {
|
||||
this.errors.push(`Concern ${concernId} missing slug for ${locale}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
validateRelationships() {
|
||||
console.log('Validating relationships...');
|
||||
const oilIds = Object.keys(oils.oils);
|
||||
const concernIds = Object.keys(concerns.concerns);
|
||||
|
||||
for (const oilId of oilIds) {
|
||||
const oil = oils.oils[oilId];
|
||||
|
||||
for (const concernId of oil.concerns) {
|
||||
this.stats.relationships++;
|
||||
|
||||
if (!concerns.concerns[concernId]) {
|
||||
this.errors.push(`Oil ${oilId} references non-existent concern: ${concernId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const concern = concerns.concerns[concernId];
|
||||
if (!concern.oils || !concern.oils.includes(oilId)) {
|
||||
this.warnings.push(`Bidirectional relationship missing: ${oilId} → ${concernId} exists, but not ${concernId} → ${oilId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const concernId of concernIds) {
|
||||
const concern = concerns.concerns[concernId];
|
||||
|
||||
for (const oilId of concern.oils || []) {
|
||||
if (!oils.oils[oilId]) {
|
||||
this.errors.push(`Concern ${concernId} references non-existent oil: ${oilId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const oil = oils.oils[oilId];
|
||||
if (!oil.concerns.includes(concernId)) {
|
||||
this.warnings.push(`Bidirectional relationship missing: ${concernId} → ${oilId} exists, but not ${oilId} → ${concernId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
validateSlugs() {
|
||||
console.log('Validating slugs...');
|
||||
const allSlugs = new Map();
|
||||
|
||||
LOCALES.forEach(locale => {
|
||||
const localeSlugs = new Set();
|
||||
|
||||
Object.values(oils.oils).forEach(oil => {
|
||||
const slug = oil.slug[locale];
|
||||
if (localeSlugs.has(slug)) {
|
||||
this.errors.push(`Duplicate slug in ${locale}: ${slug}`);
|
||||
}
|
||||
localeSlugs.add(slug);
|
||||
|
||||
const key = `${locale}:${slug}`;
|
||||
if (allSlugs.has(key)) {
|
||||
this.errors.push(`Slug collision: ${key}`);
|
||||
}
|
||||
allSlugs.set(key, `oil:${oil.id}`);
|
||||
});
|
||||
|
||||
Object.values(concerns.concerns).forEach(concern => {
|
||||
const slug = concern.slug[locale];
|
||||
if (localeSlugs.has(slug)) {
|
||||
this.errors.push(`Duplicate slug in ${locale}: ${slug}`);
|
||||
}
|
||||
localeSlugs.add(slug);
|
||||
|
||||
const key = `${locale}:${slug}`;
|
||||
if (allSlugs.has(key)) {
|
||||
this.errors.push(`Slug collision: ${key}`);
|
||||
}
|
||||
allSlugs.set(key, `concern:${concern.id}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
validateTranslations() {
|
||||
console.log('Validating translations...');
|
||||
|
||||
Object.entries(oils.oils).forEach(([id, oil]) => {
|
||||
LOCALES.forEach(locale => {
|
||||
if (!oil.name[locale] || oil.name[locale].trim() === '') {
|
||||
this.errors.push(`Empty name translation: oil ${id} for ${locale}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(concerns.concerns).forEach(([id, concern]) => {
|
||||
LOCALES.forEach(locale => {
|
||||
if (!concern.name[locale] || concern.name[locale].trim() === '') {
|
||||
this.errors.push(`Empty name translation: concern ${id} for ${locale}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
validateCategories() {
|
||||
console.log('Validating categories...');
|
||||
|
||||
Object.entries(concerns.categories).forEach(([catId, cat]) => {
|
||||
LOCALES.forEach(locale => {
|
||||
if (!cat.name[locale]) {
|
||||
this.errors.push(`Category ${catId} missing translation for ${locale}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
printResults() {
|
||||
console.log('\n=== RESULTS ===\n');
|
||||
console.log(`✓ Checked: ${this.stats.checked} entities`);
|
||||
console.log(`✓ Oils: ${this.stats.oils}`);
|
||||
console.log(`✓ Concerns: ${this.stats.concerns}`);
|
||||
console.log(`✓ Relationships: ${this.stats.relationships}`);
|
||||
|
||||
if (this.errors.length === 0 && this.warnings.length === 0) {
|
||||
console.log('\n✅ All validations passed!');
|
||||
} else {
|
||||
if (this.errors.length > 0) {
|
||||
console.log(`\n❌ ERRORS (${this.errors.length}):`);
|
||||
this.errors.forEach(err => console.log(` - ${err}`));
|
||||
}
|
||||
|
||||
if (this.warnings.length > 0) {
|
||||
console.log(`\n⚠️ WARNINGS (${this.warnings.length}):`);
|
||||
this.warnings.forEach(warn => console.log(` - ${warn}`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n');
|
||||
}
|
||||
}
|
||||
|
||||
function validateContentFiles() {
|
||||
console.log('=== CONTENT FILE VALIDATION ===\n');
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const contentDir = path.join(__dirname, '../data/content/oil-for-concern');
|
||||
const missingFiles = [];
|
||||
const extraFiles = [];
|
||||
|
||||
const expectedCombinations = [];
|
||||
|
||||
Object.entries(oils.oils).forEach(([oilId, oil]) => {
|
||||
oil.concerns.forEach(concernId => {
|
||||
expectedCombinations.push(`${oilId}-${concernId}`);
|
||||
});
|
||||
});
|
||||
|
||||
if (!fs.existsSync(contentDir)) {
|
||||
console.log('⚠️ Content directory does not exist yet');
|
||||
console.log(` Expected: ${contentDir}`);
|
||||
console.log(` Missing ${expectedCombinations.length} content files\n`);
|
||||
return { valid: false, missing: expectedCombinations };
|
||||
}
|
||||
|
||||
const existingFiles = fs.readdirSync(contentDir)
|
||||
.filter(f => f.endsWith('.json'))
|
||||
.map(f => f.replace('.json', ''));
|
||||
|
||||
expectedCombinations.forEach(combo => {
|
||||
if (!existingFiles.includes(combo)) {
|
||||
missingFiles.push(combo);
|
||||
}
|
||||
});
|
||||
|
||||
existingFiles.forEach(file => {
|
||||
if (!expectedCombinations.includes(file)) {
|
||||
extraFiles.push(file);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Content files checked: ${existingFiles.length}`);
|
||||
console.log(`Expected combinations: ${expectedCombinations.length}`);
|
||||
|
||||
if (missingFiles.length > 0) {
|
||||
console.log(`\n❌ Missing content files (${missingFiles.length}):`);
|
||||
missingFiles.forEach(f => console.log(` - ${f}.json`));
|
||||
}
|
||||
|
||||
if (extraFiles.length > 0) {
|
||||
console.log(`\n⚠️ Extra content files (${extraFiles.length}):`);
|
||||
extraFiles.forEach(f => console.log(` - ${f}.json`));
|
||||
}
|
||||
|
||||
if (missingFiles.length === 0 && extraFiles.length === 0) {
|
||||
console.log('\n✅ All content files present!\n');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: missingFiles.length === 0,
|
||||
missing: missingFiles,
|
||||
extra: extraFiles
|
||||
};
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const validator = new TaxonomyValidator();
|
||||
const taxonomyResult = validator.validate();
|
||||
|
||||
const contentResult = validateContentFiles();
|
||||
|
||||
process.exit(taxonomyResult.valid && contentResult.valid ? 0 : 1);
|
||||
}
|
||||
|
||||
module.exports = { TaxonomyValidator, validateContentFiles };
|
||||
Reference in New Issue
Block a user