SEO / Developer

META TAGS

Every Google-recognized meta tag explained. Build your tag set with editable fields and copy the HTML.

Generated HTML

      
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

// ─── Tab switching ────────────────────────────────────────────────────────────
function switchTab(id, btn) {
  document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
  document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
  document.getElementById('tab-' + id).classList.add('active');
  btn.classList.add('active');
}

// ─── Char counter ─────────────────────────────────────────────────────────────
function countChars(el, hintId, min, max) {
  const n = el.value.length;
  const hint = document.getElementById(hintId);
  if (!n) { hint.textContent = ''; hint.className = 'char-hint'; return; }
  hint.textContent = n + ' chars' + (max ? ` (ideal ${min}–${max})` : '');
  hint.className = 'char-hint' + (n > max ? ' over' : n >= min ? '' : ' warn');
}

// ─── Escape HTML ──────────────────────────────────────────────────────────────
function esc(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }

// ─── Build output ─────────────────────────────────────────────────────────────
function v(id) { return document.getElementById(id).value.trim(); }

function update() {
  const lines = [];
  const push = (tag) => lines.push(tag);

  const title  = v('f-title');
  const desc   = v('f-desc');
  const canon  = v('f-canonical');

  if (title)  push(`<title>${esc(title)}</title>`);
  if (desc)   push(`<meta name="description" content="${esc(desc)}" />`);
  if (canon)  push(`<link rel="canonical" href="${esc(canon)}" />`);

  const robots = v('f-robots');
  if (robots) push(`<meta name="robots" content="${esc(robots)}" />`);

  const googlebot = v('f-googlebot');
  if (googlebot) push(`<meta name="googlebot" content="${esc(googlebot)}" />`);

  const maxsnip = v('f-maxsnippet');
  if (maxsnip) push(`<meta name="robots" content="max-snippet:${esc(maxsnip)}" />`);

  const maximg = v('f-maximage');
  if (maximg) push(`<meta name="robots" content="max-image-preview:${esc(maximg)}" />`);

  const maxvid = v('f-maxvideo');
  if (maxvid) push(`<meta name="robots" content="max-video-preview:${esc(maxvid)}" />`);

  const notrans = v('f-notranslate');
  if (notrans) push(`<meta name="google" content="notranslate" />`);

  const nositelinks = v('f-nositelinks');
  if (nositelinks) push(`<meta name="google" content="nositelinkssearchbox" />`);

  const gsv = v('f-gsv');
  if (gsv) push(`<meta name="google-site-verification" content="${esc(gsv)}" />`);

  const rating = v('f-rating');
  if (rating) push(`<meta name="rating" content="${esc(rating)}" />`);

  const referrer = v('f-referrer');
  if (referrer) push(`<meta name="referrer" content="${esc(referrer)}" />`);

  const lang = v('f-lang');
  if (lang) push(`<link rel="alternate" hreflang="${esc(lang)}" href="${esc(canon||'https://example.com/')}" />`);

  // OG
  const ogtype     = v('f-ogtype');
  const ogsitename = v('f-ogsitename');
  const ogtitle    = v('f-ogtitle') || title;
  const ogdesc     = v('f-ogdesc')  || desc;
  const ogurl      = v('f-ogurl')   || canon;
  const ogimage    = v('f-ogimage');
  const ogimgw     = v('f-ogimgw');
  const ogimgh     = v('f-ogimgh');
  const ogimgalt   = v('f-ogimgalt');

  if (ogtype || ogsitename || ogtitle || ogdesc || ogurl || ogimage) {
    lines.push('');
    if (ogtype)     push(`<meta property="og:type" content="${esc(ogtype)}" />`);
    if (ogsitename) push(`<meta property="og:site_name" content="${esc(ogsitename)}" />`);
    if (ogtitle)    push(`<meta property="og:title" content="${esc(ogtitle)}" />`);
    if (ogdesc)     push(`<meta property="og:description" content="${esc(ogdesc)}" />`);
    if (ogurl)      push(`<meta property="og:url" content="${esc(ogurl)}" />`);
    if (ogimage) {
      push(`<meta property="og:image" content="${esc(ogimage)}" />`);
      if (ogimgw)   push(`<meta property="og:image:width" content="${esc(ogimgw)}" />`);
      if (ogimgh)   push(`<meta property="og:image:height" content="${esc(ogimgh)}" />`);
      if (ogimgalt) push(`<meta property="og:image:alt" content="${esc(ogimgalt)}" />`);
    }
  }

  // Twitter
  const twcard    = v('f-twcard');
  const twsite    = v('f-twsite');
  const twcreator = v('f-twcreator');
  const twtitle   = v('f-twtitle') || ogtitle;
  const twdesc    = v('f-twdesc')  || ogdesc;
  const twimage   = v('f-twimage') || ogimage;
  const twimgalt  = v('f-twimgalt') || ogimgalt;

  if (twcard || twsite || twcreator) {
    lines.push('');
    if (twcard)    push(`<meta name="twitter:card" content="${esc(twcard)}" />`);
    if (twsite)    push(`<meta name="twitter:site" content="${esc(twsite)}" />`);
    if (twcreator) push(`<meta name="twitter:creator" content="${esc(twcreator)}" />`);
    if (twtitle)   push(`<meta name="twitter:title" content="${esc(twtitle)}" />`);
    if (twdesc)    push(`<meta name="twitter:description" content="${esc(twdesc)}" />`);
    if (twimage) {
      push(`<meta name="twitter:image" content="${esc(twimage)}" />`);
      if (twimgalt) push(`<meta name="twitter:image:alt" content="${esc(twimgalt)}" />`);
    }
  }

  // Mobile
  const themecolor     = v('f-themecolor');
  const applemobile    = v('f-applemobile');
  const applebar       = v('f-applebar');
  const androidcapable = v('f-androidcapable');

  if (themecolor || applemobile || applebar || androidcapable) {
    lines.push('');
    if (themecolor)     push(`<meta name="theme-color" content="${esc(themecolor)}" />`);
    if (applemobile)    push(`<meta name="apple-mobile-web-app-capable" content="${esc(applemobile)}" />`);
    if (applebar)       push(`<meta name="apple-mobile-web-app-status-bar-style" content="${esc(applebar)}" />`);
    if (androidcapable) push(`<meta name="mobile-web-app-capable" content="${esc(androidcapable)}" />`);
  }

  const raw = lines.join('\n');
  document.getElementById('outputPre').innerHTML = syntaxHL(raw) || '<span style="color:var(--muted)">Fill in fields above to generate tags…</span>';
}

function syntaxHL(html) {
  if (!html) return '';
  return html
    .split('\n')
    .map(line => {
      if (!line.trim()) return '';
      line = line
        .replace(/(&lt;\/?[\w:]+)/g, '<span class="hl-tag">$1</span>')
        .replace(/(\s[\w:-]+)=/g, '<span class="hl-attr">$1</span>=')
        .replace(/&quot;([^&]*)&quot;/g, '<span class="hl-val">&quot;$1&quot;</span>');
      return line;
    })
    .join('\n');
}

function copyOutput() {
  const pre = document.getElementById('outputPre');
  navigator.clipboard.writeText(pre.innerText).then(() => {
    const btn = document.getElementById('copyAllBtn');
    btn.textContent = 'Copied!';
    btn.classList.add('copied');
    setTimeout(() => { btn.textContent = 'Copy all'; btn.classList.remove('copied'); }, 1800);
  });
}

// ─── Presets ──────────────────────────────────────────────────────────────────
const PRESETS = {
  blog: {
    'f-title': 'My Blog Post Title — MySite',
    'f-desc': 'A thoughtful exploration of the topic with practical examples and takeaways.',
    'f-canonical': 'https://example.com/blog/my-post/',
    'f-robots': 'index, follow',
    'f-ogtype': 'article',
    'f-ogsitename': 'MySite',
    'f-twcard': 'summary_large_image',
    'f-referrer': 'strict-origin-when-cross-origin',
    'f-maximage': 'large',
    'f-maxsnippet': '160',
  },
  ecommerce: {
    'f-title': 'Product Name — Shop Name',
    'f-desc': 'Buy Product Name. Free shipping on orders over $50. In stock now.',
    'f-canonical': 'https://shop.example.com/products/product-name/',
    'f-robots': 'index, follow',
    'f-ogtype': 'product',
    'f-ogsitename': 'Shop Name',
    'f-twcard': 'summary_large_image',
    'f-referrer': 'origin-when-cross-origin',
    'f-maximage': 'large',
    'f-maxsnippet': '160',
    'f-ogimgw': '1200',
    'f-ogimgh': '630',
  },
  local: {
    'f-title': 'Business Name — City, State | Category',
    'f-desc': 'Business Name in City, State. Phone: (555) 000-0000. Open Mon–Fri 9–6.',
    'f-canonical': 'https://mybusiness.example.com/',
    'f-robots': 'index, follow',
    'f-ogtype': 'website',
    'f-ogsitename': 'Business Name',
    'f-twcard': 'summary',
    'f-googlebot': 'index, follow',
    'f-referrer': 'strict-origin-when-cross-origin',
  },
  app: {
    'f-title': 'App Name — Free Online Tool',
    'f-desc': 'Do something useful with App Name. Free, no signup required.',
    'f-canonical': 'https://app.example.com/',
    'f-robots': 'index, follow',
    'f-ogtype': 'website',
    'f-ogsitename': 'App Name',
    'f-twcard': 'summary_large_image',
    'f-themecolor': '#1a1a2e',
    'f-applemobile': 'yes',
    'f-applebar': 'black-translucent',
    'f-androidcapable': 'yes',
    'f-referrer': 'strict-origin-when-cross-origin',
    'f-maxsnippet': '-1',
    'f-maximage': 'large',
  },
  news: {
    'f-title': 'Headline: Event Happens — NewsOrg',
    'f-desc': 'Full coverage of the event. Includes quotes, context, and analysis.',
    'f-canonical': 'https://news.example.com/2024/headline-slug/',
    'f-robots': 'index, follow',
    'f-googlebot': 'index, follow',
    'f-ogtype': 'article',
    'f-ogsitename': 'NewsOrg',
    'f-twcard': 'summary_large_image',
    'f-maxsnippet': '-1',
    'f-maximage': 'large',
    'f-maxvideo': '-1',
    'f-referrer': 'strict-origin-when-cross-origin',
  },
};

function loadPreset(name) {
  clearAll();
  const p = PRESETS[name];
  for (const [id, val] of Object.entries(p)) {
    const el = document.getElementById(id);
    if (!el) continue;
    el.value = val;
  }
  countChars(document.getElementById('f-title'), 'hint-title', 0, 60);
  countChars(document.getElementById('f-desc'), 'hint-desc', 120, 155);
  update();
}

function clearAll() {
  document.querySelectorAll('#tab-builder input, #tab-builder select, #tab-builder textarea').forEach(el => {
    if (el.type === 'color') el.value = '#0a0a0a';
    else el.value = '';
  });
  document.getElementById('outputPre').innerHTML = '<span style="color:var(--muted)">Fill in fields above to generate tags…</span>';
  document.getElementById('hint-title').textContent = '';
  document.getElementById('hint-desc').textContent = '';
}

// ─── Reference data ───────────────────────────────────────────────────────────
const TAGS = [
  // SEO
  { cat: 'seo', name: '<title>', title: 'Page Title', desc: 'The most important on-page SEO element. Shown in browser tabs, bookmarks, and as the blue link in Google search results. Keep under 60 characters to avoid truncation.', example: '<title>Best Hiking Boots 2024 — GearHub</title>' },
  { cat: 'seo', name: '<meta name="description">', title: 'Meta Description', desc: 'The search snippet below your title. Google may rewrite it, but a good description improves click-through rate. Aim for 120–155 characters.', example: '<meta name="description" content="Top-rated hiking boots reviewed by experts. Compare waterproofing, grip, and price." />' },
  { cat: 'seo', name: '<link rel="canonical">', title: 'Canonical URL', desc: 'Tells Google which URL is the "master" version when the same content exists at multiple URLs (e.g. with/without trailing slash, HTTP vs HTTPS). Prevents duplicate content penalties.', example: '<link rel="canonical" href="https://example.com/page/" />' },
  { cat: 'seo', name: '<meta name="robots">', title: 'Robots Directive', desc: 'Controls all crawlers. Values: index/noindex, follow/nofollow, noarchive, nosnippet, noimageindex. Combine with commas. Defaults to "index, follow" if omitted.', example: '<meta name="robots" content="index, follow" />' },
  { cat: 'seo', name: '<link rel="alternate" hreflang>', title: 'hreflang (Multilingual)', desc: 'Signals to Google which language/region version to show for a given user. Must be set on every language variant pointing to every other variant, including x-default.', example: '<link rel="alternate" hreflang="en-US" href="https://example.com/en-us/" />' },
  { cat: 'seo', name: '<meta name="author">', title: 'Author', desc: 'Declares the content author. Not a ranking signal but used by some scrapers, readers, and schema validation tools.', example: '<meta name="author" content="Jane Doe" />' },
  { cat: 'seo', name: '<meta name="keywords">', title: 'Keywords (deprecated)', desc: 'Google ignores this entirely since 2009. Bing uses it very weakly. Including it does no harm but provides zero SEO benefit — it leaks your target keywords to competitors.', example: '<meta name="keywords" content="hiking, boots, trail" />' },

  // Google-specific
  { cat: 'google', name: '<meta name="googlebot">', title: 'Googlebot Directive', desc: 'Same values as robots but applies only to Google\'s crawler. Useful for allowing other bots to index while restricting Google, or vice versa.', example: '<meta name="googlebot" content="noindex, nofollow" />' },
  { cat: 'google', name: '<meta name="robots" content="max-snippet:N">', title: 'Max Snippet', desc: 'Limits how many characters Google may use in the search snippet. -1 = no limit, 0 = no snippet at all. Part of Google\'s robots meta tag extensions.', example: '<meta name="robots" content="max-snippet:160" />' },
  { cat: 'google', name: '<meta name="robots" content="max-image-preview:SIZE">', title: 'Max Image Preview', desc: 'Controls the maximum size of image previews in Google search results. Values: none, standard, large. "large" enables rich image previews in Discover and image search.', example: '<meta name="robots" content="max-image-preview:large" />' },
  { cat: 'google', name: '<meta name="robots" content="max-video-preview:N">', title: 'Max Video Preview', desc: 'Limits the number of seconds Google can show as a video preview. -1 = no limit, 0 = no preview. Requires max-image-preview:large to show video snippets.', example: '<meta name="robots" content="max-video-preview:-1" />' },
  { cat: 'google', name: '<meta name="google" content="notranslate">', title: 'Suppress Translation Prompt', desc: 'Tells Google not to offer page translation. Useful for content that shouldn\'t be translated (poetry, language learning, code).', example: '<meta name="google" content="notranslate" />' },
  { cat: 'google', name: '<meta name="google" content="nositelinkssearchbox">', title: 'No Sitelinks Search Box', desc: 'Prevents Google from showing a sitelinks search box widget in search results. Rarely needed; mostly useful while your internal search is being rebuilt.', example: '<meta name="google" content="nositelinkssearchbox" />' },
  { cat: 'google', name: '<meta name="google-site-verification">', title: 'Google Site Verification', desc: 'Proves ownership to Google Search Console. The token comes from Search Console → Settings → Ownership verification → HTML tag method.', example: '<meta name="google-site-verification" content="abc123xyz" />' },
  { cat: 'google', name: '<meta name="rating">', title: 'Content Rating', desc: 'Classifies content for SafeSearch. "general" = family-safe, "mature" = adult content. Without this, Google infers maturity from content analysis.', example: '<meta name="rating" content="general" />' },
  { cat: 'google', name: '<meta name="referrer">', title: 'Referrer Policy', desc: 'Controls how much referrer info is sent when users click links from your page. "strict-origin-when-cross-origin" is the modern safe default — sends origin for cross-site, full URL same-site.', example: '<meta name="referrer" content="strict-origin-when-cross-origin" />' },
  { cat: 'google', name: 'data-nosnippet attribute', title: 'data-nosnippet (Element Level)', desc: 'HTML attribute you add to any element (div, span, section) to prevent its content from appearing in Google\'s search snippet. More targeted than meta nosnippet which blocks the whole page.', example: '<div data-nosnippet>This text won\'t appear in search snippets.</div>' },
  { cat: 'google', name: '<meta name="googlebot-news">', title: 'Googlebot News Directive', desc: 'Same as googlebot but applies only to Google News crawler. Lets you exclude a page from Google News while keeping it in regular web search.', example: '<meta name="googlebot-news" content="noindex" />' },
  { cat: 'google', name: '<meta name="robots" content="noimageindex">', title: 'No Image Index', desc: 'Prevents Google from indexing images on this page. The images can still appear via other pages that link to them.', example: '<meta name="robots" content="noimageindex" />' },

  // Open Graph
  { cat: 'og', name: '<meta property="og:title">', title: 'OG Title', desc: 'The title shown when someone shares your URL on Facebook, LinkedIn, WhatsApp, etc. Ideally matches your page title. Max ~60 characters for clean display.', example: '<meta property="og:title" content="Best Hiking Boots 2024" />' },
  { cat: 'og', name: '<meta property="og:description">', title: 'OG Description', desc: 'The description shown in social link previews. About 60–65 characters max (two lines). Shorter and punchier than your meta description.', example: '<meta property="og:description" content="Expert-reviewed boots for any trail." />' },
  { cat: 'og', name: '<meta property="og:image">', title: 'OG Image', desc: 'The image shown in link previews. Recommended: 1200×630px (1.91:1 ratio). Must be absolute URL. Facebook recommends under 8 MB.', example: '<meta property="og:image" content="https://example.com/og-image.jpg" />' },
  { cat: 'og', name: '<meta property="og:image:alt">', title: 'OG Image Alt', desc: 'Accessible description of the OG image. Used by screen readers when the card is rendered. Also helps when the image fails to load.', example: '<meta property="og:image:alt" content="Three hikers on a mountain trail" />' },
  { cat: 'og', name: '<meta property="og:url">', title: 'OG URL', desc: 'The canonical URL of the page being shared. Should match your canonical link tag. Used by Facebook to deduplicate shares and aggregate like counts.', example: '<meta property="og:url" content="https://example.com/page/" />' },
  { cat: 'og', name: '<meta property="og:type">', title: 'OG Type', desc: 'Declares the type of content: website, article, product, video.other, book, profile, music.song. Unlocks additional OG properties for that type.', example: '<meta property="og:type" content="article" />' },
  { cat: 'og', name: '<meta property="og:site_name">', title: 'OG Site Name', desc: 'Your website\'s display name — shown under the title in some card renderers. Should be consistent across all pages.', example: '<meta property="og:site_name" content="GearHub" />' },
  { cat: 'og', name: '<meta property="og:locale">', title: 'OG Locale', desc: 'The language and region of the page, e.g. en_US, fr_FR, de_DE. Helps social platforms serve the right language UI around your card.', example: '<meta property="og:locale" content="en_US" />' },
  { cat: 'og', name: '<meta property="article:published_time">', title: 'Article Published Time', desc: 'ISO 8601 datetime of when the article was published. Used by Facebook and Google\'s article structured data. Required for news and blog posts to show timestamps in previews.', example: '<meta property="article:published_time" content="2024-06-01T08:00:00+00:00" />' },
  { cat: 'og', name: '<meta property="article:modified_time">', title: 'Article Modified Time', desc: 'ISO 8601 datetime of the last significant update. Google may use this over the HTML timestamp for freshness signals in search.', example: '<meta property="article:modified_time" content="2024-08-15T14:30:00+00:00" />' },

  // Twitter
  { cat: 'twitter', name: '<meta name="twitter:card">', title: 'Twitter Card Type', desc: 'Required. Declares the card type: summary (small square image), summary_large_image (large banner), app (app store deep link), player (video/audio). Without this, no card renders.', example: '<meta name="twitter:card" content="summary_large_image" />' },
  { cat: 'twitter', name: '<meta name="twitter:site">', title: 'Twitter Site Handle', desc: 'The @username of the website\'s Twitter account. Shown at the bottom of some card types. Use the brand handle, not the author.', example: '<meta name="twitter:site" content="@mybrand" />' },
  { cat: 'twitter', name: '<meta name="twitter:creator">', title: 'Twitter Creator Handle', desc: 'The @username of the individual author of the content. Distinct from twitter:site. Used for attribution on article and summary cards.', example: '<meta name="twitter:creator" content="@authorhandle" />' },
  { cat: 'twitter', name: '<meta name="twitter:title">', title: 'Twitter Title', desc: 'Title shown in the Twitter card. Falls back to og:title if absent. Max ~70 characters. Twitter truncates aggressively on mobile.', example: '<meta name="twitter:title" content="Best Hiking Boots 2024" />' },
  { cat: 'twitter', name: '<meta name="twitter:description">', title: 'Twitter Description', desc: 'Description in the card. Falls back to og:description. Max ~200 characters but Twitter may truncate. Skip if og:description is already set and identical.', example: '<meta name="twitter:description" content="Expert-reviewed boots for any trail." />' },
  { cat: 'twitter', name: '<meta name="twitter:image">', title: 'Twitter Image', desc: 'Image for the card. Falls back to og:image. Minimum 144×144px, max 4096×4096px. Under 5 MB (JPG, PNG, WEBP, GIF). Absolute URL required.', example: '<meta name="twitter:image" content="https://example.com/twitter-card.jpg" />' },
  { cat: 'twitter', name: '<meta name="twitter:image:alt">', title: 'Twitter Image Alt', desc: 'Alt text for the Twitter card image. Required for accessibility. Max 420 characters.', example: '<meta name="twitter:image:alt" content="Alt text for Twitter card image" />' },

  // Mobile
  { cat: 'mobile', name: '<meta name="viewport">', title: 'Viewport', desc: 'Mandatory for mobile-friendly pages. "width=device-width, initial-scale=1" is the standard value. Google uses mobile-friendliness as a ranking signal (mobile-first indexing).', example: '<meta name="viewport" content="width=device-width, initial-scale=1.0" />' },
  { cat: 'mobile', name: '<meta name="theme-color">', title: 'Theme Color', desc: 'Colors the browser UI (address bar, tabs) on Android Chrome, Edge, and some desktop Chromium browsers. Also used as the PWA title bar color. Hex or any CSS color value.', example: '<meta name="theme-color" content="#0a0a0a" />' },
  { cat: 'mobile', name: '<meta name="apple-mobile-web-app-capable">', title: 'iOS Web App Capable', desc: 'Enables standalone (full-screen, no browser chrome) mode when the page is added to the iOS Home Screen. Required for PWA-like iOS behavior.', example: '<meta name="apple-mobile-web-app-capable" content="yes" />' },
  { cat: 'mobile', name: '<meta name="apple-mobile-web-app-status-bar-style">', title: 'iOS Status Bar Style', desc: 'Controls the iOS status bar appearance in standalone mode. "default" = white, "black" = dark, "black-translucent" = overlays the app content.', example: '<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />' },
  { cat: 'mobile', name: '<meta name="apple-mobile-web-app-title">', title: 'iOS App Title', desc: 'The app name shown below the icon on the iOS Home Screen when added via "Add to Home Screen". Defaults to the page title if omitted.', example: '<meta name="apple-mobile-web-app-title" content="My App" />' },
  { cat: 'mobile', name: '<meta name="mobile-web-app-capable">', title: 'Android Web App Capable', desc: 'Chrome for Android equivalent of the Apple tag. Enables standalone mode when added to the Android home screen.', example: '<meta name="mobile-web-app-capable" content="yes" />' },
  { cat: 'mobile', name: '<link rel="manifest">', title: 'Web App Manifest', desc: 'Links the W3C Web App Manifest JSON — the standard way to control app name, icons, theme color, display mode, and start URL for PWAs. Works across all platforms.', example: '<link rel="manifest" href="/manifest.json" />' },

  // Security
  { cat: 'security', name: '<meta http-equiv="Content-Security-Policy">', title: 'Content Security Policy', desc: 'Restricts which resources (scripts, styles, images, frames) the browser may load. One of the most powerful XSS mitigations available. Consider HTTP header for full coverage — this meta tag only covers after HTML parsing.', example: "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src 'self'; script-src 'self'\" />" },
  { cat: 'security', name: '<meta http-equiv="X-UA-Compatible">', title: 'IE Compatibility Mode', desc: 'Forces IE (deprecated) to use the latest rendering engine. Harmless on modern browsers. Still included in many templates for legacy compatibility: "IE=edge".', example: '<meta http-equiv="X-UA-Compatible" content="IE=edge" />' },
  { cat: 'security', name: '<meta name="referrer">', title: 'Referrer Policy (Security)', desc: 'Controls how much referrer information is leaked when users navigate away. "no-referrer" sends nothing; "strict-origin-when-cross-origin" is the modern best practice — full URL same-origin, only origin cross-origin, nothing downgrade.', example: '<meta name="referrer" content="strict-origin-when-cross-origin" />' },
  { cat: 'security', name: '<meta http-equiv="Permissions-Policy">', title: 'Permissions Policy', desc: 'Restricts which browser features (camera, microphone, geolocation, payment) are available to the page and iframes. Succeeds Feature-Policy.', example: '<meta http-equiv="Permissions-Policy" content="geolocation=(), camera=()" />' },
];

// ─── Render reference ─────────────────────────────────────────────────────────
let activeRefCat = 'all';

function buildRef() {
  const grid = document.getElementById('refGrid');
  grid.innerHTML = TAGS.map((t, i) => {
    const catLabel = { seo: 'SEO', google: 'Google', og: 'Open Graph', twitter: 'Twitter / X', mobile: 'Mobile & PWA', security: 'Security' }[t.cat];
    return `<div class="ref-card" data-cat="${t.cat}" data-search="${(t.name+' '+t.title+' '+t.desc).toLowerCase()}" id="rc${i}">
      <div class="ref-category ref-cat-${t.cat}">${catLabel}</div>
      <div class="ref-tag-name">${esc(t.name)}</div>
      <div class="ref-title">${esc(t.title)}</div>
      <div class="ref-desc">${esc(t.desc)}</div>
      <div class="ref-example" onclick="copyRef(this, ${i})" title="Click to copy">
        <span class="ref-copy-hint">copy</span>${esc(t.example)}
      </div>
    </div>`;
  }).join('');
  filterRef();
}

function copyRef(el, i) {
  navigator.clipboard.writeText(TAGS[i].example).then(() => {
    el.classList.add('copied');
    el.querySelector('.ref-copy-hint').textContent = 'copied!';
    setTimeout(() => { el.classList.remove('copied'); el.querySelector('.ref-copy-hint').textContent = 'copy'; }, 1600);
  });
}

function setRefCat(cat, btn) {
  activeRefCat = cat;
  document.querySelectorAll('.filter-tab').forEach(b => b.classList.remove('active'));
  btn.classList.add('active');
  filterRef();
}

function filterRef() {
  const q = document.getElementById('refSearch').value.toLowerCase();
  let visible = 0;
  document.querySelectorAll('.ref-card').forEach(card => {
    const catOk = activeRefCat === 'all' || card.dataset.cat === activeRefCat;
    const searchOk = !q || card.dataset.search.includes(q);
    const show = catOk && searchOk;
    card.classList.toggle('hidden', !show);
    if (show) visible++;
  });
  document.getElementById('refCount').textContent = visible + ' tag' + (visible !== 1 ? 's' : '');
}

// ─── Init ─────────────────────────────────────────────────────────────────────
buildRef();
update();