A product link pasted into a chat shows the product, a search engine can read the catalog, and a preview deployment is never indexed.
What the preview serves today. Every page's <head> is four lines: charset, viewport, <title> and the stylesheet. There is no <meta name="description">, no <link rel="canonical">, no OpenGraph or Twitter tags, no /robots.txt and no /sitemap.xml — and nothing tells a crawler that preview of pull #37 is not the shop. A shop's product pages are its shop window; right now a shared link is a bare URL and the previews are as indexable as production.
Done looks like
[ ] layout() in src/html.ts takes an optional description and an optional canonical path, and renders <meta name="description">, <link rel="canonical" href="<FORGE_APP_URL><path>">, og:type, og:title, og:description, og:url, og:site_name and twitter:card — every value escaped, the description trimmed to about 160 characters at a word boundary.
[ ] Product pages describe themselves from the product's blurb, category pages from the category, the home page and the search page from fixed sentences; a page that has nothing to say renders no description rather than an empty one.
[ ] Nothing private is ever described or canonicalised: /cart, /checkout, /pay/, /orders, /gift-cards and everything under /admin carry <meta name="robots" content="noindex, nofollow"> and no canonical.
[ ] FORGE_APP_ENV !== "production" makes every page noindex, nofollow and /robots.txt a blanket Disallow: / — a preview is never the shop.
[ ] GET /robots.txt is text/plain, names the sitemap in production, and disallows the private paths above.
[ ] GET /sitemap.xml lists the home page, the active categories and the active products with escaped URLs, capped (say 5,000 entries), content-type: application/xml, and a short cache-control — it answers in milliseconds from one query.
[ ] Both new routes are in src/routes.ts, both keep the security floor, and neither reveals an inactive product.
[ ] Tests in src/server.test.ts: the tags on a product page, no canonical and noindex on /cart and /admin, robots.txt in both environments, sitemap.xml listing an active product and not an inactive one, and an apostrophe in a blurb escaped in the meta tag.
[ ] The README says what the shop tells crawlers and that previews say noindex.
Files I expect to change:src/html.ts, src/routes.ts, src/shop.ts, src/pages/storefront.ts, src/pages/cart.ts, src/pages/checkout.ts, src/pages/admin.ts, src/server.test.ts, README.md.
Risks. A description is user-typed text going into an attribute — it goes through escape like everything else, and a product blurb with a quote in it belongs in the test. FORGE_APP_URL is the only origin to trust for a canonical; never the Host header, or the canonical becomes whatever a caller sends. Adding a page to the sitemap that needs a token would leak order numbers: the list is products and categories, nothing else. No image tag until #38 gives products a photograph — og:image pointing at nothing is worse than none.
Whose brief fits:mason — one concept added to the shell, and the routes table extended in the pattern it already has.
A product link pasted into a chat shows the product, a search engine can read the catalog, and a preview deployment is never indexed.
What the preview serves today. Every page's
<head>is four lines: charset, viewport,<title>and the stylesheet. There is no<meta name="description">, no<link rel="canonical">, no OpenGraph or Twitter tags, no/robots.txtand no/sitemap.xml— and nothing tells a crawler thatpreview of pull #37is not the shop. A shop's product pages are its shop window; right now a shared link is a bare URL and the previews are as indexable as production.Done looks like
layout()insrc/html.tstakes an optional description and an optional canonical path, and renders<meta name="description">,<link rel="canonical" href="<FORGE_APP_URL><path>">,og:type,og:title,og:description,og:url,og:site_nameandtwitter:card— every value escaped, the description trimmed to about 160 characters at a word boundary./cart,/checkout,/pay/,/orders,/gift-cardsand everything under/admincarry<meta name="robots" content="noindex, nofollow">and no canonical.FORGE_APP_ENV !== "production"makes every pagenoindex, nofollowand/robots.txta blanketDisallow: /— a preview is never the shop.GET /robots.txtistext/plain, names the sitemap in production, and disallows the private paths above.GET /sitemap.xmllists the home page, the active categories and the active products with escaped URLs, capped (say 5,000 entries),content-type: application/xml, and a shortcache-control— it answers in milliseconds from one query.src/routes.ts, both keep the security floor, and neither reveals an inactive product.src/server.test.ts: the tags on a product page, no canonical andnoindexon/cartand/admin,robots.txtin both environments,sitemap.xmllisting an active product and not an inactive one, and an apostrophe in a blurb escaped in the meta tag.noindex.Files I expect to change:
src/html.ts,src/routes.ts,src/shop.ts,src/pages/storefront.ts,src/pages/cart.ts,src/pages/checkout.ts,src/pages/admin.ts,src/server.test.ts,README.md.Risks. A description is user-typed text going into an attribute — it goes through
escapelike everything else, and a product blurb with a quote in it belongs in the test.FORGE_APP_URLis the only origin to trust for a canonical; never theHostheader, or the canonical becomes whatever a caller sends. Adding a page to the sitemap that needs a token would leak order numbers: the list is products and categories, nothing else. No image tag until #38 gives products a photograph —og:imagepointing at nothing is worse than none.Whose brief fits: mason — one concept added to the shell, and the routes table extended in the pattern it already has.