|
| 1 | +# Search Console 내부 링크 무결성 Implementation Plan |
| 2 | + |
| 3 | +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (\`- [ ]\`) syntax for tracking. |
| 4 | +
|
| 5 | +**Goal:** 미발행 Spring AI 4편 링크를 실제 글로 교체하고, 존재하지 않는 \`/posts/:slug\` Markdown 내부 링크를 CI 또는 로컬에서 검출한다. |
| 6 | + |
| 7 | +**Architecture:** \`scripts/check-internal-post-links.mjs\`는 Markdown 파일명에서 게시글 slug 집합을 만들고, Markdown 링크 목적지의 \`/posts/:slug\`만 비교한다. 순수 검사 함수를 내보내 Node 내장 테스트로 검증하고, CLI 실행은 \`package.json\` 스크립트로 노출한다. |
| 8 | + |
| 9 | +**Tech Stack:** Node.js ESM, Node 내장 \`node:test\`, TypeScript/Vite 빌드, Markdown 콘텐츠. |
| 10 | + |
| 11 | +## Global Constraints |
| 12 | + |
| 13 | +- 외부 링크, 앵커 링크, 쿼리 문자열, \`/posts/\` 이외의 내부 라우트는 검사하지 않는다. |
| 14 | +- 누락된 링크는 파일 경로와 원래 링크 대상 전체를 모두 출력하고 종료 코드 1로 실패한다. |
| 15 | +- 기존 미추적 \`AGENTS.md\`는 스테이징하거나 수정하지 않는다. |
| 16 | +- Search Console에서 확인된 리디렉션·canonical 제외 항목을 코드로 자동 수정하지 않는다. |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +### Task 1: Markdown 게시글 링크 검사기와 회귀 테스트 |
| 21 | + |
| 22 | +**Files:** |
| 23 | + |
| 24 | +- Create: \`scripts/check-internal-post-links.mjs\` |
| 25 | +- Create: \`scripts/check-internal-post-links.test.mjs\` |
| 26 | +- Modify: \`package.json:6-12\` |
| 27 | + |
| 28 | +**Interfaces:** |
| 29 | + |
| 30 | +- Consumes: \`content/posts/*.md\`의 파일명과 Markdown 본문. |
| 31 | +- Produces: \`findMissingPostLinks(posts)\` 및 \`npm run check:internal-links\`. |
| 32 | + |
| 33 | +- [ ] **Step 1: 누락 slug를 찾는 실패 테스트 작성** |
| 34 | + |
| 35 | +\`\`\`js |
| 36 | +import test from "node:test" |
| 37 | +import assert from "node:assert/strict" |
| 38 | +import { findMissingPostLinks } from "./check-internal-post-links.mjs" |
| 39 | + |
| 40 | +test("존재하지 않는 게시글 slug를 내부 링크 오류로 보고한다", () => { |
| 41 | + const missing = findMissingPostLinks([ |
| 42 | + { path: "content/posts/existing.md", content: "[누락 글](/posts/not-published)" }, |
| 43 | + { path: "content/posts/other.md", content: "[존재 글](/posts/existing)" }, |
| 44 | + ]) |
| 45 | + |
| 46 | + assert.deepEqual(missing, [{ |
| 47 | + path: "content/posts/existing.md", |
| 48 | + href: "/posts/not-published", |
| 49 | + slug: "not-published", |
| 50 | + }]) |
| 51 | +}) |
| 52 | +\`\`\` |
| 53 | + |
| 54 | +- [ ] **Step 2: 실패를 확인한다** |
| 55 | + |
| 56 | +Run: \`node --test scripts/check-internal-post-links.test.mjs\` |
| 57 | + |
| 58 | +Expected: \`ERR_MODULE_NOT_FOUND\` 또는 \`findMissingPostLinks\` export 누락으로 실패한다. |
| 59 | + |
| 60 | +- [ ] **Step 3: 최소 검사기를 구현한다** |
| 61 | + |
| 62 | +\`\`\`js |
| 63 | +import fs from "node:fs" |
| 64 | +import path from "node:path" |
| 65 | +import { fileURLToPath } from "node:url" |
| 66 | + |
| 67 | +const postLinkPattern = /\[[^\]]*\]\(\/posts\/([^/?#)]+)(?:[?#][^)]*)?\)/g |
| 68 | + |
| 69 | +export function findMissingPostLinks(posts) { |
| 70 | + const slugs = new Set(posts.map((post) => path.basename(post.path, ".md"))) |
| 71 | + const missing = [] |
| 72 | + |
| 73 | + for (const post of posts) { |
| 74 | + for (const match of post.content.matchAll(postLinkPattern)) { |
| 75 | + const slug = match[1] |
| 76 | + if (!slugs.has(slug)) missing.push({ |
| 77 | + path: post.path, |
| 78 | + href: match[0].slice(match[0].lastIndexOf("(") + 1, -1), |
| 79 | + slug, |
| 80 | + }) |
| 81 | + } |
| 82 | + } |
| 83 | + |
| 84 | + return missing |
| 85 | +} |
| 86 | + |
| 87 | +function run() { |
| 88 | + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") |
| 89 | + const postsDir = path.join(root, "content/posts") |
| 90 | + const posts = fs.readdirSync(postsDir) |
| 91 | + .filter((file) => file.endsWith(".md")) |
| 92 | + .map((file) => { |
| 93 | + const filePath = path.join(postsDir, file) |
| 94 | + return { path: path.relative(root, filePath), content: fs.readFileSync(filePath, "utf8") } |
| 95 | + }) |
| 96 | + const missing = findMissingPostLinks(posts) |
| 97 | + |
| 98 | + if (missing.length > 0) { |
| 99 | + console.error("Missing internal post links:") |
| 100 | + for (const item of missing) console.error("- " + item.path + ": " + item.href) |
| 101 | + process.exitCode = 1 |
| 102 | + return |
| 103 | + } |
| 104 | + |
| 105 | + console.log("Validated " + posts.length + " posts with no missing internal post links.") |
| 106 | +} |
| 107 | + |
| 108 | +if (process.argv[1] === fileURLToPath(import.meta.url)) run() |
| 109 | +\`\`\` |
| 110 | + |
| 111 | +Add this \`package.json\` script: |
| 112 | + |
| 113 | +\`\`\`json |
| 114 | +"check:internal-links": "node scripts/check-internal-post-links.mjs" |
| 115 | +\`\`\` |
| 116 | + |
| 117 | +- [ ] **Step 4: 테스트 통과를 확인한다** |
| 118 | + |
| 119 | +Run: \`node --test scripts/check-internal-post-links.test.mjs\` |
| 120 | + |
| 121 | +Expected: 1 test passed, 0 failed. |
| 122 | + |
| 123 | +- [ ] **Step 5: 실제 콘텐츠 전체를 검사한다** |
| 124 | + |
| 125 | +Run: \`npm run check:internal-links\` |
| 126 | + |
| 127 | +Expected: 현재 4편 링크 때문에 \`spring-ai-guide-02-multi-provider.md: /posts/spring-ai-guide-04-production\`을 출력하고 종료 코드 1로 실패한다. |
| 128 | + |
| 129 | +- [ ] **Step 6: 커밋한다** |
| 130 | + |
| 131 | +\`\`\`bash |
| 132 | +git add scripts/check-internal-post-links.mjs scripts/check-internal-post-links.test.mjs package.json |
| 133 | +git commit -m "test: detect missing internal post links" |
| 134 | +\`\`\` |
| 135 | + |
| 136 | +### Task 2: Spring AI 4편 404 링크 수정 |
| 137 | + |
| 138 | +**Files:** |
| 139 | + |
| 140 | +- Modify: \`content/posts/spring-ai-guide-02-multi-provider.md:118\` |
| 141 | + |
| 142 | +**Interfaces:** |
| 143 | + |
| 144 | +- Consumes: \`spring-ai-pipeline-real-world.md\` slug와 Bedrock 타임아웃 설정 설명. |
| 145 | +- Produces: 존재하는 글만 가리키는 Spring AI 멀티 프로바이더 글. |
| 146 | + |
| 147 | +- [ ] **Step 1: 수정 전 실패 상태를 재현한다** |
| 148 | + |
| 149 | +Run: \`npm run check:internal-links\` |
| 150 | + |
| 151 | +Expected: \`spring-ai-guide-02-multi-provider.md\`의 \`/posts/spring-ai-guide-04-production\` 누락 링크 때문에 실패한다. |
| 152 | + |
| 153 | +- [ ] **Step 2: 링크를 실제 타임아웃 설명 글로 변경한다** |
| 154 | + |
| 155 | +118행의 문장을 아래로 변경한다. |
| 156 | + |
| 157 | +\`\`\`md |
| 158 | +\`DefaultCredentialsProvider\`는 AWS의 기본 인증 체인(환경변수, EC2 인스턴스 프로파일, ECS 태스크 역할 등)을 따른다. 로컬에서는 \`~/.aws/credentials\`, 배포 환경에서는 IAM 역할을 자동으로 사용한다. 연결 및 소켓 타임아웃 설정은 [Spring AI 실전 적용기](/posts/spring-ai-pipeline-real-world/)에서 다룬다. |
| 159 | +\`\`\` |
| 160 | + |
| 161 | +- [ ] **Step 3: 링크 검사 통과를 확인한다** |
| 162 | + |
| 163 | +Run: \`npm run check:internal-links\` |
| 164 | + |
| 165 | +Expected: 종료 코드 0과 \`no missing internal post links\` 메시지. |
| 166 | + |
| 167 | +- [ ] **Step 4: 전체 품질 검증을 실행한다** |
| 168 | + |
| 169 | +Run: \`npm run type-check && npm run lint && npm run build && git diff --check\` |
| 170 | + |
| 171 | +Expected: 모든 명령이 종료 코드 0으로 완료된다. |
| 172 | + |
| 173 | +- [ ] **Step 5: 커밋한다** |
| 174 | + |
| 175 | +\`\`\`bash |
| 176 | +git add content/posts/spring-ai-guide-02-multi-provider.md |
| 177 | +git commit -m "fix: replace missing Spring AI post link" |
| 178 | +\`\`\` |
| 179 | + |
| 180 | +### Task 3: Search Console 후속 점검 기록 |
| 181 | + |
| 182 | +**Files:** |
| 183 | + |
| 184 | +- Create: \`docs/search-console/2026-07-12-triage.md\` |
| 185 | + |
| 186 | +**Interfaces:** |
| 187 | + |
| 188 | +- Consumes: Search Console 페이지 색인 보고서의 원인별 수와 확인된 URL. |
| 189 | +- Produces: 다음 진단 시 재사용할 수 있는 조치 우선순위 기록. |
| 190 | + |
| 191 | +- [ ] **Step 1: 조치 항목을 기록한다** |
| 192 | + |
| 193 | +\`\`\`md |
| 194 | +| 우선순위 | 항목 | 근거 | 조치 | |
| 195 | +| --- | --- | --- | --- | |
| 196 | +| P0 | 미발행 Spring AI 4편 내부 링크 | \`/posts/spring-ai-guide-04-production\`이 404이고 현재 글에서 링크됨 | 실제 운영 글 링크로 교체 | |
| 197 | +| P1 | 크롤링/발견됐지만 미색인 13개 | Search Console의 \`크롤링됨\` 8개와 \`발견됨\` 5개 | URL 목록을 내보내어 콘텐츠별 검토 | |
| 198 | +| P2 | 다른 표준 URL을 선택한 중복 31개 | Google 표준 선택 보고서 | 예시 URL의 canonical 및 내부 링크 대조 | |
| 199 | +| 모니터링 | 리디렉션 108개, canonical 대체 83개 | trailing slash·태그 쿼리 URL | 현재 301/canonical 동작을 유지 | |
| 200 | +\`\`\` |
| 201 | + |
| 202 | +- [ ] **Step 2: 변경 범위를 확인한다** |
| 203 | + |
| 204 | +Run: \`git diff --check && git status --short\` |
| 205 | + |
| 206 | +Expected: 링크 검사기, 콘텐츠 링크, 진단 문서만 추적 대상 변경으로 보이며 기존 \`AGENTS.md\`는 미추적으로 유지된다. |
| 207 | + |
| 208 | +- [ ] **Step 3: 커밋한다** |
| 209 | + |
| 210 | +\`\`\`bash |
| 211 | +git add docs/search-console/2026-07-12-triage.md |
| 212 | +git commit -m "docs: record Search Console triage" |
| 213 | +\`\`\` |
| 214 | + |
0 commit comments