Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ with the React pages); edit SEO copy there, never in the Worker.
- `npm run deploy` = build + `wrangler deploy`.
- Custom domain is configured in `wrangler.jsonc`; do not casually change worker/domain names.

## Gift guides (SEO listicles)

Avatar-locked bamboo gift guides at `/gifts` and `/gifts/:slug`.

- **Skill:** `~/.grok/skills/affiliate-gifting-seo` (`/affiliate-gifting-seo`)
- **Data:** `src/data/giftGuides.ts` — catalog-backed `productEntries` only
- **Pages:** `Gifts.tsx` hub, `GiftGuide.tsx` listicle
- **SEO:** ItemList + FAQPage via `giftsHubSeo` / `giftGuideSeo`; sitemap + routeMeta
- **Heroes:** reuse brand category / vibe / flatlay art (`heroImage` paths) until dedicated `/brand/gifts/` assets exist
- **Voice:** calm natural living; prefer role (host, couple, new home) over forced gender
- **Rules:** original prose; `giftWhy` ≠ PDP paste; footer disclosure only; no Conbal unless integrated

Wave-1 slugs: `housewarming-gifts`, `gifts-for-the-host`, `kitchen-gifts`, `eco-friendly-gifts`, `christmas-home-gifts`.

## Product page enrichment

PDPs should be **destinations** (judgment + depth), not thin Amazon hops.
Expand Down
36 changes: 36 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
<changefreq>daily</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://ibamboo.com/gifts</loc>
<lastmod>2026-08-02</lastmod>
<changefreq>weekly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://ibamboo.com/quiz</loc>
<lastmod>2026-08-02</lastmod>
Expand Down Expand Up @@ -1290,4 +1296,34 @@
<changefreq>weekly</changefreq>
<priority>0.75</priority>
</url>
<url>
<loc>https://ibamboo.com/gifts/housewarming-gifts</loc>
<lastmod>2026-08-02</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://ibamboo.com/gifts/gifts-for-the-host</loc>
<lastmod>2026-08-02</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://ibamboo.com/gifts/kitchen-gifts</loc>
<lastmod>2026-08-02</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://ibamboo.com/gifts/eco-friendly-gifts</loc>
<lastmod>2026-08-02</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
<url>
<loc>https://ibamboo.com/gifts/christmas-home-gifts</loc>
<lastmod>2026-08-02</lastmod>
<changefreq>monthly</changefreq>
<priority>0.85</priority>
</url>
</urlset>
28 changes: 27 additions & 1 deletion scripts/generate-sitemap.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,29 @@ 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: '/why', changefreq: 'monthly', priority: '0.7' },
]
Expand Down Expand Up @@ -85,6 +103,14 @@ for (const slug of products) {
})
}

for (const slug of giftSlugs) {
urls.push({
loc: `/gifts/${slug}`,
changefreq: 'monthly',
priority: '0.85',
})
}

function escapeXml(s) {
return s
.replace(/&/g, '&amp;')
Expand Down Expand Up @@ -113,7 +139,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} gifts, ${vibes.length} vibes, ${categories.length} categories)`,
)

/**
Expand Down
8 changes: 8 additions & 0 deletions scripts/route-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -16,6 +17,8 @@ import {
} from '../src/lib/seo'
import {
finalizeRouteMeta,
giftGuideSeo,
giftsHubSeo,
homeSeo,
productSeo,
quizSeo,
Expand Down Expand Up @@ -46,6 +49,11 @@ export function buildRouteMeta(): RouteMetaFile {
}
routes['/quiz'] = finalizeRouteMeta(quizSeo())
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))
Expand Down
4 changes: 4 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { ProductPage } from './pages/Product'
import { Why } from './pages/Why'
import { Quiz } from './pages/Quiz'
import { VibePage } from './pages/Vibe'
import { GiftsHubPage } from './pages/Gifts'
import { GiftGuidePage } from './pages/GiftGuide'

const Admin = lazy(() =>
import('./pages/Admin').then((m) => ({ default: m.Admin })),
Expand Down Expand Up @@ -39,6 +41,8 @@ export default function App() {
<Route index element={<Home />} />
<Route path="shop" element={<Shop />} />
<Route path="product/:slug" element={<ProductPage />} />
<Route path="gifts" element={<GiftsHubPage />} />
<Route path="gifts/:slug" element={<GiftGuidePage />} />
<Route path="why" element={<Why />} />
<Route path="quiz" element={<Quiz />} />
<Route path="vibe/:vibeId" element={<VibePage />} />
Expand Down
7 changes: 7 additions & 0 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type NavItem =
*/
const nav: NavItem[] = [
{ kind: 'link', to: '/quiz', label: 'Vibe check' },
{ kind: 'link', to: '/gifts', label: 'Gifts' },
{ kind: 'shop', mode: 'cat', cat: 'kitchen', label: 'Kitchen' },
{ kind: 'shop', mode: 'cat', cat: 'cutting-boards', label: 'Boards' },
{ kind: 'shop', mode: 'cat', cat: 'dining', label: 'Table' },
Expand All @@ -36,6 +37,7 @@ function useShopNavActive() {
return (item: NavItem): boolean => {
if (item.kind === 'link') {
if (item.to === '/quiz') return pathname.startsWith('/quiz')
if (item.to === '/gifts') return pathname.startsWith('/gifts')
return pathname === item.to
}
if (!onShop) return false
Expand Down Expand Up @@ -238,6 +240,11 @@ export function Layout() {
Our story
</Link>
</li>
<li>
<Link to="/gifts" className="hover:text-leaf">
Gift guides
</Link>
</li>
<li>
<Link to="/quiz" className="hover:text-leaf">
Vibe check
Expand Down
Loading
Loading