Task Tracker

Click a task to mark it complete. Progress saves in your browser automatically.

0 of 0 tasks complete

Bear Grips Dev Hub

Task board for shops.beargrips.com. Last updated: March 30, 2026.

SEO is done — all tasks verified Mar 30

SSR, sitemap (6,482 URLs), robots.txt, schema, GSC tag, SSL, favicons, meta tags, canonicals, OG tags, Twitter cards, rate limiting, blog route, and 6,172 static pages — all live and confirmed working. Two items remain below.

Remaining Tasks

DEV

Security Hardening

Most security is done (Stripe verification, Helmet, rate limiting). Remaining: enable CSP, review auth cookies, remove hardcoded title from source index.html.

NEW

LLM Integration API

Staff-level API for Claude to create products, manage vendor shops, write SEO/blog content, and handle DFY VIP onboarding at scale. Additive endpoints only.

What's Left

#TaskStatusPriority
1Security Hardening — Stripe webhook verification, Helmet, and rate limiting are all done. Remaining:
• Enable Content-Security-Policy in helmet config (currently contentSecurityPolicy: false)
• Review auth cookie settings (httpOnly, secure, sameSite flags)
• Add CSRF protection if session cookies are used
• Remove hardcoded <title>Bear Grips Pro Shop</title> from source index.html (line 35) — fixed in dist but will revert on next build
PARTIALMEDIUM
2LLM Integration API — Staff-level API endpoints for Claude backend access. See LLM API section for full spec.NOT STARTEDNEW

SSR / Pre-Rendering for shops.beargrips.com

Priority: CRITICAL — If Google can't render the JavaScript, none of our SEO work matters.

VERIFIED WORKING (Mar 27) — Good job!

Prerender server running via PM2 on port 3033. Nginx bot detection active. Googlebot gets full rendered HTML including product listings, prices, images, and categories. Tested with: curl -A "Googlebot" http://127.0.0.1:3033/render?url=https://shops.beargrips.com — returns complete page content. Also verified vendor shop pages (Gallery Barre) render full storefront with all products.

Why this matters

shops.beargrips.com is JavaScript-rendered. Google's crawler may see a blank page if it can't execute JS fast enough. A client with 500 CSR pages had only 3 indexed by Google. We need server-rendered HTML.

Solution: Self-Hosted Prerender (FREE — use this)

Do NOT sign up for Prerender.io

Prerender.io costs $49/mo minimum. We're using the free open-source version instead — same engine, self-hosted on our existing DO server. Zero cost.

Step 1: Install the open-source prerender server

This is the same rendering engine behind Prerender.io, just self-hosted. github.com/prerender/prerender (6.5K stars, actively maintained)

# On the DO server (167.172.23.233) mkdir -p /opt/prerender && cd /opt/prerender npm init -y npm install prerender # Create server.js cat <<'EOF' > server.js const prerender = require('prerender'); const server = prerender({ chromeLocation: '/usr/bin/google-chrome', port: 3033 }); server.use(prerender.browserForceRestart()); server.use(prerender.removeScriptTags()); server.use(prerender.httpHeaders()); server.start(); EOF # Install Chrome headless (if not already installed) curl -fsSL https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb -o chrome.deb sudo dpkg -i chrome.deb || sudo apt-get -f install -y rm chrome.deb # Test it node server.js & curl http://localhost:3033/render?url=https://shops.beargrips.com # Should return full HTML with real content

Step 2: Keep it running with PM2

npm install -g pm2 cd /opt/prerender pm2 start server.js --name prerender pm2 save pm2 startup

Step 3: Add Express middleware to detect bots

In the shops Express app, add middleware that checks the User-Agent. If it's a crawler (Googlebot, Bingbot, etc.), proxy the request to the prerender server. Human visitors get the normal React SPA.

npm install prerender-node // In your Express app (before routes) app.use(require('prerender-node').set('prerenderServiceUrl', 'http://localhost:3033/'));

That's it. The prerender-node middleware automatically detects bot user-agents and routes them to the local prerender server. Human visitors are unaffected.

Step 4: Add Nginx caching (optional but recommended)

Cache pre-rendered pages so the prerender server doesn't re-render on every bot visit:

# In nginx.conf for shops.beargrips.com proxy_cache_path /var/cache/nginx/prerender levels=1:2 keys_zone=prerender_cache:10m max_size=500m inactive=7d; # Inside the server block, before location / {} set $prerender 0; if ($http_user_agent ~* "Googlebot|Bingbot|baiduspider|yandex|Slurp|DuckDuckBot|facebookexternalhit|twitterbot|LinkedInBot") { set $prerender 1; } if ($prerender = 1) { rewrite .* /render?url=https://$host$request_uri break; proxy_pass http://localhost:3033; proxy_cache prerender_cache; proxy_cache_valid 200 7d; }

Alternative options (if self-hosted prerender doesn't fit)

Option B: Server-Side Rendering (SSR) — best long-term

Option C: Static Site Generation (SSG) — for pages that don't change often

How to verify it works

curl -A "Googlebot" https://shops.beargrips.com

This should return full HTML with real content — NOT an empty <div id="app"></div>. If you see an empty div, Google sees the same thing = no indexing.

Sitemap & Robots.txt

AUDIT RESULT (Mar 27)

Sitemap: Exists but only has 17 static URLs. The site has ~260-760 live pages right now. That's ~2% coverage — Google can't find 98% of the site.
Robots.txt: Returns 404. Not deployed.

The Gap: 17 URLs vs. ~6,900+ Total Pages

Pages that exist RIGHT NOW (not in sitemap)

Page TypeURL PatternCountIn Sitemap?
Static/marketing pages/, /catalog, /help, /resources, etc.17YES
Missing static pages/pricing, /how-it-works, /features~3-5NO
Vendor shop pages (35-40 vendors)/proshops/{slug}~40NO
Product pages (all vendors combined)/proshops/{slug}/product/{id}~200-700NO
Total live pages today~260-7602% covered

Pages built and ready to deploy (not on site yet)

ContentPage CountStatus
Product catalog (category + brand + product pages)85Built, needs deploy
Free design tools + hub page13Built, needs deploy
Niche landing pages (gym, church, sports, etc.)117Built, needs deploy
pSEO pages (resources, comparisons, glossary, guides)2,163Built, on Cloudflare Pages
Blog niche content (tool×niche, service×niche)3,808Built, needs deploy
Deploy backlog total~6,186

Bottom line: 17 out of ~6,950 pages are in the sitemap

The sitemap covers 0.2% of the total site. Even counting only pages that are live RIGHT NOW, it's ~2%. Every vendor shop page and every product page those 35-40 vendors published is invisible to Google. The dynamic sitemap (Part 2 below) is the fix — it auto-generates from the database so every new vendor and product is automatically discoverable.

Part 1: Static Sitemap (What You Have Now)

The current sitemap.xml has 17 static URLs. Keep these, but rename the file to sitemap-static.xml and add it to a sitemap index (see below).

Part 2: Dynamic Sitemap for Vendor Shops + Products (CRITICAL)

Every live vendor shop and every published product is a page that should be in the sitemap. This must be generated dynamically from the database.

This is the biggest SEO gap right now

When a gym owner publishes their shop with 20 products, that's 21 new pages Google should know about (1 shop + 20 products). Without a dynamic sitemap, Google has to discover these pages by accident. With 50+ vendors, that could be 1,000+ pages Google can't find.

Dynamic Sitemap Architecture

Create an API endpoint that generates the sitemap XML on the fly from the database:

// GET /sitemap-shops.xml — auto-generated from DB // Query: all LIVE vendor shops + all PUBLISHED products // Output: standard XML sitemap // Example output: <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <!-- Vendor shop pages --> <url> <loc>https://shops.beargrips.com/proshops/gallerybarre</loc> <lastmod>2026-03-20</lastmod> <changefreq>weekly</changefreq> <priority>0.7</priority> </url> <!-- Product pages --> <url> <loc>https://shops.beargrips.com/proshops/gallerybarre/product/abc123</loc> <lastmod>2026-03-18</lastmod> <changefreq>weekly</changefreq> <priority>0.6</priority> </url> ... </urlset>

Rules:

Sitemap Index (ties everything together)

Create a sitemap index at /sitemap.xml that references all sub-sitemaps:

<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://shops.beargrips.com/sitemap-static.xml</loc> </sitemap> <sitemap> <loc>https://shops.beargrips.com/sitemap-shops.xml</loc> </sitemap> <!-- Add more as content grows: sitemap-blog.xml, sitemap-catalog.xml --> </sitemapindex>

Part 3: Static Sitemap Pages

Rename the current sitemap.xml to sitemap-static.xml and keep it for the static pages:

<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <url> <loc>https://shops.beargrips.com/</loc> <changefreq>weekly</changefreq> <priority>1.0</priority> </url> <url> <loc>https://shops.beargrips.com/how-it-works</loc> <changefreq>monthly</changefreq> <priority>0.8</priority> </url> <url> <loc>https://shops.beargrips.com/catalog</loc> <changefreq>weekly</changefreq> <priority>0.8</priority> </url> <url> <loc>https://shops.beargrips.com/pricing</loc> <changefreq>monthly</changefreq> <priority>0.9</priority> </url> <url> <loc>https://shops.beargrips.com/features</loc> <changefreq>monthly</changefreq> <priority>0.8</priority> </url> <url> <loc>https://shops.beargrips.com/help</loc> <changefreq>monthly</changefreq> <priority>0.5</priority> </url> <url> <loc>https://shops.beargrips.com/resources</loc> <changefreq>monthly</changefreq> <priority>0.5</priority> </url> <url> <loc>https://shops.beargrips.com/terms</loc> <changefreq>yearly</changefreq> <priority>0.3</priority> </url> <url> <loc>https://shops.beargrips.com/privacy</loc> <changefreq>yearly</changefreq> <priority>0.3</priority> </url> <url> <loc>https://shops.beargrips.com/refund-policy</loc> <changefreq>yearly</changefreq> <priority>0.3</priority> </url> <url> <loc>https://shops.beargrips.com/cancellation-policy</loc> <changefreq>yearly</changefreq> <priority>0.3</priority> </url> </urlset>

Notes:

Robots.txt

DEPLOYED (Mar 27) — robots.txt is live

File deployed to /var/www/dist/robots.txt. Blocks private routes, references sitemap. Verified accessible at shops.beargrips.com/robots.txt.

Place at: https://shops.beargrips.com/robots.txt

Create the file with this content:

User-agent: * Allow: / # Block private routes Disallow: /dashboard/ Disallow: /admin/ Disallow: /api/ Disallow: /auth/ Disallow: /account/ Disallow: /vendor/ Disallow: /settings/ # Sitemaps Sitemap: https://shops.beargrips.com/sitemap.xml

Meta Tags & Canonical URLs

This is NOT about writing copy for 17 pages

This task has two parts: (1) Static copy for a handful of marketing pages — provided below, just drop it in. (2) A dynamic system that auto-generates meta tags for vendor shops, product pages, and catalog pages from the data already in the database. Most of the site's pages are dynamic — they can't have hand-written meta tags.

Canonical URLs — URGENT FIX

GSC Issue Found Mar 29, 2026

Google is flagging affiliate referral URLs like /?ref=wh3qdqgUeeMCb&sub_id=undefined as duplicate pages. Canonical tags fix this immediately. Add to EVERY public page.

Add to the <head> of every public page:

<link rel="canonical" href="https://shops.beargrips.com/CURRENT-PAGE-PATH" />

React Implementation (copy-paste into your app)

If using React Router, add this hook or component that sets the canonical tag dynamically on every route change. Drop it into your root App component or layout:

// useCanonical.js — drop in src/hooks/ import { useEffect } from 'react'; import { useLocation } from 'react-router-dom'; export function useCanonical() { const { pathname } = useLocation(); useEffect(() => { // Strip query params and hash — canonical is always the clean path const url = `https://shops.beargrips.com${pathname}`; let link = document.querySelector('link[rel="canonical"]'); if (!link) { link = document.createElement('link'); link.setAttribute('rel', 'canonical'); document.head.appendChild(link); } link.setAttribute('href', url); }, [pathname]); } // Then in App.js or your root layout: // import { useCanonical } from './hooks/useCanonical'; // function App() { // useCanonical(); // return (...); // }

If using SSR/prerender: Make sure the canonical tag is in the server-rendered HTML too, not just added by JS. Google prefers to see it in the initial HTML response.

This strips all query params (?ref=, ?sub_id=, etc.) so Google treats every URL variant as the same canonical page. This is the #1 quickest SEO win right now.

Tag Structure (Same for Every Page)

Every public page needs these tags in <head>. The values change per page, but the structure is always the same:

<title>{PAGE_TITLE} | Bear Grips Pro Shops</title> <meta name="description" content="{PAGE_DESCRIPTION}" /> <meta name="robots" content="index, follow" /> <link rel="canonical" href="https://shops.beargrips.com/{CURRENT_PATH}" /> <!-- Open Graph --> <meta property="og:title" content="{PAGE_TITLE}" /> <meta property="og:description" content="{PAGE_DESCRIPTION}" /> <meta property="og:image" content="{PAGE_IMAGE}" /> <meta property="og:url" content="https://shops.beargrips.com/{CURRENT_PATH}" /> <meta property="og:type" content="website" /> <meta property="og:site_name" content="Bear Grips Pro Shops" /> <!-- Twitter Card --> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="{PAGE_TITLE}" /> <meta name="twitter:description" content="{PAGE_DESCRIPTION}" /> <meta name="twitter:image" content="{PAGE_IMAGE}" />

Part 1: Static Pages — Copy Provided (Just Paste These In)

PageTitleDescription
/ (Homepage) Custom Branded Apparel for Your Business — No Inventory, Free Shipping Launch your own branded merch shop in minutes. Premium Nike, Bella+Canvas, Champion apparel printed on demand. No inventory, no minimums, free USA shipping. Plans from $0/mo.
/catalog Product Catalog — 100+ Premium Apparel Products Browse our full catalog of custom apparel: tees, hoodies, joggers, tanks, hats, leggings and more. Nike, Bella+Canvas, Champion, Sport-Tek brands. All-inclusive pricing includes printing and free shipping.
/help Help Center — Getting Started, FAQs & Support Get help with your Bear Grips Pro Shop. Setup guides, design tips, pricing info, shipping details, and answers to common questions. Contact us at proshops@beargrips.com.
/resources Resources — Design Services, Tools & Guides Free design tools, professional logo design, file formatting services, and step-by-step guides to build and grow your branded apparel shop.
/resources/design-services Professional Design Services — Logos, Mockups & More Professional design services for your custom apparel shop. Logo design starting at $99, file formatting for $29, mockup creation, and full shop setup.
/resources/pro-shop-onboarding Pro Shop Setup Guide — Launch Your Shop Step by Step Step-by-step onboarding guide to set up your branded apparel shop. Upload your logo, choose products, set pricing, and start selling — no inventory needed.
/resources/5-day-launch-kit 5-Day Launch Kit — Go From Signup to Selling Launch your custom apparel shop in 5 days. Day-by-day action plan covering design, product selection, pricing, and your first sale.
/resources/design-services/file-formatting File Formatting Service — $29 Print-Ready Conversion Get your logo converted to print-ready format for $29. We optimize resolution, transparency, and color profiles so your designs look perfect on every product.
/resources/design-services/add-designs-to-products Add Designs to Products — We Apply Your Logo for You Send us your design and we'll apply it to your selected products with professional mockups. Front and back placement, color variant selection included.
/resources/design-services/pro-shop-layout Pro Shop Layout Service — Professional Shop Design We design your entire shop layout: header, logo placement, product categories, sections — a polished storefront ready for customers.
/resources/design-services/graphic-designer-logo Custom Logo Design — $99 Professional Logo for Your Brand Professional custom logo design for $99. Our designers create a unique logo optimized for apparel printing. Includes revisions and print-ready files.
/resources/design-services/add-additional-logo-design Additional Logo Design — Add Another Design to Your Shop Add a new logo or design variation to your shop. Perfect for seasonal collections, special events, or expanding your product line with fresh designs.
/resources/design-services/monthly-shop-request Monthly Shop Request — Done-For-You VIP Service Send us one design per month and we handle everything: product selection, mockups, pricing, shop layout. Your shop stays fresh with zero effort on your end.
/terms-of-service Terms of Service Terms of service for Bear Grips Pro Shops. Read our terms governing use of the custom apparel platform, vendor accounts, and print-on-demand services.
/privacy-policy Privacy Policy Privacy policy for Bear Grips Pro Shops. How we collect, use, and protect your personal information on our custom apparel platform.
/refund-policy Refund Policy Refund policy for Bear Grips Pro Shops. Information about returns, exchanges, and refunds for custom printed apparel orders.
/cancellation-policy Cancellation Policy Cancellation policy for Bear Grips Pro Shops subscription plans. How to cancel, pause, or change your plan at any time.

OG Image for static pages: Use the Bear Grips logo or a branded social preview image. Same image for all static pages is fine. Recommended size: 1200x630px.

Part 2: Dynamic Meta Tags — Vendor Shops & Product Pages

This is where the real SEO value is

Every vendor shop page and every product page is a potential Google result. These pages need unique meta tags generated automatically from the database. You already have the data — vendor name, product name, description, price, mockup image. Just wire it into the meta tag system.

Vendor Shop Pages: /proshops/{slug}

<title>{Vendor Name} — Custom Apparel Shop | Bear Grips Pro Shops</title> <meta name="description" content="Shop custom apparel from {Vendor Name}. Premium branded tees, hoodies, tanks and more. Printed in the USA with free shipping on every order." /> <meta property="og:image" content="{vendor_logo_url}" /> <link rel="canonical" href="https://shops.beargrips.com/proshops/{slug}" />

If vendor has a custom description: use it as the meta description instead of the template above.

Product Pages: /proshops/{slug}/product/{id}

<title>{Product Name} — {Vendor Name} | Bear Grips Pro Shops</title> <meta name="description" content="{Product description, first 155 chars}. Custom printed for {Vendor Name}. Free USA shipping." /> <meta property="og:image" content="{product_mockup_image_url}" /> <meta property="og:type" content="product" /> <meta property="product:price:amount" content="{price}" /> <meta property="product:price:currency" content="USD" /> <link rel="canonical" href="https://shops.beargrips.com/proshops/{slug}/product/{id}" />

Catalog Product Pages (if separate from vendor pages)

<title>{Product Name} by {Brand} — Custom Apparel | Bear Grips Pro Shops</title> <meta name="description" content="Customize the {Product Name} by {Brand} with your logo. Starting at ${vip_price}. Printed in the USA, free shipping. Available in multiple colors and sizes." />

Draft/Unpublished Pages

Any vendor shop or product page that is NOT live/published must have:

<meta name="robots" content="noindex, nofollow" />

This prevents Google from indexing half-built or inactive shops.

Framework Implementation

Use react-helmet or react-helmet-async in the React app. Set the meta tags in each page component using the data already available (vendor name, product name, description, price, image URL).

Schema Markup

VERIFIED WORKING (Mar 27) — Homepage schema confirmed

SoftwareApplication JSON-LD with all 3 pricing tiers (Free, Self-Service VIP, Done-For-You VIP) found in prerendered HTML. Correct schema type, correct pricing, correct provider info.

This JSON-LD is already on the shops.beargrips.com homepage. Reference copy below for future pages:

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "SoftwareApplication", "name": "Bear Grips Pro Shops", "applicationCategory": "BusinessApplication", "operatingSystem": "Web", "description": "Print-on-demand custom apparel platform for gyms and fitness businesses. Get a free branded merch shop with no inventory, no minimums, and free USA shipping.", "url": "https://shops.beargrips.com", "offers": [ { "@type": "Offer", "name": "Free Plan", "price": "0", "priceCurrency": "USD", "description": "3 live products, full catalog access, higher base prices" }, { "@type": "Offer", "name": "Self-Service VIP", "price": "59", "priceCurrency": "USD", "description": "200 live products, lowest base prices, full control" }, { "@type": "Offer", "name": "Done-For-You VIP", "price": "109", "priceCurrency": "USD", "description": "250 live products, 1 design/month set up on top 15 best-sellers + 5 vendor's choice" } ], "provider": { "@type": "Organization", "name": "Bear Grips", "url": "https://www.beargrips.com", "logo": "https://www.beargrips.com/cdn/shop/files/Bear_Grips_Logo_on_transparent_400x.png" } } </script>

Notes:

Page Speed & SSL

Page Speed — Target 90+

Test at: pagespeed.web.dev

Common Fixes

SSL / HTTPS

Mobile Responsiveness

Google Search Console — Verification Tag

VERIFIED WORKING (Mar 27) — Tag found in prerendered HTML

GSC verification tag confirmed present. KJ can now verify the property in Google Search Console and submit the sitemap.

The meta tag is already on the shops.beargrips.com homepage. Reference copy below:

KJ: You can now verify in GSC

The tag is live. Go to Google Search Console → Add Property → shops.beargrips.com → Verify. Then submit the sitemap.

<meta name="google-site-verification" content="ixzjWs_TY1tAtRtN00_K4YBug8Z7uPOJQQCclLfIIFI" />

Let KJ know when the tag is live

Once you've added it and deployed, message KJ. He'll verify the property and submit the sitemap from his end. You don't need GSC access.

Vendor Shop SEO

Priority: HIGH — Every vendor shop and product page is a free SEO page for Bear Grips. Right now Google can see the meta tags but the page body is empty.

Why this is high priority

When a gym owner publishes products, those pages should rank for "[Gym Name] + apparel/tee/hoodie" in Google. This worked automatically on Shopify. On our custom platform, we need to make sure Google can see the full page content — not just meta tags.

What's Working (Good job!)

Issue 0: Hide Printify Image URLs (Security)

Competitive risk — Printify URLs are exposed

Product mockup images currently point directly to images-api.printify.com. Anyone viewing page source can see we use Printify, and can find the exact blueprint/variant IDs. We need to proxy these through our own domain.

Current (exposed):

<meta property="og:image" content="https://images-api.printify.com/mockup/69abb695.../comfort-soft-hoodie.jpg?camera_label=front">

What it should look like:

<meta property="og:image" content="https://shops.beargrips.com/product-images/69abb695.../comfort-soft-hoodie.jpg">

Fix: Nginx reverse proxy (quickest approach)

Add this to the nginx config for shops.beargrips.com:

location /product-images/ { proxy_pass https://images-api.printify.com/mockup/; proxy_set_header Host images-api.printify.com; proxy_set_header Accept-Encoding ""; proxy_ssl_server_name on; proxy_cache_valid 200 7d; proxy_cache_use_stale error timeout updating; expires 7d; add_header Cache-Control "public, max-age=604800"; add_header X-Content-Type-Options "nosniff"; }

Then update the app code to rewrite all Printify mockup URLs:

Also check for leaks in:

Issue 1: Empty Body Content

The <body> is empty for crawlers

Currently the pre-rendered HTML only has meta tags in <head> and a noscript redirect in <body>. Google weighs body content much heavier than meta tags. We need to render the actual product info in the body.

Current product page body (what Google sees):

<body> <noscript> <p>If you are not redirected automatically, <a href="...">click here</a>.</p> </noscript> </body>

What it should look like (use the same data already in the meta tags):

<body> <h1>Women's Favorite Tee - Bella+Canvas</h1> <p>By <a href="/proshops/gallerybarre">Gallery Barre</a></p> <p>$29.99</p> <img src="https://images-api.printify.com/mockup/.../front.jpg" alt="Women's Favorite Tee - Bella+Canvas - Gallery Barre" /> <p>Elevate your wardrobe with a versatile essential designed for both style and performance...</p> <noscript> <p>If you are not redirected automatically, <a href="...">click here</a>.</p> </noscript> <script>window.location.href = "...";</script> </body>

Same approach for shop pages:

<body> <h1>Gallery Barre</h1> <p>Shop custom apparel from Gallery Barre.</p> <img src="[vendor logo url]" alt="Gallery Barre logo" /> <!-- List published products --> <ul> <li><a href="/proshops/gallerybarre/product/abc123">Women's Favorite Tee - $29.99</a></li> <li><a href="/proshops/gallerybarre/product/def456">Performance Tank - $24.99</a></li> </ul> <noscript>...</noscript> <script>window.location.href = "...";</script> </body>

Key points:

Issue 2: Product Schema Markup

Add JSON-LD Product schema to every product page. This enables rich results in Google (price, availability, image, reviews right in search results).

Add this to the <head> of product pages (alongside existing meta tags):

<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Product", "name": "Women's Favorite Tee - Bella+Canvas", "description": "Elevate your wardrobe with a versatile essential...", "image": "https://images-api.printify.com/mockup/.../front.jpg", "brand": { "@type": "Brand", "name": "Gallery Barre" }, "offers": { "@type": "Offer", "url": "https://shops.beargrips.com/proshops/gallerybarre/product/699f8abd...", "priceCurrency": "USD", "price": "29.99", "availability": "https://schema.org/InStock", "seller": { "@type": "Organization", "name": "Gallery Barre" } } } </script>

Dynamic fields to populate from existing data:

Schema FieldSource
nameProduct title (same as og:title minus store name)
descriptionProduct description (same as meta description)
imageProduct mockup URL (same as og:image)
brand.nameVendor store name
priceRetail price (same as product:price:amount)
urlFull product page URL
availabilityAlways "InStock" (print on demand = always available)

Issue 3: Dynamic Sitemap

Google needs a sitemap that includes all live vendor shops and published products. Without it, Google has to discover pages by crawling links — which is slow and incomplete.

Create a dynamic sitemap at: https://shops.beargrips.com/sitemap-shops.xml

This should auto-generate from the database, including:

<?xml version="1.0" encoding="UTF-8"?> <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <!-- Each vendor shop page --> <url> <loc>https://shops.beargrips.com/proshops/gallerybarre</loc> <changefreq>weekly</changefreq> <priority>0.7</priority> </url> <!-- Each published product --> <url> <loc>https://shops.beargrips.com/proshops/gallerybarre/product/699f8abd...</loc> <changefreq>weekly</changefreq> <priority>0.6</priority> </url> <!-- ... repeat for all vendors and products ... --> </urlset>

Implementation notes:

Issue 4: Richer Default Descriptions

Most vendors keep the default shop description: "Shop at [Name]. Browse our store." — this is thin content that doesn't help SEO. Improve the auto-generated defaults so vendors get better rankings without doing extra work.

Default Shop Description (auto-generated)

Replace the current default with a richer template that pulls from the vendor's store name. Vendor can still override this if they want.

Current default:

Shop at [Store Name]. Browse our store.

Better default:

Shop custom apparel from [Store Name]. Browse premium tees, tanks, hoodies, joggers, hats and more. All items printed in the USA with free shipping. New designs added regularly.

Default Shop Title

Current:

[Store Name]

Better:

[Store Name] | Custom Apparel Shop

Default Product Page Title

Current format is good: [Product Name] - [Store Name]

Consider appending a tail keyword:

[Product Name] - [Store Name] | Custom Printed Apparel

Default Product Description Enrichment

When a product uses the default system description, append a vendor-specific line at the end to make each page more unique to Google:

[Existing product description from vendor or system default] Available exclusively from [Store Name]. Printed on demand in the USA. Free shipping on every order.

Why this helps vendors:

Price Display

Minor fix: Round prices to 2 decimal places in all meta tags. Currently showing raw values like 43.5552 and 26.9152.

// Before product:price:amount = 43.5552 // After product:price:amount = 43.56

Issue 5: Missing Favicon on Product Pages

Shop pages have the vendor's favicon but product pages are missing it. The <link rel="icon"> tag should be on all pages — shop and product.

Issue 6: Image Alt Tags

Product mockup images have no alt attribute. Google Images is a major traffic source — without alt text, our product images won't rank.

Current:

<img src="/product-images/69abb695.../comfort-soft-hoodie.jpg">

Fix — add descriptive alt text:

<img src="/product-images/69abb695.../comfort-soft-hoodie.jpg" alt="Comfort Soft Hoodie - Bitcoin Apparel">

Pattern: alt="[Product Name] - [Store Name]"

Apply to:

Issue 7: Canonical URLs on Product Pages

Every public page (shop page and product page) needs a canonical tag to prevent duplicate content issues.

Add to the <head> of every shop and product page:

<!-- Shop page --> <link rel="canonical" href="https://shops.beargrips.com/proshops/bitcoin-apparel" /> <!-- Product page --> <link rel="canonical" href="https://shops.beargrips.com/proshops/bitcoin-apparel/product/699cc3cc90545a0c8aa80aeb" />

Build the URL dynamically from the current page path. This tells Google "this is the one true URL for this page" — prevents indexing of URL variations with query params, trailing slashes, etc.

Issue 8: Noindex Draft/Unpublished Pages

If any product or shop pages are accessible but not yet live (drafts, unpublished, deactivated), they should not be indexed by Google.

Add this to the <head> of any non-live page:

<meta name="robots" content="noindex, nofollow" />

Only live, published shops and products should have index, follow. If a vendor deactivates their shop or unpublishes a product, flip to noindex.

Issue 9: OG Image Dimensions

Add width and height to OG image tags so social platforms (Facebook, LinkedIn, Twitter, iMessage) render previews instantly without fetching the image first.

Add alongside existing og:image tag:

<meta property="og:image:width" content="1200" /> <meta property="og:image:height" content="1200" />

If the actual mockup dimensions differ, use those values instead. Square (1200x1200) or landscape (1200x630) are both fine.

Issue 10: Custom 404 Page

Ready-made file provided

KJ will send you 404.html — a complete, styled 404 page with Bear Grips branding, homepage/catalog links, and a "Open Your Free Shop" CTA. Just wire it up as the 404 route.

To implement:

Preview the 404 page →

Impact

Once these fixes are in place, every vendor shop becomes a free SEO asset:

Favicon & Web Manifest

Favicon files are ready — just add them to shops.beargrips.com. Google shows favicons in search results now, so this matters.

Directory app already has these

The favicons are already wired into directory.beargrips.com. This section is for adding them to shops.beargrips.com.

Files to Add

KJ will provide these files (already generated). Place them in the root/public directory of the shops app:

FileSizePurpose
favicon.ico16+32+48pxBrowser tab (legacy browsers)
favicon-16x16.png16x16Browser tab
favicon-32x32.png32x32Browser tab (retina)
apple-touch-icon.png180x180iPhone home screen bookmark
android-chrome-192x192.png192x192Android home screen
android-chrome-512x512.png512x512Android splash screen

HTML Tags

Add these to the <head> of every page:

<link rel="icon" href="/favicon.ico" sizes="48x48"> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="manifest" href="/site.webmanifest"> <meta name="theme-color" content="#E31837">

Web Manifest File

Create site.webmanifest in the public/root directory:

{ "name": "Bear Grips Pro Shops", "short_name": "Bear Grips", "icons": [ { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } ], "theme_color": "#E31837", "background_color": "#ffffff", "display": "standalone" }

Free Tools Pages

Deploy the 7 free design tools + hub page to shops.beargrips.com/free-design-tools/. These are static HTML files — no build step needed.

What are these?

7 free browser-based design tools (background remover, image cropper, file compressor, transparent PNG checker, image-to-PNG converter, image resizer, text design generator) plus a hub page. They drive organic SEO traffic and funnel visitors to sign up for a Pro Shop. All processing happens client-side — no server-side dependencies.

Files to Deploy

#FileURL on Live Site
1index.html/free-design-tools/
2background-remover.html/free-design-tools/background-remover.html
3image-cropper.html/free-design-tools/image-cropper.html
4file-compressor.html/free-design-tools/file-compressor.html
5transparent-png-checker.html/free-design-tools/transparent-png-checker.html
6image-to-png-converter.html/free-design-tools/image-to-png-converter.html
7image-resizer.html/free-design-tools/image-resizer.html
8text-design-generator.html/free-design-tools/text-design-generator.html
9assets/ folderLogo, favicons (must be in /free-design-tools/assets/)

Steps

1 Create the /free-design-tools/ directory

On the server, create the directory where the tool pages will live:

sudo mkdir -p /var/www/shops-beargrips/free-design-tools sudo mkdir -p /var/www/shops-beargrips/free-design-tools/assets

Adjust the path to match wherever the shops app serves static files from.

2 Upload all files

Upload the hub page + 7 tool pages + assets folder. Use SCP, rsync, or manual upload:

# Upload all HTML files + assets scp index.html background-remover.html image-cropper.html file-compressor.html transparent-png-checker.html image-to-png-converter.html image-resizer.html text-design-generator.html user@167.172.23.233:/var/www/shops-beargrips/free-design-tools/ # Upload assets folder (logo + favicons) scp -r assets/ user@167.172.23.233:/var/www/shops-beargrips/free-design-tools/assets/

3 Verify all pages load

Open each URL in a browser and confirm they load correctly:

  • https://shops.beargrips.com/free-design-tools/ — hub page with links to all 7 tools
  • Click each tool link from the hub — all 7 should load
  • Bear Grips logo + favicon should appear on every page
  • Check mobile (Chrome DevTools → device toolbar)
  • No 404 errors, no broken assets

4 Test each tool works

For each of the 7 tools:

  • Upload a test image (use any photo or logo)
  • Use the tool (resize, crop, remove background, compress, check transparency, convert, generate text)
  • Download the result — confirm the output file is correct
  • Test on both desktop and mobile

5 Add sitemap entries

Add all 8 free tool URLs to the shops sitemap (sitemap.xml):

<url> <loc>https://shops.beargrips.com/free-design-tools/</loc> <changefreq>monthly</changefreq> <priority>0.8</priority> </url> <url> <loc>https://shops.beargrips.com/free-design-tools/background-remover.html</loc> <changefreq>monthly</changefreq> <priority>0.7</priority> </url> <url> <loc>https://shops.beargrips.com/free-design-tools/image-cropper.html</loc> <changefreq>monthly</changefreq> <priority>0.7</priority> </url> <url> <loc>https://shops.beargrips.com/free-design-tools/file-compressor.html</loc> <changefreq>monthly</changefreq> <priority>0.7</priority> </url> <url> <loc>https://shops.beargrips.com/free-design-tools/transparent-png-checker.html</loc> <changefreq>monthly</changefreq> <priority>0.7</priority> </url> <url> <loc>https://shops.beargrips.com/free-design-tools/image-to-png-converter.html</loc> <changefreq>monthly</changefreq> <priority>0.7</priority> </url> <url> <loc>https://shops.beargrips.com/free-design-tools/image-resizer.html</loc> <changefreq>monthly</changefreq> <priority>0.7</priority> </url> <url> <loc>https://shops.beargrips.com/free-design-tools/text-design-generator.html</loc> <changefreq>monthly</changefreq> <priority>0.7</priority> </url>

6 Add internal link from homepage

Add a link to /free-design-tools/ from the shops.beargrips.com homepage. Options:

  • Nav bar: Add "Free Tools" link to the top navigation
  • Footer: Add "Free Design Tools" to the footer link section
  • Both: Nav + footer for maximum visibility and SEO juice

Link text should be: "Free Design Tools"

Nginx note

If the shops app uses nginx to proxy all requests to the Node app, you may need to add a location /free-design-tools/ block to serve these as static files directly, before the proxy_pass catch-all. Ask KJ if unsure.

Niche Landing Pages (117 Pages)

Deploy 117 niche-specific landing pages to shops.beargrips.com/free-design-tools/[niche-slug]. Same 7 tools, niche-specific copy/SEO targeting long-tail keywords like "custom gym apparel" and "church t-shirt design tools."

Why niche pages?

Each page targets a unique long-tail keyword (e.g. "free design tools for gym apparel"). 117 pages = 117 chances to rank in Google. Same tools, different copy — zero extra server cost. All pages funnel to shops.beargrips.com signup.

Directory Structure

All niche pages live inside a niche/ subfolder within the free-design-tools directory:

/free-design-tools/ index.html ← Main tools hub background-remover.html ← 7 tool pages image-cropper.html file-compressor.html transparent-png-checker.html image-to-png-converter.html image-resizer.html text-design-generator.html assets/ ← Logo + favicons niche/ ← All 45+ niche landing pages gym-apparel.html crossfit-apparel.html church-apparel.html ... (45+ files)

All 117 Niche Pages to Upload

#FileLive URL
1after-school-program-apparel.html/free-design-tools/niche/after-school-program-apparel
2auto-shop-apparel.html/free-design-tools/niche/auto-shop-apparel
3bachelorette-party-shirts.html/free-design-tools/niche/bachelorette-party-shirts
4bachelor-party-shirts.html/free-design-tools/niche/bachelor-party-shirts
5bakery-apparel.html/free-design-tools/niche/bakery-apparel
6bar-apparel.html/free-design-tools/niche/bar-apparel
7barbershop-apparel.html/free-design-tools/niche/barbershop-apparel
8barre-studio-apparel.html/free-design-tools/niche/barre-studio-apparel
9baseball-apparel.html/free-design-tools/niche/baseball-apparel
10basketball-apparel.html/free-design-tools/niche/basketball-apparel
11birthday-shirts.html/free-design-tools/niche/birthday-shirts
12boat-club-apparel.html/free-design-tools/niche/boat-club-apparel
13bootcamp-apparel.html/free-design-tools/niche/bootcamp-apparel
14boxing-gym-apparel.html/free-design-tools/niche/boxing-gym-apparel
15boy-scouts-apparel.html/free-design-tools/niche/boy-scouts-apparel
16brewery-merch.html/free-design-tools/niche/brewery-merch
17cafe-apparel.html/free-design-tools/niche/cafe-apparel
18car-club-apparel.html/free-design-tools/niche/car-club-apparel
19catering-apparel.html/free-design-tools/niche/catering-apparel
20cheerleading-apparel.html/free-design-tools/niche/cheerleading-apparel
21church-apparel.html/free-design-tools/niche/church-apparel
22college-apparel.html/free-design-tools/niche/college-apparel
23company-swag.html/free-design-tools/niche/company-swag
24conference-apparel.html/free-design-tools/niche/conference-apparel
25construction-apparel.html/free-design-tools/niche/construction-apparel
26country-club-apparel.html/free-design-tools/niche/country-club-apparel
27creator-merch.html/free-design-tools/niche/creator-merch
28cruise-apparel.html/free-design-tools/niche/cruise-apparel
29dance-studio-apparel.html/free-design-tools/niche/dance-studio-apparel
30daycare-apparel.html/free-design-tools/niche/daycare-apparel
31dentist-apparel.html/free-design-tools/niche/dentist-apparel
32dispensary-apparel.html/free-design-tools/niche/dispensary-apparel
33doctor-office-apparel.html/free-design-tools/niche/doctor-office-apparel
34dog-trainer-apparel.html/free-design-tools/niche/dog-trainer-apparel
35dog-walker-apparel.html/free-design-tools/niche/dog-walker-apparel
36dropshipping-apparel.html/free-design-tools/niche/dropshipping-apparel
37electrician-apparel.html/free-design-tools/niche/electrician-apparel
38elementary-school-apparel.html/free-design-tools/niche/elementary-school-apparel
39entrepreneur-apparel.html/free-design-tools/niche/entrepreneur-apparel
40event-planner-apparel.html/free-design-tools/niche/event-planner-apparel
41family-reunion-shirts.html/free-design-tools/niche/family-reunion-shirts
42farmers-market-apparel.html/free-design-tools/niche/farmers-market-apparel
43fire-department-apparel.html/free-design-tools/niche/fire-department-apparel
44fishing-apparel.html/free-design-tools/niche/fishing-apparel
45flag-football-apparel.html/free-design-tools/niche/flag-football-apparel
46food-truck-apparel.html/free-design-tools/niche/food-truck-apparel
47fraternity-sorority-apparel.html/free-design-tools/niche/fraternity-sorority-apparel
48functional-fitness-apparel.html/free-design-tools/niche/functional-fitness-apparel
49girl-scouts-apparel.html/free-design-tools/niche/girl-scouts-apparel
50golf-apparel.html/free-design-tools/niche/golf-apparel
51graduation-apparel.html/free-design-tools/niche/graduation-apparel
52gym-apparel.html/free-design-tools/niche/gym-apparel
53gymnastics-apparel.html/free-design-tools/niche/gymnastics-apparel
54high-school-apparel.html/free-design-tools/niche/high-school-apparel
55hiit-gym-apparel.html/free-design-tools/niche/hiit-gym-apparel
56hiking-club-apparel.html/free-design-tools/niche/hiking-club-apparel
57hockey-apparel.html/free-design-tools/niche/hockey-apparel
58hospital-apparel.html/free-design-tools/niche/hospital-apparel
59hvac-apparel.html/free-design-tools/niche/hvac-apparel
60hybrid-athlete-apparel.html/free-design-tools/niche/hybrid-athlete-apparel
61instagram-merch.html/free-design-tools/niche/instagram-merch
62insurance-apparel.html/free-design-tools/niche/insurance-apparel
63kickboxing-apparel.html/free-design-tools/niche/kickboxing-apparel
64lacrosse-apparel.html/free-design-tools/niche/lacrosse-apparel
65landscaping-apparel.html/free-design-tools/niche/landscaping-apparel
66law-firm-apparel.html/free-design-tools/niche/law-firm-apparel
67martial-arts-apparel.html/free-design-tools/niche/martial-arts-apparel
68micro-influencer-merch.html/free-design-tools/niche/micro-influencer-merch
69middle-school-apparel.html/free-design-tools/niche/middle-school-apparel
70mma-apparel.html/free-design-tools/niche/mma-apparel
71mortgage-apparel.html/free-design-tools/niche/mortgage-apparel
72nonprofit-fundraiser.html/free-design-tools/niche/nonprofit-fundraiser
73nurse-apparel.html/free-design-tools/niche/nurse-apparel
74personal-brand-merch.html/free-design-tools/niche/personal-brand-merch
75personal-trainer-apparel.html/free-design-tools/niche/personal-trainer-apparel
76pest-control-apparel.html/free-design-tools/niche/pest-control-apparel
77pet-business-apparel.html/free-design-tools/niche/pet-business-apparel
78pickleball-apparel.html/free-design-tools/niche/pickleball-apparel
79pilates-studio-apparel.html/free-design-tools/niche/pilates-studio-apparel
80pizza-shop-apparel.html/free-design-tools/niche/pizza-shop-apparel
81plumbing-apparel.html/free-design-tools/niche/plumbing-apparel
82print-on-demand-platform.html/free-design-tools/niche/print-on-demand-platform
83print-on-demand-side-hustle.html/free-design-tools/niche/print-on-demand-side-hustle
84print-on-demand-tools-2026.html/free-design-tools/niche/print-on-demand-tools-2026
85private-school-apparel.html/free-design-tools/niche/private-school-apparel
86real-estate-apparel.html/free-design-tools/niche/real-estate-apparel
87rec-league-apparel.html/free-design-tools/niche/rec-league-apparel
88restaurant-apparel.html/free-design-tools/niche/restaurant-apparel
89rock-climbing-apparel.html/free-design-tools/niche/rock-climbing-apparel
90roofing-apparel.html/free-design-tools/niche/roofing-apparel
91running-club-apparel.html/free-design-tools/niche/running-club-apparel
92sales-team-apparel.html/free-design-tools/niche/sales-team-apparel
93salon-apparel.html/free-design-tools/niche/salon-apparel
94school-spirit-wear.html/free-design-tools/niche/school-spirit-wear
95side-hustle-apparel.html/free-design-tools/niche/side-hustle-apparel
96smoke-shop-apparel.html/free-design-tools/niche/smoke-shop-apparel
97soccer-apparel.html/free-design-tools/niche/soccer-apparel
98softball-apparel.html/free-design-tools/niche/softball-apparel
99spa-apparel.html/free-design-tools/niche/spa-apparel
100spin-studio-apparel.html/free-design-tools/niche/spin-studio-apparel
101startup-swag.html/free-design-tools/niche/startup-swag
102summer-camp-apparel.html/free-design-tools/niche/summer-camp-apparel
103swim-team-apparel.html/free-design-tools/niche/swim-team-apparel
104teacher-apparel.html/free-design-tools/niche/teacher-apparel
105tech-company-apparel.html/free-design-tools/niche/tech-company-apparel
106tennis-apparel.html/free-design-tools/niche/tennis-apparel
107tiktok-merch.html/free-design-tools/niche/tiktok-merch
108track-field-apparel.html/free-design-tools/niche/track-field-apparel
109university-apparel.html/free-design-tools/niche/university-apparel
110veterinary-apparel.html/free-design-tools/niche/veterinary-apparel
111volleyball-apparel.html/free-design-tools/niche/volleyball-apparel
112winery-apparel.html/free-design-tools/niche/winery-apparel
113wrestling-apparel.html/free-design-tools/niche/wrestling-apparel
114yoga-studio-apparel.html/free-design-tools/niche/yoga-studio-apparel
115youth-group-apparel.html/free-design-tools/niche/youth-group-apparel
116youth-sports-apparel.html/free-design-tools/niche/youth-sports-apparel
117youtuber-merch.html/free-design-tools/niche/youtuber-merch

Steps

1 Create the niche/ directory

Create a niche/ subfolder inside the free-design-tools directory:

sudo mkdir -p /var/www/shops-beargrips/free-design-tools/niche

2 Upload all 117 niche HTML files

Upload all .html files from the niche folder:

scp niche/*.html user@167.172.23.233:/var/www/shops-beargrips/free-design-tools/niche/

Important: Asset paths inside niche pages use ../assets/ (one level up). Make sure the assets/ folder is already uploaded at the free-design-tools level.

3 Verify niche pages load

Spot-check a few niche pages to confirm they work:

  • https://shops.beargrips.com/free-design-tools/niche/gym-apparel.html
  • https://shops.beargrips.com/free-design-tools/niche/church-apparel.html
  • https://shops.beargrips.com/free-design-tools/niche/pickleball-apparel.html
  • Logo + favicon loads on every page (from ../assets/)
  • Tool links point to ../background-remover.html etc. (correct path)
  • CTA buttons link to https://shops.beargrips.com
  • Check mobile responsiveness

4 Add all 117 niche pages to sitemap

Add all niche page URLs to sitemap.xml. Each gets priority 0.6:

<!-- Niche Landing Pages --> <url> <loc>https://shops.beargrips.com/free-design-tools/niche/gym-apparel.html</loc> <changefreq>monthly</changefreq> <priority>0.6</priority> </url> <!-- Repeat for all 117 niche pages -->

Full list of URLs in the table above. All follow the same pattern.

5 Nginx — serve niche/ as static files

If nginx proxies everything to the Node app, add a location block for the niche directory:

# Add this BEFORE the proxy_pass catch-all block location /free-design-tools/ { alias /var/www/shops-beargrips/free-design-tools/; try_files $uri $uri/ =404; }

This one block covers both the main tools pages AND the niche subdirectory.

All 117 niche pages ready

All 117 niche pages are built and ready to upload. Just upload all .html files from the niche/ folder. No code changes needed. If more pages are added later, same process — drop new .html files into the niche/ folder.

Product Catalog — 85 SEO Pages

85 static HTML pages competing with Apliiq, Printify, and Printful for "custom [product] print on demand" keywords. All pages are pre-built with full SEO — just upload to the server.

All 85 pages + images are built and ready

Generator script: built-pages/catalog/generate.js. Output: built-pages/catalog/. Run node built-pages/catalog/generate.js to regenerate if product data changes.

What's Included

TypeCountExample URL
Catalog Hub1/products/index.html
Category Pages11/products/custom-t-shirts.html, /products/custom-hoodies.html
Brand Pages7/products/brands/bella-canvas.html, /products/brands/champion.html
Audience Pages3/products/womens.html, /products/mens.html, /products/youth.html
Product Pages63/products/products/airlume-cotton-athletic-tee.html
XML Sitemap1/products/sitemap-products.xml
Product Images85/products/images/*.png (Bear Grips proprietary)
Brand Logos6/products/images/brands/*.png

SEO Features (Every Page)

Deployment Steps

1 Create /products/ directory on server

Create the products directory structure on the DO server:

sudo mkdir -p /var/www/shops-beargrips/products/images/brands sudo mkdir -p /var/www/shops-beargrips/products/products sudo mkdir -p /var/www/shops-beargrips/products/brands sudo mkdir -p /var/www/shops-beargrips/products/assets

2 Upload all files from built-pages/catalog/

Upload the entire catalog build output to the server:

# Upload root HTML files (hub + category + audience pages + sitemap) scp built-pages/catalog/*.html built-pages/catalog/*.xml user@167.172.23.233:/var/www/shops-beargrips/products/ # Upload product pages scp built-pages/catalog/products/*.html user@167.172.23.233:/var/www/shops-beargrips/products/products/ # Upload brand pages scp built-pages/catalog/brands/*.html user@167.172.23.233:/var/www/shops-beargrips/products/brands/ # Upload product images (~85 images) scp built-pages/catalog/images/*.png user@167.172.23.233:/var/www/shops-beargrips/products/images/ # Upload brand logos scp built-pages/catalog/images/brands/*.png user@167.172.23.233:/var/www/shops-beargrips/products/images/brands/ # Upload assets (logo, favicons) scp built-pages/catalog/assets/* user@167.172.23.233:/var/www/shops-beargrips/products/assets/

Total files: ~100 HTML + 85 images + 6 logos + assets + 1 XML sitemap

3 Nginx location block for /products/

Add this BEFORE the proxy_pass catch-all in the shops nginx config:

# Product Catalog — static HTML pages location /products/ { alias /var/www/shops-beargrips/products/; try_files $uri $uri/ =404; } # Note: If you already have the /free-design-tools/ block, add this # right next to it — both go BEFORE the proxy_pass catch-all

Then reload nginx:

sudo nginx -t && sudo systemctl reload nginx

4 Add product sitemap to robots.txt

Add the product catalog sitemap reference to robots.txt:

# Add this line to robots.txt (alongside existing sitemaps) Sitemap: https://shops.beargrips.com/products/sitemap-products.xml

5 Verify pages load correctly

Test these URLs after deployment:

  • https://shops.beargrips.com/products/ — Catalog hub with all categories, brands, featured products
  • https://shops.beargrips.com/products/custom-t-shirts.html — Category page
  • https://shops.beargrips.com/products/brands/bella-canvas.html — Brand page
  • https://shops.beargrips.com/products/womens.html — Audience page
  • https://shops.beargrips.com/products/products/airlume-cotton-athletic-tee.html — Product page
  • https://shops.beargrips.com/products/sitemap-products.xml — XML sitemap
  • Verify: product images load, Bear Grips logo shows, "FREE Shipping" badges visible
  • Verify: mobile responsive layout works
  • Check View Source → JSON-LD schema present in <head>

6 Add internal link from shops homepage

Add a "Browse Catalog" or "Product Catalog" link somewhere visible on shops.beargrips.com homepage — nav bar, footer, or hero section. Link to:

https://shops.beargrips.com/products/

This passes link equity from the homepage to all 85 catalog pages.

Regenerating pages (if products change)

If product data is updated in docs/products.json, regenerate all pages by running: node built-pages/catalog/generate.js — then re-upload the output. The script generates all 85 pages + sitemap in ~2 seconds.

File Structure on Server

/var/www/shops-beargrips/products/ ├── index.html # Catalog hub ├── custom-t-shirts.html # Category pages (11) ├── custom-hoodies.html ├── custom-tank-tops.html ├── custom-shorts.html ├── custom-hats.html ├── custom-leggings.html ├── custom-joggers.html ├── custom-polos.html ├── custom-quarter-zips.html ├── custom-sweatpants.html ├── custom-long-sleeves.html ├── womens.html # Audience pages (3) ├── mens.html ├── youth.html ├── sitemap-products.xml # XML sitemap (85 URLs) ├── assets/ # Logo + favicons │ ├── logo-white.png │ └── favicon files... ├── images/ # Product images (85) │ ├── airlume-cotton-athletic-tee.png │ ├── ... (63 product images) │ └── brands/ # Brand logos (6) │ ├── bear-grips-logo.png │ ├── bella-canvas-logo.png │ ├── champion-logo.png │ ├── gildan-logo.png │ ├── next-level-logo.png │ └── sport-tek-logo.png ├── brands/ # Brand pages (7) │ ├── bear-grips.html │ ├── bella-canvas.html │ ├── champion.html │ ├── gildan.html │ ├── next-level.html │ ├── sport-tek.html │ └── yupoong.html └── products/ # Individual product pages (63) ├── airlume-cotton-athletic-tee.html ├── womens-favorite-tee.html └── ... (63 total)

Programmatic SEO Pages (2,100+)

All pSEO pages are already live on Cloudflare Pages. No dev action needed for these — they're static HTML hosted separately.

Live URL: bear-grips-pseo.pages.dev

2,100+ static HTML pages deployed on Cloudflare Pages (free tier, unlimited bandwidth). Pages are SEO-optimized with JSON-LD schema, OG tags, mobile-responsive, and cross-linked.

Page Types & Counts

PhaseTypePagesExample URL
1Resource Pages (ideas, checklists, guides)354/resources/custom-gym-apparel-ideas.html
2Competitor Comparisons35/comparisons/bear-grips-vs-printful.html
3POD Glossary160/glossary/print-on-demand.html
4Profit Calculator1/tools/profit-calculator.html
5Affiliate / Side Hustle8/affiliate/best-pod-side-hustles-2026.html
6Occasion / Seasonal17/occasions/fathers-day-custom-apparel.html
7Vendor Spotlight Gallery8/vendors/gallery.html
8Fundraising Content11/fundraising/church-fundraiser-apparel.html
9Deals / Coupons1/deals/
10Niche Deep-Dives (sub-niches)620/tools/niche/boxing-gym-apparel.html
11Product × Niche821/product-niche/hoodies-for-yoga-studio-apparel.html
12How-To Guides118/how-to/how-to-start-a-gym-apparel-merch-business.html
TOTAL2,163+

What These Pages Do

Hosting Details

Subfolder Deploy Ready

pSEO pages will be served as subfolders on shops.beargrips.com (e.g., /resources/, /comparisons/, /tools/). Better SEO than a subdomain — all authority stays on the main domain. See "Deploy pSEO to shops subfolders" in sidebar.

Deploy pSEO Pages to shops.beargrips.com Subfolders

2,100+ static HTML pages need to be served as subfolders on shops.beargrips.com (e.g., /resources/, /comparisons/, /tools/). This is better for SEO than a subdomain — all link equity and authority stays on the main domain.

What This Is

These are pre-built SEO pages (comparisons, glossary, how-to guides, product pages, resources, tools) that drive organic search traffic to Bear Grips. They're currently live on Cloudflare Pages as a temporary host — this task moves them to shops.beargrips.com.

IMPORTANT: Subfolders, NOT Subdomain

We originally planned content.beargrips.com as a subdomain but subfolders are much better for SEO. Google treats subdomains as semi-separate sites, so a subdomain wouldn't pass authority to shops.beargrips.com. Subfolders keep everything under one domain.

Step-by-Step Deploy

1 Create the pSEO directory on the server

mkdir -p /var/www/pseo-pages

2 Upload pSEO files to the server

KJ will provide you with the built-pages/ folder (47MB, 2,100+ HTML files). Copy the entire contents into /var/www/pseo-pages/.

The folder structure should look like this after upload:

/var/www/pseo-pages/ ├── resources/ # 354 resource pages + 465 sub-niche resource pages ├── comparisons/ # 35 competitor comparison pages ├── glossary/ # 160 glossary term pages ├── tools/ # 8 free tools + profit calculator + 155 sub-niche tool pages ├── catalog/ # 85 product catalog pages + images ├── affiliate/ # 8 affiliate/side-hustle pages ├── occasions/ # 17 seasonal/occasion pages ├── vendors/ # 8 vendor spotlight pages ├── fundraising/ # 11 fundraising pages ├── deals/ # 1 deals/coupons page ├── product-niche/ # 821 product x niche crossover pages ├── how-to/ # 118 how-to guides └── [sitemaps in each folder]

Upload method (from your local machine):

# From your local machine (Mac/Linux): # First tar the folder tar -czf pseo-pages.tar.gz -C built-pages . # Upload to server scp pseo-pages.tar.gz root@167.172.23.233:/tmp/ # Then on the server: cd /var/www/pseo-pages tar -xzf /tmp/pseo-pages.tar.gz rm /tmp/pseo-pages.tar.gz

3 Update the shops nginx config to serve pSEO subfolders

Edit the existing shops.beargrips.com nginx config — add location blocks for each pSEO subfolder ABOVE the existing React catch-all location block.

nano /etc/nginx/sites-available/shops.beargrips.com

Add these location blocks BEFORE the main location / block that serves the React app:

# ── pSEO static pages (served from /var/www/pseo-pages/) ── # These MUST come BEFORE the React catch-all location block location /resources/ { alias /var/www/pseo-pages/resources/; try_files $uri $uri/ $uri.html =404; } location /comparisons/ { alias /var/www/pseo-pages/comparisons/; try_files $uri $uri/ $uri.html =404; } location /glossary/ { alias /var/www/pseo-pages/glossary/; try_files $uri $uri/ $uri.html =404; } location /tools/ { alias /var/www/pseo-pages/tools/; try_files $uri $uri/ $uri.html =404; } location /catalog/ { alias /var/www/pseo-pages/catalog/; try_files $uri $uri/ $uri.html =404; } location /affiliate/ { alias /var/www/pseo-pages/affiliate/; try_files $uri $uri/ $uri.html =404; } location /occasions/ { alias /var/www/pseo-pages/occasions/; try_files $uri $uri/ $uri.html =404; } location /vendors/ { alias /var/www/pseo-pages/vendors/; try_files $uri $uri/ $uri.html =404; } location /fundraising/ { alias /var/www/pseo-pages/fundraising/; try_files $uri $uri/ $uri.html =404; } location /deals/ { alias /var/www/pseo-pages/deals/; try_files $uri $uri/ $uri.html =404; } location /product-niche/ { alias /var/www/pseo-pages/product-niche/; try_files $uri $uri/ $uri.html =404; } location /how-to/ { alias /var/www/pseo-pages/how-to/; try_files $uri $uri/ $uri.html =404; } # ── End pSEO blocks ──

Important: These location blocks must appear BEFORE the React app's catch-all location / { try_files $uri /index.html; } block. Otherwise nginx will send pSEO URLs to the React app instead of serving the static files.

4 Test and reload nginx

# Test config (should say "syntax is ok") nginx -t # Reload nginx systemctl reload nginx

5 Verify it works

Test these URLs in your browser:

URLExpected
https://shops.beargrips.com/deals/Deals & coupons page (dark theme, Bear Grips branding)
https://shops.beargrips.com/glossary/print-on-demand.htmlGlossary page for "Print on Demand"
https://shops.beargrips.com/tools/profit-calculator.htmlInteractive profit calculator
https://shops.beargrips.com/how-to/how-to-start-a-gym-apparel-merch-business.htmlHow-to guide for gym merch
https://shops.beargrips.com/product-niche/hoodies-for-yoga-studio-apparel.htmlCustom hoodies for yoga studios
https://shops.beargrips.com/comparisons/bear-grips-vs-printful.htmlBear Grips vs Printful comparison

If any page returns 404, check that files were copied correctly to /var/www/pseo-pages/ and that the nginx location blocks are ABOVE the React catch-all.

Also verify the React app still works: https://shops.beargrips.com/ should load the normal shops homepage.

Important Notes

Security Hardening

Pre-launch security checklist for shops.beargrips.com. Stripe handles real money, vendor accounts hold business data — this is not optional.

CRITICAL — Complete Before Public Launch

These items protect vendor accounts, Stripe payments, and business data. A single breach can kill trust permanently. Work through each section in order.

1. Stripe Payment Security

Stripe handles PCI compliance for card data, but your server-side integration must be airtight.

1 Webhook Signature Verification

Every Stripe webhook must be verified with stripe.webhooks.constructEvent() using your webhook signing secret. Without this, anyone can fake a payment/subscription event by sending a POST to your webhook endpoint.

// In your webhook handler const sig = req.headers['stripe-signature']; const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET; let event; try { event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret); } catch (err) { console.log(`Webhook signature verification failed.`, err.message); return res.status(400).send(`Webhook Error: ${err.message}`); } // Only process verified events switch (event.type) { case 'checkout.session.completed': // Handle payment... break; case 'customer.subscription.updated': // Handle plan change... break; }

2 Server-Side Price Validation

Never trust prices from the frontend. Always fetch product base prices from your database server-side when calculating orders. A malicious user can modify frontend JS to send $0 prices.

3 Stripe API Key Security

  • Use restricted API keys with only the permissions your app needs (charges, subscriptions, customers)
  • Store keys in .env — never in source code or frontend JS
  • Publishable key (pk_live_) = safe for frontend. Secret key (sk_live_) = server only, NEVER exposed
  • Rotate keys immediately if you suspect any exposure

4 Use Stripe Checkout or Elements

Never handle raw card numbers on your server. Use Stripe Checkout (hosted payment page) or Stripe Elements (embedded card form). Both keep card data on Stripe's servers — your server never touches it. This is required for PCI compliance.

2. Authentication & Session Security

1 Secure Session Cookies

Session tokens must use all three flags:

// Cookie settings for session tokens { httpOnly: true, // JS can't read the cookie (prevents XSS token theft) secure: true, // Only sent over HTTPS sameSite: 'strict' // Not sent with cross-site requests (prevents CSRF) }

Never store session tokens in localStorage or sessionStorage — these are readable by any JS on the page, including injected scripts.

2 CSRF Protection

Add CSRF tokens to all state-changing forms and API endpoints (login, signup, plan changes, shop edits, Stripe actions). Libraries like csurf (Express) handle this.

3 Password Security

  • Hash passwords with bcrypt (cost factor 12+) or argon2
  • Never store plaintext, MD5, or SHA hashes
  • Enforce minimum 8 characters
  • Rate limit login: 5 failed attempts → 15 min lockout per IP + per account

4 Session Management

  • Auto-logout after 30-60 minutes of inactivity
  • Invalidate old sessions on password change
  • Generate new session ID after login (prevent session fixation)

3. Server Security Headers (Nginx)

Add these headers to your Nginx server block for shops.beargrips.com. They prevent clickjacking, XSS, MIME sniffing, and enforce HTTPS.

# Add to nginx server block for shops.beargrips.com # Security Headers — REQUIRED before launch # Prevent clickjacking (embedding your site in iframes) add_header X-Frame-Options "DENY" always; # Block MIME-type sniffing (prevents image-as-script attacks) add_header X-Content-Type-Options "nosniff" always; # Enable browser XSS filter add_header X-XSS-Protection "1; mode=block" always; # Control referrer info sent to other sites add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Force HTTPS for 1 year (only add AFTER SSL is confirmed working) add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # Disable camera, microphone, geolocation access add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always; # Content Security Policy — allow only trusted sources # IMPORTANT: Adjust script-src and connect-src if you add new third-party services add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://js.stripe.com; frame-src https://js.stripe.com; connect-src 'self' https://api.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';" always; # Disable directory listing autoindex off; # Force HTTPS redirect server { listen 80; server_name shops.beargrips.com; return 301 https://$host$request_uri; }

Test CSP Before Going Live

Content-Security-Policy can break things if too restrictive. Start with Content-Security-Policy-Report-Only to find issues without blocking, then switch to enforcing once verified.

4. DDoS & Bot Protection

Cloudflare is already live on shops.beargrips.com. No setup needed — just be aware that shops, content, and directory subdomains are proxied through Cloudflare.

5. Rate Limiting on API Routes

Rate limit all API endpoints to prevent brute force and abuse. The directory app already has this pattern — apply it to shops.

// Simple in-memory rate limiter (same pattern as directory app) const rateLimiter = new Map(); const RATE_LIMIT = 30; // max requests const RATE_WINDOW = 60000; // per 60 seconds function checkRateLimit(ip) { const now = Date.now(); const record = rateLimiter.get(ip); if (!record || now - record.start > RATE_WINDOW) { rateLimiter.set(ip, { count: 1, start: now }); return true; } record.count++; return record.count <= RATE_LIMIT; } // Apply to Express/API routes app.use('/api/', (req, res, next) => { const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; if (!checkRateLimit(ip)) { return res.status(429).json({ error: 'Too many requests. Try again later.' }); } next(); }); // Stricter limit for login (5 per minute) app.use('/api/auth/login', (req, res, next) => { const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; if (!checkRateLimit('login:' + ip, 5, 60000)) { return res.status(429).json({ error: 'Too many login attempts. Wait 1 minute.' }); } next(); });

6. Input Validation & Injection Prevention

SQL / NoSQL Injection

  • Always use parameterized queries — never concatenate user input into database queries
  • Use an ORM (Prisma, Sequelize, Mongoose) which parameterizes by default
  • Validate and sanitize all inputs: vendor names, shop descriptions, product titles, profile fields

XSS (Cross-Site Scripting)

  • React escapes HTML output by default — but never use dangerouslySetInnerHTML with user content
  • Sanitize any user-generated content displayed on public pages (shop names, product descriptions)
  • Use a library like DOMPurify if you must render HTML from user input

File Upload Security — Use Cloudflare R2

Why R2 (not local storage)

Vendors upload logos, designs, AND up to 4 custom mockup images per product. At scale: 1,000 vendors × 200 products × 4 mockups = 800K+ files. Local disk won't cut it. R2 has $0 egress fees, free first 10GB/month, and S3-compatible API — we're already on Cloudflare everywhere.

R2 Bucket — Ready to Use

Bucket Created: proshopuploads

Location: Eastern North America. S3-compatible API ready.

Add to .env

# Cloudflare R2 — vendor file uploads R2_ACCOUNT_ID=a40bf4d68408f041bdd80e3f0b143e2c R2_ACCESS_KEY_ID=9dbec1040cb16366f56f1a89399d0cee R2_SECRET_ACCESS_KEY=785431f0c09818f2ccbcbe7fea95aeebe1338a816b2043c6f96231ac48fe607e R2_BUCKET_NAME=proshopuploads R2_ENDPOINT=https://a40bf4d68408f041bdd80e3f0b143e2c.r2.cloudflarestorage.com

Node.js Setup

npm install @aws-sdk/client-s3
// r2.js — R2 client setup const { S3Client, PutObjectCommand, GetObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3'); const crypto = require('crypto'); const path = require('path'); const r2 = new S3Client({ region: 'auto', endpoint: process.env.R2_ENDPOINT, credentials: { accessKeyId: process.env.R2_ACCESS_KEY_ID, secretAccessKey: process.env.R2_SECRET_ACCESS_KEY, }, }); // Upload a file to R2 async function uploadToR2(fileBuffer, originalName, vendorId) { const ext = path.extname(originalName).toLowerCase(); const key = `${vendorId}/${crypto.randomUUID()}${ext}`; await r2.send(new PutObjectCommand({ Bucket: process.env.R2_BUCKET_NAME, Key: key, Body: fileBuffer, ContentType: getMimeType(ext), })); return key; // Store this in MongoDB } // Delete a file from R2 async function deleteFromR2(key) { await r2.send(new DeleteObjectCommand({ Bucket: process.env.R2_BUCKET_NAME, Key: key, })); } function getMimeType(ext) { const types = { '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp', '.svg': 'image/svg+xml' }; return types[ext] || 'application/octet-stream'; } module.exports = { r2, uploadToR2, deleteFromR2 };

Important Notes

  • Upload from Node.js backend only — never expose R2 keys to the browser
  • Store the returned R2 key (e.g. vendor123/abc-uuid.png) in MongoDB alongside the product/vendor record
  • Default product mockups still come from Printify/Printful CDN — R2 is only for vendor-uploaded files
  • Make sure .env is in .gitignore

Upload Flow

  • Validate file type server-side via magic bytes (not just extension) — allow: JPEG, PNG, WebP, SVG
  • Enforce 10MB max per file
  • Rename to UUID: {vendorId}/{uuid}.{ext} — never use original filename
  • Upload to R2 from the Node.js backend (never direct from browser to R2)
  • Store the R2 key/URL in DB alongside the product/vendor record
  • Serve images with proper MIME type + Cache-Control headers
  • Never execute uploaded files

Upload Types & Limits

Upload TypeMax FilesMax SizeNotes
Vendor logo/design~5-6 per vendor10MB eachUsed for DFY monthly designs
Custom product mockups4 per product10MB eachReplaces default front/back mockups

Volume Estimates

ScaleVendorsEst. FilesEst. StorageR2 Cost
Now20 DFY~2,000~4 GBFree
100 vendorsMixed tiers~20,000~40 GB~$0.45/mo
1,000 vendorsMixed tiers~200,000+~400 GB~$5.85/mo

7. Data Protection

Environment Variables

  • All API keys, DB credentials, Stripe keys, secrets in .env — never committed to git
  • Verify .gitignore includes: .env, .env.local, .env.production
  • Use different keys for development vs production

Database Backups

  • Automated daily backups of all vendor data, orders, account info
  • Store backups off-server (S3, Backblaze B2, or DO Spaces)
  • Test restore process at least once before launch
  • Set up weekly backup verification (restore to a test DB and confirm data integrity)

robots.txt — Block Sensitive Routes

# Add these to shops.beargrips.com/robots.txt User-agent: * Disallow: /api/ Disallow: /admin/ Disallow: /dashboard/ Disallow: /auth/ Disallow: /account/ Disallow: /vendor/settings/

8. Monitoring & Alerts

Error Logging

  • Sentry (free tier) — account created. Login: proshops@beargrips.com / fgr89^Q&6fTr
  • Log in → Create a Node.js project → copy the DSN key
  • Install: npm install @sentry/node
  • Add to Express app (before routes): const Sentry = require("@sentry/node"); Sentry.init({ dsn: "YOUR_DSN_KEY" }); app.use(Sentry.Handlers.requestHandler());
  • Add after routes: app.use(Sentry.Handlers.errorHandler());
  • Log all failed login attempts with IP and timestamp
  • Log all Stripe webhook failures
  • Never log sensitive data (passwords, card numbers, full API keys)

Quick Verification Checklist

After implementing, run these checks:

#CheckHow to Test
1HTTPS forcedVisit http://shops.beargrips.com — should redirect to https://
2Security headersRun securityheaders.com — aim for A or A+ grade
3Stripe webhooks verifiedSend a test webhook from Stripe dashboard → confirm it's processed. Then send a fake POST to your webhook URL → confirm it's rejected
4Rate limiting worksHit /api/auth/login 6+ times rapidly → should get 429 on attempt 6
5XSS blockedTry entering <script>alert(1)</script> as a shop name → should be escaped/rejected
6No directory listingVisit a directory URL like /api/ directly → should get 404/403, not a file listing
7SSL cert validRun ssllabs.com/ssltest — aim for A grade
8Sensitive routes blockedVisit /admin/, /dashboard/ as logged-out user → should redirect to login, not show content

Blog & URL Architecture

URL structure for all content pages on shops.beargrips.com. This defines where every page lives so internal links work correctly across the entire site.

Action Required: Create /blog/ Route & Deploy Content

The blog section doesn't exist yet. Create the /blog/ route in the app, set up the directory structure on the server, and deploy the static content pages listed below. The Blog API (Phase 3 in LLM API Requirements) will eventually replace static file deploys.

Complete URL Map

Resources (Curated — keep clean)

URLContentStatus
/resources/Resources hub pageLIVE
/resources/design-services/Design services overviewLIVE
/resources/design-services/graphic-designer-logo$99 logo design serviceLIVE
/resources/design-services/file-formatting$29 file formatting serviceLIVE
/resources/free-design-tools/12 free tools hub + individual tool pagesREADY — needs deploy
/resources/free-design-tools/{tool-name}Individual tool pages (12 total)READY — needs deploy

/blog/ — All SEO & Niche Content (3,800+ pages)

The blog holds all programmatic niche content. This keeps /resources/ clean while giving Google thousands of indexable pages. Structure:

URL PatternContent TypeCountStatus
/blog/Blog index/hub page1Needs creation
/blog/tools/{tool}-for-{niche}/Free tool × niche SEO pages3,264BUILT — ready to deploy
/blog/services/logo-design-{niche}/$99 logo design × niche pages272BUILT — ready to deploy
/blog/services/file-formatting-{niche}/$29 file formatting × niche pages272BUILT — ready to deploy
/blog/{article-slug}/Written blog articles (how-to, guides)3WRITTEN — in SEO/06-blog-posts.md

Other Existing Content (Separate Deploy)

URL PatternContent TypeCount
/catalog/Product catalog pages85
/comparisons/Bear Grips vs competitor pages35
/{niche}/Top-level niche landing pages (CrossFit, Yoga, etc.)6 planned

Server Setup

# Create blog directory structure on server mkdir -p /var/www/shops-beargrips/blog/tools mkdir -p /var/www/shops-beargrips/blog/services # Upload tool×niche pages (3,264 files) # Source: built-pages/tools/niche/tool-pages/*.html # Destination: /var/www/shops-beargrips/blog/tools/ # Upload service×niche pages (544 files) # Source: built-pages/tools/niche/services/*.html # Destination: /var/www/shops-beargrips/blog/services/ # Upload free design tools (12 tools + hub + assets) # Source: built-pages/tools/*.html + built-pages/tools/assets/ # Destination: /var/www/shops-beargrips/resources/free-design-tools/

Nginx Configuration

Add these location blocks to the existing shops nginx config. These serve static HTML files from the blog and tools directories while keeping the React app running for everything else.

# Free Design Tools (static HTML) location /resources/free-design-tools/ { alias /var/www/shops-beargrips/resources/free-design-tools/; try_files $uri $uri/ $uri.html =404; } # Blog content (static HTML) location /blog/ { alias /var/www/shops-beargrips/blog/; try_files $uri $uri/ $uri.html $uri/index.html =404; }

Internal Linking Rules

The Funnel

All content funnels traffic toward these conversion points:

  1. Google → Blog niche page (SEO entry point)
  2. Blog page → Free tool (user engages with tool)
  3. Free tool → Design services (if tool didn't work: $29 formatting or $99 logo)
  4. Any page → Pro Shop signup (the real conversion: free or paid plan)

Sitemap

Once deployed, add all blog URLs to the sitemap. With 3,800+ pages, split into multiple sitemap files:

Future: Blog API Replaces Static Deploys

Once the Blog API endpoints (Phase 3 in LLM API Requirements) are built, Claude can create/update/delete blog posts directly via API instead of generating static HTML. This eliminates the deploy step entirely. Until then, content is pre-built as static HTML and deployed to the server.

Content Already Built (File Locations)

ContentLocal PathFile Count
Free tools (hub + 12 tools + assets)built-pages/tools/14 files + assets/
Niche hub pagesbuilt-pages/tools/niche/271 files
Tool × niche pagesbuilt-pages/tools/niche/tool-pages/3,264 files
Logo design × niche pagesbuilt-pages/tools/niche/services/272 files
File formatting × niche pagesbuilt-pages/tools/niche/services/272 files
Blog articles (markdown)SEO/06-blog-posts.md3 articles
Product catalogbuilt-pages/catalog/85 files
Comparison pagesbuilt-pages/comparisons/35 files

Important: Canonical URLs Must Match

All pre-built pages have canonical URLs baked in. If the final URL structure changes from what's listed above, we need to update the canonical tags in all files before deploy. Run the generator scripts again with updated base URLs if needed.

LLM Integration API Requirements

Staff-level API endpoints for Claude to perform high-volume backend work — product uploads, vendor onboarding, SEO, blog content — without touching the codebase or server.

Additive Only — No Changes to Existing Platform

This document covers the API surface required for LLM-assisted staff workflows. It does not require changes to existing business logic, database schema, or any vendor-facing functionality. All endpoints listed are additive to the current platform. Existing UI workflows, admin routes, and vendor-facing features remain untouched.

What Is Claude?

Claude is an LLM (large language model) that makes HTTP API calls. It does not use a browser, does not have a GUI, and does not access the server filesystem or codebase. It operates exactly like an external API client — sending JSON requests and receiving JSON responses. It can execute hundreds or thousands of these calls in sequence with clear instructions and human oversight. We already use Claude to generate and deploy 100,000+ pages across 11 directory sites. This API extends that same capability to the Pro Shop platform.

Safety Model

Claude can only do what the API physically allows. It cannot exceed the permissions of its authentication token. It cannot call endpoints that don't exist. Every destructive operation requires explicit confirmation parameters. Every bulk operation auto-snapshots before executing. The API is the guardrail — not trust, not instructions, not good behavior. If the endpoint doesn't exist, Claude can't do it. Claude is always supervised by Kunal (owner) or designated staff. It does not operate autonomously.

1. Core Principles

1 PATCH, Never PUT

Every update endpoint must use PATCH — updating only the fields sent in the request. PUT replaces the entire object with whatever is submitted, which causes existing data to be overwritten or lost. This is the single most important technical rule in this document. No update route should accept a full object replacement.

2 Auto-Publish by Default

Product creation (POST /api/products) should auto-publish immediately by default. This is essential for high-volume batch runs — requiring a separate publish call per product adds hours of overhead across hundreds of items. An optional draft: true parameter should be supported for cases where a hold is needed.

3 Staff Role Scoping

Claude authenticates using a scoped API key (see Section 2). It inherits staff-level permissions exactly — no more, no less. No admin routes should be exposed to this layer.

4 Snapshot Before Bulk Operations

Any operation touching more than one existing record must automatically snapshot the affected records to a restore table before executing. This happens at the API level — not triggered by Claude, not optional. Snapshots retained for minimum 30 days.

5 No Destructive Wildcards

Bulk delete and bulk edit endpoints must require an explicit array of record IDs or a named filter. Wildcard operations (delete all, update all) are not permitted.

6 Idempotency on Creates

All POST (create) endpoints must accept an optional Idempotency-Key header. If the same key is sent twice (e.g., network retry), return the original response instead of creating a duplicate.

2. Authentication

Claude is a headless API client — it does not run in a browser and cannot access localStorage, cookies, or session storage. Authentication must work for a non-browser client making direct HTTP calls.

Recommended: Scoped API Key

Generate a long-lived API key tied to a staff-level account. Passed in every request header:

Authorization: Bearer bgstaff_<key>

Requirements

3. Standard Response Format

All endpoints must return a consistent JSON envelope. Inconsistent response shapes cause parsing failures across hundreds of sequential calls.

Success Response

{ "success": true, "data": { ... }, "pagination": { "page": 1, "limit": 50, "total": 312 } }

Error Response

{ "success": false, "error": { "code": "DUPLICATE_PRODUCT", "message": "A product with this base name already exists for this vendor." } }

Standard Error Codes

CodeMeaning
UNAUTHORIZEDInvalid or expired API key
FORBIDDENValid key but insufficient tier
NOT_FOUNDEntity does not exist
DUPLICATE_PRODUCTProduct with same base name exists for vendor
VALIDATION_ERRORMissing or invalid fields in request body
SNAPSHOT_REQUIREDBulk operation blocked — snapshot system unavailable
CONFIRM_REQUIREDDestructive operation missing confirm parameter
PROVIDER_ERRORUpstream error from Printify/Printful/Apliiq

4. Catalog Endpoints

The catalog is the source of truth for all product templates. Claude needs read access to list and inspect catalog products, and targeted write access for variant filtering only.

MethodEndpointPurpose
GET/api/catalogList all catalog products. Must return the full catalog with no artificial cap — support pagination (page + limit) or limit=all.
GET/api/catalog/:catalogIdGet single catalog product — sizes, description, pricing, provider data
GET/api/catalog/:catalogId/printify-dataGet Printify cached variant, color, and image data for a catalog item
PATCH/api/catalog/:catalogId/printify-dataUpdate only the printify_product_data field — used for variant filtering before upload. Must not touch any other catalog fields.

The catalog's availableSizes and availableColors fields must return plain string arrays. Both fields are used directly in product creation payloads.

5. Product Endpoints

Claude needs to do everything a staff member does on the product side — read the catalog, create products from catalog items, publish them, edit them, and delete them. The catalog is made up of products from three POD providers (Printify, Printful, Apliiq). A single batch can mix products from all three.

What Claude Needs to Do

Mixed-Provider Batches

A batch of 15 products may include items from Printify, Printful, and Apliiq mixed together. The API should abstract the provider differences — same way the frontend already works. Claude picks a catalog item, the API handles routing to the right provider. If an item in a batch fails (e.g., Printful mockup timeout), that shouldn't block the other items from completing.

6. Media Library

Two actions map to what a staff member does in the UI: upload an image to the media library, then click it to attach it to the product editor. Both need API equivalents.

MethodEndpointPurpose
GET/api/media/vendor/:vendorIdList all assets in a vendor's media library. Returns: assetId, url, filename, width, height. No cap.
GET/api/media/vendor/:vendorId?search=XSearch media library by filename.
GET/api/media/:assetIdGet a single asset by ID.
POST/api/media/uploadUpload image to vendor's media library. Accepts multipart/form-data or base64. Returns assetId, url, width, height.
POST/api/media/:assetId/selectAttach asset to product editor — programmatic equivalent of clicking an image in the UI. Returns full asset reference: assetId, url, width, height, sourceAssetId.
DELETE/api/media/:assetIdDelete a media asset. Scoped to vendor's own library only.

The assetId maps to sourceAssetId in Printify/Printful payloads. The url maps to designImage and previewUrl. The select endpoint must return all fields needed to complete a product creation payload in a single call.

7. Vendor Endpoints

MethodEndpointPurpose
GET/api/vendors/:vendorIdBasic vendor info — plan type (free/VIP), store slug, business name
GET/api/vendors/:vendorId/settingsRead vendor store settings
PATCH/api/vendors/:vendorId/settingsUpdate specific store settings — header, logo, OG metadata, social links. PATCH only. Required for DFY VIP.
GET/api/vendors/:vendorId/layoutRead vendor storefront layout
PATCH/api/vendors/:vendorId/layoutUpdate storefront layout — sections, featured products, section order, banner. PATCH only. Required for DFY VIP.
GET/api/vendors/:vendorId/logoGet the vendor's uploaded logo/design files. Returns URL(s) and metadata.
GET/api/vendors/:vendorId/categoriesList product categories on the vendor's storefront
POST/api/vendors/:vendorId/categoriesCreate a new product category
PATCH/api/vendors/:vendorId/categories/:categoryIdUpdate a category name or contents
DELETE/api/vendors/:vendorId/categories/:categoryIdDelete a category — only if it contains no products
PATCH/api/vendors/:vendorId/categories/reorderSet display order of categories on storefront

Vendor Onboarding

The platform has an existing backend onboarding submission page. Claude needs to read what the vendor submitted, download any files they uploaded (logos, designs), do the work on their shop, and mark the onboarding as complete.

MethodEndpointPurpose
GET/api/vendors/:vendorId/onboardingRead the vendor's onboarding submission — business info, preferences, plan type, and list of uploaded files.
GET/api/vendors/:vendorId/onboarding/filesList all files the vendor uploaded during onboarding (logos, designs, assets). Returns filename, URL, file type, dimensions.
GET/api/vendors/:vendorId/onboarding/files/:fileIdDownload a specific uploaded file by ID.
PATCH/api/vendors/:vendorId/onboardingMark onboarding as complete (or mark individual steps complete). PATCH only.

8. SEO & Metadata Endpoints

Claude needs to read and write SEO fields on products, pages, and blog posts — either as part of creating new content or as a standalone SEO cleanup task.

What Claude Needs to Do

Scope: SEO fields on content only. Not: sitemap config, robots.txt, domain settings, or server-level SEO infrastructure.

9. Blog & Content Endpoints

Claude needs full create/edit/delete access to blog posts, categories, and site pages. Same content management a staff member does, just through the API.

What Claude Needs to Do

Safety rule: Deleting an entire blog, wiping all categories, or removing all pages in a single call must not be possible. Bulk operations require explicit ID arrays.

10. Snapshot & Revert System

Safety net for bulk operations. Any API call that modifies or deletes more than one existing record must auto-snapshot before executing. This is at the infrastructure level — Claude does not trigger it.

MethodEndpointPurpose
GET/api/snapshotsList available snapshots — timestamp, operation type, record count, entity type
GET/api/snapshots/:snapshotIdView full snapshot contents before reverting
POST/api/snapshots/:snapshotId/revertRestore all records to pre-operation state
DELETE/api/snapshots/:snapshotIdManually delete a snapshot (admin-tier only)

Retention: minimum 30 days, then auto-purge. Each snapshot records: timestamp, operation label, affected record IDs, and full previous state. Even a bulk operation touching 10,000 records is fully recoverable.

11. SEO Infrastructure Endpoints

These endpoints give Claude visibility into the site's SEO health and the ability to generate sitemaps. Unlike Section 8 (SEO metadata on content), these are infrastructure-level — they report on the platform's indexing status and expose data needed for sitemap generation.

Why this matters

As of Mar 27, the sitemap has 17 URLs out of ~760+ live pages (2% coverage). With 35-40 vendors and 63 catalog products, there are hundreds of pages Google can't find. These endpoints let Claude auto-generate complete sitemaps and monitor SEO health without touching the codebase.

Sitemap Data Endpoints

MethodEndpointPurpose
GET/api/sitemap/shopsReturn all LIVE vendor shops with slug, lastmod date, and published product count. Used to build sitemap-shops.xml.
GET/api/sitemap/productsReturn all PUBLISHED products across all live vendors — product ID, vendor slug, lastmod date. Paginated (limit/offset). Used to build sitemap-products.xml.
GET/api/sitemap/statsSummary counts: total live shops, total published products, total static pages, total blog posts. One call to see the full picture.

Response Format — /api/sitemap/shops

{ "success": true, "data": [ { "slug": "gallerybarre", "vendorName": "Gallery Barre", "lastmod": "2026-03-20T14:30:00Z", "productCount": 12, "plan": "vip" }, { "slug": "everydayattire", "vendorName": "Everyday Attire", "lastmod": "2026-03-18T09:15:00Z", "productCount": 3, "plan": "free" } ], "total": 38 }

Response Format — /api/sitemap/products

{ "success": true, "data": [ { "productId": "abc123", "vendorSlug": "gallerybarre", "title": "Women's Favorite Tee - Bella+Canvas", "lastmod": "2026-03-20T14:30:00Z", "url": "/proshops/gallerybarre/product/abc123" } ], "pagination": { "page": 1, "limit": 500, "total": 456 } }

SEO Health Endpoint

MethodEndpointPurpose
GET/api/seo/healthFull SEO status report — Claude calls this to audit the site without manual crawling.

Response Format — /api/seo/health

{ "success": true, "data": { "ssr": { "enabled": true, "prerenderRunning": true, "lastBotRequest": "2026-03-27T10:22:00Z" }, "sitemap": { "staticPages": 17, "liveShops": 38, "publishedProducts": 456, "blogPosts": 0, "totalUrls": 511, "lastGenerated": "2026-03-27T08:00:00Z" }, "robotsTxt": { "exists": true, "blockedPaths": ["/dashboard/", "/admin/", "/api/", "/auth/"] }, "gscTag": { "present": true, "tag": "ixzjWs_TY..." }, "metaTags": { "pagesWithTitle": 511, "pagesWithDescription": 498, "pagesWithCanonical": 511, "pagesWithSchema": 456, "pagesMissingMeta": 13 } } }

Why this endpoint matters: Instead of crawling the site externally (which can miss JS-rendered content), Claude calls one endpoint and gets a complete picture of what's working. This replaces the manual audit we did on Mar 27 — catches problems before Google does.

Vendor List Endpoint (needed for sitemap + audits)

MethodEndpointPurpose
GET/api/vendorsList all vendors with basic info: vendorId, slug, businessName, plan, status (live/draft), productCount, createdAt. Paginated. Needed for sitemap generation and SEO audits across all vendors.
GET/api/vendors?status=liveFilter to only live/published vendors — the ones that should appear in sitemaps.

Note: Section 7 currently only has /api/vendors/:vendorId (single vendor lookup). This adds the list-all endpoint needed for sitemap generation and cross-vendor SEO audits.

12. Rate Limits

These can be adjusted later — the important thing is they exist from day one.

LimitRecommended Value
Requests per second10
Requests per minute300
Max concurrent async jobs20
Max items per batch create50
Max items per bulk-delete100 (with confirm_count)
Max items per bulk-patch100

Rate limit responses should return HTTP 429 with a Retry-After header (seconds).

13. Access Summary

Claude CAN DoClaude CANNOT Do
Read full catalog and vendor storesTouch the codebase or server
Create and auto-publish products (single or batch)Delete an entire store
Browse, search, and select from media libraryDelete an entire blog or category structure
Upload new files to vendor media libraryUse PUT (full object replacement) on any endpoint
PATCH existing products when instructedRun wildcard bulk operations without explicit IDs
Delete products — with confirm param for live itemsAccess admin-only routes
Bulk delete/edit with explicit ID arraysExecute a bulk operation without auto-snapshot
Write SEO metadata on any product, page, or postOverride existing vendor customizer products
Create, edit, delete blog posts and pagesModify the codebase, deploy code, or access the server
Update vendor store settings and layout (DFY VIP)Create or modify user accounts or permissions
Manage vendor product categoriesAccess payment or billing data
Read onboarding submissions, download files, mark completeBypass rate limits or confirmation requirements
Revert bulk operations via snapshot systemCall any endpoint not listed in this document
Generate sitemaps from /api/sitemap/* dataModify server-level sitemap or robots.txt files directly
Run SEO health checks via /api/seo/healthChange SSR/prerender configuration
List all vendors and filter by statusCreate or delete vendor accounts

14. Two-Tier Access Model

Build with two distinct access modes from the start to avoid retrofitting role logic later.

Staff Tier — Team Access

Admin Tier — Owner Access Only

Tier is determined by the key's associated account role — no separate credentials needed. Admin-tier endpoints return 403 when called by a staff-tier key.

15. Open Question

Blog Post Scoping

Clarify: Are blog posts global (platform-level) or per-vendor? If per-vendor, blog endpoints need to accept a vendorId filter.

16. Implementation Priority

Not everything needs to ship at once. Recommended build order based on what unblocks the most staff work first:

1 Phase 1 — Core Product Workflow

  • Authentication (API key system)
  • Health endpoint
  • Catalog read endpoints
  • Product CRUD (single create + poll + publish)
  • Media library (list, upload, select)
  • Product exists check
  • Standard response envelope

2 Phase 2 — DFY VIP Workflow

  • Batch product creation + polling
  • Vendor settings and layout PATCH
  • Vendor category management
  • Vendor onboarding (read submissions, download files, mark complete)
  • Vendor logo retrieval

3 Phase 3 — Content & SEO

  • Blog CRUD (posts, categories, pages)
  • SEO read/write endpoints
  • SEO audit endpoint
  • Bulk blog operations

4 Phase 4 — Safety & Scale

  • Snapshot system (auto-snapshot + revert)
  • Bulk delete with confirmation
  • Bulk patch
  • Rate limiting
  • Idempotency key support

5 Phase 5 — SEO Infrastructure & Monitoring

  • Vendor list endpoint (GET /api/vendors with status filter)
  • Sitemap data endpoints (shops, products, stats)
  • SEO health endpoint (SSR status, meta coverage, sitemap counts)
  • Enables Claude to auto-generate sitemaps and run SEO audits via API instead of external crawling

Phase 1 Alone Unblocks the Highest-Volume Task

Product creation is the bottleneck. Phase 1 eliminates it. Each subsequent phase adds capability without requiring changes to prior phases. Phase 5 is independent — it can be built alongside any other phase since it only reads existing data.

then initialize with gtag("config", "G-W73EBH63Q9"). Must load on every page including vendor shops and product pages.', priority: 'P1', effort: 'Tiny', category: 'shops', section: 'seo-vendor' }, { id: 'shop-blog-nav', name: 'Add "Blog" link to site navigation', desc: 'Add a "Blog" link in the main nav and/or footer pointing to /blog/. Blog hub page is already live with 6,172 articles organized by category.', priority: 'P1', effort: 'Tiny', category: 'shops', section: 'blog-architecture' }, { id: 'shop-affiliate-subid', name: 'Fix affiliate link sub_id=undefined bug', desc: 'GSC found /?ref=wh3qdqgUeeMCb&sub_id=undefined — the affiliate referral link is generating sub_id=undefined instead of a real value. Check the JS that builds affiliate URLs.', priority: 'P2', effort: 'Small', category: 'shops', section: 'seo-vendor' }, // ── SECURITY (all remain) ── { id: 'sec-stripe-webhook', name: 'Verify Stripe webhook signatures', desc: 'Use stripe.webhooks.constructEvent() with signing secret on ALL webhook endpoints. Reject any unverified events.', priority: 'P0', effort: 'Medium', category: 'security', section: 'security' }, { id: 'sec-stripe-serverside', name: 'Server-side price validation', desc: 'Never trust prices from the frontend. Always fetch base prices from DB server-side when processing orders/subscriptions.', priority: 'P0', effort: 'Medium', category: 'security', section: 'security' }, { id: 'sec-stripe-keys', name: 'Restrict Stripe API keys', desc: 'Use restricted API keys with minimal permissions. Verify sk_live_ is NEVER in frontend code. Store in .env only.', priority: 'P0', effort: 'Small', category: 'security', section: 'security' }, { id: 'sec-cookies', name: 'Secure session cookies (httpOnly, secure, sameSite)', desc: 'Set httpOnly: true, secure: true, sameSite: strict on all session cookies. Never store tokens in localStorage.', priority: 'P0', effort: 'Medium', category: 'security', section: 'security' }, { id: 'sec-csrf', name: 'Add CSRF protection to all forms', desc: 'Add CSRF tokens to login, signup, plan changes, shop edits, and all state-changing endpoints.', priority: 'P0', effort: 'Medium', category: 'security', section: 'security' }, { id: 'sec-passwords', name: 'Password hashing with bcrypt/argon2', desc: 'Verify passwords are hashed with bcrypt (cost 12+) or argon2. Never plaintext, MD5, or SHA.', priority: 'P0', effort: 'Small', category: 'security', section: 'security' }, { id: 'sec-login-ratelimit', name: 'Rate limit login attempts', desc: '5 failed attempts → 15 min lockout per IP + per account. Log all failed attempts with IP and timestamp.', priority: 'P0', effort: 'Medium', category: 'security', section: 'security' }, { id: 'sec-nginx-headers', name: 'Add security headers to Nginx', desc: 'X-Frame-Options DENY, X-Content-Type-Options nosniff, HSTS, CSP, Permissions-Policy. See Security section for full config.', priority: 'P0', effort: 'Small', category: 'security', section: 'security' }, { id: 'sec-https-redirect', name: 'Force HTTPS redirect', desc: 'All HTTP requests to shops.beargrips.com must 301 redirect to HTTPS. Add server block in Nginx.', priority: 'P0', effort: 'Tiny', category: 'security', section: 'security' }, { id: 'sec-api-ratelimit', name: 'Rate limit all API routes', desc: '30 req/min per IP on general API routes. Stricter limits on auth endpoints. Return 429 when exceeded.', priority: 'P0', effort: 'Medium', category: 'security', section: 'security' }, { id: 'sec-input-validation', name: 'Sanitize all user inputs', desc: 'Validate/sanitize vendor names, shop descriptions, product titles. Use parameterized DB queries. Never use dangerouslySetInnerHTML with user content.', priority: 'P0', effort: 'Medium', category: 'security', section: 'security' }, { id: 'sec-file-uploads', name: 'Secure file uploads → Cloudflare R2', desc: 'R2 bucket "proshopuploads" is LIVE — credentials in Security section. Install @aws-sdk/client-s3, add .env vars, validate magic bytes, 10MB limit, UUID rename, upload from backend only. Starter code provided in Security section. Default mockups from Printify/Printful stay on their CDN — R2 is only for vendor-uploaded logos, designs, and custom mockups (up to 4 per product).', priority: 'P1', effort: 'Medium', category: 'security', section: 'security' }, { id: 'sec-env-vars', name: 'Audit environment variables', desc: 'Verify all API keys, DB creds, Stripe keys are in .env and .gitignore. Use different keys for dev vs production.', priority: 'P0', effort: 'Small', category: 'security', section: 'security' }, { id: 'sec-db-backups', name: 'Set up automated daily DB backups', desc: 'Daily automated backups stored off-server (S3, DO Spaces). Test restore process at least once before launch.', priority: 'P1', effort: 'Medium', category: 'security', section: 'security' }, { id: 'sec-sentry', name: 'Set up Sentry error logging', desc: 'Account ready: proshops@beargrips.com. Log in, create Node.js project, copy DSN, install @sentry/node, add 5 lines to Express app. See Security section.', priority: 'P1', effort: 'Small', category: 'security', section: 'security' }, { id: 'sec-verify-headers', name: 'Verify security headers grade', desc: 'Run securityheaders.com — aim for A or A+ grade. Run ssllabs.com/ssltest — aim for A grade.', priority: 'P1', effort: 'Tiny', category: 'security', section: 'security' }, ]; const STORAGE_KEY = 'beargrips-devhub-tasks'; function getTaskState() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; } catch(e) { return {}; } } function saveTaskState(state) { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } let currentFilter = 'all'; function filterTasks(filter) { currentFilter = filter; document.querySelectorAll('.tracker-filter').forEach(b => b.classList.remove('active')); event.target.classList.add('active'); renderTasks(); } function toggleTask(id) { const state = getTaskState(); state[id] = !state[id]; saveTaskState(state); renderTasks(); } function resetTasks() { if (confirm('Reset all tasks to incomplete? This cannot be undone.')) { localStorage.removeItem(STORAGE_KEY); renderTasks(); } } function renderTasks() { const state = getTaskState(); const list = document.getElementById('task-list'); if (!list) return; let filtered = TASKS; if (currentFilter === 'pending') filtered = TASKS.filter(t => !state[t.id]); else if (currentFilter === 'done') filtered = TASKS.filter(t => state[t.id]); else if (currentFilter === 'shops') filtered = TASKS.filter(t => t.category === 'shops'); else if (currentFilter === 'free-tools') filtered = TASKS.filter(t => t.category === 'free-tools'); else if (currentFilter === 'catalog') filtered = TASKS.filter(t => t.category === 'catalog'); else if (currentFilter === 'security') filtered = TASKS.filter(t => t.category === 'security'); // Sort: incomplete first, then by priority const pOrder = { P0: 0, P1: 1, P2: 2, P3: 3 }; filtered.sort((a, b) => { const aDone = state[a.id] ? 1 : 0; const bDone = state[b.id] ? 1 : 0; if (aDone !== bDone) return aDone - bDone; return (pOrder[a.priority] || 9) - (pOrder[b.priority] || 9); }); list.innerHTML = filtered.map(t => `
${t.name}
${t.desc}
${t.priority} ${t.effort} ${t.category === 'free-tools' ? 'Free Tools' : t.category === 'catalog' ? 'Product Catalog' : t.category === 'security' ? 'Security' : 'Shops SEO'}
View details →
`).join(''); // Update progress const done = TASKS.filter(t => state[t.id]).length; const total = TASKS.length; document.getElementById('tracker-done').textContent = done; document.getElementById('tracker-total').textContent = total; document.getElementById('tracker-bar').style.width = total > 0 ? Math.round((done / total) * 100) + '%' : '0%'; } // Init task tracker renderTasks();