Etsy SEO Page
<html>
<head>
<style>
* { font-family: 'DM Sans', sans-serif; }
.copy-toast { animation: fadeUp 2s ease forwards; }
@keyframes fadeUp { 0% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-12px); } }
</style>
<script src="https://cdn.tailwindcss.com/3.4.17"></script>
<script src="https://cdn.jsdelivr.net/npm/lucide@0.263.0/dist/umd/lucide.min.js"></script>
<script src="/_sdk/element_sdk.js"></script>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
colors: {
bg: '#F7F5F2',
surface: '#FFFFFF',
txt: '#3B3633',
accent: '#C4896A',
muted: '#A89F97'
}
}
}
}
</script>
<style>body { box-sizing: border-box; }</style>
<script src="/_sdk/data_sdk.js" type="text/javascript"></script>
</head>
<body>
<div id="app" class="h-full w-full overflow-auto">
<div class="max-w-2xl mx-auto px-5 py-10">
<header class="mb-8 text-center">
<h1 id="appTitle" class="text-2xl font-700 tracking-tight mb-1">Etsy SEO Optimizer</h1>
<p id="appSubtitle" class="text-muted text-sm">Generate high-converting tags, keywords & titles</p>
</header><!-- FORM -->
<form id="seoForm" class="space-y-5"><!-- Step 1 -->
<div class="bg-surface rounded-xl p-5 border border-bg"><label class="flex items-center gap-2 text-xs font-600 uppercase tracking-wide text-muted mb-2"> <span class="w-5 h-5 rounded-full bg-accent text-white flex items-center justify-center text-[10px] font-700">1</span> Product Type </label> <input id="product" type="text" required placeholder="e.g. bookmark, mug, t-shirt, tumbler" class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-accent placeholder:text-muted/50">
</div><!-- Step 2 -->
<div class="bg-surface rounded-xl p-5 border border-bg"><label class="flex items-center gap-2 text-xs font-600 uppercase tracking-wide text-muted mb-2"> <span class="w-5 h-5 rounded-full bg-accent text-white flex items-center justify-center text-[10px] font-700">2</span> Theme / Style </label> <input id="theme" type="text" required placeholder="e.g. boho floral, Halloween, romantasy, retro vintage" class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-accent placeholder:text-muted/50">
</div><!-- Step 3 -->
<div class="bg-surface rounded-xl p-5 border border-bg"><label class="flex items-center gap-2 text-xs font-600 uppercase tracking-wide text-muted mb-2"> <span class="w-5 h-5 rounded-full bg-accent text-white flex items-center justify-center text-[10px] font-700">3</span> Buyer Keywords <span class="font-400 normal-case">(≤ 20 chars each, comma-separated)</span> </label> <input id="keywords" type="text" required placeholder="e.g. book lover gift, cozy reading, retro groovy" class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-accent placeholder:text-muted/50">
<div id="kwWarning" class="text-xs text-red-400 mt-1 hidden"></div>
</div><!-- Step 4 -->
<div class="bg-surface rounded-xl p-5 border border-bg"><label class="flex items-center gap-2 text-xs font-600 uppercase tracking-wide text-muted mb-2"> <span class="w-5 h-5 rounded-full bg-accent text-white flex items-center justify-center text-[10px] font-700">4</span> Long-Tail Keywords <span class="font-400 normal-case">(full search phrases, comma-separated)</span> </label> <input id="longtail" type="text" required placeholder="e.g. witchy book lover gift, retro 70s graphic tee" class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-accent placeholder:text-muted/50">
</div><!-- Step 5 -->
<div class="bg-surface rounded-xl p-5 border border-bg"><label class="flex items-center gap-2 text-xs font-600 uppercase tracking-wide text-muted mb-2"> <span class="w-5 h-5 rounded-full bg-accent text-white flex items-center justify-center text-[10px] font-700">5</span> Title Ending Phrase </label> <input id="titleEnd" type="text" required placeholder="e.g. funny cat mom shirt, handmade witchy mug" class="w-full border border-gray-200 rounded-lg px-3 py-2.5 text-sm focus:outline-none focus:border-accent placeholder:text-muted/50">
</div><button type="submit" id="genBtn" class="w-full bg-accent hover:bg-accent/90 text-white font-600 py-3 rounded-xl transition-colors text-sm"> Generate SEO Strategy </button>
</form><!-- RESULTS -->
<div id="results" class="hidden mt-8 space-y-6 pb-12">
<div class="flex items-center justify-between">
<h2 class="text-lg font-700">Your SEO Strategy</h2><button id="copyAllBtn" onclick="copyAll()" class="text-xs text-accent hover:underline flex items-center gap-1"> <i data-lucide="copy" style="width:14px;height:14px;"></i> Copy All </button>
</div><!-- Tags -->
<section class="bg-surface rounded-xl p-5 border border-bg">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-600 uppercase tracking-wide text-muted">Optimized Tags <span id="tagCount" class="font-400"></span></h3><button onclick="copySection('tagsContent')" class="text-xs text-accent hover:underline">Copy</button>
</div>
<div id="tagsContent" class="flex flex-wrap gap-2"></div>
</section><!-- Titles -->
<section class="bg-surface rounded-xl p-5 border border-bg">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-600 uppercase tracking-wide text-muted">Optimized Titles</h3><button onclick="copySection('titlesContent')" class="text-xs text-accent hover:underline">Copy</button>
</div>
<div id="titlesContent" class="space-y-2"></div>
</section><!-- Prompt -->
<section class="bg-surface rounded-xl p-5 border border-bg">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-600 uppercase tracking-wide text-muted">ChatGPT Prompt</h3><button onclick="copySection('promptContent')" class="text-xs text-accent hover:underline">Copy</button>
</div>
<p class="text-xs text-muted mb-2">Paste this into ChatGPT for AI-powered results:</p>
<div id="promptContent" class="text-sm bg-bg rounded-lg p-4 whitespace-pre-wrap leading-relaxed"></div>
</section><button onclick="document.getElementById('results').classList.add('hidden'); window.scrollTo({top:0,behavior:'smooth'});" class="w-full border border-gray-200 text-txt font-500 py-3 rounded-xl text-sm hover:bg-surface transition-colors"> ← Start Over </button> <button onclick="showEmbedModal()" class="w-full bg-accent/10 text-accent font-500 py-3 rounded-xl text-sm hover:bg-accent/20 transition-colors mt-3"> 📋 Get Embed Code </button>
</div>
</div><!-- Toast -->
<div id="toast" class="fixed bottom-6 left-1/2 -translate-x-1/2 bg-txt text-white text-xs px-4 py-2 rounded-lg hidden copy-toast">
Copied!
</div><!-- EMBED CODE SECTION -->
<div id="embedSection" class="hidden fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4">
<div class="bg-surface rounded-xl p-6 max-w-2xl w-full max-h-[80vh] overflow-auto">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-700">Embed on Your Website</h2><button onclick="document.getElementById('embedSection').classList.add('hidden')" class="text-muted hover:text-txt"> <i data-lucide="x" style="width:20px;height:20px;"></i> </button>
</div>
<p class="text-sm text-muted mb-4">Copy this code and paste it into your website's HTML where you want the tool to appear:</p>
<div class="bg-bg rounded-lg p-4 mb-4 border border-gray-200">
<pre id="embedCode" class="text-xs leading-relaxed whitespace-pre-wrap break-words text-txt"></pre>
</div><button onclick="copyEmbedCode()" class="w-full bg-accent hover:bg-accent/90 text-white font-600 py-2.5 rounded-lg text-sm transition-colors"> Copy Embed Code </button>
</div>
</div>
</div>
<script>
// --- Element SDK ---
const defaultConfig = {
app_title: 'Etsy SEO Optimizer',
app_subtitle: 'Generate high-converting tags, keywords & titles',
background_color: '#F7F5F2',
surface_color: '#FFFFFF',
text_color: '#3B3633',
accent_color: '#C4896A',
muted_color: '#A89F97',
font_family: 'DM Sans',
font_size: 14
};
function applyConfig(c) {
const el = (id) => document.getElementById(id);
el('appTitle').textContent = c.app_title || defaultConfig.app_title;
el('appSubtitle').textContent = c.app_subtitle || defaultConfig.app_subtitle;
const bg = c.background_color || defaultConfig.background_color;
const surface = c.surface_color || defaultConfig.surface_color;
const txt = c.text_color || defaultConfig.text_color;
const accent = c.accent_color || defaultConfig.accent_color;
const muted = c.muted_color || defaultConfig.muted_color;
const font = c.font_family || defaultConfig.font_family;
const size = c.font_size || defaultConfig.font_size;
document.body.style.backgroundColor = bg;
document.body.style.color = txt;
document.body.style.fontFamily = `${font}, DM Sans, sans-serif`;
document.body.style.fontSize = `${size}px`;
document.querySelectorAll('.bg-surface').forEach(el => el.style.backgroundColor = surface);
document.querySelectorAll('.bg-accent, [class*="bg-accent"]').forEach(el => {
if (el.tagName === 'BUTTON' && el.id === 'genBtn') el.style.backgroundColor = accent;
});
document.querySelectorAll('.text-muted').forEach(el => el.style.color = muted);
document.querySelectorAll('.text-accent').forEach(el => el.style.color = accent);
const stepDots = document.querySelectorAll('.bg-accent.rounded-full');
stepDots.forEach(d => d.style.backgroundColor = accent);
el('appTitle').style.fontSize = `${size * 1.7}px`;
el('appSubtitle').style.fontSize = `${size * 0.9}px`;
}
window.elementSdk.init({
defaultConfig,
onConfigChange: async (config) => applyConfig(config),
mapToCapabilities: (config) => ({
recolorables: [
{ get: () => config.background_color || defaultConfig.background_color, set: v => { config.background_color = v; window.elementSdk.setConfig({ background_color: v }); } },
{ get: () => config.surface_color || defaultConfig.surface_color, set: v => { config.surface_color = v; window.elementSdk.setConfig({ surface_color: v }); } },
{ get: () => config.text_color || defaultConfig.text_color, set: v => { config.text_color = v; window.elementSdk.setConfig({ text_color: v }); } },
{ get: () => config.accent_color || defaultConfig.accent_color, set: v => { config.accent_color = v; window.elementSdk.setConfig({ accent_color: v }); } },
{ get: () => config.muted_color || defaultConfig.muted_color, set: v => { config.muted_color = v; window.elementSdk.setConfig({ muted_color: v }); } }
],
borderables: [],
fontEditable: { get: () => config.font_family || defaultConfig.font_family, set: v => { config.font_family = v; window.elementSdk.setConfig({ font_family: v }); } },
fontSizeable: { get: () => config.font_size || defaultConfig.font_size, set: v => { config.font_size = v; window.elementSdk.setConfig({ font_size: v }); } }
}),
mapToEditPanelValues: (config) => new Map([
['app_title', config.app_title || defaultConfig.app_title],
['app_subtitle', config.app_subtitle || defaultConfig.app_subtitle]
])
});
// --- Keyword validation ---
document.getElementById('keywords').addEventListener('input', function() {
const warn = document.getElementById('kwWarning');
const bad = this.value.split(',').map(k => k.trim()).filter(k => k.length > 20);
if (bad.length) {
warn.textContent = `Too long (>20 chars): "${bad[0]}"`;
warn.classList.remove('hidden');
} else {
warn.classList.add('hidden');
}
});
// --- Generation ---
document.getElementById('seoForm').addEventListener('submit', function(e) {
e.preventDefault();
const product = document.getElementById('product').value.trim();
const theme = document.getElementById('theme').value.trim();
const keywordsRaw = document.getElementById('keywords').value.trim();
const longtailRaw = document.getElementById('longtail').value.trim();
const titleEnd = document.getElementById('titleEnd').value.trim();
const keywords = [...new Set(keywordsRaw.split(',').map(k => k.trim()).filter(k => k))];
const longtails = [...new Set(longtailRaw.split(',').map(k => k.trim()).filter(k => k))];
const themes = theme.split(',').map(t => t.trim()).filter(t => t);
// Generate tags (unique, ≤20 chars, max 13 Etsy tags)
const tagCandidates = [];
keywords.forEach(k => tagCandidates.push(k));
longtails.forEach(l => { l.split(' ').forEach(w => { if (w.length > 2) tagCandidates.push(w); }); tagCandidates.push(l); });
themes.forEach(t => tagCandidates.push(t));
tagCandidates.push(product);
tagCandidates.push(`${themes[0] || ''} ${product}`.trim());
keywords.forEach(k => tagCandidates.push(`${k} ${product}`.trim()));
tagCandidates.push(`${product} gift`);
tagCandidates.push(`custom ${product}`);
tagCandidates.push(`unique ${product}`);
tagCandidates.push(`handmade ${product}`);
const seenTags = new Set();
const tags = [];
for (const t of tagCandidates) {
const lower = t.toLowerCase();
if (lower.length > 0 && lower.length <= 20 && !seenTags.has(lower)) {
seenTags.add(lower);
tags.push(lower);
if (tags.length >= 13) break;
}
}
// Render tags
const tagsEl = document.getElementById('tagsContent');
tagsEl.innerHTML = '';
tags.forEach(tag => {
const span = document.createElement('span');
span.className = 'inline-block bg-bg text-txt text-xs px-3 py-1.5 rounded-full border border-gray-200';
span.textContent = tag;
tagsEl.appendChild(span);
});
document.getElementById('tagCount').textContent = `(${tags.length}/13)`;
// Generate 10 titles
const titleParts = [...keywords, ...longtails, ...themes, product];
const titles = generateTitles(titleParts, titleEnd, product, themes, keywords, longtails);
const titlesEl = document.getElementById('titlesContent');
titlesEl.innerHTML = '';
titles.forEach((t, i) => {
const div = document.createElement('div');
div.className = 'flex items-start gap-2 py-2 border-b border-gray-100 last:border-0';
const charClass = t.length <= 140 ? 'text-green-600' : 'text-red-400';
div.innerHTML = `<span class="text-muted text-xs mt-0.5 shrink-0">${i+1}.</span><span class="text-sm flex-1">${t}</span><span class="text-[10px] ${charClass} shrink-0 mt-0.5">${t.length}/140</span>`;
titlesEl.appendChild(div);
});
// Build ChatGPT prompt
const prompt = `Create a high-converting Etsy SEO strategy focused on [${product}] featuring ["${themes.join(', ')}", "${keywords.join('", "')}", "${longtails.join('", "')}"]. Provide:\n\nA curated list of top-selling tags and keywords (no duplicates, each ≤ 20 characters) based on Etsy search trends and buyer intent.\n\n10 long-tail, highly optimized Etsy titles (≤ 140 characters) that include the following phrase at the end: "${titleEnd}"\n\nMake sure all keywords align with Etsy's current top-performing listings in that niche.`;
document.getElementById('promptContent').textContent = prompt;
document.getElementById('results').classList.remove('hidden');
setTimeout(() => document.getElementById('results').scrollIntoView({ behavior: 'smooth' }), 100);
lucide.createIcons();
});
function generateTitles(parts, ending, product, themes, keywords, longtails) {
const titles = [];
const cap = s => s.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
const t0 = themes[0] || '';
const t1 = themes[1] || t0;
const k0 = keywords[0] || '';
const k1 = keywords[1] || k0;
const k2 = keywords[2] || k1;
const l0 = longtails[0] || '';
const l1 = longtails[1] || l0;
const templates = [
`${cap(t0)} ${cap(product)} ${cap(k0)} Unique ${cap(l0)} ${cap(ending)}`,
`${cap(l0)} ${cap(t0)} ${cap(product)} Gift for ${cap(k1)} ${cap(ending)}`,
`Cute ${cap(t0)} ${cap(product)} ${cap(k0)} ${cap(k1)} ${cap(ending)}`,
`${cap(t1)} ${cap(product)} ${cap(l1)} Perfect ${cap(k0)} ${cap(ending)}`,
`Handmade ${cap(t0)} ${cap(product)} ${cap(k2)} ${cap(l0)} ${cap(ending)}`,
`${cap(k0)} ${cap(t0)} ${cap(product)} Trendy ${cap(l1)} ${cap(ending)}`,
`Custom ${cap(t0)} ${cap(product)} ${cap(k1)} ${cap(l0)} ${cap(ending)}`,
`${cap(l1)} ${cap(t1)} ${cap(product)} ${cap(k0)} ${cap(ending)}`,
`Unique ${cap(t0)} ${cap(product)} ${cap(k2)} ${cap(k0)} ${cap(ending)}`,
`${cap(t0)} ${cap(l0)} ${cap(product)} Perfect ${cap(k1)} ${cap(ending)}`
];
templates.forEach(tmpl => {
let cleaned = tmpl.replace(/\s+/g, ' ').trim();
if (cleaned.length > 140) cleaned = cleaned.substring(0, 137) + '...';
titles.push(cleaned);
});
return titles;
}
// --- Copy helpers ---
function showToast() {
const t = document.getElementById('toast');
t.classList.remove('hidden');
t.offsetWidth;
setTimeout(() => t.classList.add('hidden'), 2000);
}
function copySection(id) {
const el = document.getElementById(id);
const text = el.innerText;
navigator.clipboard.writeText(text).then(showToast);
}
function copyAll() {
const tags = document.getElementById('tagsContent').innerText;
const titles = document.getElementById('titlesContent').innerText;
const prompt = document.getElementById('promptContent').innerText;
const all = `TAGS:\n${tags}\n\nTITLES:\n${titles}\n\nCHATGPT PROMPT:\n${prompt}`;
navigator.clipboard.writeText(all).then(showToast);
}
function showEmbedModal() {
const embedCode = `<iframe src="YOUR_CANVA_CODE_URL" style="width:100%;height:800px;border:none;border-radius:8px;" title="Etsy SEO Optimizer"></iframe>`;
document.getElementById('embedCode').textContent = embedCode;
document.getElementById('embedSection').classList.remove('hidden');
}
function copyEmbedCode() {
const embedCode = document.getElementById('embedCode').innerText;
navigator.clipboard.writeText(embedCode).then(() => {
showToast();
setTimeout(() => {
document.getElementById('embedSection').classList.add('hidden');
}, 1500);
});
}
lucide.createIcons();
</script>
<script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'9ec708c685eff50a',t:'MTc3NjIxNTA0Ni4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body>
</html>