Merge branch 'main' into uplink/prod
This commit is contained in:
@@ -129,7 +129,10 @@ If needed, you can run the same deploy logic directly on the server:
|
||||
| Command | Description |
|
||||
| ----------------- | ------------------------ |
|
||||
| `npm run dev` | Start Vite dev server |
|
||||
| `npm run build` | Production build |
|
||||
| `npm run seo:validate` | Validate SEO catalog integrity (families/slugs/keywords) |
|
||||
| `npm run seo:artifacts` | Generate sitemap artifacts in `web/public/sitemaps/` |
|
||||
| `npm run seo:prerender` | Pre-render SEO routes into `web/dist/` |
|
||||
| `npm run build` | Production build + SEO validate/artifacts/prerender |
|
||||
| `npm run lint` | ESLint check |
|
||||
| `npm run format` | Format with Prettier |
|
||||
| `npm run preview` | Preview production build |
|
||||
|
||||
@@ -35,6 +35,13 @@ server {
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Explicitly prevent indexing on auth entry routes (including legacy aliases)
|
||||
location ~ ^/(login|register|auth/login|auth/register)/?$ {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache";
|
||||
add_header X-Robots-Tag "noindex, follow" always;
|
||||
}
|
||||
|
||||
# SPA fallback — serve index.html for all routes
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
# Auth routes are intentionally crawlable so bots can observe
|
||||
# page/server-level noindex directives (meta robots + X-Robots-Tag).
|
||||
Disallow: /dashboard
|
||||
Disallow: /customers
|
||||
Disallow: /settings
|
||||
Disallow: /onboarding
|
||||
Disallow: /login
|
||||
Disallow: /register
|
||||
|
||||
Sitemap: https://pulsescore.app/sitemap.xml
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const SITE_ORIGIN = "https://pulsescore.app";
|
||||
|
||||
const FAMILY_PATH_PREFIX = {
|
||||
templates: "/templates",
|
||||
integrations: "/integrations",
|
||||
personas: "/for",
|
||||
comparisons: "/compare",
|
||||
glossary: "/glossary",
|
||||
examples: "/examples",
|
||||
curation: "/best",
|
||||
};
|
||||
const FAMILY_CONFIG = JSON.parse(
|
||||
readFileSync(new URL("../src/content/seo-family-config.json", import.meta.url), "utf8"),
|
||||
);
|
||||
|
||||
const FAMILY_PATH_PREFIX = Object.fromEntries(
|
||||
Object.entries(FAMILY_CONFIG).map(([family, config]) => [family, config.path]),
|
||||
);
|
||||
|
||||
const CHANGEFREQ = {
|
||||
core: "weekly",
|
||||
@@ -77,13 +76,7 @@ async function main() {
|
||||
const corePaths = [
|
||||
"/",
|
||||
"/pricing",
|
||||
"/templates",
|
||||
"/integrations",
|
||||
"/for",
|
||||
"/compare",
|
||||
"/glossary",
|
||||
"/examples",
|
||||
"/best",
|
||||
...Object.values(FAMILY_PATH_PREFIX),
|
||||
];
|
||||
|
||||
const byFamily = {
|
||||
|
||||
@@ -1,58 +1,11 @@
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const SITE_ORIGIN = "https://pulsescore.app";
|
||||
|
||||
const FAMILY_CONFIG = {
|
||||
templates: {
|
||||
path: "/templates",
|
||||
label: "Templates",
|
||||
hubTitle: "Customer health templates for lean SaaS teams",
|
||||
hubDescription:
|
||||
"Action-ready customer health templates you can use immediately, then operationalize in PulseScore.",
|
||||
},
|
||||
integrations: {
|
||||
path: "/integrations",
|
||||
label: "Integrations",
|
||||
hubTitle: "Integration playbooks for customer health scoring",
|
||||
hubDescription:
|
||||
"Connect billing, CRM, and support signals to unify churn-risk visibility in one workflow.",
|
||||
},
|
||||
personas: {
|
||||
path: "/for",
|
||||
label: "Personas",
|
||||
hubTitle: "PulseScore by persona and growth stage",
|
||||
hubDescription:
|
||||
"See how founders, CS, and RevOps teams tailor health scoring to their operating model.",
|
||||
},
|
||||
comparisons: {
|
||||
path: "/compare",
|
||||
label: "Comparisons",
|
||||
hubTitle: "PulseScore comparison guides",
|
||||
hubDescription:
|
||||
"Balanced comparisons focused on setup speed, pricing, and fit for lean teams.",
|
||||
},
|
||||
glossary: {
|
||||
path: "/glossary",
|
||||
label: "Glossary",
|
||||
hubTitle: "Customer success and churn glossary",
|
||||
hubDescription:
|
||||
"Clear, practical definitions with examples and implementation context for SaaS operators.",
|
||||
},
|
||||
examples: {
|
||||
path: "/examples",
|
||||
label: "Examples",
|
||||
hubTitle: "Real customer health examples",
|
||||
hubDescription:
|
||||
"Concrete examples of health scores, thresholds, alerts, and intervention patterns.",
|
||||
},
|
||||
curation: {
|
||||
path: "/best",
|
||||
label: "Best-of Guides",
|
||||
hubTitle: "Curated best-of customer success guides",
|
||||
hubDescription:
|
||||
"Research-backed shortlists of tools and approaches by team size, stack, and outcomes.",
|
||||
},
|
||||
};
|
||||
const FAMILY_CONFIG = JSON.parse(
|
||||
readFileSync(new URL("../src/content/seo-family-config.json", import.meta.url), "utf8"),
|
||||
);
|
||||
|
||||
const FAMILY_ORDER = [
|
||||
"templates",
|
||||
@@ -134,46 +87,18 @@ function withBrandTitle(base) {
|
||||
return `${truncatedBase}${suffix}`;
|
||||
}
|
||||
|
||||
function renderTemplate(template, page) {
|
||||
return template
|
||||
.replaceAll("{entity}", page.entity)
|
||||
.replaceAll("{entityLower}", page.entity.toLowerCase());
|
||||
}
|
||||
|
||||
function buildTitle(page) {
|
||||
switch (page.family) {
|
||||
case "templates":
|
||||
return withBrandTitle(`${page.entity} Template`);
|
||||
case "integrations":
|
||||
return withBrandTitle(`PulseScore + ${page.entity} Integration`);
|
||||
case "personas":
|
||||
return withBrandTitle(`Health Scoring for ${page.entity}`);
|
||||
case "comparisons":
|
||||
return withBrandTitle(`${page.entity} Comparison`);
|
||||
case "glossary":
|
||||
return withBrandTitle(`${page.entity} Definition`);
|
||||
case "examples":
|
||||
return withBrandTitle(`${page.entity} Examples`);
|
||||
case "curation":
|
||||
return withBrandTitle(page.entity);
|
||||
default:
|
||||
return withBrandTitle(page.entity);
|
||||
}
|
||||
return withBrandTitle(renderTemplate(FAMILY_CONFIG[page.family].titleTemplate, page));
|
||||
}
|
||||
|
||||
function buildDescription(page) {
|
||||
switch (page.family) {
|
||||
case "templates":
|
||||
return `Use this ${page.entity.toLowerCase()} template to prioritize risk, define thresholds, and run a repeatable retention workflow for B2B SaaS.`;
|
||||
case "integrations":
|
||||
return `Learn how ${page.entity} can feed customer health signals into PulseScore so your team can identify churn risk earlier and act faster.`;
|
||||
case "personas":
|
||||
return `See how ${page.entity.toLowerCase()} use PulseScore to monitor account health, reduce churn, and focus effort where it drives revenue retention.`;
|
||||
case "comparisons":
|
||||
return `Compare ${page.entity} across pricing, setup speed, integrations, and fit so lean customer success teams can choose with confidence.`;
|
||||
case "glossary":
|
||||
return `Understand ${page.entity.toLowerCase()} with clear definitions, formulas, examples, and practical guidance for SaaS customer success workflows.`;
|
||||
case "examples":
|
||||
return `Explore ${page.entity.toLowerCase()} with practical patterns, implementation notes, and proven workflows for retention-focused teams.`;
|
||||
case "curation":
|
||||
return `Review ${page.entity.toLowerCase()} using transparent criteria: setup complexity, integration depth, pricing, and operational fit for SMB SaaS.`;
|
||||
default:
|
||||
return `${page.entity} resources and implementation guidance from PulseScore.`;
|
||||
}
|
||||
return renderTemplate(FAMILY_CONFIG[page.family].descriptionTemplate, page);
|
||||
}
|
||||
|
||||
function buildPath(page) {
|
||||
@@ -181,20 +106,7 @@ function buildPath(page) {
|
||||
}
|
||||
|
||||
function buildHeading(page) {
|
||||
switch (page.family) {
|
||||
case "templates":
|
||||
return `Free ${page.entity} Template for B2B SaaS`;
|
||||
case "integrations":
|
||||
return `PulseScore + ${page.entity}`;
|
||||
case "personas":
|
||||
return `PulseScore for ${page.entity}`;
|
||||
case "comparisons":
|
||||
return page.entity;
|
||||
case "glossary":
|
||||
return `What is ${page.entity}?`;
|
||||
default:
|
||||
return page.entity;
|
||||
}
|
||||
return renderTemplate(FAMILY_CONFIG[page.family].h1Template, page);
|
||||
}
|
||||
|
||||
function toAbsolute(path) {
|
||||
|
||||
@@ -1,24 +1,15 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
const VALID_FAMILIES = new Set([
|
||||
"templates",
|
||||
"integrations",
|
||||
"personas",
|
||||
"comparisons",
|
||||
"glossary",
|
||||
"examples",
|
||||
"curation",
|
||||
]);
|
||||
const FAMILY_CONFIG = JSON.parse(
|
||||
readFileSync(new URL("../src/content/seo-family-config.json", import.meta.url), "utf8"),
|
||||
);
|
||||
|
||||
const FAMILY_PATH_PREFIX = {
|
||||
templates: "/templates",
|
||||
integrations: "/integrations",
|
||||
personas: "/for",
|
||||
comparisons: "/compare",
|
||||
glossary: "/glossary",
|
||||
examples: "/examples",
|
||||
curation: "/best",
|
||||
};
|
||||
const FAMILY_PATH_PREFIX = Object.fromEntries(
|
||||
Object.entries(FAMILY_CONFIG).map(([family, config]) => [family, config.path]),
|
||||
);
|
||||
|
||||
const VALID_FAMILIES = new Set(Object.keys(FAMILY_PATH_PREFIX));
|
||||
|
||||
const FAMILY_MINIMUMS = {
|
||||
templates: 20,
|
||||
@@ -43,19 +34,12 @@ function isSlugValid(slug) {
|
||||
async function main() {
|
||||
const raw = await readFile(new URL("../src/content/seo-pages.json", import.meta.url), "utf8");
|
||||
const pages = JSON.parse(raw);
|
||||
const families = Object.keys(FAMILY_PATH_PREFIX);
|
||||
|
||||
assert(Array.isArray(pages), "SEO catalog must be an array.");
|
||||
assert(pages.length >= 50, `Expected at least 50 SEO pages, found ${pages.length}.`);
|
||||
|
||||
const counts = {
|
||||
templates: 0,
|
||||
integrations: 0,
|
||||
personas: 0,
|
||||
comparisons: 0,
|
||||
glossary: 0,
|
||||
examples: 0,
|
||||
curation: 0,
|
||||
};
|
||||
const counts = Object.fromEntries(families.map((family) => [family, 0]));
|
||||
|
||||
const fullPaths = new Set();
|
||||
const keywords = new Set();
|
||||
|
||||
@@ -77,8 +77,16 @@ export default function FooterSection() {
|
||||
onSubmit={handleSubmit}
|
||||
className="mt-5 flex max-w-md flex-col gap-2 sm:flex-row"
|
||||
>
|
||||
<label htmlFor="footer-updates-email" className="sr-only">
|
||||
Work email
|
||||
</label>
|
||||
<input
|
||||
id="footer-updates-email"
|
||||
name="email"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
spellCheck={false}
|
||||
required
|
||||
placeholder="you@company.com"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
|
||||
72
web/src/content/seo-family-config.json
Normal file
72
web/src/content/seo-family-config.json
Normal file
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"templates": {
|
||||
"path": "/templates",
|
||||
"label": "Templates",
|
||||
"hubTitle": "Customer health templates for lean SaaS teams",
|
||||
"hubDescription": "Action-ready customer health templates you can use immediately, then operationalize in PulseScore.",
|
||||
"hero": "Turn retention strategy into execution with practical templates.",
|
||||
"titleTemplate": "{entity} Template",
|
||||
"descriptionTemplate": "Use this {entityLower} template to prioritize risk, define thresholds, and run a repeatable retention workflow for B2B SaaS.",
|
||||
"h1Template": "Free {entity} Template for B2B SaaS"
|
||||
},
|
||||
"integrations": {
|
||||
"path": "/integrations",
|
||||
"label": "Integrations",
|
||||
"hubTitle": "Integration playbooks for customer health scoring",
|
||||
"hubDescription": "Connect billing, CRM, and support signals to unify churn-risk visibility in one workflow.",
|
||||
"hero": "Build a single source of truth for customer health signals.",
|
||||
"titleTemplate": "PulseScore + {entity} Integration",
|
||||
"descriptionTemplate": "Learn how {entity} can feed customer health signals into PulseScore so your team can identify churn risk earlier and act faster.",
|
||||
"h1Template": "PulseScore + {entity}"
|
||||
},
|
||||
"personas": {
|
||||
"path": "/for",
|
||||
"label": "Personas",
|
||||
"hubTitle": "PulseScore by persona and growth stage",
|
||||
"hubDescription": "See how founders, CS, and RevOps teams tailor health scoring to their operating model.",
|
||||
"hero": "Find the exact customer health workflow for your team shape.",
|
||||
"titleTemplate": "Health Scoring for {entity}",
|
||||
"descriptionTemplate": "See how {entityLower} use PulseScore to monitor account health, reduce churn, and focus effort where it drives revenue retention.",
|
||||
"h1Template": "PulseScore for {entity}"
|
||||
},
|
||||
"comparisons": {
|
||||
"path": "/compare",
|
||||
"label": "Comparisons",
|
||||
"hubTitle": "PulseScore comparison guides",
|
||||
"hubDescription": "Balanced comparisons focused on setup speed, pricing, and fit for lean teams.",
|
||||
"hero": "Choose the right customer success stack with clarity, not guesswork.",
|
||||
"titleTemplate": "{entity} Comparison",
|
||||
"descriptionTemplate": "Compare {entity} across pricing, setup speed, integrations, and fit so lean customer success teams can choose with confidence.",
|
||||
"h1Template": "{entity}"
|
||||
},
|
||||
"glossary": {
|
||||
"path": "/glossary",
|
||||
"label": "Glossary",
|
||||
"hubTitle": "Customer success and churn glossary",
|
||||
"hubDescription": "Clear, practical definitions with examples and implementation context for SaaS operators.",
|
||||
"hero": "Learn the language of retention, then apply it in your workflow.",
|
||||
"titleTemplate": "{entity} Definition",
|
||||
"descriptionTemplate": "Understand {entityLower} with clear definitions, formulas, examples, and practical guidance for SaaS customer success workflows.",
|
||||
"h1Template": "What is {entity}?"
|
||||
},
|
||||
"examples": {
|
||||
"path": "/examples",
|
||||
"label": "Examples",
|
||||
"hubTitle": "Real customer health examples",
|
||||
"hubDescription": "Concrete examples of health scores, thresholds, alerts, and intervention patterns.",
|
||||
"hero": "Skip theory—see what good customer health execution looks like.",
|
||||
"titleTemplate": "{entity} Examples",
|
||||
"descriptionTemplate": "Explore {entityLower} with practical patterns, implementation notes, and proven workflows for retention-focused teams.",
|
||||
"h1Template": "{entity}"
|
||||
},
|
||||
"curation": {
|
||||
"path": "/best",
|
||||
"label": "Best-of Guides",
|
||||
"hubTitle": "Curated best-of customer success guides",
|
||||
"hubDescription": "Research-backed shortlists of tools and approaches by team size, stack, and outcomes.",
|
||||
"hero": "Use transparent evaluation criteria to pick the right tools faster.",
|
||||
"titleTemplate": "{entity}",
|
||||
"descriptionTemplate": "Review {entityLower} using transparent criteria: setup complexity, integration depth, pricing, and operational fit for SMB SaaS.",
|
||||
"h1Template": "{entity}"
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import rawSeoPages from "@/content/seo-pages.json";
|
||||
import rawFamilyConfig from "@/content/seo-family-config.json";
|
||||
|
||||
export type SeoFamily =
|
||||
| "templates"
|
||||
@@ -38,77 +39,15 @@ export interface SeoHub {
|
||||
interface FamilyConfig {
|
||||
path: string;
|
||||
label: string;
|
||||
titleSuffix: string;
|
||||
hubTitle: string;
|
||||
hubDescription: string;
|
||||
hero: string;
|
||||
titleTemplate: string;
|
||||
descriptionTemplate: string;
|
||||
h1Template: string;
|
||||
}
|
||||
|
||||
const familyConfig: Record<SeoFamily, FamilyConfig> = {
|
||||
templates: {
|
||||
path: "/templates",
|
||||
label: "Templates",
|
||||
titleSuffix: "Template",
|
||||
hubTitle: "Customer health templates for lean SaaS teams",
|
||||
hubDescription:
|
||||
"Action-ready customer health templates you can use immediately, then operationalize in PulseScore.",
|
||||
hero: "Turn retention strategy into execution with practical templates.",
|
||||
},
|
||||
integrations: {
|
||||
path: "/integrations",
|
||||
label: "Integrations",
|
||||
titleSuffix: "Integration",
|
||||
hubTitle: "Integration playbooks for customer health scoring",
|
||||
hubDescription:
|
||||
"Connect billing, CRM, and support signals to unify churn-risk visibility in one workflow.",
|
||||
hero: "Build a single source of truth for customer health signals.",
|
||||
},
|
||||
personas: {
|
||||
path: "/for",
|
||||
label: "Personas",
|
||||
titleSuffix: "for Teams",
|
||||
hubTitle: "PulseScore by persona and growth stage",
|
||||
hubDescription:
|
||||
"See how founders, CS, and RevOps teams tailor health scoring to their operating model.",
|
||||
hero: "Find the exact customer health workflow for your team shape.",
|
||||
},
|
||||
comparisons: {
|
||||
path: "/compare",
|
||||
label: "Comparisons",
|
||||
titleSuffix: "Comparison",
|
||||
hubTitle: "PulseScore comparison guides",
|
||||
hubDescription:
|
||||
"Balanced comparisons focused on setup speed, pricing, and fit for lean teams.",
|
||||
hero: "Choose the right customer success stack with clarity, not guesswork.",
|
||||
},
|
||||
glossary: {
|
||||
path: "/glossary",
|
||||
label: "Glossary",
|
||||
titleSuffix: "Definition",
|
||||
hubTitle: "Customer success and churn glossary",
|
||||
hubDescription:
|
||||
"Clear, practical definitions with examples and implementation context for SaaS operators.",
|
||||
hero: "Learn the language of retention, then apply it in your workflow.",
|
||||
},
|
||||
examples: {
|
||||
path: "/examples",
|
||||
label: "Examples",
|
||||
titleSuffix: "Examples",
|
||||
hubTitle: "Real customer health examples",
|
||||
hubDescription:
|
||||
"Concrete examples of health scores, thresholds, alerts, and intervention patterns.",
|
||||
hero: "Skip theory—see what good customer health execution looks like.",
|
||||
},
|
||||
curation: {
|
||||
path: "/best",
|
||||
label: "Best-of Guides",
|
||||
titleSuffix: "Best Guide",
|
||||
hubTitle: "Curated best-of customer success guides",
|
||||
hubDescription:
|
||||
"Research-backed shortlists of tools and approaches by team size, stack, and outcomes.",
|
||||
hero: "Use transparent evaluation criteria to pick the right tools faster.",
|
||||
},
|
||||
};
|
||||
const familyConfig = rawFamilyConfig as Record<SeoFamily, FamilyConfig>;
|
||||
|
||||
const seoPageSeeds = rawSeoPages as SeoPageSeed[];
|
||||
|
||||
@@ -116,6 +55,12 @@ function buildPath(seed: SeoPageSeed): string {
|
||||
return `${familyConfig[seed.family].path}/${seed.slug}`;
|
||||
}
|
||||
|
||||
function renderTemplate(template: string, seed: SeoPageSeed): string {
|
||||
return template
|
||||
.replaceAll("{entity}", seed.entity)
|
||||
.replaceAll("{entityLower}", seed.entity.toLowerCase());
|
||||
}
|
||||
|
||||
function withBrandTitle(base: string): string {
|
||||
const suffix = " | PulseScore";
|
||||
const maxLength = 60;
|
||||
@@ -139,66 +84,15 @@ export function toSeoTitle(base: string): string {
|
||||
}
|
||||
|
||||
function buildTitle(seed: SeoPageSeed): string {
|
||||
switch (seed.family) {
|
||||
case "templates":
|
||||
return withBrandTitle(`${seed.entity} Template`);
|
||||
case "integrations":
|
||||
return withBrandTitle(`PulseScore + ${seed.entity} Integration`);
|
||||
case "personas":
|
||||
return withBrandTitle(`Health Scoring for ${seed.entity}`);
|
||||
case "comparisons":
|
||||
return withBrandTitle(`${seed.entity} Comparison`);
|
||||
case "glossary":
|
||||
return withBrandTitle(`${seed.entity} Definition`);
|
||||
case "examples":
|
||||
return withBrandTitle(`${seed.entity} Examples`);
|
||||
case "curation":
|
||||
return withBrandTitle(seed.entity);
|
||||
default:
|
||||
return withBrandTitle(seed.entity);
|
||||
}
|
||||
return withBrandTitle(renderTemplate(familyConfig[seed.family].titleTemplate, seed));
|
||||
}
|
||||
|
||||
function buildDescription(seed: SeoPageSeed): string {
|
||||
switch (seed.family) {
|
||||
case "templates":
|
||||
return `Use this ${seed.entity.toLowerCase()} template to prioritize risk, define thresholds, and run a repeatable retention workflow for B2B SaaS.`;
|
||||
case "integrations":
|
||||
return `Learn how ${seed.entity} can feed customer health signals into PulseScore so your team can identify churn risk earlier and act faster.`;
|
||||
case "personas":
|
||||
return `See how ${seed.entity.toLowerCase()} use PulseScore to monitor account health, reduce churn, and focus effort where it drives revenue retention.`;
|
||||
case "comparisons":
|
||||
return `Compare ${seed.entity} across pricing, setup speed, integrations, and fit so lean customer success teams can choose with confidence.`;
|
||||
case "glossary":
|
||||
return `Understand ${seed.entity.toLowerCase()} with clear definitions, formulas, examples, and practical guidance for SaaS customer success workflows.`;
|
||||
case "examples":
|
||||
return `Explore ${seed.entity.toLowerCase()} with practical patterns, implementation notes, and proven workflows for retention-focused teams.`;
|
||||
case "curation":
|
||||
return `Review ${seed.entity.toLowerCase()} using transparent criteria: setup complexity, integration depth, pricing, and operational fit for SMB SaaS.`;
|
||||
default:
|
||||
return `${seed.entity} resources and implementation guidance from PulseScore.`;
|
||||
}
|
||||
return renderTemplate(familyConfig[seed.family].descriptionTemplate, seed);
|
||||
}
|
||||
|
||||
function buildH1(seed: SeoPageSeed): string {
|
||||
switch (seed.family) {
|
||||
case "templates":
|
||||
return `Free ${seed.entity} Template for B2B SaaS`;
|
||||
case "integrations":
|
||||
return `PulseScore + ${seed.entity}`;
|
||||
case "personas":
|
||||
return `PulseScore for ${seed.entity}`;
|
||||
case "comparisons":
|
||||
return seed.entity;
|
||||
case "glossary":
|
||||
return `What is ${seed.entity}?`;
|
||||
case "examples":
|
||||
return seed.entity;
|
||||
case "curation":
|
||||
return seed.entity;
|
||||
default:
|
||||
return seed.entity;
|
||||
}
|
||||
return renderTemplate(familyConfig[seed.family].h1Template, seed);
|
||||
}
|
||||
|
||||
export const seoHubs: SeoHub[] = (
|
||||
|
||||
@@ -267,6 +267,7 @@ export default function SeoProgrammaticPage({
|
||||
const sections = buildSections(page);
|
||||
const relatedPages = getRelatedPages(page, 6);
|
||||
const crossFamilyHubs = getCrossFamilyHubs(family);
|
||||
const lastUpdated = process.env.REACT_APP_SEO_LAST_UPDATED || undefined;
|
||||
|
||||
const structuredData = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user