feat: enhance SEO data loading with generated fallback
- Implemented a mechanism to load SEO data from a generated file (seoData.generated.json) if available. - Added error handling to fallback to the original SEO data file (seoData.json) if the generated file is not present.
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"test": "vitest run",
|
"test": "vitest run",
|
||||||
"seo:generate": "node scripts/generate-seo-assets.mjs"
|
"seo:generate": "node scripts/merge-keywords.mjs && node scripts/generate-seo-assets.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@microsoft/clarity": "^1.0.2",
|
"@microsoft/clarity": "^1.0.2",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,9 +9,19 @@ const publicDir = path.join(frontendRoot, 'public');
|
|||||||
const siteOrigin = String(process.env.VITE_SITE_DOMAIN || 'https://dociva.io').trim().replace(/\/$/, '');
|
const siteOrigin = String(process.env.VITE_SITE_DOMAIN || 'https://dociva.io').trim().replace(/\/$/, '');
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
const today = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
const seoConfig = JSON.parse(
|
// Prefer a generated SEO file if present (created by merge-keywords.mjs). This is opt-in and safe.
|
||||||
await readFile(path.join(frontendRoot, 'src', 'seo', 'seoData.json'), 'utf8')
|
const generatedSeoPath = path.join(frontendRoot, 'src', 'seo', 'seoData.generated.json');
|
||||||
);
|
const baseSeoPath = path.join(frontendRoot, 'src', 'seo', 'seoData.json');
|
||||||
|
const seoConfigPath = (await (async () => {
|
||||||
|
try {
|
||||||
|
await readFile(generatedSeoPath, 'utf8');
|
||||||
|
return generatedSeoPath;
|
||||||
|
} catch (e) {
|
||||||
|
return baseSeoPath;
|
||||||
|
}
|
||||||
|
})());
|
||||||
|
|
||||||
|
const seoConfig = JSON.parse(await readFile(seoConfigPath, 'utf8'));
|
||||||
const routeRegistrySource = await readFile(path.join(frontendRoot, 'src', 'config', 'routes.ts'), 'utf8');
|
const routeRegistrySource = await readFile(path.join(frontendRoot, 'src', 'config', 'routes.ts'), 'utf8');
|
||||||
|
|
||||||
const staticPages = [
|
const staticPages = [
|
||||||
|
|||||||
70
frontend/scripts/merge-keywords.mjs
Normal file
70
frontend/scripts/merge-keywords.mjs
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
const seoDir = path.join(root, 'src', 'seo');
|
||||||
|
const seoDataPath = path.join(seoDir, 'seoData.json');
|
||||||
|
const keywordsPath = path.join(seoDir, 'keywords.json');
|
||||||
|
const outPath = path.join(seoDir, 'seoData.generated.json');
|
||||||
|
|
||||||
|
async function loadJson(p) {
|
||||||
|
try {
|
||||||
|
return JSON.parse(await readFile(p, 'utf8'));
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeToolSeedFromKeyword(k) {
|
||||||
|
const en = k.language === 'ar' ? k.mainKeyword : k.mainKeyword;
|
||||||
|
const ar = k.language === 'ar' ? k.mainKeyword : '';
|
||||||
|
// minimal seed matching existing schema
|
||||||
|
return {
|
||||||
|
slug: k.slug,
|
||||||
|
toolSlug: k.slug.startsWith('pdf') ? k.slug : k.slug,
|
||||||
|
category: 'PDF',
|
||||||
|
focusKeyword: { en: en, ar: ar || en },
|
||||||
|
supportingKeywords: { en: [], ar: [] },
|
||||||
|
benefit: { en: `Use Dociva to ${k.mainKeyword}.`, ar: '' },
|
||||||
|
useCase: { en: 'Quick online processing without signup.', ar: '' },
|
||||||
|
relatedCollectionSlugs: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const seoData = await loadJson(seoDataPath);
|
||||||
|
const keywords = await loadJson(keywordsPath);
|
||||||
|
|
||||||
|
if (!seoData) {
|
||||||
|
console.error('Missing seoData.json — aborting');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!keywords) {
|
||||||
|
console.error('No keywords.json found — nothing to merge');
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingSlugs = new Set(seoData.toolPageSeeds.map((s) => s.slug));
|
||||||
|
const newSeeds = [];
|
||||||
|
for (const k of keywords.keywords || []) {
|
||||||
|
if (existingSlugs.has(k.slug)) continue; // safety: skip existing
|
||||||
|
newSeeds.push(makeToolSeedFromKeyword(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
const merged = {
|
||||||
|
toolPageSeeds: [...seoData.toolPageSeeds, ...newSeeds],
|
||||||
|
collectionPageSeeds: seoData.collectionPageSeeds || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await writeFile(outPath, JSON.stringify(merged, null, 2), 'utf8');
|
||||||
|
console.log(`Wrote ${outPath} with ${newSeeds.length} added seeds (skipped ${keywords.keywords.length - newSeeds.length}).`);
|
||||||
|
}
|
||||||
|
|
||||||
|
run().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
51
frontend/src/seo/keywords.json
Normal file
51
frontend/src/seo/keywords.json
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"keywords": [
|
||||||
|
{ "slug": "pdf-to-word-editable-free", "mainKeyword": "pdf to word editable free", "category": "conversion", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "compress-pdf-to-100kb", "mainKeyword": "compress pdf to 100kb online free", "category": "compression", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "ai-extract-text-from-pdf", "mainKeyword": "ai extract text from pdf online", "category": "ocr", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "pdf-to-excel-accurate-free", "mainKeyword": "pdf to excel accurate free online", "category": "conversion", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "merge-pdf-online-free", "mainKeyword": "merge pdf online free", "category": "merge", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "split-pdf-online-free", "mainKeyword": "split pdf online free", "category": "split", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "compress-pdf-online-free", "mainKeyword": "compress pdf online free", "category": "compression", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "unlock-pdf-online-free", "mainKeyword": "unlock pdf online free", "category": "security", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "summarize-pdf-ai", "mainKeyword": "summarize pdf ai", "category": "ai", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "convert-pdf-to-text-ai", "mainKeyword": "convert pdf to text ai", "category": "ocr", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "pdf-to-jpg-high-quality", "mainKeyword": "pdf to jpg high quality online", "category": "conversion", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "jpg-to-pdf-online-free", "mainKeyword": "jpg to pdf online free", "category": "conversion", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "reduce-pdf-size-for-email", "mainKeyword": "reduce pdf size for email", "category": "compression", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "ocr-for-scanned-pdfs", "mainKeyword": "ocr for scanned pdfs online", "category": "ocr", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "edit-pdf-online-free", "mainKeyword": "edit pdf online free", "category": "editor", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "remove-watermark-from-pdf-online", "mainKeyword": "remove watermark from pdf online", "category": "watermark", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "add-watermark-to-pdf-online", "mainKeyword": "add watermark to pdf online", "category": "watermark", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "repair-corrupted-pdf-online", "mainKeyword": "repair corrupted pdf online", "category": "repair", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "rotate-pdf-pages-online", "mainKeyword": "rotate pdf pages online", "category": "utility", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "reorder-pdf-pages-online", "mainKeyword": "reorder pdf pages online", "category": "utility", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "pdf-to-png-online", "mainKeyword": "pdf to png online", "category": "conversion", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "images-to-pdf-multiple", "mainKeyword": "combine images to pdf online", "category": "conversion", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "split-pdf-by-range-online", "mainKeyword": "split pdf by range online", "category": "split", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "compress-scanned-pdf-online", "mainKeyword": "compress scanned pdf online", "category": "compression", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "pdf-metadata-editor-online", "mainKeyword": "edit pdf metadata online", "category": "metadata", "intent": "low", "language": "en" },
|
||||||
|
{ "slug": "add-page-numbers-to-pdf-online", "mainKeyword": "add page numbers to pdf online", "category": "utility", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "protect-pdf-with-password-online", "mainKeyword": "protect pdf with password online", "category": "security", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "unlock-encrypted-pdf-online", "mainKeyword": "unlock encrypted pdf online", "category": "security", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "ocr-table-extraction-from-pdf", "mainKeyword": "extract tables from pdf online", "category": "ocr", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "pdf-to-excel-converter-online", "mainKeyword": "pdf to excel converter online free", "category": "conversion", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "extract-text-from-protected-pdf", "mainKeyword": "extract text from protected pdf", "category": "ocr", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "bulk-convert-pdf-to-word", "mainKeyword": "bulk convert pdf to word online", "category": "conversion", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "compress-pdf-for-web-upload", "mainKeyword": "compress pdf for web upload", "category": "compression", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "ocr-multi-language-pdf", "mainKeyword": "ocr multi language pdf", "category": "ocr", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "summarize-long-pdf-ai", "mainKeyword": "summarize long pdf ai", "category": "ai", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "translate-pdf-online", "mainKeyword": "translate pdf online", "category": "ai", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "convert-pdf-to-ppt-online", "mainKeyword": "convert pdf to ppt online", "category": "conversion", "intent": "medium", "language": "en" },
|
||||||
|
{ "slug": "pdf-to-pptx-free-online", "mainKeyword": "pdf to pptx free online", "category": "conversion", "intent": "high", "language": "en" },
|
||||||
|
{ "slug": "دمج-ملفات-pdf-مجاناً", "mainKeyword": "دمج ملفات PDF مجاناً", "category": "merge", "intent": "high", "language": "ar" },
|
||||||
|
{ "slug": "ضغط-بي-دي-اف-اونلاين", "mainKeyword": "ضغط بي دي اف اونلاين", "category": "compression", "intent": "high", "language": "ar" },
|
||||||
|
{ "slug": "تحويل-pdf-الى-word-قابل-للتعديل", "mainKeyword": "تحويل PDF إلى Word قابل للتعديل", "category": "conversion", "intent": "high", "language": "ar" },
|
||||||
|
{ "slug": "تحويل-jpg-الى-pdf-اونلاين", "mainKeyword": "تحويل JPG الى PDF اونلاين", "category": "conversion", "intent": "high", "language": "ar" },
|
||||||
|
{ "slug": "فصل-صفحات-pdf-اونلاين", "mainKeyword": "فصل صفحات PDF أونلاين", "category": "split", "intent": "high", "language": "ar" },
|
||||||
|
{ "slug": "ازالة-كلمة-مرور-من-pdf", "mainKeyword": "إزالة كلمة مرور من PDF", "category": "security", "intent": "high", "language": "ar" },
|
||||||
|
{ "slug": "تحويل-pdf-الى-نص-باستخدام-ocr", "mainKeyword": "تحويل PDF إلى نص باستخدام OCR", "category": "ocr", "intent": "high", "language": "ar" },
|
||||||
|
{ "slug": "تحويل-pdf-الى-excel-اونلاين", "mainKeyword": "تحويل PDF إلى Excel أونلاين", "category": "conversion", "intent": "high", "language": "ar" },
|
||||||
|
{ "slug": "تحويل-pdf-الى-صور", "mainKeyword": "تحويل PDF الى صور", "category": "conversion", "intent": "medium", "language": "ar" }
|
||||||
|
]
|
||||||
|
}
|
||||||
101
frontend/src/seo/keywords.ts
Normal file
101
frontend/src/seo/keywords.ts
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
export const seoKeywords = [
|
||||||
|
// Core / High Intent (English)
|
||||||
|
{
|
||||||
|
slug: "pdf-to-word-editable-free",
|
||||||
|
mainKeyword: "pdf to word editable free",
|
||||||
|
category: "conversion",
|
||||||
|
intent: "high",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "compress-pdf-to-100kb",
|
||||||
|
mainKeyword: "compress pdf to 100kb online free",
|
||||||
|
category: "compression",
|
||||||
|
intent: "high",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Long-tail / AI related (English)
|
||||||
|
{
|
||||||
|
slug: "ai-extract-text-from-pdf",
|
||||||
|
mainKeyword: "ai extract text from pdf online",
|
||||||
|
category: "ocr",
|
||||||
|
intent: "medium",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "pdf-to-excel-accurate-free",
|
||||||
|
mainKeyword: "pdf to excel accurate free online",
|
||||||
|
category: "conversion",
|
||||||
|
intent: "high",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Core tools (English)
|
||||||
|
{
|
||||||
|
slug: "merge-pdf-online-free",
|
||||||
|
mainKeyword: "merge pdf online free",
|
||||||
|
category: "merge",
|
||||||
|
intent: "high",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "split-pdf-online-free",
|
||||||
|
mainKeyword: "split pdf online free",
|
||||||
|
category: "split",
|
||||||
|
intent: "high",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Popular / Utility (English)
|
||||||
|
{
|
||||||
|
slug: "compress-pdf-online-free",
|
||||||
|
mainKeyword: "compress pdf online free",
|
||||||
|
category: "compression",
|
||||||
|
intent: "high",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "unlock-pdf-online-free",
|
||||||
|
mainKeyword: "unlock pdf online free",
|
||||||
|
category: "security",
|
||||||
|
intent: "high",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI / Assistant (English)
|
||||||
|
{
|
||||||
|
slug: "summarize-pdf-ai",
|
||||||
|
mainKeyword: "summarize pdf ai",
|
||||||
|
category: "ai",
|
||||||
|
intent: "medium",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Arabic keywords (RTL)
|
||||||
|
{
|
||||||
|
slug: "دمج-ملفات-pdf-مجاناً",
|
||||||
|
mainKeyword: "دمج ملفات PDF مجاناً",
|
||||||
|
category: "merge",
|
||||||
|
intent: "high",
|
||||||
|
language: "ar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "ضغط-بي دي اف-الى-100kb",
|
||||||
|
mainKeyword: "ضغط بي دي اف الى 100kb أونلاين",
|
||||||
|
category: "compression",
|
||||||
|
intent: "high",
|
||||||
|
language: "ar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "تحويل-pdf-الى-word-قابل-للتعديل",
|
||||||
|
mainKeyword: "تحويل PDF إلى Word قابل للتعديل",
|
||||||
|
category: "conversion",
|
||||||
|
intent: "high",
|
||||||
|
language: "ar",
|
||||||
|
},
|
||||||
|
|
||||||
|
// Add more keywords here to scale to 50+ later — this file is the single source of truth
|
||||||
|
];
|
||||||
|
|
||||||
|
export default seoKeywords;
|
||||||
2646
frontend/src/seo/seoData.generated.json
Normal file
2646
frontend/src/seo/seoData.generated.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,17 @@
|
|||||||
import seoSeedConfig from '@/seo/seoData.json';
|
// Prefer a generated SEO data file at build time if present (seoData.generated.json).
|
||||||
|
// This file is optional and created by frontend/scripts/merge-keywords.mjs.
|
||||||
|
let seoSeedConfig: any;
|
||||||
|
try {
|
||||||
|
// try to load generated first
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
seoSeedConfig = (await import('@/seo/seoData.generated.json')).default;
|
||||||
|
} catch (err) {
|
||||||
|
// fallback to original
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
seoSeedConfig = (await import('@/seo/seoData.json')).default;
|
||||||
|
}
|
||||||
import type {
|
import type {
|
||||||
LocalizedText,
|
LocalizedText,
|
||||||
LocalizedTextList,
|
LocalizedTextList,
|
||||||
|
|||||||
Reference in New Issue
Block a user