Add crawlable landing content, keyword tool titles and og:image subpath fix

This commit is contained in:
Anthony Stirling
2026-07-01 22:27:26 +01:00
parent 38c28b6adf
commit 6a908f2efe
6 changed files with 1031 additions and 66 deletions
+348 -50
View File
@@ -7,12 +7,12 @@
"byTool": {
"certSign": {
"image": "/og_images/certSign.png",
"title": "Certificate Sign - Stirling PDF",
"title": "Sign PDF with a Certificate - Stirling PDF",
"description": "Sign PDF documents using digital certificates"
},
"sign": {
"image": "/og_images/sign.png",
"title": "Sign - Stirling PDF",
"title": "Sign PDF - Stirling PDF",
"description": "Adds signature to PDF by drawing, text or image"
},
"sharedSign": {
@@ -22,122 +22,122 @@
},
"addText": {
"image": "/og_images/add-text.png",
"title": "Add Text - Stirling PDF",
"title": "Add Text to PDF - Stirling PDF",
"description": "Add custom text anywhere in your PDF"
},
"addPassword": {
"image": "/og_images/add-password.png",
"title": "Add Password - Stirling PDF",
"title": "Password Protect PDF - Stirling PDF",
"description": "Add password protection and restrictions to PDF files"
},
"removePassword": {
"image": "/og_images/remove-password.png",
"title": "Remove Password - Stirling PDF",
"title": "Remove PDF Password - Stirling PDF",
"description": "Remove password protection from PDF documents"
},
"removePages": {
"image": "/og_images/remove.png",
"title": "Remove Pages - Stirling PDF",
"title": "Delete Pages from PDF - Stirling PDF",
"description": "Remove specific pages from a PDF document"
},
"removeBlanks": {
"image": "/og_images/remove-blank-pages.png",
"title": "Remove Blank Pages - Stirling PDF",
"title": "Remove Blank Pages from PDF - Stirling PDF",
"description": "Remove blank pages from PDF documents"
},
"removeAnnotations": {
"image": "/og_images/remove-annotations.png",
"title": "Remove Annotations - Stirling PDF",
"title": "Remove Annotations from PDF - Stirling PDF",
"description": "Remove annotations and comments from PDF documents"
},
"removeImage": {
"image": "/og_images/remove-image.png",
"title": "Remove Images - Stirling PDF",
"title": "Remove Images from PDF - Stirling PDF",
"description": "Remove all images from a PDF document"
},
"changePermissions": {
"image": "/og_images/change-permissions.png",
"title": "Change Permissions - Stirling PDF",
"title": "Change PDF Permissions - Stirling PDF",
"description": "Change document restrictions and permissions"
},
"watermark": {
"image": "/og_images/add-watermark.png",
"title": "Add Watermark - Stirling PDF",
"title": "Add Watermark to PDF - Stirling PDF",
"description": "Add a custom watermark to your PDF document."
},
"sanitize": {
"image": "/og_images/sanitize.png",
"title": "Sanitize - Stirling PDF",
"title": "Sanitize PDF - Stirling PDF",
"description": "Remove potentially harmful elements from PDF files"
},
"split": {
"image": "/og_images/split.png",
"title": "Split - Stirling PDF",
"title": "Split PDF - Stirling PDF",
"description": "Split PDFs into multiple documents"
},
"merge": {
"image": "/og_images/mergePdfs.png",
"title": "Merge - Stirling PDF",
"title": "Merge PDF - Stirling PDF",
"description": "Merge multiple PDFs into a single document"
},
"convert": {
"image": "/og_images/convert.png",
"title": "Convert - Stirling PDF",
"title": "PDF Converter - Stirling PDF",
"description": "Convert files to and from PDF format"
},
"ocr": {
"image": "/og_images/ocr.png",
"title": "OCR - Stirling PDF",
"title": "OCR PDF - Stirling PDF",
"description": "Extract text from scanned PDFs using Optical Character Recognition"
},
"addImage": {
"image": "/og_images/add-image.png",
"title": "Add Image - Stirling PDF",
"title": "Add Image to PDF - Stirling PDF",
"description": "Add images anywhere in your PDF"
},
"rotate": {
"image": "/og_images/rotate.png",
"title": "Rotate - Stirling PDF",
"title": "Rotate PDF - Stirling PDF",
"description": "Easily rotate your PDFs."
},
"annotate": {
"image": "/og_images/annotate.png",
"title": "Annotate - Stirling PDF",
"title": "Annotate PDF - Stirling PDF",
"description": "Highlight, draw, add notes, and shapes directly in the viewer"
},
"scannerImageSplit": {
"image": "/og_images/detect-split-scanned-photos.png",
"title": "Detect & Split Scanned Photos - Stirling PDF",
"title": "Split Scanned Photos - Stirling PDF",
"description": "Detect and split scanned photos into separate pages"
},
"editTableOfContents": {
"image": "/og_images/edit-table-of-contents.png",
"title": "Edit Table of Contents - Stirling PDF",
"title": "Edit PDF Bookmarks - Stirling PDF",
"description": "Add or edit bookmarks and table of contents in PDF documents"
},
"scannerEffect": {
"image": "/og_images/scanner-effect.png",
"title": "Scanner Effect - Stirling PDF",
"title": "Make a PDF Look Scanned - Stirling PDF",
"description": "Create a PDF that looks like it was scanned"
},
"autoRename": {
"image": "/og_images/auto-rename-pdf-file.png",
"title": "Auto Rename PDF File - Stirling PDF",
"title": "Auto Rename PDF Files - Stirling PDF",
"description": "Automatically rename PDF files based on their content"
},
"pageLayout": {
"image": "/og_images/multi-page-layout.png",
"title": "Multi-Page Layout - Stirling PDF",
"title": "PDF Multi-Page Layout - Stirling PDF",
"description": "Merge multiple pages of a PDF document into a single page"
},
"scalePages": {
"image": "/og_images/adjust-page-size-scale.png",
"title": "Adjust page size/scale - Stirling PDF",
"title": "Resize PDF Pages - Stirling PDF",
"description": "Change the size/scale of a page and/or its contents."
},
"adjustContrast": {
"image": "/og_images/adjust-colors-contrast.png",
"title": "Adjust Colors/Contrast - Stirling PDF",
"title": "Adjust PDF Colors and Contrast - Stirling PDF",
"description": "Adjust colors and contrast of PDF documents"
},
"crop": {
@@ -147,37 +147,37 @@
},
"pdfToSinglePage": {
"image": "/og_images/single-large-page.png",
"title": "PDF to Single Large Page - Stirling PDF",
"title": "Combine PDF into a Single Page - Stirling PDF",
"description": "Merges all PDF pages into one large single page"
},
"repair": {
"image": "/og_images/repair.png",
"title": "Repair - Stirling PDF",
"title": "Repair PDF - Stirling PDF",
"description": "Repair corrupted or damaged PDF files"
},
"compare": {
"image": "/og_images/compare.png",
"title": "Compare - Stirling PDF",
"title": "Compare PDF Files - Stirling PDF",
"description": "Compare two PDF documents and highlight differences"
},
"addPageNumbers": {
"image": "/og_images/add-page-numbers.png",
"title": "Add Page Numbers - Stirling PDF",
"title": "Add Page Numbers to PDF - Stirling PDF",
"description": "Add Page numbers throughout a document in a set location"
},
"redact": {
"image": "/og_images/redact.png",
"title": "Redact - Stirling PDF",
"title": "Redact PDF - Stirling PDF",
"description": "Permanently remove sensitive information from PDF documents"
},
"flatten": {
"image": "/og_images/flatten.png",
"title": "Flatten - Stirling PDF",
"title": "Flatten PDF - Stirling PDF",
"description": "Remove all interactive elements and forms from a PDF"
},
"removeCertSign": {
"image": "/og_images/remove-certificate-sign.png",
"title": "Remove Certificate Sign - Stirling PDF",
"title": "Remove PDF Certificate Signature - Stirling PDF",
"description": "Remove digital signature from PDF documents"
},
"unlockPDFForms": {
@@ -187,22 +187,22 @@
},
"compress": {
"image": "/og_images/compress.png",
"title": "Compress - Stirling PDF",
"title": "Compress PDF - Stirling PDF",
"description": "Compress PDFs to reduce their file size."
},
"extractPages": {
"image": "/og_images/extract-pages.png",
"title": "Extract Pages - Stirling PDF",
"title": "Extract PDF Pages - Stirling PDF",
"description": "Extract specific pages from a PDF document"
},
"reorganizePages": {
"image": "/og_images/reorganize-pages.png",
"title": "Reorganize Pages - Stirling PDF",
"title": "Organize PDF Pages - Stirling PDF",
"description": "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
},
"extractImages": {
"image": "/og_images/extract-images.png",
"title": "Extract Images - Stirling PDF",
"title": "Extract Images from PDF - Stirling PDF",
"description": "Extract images from PDF documents"
},
"addStamp": {
@@ -212,22 +212,22 @@
},
"addAttachments": {
"image": "/og_images/add-attachments.png",
"title": "Add Attachments - Stirling PDF",
"title": "Add Attachments to PDF - Stirling PDF",
"description": "Add or remove embedded files (attachments) to/from a PDF"
},
"changeMetadata": {
"image": "/og_images/change-metadata.png",
"title": "Change Metadata - Stirling PDF",
"title": "Edit PDF Metadata - Stirling PDF",
"description": "Change/Remove/Add metadata from a PDF document"
},
"overlayPdfs": {
"image": "/og_images/overlay-pdfs.png",
"title": "Overlay Pdfs - Stirling PDF",
"title": "Overlay PDFs - Stirling PDF",
"description": "The Free Adobe Acrobat alternative (10M+ Downloads)"
},
"getPdfInfo": {
"image": "/og_images/get-all-info-on-pdf.png",
"title": "Get ALL Info on PDF - Stirling PDF",
"title": "PDF Info and Metadata Viewer - Stirling PDF",
"description": "Grabs any and all information possible on PDFs"
},
"validateSignature": {
@@ -242,42 +242,42 @@
},
"replaceColor": {
"image": "/og_images/replace-and-invert-color.png",
"title": "Replace & Invert Color - Stirling PDF",
"title": "Replace and Invert PDF Colors - Stirling PDF",
"description": "Replace or invert colors in PDF documents"
},
"showJS": {
"image": "/og_images/show-javascript.png",
"title": "Show JavaScript - Stirling PDF",
"title": "Show JavaScript in PDF - Stirling PDF",
"description": "Extract and display JavaScript code from PDF documents"
},
"bookletImposition": {
"image": "/og_images/booklet-imposition.png",
"title": "Booklet Imposition - Stirling PDF",
"title": "Create a PDF Booklet - Stirling PDF",
"description": "Create booklets with proper page ordering and multi-page layout for printing and binding"
},
"pdfTextEditor": {
"image": "/og_images/pdf-text-editor.png",
"title": "PDF Text Editor - Stirling PDF",
"title": "Edit PDF Text - Stirling PDF",
"description": "Review and edit text and images in PDFs with grouped text editing and PDF regeneration"
},
"formFill": {
"image": "/og_images/form-fill.png",
"title": "Fill Form - Stirling PDF",
"title": "Fill PDF Forms - Stirling PDF",
"description": "Fill PDF form fields interactively with a visual editor"
},
"multiTool": {
"image": "/og_images/multi-tool.png",
"title": "Multi-Tool - Stirling PDF",
"title": "PDF Page Editor - Stirling PDF",
"description": "Use multiple tools on a single PDF document"
},
"read": {
"image": "/og_images/read.png",
"title": "Read - Stirling PDF",
"title": "PDF Viewer - Stirling PDF",
"description": "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."
},
"automate": {
"image": "/og_images/automate.png",
"title": "Automate - Stirling PDF",
"title": "PDF Workflow Automation - Stirling PDF",
"description": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
},
"devApi": {
@@ -775,5 +775,303 @@
"/settings/help": "/settings/help",
"/settings/legal": "/settings/legal",
"/settings/payg": "/settings/payg"
}
},
"navLinks": [
{
"path": "/add-attachments",
"label": "Add Attachments to PDF"
},
{
"path": "/add-image",
"label": "Add Image to PDF"
},
{
"path": "/add-page-numbers",
"label": "Add Page Numbers to PDF"
},
{
"path": "/add-stamp",
"label": "Add Stamp to PDF"
},
{
"path": "/add-text",
"label": "Add Text to PDF"
},
{
"path": "/watermark",
"label": "Add Watermark to PDF"
},
{
"path": "/adjust-contrast",
"label": "Adjust PDF Colors and Contrast"
},
{
"path": "/annotations",
"label": "Annotate PDF"
},
{
"path": "/auto-rename",
"label": "Auto Rename PDF Files"
},
{
"path": "/cbr-to-pdf",
"label": "CBR to PDF Converter"
},
{
"path": "/cbz-to-pdf",
"label": "CBZ to PDF Converter"
},
{
"path": "/change-permissions",
"label": "Change PDF Permissions"
},
{
"path": "/single-large-page",
"label": "Combine PDF into a Single Page"
},
{
"path": "/compare",
"label": "Compare PDF Files"
},
{
"path": "/compress",
"label": "Compress PDF"
},
{
"path": "/booklet-imposition",
"label": "Create a PDF Booklet"
},
{
"path": "/crop",
"label": "Crop PDF"
},
{
"path": "/remove-pages",
"label": "Delete Pages from PDF"
},
{
"path": "/edit-table-of-contents",
"label": "Edit PDF Bookmarks"
},
{
"path": "/change-metadata",
"label": "Edit PDF Metadata"
},
{
"path": "/pdf-text-editor",
"label": "Edit PDF Text"
},
{
"path": "/eml-to-pdf",
"label": "Email (EML) to PDF Converter"
},
{
"path": "/extract-images",
"label": "Extract Images from PDF"
},
{
"path": "/extract-pages",
"label": "Extract PDF Pages"
},
{
"path": "/file-to-pdf",
"label": "File to PDF Converter"
},
{
"path": "/form-fill",
"label": "Fill PDF Forms"
},
{
"path": "/flatten",
"label": "Flatten PDF"
},
{
"path": "/html-to-pdf",
"label": "HTML to PDF Converter"
},
{
"path": "/img-to-pdf",
"label": "Image to PDF Converter"
},
{
"path": "/fake-scan",
"label": "Make a PDF Look Scanned"
},
{
"path": "/markdown-to-pdf",
"label": "Markdown to PDF Converter"
},
{
"path": "/merge",
"label": "Merge PDF"
},
{
"path": "/ocr",
"label": "OCR PDF"
},
{
"path": "/reorganize-pages",
"label": "Organize PDF Pages"
},
{
"path": "/overlay-pdfs",
"label": "Overlay PDFs"
},
{
"path": "/add-password",
"label": "Password Protect PDF"
},
{
"path": "/convert",
"label": "PDF Converter"
},
{
"path": "/get-pdf-info",
"label": "PDF Info and Metadata Viewer"
},
{
"path": "/page-layout",
"label": "PDF Multi-Page Layout"
},
{
"path": "/multi-tool",
"label": "PDF Page Editor"
},
{
"path": "/pdf-to-cbr",
"label": "PDF to CBR Converter"
},
{
"path": "/pdf-to-cbz",
"label": "PDF to CBZ Converter"
},
{
"path": "/pdf-to-csv",
"label": "PDF to CSV Converter"
},
{
"path": "/pdf-to-xlsx",
"label": "PDF to Excel Converter"
},
{
"path": "/pdf-to-html",
"label": "PDF to HTML Converter"
},
{
"path": "/pdf-to-img",
"label": "PDF to Image Converter"
},
{
"path": "/pdf-to-markdown",
"label": "PDF to Markdown Converter"
},
{
"path": "/pdf-to-pdfa",
"label": "PDF to PDF/A Converter"
},
{
"path": "/pdf-to-presentation",
"label": "PDF to PowerPoint Converter"
},
{
"path": "/pdf-to-text",
"label": "PDF to Text Converter"
},
{
"path": "/pdf-to-word",
"label": "PDF to Word Converter"
},
{
"path": "/pdf-to-xml",
"label": "PDF to XML Converter"
},
{
"path": "/read",
"label": "PDF Viewer"
},
{
"path": "/automate",
"label": "PDF Workflow Automation"
},
{
"path": "/redact",
"label": "Redact PDF"
},
{
"path": "/remove-annotations",
"label": "Remove Annotations from PDF"
},
{
"path": "/remove-blanks",
"label": "Remove Blank Pages from PDF"
},
{
"path": "/remove-image",
"label": "Remove Images from PDF"
},
{
"path": "/remove-certificate-sign",
"label": "Remove PDF Certificate Signature"
},
{
"path": "/remove-password",
"label": "Remove PDF Password"
},
{
"path": "/repair",
"label": "Repair PDF"
},
{
"path": "/replace-color-pdf",
"label": "Replace and Invert PDF Colors"
},
{
"path": "/scale-pages",
"label": "Resize PDF Pages"
},
{
"path": "/rotate",
"label": "Rotate PDF"
},
{
"path": "/sanitize",
"label": "Sanitize PDF"
},
{
"path": "/shared-sign",
"label": "Shared Signing"
},
{
"path": "/show-js",
"label": "Show JavaScript in PDF"
},
{
"path": "/sign",
"label": "Sign PDF"
},
{
"path": "/cert-sign",
"label": "Sign PDF with a Certificate"
},
{
"path": "/split",
"label": "Split PDF"
},
{
"path": "/scanner-image-split",
"label": "Split Scanned Photos"
},
{
"path": "/timestamp-pdf",
"label": "Timestamp PDF"
},
{
"path": "/unlock-pdf-forms",
"label": "Unlock PDF Forms"
},
{
"path": "/validate-signature",
"label": "Validate PDF Signature"
}
]
}
@@ -3,8 +3,9 @@
// strings) plus the actual images in public/og_images.
//
// Outputs:
// src/core/data/ogImageMap.json - { toolId: imageBasename } (imported by the client)
// public/og-metadata.json - { default, byTool, byPath } (read by the backend at startup)
// src/core/data/ogImageMap.json - { toolId: imageBasename } (imported by the client)
// src/core/data/urlSeoOverrides.json - { path: { title, description } } (client per-URL meta)
// public/og-metadata.json - { default, byTool, byPath, navLinks } (read by the build-time OG prerender)
//
// Run: `node scripts/generate-og-metadata.mjs` (writes files)
// `node scripts/generate-og-metadata.mjs --check` (CI drift guard: fails if stale)
@@ -131,6 +132,69 @@ const CONVERT_SEO_PAGES = {
},
};
// Keyword titles for base tools. The in-app label ("Compress") is the UI name,
// not the phrase people search ("Compress PDF"). Keyed by tool id; overrides the
// prerendered <title>, the crawlable H1, and the nav-hub label, and is fed to the
// running SPA (per alias) so the hydrated title matches. The in-app UI name is
// unaffected - it comes from the translation registry, not this file. The tool's
// own description is kept. Convert aliases are handled by CONVERT_SEO_PAGES.
const TOOL_SEO_TITLES = {
compress: "Compress PDF",
merge: "Merge PDF",
split: "Split PDF",
rotate: "Rotate PDF",
convert: "PDF Converter",
ocr: "OCR PDF",
watermark: "Add Watermark to PDF",
sign: "Sign PDF",
certSign: "Sign PDF with a Certificate",
redact: "Redact PDF",
crop: "Crop PDF",
flatten: "Flatten PDF",
repair: "Repair PDF",
compare: "Compare PDF Files",
sanitize: "Sanitize PDF",
addPassword: "Password Protect PDF",
removePassword: "Remove PDF Password",
changePermissions: "Change PDF Permissions",
addPageNumbers: "Add Page Numbers to PDF",
addText: "Add Text to PDF",
addImage: "Add Image to PDF",
addStamp: "Add Stamp to PDF",
addAttachments: "Add Attachments to PDF",
extractPages: "Extract PDF Pages",
removePages: "Delete Pages from PDF",
removeBlanks: "Remove Blank Pages from PDF",
removeImage: "Remove Images from PDF",
removeAnnotations: "Remove Annotations from PDF",
reorganizePages: "Organize PDF Pages",
extractImages: "Extract Images from PDF",
pageLayout: "PDF Multi-Page Layout",
scalePages: "Resize PDF Pages",
pdfToSinglePage: "Combine PDF into a Single Page",
adjustContrast: "Adjust PDF Colors and Contrast",
replaceColor: "Replace and Invert PDF Colors",
changeMetadata: "Edit PDF Metadata",
getPdfInfo: "PDF Info and Metadata Viewer",
overlayPdfs: "Overlay PDFs",
editTableOfContents: "Edit PDF Bookmarks",
pdfTextEditor: "Edit PDF Text",
formFill: "Fill PDF Forms",
multiTool: "PDF Page Editor",
read: "PDF Viewer",
annotate: "Annotate PDF",
automate: "PDF Workflow Automation",
scannerEffect: "Make a PDF Look Scanned",
scannerImageSplit: "Split Scanned Photos",
autoRename: "Auto Rename PDF Files",
bookletImposition: "Create a PDF Booklet",
unlockPDFForms: "Unlock PDF Forms",
removeCertSign: "Remove PDF Certificate Signature",
validateSignature: "Validate PDF Signature",
timestampPdf: "Timestamp PDF",
showJS: "Show JavaScript in PDF",
};
// Tools whose art exists under a legacy v1 filename that does not match the
// tool id or any current URL slug. Verified against public/og_images contents.
const LEGACY_IMAGE_OVERRIDES = {
@@ -230,6 +294,15 @@ for (const id of allIds) {
};
}
// Upgrade base-tool titles to their search phrases (see TOOL_SEO_TITLES).
for (const id of Object.keys(TOOL_SEO_TITLES)) {
if (!byTool[id]) {
console.warn(`[og] TOOL_SEO_TITLES: unknown tool id "${id}" - skipped`);
continue;
}
byTool[id].title = `${TOOL_SEO_TITLES[id]} - ${SITE_NAME}`;
}
// path -> toolId for every canonical path and every alias
const byPath = {};
for (const id of allIds) byPath[canonicalPath(id)] = id;
@@ -297,6 +370,47 @@ for (const [routePath, seo] of Object.entries(CONVERT_SEO_PAGES)) {
};
}
// Base-tool keyword copy for the running SPA, on every alias of the tool, so the
// hydrated <title> matches the prerendered one. Convert pages set above win (they
// have more specific per-alias keywords), hence the guard.
for (const [id, name] of Object.entries(TOOL_SEO_TITLES)) {
if (!byTool[id]) continue;
for (const p of [canonicalPath(id), ...(aliasesByTool[id] || [])])
if (!(p in urlSeoOverrides))
urlSeoOverrides[p] = { title: name, description: descFor(id) };
}
// --- crawlable internal-link hub --------------------------------------------
// One routable link per tool + each convert SEO landing page. The in-app
// toolbar uses onClick buttons (not <a href>), and the prerendered body is
// otherwise empty, so these baked links are the only tool links a crawler can
// follow - they spread link equity and help discovery. Paths are the first
// alias for each tool (guaranteed routable by parseToolRoute). Dev/link tools
// are external redirects, not landing pages, so they are excluded.
const stripSuffix = (t) =>
t.endsWith(` - ${SITE_NAME}`) ? t.slice(0, -` - ${SITE_NAME}`.length) : t;
const primaryPathByTool = {};
for (const [p, id] of Object.entries(urlToTool))
if (!(id in primaryPathByTool)) primaryPathByTool[id] = p;
const navLinks = [];
const seenNav = new Set();
const pushNav = (p, label) => {
if (p && !seenNav.has(p)) {
seenNav.add(p);
navLinks.push({ path: p, label });
}
};
for (const id of allIds) {
if (linkIds.includes(id)) continue;
pushNav(
primaryPathByTool[id] || canonicalPath(id),
stripSuffix(byTool[id].title),
);
}
for (const [p, seo] of Object.entries(CONVERT_SEO_PAGES)) pushNav(p, seo.name);
navLinks.sort((a, b) => a.label.localeCompare(b.label));
const manifest = {
default: {
image: `/og_images/${DEFAULT_IMAGE_BASENAME}.png`,
@@ -305,6 +419,7 @@ const manifest = {
},
byTool,
byPath,
navLinks,
};
const mapJson = JSON.stringify(ogImageMap, null, 2) + "\n";
+13 -1
View File
@@ -14,18 +14,25 @@ export interface OgInjectOptions {
noindex?: boolean;
siteRoot?: string | null;
isHome?: boolean;
pathPrefix?: string;
}
export interface OgNavLink {
path: string;
label: string;
}
export interface OgManifest {
default: OgEntry;
byTool: Record<string, OgEntry>;
byPath: Record<string, string>;
navLinks?: OgNavLink[];
}
export function escapeHtml(value: string): string;
export function buildOgTags(
entry: OgEntry,
opts?: { ogBase?: string; pageUrlPath?: string | null },
opts?: { ogBase?: string; pageUrlPath?: string | null; pathPrefix?: string },
): string;
export function buildRobotsTag(noindex: boolean): string;
export function buildCanonicalTag(canonicalUrl: string | null): string | null;
@@ -33,6 +40,11 @@ export function buildJsonLd(
entry: OgEntry,
opts: { siteRoot: string; pageUrl: string; isHome: boolean },
): string;
export function buildBodyContent(
entry: OgEntry,
opts?: { navLinks?: OgNavLink[]; heading?: string | null },
): string;
export function injectBody(html: string, content: string): string;
export function injectOg(
html: string,
entry: OgEntry,
+87 -13
View File
@@ -25,12 +25,17 @@ const absolute = (urlPath, ogBase) => (ogBase ? ogBase + urlPath : urlPath);
/**
* Build the OG/Twitter <meta> block for one route.
* @param {{image:string,title:string,description:string}} entry
* @param {{ogBase?:string, pageUrlPath?:string|null}} opts
* @param {{ogBase?:string, pageUrlPath?:string|null, pathPrefix?:string}} opts
*/
export function buildOgTags(entry, { ogBase = "", pageUrlPath = null } = {}) {
export function buildOgTags(
entry,
{ ogBase = "", pageUrlPath = null, pathPrefix = "" } = {},
) {
const title = escapeHtml(entry.title);
const description = escapeHtml(entry.description);
const imageUrl = absolute(entry.image, ogBase);
// entry.image is root-relative (/og_images/x.png); the assets deploy under the
// sub-path too, so the absolute form must carry pathPrefix like canonical/logo.
const imageUrl = ogBase ? ogBase + pathPrefix + entry.image : entry.image;
const image = escapeHtml(imageUrl);
const pageUrl = pageUrlPath
? escapeHtml(absolute(pageUrlPath, ogBase))
@@ -135,7 +140,7 @@ export function buildJsonLd(entry, { siteRoot, pageUrl, isHome }) {
* link plus JSON-LD structured data.
* @param {object} entry
* @param {{ogBase?:string, pageUrlPath?:string|null, canonicalPath?:string|null,
* noindex?:boolean, siteRoot?:string|null, isHome?:boolean}} opts
* noindex?:boolean, siteRoot?:string|null, isHome?:boolean, pathPrefix?:string}} opts
*/
export function injectOg(html, entry, opts = {}) {
const {
@@ -145,13 +150,14 @@ export function injectOg(html, entry, opts = {}) {
noindex = false,
siteRoot = null,
isHome = false,
pathPrefix = "",
} = opts;
const canonicalUrl = ogBase
? absolute(canonicalPath ?? pageUrlPath ?? "/", ogBase)
: null;
const resolvedSiteRoot = siteRoot ?? (ogBase ? `${ogBase}/` : "/");
const blocks = [
buildOgTags(entry, { ogBase, pageUrlPath }),
buildOgTags(entry, { ogBase, pageUrlPath, pathPrefix }),
buildRobotsTag(noindex),
buildCanonicalTag(canonicalUrl),
ogBase
@@ -176,6 +182,62 @@ export function injectOg(html, entry, opts = {}) {
.replace("</head>", ` ${head}</head>`);
}
// Scoped styling for the prerendered body content so the pre-hydration paint
// (what crawlers see and the first visible paint for users) looks intentional.
// It lives inside #root and is wiped when React mounts.
const SEO_STYLE =
"<style>.spdf-seo{max-width:760px;margin:0 auto;padding:56px 24px;" +
"font-family:system-ui,-apple-system,Segoe UI,Roboto,sans-serif;color:#1a1a1a}" +
".spdf-seo h1{font-size:2rem;line-height:1.2;margin:0 0 .75rem}" +
".spdf-seo p{font-size:1.05rem;line-height:1.6;color:#444;margin:0 0 1rem}" +
".spdf-seo nav{margin-top:2.5rem}.spdf-seo nav h2{font-size:.95rem;" +
"text-transform:uppercase;letter-spacing:.04em;color:#666;margin:0 0 .5rem}" +
".spdf-seo nav a{display:inline-block;margin:0 .75rem .35rem 0;" +
"color:#0a58ca;text-decoration:none;font-size:.92rem}" +
"@media(prefers-color-scheme:dark){.spdf-seo{color:#e8e8e8}" +
".spdf-seo p{color:#b8b8b8}.spdf-seo nav a{color:#6ea8fe}}</style>";
/**
* Build the crawlable body content injected into #root: an H1, the page
* description, and a hub of real <a href> links to every tool. React (createRoot)
* replaces it on mount, so it only ever shows on a full page load - i.e. to
* crawlers and as the first paint (which also helps LCP). Links are relative so
* they resolve against <base href> under any deploy (root, /app, context path).
* @param {{title:string,description:string}} entry
* @param {{navLinks?:{path:string,label:string}[], heading?:string|null}} opts
*/
export function buildBodyContent(
entry,
{ navLinks = [], heading = null } = {},
) {
const name =
heading ||
(entry.title.endsWith(APP_SUFFIX)
? entry.title.slice(0, -APP_SUFFIX.length)
: entry.title);
const links = navLinks
.map(
(l) =>
`<a href="${escapeHtml(l.path.replace(/^\//, ""))}">${escapeHtml(l.label)}</a>`,
)
.join("\n ");
const nav = links
? `\n <nav aria-label="All PDF tools">\n <h2>All PDF tools</h2>\n ${links}\n </nav>`
: "";
return (
`<div class="spdf-seo">${SEO_STYLE}\n ` +
`<h1>${escapeHtml(name)}</h1>\n ` +
`<p>${escapeHtml(entry.description)}</p>${nav}\n </div>`
);
}
const ROOT_RE = /<div id="root">\s*<\/div>/;
/** Inject crawlable content into the (otherwise empty) React mount point. */
export function injectBody(html, content) {
return html.replace(ROOT_RE, `<div id="root">${content}</div>`);
}
const BASE_HREF_RE = /<base\s+href="[^"]*"\s*\/?>/i;
// Clean single/multi-segment route (e.g. /compress, /settings/people); rejects
@@ -210,17 +272,25 @@ export async function prerenderOg({
const pathPrefix = baseHref.replace(/\/+$/, ""); // "" or "/app"
const siteRoot = ogBase ? `${ogBase}${pathPrefix}/` : "/";
const homePath = `${pathPrefix}/`;
const navLinks = manifest.navLinks || [];
await fs.writeFile(
path.join(distDir, "index.html"),
injectOg(template, manifest.default, {
ogBase,
pageUrlPath: ogBase ? homePath : null,
canonicalPath: homePath,
siteRoot,
isHome: true,
let home = injectOg(template, manifest.default, {
ogBase,
pageUrlPath: ogBase ? homePath : null,
canonicalPath: homePath,
siteRoot,
isHome: true,
pathPrefix,
});
// Home <title> stays the brand ("Stirling PDF"); the H1 targets the keyword.
home = injectBody(
home,
buildBodyContent(manifest.default, {
navLinks,
heading: "Free Online PDF Tools",
}),
);
await fs.writeFile(path.join(distDir, "index.html"), home);
let count = 0;
for (const [routePath, id] of Object.entries(manifest.byPath || {})) {
@@ -235,7 +305,11 @@ export async function prerenderOg({
noindex: !!entry.noindex,
siteRoot,
isHome: false,
pathPrefix,
});
// App/auth pages (noindex) stay the bare shell - no crawlable landing copy.
if (!entry.noindex)
html = injectBody(html, buildBodyContent(entry, { navLinks }));
const nested = segments.length > 1;
if (nested)
html = html.replace(BASE_HREF_RE, `<base href="${baseHref}" />`);
@@ -6,8 +6,10 @@ import { getToolOgImage } from "@app/data/ogImage";
// Build tooling (plain ESM, node:fs only) - import the helpers for coverage.
// eslint-disable-next-line no-restricted-imports -- build script lives outside the @app alias root
import {
buildBodyContent,
buildOgTags,
buildSitemap,
injectBody,
injectOg,
prerenderOg,
} from "../../../scripts/og-prerender.mjs";
@@ -103,6 +105,21 @@ describe("injectOg (build-time prerender)", () => {
);
expect(tags).toContain("A &quot;B&quot; &amp; &lt;C&gt;");
});
it("weaves the sub-path prefix into the absolute image URL", () => {
const tags = buildOgTags(entry, {
ogBase: "https://stirling.com",
pageUrlPath: "/app/compress",
pathPrefix: "/app",
});
// image lives under the sub-path too, like canonical/og:url/logo
expect(tags).toContain(
'<meta property="og:image" content="https://stirling.com/app/og_images/compress.png" />',
);
expect(tags).toContain(
'<meta name="twitter:image" content="https://stirling.com/app/og_images/compress.png" />',
);
});
});
describe("injectOg SEO extras (robots, canonical, JSON-LD)", () => {
@@ -216,6 +233,103 @@ describe("buildSitemap", () => {
});
});
describe("buildBodyContent + injectBody (crawlable landing content)", () => {
const entry = {
image: "/og_images/compress.png",
title: "PDF to Word Converter - Stirling PDF",
description: "Convert PDF files into editable Word documents.",
};
const navLinks = [
{ path: "/compress", label: "Compress" },
{ path: "/merge", label: "Merge" },
];
it("emits an H1 (keyword, suffix stripped), the description, and relative tool links", () => {
const body = buildBodyContent(entry, { navLinks });
expect(body).toContain("<h1>PDF to Word Converter</h1>");
expect(body).toContain(
"<p>Convert PDF files into editable Word documents.</p>",
);
// links are relative (no leading slash) so they resolve against <base href>
expect(body).toContain('<a href="compress">Compress</a>');
expect(body).toContain('<a href="merge">Merge</a>');
expect(body).not.toContain('href="/compress"');
});
it("uses an explicit heading override for the H1 when given", () => {
const body = buildBodyContent(entry, {
navLinks,
heading: "Free Online PDF Tools",
});
expect(body).toContain("<h1>Free Online PDF Tools</h1>");
expect(body).not.toContain("<h1>PDF to Word Converter</h1>");
});
it("escapes HTML in the H1/description", () => {
const body = buildBodyContent(
{ ...entry, title: "A & <B>", description: 'x "y"' },
{ navLinks: [] },
);
expect(body).toContain("<h1>A &amp; &lt;B&gt;</h1>");
expect(body).toContain("x &quot;y&quot;");
});
it("injectBody fills the empty React mount point", () => {
const out = injectBody(
'<body><div id="root"></div><script></script></body>',
"<h1>hi</h1>",
);
expect(out).toContain('<div id="root"><h1>hi</h1></div>');
});
it("prerenderOg injects landing content on indexable pages but not noindex ones", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "og-body-"));
await fs.writeFile(path.join(dir, "index.html"), TEMPLATE);
const manifest = {
default: {
image: "/og_images/home.png",
title: "Stirling PDF",
description: "home",
},
byTool: {
compress: {
image: "/og_images/compress.png",
title: "Compress - Stirling PDF",
description: "c",
},
"/settings/people": {
image: "/og_images/home.png",
title: "People Settings - Stirling PDF",
description: "p",
noindex: true,
},
},
byPath: {
"/compress": "compress",
"/settings/people": "/settings/people",
},
navLinks: [{ path: "/compress", label: "Compress" }],
};
await prerenderOg({ distDir: dir, manifest, ogBase: "", baseHref: "/" });
const compress = await fs.readFile(path.join(dir, "compress.html"), "utf8");
expect(compress).toContain('<div id="root"><div class="spdf-seo">');
expect(compress).toContain("<h1>Compress</h1>");
const settings = await fs.readFile(
path.join(dir, "settings", "people.html"),
"utf8",
);
expect(settings).toContain('<div id="root"></div>'); // noindex: bare shell
const home = await fs.readFile(path.join(dir, "index.html"), "utf8");
expect(home).toContain("spdf-seo");
await fs.rm(dir, { recursive: true, force: true });
});
});
describe("prerenderOg (flat + nested route files)", () => {
it("writes a flat file per single-segment route and a nested file (absolute base) per sub-route", async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "og-prerender-"));
@@ -74,5 +74,357 @@
"/pdf-to-cbz": {
"title": "PDF to CBZ Converter",
"description": "Convert PDF documents into CBZ comic book archives. Free and private."
},
"/compress": {
"title": "Compress PDF",
"description": "Compress PDFs to reduce their file size."
},
"/compress-pdf": {
"title": "Compress PDF",
"description": "Compress PDFs to reduce their file size."
},
"/merge": {
"title": "Merge PDF",
"description": "Merge multiple PDFs into a single document"
},
"/merge-pdfs": {
"title": "Merge PDF",
"description": "Merge multiple PDFs into a single document"
},
"/split": {
"title": "Split PDF",
"description": "Split PDFs into multiple documents"
},
"/split-pdfs": {
"title": "Split PDF",
"description": "Split PDFs into multiple documents"
},
"/auto-split-pdf": {
"title": "Split PDF",
"description": "Split PDFs into multiple documents"
},
"/auto-size-split-pdf": {
"title": "Split PDF",
"description": "Split PDFs into multiple documents"
},
"/split-by-size-or-count": {
"title": "Split PDF",
"description": "Split PDFs into multiple documents"
},
"/split-pdf-by-sections": {
"title": "Split PDF",
"description": "Split PDFs into multiple documents"
},
"/split-pdf-by-chapters": {
"title": "Split PDF",
"description": "Split PDFs into multiple documents"
},
"/rotate": {
"title": "Rotate PDF",
"description": "Easily rotate your PDFs."
},
"/rotate-pdf": {
"title": "Rotate PDF",
"description": "Easily rotate your PDFs."
},
"/convert": {
"title": "PDF Converter",
"description": "Convert files to and from PDF format"
},
"/convert-pdf": {
"title": "PDF Converter",
"description": "Convert files to and from PDF format"
},
"/ocr": {
"title": "OCR PDF",
"description": "Extract text from scanned PDFs using Optical Character Recognition"
},
"/ocr-pdf": {
"title": "OCR PDF",
"description": "Extract text from scanned PDFs using Optical Character Recognition"
},
"/watermark": {
"title": "Add Watermark to PDF",
"description": "Add a custom watermark to your PDF document."
},
"/add-watermark": {
"title": "Add Watermark to PDF",
"description": "Add a custom watermark to your PDF document."
},
"/sign": {
"title": "Sign PDF",
"description": "Adds signature to PDF by drawing, text or image"
},
"/cert-sign": {
"title": "Sign PDF with a Certificate",
"description": "Sign PDF documents using digital certificates"
},
"/manage-signatures": {
"title": "Sign PDF with a Certificate",
"description": "Sign PDF documents using digital certificates"
},
"/redact": {
"title": "Redact PDF",
"description": "Permanently remove sensitive information from PDF documents"
},
"/auto-redact": {
"title": "Redact PDF",
"description": "Permanently remove sensitive information from PDF documents"
},
"/crop": {
"title": "Crop PDF",
"description": "Crop a PDF to reduce its size (maintains text!)"
},
"/flatten": {
"title": "Flatten PDF",
"description": "Remove all interactive elements and forms from a PDF"
},
"/repair": {
"title": "Repair PDF",
"description": "Repair corrupted or damaged PDF files"
},
"/compare": {
"title": "Compare PDF Files",
"description": "Compare two PDF documents and highlight differences"
},
"/sanitize": {
"title": "Sanitize PDF",
"description": "Remove potentially harmful elements from PDF files"
},
"/sanitize-pdf": {
"title": "Sanitize PDF",
"description": "Remove potentially harmful elements from PDF files"
},
"/add-password": {
"title": "Password Protect PDF",
"description": "Add password protection and restrictions to PDF files"
},
"/remove-password": {
"title": "Remove PDF Password",
"description": "Remove password protection from PDF documents"
},
"/change-permissions": {
"title": "Change PDF Permissions",
"description": "Change document restrictions and permissions"
},
"/add-page-numbers": {
"title": "Add Page Numbers to PDF",
"description": "Add Page numbers throughout a document in a set location"
},
"/add-text": {
"title": "Add Text to PDF",
"description": "Add custom text anywhere in your PDF"
},
"/add-image": {
"title": "Add Image to PDF",
"description": "Add images anywhere in your PDF"
},
"/add-stamp": {
"title": "Add Stamp to PDF",
"description": "Add text or add image stamps at set locations"
},
"/stamp": {
"title": "Add Stamp to PDF",
"description": "Add text or add image stamps at set locations"
},
"/add-attachments": {
"title": "Add Attachments to PDF",
"description": "Add or remove embedded files (attachments) to/from a PDF"
},
"/extract-pages": {
"title": "Extract PDF Pages",
"description": "Extract specific pages from a PDF document"
},
"/extract-page": {
"title": "Extract PDF Pages",
"description": "Extract specific pages from a PDF document"
},
"/remove-pages": {
"title": "Delete Pages from PDF",
"description": "Remove specific pages from a PDF document"
},
"/remove-blanks": {
"title": "Remove Blank Pages from PDF",
"description": "Remove blank pages from PDF documents"
},
"/remove-image": {
"title": "Remove Images from PDF",
"description": "Remove all images from a PDF document"
},
"/remove-image-pdf": {
"title": "Remove Images from PDF",
"description": "Remove all images from a PDF document"
},
"/remove-annotations": {
"title": "Remove Annotations from PDF",
"description": "Remove annotations and comments from PDF documents"
},
"/reorganize-pages": {
"title": "Organize PDF Pages",
"description": "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
},
"/pdf-organizer": {
"title": "Organize PDF Pages",
"description": "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."
},
"/extract-images": {
"title": "Extract Images from PDF",
"description": "Extract images from PDF documents"
},
"/page-layout": {
"title": "PDF Multi-Page Layout",
"description": "Merge multiple pages of a PDF document into a single page"
},
"/multi-page-layout": {
"title": "PDF Multi-Page Layout",
"description": "Merge multiple pages of a PDF document into a single page"
},
"/scale-pages": {
"title": "Resize PDF Pages",
"description": "Change the size/scale of a page and/or its contents."
},
"/pdf-to-single-page": {
"title": "Combine PDF into a Single Page",
"description": "Merges all PDF pages into one large single page"
},
"/single-large-page": {
"title": "Combine PDF into a Single Page",
"description": "Merges all PDF pages into one large single page"
},
"/adjust-contrast": {
"title": "Adjust PDF Colors and Contrast",
"description": "Adjust colors and contrast of PDF documents"
},
"/replace-color": {
"title": "Replace and Invert PDF Colors",
"description": "Replace or invert colors in PDF documents"
},
"/replace-color-pdf": {
"title": "Replace and Invert PDF Colors",
"description": "Replace or invert colors in PDF documents"
},
"/replace-and-invert-color-pdf": {
"title": "Replace and Invert PDF Colors",
"description": "Replace or invert colors in PDF documents"
},
"/change-metadata": {
"title": "Edit PDF Metadata",
"description": "Change/Remove/Add metadata from a PDF document"
},
"/get-pdf-info": {
"title": "PDF Info and Metadata Viewer",
"description": "Grabs any and all information possible on PDFs"
},
"/get-info-on-pdf": {
"title": "PDF Info and Metadata Viewer",
"description": "Grabs any and all information possible on PDFs"
},
"/overlay-pdfs": {
"title": "Overlay PDFs",
"description": "The Free Adobe Acrobat alternative (10M+ Downloads)"
},
"/overlay-pdf": {
"title": "Overlay PDFs",
"description": "The Free Adobe Acrobat alternative (10M+ Downloads)"
},
"/edit-table-of-contents": {
"title": "Edit PDF Bookmarks",
"description": "Add or edit bookmarks and table of contents in PDF documents"
},
"/pdf-text-editor": {
"title": "Edit PDF Text",
"description": "Review and edit text and images in PDFs with grouped text editing and PDF regeneration"
},
"/form-fill": {
"title": "Fill PDF Forms",
"description": "Fill PDF form fields interactively with a visual editor"
},
"/multi-tool": {
"title": "PDF Page Editor",
"description": "Use multiple tools on a single PDF document"
},
"/read": {
"title": "PDF Viewer",
"description": "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."
},
"/view-pdf": {
"title": "PDF Viewer",
"description": "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."
},
"/annotate": {
"title": "Annotate PDF",
"description": "Highlight, draw, add notes, and shapes directly in the viewer"
},
"/annotations": {
"title": "Annotate PDF",
"description": "Highlight, draw, add notes, and shapes directly in the viewer"
},
"/automate": {
"title": "PDF Workflow Automation",
"description": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
},
"/pipeline": {
"title": "PDF Workflow Automation",
"description": "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."
},
"/scanner-effect": {
"title": "Make a PDF Look Scanned",
"description": "Create a PDF that looks like it was scanned"
},
"/fake-scan": {
"title": "Make a PDF Look Scanned",
"description": "Create a PDF that looks like it was scanned"
},
"/scanner-image-split": {
"title": "Split Scanned Photos",
"description": "Detect and split scanned photos into separate pages"
},
"/extract-image-scans": {
"title": "Split Scanned Photos",
"description": "Detect and split scanned photos into separate pages"
},
"/auto-rename": {
"title": "Auto Rename PDF Files",
"description": "Automatically rename PDF files based on their content"
},
"/booklet-imposition": {
"title": "Create a PDF Booklet",
"description": "Create booklets with proper page ordering and multi-page layout for printing and binding"
},
"/unlock-p-d-f-forms": {
"title": "Unlock PDF Forms",
"description": "Remove read-only property of form fields in a PDF document."
},
"/unlock-pdf-forms": {
"title": "Unlock PDF Forms",
"description": "Remove read-only property of form fields in a PDF document."
},
"/remove-cert-sign": {
"title": "Remove PDF Certificate Signature",
"description": "Remove digital signature from PDF documents"
},
"/remove-certificate-sign": {
"title": "Remove PDF Certificate Signature",
"description": "Remove digital signature from PDF documents"
},
"/validate-signature": {
"title": "Validate PDF Signature",
"description": "Verify digital signatures and certificates in PDF documents"
},
"/timestamp-pdf": {
"title": "Timestamp PDF",
"description": "Add an RFC 3161 document timestamp to prove when your PDF existed"
},
"/show-j-s": {
"title": "Show JavaScript in PDF",
"description": "Extract and display JavaScript code from PDF documents"
},
"/show-js": {
"title": "Show JavaScript in PDF",
"description": "Extract and display JavaScript code from PDF documents"
},
"/show-javascript": {
"title": "Show JavaScript in PDF",
"description": "Extract and display JavaScript code from PDF documents"
}
}