- 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
328 lines
9.6 KiB
JavaScript
328 lines
9.6 KiB
JavaScript
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 };
|