Deploy / build (push) Successful in 33s
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.
73 lines
4.1 KiB
Markdown
73 lines
4.1 KiB
Markdown
# 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.
|