From 20f36289129e3dde58c3e3837398877f6ef27baa Mon Sep 17 00:00:00 2001 From: Steve Simonson <144349219+SteveSimonson@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:04:04 -0500 Subject: [PATCH] feat: avatar-locked beauty gift guides (SEO wave 1) Port Kyasi gifts pattern to Adazo with five catalog-backed listicles: gifts-for-her, gifts-for-mom, gifts-for-wife, self-care-gift-edit, gifts-under-50. Hub + guide pages, nav/footer, home rail, PDP chips, ItemList/FAQ JSON-LD, sitemap and route-meta wiring. --- AGENTS.md | 10 + public/sitemap.xml | 218 +++++++----- scripts/generate-sitemap.mjs | 28 +- scripts/route-meta.ts | 7 + src/App.tsx | 4 + src/components/Layout.tsx | 6 + src/data/giftGuides.ts | 579 ++++++++++++++++++++++++++++++++ src/data/types.ts | 67 ++++ src/lib/seo.ts | 21 ++ src/lib/seoData.ts | 76 ++++- src/pages/GiftGuide.tsx | 325 ++++++++++++++++++ src/pages/Gifts.tsx | 120 +++++++ src/pages/Home.tsx | 68 ++++ src/pages/Product.tsx | 21 ++ worker/generated/routeMeta.json | 2 +- 15 files changed, 1458 insertions(+), 94 deletions(-) create mode 100644 src/data/giftGuides.ts create mode 100644 src/pages/GiftGuide.tsx create mode 100644 src/pages/Gifts.tsx diff --git a/AGENTS.md b/AGENTS.md index 76824d0..973c130 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,6 +4,16 @@ **Women’s beauty & Riviera finish.** Discover on Adazo; buy on Amazon. +## Gift guides (avatar-locked SEO listicles) + +Wave-1 beauty gift guides live at `/gifts` and `/gifts/:slug`. + +- **Data:** `src/data/giftGuides.ts` + gift types in `src/data/types.ts` +- **Pages:** `src/pages/Gifts.tsx` (hub), `src/pages/GiftGuide.tsx` (listicle) +- **SEO:** `giftsHubSeo` / `giftGuideSeo` in `src/lib/seoData.ts`; `itemListJsonLd` in `src/lib/seo.ts`; sitemap + `scripts/route-meta.ts` include gift URLs +- **Rules:** Catalog-backed `productEntries` only (every slug must exist in `products.ts`). Original gift framing (`giftWhy` ≥ ~40 words). Warm Riviera voice — never clinical cure claims. Skip Conbal slots (no Conbal on Adazo). Buy on Amazon with `rel="sponsored"`. Footer disclosure already on Layout. +- **Nav:** Primary nav + footer “Gifts”; Home featured rail; PDP “Also on gift guides” chips via `guidesForProduct`. + ## Product page enrichment Every catalog SKU should have long-form enrichment (review snapshot, item blog, FAQ, setup tips) so PDPs rank and convert as destinations—not thin affiliate hops. diff --git a/public/sitemap.xml b/public/sitemap.xml index 9c0de28..c8efc4f 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -2,547 +2,583 @@ https://adazo.com/ - 2026-08-01 + 2026-08-02 daily 1.0 https://adazo.com/shop - 2026-08-01 + 2026-08-02 daily 0.95 https://adazo.com/shop?limited=1 - 2026-08-01 + 2026-08-02 daily 0.9 + + https://adazo.com/gifts + 2026-08-02 + weekly + 0.9 + https://adazo.com/quiz - 2026-08-01 + 2026-08-02 weekly 0.85 https://adazo.com/reels - 2026-08-01 + 2026-08-02 weekly 0.8 https://adazo.com/watch - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/why - 2026-08-01 + 2026-08-02 monthly 0.7 + + https://adazo.com/gifts/gifts-for-her + 2026-08-02 + weekly + 0.85 + + + https://adazo.com/gifts/gifts-for-mom + 2026-08-02 + weekly + 0.85 + + + https://adazo.com/gifts/gifts-for-wife + 2026-08-02 + weekly + 0.85 + + + https://adazo.com/gifts/self-care-gift-edit + 2026-08-02 + weekly + 0.85 + + + https://adazo.com/gifts/gifts-under-50 + 2026-08-02 + weekly + 0.85 + https://adazo.com/shop?cat=handbags - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=jewelry - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=watches - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=gold - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=luxury - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=fragrance - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=skincare - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=hair - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=makeup - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=body - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=tools - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=sun-spf - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=lips - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/shop?cat=wellness - 2026-08-01 + 2026-08-02 daily 0.85 https://adazo.com/vibe/luxe - 2026-08-01 + 2026-08-02 weekly 0.8 https://adazo.com/vibe/muse - 2026-08-01 + 2026-08-02 weekly 0.8 https://adazo.com/vibe/sillage - 2026-08-01 + 2026-08-02 weekly 0.8 https://adazo.com/vibe/atelier - 2026-08-01 + 2026-08-02 weekly 0.8 https://adazo.com/vibe/dew - 2026-08-01 + 2026-08-02 weekly 0.8 https://adazo.com/vibe/gilded - 2026-08-01 + 2026-08-02 weekly 0.8 https://adazo.com/product/cerave-hydrating-facial-cleanser - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/la-roche-posay-toleriane-double-repair - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/neutrogena-hydro-boost-water-gel - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/the-ordinary-niacinamide-10-zinc-1 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/elf-holy-hydration-makeup-melting-cleansing-balm - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/olaplex-no3-hair-perfector - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/living-proof-dry-shampoo - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/maybelline-lash-sensational-sky-high-mascara - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/rare-beauty-soft-pinch-liquid-blush - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/eos-shea-better-body-lotion - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/sol-de-janeiro-brazilian-crush-body-mist - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/supergoop-unseen-sunscreen-spf-40 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/biore-uv-aqua-rich-watery-essence - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/dyson-airwrap-multi-styler - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/revlon-one-step-volumizer-plus - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/laneige-lip-sleeping-mask - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/vital-proteins-collagen-peptides - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/the-inkey-list-retinol-eye-cream - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/gisou-honey-infused-hair-oil - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/tatcha-the-dewy-skin-cream - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/sk-ii-facial-treatment-essence - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/drunk-elephant-protini-polypeptide-cream - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/charlotte-tilbury-pillow-talk-lipstick - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/la-mer-creme-de-la-mer - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/ysl-libre-eau-de-parfum - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/viktor-rolf-flowerbomb - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/chanel-chance-eau-tendre - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/paco-rabanne-olympea - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/dazzlingrock-necklace-lk8m - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/affy-necklace-dzs3 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/chow-sang-sang-necklace-29s8 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/houston-diamond-tennis-bracelet-9g2k - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/fine-jewelry-necklace-jzkd - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/fine-jewelry-tennis-bracelet-5cxp - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/le-vian-bracelet-2r3q - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/jewels-by-lux-tennis-bracelet-wqkb - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/louis-vuitton-pochette-4blf - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/gucci-tote-kfwh - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/louis-vuitton-tote-k56m - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/prada-tote-hpwg - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/prada-crossbody-ctjd - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/prada-crossbody-pggj - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/gucci-belt-bag-76kr - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/jacquemus-handbag-sx9c - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/prada-top-handle-g1d4 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/louis-vuitton-handbag-qvp3 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/rado-quartz-watch-8pg4 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/tissot-automatic-watch-dfk5 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/versace-chronograph-g8ck - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/rado-automatic-watch-z2m1 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/versace-luxury-watch-m7s1 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/rado-automatic-watch-341n - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/18k-gold-chain-necklace-2l6g - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/18k-rope-chain-1g9x - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/14k-box-chain-fvcq - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/14k-herringbone-chain-lf26 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/18k-rope-chain-7g3j - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/14k-herringbone-chain-gmpg - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/18k-cuban-link-chain-3gdl - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/14k-herringbone-chain-ffhw - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/10k-wedding-band-y5tz - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/14k-wedding-band-g5z2 - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/14k-gold-pendant-fpww - 2026-08-01 + 2026-08-02 weekly 0.75 https://adazo.com/product/14k-box-chain-2aaa - 2026-08-01 + 2026-08-02 weekly 0.75 diff --git a/scripts/generate-sitemap.mjs b/scripts/generate-sitemap.mjs index 8993138..80b4f5b 100644 --- a/scripts/generate-sitemap.mjs +++ b/scripts/generate-sitemap.mjs @@ -58,17 +58,43 @@ const products = [...new Set(productSlugs)].filter( (s) => !s.startsWith('fill-'), ) +/** Only top-level guide slugs (avoid product slugs inside productEntries). */ +function extractGiftGuideSlugs(filePath) { + try { + const src = readFileSync(filePath, 'utf8') + const slugs = [] + for (const m of src.matchAll( + /\{\s*slug:\s*['"]([a-z0-9][a-z0-9-]*)['"]\s*,\s*title:/g, + )) { + slugs.push(m[1]) + } + return [...new Set(slugs)] + } catch { + return [] + } +} +const giftSlugs = extractGiftGuideSlugs(join(ROOT, 'src/data/giftGuides.ts')) + /** @type {{ loc: string, changefreq: string, priority: string }[]} */ const urls = [ { loc: '/', changefreq: 'daily', priority: '1.0' }, { loc: '/shop', changefreq: 'daily', priority: '0.95' }, { loc: '/shop?limited=1', changefreq: 'daily', priority: '0.9' }, + { loc: '/gifts', changefreq: 'weekly', priority: '0.9' }, { loc: '/quiz', changefreq: 'weekly', priority: '0.85' }, { loc: '/reels', changefreq: 'weekly', priority: '0.8' }, { loc: '/watch', changefreq: 'weekly', priority: '0.75' }, { loc: '/why', changefreq: 'monthly', priority: '0.7' }, ] +for (const slug of giftSlugs) { + urls.push({ + loc: `/gifts/${slug}`, + changefreq: 'weekly', + priority: '0.85', + }) +} + for (const cat of categories) { urls.push({ loc: `/shop?cat=${cat}`, @@ -121,7 +147,7 @@ ${body} const out = join(ROOT, 'public/sitemap.xml') writeFileSync(out, xml) console.log( - `Wrote ${out} (${urls.length} URLs: ${products.length} products, ${vibes.length} vibes, ${categories.length} categories)`, + `Wrote ${out} (${urls.length} URLs: ${products.length} products, ${giftSlugs.length} gift guides, ${vibes.length} vibes, ${categories.length} categories)`, ) /** diff --git a/scripts/route-meta.ts b/scripts/route-meta.ts index 1f90ff0..bba1dda 100644 --- a/scripts/route-meta.ts +++ b/scripts/route-meta.ts @@ -7,6 +7,7 @@ * the React app sets after hydration. og:image stays sitewide per scope. */ import { CATEGORY_OPTIONS, products } from '../src/data/catalog' +import { giftGuides } from '../src/data/giftGuides' import { getProductEnrichment } from '../src/data/productEnrichments' import { VIBE_LIST } from '../src/data/vibes' import { @@ -16,6 +17,8 @@ import { } from '../src/lib/seo' import { finalizeRouteMeta, + giftGuideSeo, + giftsHubSeo, homeSeo, productSeo, quizSeo, @@ -50,6 +53,10 @@ export function buildRouteMeta(): RouteMetaFile { routes['/reels'] = finalizeRouteMeta(reelsSeo()) routes['/watch'] = finalizeRouteMeta(watchSeo()) routes['/why'] = finalizeRouteMeta(whySeo()) + routes['/gifts'] = finalizeRouteMeta(giftsHubSeo()) + for (const g of giftGuides) { + routes[`/gifts/${g.slug}`] = finalizeRouteMeta(giftGuideSeo(g)) + } for (const vibe of VIBE_LIST) { routes[`/vibe/${vibe.id}`] = finalizeRouteMeta(vibeSeo(vibe)) diff --git a/src/App.tsx b/src/App.tsx index c7fd58e..ba72db8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,8 @@ import { Quiz } from './pages/Quiz' import { VibePage } from './pages/Vibe' import { ReelsPage } from './pages/Reels' import { WatchPage } from './pages/Watch' +import { GiftsHubPage } from './pages/Gifts' +import { GiftGuidePage } from './pages/GiftGuide' export default function App() { return ( @@ -21,6 +23,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } /> } /> } /> diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index 10a30b2..941a4ec 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -9,6 +9,7 @@ import { GlobalSeo } from './Seo' /** Lean primary nav — categories live on /shop, not the header. */ const primaryNav = [ { to: '/shop', label: 'Shop', match: (path: string) => path === '/shop' || path.startsWith('/shop') || path.startsWith('/product/') }, + { to: '/gifts', label: 'Gifts', match: (path: string) => path.startsWith('/gifts') }, { to: '/quiz', label: 'Vibe', match: (path: string) => path.startsWith('/quiz') || path.startsWith('/vibe/') }, { to: '/watch', label: 'Watch', match: (path: string) => path.startsWith('/watch') || path.startsWith('/reels') }, ] as const @@ -185,6 +186,11 @@ export function Layout() { Shop +
  • + + Gifts + +
  • This week diff --git a/src/data/giftGuides.ts b/src/data/giftGuides.ts new file mode 100644 index 0000000..5ed481e --- /dev/null +++ b/src/data/giftGuides.ts @@ -0,0 +1,579 @@ +/** + * Adazo gift guides — avatar-locked beauty listicles. + * Catalog-backed only. Original prose. Warm Riviera voice; never clinical cure claims. + */ +import type { GiftGuide, GiftOccasionId, GiftRecipientId } from './types' + +const U = '2026-08-02' +const P = '2026-08-02' + +const AMAZON_FAQ = { + q: 'Will it arrive in time, and can she return it?', + a: 'Shipping speed and Prime eligibility depend on the live Amazon listing and seller. Returns follow Amazon’s policies for that order. Adazo shows typical street prices; always confirm delivery dates and return windows on Amazon before you buy.', +} + +export const giftGuides: GiftGuide[] = [ + { + slug: 'gifts-for-her', + title: 'Beauty Gifts for Her', + dek: 'Skincare she will reach for, hair that smells like a weekend, fragrance that lingers — and one tool that feels like a private salon.', + primaryQuery: 'gifts for her', + recipientIds: ['her', 'girlfriend', 'friend'], + occasionIds: ['birthday', 'christmas', 'valentines', 'just-because'], + budgetBands: ['under-50', '50-150', 'splurge'], + publishedAt: P, + updatedAt: U, + readMinutes: 7, + intro: + 'You are not shopping for “a woman who likes beauty.” You are shopping for someone who notices when a cream feels expensive on the cheek, when a mist blooms in a room, when a brush stroke looks considered rather than loud. This list stays on Adazo’s shelf: prestige and cult favorites with a clear job — glow, scent, soft hair, a flush of color. Skip novelty kits. Buy the piece that earns a permanent place on her vanity.', + productEntries: [ + { + productSlug: 'tatcha-the-dewy-skin-cream', + rank: 1, + priceBand: '50-150', + badge: 'Crowd-pleaser', + giftWhy: + 'The cream that looks like you already know her bathroom shelf. Rich without greasy theater, dewy without sticky film — the kind of jar people finish and reorder. Safe when she loves a luminous finish; less right if she only wears oil-free gels. Wrap it with a handwritten note and it reads as care, not guesswork.', + }, + { + productSlug: 'ysl-libre-eau-de-parfum', + rank: 2, + priceBand: '50-150', + badge: 'Fragrance win', + giftWhy: + 'Lavender-orange blossom drama in a bottle she can wear to dinner or a Tuesday. Libre feels modern and slightly rebellious — perfect when you want perfume, not a body spray. Confirm she likes floral-woody florals before you go all-in; if she lives in soft powder, Chance may suit better.', + }, + { + productSlug: 'olaplex-no3-hair-perfector', + rank: 3, + priceBand: 'under-50', + giftWhy: + 'For the woman who colors, heat-styles, or simply wants hair that feels stronger in the hand. A weekly treatment reads as thoughtful — you noticed her routine, not just her lipstick. Easy to ship, easy to use, and it pairs with any dryer or iron already in the house.', + }, + { + productSlug: 'rare-beauty-soft-pinch-liquid-blush', + rank: 4, + priceBand: 'under-50', + badge: 'Color gift', + giftWhy: + 'A little bottle that delivers a soft, lived-in flush — the opposite of heavy powder. Ideal when she already has foundations and needs the one product that makes a bare face feel finished. Shade is personal; if you know her undertone, this lands. If not, keep the receipt energy by gifting with a note that she can exchange.', + }, + { + productSlug: 'laneige-lip-sleeping-mask', + rank: 5, + priceBand: 'under-50', + giftWhy: + 'The overnight lip ritual that feels like a tiny spa. Berry-sweet, plush, and endlessly shareable in photos of nightstands. Works as a stocking stuffer or the “I saw this and thought of you” midweek gift. Pair with champagne or a paperback for maximum softness.', + }, + { + productSlug: 'sol-de-janeiro-brazilian-crush-body-mist', + rank: 6, + priceBand: 'under-50', + giftWhy: + 'Pistachio-caramel warmth that turns a hallway into vacation. Body mist is the low-risk fragrance gift — lighter than perfume, louder than lotion. She will spray it after the shower and smile. Best for playful taste; less ideal for ultra-minimal, unscented purists.', + }, + { + productSlug: 'dyson-airwrap-multi-styler', + rank: 7, + priceBand: 'splurge', + badge: 'Splurge', + giftWhy: + 'The tool that makes a home bathroom feel like a Riviera salon. Airwrap is for the woman who invests time in hair and will use attachments, not leave them in a drawer. Confirm voltage and hair length culture before you buy — this is a statement gift, not a polite thank-you.', + }, + { + productSlug: 'sk-ii-facial-treatment-essence', + rank: 8, + priceBand: '50-150', + badge: 'Prestige', + giftWhy: + 'The iconic essence bottle that signals “I take your glow seriously.” Silky, ritualistic, and instantly recognizable on a vanity. Ideal for skincare-curious giftees who already like serums; skip if she only wants fragrance or color. The glass alone makes unwrapping feel expensive.', + }, + ], + sections: [ + { + heading: 'Who this list is for', + body: 'Women who keep a vanity, not a junk drawer of half-used minis. Commuters who want soft hair, hosts who want a signature scent, friends who collect jars that feel special in the hand.\n\nIf you only know “she likes beauty,” start with lip mask, body mist, or a liquid blush — highest odds of delight without shade anxiety.', + }, + { + heading: 'How to choose in sixty seconds', + body: 'Routine first: skincare lover → Tatcha or SK-II; scent-first → Libre or Sol de Janeiro; color → Rare Beauty; hair drama → Olaplex or Dyson. Budget under $50? Stay on the under-$50 guide. Splurge only when you are sure she will use a pro tool.', + }, + ], + faq: [ + AMAZON_FAQ, + { + q: 'What if she already owns one of these?', + a: 'Check her vanity and shower ledge before you buy a flagship cream or the Airwrap. When she “has everything,” shift to a different texture (essence instead of cream), a new scent family, or a consumable she refills — mist, lip mask, dry shampoo.', + }, + { + q: 'Is this only for romantic partners?', + a: 'The query is “for her,” but the products are beauty tools and rituals, not relationship costumes. Shop the job (glow, scent, hair). Use our mom or wife guides when the recipient frame is more specific.', + }, + { + q: 'Why buy through Adazo instead of searching Amazon yourself?', + a: 'We curate a short list with house judgment and product pages that explain fit. You still check out on Amazon with normal shipping and returns. We may earn a referral commission on qualifying purchases.', + }, + ], + }, + { + slug: 'gifts-for-mom', + title: 'Beauty Gifts for Mom', + dek: 'Thoughtful care — soft lips, dewy cream, a quiet fragrance, and tools that make mornings easier.', + primaryQuery: 'gifts for mom', + recipientIds: ['mom'], + occasionIds: ['mothers-day', 'christmas', 'birthday'], + budgetBands: ['under-50', '50-150', '150-400'], + publishedAt: P, + updatedAt: U, + readMinutes: 6, + seasonal: { peakMonths: [4, 5, 11, 12], yearHint: 2026 }, + intro: + 'Mom gifts fail when they are cute and unused. They work when they remove friction: dry lips, dull skin days, frizzy mornings, a scent that feels like her. This list is Mother’s Day and Christmas ready, but the products are evergreen. Beauty she will not return out of politeness.', + productEntries: [ + { + productSlug: 'laneige-lip-sleeping-mask', + rank: 1, + priceBand: 'under-50', + badge: 'Mother’s Day softie', + giftWhy: + 'The gift that ends the chapstick hunt on her nightstand. A plush overnight mask feels indulgent without asking her to learn a ten-step routine. Easy to pack for travel, easy to love. If she already lives in Laneige, step up to Tatcha or a fragrance she has not tried.', + }, + { + productSlug: 'tatcha-the-dewy-skin-cream', + rank: 2, + priceBand: '50-150', + giftWhy: + 'A jar that says you want her skin to feel cushioned, not clinical. Dewy cream is tactile luxury — the kind of texture moms notice and finish. Perfect when she still cares about glow but has no interest in trend serums. Skip if she insists on oil-free only and already has a favorite gel.', + }, + { + productSlug: 'sk-ii-facial-treatment-essence', + rank: 3, + priceBand: '50-150', + badge: 'Prestige ritual', + giftWhy: + 'For the mom who enjoys a quiet morning ritual and recognizes a heritage bottle. Essence feels like a spa step without a appointment. Pair with a soft towel or a brunch reservation. Confirm she likes watery textures; cream-only devotees may prefer Tatcha alone.', + }, + { + productSlug: 'chanel-chance-eau-tendre', + rank: 4, + priceBand: '50-150', + badge: 'Scent memory', + giftWhy: + 'Soft floral that feels polished at lunch and gentle in a hug. Chance Eau Tendre is the opposite of a teenage body spray — refined, pink-adjacent, easy to wear. Ideal when mom already has heavy perfume and needs something light for daytime. Always respect scent sensitivity.', + }, + { + productSlug: 'gisou-honey-infused-hair-oil', + rank: 5, + priceBand: 'under-50', + giftWhy: + 'A few drops for shine that photographs like sunlight on a terrace. Honey oil is the hair gift for women who still want softness without a salon day. Works on dry ends and as a scent trail in a coat collar. If her hair is very fine, suggest a light hand — a little goes far.', + }, + { + productSlug: 'revlon-one-step-volumizer-plus', + rank: 6, + priceBand: 'under-50', + giftWhy: + 'The practical tool that shortens blow-dry time on busy mornings. One-step volume reads as useful love, not clutter. Great for moms who still style hair and hate juggling brush plus dryer. Confirm she is open to a heated brush; pure air-dry loyalists may prefer oil or dry shampoo instead.', + }, + { + productSlug: 'drunk-elephant-protini-polypeptide-cream', + rank: 7, + priceBand: '50-150', + giftWhy: + 'A modern moisturizer with a clean, cult following — for moms who like bright packaging and a bouncy finish. Protini feels current without being gimmicky. Gift when she already browses “clean” beauty shelves; if she only trusts heritage brands, SK-II or Tatcha may land softer.', + }, + ], + sections: [ + { + heading: 'Who this list is for', + body: 'Moms who still like a polished exit — school run, office, dinner with friends. They want softness, scent, and fewer steps, not a laboratory of actives.\n\nIf she is fragrance-sensitive, lead with lip mask, cream, or hair oil and save perfume for someone who asks for it.', + }, + { + heading: 'Mother’s Day timing', + body: 'Order early and check Amazon delivery promises. Beauty ships light, but popular creams and Chanel sell through. A handwritten card matters more than a bigger box.', + }, + ], + faq: [ + AMAZON_FAQ, + { + q: 'What if mom says she does not want anything?', + a: 'She often means she does not want clutter. Choose a consumable she will empty — lip mask, mist, cream — or one tool that saves morning time. Frame it as “for the house” if that helps her accept it.', + }, + { + q: 'Is fragrance too personal for Mother’s Day?', + a: 'It can be. If you do not know her scent family, pick unfragranced-feeling care (Laneige, Tatcha) or a light floral like Chance Eau Tendre only if she already wears soft florals. When in doubt, ask her sister or your other parent.', + }, + { + q: 'Can I gift these with other siblings?', + a: 'Yes — pool for SK-II, Chanel, or a larger prestige cream. Solo gifting works beautifully for Laneige, Gisou, and Revlon under fifty.', + }, + ], + }, + { + slug: 'gifts-for-wife', + title: 'Luxury Beauty Gifts for Your Wife', + dek: 'Upgrade prestige — La Mer, signature fragrance, lipstick she already loves, and the tool that feels like a private salon.', + primaryQuery: 'gifts for wife', + recipientIds: ['wife'], + occasionIds: ['valentines', 'birthday', 'christmas', 'wedding', 'just-because'], + budgetBands: ['50-150', '150-400', 'splurge'], + publishedAt: P, + updatedAt: U, + readMinutes: 7, + seasonal: { peakMonths: [1, 2, 11, 12], yearHint: 2026 }, + intro: + 'Wife gifts fail when they are generic “for her” baskets. They work when they feel like an upgrade she would hesitate to buy herself — the cream, the bottle, the tool. This list lives in prestige: fragrance with presence, lipstick with a name, skin that feels expensive to the touch. You still buy on Amazon; the judgment is Adazo’s.', + productEntries: [ + { + productSlug: 'la-mer-creme-de-la-mer', + rank: 1, + priceBand: '150-400', + badge: 'Icon jar', + giftWhy: + 'The cream people whisper about in elevators. La Mer is for the wife who understands legacy texture and likes a ritual that feels slow. It is a statement, not a starter moisturizer — best when she already invests in skin or has admired the jar in a magazine. Warm the cream between fingers; the gift is the ceremony as much as the formula.', + }, + { + productSlug: 'ysl-libre-eau-de-parfum', + rank: 2, + priceBand: '50-150', + badge: 'Signature scent', + giftWhy: + 'A modern classic with spine — lavender and orange blossom that reads confident at dinner. Libre works when she wants perfume that announces her entrance without shouting. If her drawer is already full of Libre, pivot to Flowerbomb or Chance for a different mood. Size up only if she empties bottles quickly.', + }, + { + productSlug: 'viktor-rolf-flowerbomb', + rank: 3, + priceBand: '50-150', + giftWhy: + 'Explosive floral sweetness for the wife who likes a trail that lasts past the coat check. Flowerbomb is romantic without being shy — Valentine’s and anniversary energy. Confirm she enjoys bold florals; if she lives in sheer citrus, this may overwhelm. One spritz demonstration on a card is a thoughtful unboxing move.', + }, + { + productSlug: 'dyson-airwrap-multi-styler', + rank: 4, + priceBand: 'splurge', + badge: 'Splurge', + giftWhy: + 'The upgrade tool for the woman who already owns “fine” and deserves “pro.” Airwrap says you noticed how long mornings take and bought her time and polish. Coordinate colorway with her bathroom if you can. This is not a joke gift — check that she wants heat styling before you commit.', + }, + { + productSlug: 'sk-ii-facial-treatment-essence', + rank: 5, + priceBand: '50-150', + giftWhy: + 'Clear bottle, cult status, daily ritual. SK-II is the prestige step she might delay buying herself because “the cream is enough.” Gift it as a pair with a soft cloth or a weekend away. Ideal for skincare-curious wives; less ideal if she only wants color and scent.', + }, + { + productSlug: 'charlotte-tilbury-pillow-talk-lipstick', + rank: 6, + priceBand: 'under-50', + badge: 'Universal glam', + giftWhy: + 'The nude-rose lipstick that photographs like a compliment. Pillow Talk is the safe glam when you know her makeup bag is considered but not experimental. It upgrades a date night without requiring a full face tutorial. If she already owns the shade, choose a matching liner or a different Charlotte finish she has eyed.', + }, + { + productSlug: 'chanel-chance-eau-tendre', + rank: 7, + priceBand: '50-150', + giftWhy: + 'Softer Chanel for daytime — the scent of polished weekends and linen. Eau Tendre is for the wife who wants quiet luxury rather than nightclub sillage. Beautiful when paired with a silk scarf or a reservation. Avoid if she dislikes florals or prefers completely unscented skin.', + }, + { + productSlug: 'tatcha-the-dewy-skin-cream', + rank: 8, + priceBand: '50-150', + giftWhy: + 'A dewy jar that feels like a trip to a calm spa in a city hotel. Less mythic than La Mer, still unmistakably gift-grade. Perfect mid-tier prestige when you want luxury without the top-shelf shock. She will use it; you will smell the soft finish on her cheek when you kiss her goodbye.', + }, + ], + sections: [ + { + heading: 'Who this list is for', + body: 'Partners who share a life and a bathroom counter. You know her taste well enough to upgrade it — not to guess a stranger’s shade.\n\nIf you are still learning her routine, start with Pillow Talk, Tatcha, or a fragrance she has sampled, and save Airwrap and La Mer for moments when certainty is high.', + }, + { + heading: 'Anniversary vs just because', + body: 'Anniversaries can carry the splurge tool or the icon jar. “Just because” thrives on lipstick, essence, and scent — small enough to surprise on a Wednesday, beautiful enough to feel intentional.', + }, + ], + faq: [ + AMAZON_FAQ, + { + q: 'What if she already owns La Mer or Libre?', + a: 'Move laterally: different fragrance family, SK-II if she is cream-only, Airwrap if tools are the gap, or Pillow Talk if color is missing. Consumable prestige still feels special when the bottle is new to her drawer.', + }, + { + q: 'Is Dyson too practical for romance?', + a: 'Not if she will use it. Romance is also fewer rushed mornings and hair she loves in photos. Pair the box with a note about why you chose it — not a receipt speech.', + }, + { + q: 'Should I buy from a boutique instead?', + a: 'You can. Adazo lists Amazon-available prestige so shipping and returns stay familiar. Confirm authenticity cues and seller ratings on the live listing either way.', + }, + ], + }, + { + slug: 'self-care-gift-edit', + title: 'Self-Care Beauty Edit', + dek: 'The ritual you gift yourself — soft lips, body mist, protein cream, hair oil, and the little luxuries that make a Sunday feel longer.', + primaryQuery: 'self care gifts', + recipientIds: ['self', 'friend'], + occasionIds: ['just-because', 'birthday', 'black-friday'], + budgetBands: ['under-50', '50-150'], + publishedAt: P, + updatedAt: U, + readMinutes: 6, + intro: + 'Self-gifting is not a consolation prize. It is how you stock a bathroom that feels like a small hotel on the Riviera. This edit is for the cart you build for yourself — or the friend who needs permission to choose softness. Nothing clinical. Nothing that promises to “fix” you. Just textures, scents, and steps that make the day feel finished.', + productEntries: [ + { + productSlug: 'sol-de-janeiro-brazilian-crush-body-mist', + rank: 1, + priceBand: 'under-50', + badge: 'Mood shift', + giftWhy: + 'Spray after the shower and the room changes temperature in your mind. Brazilian Crush is vacation in a mist — warm, sweet, and unapologetically fun. Perfect when you need a five-second ritual between meetings. Keep one in the gym bag and one on the vanity if you empty bottles the way some people empty coffee.', + }, + { + productSlug: 'laneige-lip-sleeping-mask', + rank: 2, + priceBand: 'under-50', + giftWhy: + 'The last thing you touch before sleep. A lip mask is self-care with zero learning curve — open, swipe, rest. Ideal for dry office air and long flights. Buy it for yourself when you are tired of disposable balms that disappear by noon.', + }, + { + productSlug: 'drunk-elephant-protini-polypeptide-cream', + rank: 3, + priceBand: '50-150', + badge: 'Face ritual', + giftWhy: + 'A bouncy cream for nights when you want your skin to feel fed, not stripped. Protini is the self-gift that looks current on a shelf and feels modern under makeup the next morning. Use it as the “I deserve the nice jar” moment after a hard week.', + }, + { + productSlug: 'gisou-honey-infused-hair-oil', + rank: 4, + priceBand: 'under-50', + giftWhy: + 'Honey-scented shine for ends that have seen too much heat and too little patience. A few drops turn a quick brush-through into a ritual you look forward to. Self-care that still lets you leave the house in twelve minutes — the best kind of luxury on a weekday.', + }, + { + productSlug: 'supergoop-unseen-sunscreen-spf-40', + rank: 5, + priceBand: 'under-50', + giftWhy: + 'Invisible finish sunscreen that plays well under makeup and on bare skin days. Unseen is the practical self-gift: protection that does not feel like a chore. Keep it next to your keys so the habit sticks. Always recheck the live listing for current SPF claims and size.', + }, + { + productSlug: 'living-proof-dry-shampoo', + rank: 6, + priceBand: 'under-50', + giftWhy: + 'Second-day hair that still photographs clean. Dry shampoo is permission to sleep in and still look intentional at the café. A self-care staple for travel weeks and late nights. Shake well, section, and brush through — the small ritual is half the pleasure.', + }, + { + productSlug: 'rare-beauty-soft-pinch-liquid-blush', + rank: 7, + priceBand: 'under-50', + giftWhy: + 'One pinch of color when you cannot face a full face. Liquid blush is the “I still showed up” product — soft, buildable, kind under morning light. Gift it to yourself when your makeup bag feels heavy and you want one joyful step.', + }, + { + productSlug: 'vital-proteins-collagen-peptides', + rank: 8, + priceBand: 'under-50', + giftWhy: + 'A scoop in coffee or smoothie as a quiet morning habit. Collagen peptides are the wellness-adjacent self-gift for people who like routine more than drama. Not a miracle — just a small ritual that makes the first mug feel intentional. Confirm dietary preferences before sharing this pick with friends.', + }, + ], + sections: [ + { + heading: 'How to build a self-care night', + body: 'Shower. Mist. Oil on ends. Cream on face. Lip mask last. Phone face-down. That is the edit. Add sunscreen to the morning side of the same shelf so the story continues at 8 a.m.', + }, + { + heading: 'Gifting this list to a friend', + body: 'Bundle three under-$50 pieces in tissue — mist, lip mask, blush — and write “permission to keep these.” Self-care language lands better as invitation than instruction.', + }, + ], + faq: [ + AMAZON_FAQ, + { + q: 'Is self-gifting wasteful?', + a: 'Not when you choose pieces you will empty. Start with mist, lip mask, or dry shampoo — high use, low regret. Leave the biggest jars for when you know your texture preferences.', + }, + { + q: 'What if I already own half of this?', + a: 'Fill the gaps only. Self-care lists are menus, not mandates. Rotate scent families and textures so the shelf stays interesting.', + }, + { + q: 'Can men use this edit?', + a: 'Anyone who wants soft lips, clean-feeling hair days, and a body mist can shop these jobs. The framing is ritual, not gender. Skip shade-specific blush if it is not your lane.', + }, + ], + }, + { + slug: 'gifts-under-50', + title: 'Beauty Gifts Under $50', + dek: 'True under-fifty wins — serums, mascara, lip mask, body mist, and the heated brush that still fits the budget.', + primaryQuery: 'gifts under 50', + recipientIds: ['her', 'friend', 'coworker', 'teen-girl', 'self'], + occasionIds: ['christmas', 'birthday', 'just-because', 'black-friday'], + budgetBands: ['under-50'], + publishedAt: P, + updatedAt: U, + readMinutes: 5, + intro: + 'Under fifty is not code for forgettable. It is where beauty gifts get used up and reordered — the cleanser, the mascara, the mist, the lip mask. Every pick on this list sits near or under fifty on typical Amazon pricing; prices move, so confirm the live listing. No handbags, no jewelry, no jokes. Just pieces that feel generous in the hand.', + productEntries: [ + { + productSlug: 'the-ordinary-niacinamide-10-zinc-1', + rank: 1, + priceBand: 'under-50', + badge: 'Cult serum', + giftWhy: + 'A small bottle with outsized cult energy. Niacinamide serum is the smart stocking stuffer for skincare-curious friends who like a simple step. Lightweight, easy to wrap, and it looks intentional next to a handwritten note. Pair with a soft washcloth for a complete mini ritual under twenty dollars all-in on a good day.', + }, + { + productSlug: 'elf-holy-hydration-makeup-melting-cleansing-balm', + rank: 2, + priceBand: 'under-50', + giftWhy: + 'The balm that turns makeup removal into something almost luxurious on a budget. Melting texture feels expensive for the price and photographs well in unboxing. Perfect for teens, coworkers, and anyone who still sleeps in mascara sometimes. Include a clean muslin cloth if you want to look extra thoughtful.', + }, + { + productSlug: 'maybelline-lash-sensational-sky-high-mascara', + rank: 3, + priceBand: 'under-50', + badge: 'Lash lift look', + giftWhy: + 'Length that reads expensive in photos without a salon lash appointment. Sky High is the safe color gift — one size, easy return path if shade of black is wrong, and it lives in every “clean girl” drawer. Stocking-sized. Hostess-gift sized. “I saw this and thought of you” sized.', + }, + { + productSlug: 'eos-shea-better-body-lotion', + rank: 4, + priceBand: 'under-50', + giftWhy: + 'Soft skin gift without the prestige price. Shea lotion is the practical pleasure people actually empty. Choose a scent profile she already likes if you can; unscented-leaning households may prefer fragrance-free care instead. Bundle two and it still stays under fifty on many days.', + }, + { + productSlug: 'cerave-hydrating-facial-cleanser', + rank: 5, + priceBand: 'under-50', + giftWhy: + 'The cleanser dermatologist-adjacent bathrooms keep rebuying. Hydrating formula is a kind gift for winter skin and sensitive routines — never flashy, always useful. Best when you know she likes a cream cleanser; foam purists may want a different texture. Pair with a soft towel for a complete under-thirty moment.', + }, + { + productSlug: 'the-inkey-list-retinol-eye-cream', + rank: 6, + priceBand: 'under-50', + giftWhy: + 'An approachable eye cream for the friend who is curious about retinol but not ready for a vanity full of actives. Small tube, clear brand story, easy to wrap. Remind her to go slow and use sunscreen in the morning — Adazo does not do clinical promises, only sensible routine pairing.', + }, + { + productSlug: 'laneige-lip-sleeping-mask', + rank: 7, + priceBand: 'under-50', + badge: 'Always works', + giftWhy: + 'The under-fifty gift that feels like more. Lip sleeping mask is texture theater — glossy jar, soft scent, nightly use. Ideal for Secret Santa, desk gifts, and “just because” texts that arrive as packages. If prices creep over fifty, wait for a deal or swap to Ordinary or e.l.f.', + }, + { + productSlug: 'sol-de-janeiro-brazilian-crush-body-mist', + rank: 8, + priceBand: 'under-50', + giftWhy: + 'Fragrance joy without full perfume commitment. Body mist is the playful under-fifty scent gift — loud enough to delight, light enough to risk. Perfect for friends who love warm gourmand notes. Skip for scent-free offices or highly sensitive skin households.', + }, + { + productSlug: 'rare-beauty-soft-pinch-liquid-blush', + rank: 9, + priceBand: 'under-50', + giftWhy: + 'A modern blush that earns compliments in natural light. Soft Pinch is the color gift under fifty that still feels current and considered. Shade risk is real — if you do not know her undertone, include a note that Amazon returns are straightforward for beauty when unopened per seller policy.', + }, + { + productSlug: 'revlon-one-step-volumizer-plus', + rank: 10, + priceBand: 'under-50', + badge: 'Tool under $50', + giftWhy: + 'The rare heated tool that often sits near the under-fifty line and replaces a drawer of half-working dryers. Volume and speed for people who style hair on weekdays. Confirm live price — if it spikes, drop to mist plus mascara and keep the promise of the query. Always check voltage and reviews for the exact model.', + }, + ], + sections: [ + { + heading: 'How to gift under $50 without looking cheap', + body: 'Bundle with intention: cleanser + lip mask, or mascara + blush, wrapped as a “morning kit.” One excellent object beats three forgettable minis. Avoid mystery multi-packs with no brand story.', + }, + { + heading: 'Price honesty', + body: 'Amazon prices move. Adazo hints are typical ranges. Always confirm the live listing. If an “under $50” pick is suddenly $59, swap to the next item — the query is the promise.', + }, + ], + faq: [ + AMAZON_FAQ, + { + q: 'What is the single best sub-$25 beauty gift?', + a: 'Laneige lip mask when it is on a typical street price, or The Ordinary niacinamide for skincare-curious friends. Both hit daily use and feel intentional.', + }, + { + q: 'Can I stay under $50 for a serious fragrance lover?', + a: 'Full-size prestige perfume often sits above fifty. Choose body mist for scent joy under the cap, or save toward Libre or Chance on our other guides when the budget opens.', + }, + { + q: 'Is Secret Santa different?', + a: 'Yes — favor universal items (lip mask, mascara, cleansing balm). Avoid highly personal shade matches unless you know her undertone and makeup style.', + }, + ], + }, +] + +const bySlug = new Map(giftGuides.map((g) => [g.slug, g])) + +export function getGiftGuide(slug: string): GiftGuide | undefined { + return bySlug.get(slug) +} + +export function guidesForProduct(productSlug: string): GiftGuide[] { + return giftGuides.filter((g) => + g.productEntries.some((e) => e.productSlug === productSlug), + ) +} + +export function guidesForOccasion(occasion: GiftOccasionId): GiftGuide[] { + return giftGuides.filter((g) => g.occasionIds.includes(occasion)) +} + +export function guidesForRecipient(recipient: GiftRecipientId): GiftGuide[] { + return giftGuides.filter((g) => g.recipientIds.includes(recipient)) +} + +export function featuredGiftGuides(limit = 5): GiftGuide[] { + const month = new Date().getMonth() + 1 + const scored = giftGuides.map((g) => { + const seasonalBoost = g.seasonal?.peakMonths.includes(month) ? 100 : 0 + const updated = g.updatedAt || g.publishedAt + return { g, score: seasonalBoost + (updated >= U ? 10 : 0) } + }) + scored.sort((a, b) => b.score - a.score) + return scored.slice(0, limit).map((x) => x.g) +} + +export const BUDGET_LABELS: Record = { + 'under-50': 'Under $50', + '50-150': '$50–150', + '150-400': '$150–400', + splurge: 'Splurge', +} + +export const RECIPIENT_LABELS: Record = { + her: 'For her', + wife: 'For your wife', + mom: 'For mom', + girlfriend: 'For girlfriend', + 'teen-girl': 'Teen', + coworker: 'Coworker', + host: 'Host', + couple: 'Couple', + self: 'Self', + friend: 'Friend', +} diff --git a/src/data/types.ts b/src/data/types.ts index ff0ed1e..b99d2ec 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -130,3 +130,70 @@ export interface ProductEnrichment { /** Optional research note sites (names only, not full articles) */ researchNotes?: string[] } + +/** Gift guide listicles — avatar-locked beauty edits from the Adazo shelf */ +export type GiftRecipientId = + | 'her' + | 'wife' + | 'mom' + | 'girlfriend' + | 'teen-girl' + | 'coworker' + | 'host' + | 'couple' + | 'self' + | 'friend' + +export type GiftOccasionId = + | 'christmas' + | 'mothers-day' + | 'valentines' + | 'birthday' + | 'graduation' + | 'wedding' + | 'housewarming' + | 'black-friday' + | 'just-because' + +export type GiftBudgetBand = 'under-50' | '50-150' | '150-400' | 'splurge' + +export interface GiftGuideProductEntry { + productSlug: string + rank?: number + /** Why this works as a gift — not a PDP paste */ + giftWhy: string + priceBand?: GiftBudgetBand + badge?: string +} + +export interface GiftGuideSection { + heading: string + body: string +} + +export interface GiftGuideFaq { + q: string + a: string +} + +export interface GiftGuide { + slug: string + title: string + dek: string + primaryQuery: string + recipientIds: GiftRecipientId[] + occasionIds: GiftOccasionId[] + budgetBands: GiftBudgetBand[] + productEntries: GiftGuideProductEntry[] + intro: string + sections: GiftGuideSection[] + faq: GiftGuideFaq[] + heroImage?: string + publishedAt: string + updatedAt: string + seasonal?: { + peakMonths: number[] + yearHint?: number + } + readMinutes?: number +} diff --git a/src/lib/seo.ts b/src/lib/seo.ts index a28b689..57f3b37 100644 --- a/src/lib/seo.ts +++ b/src/lib/seo.ts @@ -170,3 +170,24 @@ export function faqPageJsonLd( url: absoluteUrl(pagePath), } } + +export function itemListJsonLd(opts: { + name: string + path: string + items: { name: string; path: string; position: number }[] +}) { + return { + '@context': 'https://schema.org', + '@type': 'ItemList', + name: opts.name, + url: absoluteUrl(opts.path), + itemListOrder: 'https://schema.org/ItemListOrderAscending', + numberOfItems: opts.items.length, + itemListElement: opts.items.map((item) => ({ + '@type': 'ListItem', + position: item.position, + url: absoluteUrl(item.path), + name: item.name, + })), + } +} diff --git a/src/lib/seoData.ts b/src/lib/seoData.ts index e28d65a..35315fe 100644 --- a/src/lib/seoData.ts +++ b/src/lib/seoData.ts @@ -9,11 +9,18 @@ import { CATEGORY_LABELS, categoryLabel, filterProducts, + getProduct, productGalleryThumbs, productImageChain, } from '../data/catalog' import { getCategoryHero } from '../data/categoryHeroes' -import type { Category, Product, ProductEnrichment } from '../data/types' +import { giftGuides } from '../data/giftGuides' +import type { + Category, + GiftGuide, + Product, + ProductEnrichment, +} from '../data/types' import type { VibeProfile } from '../data/vibes' import { isQuietPlaceholder } from './productImages' import { @@ -23,6 +30,7 @@ import { breadcrumbJsonLd, clipMeta, faqPageJsonLd, + itemListJsonLd, pageTitle, productJsonLd, type PageSeo, @@ -232,6 +240,72 @@ export function watchSeo(): PageSeo { } } +export function giftsHubSeo(): PageSeo { + return { + title: 'Beauty gift guides', + description: clipMeta( + `Gifts for her, mom, wife, self-care, and under $50 — ${giftGuides.length} curated Adazo beauty guides. Chosen here; buy on Amazon.`, + ), + path: '/gifts', + image: '/brand/social.png', + jsonLd: [ + breadcrumbJsonLd([ + { name: 'Home', path: '/' }, + { name: 'Gifts', path: '/gifts' }, + ]), + itemListJsonLd({ + name: 'Adazo beauty gift guides', + path: '/gifts', + items: giftGuides.map((g, i) => ({ + name: g.title, + path: `/gifts/${g.slug}`, + position: i + 1, + })), + }), + ], + } +} + +export function giftGuideSeo(g: GiftGuide): PageSeo { + const path = `/gifts/${g.slug}` + const products = g.productEntries + .map((e) => getProduct(e.productSlug)) + .filter(Boolean) as Product[] + + const jsonLd: Record[] = [ + breadcrumbJsonLd([ + { name: 'Home', path: '/' }, + { name: 'Gifts', path: '/gifts' }, + { name: g.title, path }, + ]), + itemListJsonLd({ + name: g.title, + path, + items: products.map((p, i) => ({ + name: p.name, + path: `/product/${p.slug}`, + position: g.productEntries[i]?.rank ?? i + 1, + })), + }), + ] + + if (g.faq.length) { + const faqLd = faqPageJsonLd(g.faq, path) + if (faqLd) jsonLd.push(faqLd) + } + + return { + title: g.title, + description: clipMeta( + `${g.dek} ${g.productEntries.length} beauty picks on Adazo. Updated ${g.updatedAt.slice(0, 4)}.`, + ), + path, + type: 'article', + image: g.heroImage || products[0]?.images?.[0] || '/brand/social.png', + jsonLd, + } +} + /** Rendered head values for one route (pageTitle/clipMeta applied, absolute URLs). */ export type RouteMeta = { title: string diff --git a/src/pages/GiftGuide.tsx b/src/pages/GiftGuide.tsx new file mode 100644 index 0000000..5642769 --- /dev/null +++ b/src/pages/GiftGuide.tsx @@ -0,0 +1,325 @@ +import { useState } from 'react' +import { Link, useParams } from 'react-router-dom' +import { + ArrowLeft, + ChevronDown, + Clock3, + ExternalLink, + Gift, +} from 'lucide-react' +import { formatMoney, getProduct, primaryImage } from '../data/catalog' +import { + BUDGET_LABELS, + getGiftGuide, + giftGuides, +} from '../data/giftGuides' +import { ProductCard } from '../components/ProductCard' +import { Seo } from '../components/Seo' +import { affiliateUrl } from '../lib/amazon' +import { trackAmazonClick } from '../lib/analytics' +import { giftGuideSeo } from '../lib/seoData' + +function Paragraphs({ text }: { text: string }) { + return ( + <> + {text.split(/\n\n+/).map((para, i) => ( +

    + {para} +

    + ))} + + ) +} + +function FaqItem({ q, a }: { q: string; a: string }) { + const [open, setOpen] = useState(false) + return ( +
    + + {open ? ( +

    + {a} +

    + ) : null} +
    + ) +} + +export function GiftGuidePage() { + const { slug } = useParams() + const guide = slug ? getGiftGuide(slug) : undefined + + if (!guide) { + return ( +
    + +

    Not found

    + + Back to gifts + +
    + ) + } + + const entries = guide.productEntries + .map((e) => { + const product = getProduct(e.productSlug) + return product ? { entry: e, product } : null + }) + .filter(Boolean) as { + entry: (typeof guide.productEntries)[0] + product: NonNullable> + }[] + + const related = giftGuides.filter((g) => g.slug !== guide.slug).slice(0, 3) + + const heroSrc = + guide.heroImage || + (entries[0] + ? primaryImage(entries[0].product) || entries[0].product.images?.[0] + : undefined) + + const updated = new Date(guide.updatedAt + 'T12:00:00').toLocaleDateString( + 'en-US', + { month: 'long', year: 'numeric' }, + ) + + return ( +
    + + +
    +
    + + Home + + / + + Gifts + + / + + {guide.title} + +
    +
    + + {heroSrc ? ( +
    + +
    +
    + ) : null} + +
    + + All gift guides + + +

    + Gift guide +

    +

    + {guide.title} +

    +

    + {guide.dek} +

    +

    + + + {guide.readMinutes ?? 6} min read + + · + Updated {updated} + · + {entries.length} picks +

    + +
    + {guide.budgetBands.map((b) => ( + + {BUDGET_LABELS[b] || b} + + ))} +
    + +
    + +
    + +
    +

    + The list +

    +
      + {entries.map(({ entry, product }, i) => { + const shopUrl = affiliateUrl({ + asin: product.asin, + searchKeywords: product.searchKeywords, + name: product.name, + }) + return ( +
    1. +
      +
      + +
      +
      +
      +
      +

      + #{entry.rank ?? i + 1} + {entry.badge ? ` · ${entry.badge}` : ''} + {entry.priceBand + ? ` · ${BUDGET_LABELS[entry.priceBand] || entry.priceBand}` + : ''} +

      +

      + + {product.name} + +

      +

      + ~{formatMoney(product.priceHint)} on Amazon · varies +

      +
      +
      +

      + {entry.giftWhy} +

      + +
      +
      +
    2. + ) + })} +
    +
    + + {guide.sections.map((sec) => ( +
    +

    + {sec.heading} +

    + +
    + ))} + + {guide.faq.length > 0 && ( +
    +

    + Questions before you buy +

    +
    + {guide.faq.map((f) => ( + + ))} +
    +
    + )} +
    + + {related.length > 0 && ( +
    +

    More gift edits

    +

    + Related guides +

    +
    + {related.map((g) => ( + +

    + {g.primaryQuery} +

    +

    + {g.title} +

    +

    + {g.dek} +

    + + ))} +
    +
    + )} +
    + ) +} diff --git a/src/pages/Gifts.tsx b/src/pages/Gifts.tsx new file mode 100644 index 0000000..b84ebe4 --- /dev/null +++ b/src/pages/Gifts.tsx @@ -0,0 +1,120 @@ +import { Link } from 'react-router-dom' +import { ArrowRight, Gift } from 'lucide-react' +import { + BUDGET_LABELS, + RECIPIENT_LABELS, + giftGuides, +} from '../data/giftGuides' +import { getProduct, primaryImage } from '../data/catalog' +import { Seo } from '../components/Seo' +import { giftsHubSeo } from '../lib/seoData' + +function guideCardImage(slug: string, heroImage?: string): string | undefined { + if (heroImage) return heroImage + const guide = giftGuides.find((g) => g.slug === slug) + const first = guide?.productEntries[0]?.productSlug + if (!first) return undefined + const p = getProduct(first) + return p ? primaryImage(p) || p.images?.[0] : undefined +} + +export function GiftsHubPage() { + return ( +
    + +
    +
    +
    +
    +

    + Gift edit +

    +

    + Gifts worth unwrapping +

    +

    + Beauty listicles locked to Adazo’s shelf — for her, mom, wife, + self-care, and under $50. Chosen here. Bought on Amazon. +

    +
    + {giftGuides.map((g) => ( + + {g.primaryQuery} + + ))} +
    +
    +
    + +
    +
    +
    +

    Wave one

    +

    + Start with the recipient +

    +
    +
    +
    + {giftGuides.map((g) => { + const recipients = g.recipientIds + .map((id) => RECIPIENT_LABELS[id] || id) + .filter(Boolean) + .slice(0, 2) + const budgets = g.budgetBands + .map((b) => BUDGET_LABELS[b]) + .filter(Boolean) + .slice(0, 2) + const img = guideCardImage(g.slug, g.heroImage) + return ( + +
    + {img ? ( + + ) : ( +
    + )} +
    +
    +
    +

    + {recipients.join(' · ') || 'Gift guide'} + {g.readMinutes ? ` · ${g.readMinutes} min` : ''} +

    +

    + {g.title} +

    +

    + {g.dek} +

    + {budgets.length > 0 && ( +

    + {budgets.join(' · ')} · {g.productEntries.length} picks +

    + )} + + Open guide + +
    + + ) + })} +
    +
    +
    + ) +} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 08efd62..7b1d531 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -4,11 +4,14 @@ import { bigTicketProducts, bsrLeaders, CATEGORY_OPTIONS, + getProduct, limitedProducts, limitedTimeCopy, + primaryImage, shopProducts, } from '../data/catalog' import { HEROES } from '../data/categoryHeroes' +import { featuredGiftGuides } from '../data/giftGuides' import { VIBE_LIST, vibePath } from '../data/vibes' import { ProductGrid } from '../components/ProductGrid' import { LookbookHero } from '../components/LookbookHero' @@ -219,6 +222,71 @@ export function Home() { )} +
    +
    +
    +
    +

    Gift edit

    +

    + Gifts worth unwrapping +

    +

    + For her, mom, wife, self-care — and true under $50. Listicles from + the Adazo shelf only. +

    +
    + + All gift guides + +
    +
    + {featuredGiftGuides(5).map((g) => { + const firstSlug = g.productEntries[0]?.productSlug + const first = firstSlug ? getProduct(firstSlug) : undefined + const img = + g.heroImage || + (first ? primaryImage(first) || first.images?.[0] : undefined) + return ( + + {img ? ( +
    + +
    + ) : null} +
    +

    + {g.primaryQuery} +

    +

    + {g.title} +

    +

    + {g.dek} +

    + + Open guide + +
    + + ) + })} +
    +
    +
    +
    diff --git a/src/pages/Product.tsx b/src/pages/Product.tsx index a23475e..6d6e099 100644 --- a/src/pages/Product.tsx +++ b/src/pages/Product.tsx @@ -25,6 +25,7 @@ import { ProductEnrichmentSections } from '../components/ProductEnrichment' import { StarRating } from '../components/StarRating' import { Seo } from '../components/Seo' import { getProductEnrichment } from '../data/productEnrichments' +import { guidesForProduct } from '../data/giftGuides' import { affiliateUrl } from '../lib/amazon' import { trackAmazonClick, trackViewItem } from '../lib/analytics' import { isQuietPlaceholder } from '../lib/productImages' @@ -102,6 +103,7 @@ export function ProductPage() { // Local alias so nested handlers keep the narrowed product type const p = product const enrichment = getProductEnrichment(p.slug) + const onGiftGuides = guidesForProduct(p.slug) const shopUrl = affiliateUrl({ asin: p.asin, searchKeywords: p.searchKeywords, @@ -319,6 +321,25 @@ export function ProductPage() { ))} + {onGiftGuides.length > 0 && ( +
    +

    + Also on gift guides +

    +
    + {onGiftGuides.map((g) => ( + + {g.title} + + ))} +
    +
    + )} +