Queue-North-Website/src/pages/PrivacyPolicy.jsx

165 lines
5.9 KiB
React
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
import SEO from '@/components/SEO'
import { buildBreadcrumbLd } from '@/lib/seo'
import { ShieldCheck } from 'lucide-react'
import { EFFECTIVE_DATE, LAST_UPDATED, PRIVACY_EMAIL, sections } from '@/data/privacyPolicy'
const EmailLink = ({ className = '' }) => (
<a
href={`mailto:${PRIVACY_EMAIL}`}
className={`font-semibold text-primary-blue underline underline-offset-4 hover:text-primary-navy transition-colors ${className}`}
>
{PRIVACY_EMAIL}
</a>
)
// Renders a paragraph, splitting the privacy address out as a mailto link when present.
const Paragraph = ({ text, linkEmail }) => {
if (!linkEmail || !text.includes(PRIVACY_EMAIL)) {
return <p className="mt-4 text-base leading-relaxed text-soft-text">{text}</p>
}
const [before, after] = text.split(PRIVACY_EMAIL)
return (
<p className="mt-4 text-base leading-relaxed text-soft-text">
{before}
<EmailLink />
{after}
</p>
)
}
const Block = ({ block }) => {
switch (block.type) {
case 'h3':
return <h3 className="mt-8 text-lg font-semibold text-primary-navy">{block.text}</h3>
case 'ul':
return (
<ul className="mt-4 space-y-2">
{block.items.map((item) => (
<li key={item} className="flex gap-3 text-base leading-relaxed text-soft-text">
<span className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-primary-cyan" aria-hidden="true" />
<span>{item}</span>
</li>
))}
</ul>
)
case 'callout':
return (
<p className="mt-5 rounded-md border border-border border-l-[3px] border-l-accent-gold bg-section-alt p-5 text-base leading-relaxed text-text">
{block.text}
</p>
)
case 'email':
return (
<p className="mt-4 text-base leading-relaxed">
<EmailLink />
</p>
)
case 'contactBlock':
return (
<div className="mt-5 rounded-md border border-border bg-section-alt p-6">
<p className="text-base font-semibold text-primary-navy">Queue North Technologies</p>
<p className="mt-2 text-base leading-relaxed text-soft-text">
Email: <EmailLink />
</p>
<p className="mt-1 text-base leading-relaxed text-soft-text">
Website:{' '}
<a
href="https://queuenorth.com"
className="font-semibold text-primary-blue underline underline-offset-4 hover:text-primary-navy transition-colors"
>
https://queuenorth.com
</a>
</p>
</div>
)
default:
return <Paragraph text={block.text} linkEmail={block.linkEmail} />
}
}
const PrivacyPolicy = () => {
const numberedSections = sections.filter((section) => section.number)
return (
<>
<SEO
title="Privacy Policy | Queue North Technologies"
description="How Queue North collects, uses, discloses, retains, and protects information across our website, advertisements, and hosted lead forms."
url="https://queuenorth.com/privacy-policy"
jsonLd={buildBreadcrumbLd([{ name: 'Privacy Policy', path: '/privacy-policy' }])}
/>
{/* Page Hero */}
<section className="bg-primary-navy py-16 lg:py-20 text-white">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="inline-flex items-center gap-2 rounded-md border border-white/20 bg-white/10 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-primary-cyan">
<ShieldCheck className="h-4 w-4" aria-hidden="true" />
Legal
</div>
<h1 className="mt-6 text-4xl md:text-5xl font-bold leading-tight">
Queue North Technologies Privacy Policy
</h1>
<dl className="mt-6 flex flex-col gap-2 text-sm text-white/75 sm:flex-row sm:gap-8">
<div className="flex gap-2">
<dt className="font-semibold text-white">Effective Date:</dt>
<dd>{EFFECTIVE_DATE}</dd>
</div>
<div className="flex gap-2">
<dt className="font-semibold text-white">Last Updated:</dt>
<dd>{LAST_UPDATED}</dd>
</div>
</dl>
</div>
</section>
{/* Policy Body */}
<section className="bg-background py-16 lg:py-20">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Contents */}
<nav aria-label="Privacy policy contents" className="rounded-md border border-border bg-white p-6 shadow-sm">
<h2 className="text-sm font-semibold uppercase tracking-wide text-primary-blue">Contents</h2>
<ol className="mt-4 grid grid-cols-1 gap-x-8 gap-y-2 sm:grid-cols-2">
{numberedSections.map((section) => (
<li key={section.id} className="text-sm">
<a
href={`#${section.id}`}
className="text-soft-text hover:text-primary-blue transition-colors"
>
<span className="font-numeric font-semibold text-primary-navy">{section.number}.</span>{' '}
{section.title}
</a>
</li>
))}
</ol>
</nav>
{sections.map((section) => (
<article key={section.id} id={section.id} className="mt-12 scroll-mt-28">
<h2 className="text-2xl md:text-3xl font-bold text-primary-navy">
{section.number ? (
<>
<span className="font-numeric">{section.number}.</span> {section.title}
</>
) : (
section.title
)}
</h2>
{section.blocks.map((block, index) => (
<Block key={`${section.id}-${index}`} block={block} />
))}
</article>
))}
</div>
</section>
</>
)
}
export default PrivacyPolicy