From b00714e404ad7046dff3d98fb859d19af035a137 Mon Sep 17 00:00:00 2001 From: Jay Jones Date: Tue, 28 Jul 2026 00:04:55 -0400 Subject: [PATCH] Add generated product pages --- index.html | 3 +- products.json | 26 ++++ products/index.html | 66 ++++++++++ .../index.html | 88 +++++++++++++ scripts/generate-ingredient-pages.js | 123 +++++++++++++++++- sitemap.xml | 12 ++ 6 files changed, 314 insertions(+), 4 deletions(-) create mode 100644 products.json create mode 100644 products/index.html create mode 100644 products/pit-boss-150-wood-pellet-grill-smoker/index.html diff --git a/index.html b/index.html index f080617..0b8fe8f 100644 --- a/index.html +++ b/index.html @@ -657,13 +657,14 @@

Seasoning Guides

-

Practical spice basics and BBQ prep notes before you start moving sliders.

+

Practical spice basics, BBQ prep notes, and gear picks before you start moving sliders.

5 Basic Spices Seasoning Pairings Brisket Rub Timing Rub Binders + Gear Picks
diff --git a/products.json b/products.json new file mode 100644 index 0000000..61039c8 --- /dev/null +++ b/products.json @@ -0,0 +1,26 @@ +[ + { + "id": "pit-boss-150-pellet-grill-smoker", + "slug": "pit-boss-150-wood-pellet-grill-smoker", + "title": "Pit Boss 150 Wood Pellet Grill and Smoker", + "category": "Smokers", + "categories": [ + "Smokers", + "Pellet Grills", + "Portable Grills" + ], + "productType": "Portable Pellet Grill", + "description": "Portable pellet grill and smoker with a compact cooking surface for tailgating, camping, travel, and smaller backyard cooks.", + "affiliateUrl": "https://amzn.to/3RC5q87", + "imageUrl": null, + "imageAlt": null, + "brand": "Pit Boss", + "features": [ + "Portable pellet grill format for smaller spaces and travel.", + "Useful for testing rubs, smoking smaller cuts, and cooking for a small group.", + "Add a product image URL later to replace the generated placeholder." + ], + "changefreq": "monthly", + "priority": "0.5" + } +] diff --git a/products/index.html b/products/index.html new file mode 100644 index 0000000..7f5b3bf --- /dev/null +++ b/products/index.html @@ -0,0 +1,66 @@ + + + + + +BBQ and Grilling Product Picks | PitBlend + + + + + + + +
+ +
+
+

Gear Picks

+

BBQ and Grilling Product Picks

+

Browse PitBlend product picks for grills, smokers, pellets, brushes, jars, and outdoor cooking gear. Product pages are generated from products.json.

+
Pit Boss 150 Wood Pellet Grill and SmokerSmokers - Portable pellet grill and smoker with a compact cooking surface for tailgating, camping, travel, and smaller backyard cooks.
+
+ + diff --git a/products/pit-boss-150-wood-pellet-grill-smoker/index.html b/products/pit-boss-150-wood-pellet-grill-smoker/index.html new file mode 100644 index 0000000..d714dcb --- /dev/null +++ b/products/pit-boss-150-wood-pellet-grill-smoker/index.html @@ -0,0 +1,88 @@ + + + + + +Pit Boss 150 Wood Pellet Grill and Smoker | PitBlend Gear Picks + + + + + + + +
+ +
+
+

Smokers

+

Pit Boss 150 Wood Pellet Grill and Smoker

+

Portable pellet grill and smoker with a compact cooking surface for tailgating, camping, travel, and smaller backyard cooks.

+
+
+
Image pending
+
+ +
+

Why It Is Listed

+
  • Portable pellet grill format for smaller spaces and travel.
  • +
  • Useful for testing rubs, smoking smaller cuts, and cooking for a small group.
  • +
  • Add a product image URL later to replace the generated placeholder.
+
+
+
+ + diff --git a/scripts/generate-ingredient-pages.js b/scripts/generate-ingredient-pages.js index 3dcc6fc..8a1e381 100644 --- a/scripts/generate-ingredient-pages.js +++ b/scripts/generate-ingredient-pages.js @@ -6,7 +6,9 @@ const ROOT = path.resolve(__dirname, ".."); const INGREDIENTS_PATH = path.join(ROOT, "ingredients.json"); const RECIPES_INDEX_PATH = path.join(ROOT, "recipes", "index.json"); const ARTICLES_INDEX_PATH = path.join(ROOT, "articles", "index.json"); +const PRODUCTS_PATH = path.join(ROOT, "products.json"); const INGREDIENTS_DIR = path.join(ROOT, "ingredients"); +const PRODUCTS_DIR = path.join(ROOT, "products"); const SITEMAP_PATH = path.join(ROOT, "sitemap.xml"); const FLAVOR_LABELS = { @@ -93,6 +95,31 @@ function loadArticles() { })); } +function loadProducts() { + if (!fs.existsSync(PRODUCTS_PATH)) return []; + const entries = readJson(PRODUCTS_PATH); + if (!Array.isArray(entries)) return []; + + return entries + .filter(product => product && product.title && product.affiliateUrl) + .map(product => { + const categories = Array.isArray(product.categories) && product.categories.length + ? product.categories + : [product.category || "Grill Gear"]; + return { + ...product, + slug: product.slug || slugify(product.title), + categories, + category: categories[0], + description: product.description || "Recommended outdoor cooking gear for BBQ and seasoning prep.", + imageUrl: product.imageUrl || null, + changefreq: product.changefreq || "monthly", + priority: product.priority || "0.5" + }; + }) + .filter(product => product.slug); +} + function buildSlugMap(ingredients) { const used = new Set(); const map = {}; @@ -168,6 +195,9 @@ h1{font-family:Georgia,serif;font-size:clamp(2.1rem,5vw,4rem);line-height:.98;ma .ingredient-link{display:block;text-decoration:none;background:var(--panel);border:1px solid var(--line);border-radius:8px;padding:14px} .ingredient-link strong{display:block;margin-bottom:3px} .ingredient-link span{color:var(--muted);font-size:.86rem} +.product-media{min-height:260px;display:flex;align-items:center;justify-content:center;overflow:hidden} +.product-media img{display:block;max-width:100%;height:auto;border-radius:8px} +.product-placeholder{width:100%;min-height:220px;border:1px dashed var(--line);border-radius:8px;display:flex;align-items:center;justify-content:center;color:var(--faint);text-transform:uppercase;letter-spacing:.08em;font-size:.78rem;font-weight:800;background:var(--panel2)} @@ -249,7 +279,67 @@ function ingredientIndexPage(ingredients, slugMap) { }); } -function writeSitemap(slugMap, articles) { +function productPage(product) { + const image = product.imageUrl + ? `${escapeAttr(product.imageAlt || product.title)}` + : `
Image pending
`; + const features = Array.isArray(product.features) && product.features.length + ? `` + : `

More product notes can be added in products.json.

`; + const body = `
+

${escapeHtml(product.category)}

+

${escapeHtml(product.title)}

+

${escapeHtml(sentence(product.description))}

+
+
+ ${image} +
+ +
+

Why It Is Listed

+ ${features} +
+
+
`; + + return pageShell({ + title: `${product.title} | PitBlend Gear Picks`, + description: `${product.title}. ${sentence(product.description)} Browse PitBlend BBQ, grilling, smoker, and seasoning gear picks.`, + canonicalPath: `/products/${product.slug}/`, + body + }); +} + +function productIndexPage(products) { + const links = products + .sort((a, b) => a.title.localeCompare(b.title)) + .map(product => `${escapeHtml(product.title)}${escapeHtml(product.category)} - ${escapeHtml(product.description)}`) + .join("\n"); + const body = `
+

Gear Picks

+

BBQ and Grilling Product Picks

+

Browse PitBlend product picks for grills, smokers, pellets, brushes, jars, and outdoor cooking gear. Product pages are generated from products.json.

+
${links}
+
`; + + return pageShell({ + title: "BBQ and Grilling Product Picks | PitBlend", + description: "Browse PitBlend product picks for grills, smokers, pellets, brushes, jars, and outdoor cooking gear with affiliate product links.", + canonicalPath: "/products/", + body + }); +} + +function writeSitemap(slugMap, articles, products) { const ingredientUrls = Object.values(slugMap) .sort() .map(slug => ` @@ -267,6 +357,20 @@ function writeSitemap(slugMap, articles) { `) .join("\n"); + const productIndexUrl = products.length ? ` + ${SITE_URL}/products/ + weekly + 0.6 + ` : ""; + + const productUrls = products + .map(product => ` + ${SITE_URL}/products/${product.slug}/ + ${product.changefreq} + ${product.priority} + `) + .join("\n"); + const sitemap = ` @@ -281,6 +385,8 @@ function writeSitemap(slugMap, articles) { ${ingredientUrls} ${articleUrls ? "\n" + articleUrls : ""} +${productIndexUrl ? "\n" + productIndexUrl : ""} +${productUrls ? "\n" + productUrls : ""} `; fs.writeFileSync(SITEMAP_PATH, sitemap, "utf8"); @@ -290,6 +396,7 @@ function main() { const ingredients = readJson(INGREDIENTS_PATH); const recipes = loadRecipes(); const articles = loadArticles(); + const products = loadProducts(); const slugMap = buildSlugMap(ingredients); fs.mkdirSync(INGREDIENTS_DIR, { recursive: true }); @@ -303,8 +410,18 @@ function main() { fs.writeFileSync(path.join(dir, "index.html"), ingredientPage(key, ingredient, slug, recipesUsingIngredient), "utf8"); }); - writeSitemap(slugMap, articles); - console.log(`Generated ${Object.keys(ingredients).length} ingredient pages and sitemap.xml`); + if (products.length) { + fs.mkdirSync(PRODUCTS_DIR, { recursive: true }); + fs.writeFileSync(path.join(PRODUCTS_DIR, "index.html"), productIndexPage(products), "utf8"); + products.forEach(product => { + const dir = path.join(PRODUCTS_DIR, product.slug); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(path.join(dir, "index.html"), productPage(product), "utf8"); + }); + } + + writeSitemap(slugMap, articles, products); + console.log(`Generated ${Object.keys(ingredients).length} ingredient pages, ${products.length} product pages and sitemap.xml`); } main(); diff --git a/sitemap.xml b/sitemap.xml index ef100a5..69f52c0 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -261,4 +261,16 @@ monthly 0.6 + + + https://pitblend.com/products/ + weekly + 0.6 + + + + https://pitblend.com/products/pit-boss-150-wood-pellet-grill-smoker/ + monthly + 0.5 +