Skip to content

Site checklist

For a static content site (blog, documentation). Stack-independent — HTML tags, files at the site root, HTTP headers, DNS records. Roughly ordered by effort/return.

Three files cover every client. Generate all of them from one SVG source at build time.

  • favicon.ico, multi-size 16/32/48. Not a bare 16×16 — Google renders at 32px minimum and wants a source above 48px, so a 16px original ships upscaled.
  • icon.svg, with an explicit type="image/svg+xml". Put a prefers-color-scheme media query in the SVG’s own <style> block and one file covers light and dark tabs — no JS, no extra request. Safari ignores it; the light version shows.
  • apple-touch-icon.png at 180×180. iOS ignores rel="icon" and bookmarks a screenshot of the page without it. Crop to the alpha bounding box and centre on white — iOS composites transparency onto black.
<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<!-- PWA only: -->
<link rel="manifest" href="/manifest.webmanifest">
  • Google reads BMP, GIF, ICO, PNG, JPEG, PPM, TIFF — not SVG. Verify with google.com/s2/favicons?domain=<host>; blank means no raster.
  • Keep icon URLs stable. Expect days to weeks before a change shows in results.
  • 192/512 PNGs + manifest.webmanifest buy an Android home-screen install. Skip unless you want that.

Sources: Google Search Central · RealFaviconGenerator

  • Emit the full Open Graph + Twitter tag set: og:type=article with article:published_time, twitter:card=summary_large_image, and og:image:alt.
  • Share image 1200×630 (1.91:1). Declare og:image:width/height so the crawler doesn’t have to fetch it to lay out the card.
  • Generate a share image per page from that page’s own title and description.
  • Budget the text separately: ~160 chars for meta description, ~220 for the card. Whole sentences, never a cut phrase.
  • Debug with opengraph.dev.
  • title ≤ 60 chars, description ≤ 160 — where Google truncates.
  • Derive a description per page (subtitle, else first prose paragraph). Never let pages inherit one site-wide blurb.
  • JSON-LD: WebSite + Organization on the home page; BreadcrumbList + an article type elsewhere, with author, datePublished, dateModified, and image equal to og:image. Don’t assert more than the visible page says.
    • Article for a blog post, BlogPosting for a dated one, TechArticle for documentation — the same fields, a more specific claim.
    • Other types worth knowing for a content site: FAQPage, QAPage, HowTo, SoftwareApplication, Dataset, AboutPage, Person.
    • Full vocabulary: schema.org. What Google actually does something with is a much shorter list — search gallery. Validate with the Rich Results Test (Google’s view) and the Schema Markup Validator (everything else).
  • <meta name="robots" content="max-image-preview:large">. Without it Google shows thumbnail-size images only, and the page cannot appear in Discover at all. One tag, site-wide. Use X-Robots-Tag for non-HTML files.
  • Sitemap lastmod from the content’s real change date (git commit date), not the build date. Google retired the sitemap ping endpoint in 2023 — lastmod is the ping now.
  • Check links in CI (lychee or equivalent), internal and external. Link rot is the default state of a site nobody checks.
  • One <h1>, semantic <main>/<article>/<nav>, no JS-gated content, stable anchors.
  • Audit the built HTML in CI: titles, descriptions, canonicals, heading order, image alt and dimensions, structured data, sitemap-vs-shipped-files, internal links, crawl directives. Lighthouse gives a score, not a diff.
  • Cite sources and include statistics; don’t keyword-stuff. Princeton GEO study (KDD 2024): +40%, +37%, −10% respectively.

Ship a Markdown twin of every page. Generate it from the Markdown source if you have one.

Ways to serve it, strongest first:

  • Content negotiation at the page URL — an edge function that returns Markdown when Accept: asks for it. An agent hitting the normal URL gets Markdown with no second request and nothing to discover. Set Vary: Accept, and check your CDN actually honours it — some vary only on Accept-Encoding, which lets a cached Markdown response reach a browser.
  • A guessable flat URL/<slug>.md. Costs nothing and an agent can try it blind.
  • <link rel="alternate" type="text/markdown"> and an HTTP Link: header. Correct, but fallbacks — assume most agents don’t look.
  • llms.txt + llms-full.txt. Widely talked about, weakest of the four in practice: a whole extra document an agent has to know to fetch. If you ship one, curate it — 10–50 links in sections, not an exhaustive dump of every URL.

Serve every twin as Content-Type: text/markdown; charset=utf-8 — RFC 7763 makes the charset parameter mandatory.

  • robots.txt: allow everything, name the AI agents explicitly (OAI-SearchBot, PerplexityBot, Claude-SearchBot, Google-Extended, GPTBot, ClaudeBot), and add Content-Signal search=yes, ai-input=yes, ai-train=yes. Content-Signal is a group directive and a crawler obeys exactly one group — repeat it in every group, don’t declare it once under *.

Hand the URL to any agent with Lighthouse and let it work the report to green. The fixes are mechanical: image formats and dimensions, render-blocking CSS/JS, font loading, cache headers.

Images it will flag, but not tell you the whole fix:

  • Explicit width/height attributes on every image, plus img { width: 100%; height: auto; } in CSS. The attributes give the browser an aspect ratio to reserve space with; height: auto is what lets it scale responsively instead of stretching to the literal attribute height. Both halves or neither — width: 100% alone distorts.
  • loading="lazy" below the fold, decoding="async".

Two it won’t suggest at all:

  • Speculation Rules — a <script type="speculationrules"> block that prerenders the next page on hover or when a link enters the viewport. Navigation becomes instant. Chromium only, degrades to nothing elsewhere, and a static site can afford to be eager.
  • LQIP for anything above the fold — a placeholder small enough to inline in the HTML, so there is something in the box before the real image lands. Doesn’t help the metrics that width/height already fix; it fixes what the wait looks like. ThumbHash (~25 bytes, handles alpha and aspect ratio) over blurhash, or lqip-modern for a tiny inline WebP. See hash algorithms.

The one thing you can’t retrofit.

  • Pick one canonical host and one trailing-slash policy. 301 every other form to it.
  • <link rel="canonical"> on every page, absolute.
  • Never break a URL. Keep a redirect map in the repo and only ever append to it.
  • The 404 page must return status 404. A catch-all serving 200 makes every typo an indexable duplicate.
  • Slugs from the title, not the date or an id. No query params on content pages.
  • RSS/Atom, full content, declared with <link rel="alternate" type="application/rss+xml">.
  • On-site search: Pagefind — static chunked index built at build time, no server or SaaS.

Set at the host (_headers or equivalent):

  • Strict-Transport-Security, X-Content-Type-Options: nosniff, Referrer-Policy, Permissions-Policy, and a Content-Security-Policy including frame-ancestors. With no inline scripts a static site can run a strict CSP.
  • Cache-Control: immutable only for content-hashed filenames. Everything else keeps ETag revalidation.
  • /.well-known/security.txt (RFC 9116).
  • Grade at securityheaders.com or Mozilla Observatory.
  • SPF, DKIM, DMARC even if the domain never sends mail — otherwise anyone can spoof it. Sending nothing: -all SPF, p=reject DMARC, and a null MX (RFC 7505).
  • CAA record to pin which CAs may issue for the domain.
  • TLS itself needs no work — Let’s Encrypt is automatic on Netlify, Cloudflare, Vercel, GitHub Pages.

Same structure crawlers and LLMs extract.

  • <html lang>, real alt text, no skipped heading levels, visible focus states, contrast, a skip link, prefers-reduced-motion.
  • Run axe or pa11y in CI.
  • Google Search Console: verify by DNS TXT at the apex, and register the domain property, not the URL-prefix one. Covers every subdomain and survives a host migration.
  • Submit the sitemap. That is the whole of discovery.
  • Register with Bing Webmaster Tools too.
  • IndexNow on deploy: one ping reaches Bing, Naver, Seznam, Yep. Google does not support it — for Google, sitemap lastmod is the signal.
  • Submit the site to the Wayback Machine once.

Worth doing for distribution, not for SEO. rel=canonical is a hint, not a directive, and pointing one from a high-authority platform at a low-authority personal site is the case Google is most likely to ignore — the platform’s copy can end up as the indexed one. It passes no link equity either; platform outbound links are nofollow. Without a canonical it is strictly worse: two copies compete and the stronger domain wins.

  • Publish on your own site first and confirm it is indexed, then cross-post. The order matters more than the tag.
  • dev.to: canonical_url in frontmatter or API. Hashnode: canonical field in the editor and API. Medium: only via Import Story, which sets it automatically — writing in the editor directly does not.
  • Use the absolute URL in your chosen host and trailing-slash form.
  • Posting a link to HN / lobste.rs / Reddit reaches the same audience with no duplicate at all.

Google’s E-E-A-T ranks who is saying this. An unattributed page is the weakest form.

  • Ship an about/author page.
  • Point every page’s JSON-LD author at that page, so authorship resolves somewhere instead of dead-ending at a name. Emit AboutPage + Person on it, with sameAs.
  • rel="me" links to your GitHub/Mastodon profiles.
  • Bluesky: a _atproto DNS TXT record makes your domain your handle — the platform’s actual verification mechanism, and free.
  • <meta name="fediverse:creator" content="@you@instance"> (Mastodon 4.3+) for an author byline under link previews. Allow the domain under Preferences → Public profile → Verification.
  • State a content licence explicitly.
  • Pick a cookieless analytics tool (Plausible, Umami, Cloudflare Web Analytics, GoatCounter). No client-side storage means no consent banner, which is both less code and less friction than the alternative.
  • Know the retention before you pick: Cloudflare keeps unsampled data ~7 days then aggregates; some tools keep months but expose no API without a paid plan.
  • Search Console only compares two periods and exports the current one, and drops data after 16 months. If the numbers matter, snapshot them into the repo on a schedule.
  • Domain auto-renew on, registrar lock on. An expired domain loses the URLs, the backlinks, the email, and the search history at once. It is the only failure on this page you cannot undo.
  • Uptime check on the home page.
  • Keep the build reproducible and the content in git. A static site’s real backup is the repo, not the host.

If the domain actually sends mail (newsletter, transactional), you get a logo in the inbox via BIMI — the email counterpart to a favicon.

Prerequisites, in order:

  1. SPF and DKIM passing, aligned.
  2. DMARC at enforcementp=quarantine or p=reject. p=none does not qualify.
  3. Logo as SVG Tiny PS, square, under 32 KB, served over HTTPS.
  4. A BIMI DNS TXT record at default._bimi.<domain> pointing at the SVG (and the certificate, if any).

The certificate is the expensive part, and it decides where the logo shows:

  • VMC (Verified Mark Certificate) — needs a registered trademark. Gets the logo plus the blue checkmark in Gmail, and rendering in Apple Mail (iOS 16 / macOS 13+).
  • CMC (Common Mark Certificate) — no trademark, but the logo must have been in continuous public use on the domain for 12+ months (the CA checks web archives). Logo only, no checkmark, Gmail and Yahoo only.
  • Either runs roughly $750–1750/yr plus trademark costs, 2–4 weeks to validate. Sectigo is the main CA now — Entrust exited and sold to them.

Worth it for a brand sending volume; not for a personal blog’s newsletter. Steps 1–2 are worth doing regardless. Apple Business Connect is a separate, certificate-free path to an Apple Mail logo.