mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Fix OG metadata drift, dev-tool indexing and the /processor price-0 Offer
This commit is contained in:
@@ -272,7 +272,7 @@
|
||||
},
|
||||
"formFill": {
|
||||
"image": "/og_images/form-fill.png",
|
||||
"title": "Form Editor - Stirling PDF",
|
||||
"title": "Fill PDF Forms - Stirling PDF",
|
||||
"description": "Fill, create, edit, and delete PDF form fields with a visual editor"
|
||||
},
|
||||
"multiTool": {
|
||||
@@ -293,22 +293,26 @@
|
||||
"devApi": {
|
||||
"image": "/og_images/dev-api.png",
|
||||
"title": "API - Stirling PDF",
|
||||
"description": "Link to API documentation"
|
||||
"description": "Link to API documentation",
|
||||
"noindex": true
|
||||
},
|
||||
"devFolderScanning": {
|
||||
"image": "/og_images/dev-folder-scanning.png",
|
||||
"title": "Automated Folder Scanning - Stirling PDF",
|
||||
"description": "Link to automated folder scanning guide"
|
||||
"description": "Link to automated folder scanning guide",
|
||||
"noindex": true
|
||||
},
|
||||
"devSsoGuide": {
|
||||
"image": "/og_images/dev-sso-guide.png",
|
||||
"title": "SSO Guide - Stirling PDF",
|
||||
"description": "Link to SSO guide"
|
||||
"description": "Link to SSO guide",
|
||||
"noindex": true
|
||||
},
|
||||
"devAirgapped": {
|
||||
"image": "/og_images/dev-airgapped.png",
|
||||
"title": "Air-gapped Setup - Stirling PDF",
|
||||
"description": "Link to air-gapped setup guide"
|
||||
"description": "Link to air-gapped setup guide",
|
||||
"noindex": true
|
||||
},
|
||||
"/editor": {
|
||||
"image": "/og_images/home.png",
|
||||
@@ -933,6 +937,10 @@
|
||||
"path": "/change-permissions",
|
||||
"label": "Change PDF Permissions"
|
||||
},
|
||||
{
|
||||
"path": "/classify",
|
||||
"label": "Classify"
|
||||
},
|
||||
{
|
||||
"path": "/single-large-page",
|
||||
"label": "Combine PDF into a Single Page"
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
},
|
||||
"formFill": {
|
||||
"image": "/og_images/form-fill.png",
|
||||
"title": "Form Editor - Stirling PDF",
|
||||
"title": "Fill PDF Forms - Stirling PDF",
|
||||
"description": "Fill, create, edit, and delete PDF form fields with a visual editor"
|
||||
},
|
||||
"multiTool": {
|
||||
@@ -294,22 +294,26 @@
|
||||
"devApi": {
|
||||
"image": "/og_images/dev-api.png",
|
||||
"title": "API - Stirling PDF",
|
||||
"description": "Link to API documentation"
|
||||
"description": "Link to API documentation",
|
||||
"noindex": true
|
||||
},
|
||||
"devFolderScanning": {
|
||||
"image": "/og_images/dev-folder-scanning.png",
|
||||
"title": "Automated Folder Scanning - Stirling PDF",
|
||||
"description": "Link to automated folder scanning guide"
|
||||
"description": "Link to automated folder scanning guide",
|
||||
"noindex": true
|
||||
},
|
||||
"devSsoGuide": {
|
||||
"image": "/og_images/dev-sso-guide.png",
|
||||
"title": "SSO Guide - Stirling PDF",
|
||||
"description": "Link to SSO guide"
|
||||
"description": "Link to SSO guide",
|
||||
"noindex": true
|
||||
},
|
||||
"devAirgapped": {
|
||||
"image": "/og_images/dev-airgapped.png",
|
||||
"title": "Air-gapped Setup - Stirling PDF",
|
||||
"description": "Link to air-gapped setup guide"
|
||||
"description": "Link to air-gapped setup guide",
|
||||
"noindex": true
|
||||
},
|
||||
"/editor": {
|
||||
"image": "/og_images/saas/app-editor.png",
|
||||
@@ -692,7 +696,8 @@
|
||||
"image": "/og_images/saas/app-processor.png",
|
||||
"title": "Stirling Processor - Govern every PDF your organization touches",
|
||||
"ogTitle": "Govern every PDF your organization touches",
|
||||
"description": "Redaction, retention, and encryption policies enforced everywhere PDFs enter your org. Distribute the free Editor anywhere. 1¢ per PDF."
|
||||
"description": "Redaction, retention, and encryption policies enforced everywhere PDFs enter your org. Distribute the free Editor anywhere. 1¢ per PDF.",
|
||||
"noOffer": true
|
||||
}
|
||||
},
|
||||
"byPath": {
|
||||
@@ -948,6 +953,10 @@
|
||||
"path": "/change-permissions",
|
||||
"label": "Change PDF Permissions"
|
||||
},
|
||||
{
|
||||
"path": "/classify",
|
||||
"label": "Classify"
|
||||
},
|
||||
{
|
||||
"path": "/single-large-page",
|
||||
"label": "Combine PDF into a Single Page"
|
||||
|
||||
@@ -216,6 +216,8 @@ const SAAS_ROUTE_OVERRIDES = {
|
||||
ogTitle: "Govern every PDF your organization touches",
|
||||
description:
|
||||
"Redaction, retention, and encryption policies enforced everywhere PDFs enter your org. Distribute the free Editor anywhere. 1¢ per PDF.",
|
||||
// Metered product - a price-0 Offer would contradict the page copy.
|
||||
noOffer: true,
|
||||
},
|
||||
"/editor": {
|
||||
image: "/og_images/saas/app-editor.png",
|
||||
@@ -322,6 +324,8 @@ for (const id of allIds) {
|
||||
image: `/og_images/${img || DEFAULT_IMAGE_BASENAME}.png`,
|
||||
title: titleFor(id),
|
||||
description: descFor(id),
|
||||
// Link tools are external redirects with no component, not landing pages.
|
||||
...(linkIds.includes(id) ? { noindex: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface OgEntry {
|
||||
title: string;
|
||||
description: string;
|
||||
noindex?: boolean;
|
||||
/** Omit the price-0 Offer from JSON-LD (metered/paid surfaces). */
|
||||
noOffer?: boolean;
|
||||
/** Punchier social-card title; falls back to `title` for the <title> tag. */
|
||||
ogTitle?: string;
|
||||
}
|
||||
|
||||
@@ -79,9 +79,9 @@ export function buildCanonicalTag(canonicalUrl) {
|
||||
|
||||
/**
|
||||
* Build a JSON-LD structured-data block. Home gets WebSite + Organization;
|
||||
* tool pages get a free WebApplication plus a Home > Tool breadcrumb. Needs an
|
||||
* tool pages get a WebApplication plus a Home > Tool breadcrumb. Needs an
|
||||
* absolute origin, so callers only invoke it when a canonical base is known.
|
||||
* @param {{title:string,description:string}} entry
|
||||
* @param {{title:string,description:string,noOffer?:boolean}} entry
|
||||
* @param {{siteRoot:string, pageUrl:string, isHome:boolean}} opts
|
||||
*/
|
||||
export function buildJsonLd(entry, { siteRoot, pageUrl, isHome }) {
|
||||
@@ -115,7 +115,12 @@ export function buildJsonLd(entry, { siteRoot, pageUrl, isHome }) {
|
||||
applicationCategory: "BusinessApplication",
|
||||
operatingSystem: "All",
|
||||
browserRequirements: "Requires JavaScript. Requires HTML5.",
|
||||
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
|
||||
// Manifest entries for metered products opt out (see noOffer).
|
||||
...(entry.noOffer
|
||||
? {}
|
||||
: {
|
||||
offers: { "@type": "Offer", price: "0", priceCurrency: "USD" },
|
||||
}),
|
||||
isPartOf: { "@type": "WebSite", name: SITE_NAME, url: siteRoot },
|
||||
publisher: organization,
|
||||
},
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getToolOgImage } from "@app/data/ogImage";
|
||||
import { CORE_LINK_TOOL_IDS } from "@app/types/toolId";
|
||||
// Build tooling (plain ESM, node:fs only) - import the helpers for coverage.
|
||||
// oxlint-disable-next-line no-restricted-imports -- build script lives outside the @app alias root
|
||||
import {
|
||||
buildBodyContent,
|
||||
buildJsonLd,
|
||||
buildOgTags,
|
||||
buildSitemap,
|
||||
injectBody,
|
||||
@@ -181,6 +184,18 @@ describe("injectOg SEO extras (robots, canonical, JSON-LD)", () => {
|
||||
expect(out).toContain('"@type":"BreadcrumbList"');
|
||||
});
|
||||
|
||||
it("omits the price-0 Offer when the entry opts out (metered surfaces)", () => {
|
||||
const opts = {
|
||||
siteRoot: "https://stirling.com/",
|
||||
pageUrl: "https://stirling.com/processor",
|
||||
isHome: false,
|
||||
};
|
||||
expect(buildJsonLd(entry, opts)).toContain('"price":"0"');
|
||||
expect(buildJsonLd({ ...entry, noOffer: true }, opts)).not.toContain(
|
||||
'"offers"',
|
||||
);
|
||||
});
|
||||
|
||||
it("escapes '<' inside JSON-LD so a value cannot close the script early", () => {
|
||||
const out = injectOg(
|
||||
TEMPLATE,
|
||||
@@ -564,3 +579,45 @@ describe("prerenderOg (flat + nested route files)", () => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
|
||||
// Contract over the committed generator output, which the fixture-based suites
|
||||
// above cannot see. Regenerate with `node scripts/generate-og-metadata.mjs`.
|
||||
describe("shipped OG manifests", () => {
|
||||
// Read import.meta.url via a variable: inlined, vite rewrites the
|
||||
// `new URL(..., import.meta.url)` asset pattern and the path resolves wrong.
|
||||
const here = import.meta.url;
|
||||
const load = async (name: string) =>
|
||||
JSON.parse(
|
||||
await fs.readFile(
|
||||
fileURLToPath(new URL(`../../../public/${name}`, here)),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
|
||||
it.each(["og-metadata.json", "og-metadata.saas.json"])(
|
||||
"%s marks every link tool noindex and keeps it out of the sitemap",
|
||||
async (name) => {
|
||||
const manifest = await load(name);
|
||||
const linkIds: readonly string[] = CORE_LINK_TOOL_IDS;
|
||||
for (const id of linkIds)
|
||||
expect(manifest.byTool[id]?.noindex, id).toBe(true);
|
||||
|
||||
const linkPaths = Object.entries(manifest.byPath)
|
||||
.filter(([, id]) => linkIds.includes(id as string))
|
||||
.map(([routePath]) => routePath);
|
||||
expect(linkPaths.length).toBeGreaterThanOrEqual(linkIds.length);
|
||||
|
||||
const xml = buildSitemap(manifest, { ogBase: "https://stirling.com" });
|
||||
for (const routePath of linkPaths)
|
||||
expect(xml).not.toContain(
|
||||
`<loc>https://stirling.com${routePath}</loc>`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it("opts the metered /processor out of the price-0 Offer, not /editor", async () => {
|
||||
const manifest = await load("og-metadata.saas.json");
|
||||
expect(manifest.byTool["/processor"].noOffer).toBe(true);
|
||||
expect(manifest.byTool["/editor"].noOffer).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
},
|
||||
"/form-fill": {
|
||||
"title": "Fill PDF Forms",
|
||||
"description": "Fill PDF form fields interactively with a visual editor"
|
||||
"description": "Fill, create, edit, and delete PDF form fields with a visual editor"
|
||||
},
|
||||
"/multi-tool": {
|
||||
"title": "PDF Page Editor",
|
||||
|
||||
Reference in New Issue
Block a user