Sanudin Logo
← Back to blog

HTML Meta Tags: My Reference Notes

#html#webdev#seo#security

I keep re-looking these up every time I start a new project, so this is where I'm dumping them all in one place — with a short note on why each one matters, so future-me doesn't have to re-derive it from scratch.

Jump to:

Document Core & Responsive Setup

<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
  • charset="UTF-8" — needs to sit near the top of <head>, or the browser can end up re-parsing the whole doc if it hits non-ASCII bytes first.
  • viewport — without it, mobile just renders the desktop layout zoomed way out. Non-negotiable, always include.

Basic SEO & Indexing

<meta name="description" content="...">
<meta name="robots" content="index, follow">
<link rel="canonical" href="https://sanudin.dev/">
  • description — the snippet under my link in search results, ~150–160 chars. Doesn't move ranking directly, but affects click-through.
  • robots — default is already index, follow, so I only need this tag when I want the opposite (noindex, nofollow on staging routes).
  • canonical — points to the "real" URL when the same content is reachable multiple ways (trailing slash, query params, etc).

Social Cards (OG + X)

Open Graph = the universal standard for FB/LinkedIn/Discord/Slack preview cards. X falls back to OG unless I add twitter:* tags specifically.

Image size to remember: 1200×630px (1.91:1 ratio).

<meta property="og:type" content="website">
<meta property="og:title" content="Sanudin | Software Engineer">
<meta property="og:description" content="Backend-first software engineer writing about web development, from Indonesia.">
<meta property="og:image" content="https://sanudin.dev/og-cover.png">
<meta property="og:url" content="https://sanudin.dev/">
<meta property="og:site_name" content="Sanudin">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Sanudin | Software Engineer">
<meta name="twitter:description" content="Backend-first software engineer writing about web development, from Indonesia.">
<meta name="twitter:image" content="https://sanudin.dev/og-cover.png">

Reminder: og:type should be article on blog posts, website is only for the homepage/landing pages.

GEO / AI Crawlers

Two separate places to control this — meta tag (page-level) and robots.txt (site-wide).

<meta name="robots" content="noai, noimageai">
User-agent: GPTBot
Disallow: /

User-agent: ClaudeBot
Disallow: /

User-agent: Googlebot
Allow: /

Note to self: noai/noimageai isn't an official spec tag (started at DeviantArt, 2022) — adoption is spotty at best. robots.txt is the one that actually does something if I care about blocking AI training.

JSON-LD for identity — why I bother with this

This block isn't for humans, it's for search engines and AI systems trying to figure out who is talking. A page full of prose saying "I'm Sanudin, a software engineer" is something they have to infer; a JSON-LD block just states it as structured fact.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Person",
  "name": "Sanudin",
  "jobTitle": "Software Engineer",
  "url": "https://sanudin.dev",
  "sameAs": [
    "https://github.com/sanudin-dev",
    "https://linkedin.com/in/sanudin"
  ],
  "knowsAbout": ["TypeScript", "Next.js", "Node.js", "PHP", "Web Development"]
}
</script>
  • @type: "Person" — marks this specifically as data about an entity, not just page content. This is the difference between search engines guessing at who wrote something vs. being told directly.
  • sameAs — this is the field doing the real work. It's what lets a search engine or AI system connect "Sanudin on GitHub," "Sanudin on LinkedIn," and "Sanudin on sanudin.dev" as the same person instead of three unrelated mentions. Without this array, every profile is an island.
  • jobTitle — needs to match my canonical bio wording exactly ("Software Engineer," not a synonym). Consistency across every profile is literally what builds the entity association — a mismatch works against the whole point of this block.
  • knowsAbout — a topical signal, not keyword stuffing. Should genuinely reflect what the site's content backs up, or it's just noise.
  • Before publishing: run it through Google's Rich Results Test or the schema.org validator. JSON-LD fails silently — a typo just gets ignored rather than throwing an error I'd notice.
  • This is site identity schema. If I want individual posts to carry their own structured data later, that's a separate Article type with an author field pointing back to this Person — a future addition, not something I need to retrofit now.

Security Headers

Content Security Policy — what each part is actually doing

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self' https://trustedscripts.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;">
  • default-src 'self' — the baseline: nothing loads unless it's same-origin. Every other directive below is an exception to this.
  • script-src 'self' https://trustedscripts.com — only my own scripts and this one whitelisted domain can execute. This is the directive that actually matters for stopping injected/XSS scripts — the fact that I haven't added 'unsafe-inline' here is the important part, not an oversight.
  • style-src 'self' 'unsafe-inline''unsafe-inline' here is a real trade-off, not a default I should ignore. It's needed because a lot of frameworks inject style="" at runtime, but it does weaken protection against injected inline styles specifically (there are real CSS-based UI-redress/exfiltration tricks that rely on exactly this). Much smaller risk than allowing it on script-src, but if I ever want to close this gap, the fix is nonces or hashes per style block, not just removing the directive.
  • img-src 'self' data: https:data: allows base64-inlined images (small icons, placeholders), https: broadly allows images from any HTTPS source so I'm not maintaining an allowlist of every image domain.

Reminder on the meta-tag limitation: frame-ancestors, report-uri/report-to, and sandbox don't work when CSP is set via <meta> — only via an actual HTTP header. The reason is timing: those specific protections need to be enforced before the page starts loading anything, and a meta tag sitting inside the HTML is already too late for that. This meta version is a fallback for when I don't control server headers (static hosting), not a full substitute.

Referrer Policy — what strict-origin-when-cross-origin actually sends

<meta name="referrer" content="strict-origin-when-cross-origin">

Breaking down what this value actually does, since "strict-origin-when-cross-origin" doesn't explain itself:

  • Same-origin request → sends the full URL (path + query string).
  • Cross-origin request, but same protocol security level (https → https) → sends only the origin (scheme + host), not the path.
  • Downgrade request (https → http) → sends nothing at all.

Why this matters: without it, clicking an outbound link can leak my site's internal paths and query strings to whatever site the link points to. Note to self: most modern browsers already default to this exact policy now, so this tag is more about being explicit and guarding against a future default change (or another policy set elsewhere conflicting) than strictly necessary today.

Other values worth remembering if I need something stricter or looser: no-referrer (send nothing, ever — most private, but can break analytics or referrer-based auth flows), same-origin (nothing at all cross-origin), unsafe-url (always send the full URL — avoid, leaks the most).

PWA / Browser

<meta name="theme-color" media="(prefers-color-scheme: light)" content="#ffffff">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#09090b">

Colors the browser's own UI chrome (mobile address bar, OS task-switcher card) to match the page. Two declarations because prefers-color-scheme is a media query — a single tag without it would apply the same color regardless of system theme.

<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Sanudin">
  • mobile-web-app-capable — puts the page into standalone mode (no URL bar, no browser nav) when launched from the home screen. This is the current, unprefixed tag — Chrome DevTools actively flags the old apple-mobile-web-app-capable as deprecated now and asks for this version instead. Safari historically only recognized the apple-prefixed one, so I may still need both for older iOS — worth rechecking current Safari support before dropping the legacy tag entirely. Also: as of iOS 26, Safari defaults every home-screen-added site to open in web-app mode regardless, so this tag matters less on iOS than it used to.
  • apple-mobile-web-app-status-bar-style — still Apple-only, no standardized replacement exists yet. black-translucent lets page content scroll edge-to-edge under the status bar; default/black keep a solid bar instead.
  • apple-mobile-web-app-title — also still Apple-specific, controls the label under the home screen icon. The modern approach is to set this via name/short_name in manifest.json instead — keeping this meta tag around as a fallback since Safari doesn't always read the manifest name reliably.

Bigger note to self: the actual current guidance (Chrome/web.dev) is to lean on the Web App Manifest for all of this long-term, not these meta tags — they predate the manifest spec and can produce worse fallback behavior if the manifest fails to load. Treat everything above as a practical compatibility layer, not the ideal end state.

<link rel="manifest" href="/manifest.json">
<link rel="apple-touch-icon" href="/icons/apple-icon-180.png">
<link rel="icon" href="/favicon.ico" sizes="any">
<link rel="icon" href="/icon.svg" type="image/svg+xml">

This is the actual foundation — manifest.json is where name, icons, start_url, display mode, and theme colors are meant to live going forward. apple-touch-icon stays as a separate link because Safari treats it as an override rather than reliably reading icons from the manifest.

Quick Table

TagPurpose
charsetCorrect character encoding
viewportResponsive mobile rendering
descriptionSERP snippet text
robotsIndex/crawl instructions
canonicalAvoid duplicate-content issues
og:*Preview cards (FB, LinkedIn, Discord)
twitter:*Preview cards on X
robots: noai, noimageaiVoluntary AI-training opt-out signal
robots.txt (GPTBot, etc.)Site-wide AI crawler blocking
JSON-LD (Person)Structured identity data for AI/search
Content-Security-PolicyRestrict allowed script/style/media sources
referrerControl outbound referrer data
theme-colorMobile browser chrome color
mobile-web-app-capableStandalone launch mode (current tag)
apple-mobile-web-app-*iOS-specific home screen behavior
manifestPWA installability

What I Actually Use

Always: charset, viewport, description, canonical, og:/twitter:.

Once the basics are set: robots (only when I actually need noindex), theme-color, PWA tags, CSP.

Situational, revisit if it matters that day: GEO tags, JSON-LD.