Queue-North-Website/src/lib/seo.js

46 lines
1.4 KiB
JavaScript
Raw Normal View History

feat(seo): publish privacy policy, remove street address, prerender all routes (batch 0.9.3) Client directive (Levi Halford, 2026-08-01) ahead of Google/Meta lead forms. Privacy policy: - Publish approved policy verbatim at /privacy-policy (src/data/privacyPolicy.js is the single source of truth; 292/292 source lines verified present) - Privacy Policy link in the footer of every page - Effective/Last Updated 2026-07-31, privacy@queuenorth.com as mailto Remove St. Petersburg street address from every surface named in the brief: footer, contact page, schema markup, SEO metadata, Google Maps links. Collapse ProfessionalService + Organization schema into a single Organization with areaServed: United States; drop geo coordinates, priceRange, openingHours. Add the approved US-coverage sentence to About. No replacement address. Crawler visibility (the site previously served 0 bytes of body HTML without JS): - Prerender all 19 routes at build time via src/entry-server.jsx + scripts/prerender.js - Hoist title/meta/canonical/JSON-LD into <head>; renderToString does not do this and react-helmet-async's context is empty under React 19 - Serve prerendered HTML; return a real 404 for unknown paths instead of 200 - Hydrate instead of discarding the prerendered markup SEO/perf: - Titles <=60 and descriptions <=160 chars across all pages - Add BreadcrumbList to interior pages, WebSite to home - Generate sitemap.xml from the route list with git-derived lastmod - 301 duplicate URL forms (trailing slash, //, /index.html), preserving query - Immutable caching for content-hashed assets; no-cache for HTML - Split the 522 KB bundle into app/react-vendor/router/icons - loading/decoding/fetchpriority + per-route hero preload; drop unused asset Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 01:45:52 -05:00
export const SITE_URL = 'https://queuenorth.com'
// Sitewide search/entity anchor. Referenced by the Organization entity on Home.
export const websiteLd = {
'@context': 'https://schema.org',
'@type': 'WebSite',
'@id': `${SITE_URL}/#website`,
url: SITE_URL,
name: 'Queue North Technologies',
publisher: { '@id': `${SITE_URL}/#organization` },
inLanguage: 'en-US',
}
const MAX_DESCRIPTION = 158
/**
* Joins description fragments and trims to what search engines actually display,
* cutting on a word boundary rather than mid-word.
* @param {...string} parts
*/
export const clampDescription = (...parts) => {
const text = parts.filter(Boolean).join(' ').replace(/\s+/g, ' ').trim()
if (text.length <= MAX_DESCRIPTION) return text
const cut = text.slice(0, MAX_DESCRIPTION)
return `${cut.slice(0, cut.lastIndexOf(' ')).replace(/[,;:.]$/, '')}`
}
/**
* Builds a BreadcrumbList for a page's position in the site hierarchy.
* @param {Array<{name: string, path: string}>} trail ordered from the site root
*/
export const buildBreadcrumbLd = (trail) => ({
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ name: 'Home', path: '/' },
...trail,
].map((crumb, index) => ({
'@type': 'ListItem',
position: index + 1,
name: crumb.name,
item: `${SITE_URL}${crumb.path === '/' ? '' : crumb.path}`,
})),
})