2026-05-18 13:55:06 -05:00
|
|
|
import { Helmet } from 'react-helmet-async'
|
|
|
|
|
|
|
|
|
|
const DEFAULT_IMAGE = 'https://queuenorth.com/assets/og-image.png'
|
|
|
|
|
const DEFAULT_IMAGE_ALT = 'Queue North Technologies — Business Communications & IT Partner'
|
|
|
|
|
const SITE_NAME = 'Queue North Technologies'
|
|
|
|
|
|
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
|
|
|
const SEO = ({ title, description, url, type = 'website', image = DEFAULT_IMAGE, jsonLd, noindex = false }) => {
|
2026-05-18 13:55:06 -05:00
|
|
|
const schemas = jsonLd ? (Array.isArray(jsonLd) ? jsonLd : [jsonLd]) : []
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Helmet>
|
|
|
|
|
<title>{title}</title>
|
|
|
|
|
<meta name="description" content={description} />
|
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
|
|
|
{noindex && <meta name="robots" content="noindex, follow" />}
|
2026-05-18 13:55:06 -05:00
|
|
|
|
2026-05-18 13:58:35 -05:00
|
|
|
{/* Canonical URL — prevents duplicate content */}
|
|
|
|
|
<link rel="canonical" href={url} />
|
|
|
|
|
|
2026-05-18 13:55:06 -05:00
|
|
|
{/* Open Graph — Facebook, LinkedIn, iMessage, Google Messages, Slack */}
|
|
|
|
|
<meta property="og:title" content={title} />
|
|
|
|
|
<meta property="og:description" content={description} />
|
|
|
|
|
<meta property="og:url" content={url} />
|
|
|
|
|
<meta property="og:type" content={type} />
|
|
|
|
|
<meta property="og:site_name" content={SITE_NAME} />
|
|
|
|
|
<meta property="og:locale" content="en_US" />
|
|
|
|
|
<meta property="og:image" content={image} />
|
|
|
|
|
<meta property="og:image:secure_url" content={image} />
|
|
|
|
|
<meta property="og:image:type" content="image/png" />
|
|
|
|
|
<meta property="og:image:width" content="1200" />
|
|
|
|
|
<meta property="og:image:height" content="630" />
|
|
|
|
|
<meta property="og:image:alt" content={DEFAULT_IMAGE_ALT} />
|
|
|
|
|
|
|
|
|
|
{/* Twitter / X — also used by Apple Messages on iOS 13+ */}
|
|
|
|
|
<meta name="twitter:card" content="summary_large_image" />
|
|
|
|
|
<meta name="twitter:title" content={title} />
|
|
|
|
|
<meta name="twitter:description" content={description} />
|
|
|
|
|
<meta name="twitter:image" content={image} />
|
|
|
|
|
<meta name="twitter:image:alt" content={DEFAULT_IMAGE_ALT} />
|
|
|
|
|
|
|
|
|
|
{schemas.map((schema, i) => (
|
|
|
|
|
<script key={i} type="application/ld+json">{JSON.stringify(schema)}</script>
|
|
|
|
|
))}
|
|
|
|
|
</Helmet>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default SEO
|