[object Object]

← back to Wallco Ai

feat(scenic+studio): sub-brand rename — product_line column + UI chips + /scenic + /studio routes

b0577b9d8f1ff84180104250babcaee3152f07c7 · 2026-05-21 20:01:58 -0700 · Steve Abrams

Per Steve approval 2026-05-21 of the sub-brand proposal at
docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md. Names locked: Scenic +
Studio. Trade-pricing gate deferred (separate decision).

Schema (both local + Kamatera):
- ALTER TABLE all_designs ADD COLUMN IF NOT EXISTS product_line TEXT
- Index on product_line WHERE NOT NULL
- Backfill:
    cactus-11ft-mural   → 'Scenic'
    tree-mural          → 'Scenic'
    cactus-pine-scenic  → 'Studio'
- Local view spoon_all_designs recreated to expose the new column

Server.js:
- New productLine(d) helper near designHasImage() — reads PG column when
  present, falls back to the category→line mapping in-memory. Source of
  truth is PG; helper is the in-memory mirror for the JSON-backed cache.
- /designs accepts ?line=Scenic|Studio (case-insensitive) and filters the
  catalog accordingly. Combines cleanly with existing cat/motif/hue/q
  filters.
- New routes /scenic and /studio — 302 redirect to /designs?line=Scenic
  and ?line=Studio respectively. SEO-stable per-line entry points,
  bookmarkable, social-share-friendly.
- PDP gets a pill chip immediately under the title: "wallco / Scenic —
  Photoreal hospitality mural" or "wallco / Studio — Hand-painted scenic
  register". Chip links to the line's landing URL.
- Grid card gets a smaller line chip in the card-tags row alongside the
  Curated-Designed + style badges. Color-coded: indigo for Studio,
  burnt-umber for Scenic. Same palette as #39055.

Live counts (prod):
  Scenic: 121 designs, 90 published
  Studio: 44 designs, 24 published

Smoke verified:
  /scenic        → 302 → /designs?line=Scenic    page=200
  /studio        → 302 → /designs?line=Studio   page=200
  /design/39055  page=200, "wallco / Studio" chip in HTML
  /design/38842  page=200, "wallco / Scenic" chip in HTML

Not in scope (carried for future):
- Per-line landing-page hero copy ("Photoreal hospitality murals —
  Phillip Jeffries quality, AI-scale catalog" / "Hand-painted-silk
  scenic in the Gracie / de Gournay register at 60% of the price")
- Trade-pricing gate on Studio products
- Sub-tags inside Scenic by hospitality brand fit (Aman/Auberge/Four
  Seasons style packs)
- DESIGNS.json refresh to include product_line column on each row (the
  helper covers this in-memory; refreshing the JSON is a one-line PG
  export script + restart, can do whenever)

Files touched

Diff

commit b0577b9d8f1ff84180104250babcaee3152f07c7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu May 21 20:01:58 2026 -0700

    feat(scenic+studio): sub-brand rename — product_line column + UI chips + /scenic + /studio routes
    
    Per Steve approval 2026-05-21 of the sub-brand proposal at
    docs/SUB-BRAND-PROPOSAL-SCENIC-VS-STUDIO.md. Names locked: Scenic +
    Studio. Trade-pricing gate deferred (separate decision).
    
    Schema (both local + Kamatera):
    - ALTER TABLE all_designs ADD COLUMN IF NOT EXISTS product_line TEXT
    - Index on product_line WHERE NOT NULL
    - Backfill:
        cactus-11ft-mural   → 'Scenic'
        tree-mural          → 'Scenic'
        cactus-pine-scenic  → 'Studio'
    - Local view spoon_all_designs recreated to expose the new column
    
    Server.js:
    - New productLine(d) helper near designHasImage() — reads PG column when
      present, falls back to the category→line mapping in-memory. Source of
      truth is PG; helper is the in-memory mirror for the JSON-backed cache.
    - /designs accepts ?line=Scenic|Studio (case-insensitive) and filters the
      catalog accordingly. Combines cleanly with existing cat/motif/hue/q
      filters.
    - New routes /scenic and /studio — 302 redirect to /designs?line=Scenic
      and ?line=Studio respectively. SEO-stable per-line entry points,
      bookmarkable, social-share-friendly.
    - PDP gets a pill chip immediately under the title: "wallco / Scenic —
      Photoreal hospitality mural" or "wallco / Studio — Hand-painted scenic
      register". Chip links to the line's landing URL.
    - Grid card gets a smaller line chip in the card-tags row alongside the
      Curated-Designed + style badges. Color-coded: indigo for Studio,
      burnt-umber for Scenic. Same palette as #39055.
    
    Live counts (prod):
      Scenic: 121 designs, 90 published
      Studio: 44 designs, 24 published
    
    Smoke verified:
      /scenic        → 302 → /designs?line=Scenic    page=200
      /studio        → 302 → /designs?line=Studio   page=200
      /design/39055  page=200, "wallco / Studio" chip in HTML
      /design/38842  page=200, "wallco / Scenic" chip in HTML
    
    Not in scope (carried for future):
    - Per-line landing-page hero copy ("Photoreal hospitality murals —
      Phillip Jeffries quality, AI-scale catalog" / "Hand-painted-silk
      scenic in the Gracie / de Gournay register at 60% of the price")
    - Trade-pricing gate on Studio products
    - Sub-tags inside Scenic by hospitality brand fit (Aman/Auberge/Four
      Seasons style packs)
    - DESIGNS.json refresh to include product_line column on each row (the
      helper covers this in-memory; refreshing the JSON is a one-line PG
      export script + restart, can do whenever)
---
 server.js | 49 +++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 49 insertions(+)

diff --git a/server.js b/server.js
index 04f8cc3..0bc789b 100644
--- a/server.js
+++ b/server.js
@@ -3506,6 +3506,11 @@ app.post('/login', express.urlencoded({ extended: false, limit: '2kb' }), (req,
   res.redirect('/login');
 });
 
+// Sub-brand entry points — clean URLs that redirect into /designs?line=…
+// so search engines + bookmarks have a stable path per product line.
+app.get('/scenic', (req, res) => res.redirect('/designs?line=Scenic'));
+app.get('/studio', (req, res) => res.redirect('/designs?line=Studio'));
+
 app.get('/designs', (req, res) => {
   // 2026-05-20 Steve directive: NO hard server gate. Allow 1 page browsing,
   // then a client-side scroll-triggered modal fades in requiring login.
@@ -3634,6 +3639,14 @@ app.get('/designs', (req, res) => {
   // to public /designs (live on /library admin-only instead).
   const _VENDOR_DENY = /\b(thibaut|koroseal|schumacher|maya[ -]romanoff|scalamandre|arte[ -]international|cole[ -]?(and|&)[ -]?son|phillip[ -]jeffries|osborne[ -]?(and|&)?[ -]?little|de[ -]gournay|fromental|sister[ -]parish|dedar|brewster|mind[ -]the[ -]gap|hygge|designtex|wolf[ -]gordon|carnegie|coordonn|daisy[ -]bennett|china[ -]seas|york|marburg|romo|fabricut|stout|knoll|arteriors|timorous[ -]beasties|hangups)\b/i;
   let filtered = DESIGNS.filter(d => designHasImage(d) && !_VENDOR_DENY.test(String(d.category || '')));
+  // Sub-brand filter — `?line=Scenic` or `?line=Studio`. Set automatically
+  // when the customer hits /scenic or /studio (those routes redirect here
+  // with the matching query). Case-insensitive accept.
+  const lineQ = String(req.query.line || '').trim();
+  if (lineQ) {
+    const want = lineQ.toLowerCase();
+    filtered = filtered.filter(d => String(productLine(d) || '').toLowerCase() === want);
+  }
   if (cat)    filtered = filtered.filter(d => d.category === cat);
   if (motifQ) filtered = filtered.filter(d => (d.motifs || []).includes(motifQ));
   if (hueQ)   filtered = filtered.filter(d => hueBucketOf(d.dominant_hex) === hueQ);
@@ -3767,6 +3780,14 @@ app.get('/designs', (req, res) => {
           <span class="card-tags">
             <span class="card-ai-badge" title="Curated-designed pattern">✦ Curated-Designed</span>
             <span class="card-style">${styleTag}</span>
+            ${(() => {
+              // Sub-brand chip on grid card. Only renders for Scenic + Studio
+              // designs; legacy categories stay unchipped.
+              const _line = productLine(d);
+              if (!_line) return '';
+              const _bg = _line === 'Studio' ? '#1E3550' : '#7A4A28';
+              return `<span class="card-line-chip" title="wallco / ${_line}" style="font-size:9px;letter-spacing:.16em;text-transform:uppercase;padding:2px 8px;background:${_bg};color:#F2EADB;border-radius:999px;font-weight:600">${_line}</span>`;
+            })()}
           </span>
         </div>
       </a>
@@ -8345,6 +8366,19 @@ ${htmlHeader('/designs')}
       </script>
       <h1 class="detail-title">${design.title}</h1>
 
+      ${(() => {
+        // Product-line chip — Scenic (photoreal hospitality murals) or
+        // Studio (flat hand-painted-silk Gracie register). Renders only
+        // for designs in those two lines; legacy categories stay unchipped.
+        const _line = productLine(design);
+        if (!_line) return '';
+        const _hue = _line === 'Studio'
+          ? { bg: '#1E3550', fg: '#F2EADB' }   // indigo on cream — Studio inky-luxe
+          : { bg: '#7A4A28', fg: '#F2EADB' };  // burnt-umber on cream — Scenic earthy
+        const _tag = _line === 'Studio' ? 'Hand-painted scenic register' : 'Photoreal hospitality mural';
+        return `<a href="/${_line.toLowerCase()}" class="product-line-chip" title="${_tag}" style="display:inline-flex;align-items:center;gap:8px;margin:6px 0 8px;padding:5px 12px 5px 10px;background:${_hue.bg};color:${_hue.fg};border-radius:999px;text-decoration:none;font:11px var(--sans,system-ui);font-weight:600;letter-spacing:.10em;text-transform:uppercase">wallco / ${_line} <span style="opacity:.7;font-weight:400;text-transform:none;letter-spacing:.02em">— ${_tag}</span></a>`;
+      })()}
+
       <div class="provenance-badge">
         ${_isAdmin ? `<span class="ai-badge">Curated-Original</span>` : ''}
         Pattern No.${String(design.id).padStart(4,'0')} &middot; ${(design.category || 'mixed').charAt(0).toUpperCase()+(design.category || 'mixed').slice(1)} &middot; ${design.kind === 'seamless_tile' ? 'Seamless Tile' : (design.kind || 'pattern')}
@@ -12245,6 +12279,21 @@ function imageFilesOnDisk() {
   }
   return _imgExistsCache;
 }
+// Product-line classifier — Steve directive 2026-05-21. Maps a design's
+// category to its sub-brand: 'Scenic' (photoreal hospitality murals) or
+// 'Studio' (flat hand-painted-silk Gracie/de Gournay register). Designs
+// outside these two categories return null and render with no line chip.
+// Source of truth is the product_line column in PG; this helper is the
+// in-memory mirror used by the JSON-file-backed DESIGNS array.
+function productLine(d) {
+  if (!d) return null;
+  if (d.product_line) return d.product_line;
+  const c = String(d.category || '');
+  if (c === 'cactus-11ft-mural' || c === 'tree-mural') return 'Scenic';
+  if (c === 'cactus-pine-scenic') return 'Studio';
+  return null;
+}
+
 function designHasImage(d) {
   if (!d) return false;
   if (BLANK_IDS.has(d.id)) return false;   // failed-generation blank — never show

← b6510d8 feat(studio): 12 new Gracie-language scenic compositions (St  ·  back to Wallco Ai  ·  generator: wire product_line column on INSERT + backfill des 28ef4d5 →