Files
hoelee-blog/docs/seo-reference.md
T
hoelee 7f2680ea92
Deploy / build (push) Successful in 33s
feat(related): photo cards — 6 on desktop, 4 on phones, each post's own share card
Related posts were 4 text-only cards. They now carry the post's language-specific
ogImage (/og/<slug>.jpg for EN, /og/zh-<slug>.jpg for ZH), so an English page never
shows Chinese art. Grid is 3 across above 900px, 2 across below, and phones show the
first 4 only; read-time is dropped under 600px so the date fits.

Verified in the built HTML + headless Chrome at 1264/884/500px: 3/2/2 columns,
6/6/4 visible cards, no horizontal overflow, all six images decoded.
2026-10-20 22:50:37 +08:00

73 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SEO & Metadata Reference — blog.hoelee.com
Operational reference for the SEO/GEO decisions baked into this project. Read when adding posts, changing `<head>` markup, or debugging search visibility.
---
## Two-lane strategy (2026)
Optimize for **both**:
1. **Classic SEO** — Google blue links (still the majority of name searches).
2. **GEO (Generative Engine Optimization)** — getting cited by Google AI Overviews, ChatGPT Search, Perplexity. They pull from well-structured, question-answering content.
---
## Identity & E-E-A-T (do this first, once)
The entire blog strategy is a **name-search play**, so identity must be airtight:
- **One name everywhere:** "Lee Teong Hoe" / "Mr Hoelee" — pick one and keep it identical on blog, LinkedIn, GitHub, git.hoelee.com.
- **One photo** across all profiles.
- **`sameAs` handles** in the `Person`/`ProfilePage` schema → LinkedIn, GitHub, git.hoelee.com.
- **`ProfilePage` schema** wrapping every post's author bio — how Google connects my writing to my professional identity.
---
## Per-post checklist
- [ ] Unique title ≤ 60 chars (target keyword near front)
- [ ] Meta description ≤ 155 chars
- [ ] Canonical URL set
- [ ] Custom `og:image` (1200×630) — **never** the reused logo
- [ ] `og:image:width/height` declared (else WhatsApp/Telegram/LinkedIn reject the preview)
- [ ] `twitter:card: summary_large_image`
- [ ] Category + tags set
- [ ] Author card present
- [ ] Internal links to ≥1 pillar post
---
## Structural checklist (site-wide)
- [ ] `sitemap.xml` + `robots.txt` + RSS — auto-generated by Astro
- [ ] Core Web Vitals 90+ (verify Lighthouse on 390px **and** 1280px)
- [ ] Question-shaped H2/H3 (the phrasing a learner actually searches)
- [ ] Related-posts module (6 cards) on every post
- [ ] No orphaned/thin pages (avoid the hoelee.com ~400-attachment mistake)
### Crawler / sitemap mechanics (as built)
- `robots.txt` is a **build-time endpoint** (`src/pages/robots.txt.ts`), not a static file in `public/`. It interpolates `SITE.url` so the `Sitemap:` line can't drift. **Keep it pure ASCII** — the response has no charset, so a non-ASCII byte renders as mojibake (`鈥�`) in viewers that fall back to a legacy codepage. Check: `LC_ALL=C grep -c '[^ -~]' dist/robots.txt` must be `0`.
- Policy is **allow-all**, including AI *training* crawlers. Rationale: `robots.txt` is advisory and unenforced (Cloudflare documents it as voluntarily honoured), so a blocklist there buys nothing; enforcement, if ever wanted, belongs in Cloudflare AI Crawl Control. `/pagefind/` is the only exclusion (build artifacts, not content).
- **Why a real robots.txt matters at all:** writing one turned Cloudflare's *Content Signals Policy* placeholder from a **replacement** into a **prepend**. On a Free-plan zone with no origin `robots.txt`, Cloudflare serves that placeholder *in place of* your file — a comment block with no `User-agent`, no directives and no `Sitemap:` line. Confirm the origin is serving yours before blaming Astro.
- Sitemap `lastmod` + `xhtml:link` hreflang alternates are **derived**, never authored — see the rule in the project skill. `sitemap-index.xml` legitimately contains exactly one entry (`sitemap-0.xml`) and looks empty in a viewer; the URLs are in `sitemap-0.xml` (77 of them at 29 posts). Don't "fix" the index.
- hreflang exists in **two places on purpose**: in-page `<link rel="alternate">` (from `BaseLayout`'s `altLocaleUrl`) and sitemap `xhtml:link`. Google treats the sitemap as more authoritative when they disagree, so both must be kept in sync — which is automatic for posts and for any page that passes `altLocaleUrl`.
---
## Syndication policy
- Primary home = **this domain** (own the SEO).
- **Cross-post to Dev.to / Medium** with a `rel=canonical` pointing back to blog.hoelee.com — reach without platform lock-in.
- Never make a third-party platform the primary host.
---
## The "hard job → post" GEO framing
When writing, phrase the opening as the exact question someone types into an AI engine or Google:
> "How do I set up site-to-site OpenVPN behind CGNAT?"
Then answer it directly with clear H2/H3 headings. This is what both Google and the AI engines cite.