Dev spec

VapeHub Product Page (PDP) — Developer Spec

What the PDP prototype demonstrates and what the production build must supply. One type-aware page on a single shared skeleton, adapting across four product types. Real VapeHub design system, not the mock's chrome. Companion to the design pack's product brief.

The prototype's 4-type switcher and the stock-state toggle are review affordances (they let you flip between templates and lifecycle states in one file) — in production each type renders as its own URL and stock comes from the catalogue, not a button.


★ The #1 fix — Server-render the buy box

The current live PDP ships the entire <main> as a "Loading product…" spinner: H1, price, variant selector, gallery, description, reviews are all client-rendered, absent from the server HTML. This is both the organic-decline root cause (crawlers see a spinner) and a conversion/reliability risk (a JS error, slow 4G or old Android leaves a spinner-locked buy box).

Server-render (or stream) the buy box — title, price, flavour selector, stock, add-to-basket, specs, reviews, FAQ, and the whole content deck below. The existing JSON-LD (Product + Breadcrumb + FAQ + aggregateRating) is the right shape — keep it, but see §7 on aggregateRating. This one change aligns the SEO-recovery and conversion agendas and is the highest-ROI fix on the site. The skeleton treatment in the prototype is for deferred data only (live stock, review counts) — never for the buy box itself.

SEO head tags — server-render these too (per type)

SSR'ing the buy box is necessary but not sufficient. Each of the four type URLs must serve, in the initial HTML: a unique <title> and meta description, a self-referential rel=canonical (the prototype ships neither), and OG/Twitter tags. Pick one canonical URL form and hold it everywhere — the prototype is inconsistent (schema @id/url are slash-less like /hayati-pro-max-plus while nav/internal links use trailing slashes, and the pod category appears as /prefilled-pod-kits/, /prefilled-pods/ and /hayati-pods/). Align nav, breadcrumb, JSON-LD @id/url, internal links and rel=canonical to the single chosen form before it fragments canonical signals.

1. The four types — one skeleton

All four share the same skeleton: sticky gallery + buy box (2-col desktop; single column + sticky Add bar on mobile, container-query breakpoint ~860px), then a content deck (description → specs → VapeHub Tested → reviews → FAQ → cross-sell). What changes per type is which selectors lead and which content blocks appear:

Type (data-p)Reference productLead selectorType-specific blocks
Device / Kit (device)Hayati Pro Max Plus 6000 (/hayati-pro-max-plus)Flavour range (searchable)Range-comparison table (this vs siblings, cost-per-1k-puffs), VapeHub Tested block
Pod / Refill (pod)Hayati Pro Max Plus Refill Pods (/hayati-pro-max-plus-pods)Flavour, compatibility-firstCompatibility panel (fits / doesn't fit, hard rule), pack-size
E-Liquid (liquid)Elfliq Nic Salt 10ml (/elfliq-nic-salts-by-elf-bar)Strength-first, then flavourVG/PG ratio, nic-salt vs freebase note, shortfill maths, flavour glossary
Nicotine Pouches (oral)VELO Nicotine Pouches (/velo-nicotine-pouches)Strength + formatFormat (slim/mini), pouches-per-can, tobacco-free framing, "start low" guidance

Each type keeps its own JSON-LD Product schema with its real canonical URL, brand, price and aggregateRating. Don't invent variants, stock or review counts — every one resolves to a real catalogue SKU or is flagged.

2. Variant selector — logic

The flavour is the product for kits/pods — treat it as first-class, never a <select>.

3. Bundle selector — multibuy as a tappable choice

Multibuy is a primary AOV lever, so it's selectable bundle cards (radio-style), not a footnote. Real deal tiers from the prototype (confirm against the promo engine):

TypeSingleTier 1Tier 2
Device£7.993 for £23 (£7.67 ea)4 for £30
Pod£5.493 for £15 (£5.00 ea)6 for £30
E-Liquid£2.895 for £10 (£2.00 ea)15 for £30
Pouches£3.993 for £10 (£3.33 ea)9 for £30

4. Add to Basket — logic

5. Stock lifecycle — four states

One stock value per product/variant drives the whole buy box. instock + low are buyable; soldout + discontinued are not, and must suppress every promise we can't honour:

Notify-me validates the email client-side in the prototype; production posts it to a back-in-stock list keyed to the variant SKU and emails on restock.

6. VapeHub Tested — the differentiator

The one inverted band on the page (Device type). It reports VapeHub's own measured data vs the manufacturer claim — e.g. measured 4,872 puffs vs a claimed 6,000 — plus per-profile real-use estimates and cost-per-day. This is genuine E-E-A-T and the block competitors don't have. It must carry real test figures or be withheld — never fabricated numbers, and the disclosure line (how/when tested) stays.

7. Content deck, reviews, trust

8. Delivery reassurance + dispatch countdown

In the buy box: "Free next-day delivery over £30 · £2.99 under", 30-day returns, plain packaging. Live dispatch countdown (same engine as basket/checkout): before 16:00 UK Mon–Fri → "Order within {Xh Ym Zs} for same-day dispatch" (tabular-nums); after cutoff / weekend / holiday → "ships {next working day}". Real/server time, never resets per visit. Fix the sitewide copy mismatch: same-day dispatch, not "shipping".

9. Accessibility & performance

10. Data model the back-end needs

11. Still to confirm

Multibuy promo model (per-SKU vs mix-and-match group) · real variant matrix + per-variant stock/SKUs/images · real review data + Trustpilot score · VapeHub Tested figures per product · dispatch calendar · loyalty earn rate · back-in-stock provider.

12. Compliance

No "disposable" wording · 18+ throughout · no health / "safer" / quit claims · plain unbranded packaging · factual button + description copy.

Compliance eyeball — pouch copy. The Nicotine Pouches descriptor "no vapour, no smoke, no smell" is factual (it describes the format) and low-risk, but reads as an implicit comparison to smoking — get it signed off with the rest of the copy.

Open compliance item — the "Ban-Proof Big Puffs" range name. It carries no "disposable" and no health claim, so it clears the black-letter rules, but framing a range around the ban and high puff-counts is exactly what Trading Standards / ASA are scrutinising next. Get legal/ASA sign-off on the range name (used in nav, breadcrumb, gallery tag, JSON-LD breadcrumb and body copy) before production.