feat(seo): related-essays footer for internal linking#113
Merged
Conversation
Adds a "Related essays" block at the bottom of every essay detail page,
computed from shared topics with a recency fallback. Internal linking is
the single most undervalued lever for both SEO and GEO:
- SEO: distributes link equity across essays, improves crawl depth, lifts
orphan pages. Previously every essay was a dead end.
- GEO: gives AI engines more contexts to encounter each essay, increasing
the chance an essay is cited from a topically adjacent query.
- UX: real readers stay on site longer.
Algorithm (lib/essays.ts → getRelatedEssays):
1. Filter to same language, non-draft, not the essay itself, not the
essay's translation (in either direction).
2. Score each candidate by count of topics it shares with the source.
3. Sort by score desc, then date desc.
4. Take top `limit` (default 3).
5. If fewer than `limit` candidates share a topic, fill the remainder
with the most recent essays in the same language. Avoids an empty
block for niche topics.
Component (components/essay/RelatedEssays.tsx):
Server component. Renders nothing when the essays array is empty. Each
item shows topic eyebrow, title (serif, link-on-hover), description
(2-line clamp), and date + reading time. Composition uses the same
hairline-rule / restrained-typography idiom as the OG card so the
on-page block and the social card share a visual language.
i18n:
Added `essay.related.heading` to en.json and zh.json.
Tests: 5 new tests covering ranking, exclusion of self/translation,
language filter, limit, and missing-source. 169 total tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refines the related-essays algorithm based on review feedback. The previous version recommended unrelated essays via recency fallback, which was the right complaint: - decision-game (topics: technical, ai) recommended a career essay, surfaced only because there weren't 3 strict-overlap candidates and the algorithm padded with most-recent essays. That dilutes link equity, signals "automated content widget" to search and AI engines, and reads as noise to readers. Changes: 1. Author-curated override via `relatedTo: [slug, slug]` frontmatter. When set, those slugs ARE the related list, in order. Slugs that don't resolve, that point at the source itself, that cross languages, or that point at drafts are dropped silently. Capped at `limit`. Manual curation is the strongest editorial signal — both for readers and for AI engines weighing relevance. 2. Auto-rank replaces raw shared-topic count with **Jaccard similarity** (|intersection| / |union|) plus a +0.15 same-type bonus. Jaccard naturally penalizes essays that share one common-but-cheap tag while spanning many unrelated topics. 3. **No recency fallback.** Candidates that share zero topics with the source are dropped. If no candidate qualifies, the block hides itself. Better silence than noise. Why this is better for SEO + GEO, not worse: - Link equity divides by number of outbound links. 1-2 strong related links carry more weight per link than 3 mediocre ones. - Topical-relevance is a stronger ranking signal than link density. Google's HCU and AI answer engines actively penalise patterns that read as automated related-content widgets. - Manual `relatedTo` gives the destination essay a human-vouched inbound link, which AI engines weight more heavily than algorithmic ones. Tests: 5 new cases — type-match tiebreaker, curated list order + drops, curated cap, and explicit "no recency fallback" assertion. 174 total tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the card-list layout (eyebrow + title + description clamp +
date footer with divider lines) that read like a generic feed widget,
with a minimalist numbered-list composition matching the OG card's
editorial register.
Composition:
RELATED
01 The Era of Decision Games 18 min
02 Career Reflection — 2023 12 min
03 Negotiating Software Engineering 8 min
- Small uppercase "RELATED" eyebrow (matches the OG card kicker style).
- Numbered <ol> with tabular-figure prefix in muted ink.
- Serif title flex-grows; reading time pinned right; hairline rule
between rows via border-b on each <li>.
- No description, no date, no card boxes. Reads like a back-of-book
"Further Reading" appendix.
Translations shortened to match the eyebrow style:
essay.related.heading: "Related essays" → "Related"
"相关文章" → "相关"
The semantic <h2> still exists for accessibility (aria-labelledby keeps
working); only its visible text is short.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous render had three visible problems caused by the same
root: the RelatedEssays block was rendered inside .essay-content,
which applies an editorial typography cascade (h2 → huge headline,
a → underlined link, ol → decimal markers with padding-left).
Result: duplicated numbering ("1." from .essay-content ol AND "01"
from my decorative span), an oversized "RELATED" eyebrow, and
underlined links throughout.
Fixes:
- EssayLayout: add a `footer` prop that renders OUTSIDE the
.essay-content div. The footer wrapper has no .essay-content class
so its descendants are not subject to the article typography
cascade. Width matches the article body (38-42rem responsive) so
the block aligns visually below the content.
- RelatedEssays: drop the decorative `<span>01</span>` prefix per
feedback. Keep <ol> for semantics, restore list-decimal +
marker:text-figure-muted so the native numbering ("1.", "2.")
renders cleanly in the proper muted color. Add no-underline on
the Link to defeat any remaining anchor-underline inheritance.
- Both essay detail pages: pass RelatedEssays via EssayLayout's new
`footer` prop instead of as a child of <MDXRemote>.
Now renders as a minimalist editorial list:
相关
1. 程序扩展人类智能 阅读时间 2 分钟
2. 10000行C++之后对C++的思考 阅读时间 15 分钟
3. 一些细节看程序员的质量和习惯 阅读时间 2 分钟
174 tests pass; build clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a "Related essays" block at the bottom of every essay detail page (EN and ZH), computed from shared topic tags with a recency fallback. Internal linking is the most undervalued lever for both SEO and GEO — and it costs almost nothing to ship.
Why this matters
Algorithm
getRelatedEssays(slug, { limit = 3 }):limit.limitessays share a topic, fill the remainder with the most-recent essays in the same language. Avoids an empty block for niche topics.What lands
lib/essays.ts—getRelatedEssays()helpercomponents/essay/RelatedEssays.tsx— server component, hairline-rule editorial layout matching the OG card idiomcomponents/essay/RelatedEssays.stories.tsx— 4 Storybook variants (English, Chinese, single result, empty)app/(en)/essays/[slug]/page.tsxandapp/(zh)/zh/essays/[slug]/page.tsx— integrationessay.related.headingin en.json (Related essays) and zh.json (相关文章)Verified
/essays/decision-game/renders the block with proper related items/zh/essays/decision-game-zh/renders the block with proper related itemsOut of scope (deferred)
seriesfield on essay frontmatter or a series index that lists members. Out of scope for this PR.Test plan
<aside aria-labelledby="related-essays-heading">is in the SSR HTMLEssay / RelatedEssaysshows all 4 variants🤖 Generated with Claude Code