Commit Graph

120 Commits

Author SHA1 Message Date
null 798575576b ui: the site claimed 25 years of proven reliability, not 25 years of experience
Null's instruction, 2026-09-10. The number itself is confirmed and stays
everywhere it appears. What comes off is the claim wrapped around it in three
places, each of which asserted an outcome over that time rather than experience:

  index.html:12   meta description      "25+ years of proven reliability."
  index.html:15   og:description        "25+ years of proven reliability."
  Home.jsx:97     the home description  "...and networking. 25+ years."

All three now read "25+ years of industry experience", which is what the rest of
the site already said: the home trust chip ("25+ Years Experience"), the About
stat ("Communications and infrastructure experience"), the contact page ("25+
years of experience") and the footer.

Two nearby claims were left alone as different in kind, and are worth a second
look if the intent was broader: Contact.jsx:179 "8x8 Certified Partner with
proven expertise", which is about a partnership rather than a track record, and
About.jsx:83 "technology that works - proven, stable, and repeatable", which is
about the technology rather than the customers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 06:00:44 -05:00
null aff80334df feat(content): publish Levi's approved UC and Contact Center pages
The work this batch exists for. Levi Halford approved two copy sheets on
2026-08-28 and sent them the same day; they were never deployed. Closes #220,
#219 and #218.

What ships, verbatim from the sheets:

  /services/unified-communications   1,125 words, 10 sections,  7 FAQs
  /services/contact-center           2,499 words, 12 sections, 10 FAQs

Titles, H1s, hero subheadings and descriptions are the approved strings exactly,
including descriptions at 164 and 191 characters, which are emitted whole rather
than clamped. Each section opens with its direct answer, because that passage is
what a search result or an AI answer quotes. Every section carries a stable,
letter-first anchor, so an answer can deep-link to it. The FAQ is visible h3 and
p, with no FAQPage markup, as the sheets require.

The old copy goes: the unattributed "99.999% uptime reliability" the sheet named,
and both pages' short blurbs, which the approved sections now say at length.
idealFor stays, because the services index prints it as "Best fit".

Levi's two constraints are held and proven, not assumed. The footer is byte for
byte identical to the pre-batch build on all 19 pages. The Quick Info box is on
both pages, unchanged.

Verified against the built HTML, not the source: title, description and H1 match
the sheets exactly; all 22 approved H2s and all 17 FAQ questions are present; no
website-manager direction survived (checked against 20 phrases); no 99.999, no em
dash, no U+FFFD, no markdown. In a browser: no horizontal scroll, no broken
image, no console error at 320, 390, 768 and 1024; hydration clean on all 18
pages; all 24 section anchors deep-link correctly.

Docs corrected in the same commit, because each would have sent the next writer
the wrong way: REDESIGN_REVIEW's metrics lists now carry the owner's 2026-09-10
direction that a figure must be current, vendor-attributed and verified;
OVERHAUL_PLAN gains the long-form blueprint and stops recommending an accordion
for an FAQ; README stops claiming a server-rendered privacy fallback that never
existed, and says what hydration actually does now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 05:05:21 -05:00
null cd0a517d4a feat(seo): the industry and support pages now point at the two service pages
Both approved sheets ask for it: Unified Communications section 15 and Contact
Center section 18 each list the pages that should link back to them. Home, the
services index, the header and the footer already did. Managed Support and the
four industry pages did not, and the industry pages linked to no service at all.

Each industry page's own description already tells the reader Queue North
"delivers phone, contact center, network, and IT support" for that industry, so
all four qualify. The anchors are descriptive rather than the page names:
"Business phone and UCaaS solutions" and "Contact center and CCaaS solutions".

Rendered through the shared RelatedLinks component, in the industry sidebar card
and after "Ideal For" on a service page. It renders nothing without links, so
the five service pages that have none are untouched: only 5 of the 18 pages gain
a Related services block, and they are exactly the five the sheets name.

Verified against the build: both hrefs are present on managed-support and on all
four industry pages, the built-HTML audit passes with every link and fragment
resolving, and no other page gained the block.

Closes #221.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:59:49 -05:00
null 7ec4241f7d refactor(ui): one content renderer, and it refuses a block it does not know
The privacy policy held the site's only block renderer, and the two approved
service pages need the same vocabulary plus ordered steps, a figure and a link
list. Two renderers would have drifted, so there is one:
src/components/content/ContentBlocks.jsx, with p (including inline links), h3,
ul, ol (a step, optionally with its explanation), callout, image and links.

The old renderer sent an unknown block type to its paragraph case, which
rendered an empty <p>. A misspelt type silently dropped a paragraph of copy and
nothing said a word. The shared one throws, and the prerender turns that into a
build failure naming the route, the section and the block index.

PrivacyPolicy passes in the two block types only a policy has, email and
contactBlock, rather than forking the renderer. That page is the one an ad
platform must be able to read, so its markup had to be untouched: the renderer
was written to emit exactly what the page emitted before, prop order included.

Proven: dist/privacy-policy/index.html is byte for byte identical before and
after, 82,392 bytes either way, with asset hashes normalised. And a block type
misspelt as 'emial' fails the build with "prerender: /privacy-policy could not
be rendered: content: section "marketing", block 3: unknown block type".

RelatedLinks is the second shared piece, for the links the sheets ask each page
to carry. It renders nothing when a page has no links, so the pages that have
none are unchanged.

Closes #229.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:45:48 -05:00
null 0d575f2977 feat(build): the copy is checked before a single page is built from it
src/data is prose in a data structure, and nothing checked it. The long-form
service pages make that dangerous in a specific way: their copy arrives as an
owner-approved markdown sheet that MIXES DIRECTIONS TO THE WEBSITE MANAGER INTO
THE COPY. "Do not promise that every number is always portable." "Keep this
factual:" "Place an official 8x8 Work screenshot beside this section." Those
lines look exactly like copy, and publishing one puts an internal instruction on
a customer-facing page.

scripts/lib/content.js decides whether the content layer is publishable, and
prerender.js runs it before rendering anything, so every build enforces it: the
pre-commit hook, npm run verify, and the Docker image build. It refuses a
website-manager direction, an em dash, a U+FFFD, markdown or an HTML tag left in
a string, an unknown block type, a section id that is not letter-first, unique
and free of the layout's own ids, a section that does not open with its direct
answer (unless it declares kind list or faq), a FAQ question with no answer, a
link to a route or fragment that does not exist, an image whose src is missing
from public/ or has no alt or no dimensions, and the missing benefits or
idealFor list that the short layout maps without checking. A description over
160 characters is a note, not a failure: owner-approved copy is published as
written.

scripts/lib/routes.js is now the one route list. prerender.js built its own
while src/routes.jsx built the router's, and nothing compared them: a route in
one and not the other is never prerendered, so the server answers it with
404.html while the site's own navigation links to it. entry-server.jsx exports
the router table so the build can compare the two.

Proven by mutation, seventeen of them, each expecting exactly one finding and
getting it: unknown block type, FAQ answer removed, answer moved below its list,
duplicate id, digit-leading id, link to /services/contact-centre,
#no-such-section, missing image file, image without dimensions, image without
alt, em dash, a manager direction, markdown bold, U+FFFD, an HTML tag, missing
h1, empty section. Both generated content modules pass unmutated. Against a real
build: an em dash added to industries.js failed npm run build naming the field,
and a /pricing route added to src/routes.jsx failed it naming the route.

Closes #230.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:43:01 -05:00
null a6b87c7123 fix(ui): React was throwing away the prerendered page on every route
Suspected from the code while planning Batch 17, then confirmed in Chromium:
every page logged React error #418, a hydration mismatch. React answers a
mismatch by discarding the server DOM and re-rendering the page on the client.
So the prerender ran, crawlers received it, and every visitor's browser threw it
away and did the work again.

Two causes, both ours:

1. prerender hoisted the JSON-LD scripts out of the body into <head>. React
   hoists only async scripts with a src, so on the client that script stays
   where its component renders it. The DOM and the client's first render
   therefore disagreed on every page that emits structured data. JSON-LD is
   valid anywhere in the document, so it now stays where React puts it. Title,
   meta and link tags are still hoisted, because React hoists those itself.

2. main.jsx rendered sonner's <Toaster> in the first client pass, and the server
   entry never rendered one, so the client expected a <section> the prerendered
   HTML did not have. It mounts after hydration instead, which costs nothing: a
   toast can only follow an interaction.

Measured in a real browser, all 18 sitemap pages, before and after: hydration
errors 18 to 0, other console and page errors 0. Each page keeps its
server-rendered DOM (an h1 stamped before hydration survives), and still has
exactly one head title and one canonical. Structured data is unchanged in
substance: 27 JSON-LD blocks across 19 pages, and OAI-SearchBot still receives
Service, BreadcrumbList and Organization on the contact-center page.

Closes #226.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:37:33 -05:00
null b260bca24f fix(ui): a link with an odd fragment took the whole page down
document.querySelector THROWS on a fragment that is not a valid CSS selector,
and an id starting with a digit is not one. The call sat in an effect inside the
root route, so the error boundary replaced the header, the page and the footer
with the error screen. Live today: /services#1 blanks the page. Confirmed in
Chromium, where querySelector('#1') raises a DOMException.

That matters now because the long-form service pages exist to be deep-linked
from search results and AI answers, and two of their sections are about 8x8. A
link nobody here wrote must not be able to blank a page.

getElementById takes an id rather than a selector, so it cannot throw on one,
and the decode is wrapped for a fragment that is not valid percent-encoding.
An unknown id scrolls to the top, as before.

Proven in a real browser against the built site: /services#1, /#%E0 and
/services#8x8-implementation all render with header, footer and h1 intact and no
page error, and /contact#contact-form still scrolls to the form.

That run also confirmed at runtime what #226 suspected from reading the code:
every page logs React error #418, a hydration mismatch. It is pre-existing and
unrelated to this change, and it is fixed next.

Closes #231.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:34:41 -05:00
null fef8b0718c 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>
2026-09-10 04:28:25 -05:00
null aab0104b01 ui: no em dashes in anything a visitor or a crawler reads
Null's direction of 2026-09-10. Punctuation only: not one word changed.

Nine of them, and the share-image alt text was on every page twice:

  src/components/SEO.jsx:4        the default og:image and twitter:image alt
  index.html:25,:31               the same string, as template fallbacks
  src/pages/Home.jsx:257,:290
  src/pages/Services.jsx:99
  src/pages/Contact.jsx:183
  src/data/services.js:120        the wireless card on Home and the index
  public/site.webmanifest:4       linked from every page

Thirteen more sit in code comments and reach no page, so they stay. The hours
range on /contact uses en dashes, which are a different character and are not
what was asked about.

This lands before the guards that reject an em dash in content or in built
HTML, because until now every built page carried one and those guards would
have gone red on arrival.

Verified against the build, not the source: no U+2014 in any file under dist/,
including the HTML, the webmanifest, the sitemap and the JS bundle.

Closes #222.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 04:26:21 -05:00
Scarlett 231f07c62d fix(ui): header CTA clipped at iPad portrait, and reCAPTCHA cut off at 320px
Two real defects, found by rendering the site rather than reading it. Neither
was among the twenty issues filed for Batches 10 and 11.

#214 (P1) — at exactly 768px, iPad portrait, the header's "Request
Consultation" CTA measured x 676-778 against a 768px viewport: 10px sliced off,
with no scrollbar to reveal it because html/body carry overflow-x:hidden. At
that width the burger menu is already hidden, so the primary conversion action
was simply unreachable. The nav's five gap-6 gaps were the slack; gap-4 at md
(gap-6 from lg) frees 40px, keeping the CTA on screen at md rather than
deferring it to lg and leaving 768-1023px with no CTA at all.

#215 (P2) — Google's reCAPTCHA checkbox iframe is a fixed 304px that cannot be
resized. At 320px it measured x 41-345, so 25px of branding and the privacy and
terms links were clipped. Scaled to 0.85 below 360px with the wrapper height
reduced to match, since transform does not affect layout and the form would
otherwise gain dead space.

Also adds scripts/qa-browser.mjs, which found them. It measures horizontal
scroll, broken images, elements past the right edge, CLS and LCP across pages
and viewports. Written because seven of the ten issues in Batches 10 and 11
misstated their own evidence, and two of those would have made the site worse
if actioned.

Two false positives in the tool itself, both fixed before trusting it:
- Lazy images below the fold read as broken. It now scrolls the page AND chases
  each un-loaded image individually, and reports observed HTTP failures apart
  from never-rendered images.
- `--paths / --viewports 320` swallowed "320" as a path. Argument collection now
  stops at the next flag.

Verified against a local production build: 5 paths x 5 viewports, zero findings.
Before the fix the same run reported the CTA overflow at 768 on four pages and
the reCAPTCHA overflow at 320 on /contact.

Not yet live — this needs a release and a deploy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 04:05:37 -05:00
Scarlett 9fdc6b6032 fix(a11y): visible focus indicator, aria-expanded, and six Label-in-Name failures
Batch 10, and only three of its six issues were real. The other three are
answered on the tracker with the arithmetic.

#189 — focus indicator. Footer and Home carried zero focus styling across 17
links. Added one global :focus-visible rule rather than sprinkling classes,
because the issue's own scope was "all interactive <a> and <Link> elements".

Not the ring the issue asked for. It suggested ring-primary-cyan; #22D3EE is
6.26:1 on navy and 2.38:1 on white, so on this light-first design that ring
would have failed WCAG 1.4.11 across most of the site. Two rings instead — white
inside, navy outside — so the white carries the dark bands and the navy carries
the light sections. Worst case across every background in the palette is
13.62:1. box-shadow so both rings follow each element's own border-radius, plus
a transparent outline for forced-colors mode.

#192 — aria-expanded={isOpen} and aria-controls on the mobile nav trigger, and
the id on SheetContent it now points at. Confirmed as "false" in the
prerendered HTML.

#193 — INVERTED. The issue asked for aria-labels to be added consistently to
hero CTAs. The aria-labels already there were WCAG 2.5.3 Label in Name failures,
Level A: "Schedule a consultation" is not a superset of the visible "Schedule
Consultation", so a voice-control user saying what they see cannot activate the
link. Doing what the issue asked would have spread a Level A failure. Removed
the six that broke it; kept the eight that genuinely add context and do contain
their visible text, including the icon-only header logo link that needs one.

Audit now reports 0 failures across Home, Services, Footer and Header.

Verified: npm run build; the ring is in the built CSS, aria-expanded="false" and
id="mobile-nav-content" are in the prerendered HTML, all six removed labels are
absent from it, and the visible CTA text still renders.

NOT verified: nothing has been walked keyboard-only in a browser, and no screen
reader has been used. That gap is recorded in docs/qa/ClaudeQACoverage.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:31:13 -05:00
null ac6f5d1f78 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
null 196389ddf3 fix(brand): update veteran-owned badge to certified mark logo (batch 0.9.2) 2026-06-14 16:17:42 -05:00
null d074e597b2 feat(zoho): WebToLead forwarding mode, veteran-owned certified badge, Docker/env/CI updates (batch 0.9.1) 2026-06-14 16:08:29 -05:00
null 05b27d216a feat: Dockerfile/docker-compose updates, server improvements, contact form with recaptcha, API integration (batch 0.9.0) 2026-06-14 15:37:26 -05:00
null a8d9492a80 added 404 2026-05-28 00:41:24 -05:00
null 0f272fcf19 error and injection 2026-05-28 00:18:08 -05:00
null 3a61000c12 fix scroll 2026-05-27 23:40:09 -05:00
null e625a24b6e redirect to contact form 2026-05-27 22:33:54 -05:00
null f35de43952 fix footer 2026-05-27 22:19:04 -05:00
null c43d3bc955 company name footer one line 2026-05-27 22:08:46 -05:00
null ec14701795 center footer 2026-05-27 21:54:44 -05:00
null 78967ff56f phone 2026-05-27 21:49:21 -05:00
null cc1970fd1d added phone numebr hamburger 2026-05-27 21:44:06 -05:00
null 8c1e0f4c3d ui 2026-05-27 21:35:12 -05:00
null fb12d8cf3c 1 2026-05-27 21:23:17 -05:00
null a3ba03b7e1 cisco about 2026-05-27 21:15:54 -05:00
null f59d053afd mobile navbar text per owner 2026-05-27 21:04:30 -05:00
null 033bdf6625 Form now POSTs to Zoho 2026-05-27 20:57:55 -05:00
null 548e20e6f0 . 2026-05-27 14:43:50 -05:00
null 8f20670292 breadcrumbs 2026-05-27 14:27:30 -05:00
null 4410f01d48 Missing `Description` or `aria-describedby={undefined}` for {DialogContent}. 2026-05-27 14:11:48 -05:00
null 6bab4b5c89 corrections 2026-05-27 14:10:28 -05:00
null f9b36c198b badges size 2026-05-27 13:58:31 -05:00
null 4b17e9f109 badges 2026-05-27 13:28:13 -05:00
null 6de9490764 badge center 2026-05-27 12:51:08 -05:00
null a81e97fb31 mobile view 2026-05-27 12:42:25 -05:00
null 362a7e0059 heatmap 2026-05-26 17:08:55 -05:00
null 510edace07 removed duplicate card 2026-05-26 13:41:46 -05:00
null 8731903463 updated badges homepage 2026-05-26 13:36:16 -05:00
null a2a32687ff branding 2026-05-26 13:29:37 -05:00
null f0f0cfd599 #181 2026-05-26 13:04:57 -05:00
null 335601b00e remove submit ticket 2026-05-26 12:55:23 -05:00
null 85d7ae4bb1 logo enlarge 2026-05-26 12:44:10 -05:00
null bfcf7f114c #176 2026-05-26 12:31:26 -05:00
null f2a5e2c1bc Hero name widen 2026-05-26 12:26:13 -05:00
null d626c34ba2 parternership branding 2026-05-26 12:22:11 -05:00
null fd1e2b6f50 update 2026-05-25 20:26:50 -05:00
null afec6547c1 RECAPTCHA 2026-05-25 20:20:15 -05:00
null 7f48847049 CTA 2026-05-25 20:08:04 -05:00