Marketing site for the NumeraSheets shop: a home page listing every spreadsheet template, one page per product, and a free calculator. It never takes a payment — buying happens on the shop, and every outbound link is built in one place so it stays trackable.
Next.js 16 (App Router) · React 19 · Tailwind CSS v4 · TypeScript. Type is Archivo + IBM Plex Mono; the palette is the shop's own ink navy and gold.
npm install
npm run devLive on numerasheets.com, served by Cloudflare Pages (the site left
Vercel on 2026-09-04 — see MIGRACAO-VERCEL-CLOUDFLARE.md in the workspace).
| Host | What happens | Why |
|---|---|---|
numerasheets.com |
serves the site | the canonical host |
www.numerasheets.com |
301 → apex, path and query preserved | measured 2026-09-09; the redirect rule lives in the Cloudflare dashboard, not in this repo — see below |
numerasheets-site.pages.dev |
200 + X-Robots-Tag: noindex |
the Pages host must keep working for deploy previews, so it is not redirected — only kept out of the index. Rule lives in public/_headers |
The canonical tag is what keeps this safe, and it needs no host logic:
SITE_URL in lib/products.ts is absolute, so every page declares
https://numerasheets.com/... no matter which host served it. Verified by
fetching www and pages.dev directly — both return the apex canonical.
Static export has no redirects() and no headers() — there is no server
to run them. Everything that used to live in next.config.ts had to move:
headers became public/_headers, which Cloudflare Pages reads.
Redirects cannot follow the same path: Cloudflare's _redirects file is
path-based only and explicitly does not support domain-level redirects.
A www → apex redirect has to be a Redirect Rule / Bulk Redirect in the
Cloudflare dashboard, not a file in this repo. Do not try to express it in
_redirects; it will silently never match — which is why looking for it here
and finding nothing does not mean it is missing.
It is in place. Measured on 2026-09-09:
https://www.numerasheets.com/templates/debt-payoff-tracker?utm_source=pinterest
-> 301 https://numerasheets.com/templates/debt-payoff-tracker?utm_source=pinterest
Permanent, and it preserves path and query — which is what campaign links
need, since they carry ?utm_source=. Test it from the command line, not the
browser: host-level routing is invisible to a normal page load.
This paragraph used to read "until that rule exists,
wwwis a duplicate host held in check by the canonical tag alone." The rule was created and the warning stayed — an orphaned note, the same failure mode already recorded in.claude/rules/etsy-titulo.md. A note that justifies the absence of something has to be re-read when that something arrives, or the next review spends its time chasing a problem that is already solved.
- Product data lives in
lib/products.ts.listingUrl()is the only place a shop URL is built andproductUrl()the only place a page URL is, so theBUY_ONswitch can move the whole catalogue between shops in one line. - The home page's product count, the price ceiling and the
keywordslist are all derived from that array. Numbers written by hand drift; these have. - The bundle is in
productsbut is not a template. It carrieskind: "bundle", which keeps it out oftemplates— the list the catalogue counts and renders as cards. It still gets a page, a sitemap entry and a keyword, because those come fromproducts. Without the flag the heading would read "Thirteen templates" while listing twelve, counting the eleven inside the bundle a second time. It also renders as its own offer block rather than a card, because a set sells on the saving it names. - Listing images in
public/shots/are renders of the real workbooks, resized from the artwork used on the shop listings themselves. They are generated byProducts/_shared/export_card_shots.pyin the workspace repo, never by hand. app/opengraph-image.pngmust be captured from the deployed production URL, never fromnext dev— the dev server injects a floating indicator.