diff --git a/apps/caramel-app/src/app/(marketing)/coupons/[store]/page.tsx b/apps/caramel-app/src/app/(marketing)/coupons/[store]/page.tsx index df150e50..ceeaec21 100644 --- a/apps/caramel-app/src/app/(marketing)/coupons/[store]/page.tsx +++ b/apps/caramel-app/src/app/(marketing)/coupons/[store]/page.tsx @@ -91,8 +91,6 @@ export async function generateMetadata({ // rather than merging into it, so the inherited og:image has to be restated // here or these pages share links with no preview image at all. const banner = `${baseUrl}/caramel_banner.png` - const title = `${base} Coupons & Promo Codes | Caramel` - const description = `Find ${base} coupon codes, promo codes, and discounts — refreshed as new codes are found.` // Canonical always points at the NORMALIZED base-domain URL: this route // serves the same content for /coupons/www.nike.com, /coupons/shop.nike.com // and /coupons/nike.com, so every variant must canonicalize to ONE URL or @@ -104,6 +102,22 @@ export async function generateMetadata({ // "no codes right now" pages in the index are soft-404 bloat. cache() // makes this share one catalog read with the page body. const { total } = await fetchStoreCoupons(storeParam) + // Search Console (90 days to 2026-09-11): store pages sit at positions + // 25–55 for " promo code" queries. The title/description use only + // data the page already renders — the base domain and the live `total` + // (the same "N active codes" the prose below states) — never an invented + // display name or date. The zero-coupon page keeps the generic title: it + // is noindexed above and must not advertise codes it does not have. + const count = total.toLocaleString('en-US') + const codeWord = total === 1 ? 'code' : 'codes' + const title = + total > 0 + ? `${base} coupons & promo codes — ${count} active ${codeWord} | Caramel` + : `${base} Coupons & Promo Codes | Caramel` + const description = + total > 0 + ? `Caramel lists ${count} active coupon ${codeWord} for ${base} — promo codes and discounts refreshed as new codes are found and dead ones retired.` + : `Find ${base} coupon codes, promo codes, and discounts — refreshed as new codes are found.` return { title, diff --git a/apps/caramel-app/src/app/(marketing)/coupons/page.tsx b/apps/caramel-app/src/app/(marketing)/coupons/page.tsx index 0cdde37a..0dc6023c 100644 --- a/apps/caramel-app/src/app/(marketing)/coupons/page.tsx +++ b/apps/caramel-app/src/app/(marketing)/coupons/page.tsx @@ -17,7 +17,7 @@ const PAGE_SIZE = 5 const title = 'Coupon Codes & Promo Codes for Top Stores | Caramel' const description = - 'Browse coupon codes and promo codes for your favorite stores. Caramel finds and applies the best deals automatically at checkout.' + 'Browse Caramel coupon codes and promo codes for 4,000+ online stores. The free Caramel extension finds and applies the best codes automatically at checkout.' const canonicalUrl = 'https://grabcaramel.com/coupons' const banner = `${BASE_URL}/caramel_banner.png` diff --git a/apps/caramel-app/src/app/(marketing)/pricing/page.tsx b/apps/caramel-app/src/app/(marketing)/pricing/page.tsx index 0e8f29a0..29e86698 100644 --- a/apps/caramel-app/src/app/(marketing)/pricing/page.tsx +++ b/apps/caramel-app/src/app/(marketing)/pricing/page.tsx @@ -2,9 +2,16 @@ import { BASE_URL } from '@/lib/env.client' import type { Metadata } from 'next' import PricingPageClient from './PricingPageClient' -const title = 'Caramel Pricing | Free Forever Coupon Extension' +// Search Console (90 days to 2026-09-11): 513 impressions, 2 clicks, position +// 7.9 — a 0.4% CTR against a ~3% benchmark for that position. Every query +// that reaches this page is a brand query (`caramel extension`, `caramel +// coupon`) on which the home page already ranks #1, so a snippet that repeats +// the home pitch only competes with it. The title/description therefore state +// the ONE thing this page answers — "Is Caramel free?" — so it wins the +// price-intent variant instead of duplicating the home snippet. +const title = 'Is Caramel Free? Yes — Free Forever & Open Source | Caramel' const description = - 'Caramel is 100% free, open source, and privacy-first. No hidden fees, no data selling, no credit card required. Save money with the trusted alternative to Honey.' + 'Yes, Caramel is free forever: no premium tier, no hidden fees, no credit card. An open-source coupon extension for 4,000+ stores that never sells your data.' const canonicalUrl = 'https://grabcaramel.com/pricing' const base = BASE_URL const banner = `${base}/caramel_banner.png` diff --git a/apps/caramel-app/src/app/(marketing)/supported-stores/page.tsx b/apps/caramel-app/src/app/(marketing)/supported-stores/page.tsx index 832684f5..afcbdc01 100644 --- a/apps/caramel-app/src/app/(marketing)/supported-stores/page.tsx +++ b/apps/caramel-app/src/app/(marketing)/supported-stores/page.tsx @@ -13,9 +13,13 @@ import type { Metadata } from 'next' // never prerendered at build time (same pattern as app/sitemap.ts). export const dynamic = 'force-dynamic' -const title = 'Caramel | Supported Stores' +// Search Console (90 days to 2026-09-11): 175 impressions at position 37 with +// a title that said nothing a searcher types. The count is the reconciled +// "4,000+" figure (floor 4,314 store pages in sitemap.xml on 2026-09-11, +// rounded DOWN — the same constant heroStats.ts / PricingSection.tsx quote). +const title = '4,000+ Supported Stores — Caramel Coupon Extension' const description = - 'Explore the stores supported by Caramel and start saving with our coupon extension.' + 'Search the 4,000+ online stores where the free Caramel coupon extension finds and applies promo codes at checkout, and see the stores added most recently.' const canonicalUrl = 'https://grabcaramel.com/supported-stores' const base = BASE_URL const banner = `${base}/caramel_banner.png` diff --git a/apps/caramel-app/src/app/layout.tsx b/apps/caramel-app/src/app/layout.tsx index 70c92894..c4cd3370 100644 --- a/apps/caramel-app/src/app/layout.tsx +++ b/apps/caramel-app/src/app/layout.tsx @@ -13,18 +13,25 @@ import type { Metadata, Viewport } from 'next' import { ReactNode } from 'react' import Providers from './providers' -// Kept at 157 chars so Google/social cards never truncate it mid-claim; the -// openGraph copy below is the same string on purpose. +// Kept under 160 chars so Google/social cards never truncate it mid-claim; the +// openGraph copy below is the same string on purpose. Title + description are +// query-driven (Search Console, 90 days to 2026-09-11): the home page owns the +// "honey alternative" cluster at position ~2.3 (615 impr on `honey alternative +// extension` alone), so "Honey Alternative" stays in the title; "coupon +// extension" is added for the generic cluster (`coupon extension` 117 impr at +// position 10.2, `coupon extension safari` 63 at 9.7) and "app" + the browser +// list for `caramel app` (67 impr, position 6.0) / `caramel chrome` (28, 4.3). +// "Caramel" stays first: brand queries sit at position 1.3. const description = - "Open-source, privacy-first coupon extension that finds and applies the best codes at checkout — without selling your data or hijacking creators' commissions." + 'Free, open-source coupon extension and app for Chrome, Firefox, Edge & Safari — the privacy-first Honey alternative that applies the best codes at checkout.' export const metadata: Metadata = { - title: 'Caramel | The Trusted Alternative To Honey For Finding Coupons', + title: 'Caramel: Free Coupon Extension & Trusted Honey Alternative', description, metadataBase: new URL(BASE_URL), openGraph: { type: 'website', - title: 'Caramel | The Trusted Alternative To Honey For Finding Coupons', + title: 'Caramel: Free Coupon Extension & Trusted Honey Alternative', description, url: '/', images: ['/caramel_banner.png'], diff --git a/apps/caramel-app/src/components/FaqSection.tsx b/apps/caramel-app/src/components/FaqSection.tsx index 800d6e86..7373376c 100644 --- a/apps/caramel-app/src/components/FaqSection.tsx +++ b/apps/caramel-app/src/components/FaqSection.tsx @@ -15,6 +15,17 @@ import { FaChevronDown } from 'react-icons/fa' // + cart-signals.js payload (title/meta/up to 6 item names, no payment data); // sign-in token lives in browser extension storage (popup.js/coupon-runner.js). // - browsers: the four live store listings in src/lib/brandLinks.ts. +// - Safari answer: the App Store listing (SAFARI_APP_STORE_URL) fetched +// 2026-09-12 states iPhone "Requires iOS 16.4 or later", iPad, Mac +// "Requires macOS 13.0 or later" (versions deliberately not quoted here). +// - SimplyCodes answer: simplycodes.com fetched 2026-09-12 (200) links no +// source code / GitHub anywhere — the same absence test claim-inventory +// applies to Honey, Capital One Shopping, Rakuten and Coupert. Nothing +// else about SimplyCodes is asserted. +// - Same-as-Honey answer: exists because ~400 impressions/90 days land on +// the home page from people searching for Honey itself (`honey coupons` +// 187 impr, `honey coupon finder` 48, `honey code finder` 38 — positions +// 9–11); it answers them truthfully instead of pretending to be Honey. // - numbers: 107,827 visible codes (GET /api/coupons?limit=1 → total) / // 4,314 stores with live codes (sitemap /coupons/ URLs) from PROD on // 2026-09-11, rounded DOWN. Never round up, and never derive the catalog @@ -32,14 +43,26 @@ const faqItems = [ question: 'What data does the Caramel extension collect?', answer: "The extension never sells or shares your personal information, and it contains no ads and no third-party trackers. To do its job it talks to Caramel's own servers: as you browse, it asks whether Caramel has codes for the current site's domain so the toolbar badge can show a count; when you reach checkout on a supported store it fetches those codes, sends the page and cart context (page title and item names — never payment details) so the right category of codes is chosen, and reports whether a code worked (linked to your account only if you're signed in) so rankings stay accurate for everyone. Your settings and optional sign-in are kept in your browser's extension storage.", }, + { + question: 'Is Caramel the same as Honey?', + answer: "No. Caramel is an independent coupon extension, not affiliated with Honey or PayPal. Like Honey, it looks up coupon codes for the store you are checking out on and applies the best one; unlike Honey, its extension and web app are open source, it contains no affiliate code, and it leaves creators' affiliate links untouched.", + }, { question: 'How is Caramel different from Honey?', answer: "Honey has been publicly documented replacing creators' affiliate links with its own, and its code is closed source, so its behavior can't be independently audited. Caramel is the opposite by design: fully open source under the AGPL-3.0 license, it never touches affiliate links, and it is free with no premium tier.", }, + { + question: 'How is Caramel different from SimplyCodes?', + answer: "SimplyCodes does not publish its source code, so what its extension does with your data and with affiliate links can only be taken on trust. Caramel's extension and web app are published under the AGPL-3.0 license for anyone to audit, it contains no affiliate code at all, and it works without an account.", + }, { question: 'Which browsers does Caramel support?', answer: 'Caramel is available for Chrome on the Chrome Web Store, for Firefox on Firefox Add-ons, for Microsoft Edge on Edge Add-ons, and for Safari through the App Store.', }, + { + question: 'Is there a Caramel coupon extension for Safari?', + answer: 'Yes. Caramel ships as a Safari extension through the App Store for Mac (macOS) and for iPhone and iPad (iOS), and it is free there like every other version — the same open-source coupon extension as the Chrome, Firefox and Edge builds.', + }, { question: 'How many coupon codes does Caramel have?', answer: "Caramel's catalog holds over 100,000 coupon codes across more than 4,000 online stores, and it is refreshed continuously as new codes are found and dead ones are retired.", diff --git a/apps/caramel-app/tests/unit/coupons-store-page.test.ts b/apps/caramel-app/tests/unit/coupons-store-page.test.ts index b1741d03..13e93ee1 100644 --- a/apps/caramel-app/tests/unit/coupons-store-page.test.ts +++ b/apps/caramel-app/tests/unit/coupons-store-page.test.ts @@ -220,6 +220,15 @@ describe('StoreCouponsPage generateMetadata — canonical normalization + thin-p expect(metadata.openGraph?.url).toBe(`${BASE_URL}/coupons/example.com`) // A store WITH coupons is indexable — no robots override. expect(metadata.robots).toBeUndefined() + // Title/description carry the live code count (the only data the + // page has besides the base domain) — singular for exactly one code. + expect(metadata.title).toBe( + 'example.com coupons & promo codes — 1 active code | Caramel', + ) + expect(metadata.description).toBe( + 'Caramel lists 1 active coupon code for example.com — promo codes and discounts refreshed as new codes are found and dead ones retired.', + ) + expect(metadata.openGraph?.title).toBe(metadata.title) }) it('noindexes (but still follows) a store page with zero visible coupons', async () => { @@ -234,5 +243,13 @@ describe('StoreCouponsPage generateMetadata — canonical normalization + thin-p }) expect(metadata.robots).toEqual({ index: false, follow: true }) + // The empty state keeps the generic title: it must never advertise a + // code count it does not have. + expect(metadata.title).toBe( + 'example.com Coupons & Promo Codes | Caramel', + ) + expect(metadata.description).toBe( + 'Find example.com coupon codes, promo codes, and discounts — refreshed as new codes are found.', + ) }) })