Skip to content

Commit 9ff83a9

Browse files
Land swept WIP: concept page titles and canonicals, SEO roadmap (#127)
Recovers wip/sweep-20260926. Concept pages set their own title and point canonical at the static curriculum page, restoring the shell title and canonical when the route changes. Adds the queued SEO roadmap (the repo stays maintenance-only). The swept JSON-LD edit to index.html is not included: that block is generated from the agent-surfaces registry. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7cb6f80 commit 9ff83a9

2 files changed

Lines changed: 67 additions & 1 deletion

File tree

‎docs/seo-sprint.md‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# swe-interview-prep SEO sprint roadmap
2+
3+
> Note: this repo is **maintenance-only since 2026-07-10** (`STATUS.md`). These
4+
> phases are queued intent, not active work — resume only if the owner lifts
5+
> maintenance mode; until then the indexing loop below still runs passively.
6+
7+
Satellite playbook (see `saas-maker/tooling/skills/seo-sprint`). Baseline
8+
(28d ending 2026-09-18): **8,274 imp · 2 clicks · pos 24 · 23 indexed / 300
9+
pending** — big impression volume on junk queries; real queries rank but get
10+
zero clicks.
11+
12+
## Evidence
13+
14+
- **Junk signal**: most top impressions are `"indocad" …` copied-content
15+
searches at pos 2–3 — don't optimize for these; they're people hunting a
16+
leaked package, not interview prep.
17+
- **Real signal**: `"explain_estimated_rows"` pos 7 — Postgres EXPLAIN
18+
internals. Curriculum content is surfacing for genuine technical questions.
19+
20+
## Phases
21+
22+
- [ ] **Phase 1 — topic landing pages for real interview queries.** From the
23+
curriculum: system design, Postgres/database internals, behavioral, coding
24+
patterns. Each page = the actual curriculum section + practice questions,
25+
not a stub. Target the query classes where pos is already 2–8.
26+
- [ ] **Phase 2 — question pages.** One page per curriculum question with a
27+
real worked answer — the "<question> interview" long tail. Buildable from
28+
`shared/data`.
29+
- [ ] **Phase 3 — company-tagged pages** ("<company> interview questions")
30+
only where the curriculum genuinely covers that company's known pattern —
31+
don't fabricate company specificity.
32+
- [ ] **Phase 4 — indexing + CTR.** 300 pending churn via the daily agent;
33+
pages at pos 2–8 with 0 clicks need title/snippet work (the scoreboard
34+
flags them as `striking-distance`).
35+
36+
## Rules
37+
38+
- Never write an answer the curriculum doesn't actually teach — interview
39+
searchers spot fake prep instantly.
40+
- Register each phase: `seo-scoreboard.mjs register --project
41+
swe-interview-prep --lane editorial|programmatic --summary "…"`.

‎src/pages/ConceptDetail.tsx‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
Lock,
88
Sparkles,
99
} from 'lucide-react';
10-
import { useState } from 'react';
10+
import { useEffect, useState } from 'react';
1111
import { Link, useParams } from 'react-router-dom';
1212

1313
import ConceptLibrary from '../components/ConceptLibrary';
@@ -52,13 +52,38 @@ const RATINGS: { id: 'again' | 'hard' | 'good' | 'easy'; label: string; tone: st
5252
{ id: 'easy', label: 'Easy', tone: 'emerald' },
5353
];
5454

55+
const CANONICAL_ORIGIN = 'https://learn.significanthobbies.com';
56+
5557
export default function ConceptDetail() {
5658
const { id } = useParams();
5759
const concept = id ? CONCEPT_BY_ID[id] : undefined;
5860
const { mastery, review } = useConceptMastery();
5961
const { gateStatus } = useGates();
6062
const [copied, setCopied] = useState(false);
6163

64+
// The SPA shell serves a generic <title> and a site-root canonical. Give each
65+
// concept its own title and point canonical at the static curriculum page so
66+
// the duplicate /concepts/ and /curriculum/concepts/ URLs consolidate
67+
// ranking signals. Restore both when the route changes.
68+
useEffect(() => {
69+
if (!concept) return;
70+
const previousTitle = document.title;
71+
let canonical = document.head.querySelector<HTMLLinkElement>('link[rel="canonical"]');
72+
const previousHref = canonical?.getAttribute('href') ?? null;
73+
if (!canonical) {
74+
canonical = document.createElement('link');
75+
canonical.rel = 'canonical';
76+
document.head.appendChild(canonical);
77+
}
78+
document.title = `${concept.name} · SWE Prep`;
79+
canonical.href = `${CANONICAL_ORIGIN}/curriculum/concepts/${concept.id}`;
80+
return () => {
81+
document.title = previousTitle;
82+
if (previousHref === null) canonical.remove();
83+
else canonical.setAttribute('href', previousHref);
84+
};
85+
}, [concept]);
86+
6287
if (!concept) {
6388
return (
6489
<PageShell>

0 commit comments

Comments
 (0)