← back to Wallco Ai
footer: add visible phone, DW corp address, and © 2026 line
2ac2375d6f74531facd378e35c48a73287c43320 · 2026-05-13 14:54:10 -0700 · Steve Abrams
DW standing rule — every public site's visible footer must surface
info@wallco.ai mailto, 888-373-4564 phone, 15442 Ventura Bl #102 Sherman
Oaks CA 91403 address, and current-year copyright. Address was already
in JSON-LD schema; this lifts it into human-visible HTML.
Files touched
M public/css/site.cssM server.js
Diff
commit 2ac2375d6f74531facd378e35c48a73287c43320
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed May 13 14:54:10 2026 -0700
footer: add visible phone, DW corp address, and © 2026 line
DW standing rule — every public site's visible footer must surface
info@wallco.ai mailto, 888-373-4564 phone, 15442 Ventura Bl #102 Sherman
Oaks CA 91403 address, and current-year copyright. Address was already
in JSON-LD schema; this lifts it into human-visible HTML.
---
public/css/site.css | 18 ++++
server.js | 295 ++++++++++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 303 insertions(+), 10 deletions(-)
diff --git a/public/css/site.css b/public/css/site.css
index 6031d97..7f8ac2c 100644
--- a/public/css/site.css
+++ b/public/css/site.css
@@ -814,6 +814,24 @@ main { padding-top: 73px; }
margin: 0 0 6px;
}
.site-footer a { color: var(--ink-soft); }
+.site-footer .footer-address {
+ font-size: 13px;
+ font-style: normal;
+ color: var(--ink-faint);
+ margin: 0 0 6px;
+ letter-spacing: 0.02em;
+}
+.site-footer .footer-contact span { padding: 0 8px; color: var(--ink-faint); opacity: .55; }
+.site-footer .footer-legal {
+ margin-top: 14px;
+ padding-top: 14px;
+ border-top: 1px solid var(--line);
+ font-size: 11px;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--ink-faint);
+ display: inline-block;
+}
/* ── EMPTY STATE */
.empty-state { padding: 60px; text-align: center; color: var(--ink-faint); }
diff --git a/server.js b/server.js
index b713aa7..e0f2ab3 100644
--- a/server.js
+++ b/server.js
@@ -84,6 +84,11 @@ app.use(compression({
}
}));
+// ── Marketplace early-mount: the Shopify orders/fulfilled webhook MUST register
+// before the global express.json() so the raw request body is preserved for
+// HMAC verification (express.raw runs on that route only).
+try { require('./src/marketplace').mountEarly(app); } catch (e) { console.error('Marketplace early mount failed:', e.message); }
+
app.use(express.json({ limit: '25mb' })); // 25mb to accept full-res baked PNGs from /api/design/:id/bake
app.use(express.urlencoded({ extended: false }));
@@ -145,7 +150,7 @@ app.get('/robots.txt', (req, res) => {
res.setHeader('Cache-Control', 'public, max-age=86400, must-revalidate');
if (maybe304(req, res, ROBOTS_LAST_MODIFIED)) return;
res.type('text/plain').send(
- `User-agent: *\nAllow: /\nDisallow: /admin/\nDisallow: /api/\nDisallow: /_devlogin\n\nSitemap: https://${SITE}/sitemap.xml\n`
+ `User-agent: *\nAllow: /\nDisallow: /admin/\nDisallow: /api/\nDisallow: /_devlogin\n\nSitemap: https://${SITE}/sitemap.xml\nSitemap: https://${SITE}/sitemap-marketplace.xml\n`
);
});
@@ -2395,7 +2400,15 @@ function htmlHeader(active) {
const FOOTER = `<footer class="site-footer">
<p>wallco.ai — AI-original wallpaper & murals. Every pattern generated, never repeated.</p>
<p>Part of the <a href="https://designerwallcoverings.com">Designer Wallcoverings</a> family.</p>
- <p><a href="mailto:info@wallco.ai">info@wallco.ai</a></p>
+ <p class="footer-contact">
+ <a href="mailto:info@wallco.ai">info@wallco.ai</a>
+ <span aria-hidden="true">·</span>
+ <a href="tel:+18883734564">888-373-4564</a>
+ </p>
+ <address class="footer-address">
+ Designer Wallcoverings · 15442 Ventura Bl #102 · Sherman Oaks, CA 91403
+ </address>
+ <p class="footer-legal">© ${new Date().getFullYear()} Designer Wallcoverings. Memo samples are always free.</p>
</footer>
<script src="/js/wallco-game.js" defer></script>
<!-- Mobile tap-target minimums (Apple HIG / WCAG 2.5.5) — applied site-wide.
@@ -7471,8 +7484,13 @@ ${htmlHeader('/designs')}
<button type="button" class="btn-outline" id="btn-room" data-id="${design.id}">See in a Room</button>
<button type="button" class="btn-outline" id="btn-spoon-dry" data-id="${design.id}">Spoonflower (dry-run)</button>
<button type="button" class="btn-outline" id="btn-spoon-live" data-id="${design.id}">Spoonflower — Upload as New</button>
+ <button type="button" class="btn-outline" id="btn-shopify" data-id="${design.id}">Upload to Shopify</button>
<button type="button" class="btn-outline" id="btn-narrate" data-id="${design.id}">Narrate (ElevenLabs)</button>
</div>
+ <div style="margin-top:8px;font:11px/1.4 var(--sans);color:#666">
+ DW Original Number:
+ <code style="font:12px ui-monospace,Menlo,monospace;background:rgba(0,0,0,.05);padding:2px 6px;border-radius:3px">${design.dig_number || 'DIG_AI_' + String(design.id).padStart(6,'0')}</code>
+ </div>
<div id="tool-result" style="margin-top:16px;display:none;padding:14px;border-radius:6px;background:rgba(0,0,0,.05);font-size:13px;line-height:1.5"></div>
<div id="room-result" style="margin-top:14px;display:none">
<img id="room-img" alt="Room preview" style="max-width:100%;border-radius:8px;border:1px solid rgba(0,0,0,.08)">
@@ -7519,14 +7537,89 @@ ${htmlHeader('/designs')}
document.getElementById('btn-spoon-live').addEventListener('click', async function(){
if (!confirm('Upload this design to Spoonflower as a NEW product? This is a public publish.')) return;
- show('Live upload running…', false);
- this.disabled = true;
- var j = await post('/api/spoonflower/upload', { design_id: this.dataset.id, dry: false });
- this.disabled = false;
- if (j.ok) show('Upload kicked off. job=' + j.job_id + '. <a href="/api/spoonflower/status/' + j.job_id + '" target="_blank" rel="noopener noreferrer">view log</a>', false);
- else show('Upload failed: ' + (j.error||'unknown'), true);
+ var btn = this;
+ var designId = this.dataset.id;
+ show('Live upload running… (Spoonflower publish typically takes 60–180s)', false);
+ btn.disabled = true;
+ var j = await post('/api/spoonflower/upload', { design_id: designId, dry: false });
+ if (!j.ok) {
+ btn.disabled = false;
+ show('Upload failed: ' + (j.error||'unknown'), true);
+ return;
+ }
+ var jobId = j.job_id;
+ show('Upload kicked off (job ' + jobId + '). Polling for the new Spoonflower pattern number…', false);
+ // Poll /api/spoonflower/status/:job until the python script
+ // finishes. 5s cadence, hard cap at 6 minutes.
+ var started = Date.now();
+ var poll = setInterval(async function(){
+ try {
+ var r = await fetch('/api/spoonflower/status/' + jobId);
+ var s = await r.json();
+ var st = s && s.job && s.job.status;
+ if (st === 'done' || st === 'error') {
+ clearInterval(poll);
+ btn.disabled = false;
+ if (st === 'error') {
+ show('Spoonflower upload errored. <a href="/api/spoonflower/status/' + jobId + '" target="_blank" rel="noopener noreferrer">view log</a>', true);
+ return;
+ }
+ if (s.sf_design_id) {
+ var link = s.sf_public_url || ('https://www.spoonflower.com/en/shop/wallpaper/' + s.sf_design_id);
+ show(
+ '<strong>Spoonflower pattern #' + s.sf_design_id + '</strong> created. ' +
+ '<a href="' + link + '" target="_blank" rel="noopener noreferrer">View on Spoonflower ↗</a><br>' +
+ '<span style="font-size:12px;color:#3a3631">Next: click <b>Upload to Shopify</b> below to publish to designer-laboratory-sandbox.</span>',
+ false
+ );
+ var sb = document.getElementById('btn-shopify');
+ if (sb) { sb.style.borderColor = '#c9a14b'; sb.style.background = '#fff8e6'; }
+ } else {
+ show('Upload finished but Spoonflower pattern ID could not be parsed. <a href="/api/spoonflower/status/' + jobId + '" target="_blank" rel="noopener noreferrer">view log</a>', true);
+ }
+ return;
+ }
+ if (Date.now() - started > 6 * 60 * 1000) {
+ clearInterval(poll);
+ btn.disabled = false;
+ show('Upload still running after 6 min — check <a href="/api/spoonflower/status/' + jobId + '" target="_blank" rel="noopener noreferrer">job log</a> manually.', true);
+ }
+ } catch (e) { /* swallow transient poll errors */ }
+ }, 5000);
});
+ // Upload to Shopify — admin only (endpoint 404s for non-admins).
+ var shopifyBtn = document.getElementById('btn-shopify');
+ if (shopifyBtn) {
+ shopifyBtn.addEventListener('click', async function(){
+ if (!confirm('Create a new Shopify product on designer-laboratory-sandbox for this design? It will land as DRAFT.')) return;
+ var btn = this;
+ btn.disabled = true;
+ show('Creating Shopify product…', false);
+ try {
+ var r = await fetch('/api/design/' + btn.dataset.id + '/upload-shopify', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({})
+ });
+ var j = await r.json();
+ if (r.ok && j.ok) {
+ show(
+ '<strong>Shopify product ' + j.shopify_product_id + '</strong> created ' +
+ (j.already_uploaded ? '(already existed)' : '(DRAFT)') + '. ' +
+ 'SKU <code>' + (j.dig_number || '') + '</code>. ' +
+ '<a href="' + j.shopify_product_url + '" target="_blank" rel="noopener noreferrer">View on store ↗</a>' +
+ (j.shopify_admin_url ? ' · <a href="' + j.shopify_admin_url + '" target="_blank" rel="noopener noreferrer">admin ↗</a>' : ''),
+ false
+ );
+ } else {
+ show('Shopify upload failed: ' + (j.error || (j.shopify_response && JSON.stringify(j.shopify_response).slice(0,300)) || 'unknown'), true);
+ }
+ } catch (e) { show('Shopify upload error: ' + e.message, true); }
+ btn.disabled = false;
+ });
+ }
+
document.getElementById('btn-narrate').addEventListener('click', async function(){
show('Generating narration…', false);
this.disabled = true;
@@ -10585,17 +10678,180 @@ app.post('/api/spoonflower/upload', (req, res) => {
proc.stderr.pipe(out, { end: false });
proc.on('exit', code => {
out.end();
+ // Parse the JSON-line log so the new Spoonflower pattern number + URL
+ // can be persisted onto the design row. The python script emits one
+ // JSON object per line; the final `done` event carries sf_design_id
+ // and sf_public_url on a successful LIVE publish.
+ let sfDesignId = null, sfUrl = null;
try {
+ const raw = fs.readFileSync(logFile, 'utf8');
+ for (const line of raw.split('\n')) {
+ const s = line.trim();
+ if (!s.startsWith('{')) continue;
+ try {
+ const ev = JSON.parse(s);
+ if (ev && ev.sf_design_id) sfDesignId = String(ev.sf_design_id);
+ if (ev && ev.sf_public_url) sfUrl = String(ev.sf_public_url);
+ } catch {}
+ }
psqlQuery(`UPDATE spoonflower_jobs SET status='${code===0?'done':'error'}',
finished_at=now(),
- log='${fs.readFileSync(logFile,'utf8').replace(/'/g,"''").slice(-8000)}'
+ log='${raw.replace(/'/g,"''").slice(-8000)}'
WHERE id=${jobId};`);
+ if (code === 0 && !dry && sfDesignId) {
+ const did = parseInt(design_id, 10);
+ psqlQuery(`UPDATE spoon_all_designs SET
+ spoonflower_design_id = ${pgEsc(sfDesignId)},
+ spoonflower_url = ${pgEsc(sfUrl)},
+ spoonflower_uploaded_at = NOW()
+ WHERE id = ${did};`);
+ }
} catch {}
});
proc.unref();
res.json({ ok: true, job_id: jobId, dry, log_url: `/api/spoonflower/status/${jobId}` });
});
+// ── POST /api/design/:id/upload-shopify — push a wallco.ai design as a NEW Shopify product
+//
+// Admin-only. Reads the design row, picks the DW Original Number
+// (DIG_AI_xxxxxx — auto-assigned at INSERT-time by trg_spoon_all_designs_assign_dig)
+// as the SKU, and creates a draft Shopify product on
+// designer-laboratory-sandbox.myshopify.com with:
+// • title = ai_title || title
+// • vendor = "Designer Wallcoverings"
+// • product_type= "Wallpaper"
+// • tags = wallco-ai, wallco-original, <category>, <ai_pattern>
+// • SKU = dig_number
+// • image = base64 of local_path
+// • metafields = dwc.dig_number, dwc.spoonflower_url, dwc.wallco_design_id
+//
+// Persists shopify_product_id + shopify_product_url onto the design row so
+// /designs index and /design/:id can show the linkage.
+app.post('/api/design/:id/upload-shopify', requireAdmin, express.json({ limit: '16kb' }), async (req, res) => {
+ const id = parseInt(req.params.id, 10);
+ if (!Number.isFinite(id) || id < 1) return res.status(400).json({ error: 'bad id' });
+
+ const token = process.env.SHOPIFY_ADMIN_TOKEN;
+ const store = process.env.SHOPIFY_STORE;
+ if (!token || !store) {
+ return res.status(503).json({ error: 'SHOPIFY_ADMIN_TOKEN / SHOPIFY_STORE not configured' });
+ }
+
+ let d;
+ try {
+ const raw = psqlQuery(`SELECT row_to_json(t) FROM (
+ SELECT id, title, ai_title, ai_pattern, ai_color_name, category, tags,
+ dominant_hex, dig_number, local_path, spoonflower_design_id,
+ spoonflower_url, shopify_product_id, shopify_product_url
+ FROM spoon_all_designs WHERE id=${id}) t;`);
+ d = JSON.parse(raw || '{}');
+ } catch (e) { return res.status(500).json({ error: 'db: ' + e.message }); }
+ if (!d || !d.id) return res.status(404).json({ error: 'design not found' });
+
+ if (d.shopify_product_id) {
+ return res.json({
+ ok: true,
+ already_uploaded: true,
+ shopify_product_id: d.shopify_product_id,
+ shopify_product_url: d.shopify_product_url,
+ });
+ }
+
+ if (!d.local_path || !fs.existsSync(d.local_path)) {
+ return res.status(409).json({ error: 'image file missing on disk', local_path: d.local_path });
+ }
+
+ const dig = d.dig_number || `DIG_AI_${String(d.id).padStart(6, '0')}`;
+ const title = d.ai_title || d.title || `wallco.ai #${d.id}`;
+ const tagList = [
+ 'wallco-ai', 'wallco-original',
+ d.category, d.ai_pattern, d.ai_color_name,
+ ].filter(Boolean).join(', ');
+
+ const imgB64 = fs.readFileSync(d.local_path).toString('base64');
+ const imgExt = (path.extname(d.local_path) || '.png').replace('.', '');
+
+ const productBody = {
+ product: {
+ title,
+ vendor: 'Designer Wallcoverings',
+ product_type: 'Wallpaper',
+ status: 'draft',
+ tags: tagList,
+ body_html: `<p>An AI-original wallcovering pattern from wallco.ai. DW Original Number ${dig}.</p>`,
+ variants: [{
+ sku: dig,
+ price: '0.00',
+ inventory_management: null,
+ inventory_policy: 'continue',
+ }],
+ images: [{
+ attachment: imgB64,
+ filename: `${dig}.${imgExt}`,
+ alt: title,
+ }],
+ metafields: [
+ { namespace: 'dwc', key: 'dig_number', value: dig, type: 'single_line_text_field' },
+ { namespace: 'dwc', key: 'wallco_design_id', value: String(d.id), type: 'single_line_text_field' },
+ ...(d.spoonflower_url ? [
+ { namespace: 'dwc', key: 'spoonflower_url', value: d.spoonflower_url, type: 'url' },
+ ] : []),
+ ...(d.spoonflower_design_id ? [
+ { namespace: 'dwc', key: 'spoonflower_id', value: String(d.spoonflower_design_id), type: 'single_line_text_field' },
+ ] : []),
+ ],
+ }
+ };
+
+ const url = `https://${store}/admin/api/2026-01/products.json`;
+ let r, body;
+ try {
+ r = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'X-Shopify-Access-Token': token,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(productBody),
+ });
+ body = await r.json();
+ } catch (e) {
+ return res.status(502).json({ error: 'shopify fetch failed: ' + e.message });
+ }
+
+ if (!r.ok || !body || !body.product || !body.product.id) {
+ return res.status(502).json({
+ error: 'shopify create failed',
+ status: r.status,
+ shopify_response: body,
+ });
+ }
+
+ const sid = String(body.product.id);
+ const handle = body.product.handle || '';
+ const sUrl = `https://${store.replace('.myshopify.com','')}.myshopify.com/admin/products/${sid}`;
+ const publicUrl = handle ? `https://${store}/products/${handle}` : sUrl;
+
+ try {
+ psqlQuery(`UPDATE spoon_all_designs SET
+ shopify_product_id = ${pgEsc(sid)},
+ shopify_product_url = ${pgEsc(publicUrl)},
+ shopify_uploaded_at = NOW()
+ WHERE id = ${id};`);
+ } catch {}
+
+ res.json({
+ ok: true,
+ id,
+ dig_number: dig,
+ shopify_product_id: sid,
+ shopify_product_url: publicUrl,
+ shopify_admin_url: sUrl,
+ shopify_status: body.product.status,
+ });
+});
+
// ── POST /api/spoonflower/launch-chrome — open dedicated debug Chrome for cookie reuse
app.post('/api/spoonflower/launch-chrome', (req, res) => {
try {
@@ -10677,7 +10933,26 @@ app.get('/api/spoonflower/status/:job', (req, res) => {
const p = path.join(SF_JOBS_DIR, `${f}_${jobId}.log`);
if (fs.existsSync(p)) { log = fs.readFileSync(p, 'utf8'); break; }
}
- res.type('application/json').send(JSON.stringify({ ok:true, job: JSON.parse(row), log_tail: log.slice(-8000) }, null, 2));
+ // Extract the Spoonflower design id + URL from the JSON-line log so the
+ // /design/:id frontend can render "Spoonflower pattern #N → view" after
+ // polling without re-reading the raw log.
+ let sf_design_id = null, sf_public_url = null;
+ for (const line of log.split('\n')) {
+ const s = line.trim();
+ if (!s.startsWith('{')) continue;
+ try {
+ const ev = JSON.parse(s);
+ if (ev.sf_design_id) sf_design_id = String(ev.sf_design_id);
+ if (ev.sf_public_url) sf_public_url = String(ev.sf_public_url);
+ } catch {}
+ }
+ res.type('application/json').send(JSON.stringify({
+ ok: true,
+ job: JSON.parse(row),
+ sf_design_id,
+ sf_public_url,
+ log_tail: log.slice(-8000)
+ }, null, 2));
});
// ── GET /spoonflower — admin page: pulled DW library + per-design publish buttons
← a1c0a71 marketplace ai/generate-colorways: dedicated smoke test
·
back to Wallco Ai
·
marketplace SEO: add smoke tests for sitemap + designer/patt e092403 →