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) <noreply@anthropic.com>
This commit is contained in:
null 2026-09-10 04:28:25 -05:00
parent aab0104b01
commit fef8b0718c
3 changed files with 45 additions and 19 deletions

View File

@ -13,13 +13,35 @@ export const websiteLd = {
const MAX_DESCRIPTION = 158 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, * The description a page emits, from one of two sources.
* cutting on a word boundary rather than mid-word. *
* @param {...string} parts * `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) => { export const buildDescription = ({ approved, parts = [] }) => {
const text = parts.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim() 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 if (text.length <= MAX_DESCRIPTION) return text
const cut = text.slice(0, MAX_DESCRIPTION) const cut = text.slice(0, MAX_DESCRIPTION)

View File

@ -1,5 +1,5 @@
import SEO from '@/components/SEO' 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 { useParams } from 'react-router-dom'
import { industries } from '@/data/industries' import { industries } from '@/data/industries'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
@ -33,11 +33,13 @@ const IndustryDetail = () => {
} }
const industryTitle = `${industry.name} Communications & IT | Queue North` const industryTitle = `${industry.name} Communications & IT | Queue North`
const industryDesc = clampDescription( const industryDesc = buildDescription({
industry.shortDesc, parts: [
`Queue North delivers phone, contact center, network, and IT support for ${industry.name.toLowerCase()} organizations.`, 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 industryUrl = `${SITE_URL}/industries/${industry.id}`
const industryBreadcrumbLd = buildBreadcrumbLd([ const industryBreadcrumbLd = buildBreadcrumbLd([
{ name: 'Industries', path: '/industries' }, { name: 'Industries', path: '/industries' },
{ name: industry.name, path: `/industries/${industry.id}` }, { name: industry.name, path: `/industries/${industry.id}` },

View File

@ -1,5 +1,5 @@
import SEO from '@/components/SEO' 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 { useParams } from 'react-router-dom'
import { services } from '@/data/services' import { services } from '@/data/services'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
@ -38,12 +38,14 @@ const ServiceDetail = () => {
) )
} }
const serviceTitle = `${service.name} | Queue North` // A service with owner-approved copy carries its own title and description in
const serviceDesc = clampDescription( // `page.seo`, and both are emitted verbatim. Everything else composes.
service.shortDesc, const serviceTitle = service.page?.seo?.title ?? `${service.name} | Queue North`
'Delivered by Queue North, a veteran-owned 8x8 and Cisco Certified Partner.', const serviceDesc = buildDescription({
) approved: service.page?.seo?.description,
const serviceUrl = `https://queuenorth.com/services/${service.id}` 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([ const serviceBreadcrumbLd = buildBreadcrumbLd([
{ name: 'Services', path: '/services' }, { name: 'Services', path: '/services' },
{ name: service.name, path: `/services/${service.id}` }, { name: service.name, path: `/services/${service.id}` },
@ -52,7 +54,7 @@ const ServiceDetail = () => {
'@context': 'https://schema.org', '@context': 'https://schema.org',
'@type': 'Service', '@type': 'Service',
name: service.name, name: service.name,
description: service.shortDesc, description: service.page?.seo?.description ?? service.shortDesc,
provider: { provider: {
'@type': 'Organization', '@type': 'Organization',
name: 'Queue North Technologies', name: 'Queue North Technologies',