Генератор Schema Markup - JSON-LD
Улучшите SEO с помощью структурированных данных.
⚡ Автозаполнение по URL
Попытка извлечь заголовок, описание, изображение, автора и т.д.
Улучшите SEO с помощью структурированных данных.
Попытка извлечь заголовок, описание, изображение, автора и т.д.
Определите иерархию сайта.
`, video: ` ` }; // Improved addFaq that uses DOM properties prevents quoting issues function addFaq(qVal = '', aVal = '') { const div = document.createElement('div'); div.className = 'faq-row form-group'; div.style.cssText = 'border-bottom:1px solid #eee; padding-bottom:1rem; margin-bottom:1rem;'; const qInput = document.createElement('input'); qInput.type = 'text'; qInput.className = 'form-control inp-q'; qInput.placeholder = 'Вопрос'; qInput.style.marginBottom = '0.5rem'; qInput.value = qVal; // Safe assignment qInput.oninput = updateJson; const aInput = document.createElement('textarea'); aInput.className = 'form-control inp-a'; aInput.placeholder = 'Ответ'; aInput.value = aVal; // Safe assignment aInput.oninput = updateJson; const rmBtn = document.createElement('button'); rmBtn.className = 'btn btn-sm btn-link'; rmBtn.innerText = 'Удалить'; rmBtn.onclick = function () { div.remove(); updateJson(); }; div.appendChild(qInput); div.appendChild(aInput); div.appendChild(rmBtn); document.getElementById('faq-rows').appendChild(div); } async function fetchUrlData() { const url = document.getElementById('fetch-url').value; if (!url) return alert('Введите URL'); const btn = document.getElementById('btn-fetch'); App.setLoading(btn, true); try { const res = await API.fetch('proxy.php', { url }); if (res.content) { const parser = new DOMParser(); const doc = parser.parseFromString(res.content, 'text/html'); // --- 1. JSON-LD Extraction (Priority) --- let jsonLd = null; const scripts = doc.querySelectorAll('script[type="application/ld+json"]'); // Helper to find specific type in JSON-LD (supports graph and array) const findInJson = (obj, targetType) => { if (!obj) return null; if (Array.isArray(obj)) return obj.find(i => findInJson(i, targetType)); if (obj['@graph']) return obj['@graph'].find(i => findInJson(i, targetType)); if (obj['@type']) { const t = Array.isArray(obj['@type']) ? obj['@type'] : [obj['@type']]; if (t.some(type => type.toLowerCase().includes(targetType))) return obj; } return null; }; for (let s of scripts) { try { const json = JSON.parse(s.innerText); // We accumulate? No, let's look for specific types we support. // Prority: Product > FAQ > Article > Org if (!jsonLd) jsonLd = { product: findInJson(json, 'product'), faq: findInJson(json, 'faqpage'), article: findInJson(json, 'article') || findInJson(json, 'blogposting') || findInJson(json, 'newsarticle'), org: findInJson(json, 'organization'), website: findInJson(json, 'website'), video: findInJson(json, 'videoobject') }; } catch (e) { } } // --- 2. Meta Tag Extraction (Fallback) --- const meta = (name) => doc.querySelector(`meta[name="${name}"]`)?.content || doc.querySelector(`meta[property="${name}"]`)?.content || ''; const metaData = { title: meta('title') || meta('og:title') || doc.title, desc: meta('description') || meta('og:description'), image: meta('og:image') || meta('twitter:image'), author: meta('author') || meta('article:author'), pub: meta('og:site_name'), date: meta('article:published_time')?.split('T')[0], price: meta('product:price:amount'), currency: meta('product:price:currency') || 'USD', url: url }; // --- 3. Type Decision --- let detectedType = 'article'; // Default let dataSrc = 'meta'; // 'json' or 'meta' let activeData = null; // A. Check URL Context const padding = url.split('?')[0]; // Removing query params const isHome = (new URL(url).pathname === '/'); // Logic: // 1. If Homepage -> Org (or WebSite) // 2. If valid FAQ JSON or huge FAQ HTML -> FAQ // 3. If Product JSON or Product Meta -> Product // 4. Else Article if (isHome) { detectedType = 'org'; if (jsonLd?.org) { activeData = jsonLd.org; dataSrc = 'json'; } else if (jsonLd?.website) { activeData = jsonLd.website; dataSrc = 'json'; } // Fallback to Website JSON for Org fields } else if (jsonLd?.faq || (!jsonLd?.product && doc.querySelectorAll('details').length > 1)) { detectedType = 'faq'; activeData = jsonLd?.faq; // Might be null if HTML detected } else if (jsonLd?.product || metaData.price || meta('og:type') === 'product') { detectedType = 'product'; activeData = jsonLd?.product; } else if (activeData = jsonLd?.article) { detectedType = 'article'; dataSrc = 'json'; } else if (activeData = jsonLd?.video) { detectedType = 'video'; dataSrc = 'json'; } // --- 4. Switch Form --- const typeSelect = document.getElementById('schema-type'); typeSelect.value = detectedType; renderForm(); // --- 5. Fill Fields (Hybrid) --- const setVal = (cls, val) => { const el = document.querySelector(cls); if (el && val) el.value = val; }; // Helper to safely get nested JSON props const j = (key) => activeData ? (activeData[key] || (activeData[key.toLowerCase()] || '')) : ''; // Helper to get image url from JSON (could be string or object) const jImg = () => { if (!activeData) return ''; const img = activeData.image; if (typeof img === 'string') return img; if (Array.isArray(img)) return img[0]; if (img && img.url) return img.url; return ''; }; const jAuth = () => { if (!activeData) return ''; const a = activeData.author; if (typeof a === 'string') return a; if (a && a.name) return a.name; if (Array.isArray(a) && a[0]) return a[0].name || a[0]; return ''; }; if (detectedType === 'org') { // Priority: JSON > Meta setVal('.inp-name', j('name') || metaData.pub || metaData.title); setVal('.inp-url', j('url') || metaData.url); setVal('.inp-logo', jImg() || metaData.image); // Logo? // Try to find sameAs if (activeData && activeData.sameAs) { const sa = Array.isArray(activeData.sameAs) ? activeData.sameAs.join('\n') : activeData.sameAs; setVal('.inp-sameas', sa); } } else if (detectedType === 'product') { setVal('.inp-name', j('name') || metaData.title); setVal('.inp-desc', j('description') || metaData.desc); setVal('.inp-img', jImg() || metaData.image); // Price often inside "offers" let p = '', c = 'USD'; if (activeData && activeData.offers) { const o = Array.isArray(activeData.offers) ? activeData.offers[0] : activeData.offers; p = o.price || ''; c = o.priceCurrency || 'USD'; } setVal('.inp-price', p || metaData.price); setVal('.inp-cur', c || metaData.currency); // Brand let b = ''; if (activeData && activeData.brand) b = activeData.brand.name || activeData.brand; setVal('.inp-brand', b || metaData.pub); } else if (detectedType === 'article') { setVal('.inp-headline', j('headline') || j('name') || metaData.title); setVal('.inp-desc', j('description') || metaData.desc); setVal('.inp-img', jImg() || metaData.image); setVal('.inp-author', jAuth() || metaData.author); let pub = ''; if (activeData && activeData.publisher) pub = activeData.publisher.name || activeData.publisher; setVal('.inp-pub', pub || metaData.pub); setVal('.inp-date', j('datePublished') || metaData.date); } else if (detectedType === 'faq') { document.getElementById('faq-rows').innerHTML = ''; let pairs = []; // Try JSON First if (activeData && activeData.mainEntity) { activeData.mainEntity.forEach(q => { if (q['@type'] === 'Question') { const txt = q.name; let ans = ''; if (q.acceptedAnswer) ans = q.acceptedAnswer.text; if (txt && ans) pairs.push({ q: txt, a: ans }); } }); } // If HTML Fallback needed if (pairs.length === 0) { doc.querySelectorAll('details').forEach(d => { const q = d.querySelector('summary')?.innerText.trim(); if (q) { const clone = d.cloneNode(true); clone.querySelector('summary')?.remove(); const a = clone.innerText.trim(); if (a) pairs.push({ q, a }); } }); // Heuristic class fallback if (pairs.length === 0) { const qs = doc.querySelectorAll('.faq-question, .question, .faq-title'); qs.forEach(qNode => { const q = qNode.innerText.trim(); let aNode = qNode.nextElementSibling; if (!aNode && qNode.parentElement.nextElementSibling) aNode = qNode.parentElement.nextElementSibling; if (aNode && aNode.innerText.length > 5) pairs.push({ q, aNode: aNode.innerText.trim() }); }); } } pairs.forEach(p => addFaq(p.q, p.a || p.aNode)); if (pairs.length === 0) addFaq(); } else if (detectedType === 'video') { setVal('.inp-name', j('name') || metaData.title); setVal('.inp-desc', j('description') || metaData.desc); setVal('.inp-thumb', jImg() || metaData.image); setVal('.inp-date', j('uploadDate') || metaData.date); } updateJson(); alert(`Обнаружено: ${detectedType.toUpperCase()} (Источник: ${dataSrc.toUpperCase()})\nСхема сгенерирована!`); } } catch (e) { alert('Ошибка: ' + e.message); console.error(e); } finally { App.setLoading(btn, false, 'Fetch'); } } // Update addFaq to support params // This function is replaced by the new one above. // function addFaq(qVal = '', aVal = '') { // const div = document.createElement('div'); // div.className = 'faq-row form-group'; // div.style.cssText = 'border-bottom:1px solid #eee; padding-bottom:1rem; margin-bottom:1rem;'; // div.innerHTML = ` // // // // `; // document.getElementById('faq-rows').appendChild(div); // } function renderForm() { const type = document.getElementById('schema-type').value; document.getElementById('dynamic-form').innerHTML = forms[type]; if (type === 'faq') addFaq(); if (type === 'breadcrumb') addCrumb(); document.querySelectorAll('#dynamic-form input, #dynamic-form textarea').forEach(i => i.addEventListener('input', updateJson)); updateJson(); } function updateJson() { const type = document.getElementById('schema-type').value; let data = {}; if (type === 'faq') { // Existing logic data = { "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [] }; document.querySelectorAll('.faq-row').forEach(row => { const q = row.querySelector('.inp-q').value; const a = row.querySelector('.inp-a').value; if (q) data.mainEntity.push({ "@type": "Question", "name": q, "acceptedAnswer": { "@type": "Answer", "text": a } }); }); } else if (type === 'article') { // Existing logic data = { "@context": "https://schema.org", "@type": "Article", "headline": document.querySelector('.inp-headline').value, "image": document.querySelector('.inp-img').value ? [document.querySelector('.inp-img').value] : [], "description": document.querySelector('.inp-desc').value, "author": [{ "@type": "Person", "name": document.querySelector('.inp-author').value }], "publisher": { "@type": "Organization", "name": document.querySelector('.inp-pub').value }, "datePublished": document.querySelector('.inp-date').value }; } else if (type === 'org') { // Existing logic const sameAs = document.querySelector('.inp-sameas').value.split('\n').filter(u => u.trim()); data = { "@context": "https://schema.org", "@type": "Organization", "name": document.querySelector('.inp-name').value, "url": document.querySelector('.inp-url').value, "logo": document.querySelector('.inp-logo').value, "sameAs": sameAs }; } else if (type === 'local') { // Existing logic data = { "@context": "https://schema.org", "@type": "LocalBusiness", "name": document.querySelector('.inp-name').value, "image": document.querySelector('.inp-img').value, "telephone": document.querySelector('.inp-phone').value, "priceRange": document.querySelector('.inp-price').value, "address": { "@type": "PostalAddress", "streetAddress": document.querySelector('.inp-addr').value, "addressLocality": document.querySelector('.inp-city').value, "postalCode": document.querySelector('.inp-zip').value } }; } else if (type === 'product') { data = { "@context": "https://schema.org", "@type": "Product", "name": document.querySelector('.inp-name').value, "image": [document.querySelector('.inp-img').value], "description": document.querySelector('.inp-desc').value, "brand": { "@type": "Brand", "name": document.querySelector('.inp-brand').value }, "sku": document.querySelector('.inp-sku').value, "offers": { "@type": "Offer", "priceCurrency": document.querySelector('.inp-cur').value, "price": document.querySelector('.inp-price').value } }; } else if (type === 'event') { data = { "@context": "https://schema.org", "@type": "Event", "name": document.querySelector('.inp-name').value, "startDate": document.querySelector('.inp-start').value, "endDate": document.querySelector('.inp-end').value, "location": { "@type": "Place", "name": document.querySelector('.inp-loc').value, "address": { "@type": "PostalAddress", "streetAddress": document.querySelector('.inp-addr').value } } }; } else if (type === 'recipe') { const ings = document.querySelector('.inp-ing').value.split('\n').filter(s => s.trim()); const steps = document.querySelector('.inp-instr').value.split('\n').filter(s => s.trim()).map((s, i) => ({ "@type": "HowToStep", "text": s, "position": i + 1 })); data = { "@context": "https://schema.org", "@type": "Recipe", "name": document.querySelector('.inp-name').value, "image": [document.querySelector('.inp-img').value], "author": { "@type": "Person", "name": document.querySelector('.inp-author').value }, "recipeIngredient": ings, "recipeInstructions": steps }; } else if (type === 'job') { data = { "@context": "https://schema.org", "@type": "JobPosting", "title": document.querySelector('.inp-title').value, "description": document.querySelector('.inp-desc').value, "datePosted": document.querySelector('.inp-date').value, "hiringOrganization": { "@type": "Organization", "name": document.querySelector('.inp-comp').value }, "jobLocation": { "@type": "Place", "address": { "@type": "PostalAddress", "streetAddress": document.querySelector('.inp-loc').value } } }; } else if (type === 'breadcrumb') { // Existing logic data = { "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [] }; let pos = 1; document.querySelectorAll('.crumb-row').forEach(row => { const name = row.querySelector('.inp-name').value; const item = row.querySelector('.inp-item').value; if (name) { data.itemListElement.push({ "@type": "ListItem", "position": pos++, "name": name, "item": item }); } }); } else if (type === 'video') { // Existing logic data = { "@context": "https://schema.org", "@type": "VideoObject", "name": document.querySelector('.inp-name').value, "description": document.querySelector('.inp-desc').value, "thumbnailUrl": [document.querySelector('.inp-thumb').value], "uploadDate": document.querySelector('.inp-date').value }; } if (Object.keys(data).length > 0) { document.getElementById('json-output').value = JSON.stringify(data, null, 2); } } // Init renderForm();