← back to Designer Wallcoverings
Host China Seas mailer vendor logos on Shopify CDN; swap 6 img src to permanent cdn.shopify.com URLs (unblocks logo-hosting check)
ec18eb1cda46d55aaf00a443bbdb78ba90a038cf · 2026-06-24 12:43:34 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M mailers/china-seas-launch.htmlA mailers/upload-vendor-logos.js
Diff
commit ec18eb1cda46d55aaf00a443bbdb78ba90a038cf
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Jun 24 12:43:34 2026 -0700
Host China Seas mailer vendor logos on Shopify CDN; swap 6 img src to permanent cdn.shopify.com URLs (unblocks logo-hosting check)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
mailers/china-seas-launch.html | 12 ++---
mailers/upload-vendor-logos.js | 102 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 108 insertions(+), 6 deletions(-)
diff --git a/mailers/china-seas-launch.html b/mailers/china-seas-launch.html
index b0fcec96..cddf3cd3 100644
--- a/mailers/china-seas-launch.html
+++ b/mailers/china-seas-launch.html
@@ -161,34 +161,34 @@
<tr>
<td class="stack" width="33%" valign="middle" align="center" style="padding: 14px 8px;">
<a href="https://designerwallcoverings.com/collections/china-seas?utm_source=email&utm_medium=mailer&utm_campaign=china-seas-launch" target="_blank">
- <img src="https://designerwallcoverings.com/vendor-logos/china-seas.png" alt="China Seas" width="150" style="width:100%; max-width:150px; height:auto; margin:0 auto;">
+ <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/china-seas.webp?v=1782330141" alt="China Seas" width="150" style="width:100%; max-width:150px; height:auto; margin:0 auto;">
</a>
</td>
<td class="stack" width="33%" valign="middle" align="center" style="padding: 14px 8px;">
<a href="https://designerwallcoverings.com/collections/quadrille-wallcoverings?utm_source=email&utm_medium=mailer&utm_campaign=china-seas-launch" target="_blank">
- <img src="https://designerwallcoverings.com/vendor-logos/quadrille.png" alt="Quadrille" width="150" style="width:100%; max-width:150px; height:auto; margin:0 auto;">
+ <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/quadrille.png?v=1782330144" alt="Quadrille" width="150" style="width:100%; max-width:150px; height:auto; margin:0 auto;">
</a>
</td>
<td class="stack" width="33%" valign="middle" align="center" style="padding: 14px 8px;">
<a href="https://designerwallcoverings.com/collections/nina-campbell?utm_source=email&utm_medium=mailer&utm_campaign=china-seas-launch" target="_blank">
- <img src="https://designerwallcoverings.com/vendor-logos/nina-campbell-shopify.jpg" alt="Nina Campbell" width="120" style="width:100%; max-width:120px; height:auto; margin:0 auto;">
+ <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/nina-campbell-shopify.jpg?v=1771978096" alt="Nina Campbell" width="120" style="width:100%; max-width:120px; height:auto; margin:0 auto;">
</a>
</td>
</tr>
<tr>
<td class="stack" width="33%" valign="middle" align="center" style="padding: 14px 8px;">
<a href="https://designerwallcoverings.com/collections/schumacher-wallpaper?utm_source=email&utm_medium=mailer&utm_campaign=china-seas-launch" target="_blank">
- <img src="https://designerwallcoverings.com/vendor-logos/schumacher-shopify.png" alt="Schumacher" width="130" style="width:100%; max-width:130px; height:auto; margin:0 auto;">
+ <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/schumacher-shopify.png?v=1771978111" alt="Schumacher" width="130" style="width:100%; max-width:130px; height:auto; margin:0 auto;">
</a>
</td>
<td class="stack" width="33%" valign="middle" align="center" style="padding: 14px 8px;">
<a href="https://designerwallcoverings.com/collections/thibaut-wallcoverings?utm_source=email&utm_medium=mailer&utm_campaign=china-seas-launch" target="_blank">
- <img src="https://designerwallcoverings.com/vendor-logos/thibaut.png" alt="Thibaut" width="150" style="width:100%; max-width:150px; height:auto; margin:0 auto;">
+ <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/thibaut.png?v=1771978235" alt="Thibaut" width="150" style="width:100%; max-width:150px; height:auto; margin:0 auto;">
</a>
</td>
<td class="stack" width="33%" valign="middle" align="center" style="padding: 14px 8px;">
<a href="https://designerwallcoverings.com/collections/cole-sons?utm_source=email&utm_medium=mailer&utm_campaign=china-seas-launch" target="_blank">
- <img src="https://designerwallcoverings.com/vendor-logos/cole-son.png" alt="Cole & Son" width="150" style="width:100%; max-width:150px; height:auto; margin:0 auto;">
+ <img src="https://cdn.shopify.com/s/files/1/0015/4117/7456/files/cole-son_3259d3af-8870-4ffe-aaaf-0288dea61a73.png?v=1782330147" alt="Cole & Son" width="150" style="width:100%; max-width:150px; height:auto; margin:0 auto;">
</a>
</td>
</tr>
diff --git a/mailers/upload-vendor-logos.js b/mailers/upload-vendor-logos.js
new file mode 100644
index 00000000..dc9c5984
--- /dev/null
+++ b/mailers/upload-vendor-logos.js
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+// One-shot: upload vendor logos to Shopify Files via stagedUploadsCreate -> PUT -> fileCreate -> poll READY.
+// Uses SHOPIFY_DRAFT_TOKEN (has write_files). Live store designer-laboratory-sandbox.
+const fs = require('fs');
+const path = require('path');
+
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = process.env.SHOPIFY_DRAFT_TOKEN;
+if (!TOKEN) { console.error('Missing SHOPIFY_DRAFT_TOKEN'); process.exit(1); }
+
+const DIR = path.join(__dirname, '..', 'vendor-logos');
+
+// Files that need fresh upload (the other 3 already exist in Files and are reused).
+// china-seas.png is actually a WebP container -> declare true mimeType.
+const FILES = [
+ { file: 'china-seas.png', mime: 'image/webp' },
+ { file: 'quadrille.png', mime: 'image/png' },
+ { file: 'cole-son.png', mime: 'image/png' },
+];
+
+async function gql(query, variables) {
+ const res = await fetch(`https://${DOMAIN}/admin/api/${API}/graphql.json`, {
+ method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query, variables }),
+ });
+ const j = await res.json();
+ if (j.errors) throw new Error('GraphQL errors: ' + JSON.stringify(j.errors));
+ return j.data;
+}
+
+async function stagedUpload(filename, mime, sizeBytes) {
+ const q = `mutation stagedUploadsCreate($input:[StagedUploadInput!]!){
+ stagedUploadsCreate(input:$input){
+ stagedTargets{ url resourceUrl parameters{ name value } }
+ userErrors{ field message }
+ }
+ }`;
+ const d = await gql(q, { input: [{
+ resource: 'FILE', filename, mimeType: mime, httpMethod: 'POST', fileSize: String(sizeBytes),
+ }]});
+ const ue = d.stagedUploadsCreate.userErrors;
+ if (ue && ue.length) throw new Error('stagedUploadsCreate: ' + JSON.stringify(ue));
+ return d.stagedUploadsCreate.stagedTargets[0];
+}
+
+async function putBytes(target, buf, mime) {
+ // Google Cloud Storage staged target uses multipart POST with the provided params.
+ const fd = new FormData();
+ for (const p of target.parameters) fd.append(p.name, p.value);
+ fd.append('file', new Blob([buf], { type: mime }));
+ const res = await fetch(target.url, { method: 'POST', body: fd });
+ if (!(res.status >= 200 && res.status < 300)) {
+ const t = await res.text();
+ throw new Error(`Upload PUT/POST failed ${res.status}: ${t.slice(0,300)}`);
+ }
+}
+
+async function fileCreate(resourceUrl, filename, alt) {
+ const q = `mutation fileCreate($files:[FileCreateInput!]!){
+ fileCreate(files:$files){
+ files{ id fileStatus alt ... on MediaImage { image { url } } }
+ userErrors{ field message }
+ }
+ }`;
+ const d = await gql(q, { files: [{
+ originalSource: resourceUrl, contentType: 'IMAGE', alt, filename,
+ }]});
+ const ue = d.fileCreate.userErrors;
+ if (ue && ue.length) throw new Error('fileCreate: ' + JSON.stringify(ue));
+ return d.fileCreate.files[0];
+}
+
+async function poll(id) {
+ const q = `query($id:ID!){ node(id:$id){ ... on MediaImage { id fileStatus image { url } } } }`;
+ for (let i = 0; i < 30; i++) {
+ const d = await gql(q, { id });
+ const n = d.node;
+ if (n && n.fileStatus === 'READY' && n.image && n.image.url) return n.image.url;
+ if (n && n.fileStatus === 'FAILED') throw new Error('file FAILED: ' + id);
+ await new Promise(r => setTimeout(r, 2000));
+ }
+ throw new Error('timeout waiting READY: ' + id);
+}
+
+(async () => {
+ const out = {};
+ for (const { file, mime } of FILES) {
+ const fp = path.join(DIR, file);
+ const buf = fs.readFileSync(fp);
+ const alt = file.replace(/\.[a-z]+$/i, '').replace(/-/g, ' ') + ' logo';
+ process.stdout.write(`Uploading ${file} (${mime}, ${buf.length}B)... `);
+ const target = await stagedUpload(file, mime, buf.length);
+ await putBytes(target, buf, mime);
+ const created = await fileCreate(target.resourceUrl, file, alt);
+ const url = await poll(created.id);
+ out[file] = url;
+ console.log('READY ->', url);
+ }
+ console.log('\nRESULT_JSON ' + JSON.stringify(out));
+})().catch(e => { console.error('FATAL', e.message); process.exit(1); });
← dbcd3125 Add Constant Contact v3 API scaffold for China Seas mailer (
·
back to Designer Wallcoverings
·
China Seas mailer: confirmed DW postal address + phone in fo 4c4f363f →