From fef8b0718c6ed5c897a187a178406e14730ea1ff Mon Sep 17 00:00:00 2001 From: null Date: Thu, 10 Sep 2026 04:28:25 -0500 Subject: [PATCH] fix(seo): descriptions were two sentences run together on all eleven detail pages Live until now, on every service and industry page: "...seamless integration Delivered by Queue North, a veteran-owned..." "...for medical providers Queue North delivers phone, contact center..." clampDescription joined its fragments with a space, and no shortDesc in services.js or industries.js ends in a full stop. Nothing added one. The description is the first thing a searcher reads, and it read as a run-on on eleven of the eighteen pages. buildDescription replaces it and joins fragments AS SENTENCES. It also takes an `approved` description, which it emits verbatim: never joined, never clamped. Levi's approved copy for the two long-form pages is 164 and 191 characters, and the old 158-character clamp would have cut his sentences off with an ellipsis. ServiceDetail reads it from `page.seo`, which the content commit fills in. ServiceDetail and IndustryDetail now build their canonical from SITE_URL rather than repeating the origin. The three files release.sh greps for that literal are untouched, so the origin guard still has three copies to compare. Verified against a build, page by page: all 11 detail descriptions gained exactly a full stop and changed in no other way, and no other page's description moved. Closes #234. Co-Authored-By: Claude Opus 5 (1M context) --- src/lib/seo.js | 32 +++++++++++++++++++++++++++----- src/pages/IndustryDetail.jsx | 14 ++++++++------ src/pages/ServiceDetail.jsx | 18 ++++++++++-------- 3 files changed, 45 insertions(+), 19 deletions(-) diff --git a/src/lib/seo.js b/src/lib/seo.js index d673ae5..f16296a 100644 --- a/src/lib/seo.js +++ b/src/lib/seo.js @@ -13,13 +13,35 @@ export const websiteLd = { const MAX_DESCRIPTION = 158 +// A fragment that already ends a sentence keeps its punctuation, including when +// it closes with a quote or a bracket. One that does not gets a full stop. +const asSentence = (text) => (/[.!?…]["'”’)\]]?$/.test(text) ? text : `${text}.`) + /** - * Joins description fragments and trims to what search engines actually display, - * cutting on a word boundary rather than mid-word. - * @param {...string} parts + * The description a page emits, from one of two sources. + * + * `approved` is owner-approved text. It is emitted exactly as written: never + * joined to anything, never clamped. Levi's Unified Communications and Contact + * Center descriptions are 164 and 191 characters, and clamping them would have + * shipped his approved copy cut off mid-sentence with an ellipsis. + * + * `parts` are fragments this site composes itself, and they are joined AS + * SENTENCES. Joining them with a space alone is what produced "...seamless + * integration Delivered by Queue North..." on all eleven detail pages: no + * shortDesc in services.js or industries.js ends in a full stop, and nothing + * added one. The description is the first thing a searcher reads. + * + * @param {{approved?: string, parts?: string[]}} source */ -export const clampDescription = (...parts) => { - const text = parts.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim() +export const buildDescription = ({ approved, parts = [] }) => { + if (approved) return approved + + const text = parts + .map((part) => part?.replace(/\s+/g, ' ').trim()) + .filter(Boolean) + .map(asSentence) + .join(' ') + if (text.length <= MAX_DESCRIPTION) return text const cut = text.slice(0, MAX_DESCRIPTION) diff --git a/src/pages/IndustryDetail.jsx b/src/pages/IndustryDetail.jsx index 74e922b..9efc361 100644 --- a/src/pages/IndustryDetail.jsx +++ b/src/pages/IndustryDetail.jsx @@ -1,5 +1,5 @@ import SEO from '@/components/SEO' -import { buildBreadcrumbLd, clampDescription } from '@/lib/seo' +import { SITE_URL, buildBreadcrumbLd, buildDescription } from '@/lib/seo' import { useParams } from 'react-router-dom' import { industries } from '@/data/industries' import { Link } from 'react-router-dom' @@ -33,11 +33,13 @@ const IndustryDetail = () => { } const industryTitle = `${industry.name} Communications & IT | Queue North` - const industryDesc = clampDescription( - industry.shortDesc, - `Queue North delivers phone, contact center, network, and IT support for ${industry.name.toLowerCase()} organizations.`, - ) - const industryUrl = `https://queuenorth.com/industries/${industry.id}` + const industryDesc = buildDescription({ + parts: [ + industry.shortDesc, + `Queue North delivers phone, contact center, network, and IT support for ${industry.name.toLowerCase()} organizations.`, + ], + }) + const industryUrl = `${SITE_URL}/industries/${industry.id}` const industryBreadcrumbLd = buildBreadcrumbLd([ { name: 'Industries', path: '/industries' }, { name: industry.name, path: `/industries/${industry.id}` }, diff --git a/src/pages/ServiceDetail.jsx b/src/pages/ServiceDetail.jsx index 20a595c..f42d641 100644 --- a/src/pages/ServiceDetail.jsx +++ b/src/pages/ServiceDetail.jsx @@ -1,5 +1,5 @@ import SEO from '@/components/SEO' -import { buildBreadcrumbLd, clampDescription } from '@/lib/seo' +import { SITE_URL, buildBreadcrumbLd, buildDescription } from '@/lib/seo' import { useParams } from 'react-router-dom' import { services } from '@/data/services' import { Link } from 'react-router-dom' @@ -38,12 +38,14 @@ const ServiceDetail = () => { ) } - const serviceTitle = `${service.name} | Queue North` - const serviceDesc = clampDescription( - service.shortDesc, - 'Delivered by Queue North, a veteran-owned 8x8 and Cisco Certified Partner.', - ) - const serviceUrl = `https://queuenorth.com/services/${service.id}` + // A service with owner-approved copy carries its own title and description in + // `page.seo`, and both are emitted verbatim. Everything else composes. + const serviceTitle = service.page?.seo?.title ?? `${service.name} | Queue North` + const serviceDesc = buildDescription({ + approved: service.page?.seo?.description, + parts: [service.shortDesc, 'Delivered by Queue North, a veteran-owned 8x8 and Cisco Certified Partner.'], + }) + const serviceUrl = `${SITE_URL}/services/${service.id}` const serviceBreadcrumbLd = buildBreadcrumbLd([ { name: 'Services', path: '/services' }, { name: service.name, path: `/services/${service.id}` }, @@ -52,7 +54,7 @@ const ServiceDetail = () => { '@context': 'https://schema.org', '@type': 'Service', name: service.name, - description: service.shortDesc, + description: service.page?.seo?.description ?? service.shortDesc, provider: { '@type': 'Organization', name: 'Queue North Technologies',