seo-meta

安装量: 544
排名: #2007

安装

npx skills add https://github.com/jezweb/claude-skills --skill seo-meta

SEO Meta Tags

Status: Production Ready ✅ Last Updated: 2026-01-14 Source: Schema.org, Open Graph Protocol, Twitter Developer Docs

Quick Start

Every page needs:

{/ Basic SEO /} Service in Location | Brand Name

{/ Open Graph /}

{/ Twitter Card /}

{/ JSON-LD Structured Data /}

Title Tag Patterns

Character Limits:

Google: 50-60 characters (desktop), 78 (mobile) Social: 60-70 characters

Page Type Formulas:

Page Type Pattern Example Home [Brand] - [Primary Service] in [Location] Acme Plumbing - 24/7 Emergency Plumber Sydney Service [Service] in [Location] | [Brand] Hot Water Repairs Sydney | Acme Plumbing Location [Service] [Suburb] | [Brand] Plumber Bondi | Acme Plumbing About About [Brand] - [Tagline/USP] About Acme - Licensed Plumbers Since 1995 Contact Contact [Brand] - [Location] | [Phone] Contact Acme Plumbing - Sydney | 1300 XXX XXX

Title Modifiers (add credibility):

24/7 Emergency Licensed & Insured Free Quotes Same Day Service Family Owned Award Winning

Anti-Patterns (avoid):

❌ "Welcome to..." (wastes characters) ❌ Keyword stuffing (plumber plumbing plumbers) ❌ ALL CAPS (looks spammy) ❌ Special characters (★ § ¶) Meta Description Patterns

Character Limits:

Desktop: 155-160 characters Mobile: 120-130 characters

Formula:

[Value prop] [Service] in [Location]. [Differentiator]. [CTA].

Examples by Page Type:

Home Page:

Fast, reliable plumbing services in Sydney. 24/7 emergency response, licensed plumbers, upfront pricing. Call 1300 XXX XXX for same-day service.

Service Page:

Expert hot water repairs in Sydney. Fix or replace electric, gas & solar systems. Licensed technicians, 1-year warranty. Book online or call 1300 XXX XXX.

Location Page:

Trusted plumber in Bondi. Blocked drains, leaks, hot water, gas fitting. Same-day service, upfront quotes. Call your local plumber on 1300 XXX XXX.

Power Words (use sparingly):

Trust: Licensed, Certified, Insured, Guaranteed, Trusted Speed: Fast, Quick, Same Day, Emergency, 24/7, Instant Value: Affordable, Competitive, Upfront, No Hidden Fees, Free Quote Local: Local, Nearby, Your Area, [Suburb Name] Quality: Expert, Professional, Experienced, Award Winning Open Graph Tags

Required Tags:

Image Requirements:

Dimensions: 1200x630px (1.91:1 ratio) Format: JPG or PNG (JPG preferred for file size) File size: <1MB (ideally <300KB) Text overlay: Keep important text center (safe zone: 1000x530)

og:type Values by Page Type:

Page Type og:type Home, Service, Location website Blog Post article Business Profile business.business

Optional but Recommended:

Twitter Cards

Card Types:

Type Use Case summary Small square image (1:1), basic info summary_large_image Large image (1.91:1), most common

Required Tags:

Optional:

Fallback Behavior:

If twitter:title missing, uses og:title If twitter:description missing, uses og:description If twitter:image missing, uses og:image

Best Practice: Define og: tags first, only add twitter: if values differ.

JSON-LD Structured Data LocalBusiness Schema (Most Important)

Use for homepage and contact page:

{ "@context": "https://schema.org", "@type": "Plumber", "name": "Acme Plumbing", "description": "Licensed plumbing services in Sydney", "@id": "https://acmeplumbing.com.au", "url": "https://acmeplumbing.com.au", "logo": "https://acmeplumbing.com.au/logo.png", "image": "https://acmeplumbing.com.au/og-image.jpg", "telephone": "+61-XXX-XXX-XXX", "email": "info@acmeplumbing.com.au", "priceRange": "$$", "address": { "@type": "PostalAddress", "streetAddress": "123 Main Street", "addressLocality": "Sydney", "addressRegion": "NSW", "postalCode": "2000", "addressCountry": "AU" }, "geo": { "@type": "GeoCoordinates", "latitude": -33.8688, "longitude": 151.2093 }, "openingHoursSpecification": [ { "@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"], "opens": "07:00", "closes": "17:00" } ], "sameAs": [ "https://facebook.com/acmeplumbing", "https://instagram.com/acmeplumbing" ] }

Specific Business Types (instead of generic LocalBusiness):

Plumber, Electrician, Locksmith, HVAC (HVACBusiness) Dentist, Attorney, Accountant Restaurant, Cafe, FoodEstablishment Store, AutoRepair, BeautySalon Service Schema

Use for service pages:

{ "@context": "https://schema.org", "@type": "Service", "name": "Hot Water Repairs", "description": "Fast hot water system repairs in Sydney", "provider": { "@type": "Plumber", "name": "Acme Plumbing", "url": "https://acmeplumbing.com.au" }, "areaServed": { "@type": "City", "name": "Sydney" }, "availableChannel": { "@type": "ServiceChannel", "serviceUrl": "https://acmeplumbing.com.au/hot-water-repairs", "servicePhone": "+61-XXX-XXX-XXX" } }

FAQ Schema (Rich Snippets)

Use for FAQ sections:

{ "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ { "@type": "Question", "name": "How much does a plumber cost in Sydney?", "acceptedAnswer": { "@type": "Answer", "text": "Plumbing rates in Sydney typically range from $100-$150 per hour for standard work. Emergency callouts may incur higher rates. We provide upfront quotes before starting work." } }, { "@type": "Question", "name": "Do you offer same-day service?", "acceptedAnswer": { "@type": "Answer", "text": "Yes, we offer same-day plumbing service across Sydney for urgent repairs. Call us before 2pm for same-day availability." } } ] }

BreadcrumbList Schema

Use on all pages except homepage:

{ "@context": "https://schema.org", "@type": "BreadcrumbList", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "Home", "item": "https://acmeplumbing.com.au" }, { "@type": "ListItem", "position": 2, "name": "Services", "item": "https://acmeplumbing.com.au/services" }, { "@type": "ListItem", "position": 3, "name": "Hot Water Repairs", "item": "https://acmeplumbing.com.au/hot-water-repairs" } ] }

Canonical URLs

When to Use:

Every page should have a self-referencing canonical Duplicate content (pagination, filters, print versions) Syndicated content Cross-domain duplicates

Self-Referencing Canonical:

Pagination:

Common Mistakes:

❌ Missing canonical (Google chooses for you) ❌ Relative URLs (use absolute URLs) ❌ Canonical pointing to different content ❌ Multiple canonicals (only one per page) Validation Tools

Check Your Implementation:

Tool Purpose URL Google Rich Results Test Test structured data search.google.com/test/rich-results Schema Markup Validator Validate JSON-LD validator.schema.org Facebook Debugger Test Open Graph tags developers.facebook.com/tools/debug Twitter Card Validator Test Twitter Cards cards-dev.twitter.com/validator Screaming Frog Audit all pages screamingfrog.co.uk/seo-spider

Browser Extensions:

SEO Meta in 1 Click (Chrome) META SEO inspector (Firefox) Quick Reference Checklist

For every page, include:

(50-60 chars, unique per page) <meta name="description"> (150-160 chars, unique per page) <link rel="canonical"> (absolute URL) Open Graph tags (og:title, og:description, og:image, og:url, og:type) Twitter Card tags (twitter:card, twitter:title, twitter:description, twitter:image) JSON-LD structured data (LocalBusiness on homepage, Service on service pages) BreadcrumbList schema (all pages except homepage) Mobile viewport meta tag Charset meta tag (UTF-8)</p> <p>Never:</p> <p>❌ Duplicate titles across pages ❌ Use "Welcome to..." in titles ❌ Omit og:image (critical for social sharing) ❌ Use generic @type (LocalBusiness instead of Plumber) ❌ Skip CTA in meta description Error Prevention Common Issues Issue Cause Fix No rich snippets in search Invalid JSON-LD Use validator.schema.org, check commas/quotes Social share shows wrong image og:image missing or wrong size Use 1200x630px, test with Facebook Debugger Title truncated in search Too long Keep under 60 chars Description truncated Too long Keep under 160 chars Multiple pages rank for same keyword Duplicate titles Make each title unique Testing Workflow Validate HTML: Use W3C validator Test Structured Data: Google Rich Results Test Test Social Sharing: Facebook Debugger + Twitter Card Validator Mobile Preview: Google Search Console URL Inspection Cross-Browser Check: Test meta rendering in Chrome/Firefox/Safari Best Practices Summary</p> <p>Title Tags:</p> <p>50-60 characters maximum Include primary keyword + location + brand Unique for every page Use modifiers (24/7, Licensed, Free) sparingly</p> <p>Meta Descriptions:</p> <p>150-160 characters maximum Include value prop + differentiator + CTA Write for humans, not search engines Every page needs unique description</p> <p>Open Graph:</p> <p>Always include og:image (1200x630px) Use absolute URLs Keep og:title under 60 chars Test with Facebook Debugger before launch</p> <p>JSON-LD:</p> <p>Use specific @type (Plumber, not LocalBusiness) Include all contact info (phone, address, email) Add openingHoursSpecification for better display Validate with schema.org validator</p> <p>Canonical URLs:</p> <p>Every page needs canonical Always use absolute URLs Self-referencing is good practice Only one canonical per page</p> <p>Production Notes:</p> <p>Use react-helmet-async for React apps (SSR-safe) Generate JSON-LD dynamically from CMS/database Cache meta tag components for performance Monitor Search Console for indexing issues Update structured data when business details change</p> </article> <a href="/" class="back-link">← <span data-i18n="detail.backToLeaderboard">返回排行榜</span></a> </div> <aside class="sidebar"> <section class="related-skills" id="relatedSkillsSection"> <h2 class="related-title" data-i18n="detail.relatedSkills">相关 Skills</h2> <div class="related-list" id="relatedSkillsList"> <div class="skeleton-card"></div> <div class="skeleton-card"></div> <div class="skeleton-card"></div> </div> </section> </aside> </div> </div> <script src="https://unpkg.com/i18next@23.11.5/i18next.min.js" defer></script> <script src="https://unpkg.com/i18next-browser-languagedetector@7.2.1/i18nextBrowserLanguageDetector.min.js" defer></script> <script defer> // Language resources - same pattern as index page const resources = { 'zh-CN': null, 'en': null, 'ja': null, 'ko': null, 'zh-TW': null, 'es': null, 'fr': null }; // Load language files (only current + fallback for performance) async function loadLanguageResources() { const savedLang = localStorage.getItem('i18nextLng') || 'en'; const langsToLoad = new Set([savedLang, 'en']); // current + fallback await Promise.all([...langsToLoad].map(async (lang) => { try { const response = await fetch(`/locales/${lang}.json`); if (response.ok) { resources[lang] = { translation: await response.json() }; } } catch (error) { console.warn(`Failed to load ${lang} language file:`, error); } })); } // Load a single language on demand (for language switching) async function loadLanguage(lang) { if (resources[lang]) return; try { const response = await fetch(`/locales/${lang}.json`); if (response.ok) { resources[lang] = { translation: await response.json() }; i18next.addResourceBundle(lang, 'translation', resources[lang].translation); } } catch (error) { console.warn(`Failed to load ${lang} language file:`, error); } } // Initialize i18next async function initI18n() { try { await loadLanguageResources(); // Filter out null values from resources const validResources = {}; for (const [lang, data] of Object.entries(resources)) { if (data !== null) { validResources[lang] = data; } } console.log('Loaded languages:', Object.keys(validResources)); console.log('zh-CN resource:', validResources['zh-CN']); console.log('detail.home in resource:', validResources['zh-CN']?.translation?.detail?.home); // 检查是否有保存的语言偏好 const savedLang = localStorage.getItem('i18nextLng'); // 如果没有保存的语言偏好,默认使用英文 const defaultLang = savedLang && ['zh-CN', 'en', 'ja', 'ko', 'zh-TW', 'es', 'fr'].includes(savedLang) ? savedLang : 'en'; await i18next .use(i18nextBrowserLanguageDetector) .init({ lng: defaultLang, // 强制设置初始语言 fallbackLng: 'en', supportedLngs: ['zh-CN', 'en', 'ja', 'ko', 'zh-TW', 'es', 'fr'], resources: validResources, detection: { order: ['localStorage'], // 只使用 localStorage,不检测浏览器语言 caches: ['localStorage'], lookupLocalStorage: 'i18nextLng' }, interpolation: { escapeValue: false } }); console.log('i18next initialized, language:', i18next.language); console.log('Test translation:', i18next.t('detail.home')); // Set initial language in selector const langSwitcher = document.getElementById('langSwitcher'); langSwitcher.value = i18next.language; // Update page language updatePageLanguage(); // Language switch event langSwitcher.addEventListener('change', async (e) => { await loadLanguage(e.target.value); // load on demand i18next.changeLanguage(e.target.value).then(() => { updatePageLanguage(); localStorage.setItem('i18nextLng', e.target.value); }); }); } catch (error) { console.error('i18next init failed:', error); } } // Translation helper function t(key, options = {}) { return i18next.t(key, options); } // Update all translatable elements function updatePageLanguage() { // Update HTML lang attribute document.documentElement.lang = i18next.language; // Update elements with data-i18n attribute document.querySelectorAll('[data-i18n]').forEach(el => { const key = el.getAttribute('data-i18n'); el.textContent = t(key); }); } // Copy command function function copyCommand() { const command = document.getElementById('installCommand').textContent; const btn = document.getElementById('copyBtn'); navigator.clipboard.writeText(command).then(() => { btn.textContent = t('copied'); btn.classList.add('copied'); setTimeout(() => { btn.textContent = t('copy'); btn.classList.remove('copied'); }, 2000); }).catch(() => { // Fallback for non-HTTPS const textArea = document.createElement('textarea'); textArea.value = command; textArea.style.position = 'fixed'; textArea.style.left = '-9999px'; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); btn.textContent = t('copied'); btn.classList.add('copied'); setTimeout(() => { btn.textContent = t('copy'); btn.classList.remove('copied'); }, 2000); }); } // Initialize document.getElementById('copyBtn').addEventListener('click', copyCommand); initI18n(); // 异步加载相关 Skills async function loadRelatedSkills() { const owner = 'jezweb'; const skillName = 'seo-meta'; const currentLang = 'fr'; const listContainer = document.getElementById('relatedSkillsList'); const section = document.getElementById('relatedSkillsSection'); try { const response = await fetch(`/api/related-skills/${encodeURIComponent(owner)}/${encodeURIComponent(skillName)}?limit=6`); if (!response.ok) { throw new Error('Failed to load'); } const data = await response.json(); const relatedSkills = data.related_skills || []; if (relatedSkills.length === 0) { // 没有相关推荐时隐藏整个区域 section.style.display = 'none'; return; } // 渲染相关 Skills listContainer.innerHTML = relatedSkills.map(skill => { const desc = skill.description || ''; const truncatedDesc = desc.length > 60 ? desc.substring(0, 60) + '...' : desc; return ` <a href="${currentLang === 'en' ? '' : '/' + currentLang}/skill/${skill.owner}/${skill.repo}/${skill.skill_name}" class="related-card"> <div class="related-name">${escapeHtml(skill.skill_name)}</div> <div class="related-meta"> <span class="related-owner">${escapeHtml(skill.owner)}</span> <span class="related-installs">${skill.installs}</span> </div> <div class="related-desc">${escapeHtml(truncatedDesc)}</div> </a> `; }).join(''); } catch (error) { console.error('Failed to load related skills:', error); // 加载失败时显示提示或隐藏 listContainer.innerHTML = '<div class="related-empty">暂无相关推荐</div>'; } } // HTML 转义 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 页面加载完成后异步加载相关 Skills if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', loadRelatedSkills); } else { loadRelatedSkills(); } </script> </body> </html>