diff --git a/content/posts/ko/spring-ai-guide-02-multi-provider.md b/content/posts/ko/spring-ai-guide-02-multi-provider.md index 3ffd955..40660ab 100644 --- a/content/posts/ko/spring-ai-guide-02-multi-provider.md +++ b/content/posts/ko/spring-ai-guide-02-multi-provider.md @@ -115,7 +115,7 @@ public BedrockProxyChatModel bedrockChatModel() { } ``` -`DefaultCredentialsProvider`는 AWS의 기본 인증 체인(환경변수, EC2 인스턴스 프로파일, ECS 태스크 역할 등)을 따른다. 로컬에서는 `~/.aws/credentials`, 배포 환경에서는 IAM 역할을 자동으로 사용한다. 타임아웃 설정은 4편에서 다룬다. +`DefaultCredentialsProvider`는 AWS의 기본 인증 체인(환경변수, EC2 인스턴스 프로파일, ECS 태스크 역할 등)을 따른다. 로컬에서는 `~/.aws/credentials`, 배포 환경에서는 IAM 역할을 자동으로 사용한다. 연결 및 소켓 타임아웃 설정은 [Spring AI 실전 적용기](/posts/spring-ai-pipeline-real-world/)에서 다룬다. ## 프로바이더별 옵션 제어 diff --git a/docs/search-console/2026-07-12-triage.md b/docs/search-console/2026-07-12-triage.md new file mode 100644 index 0000000..b6ba678 --- /dev/null +++ b/docs/search-console/2026-07-12-triage.md @@ -0,0 +1,27 @@ +# Search Console 색인 생성 분류 기록 + +확인일: 2026-07-12 + +## 확인된 현황 + +- 색인 생성됨: 111개 +- 색인 생성되지 않음: 239개 +- 제출된 사이트맵: `https://devy1540.dev/sitemap.xml` (성공, 72개 페이지 발견) + +| 우선순위 | 항목 | 근거 | 조치 | +| --- | --- | --- | --- | +| P0 | 미발행 Spring AI 4편 내부 링크 | `/posts/spring-ai-guide-04-production`이 404이고 현재 글에서 링크됨 | `spring-ai-pipeline-real-world` 글 링크로 교체 완료 | +| P1 | 크롤링/발견됐지만 미색인 13개 | Search Console의 `크롤링됨` 8개와 `발견됨` 5개 | URL 목록을 내보내어 콘텐츠별 검토 | +| P2 | 다른 표준 URL을 선택한 중복 31개 | Google 표준 선택 보고서 | 예시 URL의 canonical 및 내부 링크 대조 | +| 모니터링 | 리디렉션 108개, canonical 대체 83개 | trailing slash·태그 쿼리 URL | 현재 301/canonical 동작을 유지 | + +## 404 세부 항목 + +- `/posts/spring-ai-guide-04-production`: 내부 Markdown 링크가 원인이었으며 수정했다. +- `/cdn-cgi/l/email-protection`: 현재 404이나 블로그 콘텐츠에서 직접 링크하는지 추가 확인이 필요하다. + +## 비범위 + +- Google Search Console의 색인 생성 요청 또는 수정 결과 확인 버튼 실행 +- 호스팅/CDN 계층의 5xx 원인 수정 +- 미발행 Spring AI 4편 작성 diff --git a/docs/superpowers/plans/2026-07-12-search-console-link-integrity.md b/docs/superpowers/plans/2026-07-12-search-console-link-integrity.md new file mode 100644 index 0000000..93b3a00 --- /dev/null +++ b/docs/superpowers/plans/2026-07-12-search-console-link-integrity.md @@ -0,0 +1,214 @@ +# Search Console 내부 링크 무결성 Implementation Plan + +> **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. + +**Goal:** 미발행 Spring AI 4편 링크를 실제 글로 교체하고, 존재하지 않는 \`/posts/:slug\` Markdown 내부 링크를 CI 또는 로컬에서 검출한다. + +**Architecture:** \`scripts/check-internal-post-links.mjs\`는 Markdown 파일명에서 게시글 slug 집합을 만들고, Markdown 링크 목적지의 \`/posts/:slug\`만 비교한다. 순수 검사 함수를 내보내 Node 내장 테스트로 검증하고, CLI 실행은 \`package.json\` 스크립트로 노출한다. + +**Tech Stack:** Node.js ESM, Node 내장 \`node:test\`, TypeScript/Vite 빌드, Markdown 콘텐츠. + +## Global Constraints + +- 외부 링크, 앵커 링크, 쿼리 문자열, \`/posts/\` 이외의 내부 라우트는 검사하지 않는다. +- 누락된 링크는 파일 경로와 원래 링크 대상 전체를 모두 출력하고 종료 코드 1로 실패한다. +- 기존 미추적 \`AGENTS.md\`는 스테이징하거나 수정하지 않는다. +- Search Console에서 확인된 리디렉션·canonical 제외 항목을 코드로 자동 수정하지 않는다. + +--- + +### Task 1: Markdown 게시글 링크 검사기와 회귀 테스트 + +**Files:** + +- Create: \`scripts/check-internal-post-links.mjs\` +- Create: \`scripts/check-internal-post-links.test.mjs\` +- Modify: \`package.json:6-12\` + +**Interfaces:** + +- Consumes: \`content/posts/*.md\`의 파일명과 Markdown 본문. +- Produces: \`findMissingPostLinks(posts)\` 및 \`npm run check:internal-links\`. + +- [ ] **Step 1: 누락 slug를 찾는 실패 테스트 작성** + +\`\`\`js +import test from "node:test" +import assert from "node:assert/strict" +import { findMissingPostLinks } from "./check-internal-post-links.mjs" + +test("존재하지 않는 게시글 slug를 내부 링크 오류로 보고한다", () => { + const missing = findMissingPostLinks([ + { path: "content/posts/existing.md", content: "[누락 글](/posts/not-published)" }, + { path: "content/posts/other.md", content: "[존재 글](/posts/existing)" }, + ]) + + assert.deepEqual(missing, [{ + path: "content/posts/existing.md", + href: "/posts/not-published", + slug: "not-published", + }]) +}) +\`\`\` + +- [ ] **Step 2: 실패를 확인한다** + +Run: \`node --test scripts/check-internal-post-links.test.mjs\` + +Expected: \`ERR_MODULE_NOT_FOUND\` 또는 \`findMissingPostLinks\` export 누락으로 실패한다. + +- [ ] **Step 3: 최소 검사기를 구현한다** + +\`\`\`js +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const postLinkPattern = /\[[^\]]*\]\(\/posts\/([^/?#)]+)(?:[?#][^)]*)?\)/g + +export function findMissingPostLinks(posts) { + const slugs = new Set(posts.map((post) => path.basename(post.path, ".md"))) + const missing = [] + + for (const post of posts) { + for (const match of post.content.matchAll(postLinkPattern)) { + const slug = match[1] + if (!slugs.has(slug)) missing.push({ + path: post.path, + href: match[0].slice(match[0].lastIndexOf("(") + 1, -1), + slug, + }) + } + } + + return missing +} + +function run() { + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") + const postsDir = path.join(root, "content/posts") + const posts = fs.readdirSync(postsDir) + .filter((file) => file.endsWith(".md")) + .map((file) => { + const filePath = path.join(postsDir, file) + return { path: path.relative(root, filePath), content: fs.readFileSync(filePath, "utf8") } + }) + const missing = findMissingPostLinks(posts) + + if (missing.length > 0) { + console.error("Missing internal post links:") + for (const item of missing) console.error("- " + item.path + ": " + item.href) + process.exitCode = 1 + return + } + + console.log("Validated " + posts.length + " posts with no missing internal post links.") +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) run() +\`\`\` + +Add this \`package.json\` script: + +\`\`\`json +"check:internal-links": "node scripts/check-internal-post-links.mjs" +\`\`\` + +- [ ] **Step 4: 테스트 통과를 확인한다** + +Run: \`node --test scripts/check-internal-post-links.test.mjs\` + +Expected: 1 test passed, 0 failed. + +- [ ] **Step 5: 실제 콘텐츠 전체를 검사한다** + +Run: \`npm run check:internal-links\` + +Expected: 현재 4편 링크 때문에 \`spring-ai-guide-02-multi-provider.md: /posts/spring-ai-guide-04-production\`을 출력하고 종료 코드 1로 실패한다. + +- [ ] **Step 6: 커밋한다** + +\`\`\`bash +git add scripts/check-internal-post-links.mjs scripts/check-internal-post-links.test.mjs package.json +git commit -m "test: detect missing internal post links" +\`\`\` + +### Task 2: Spring AI 4편 404 링크 수정 + +**Files:** + +- Modify: \`content/posts/spring-ai-guide-02-multi-provider.md:118\` + +**Interfaces:** + +- Consumes: \`spring-ai-pipeline-real-world.md\` slug와 Bedrock 타임아웃 설정 설명. +- Produces: 존재하는 글만 가리키는 Spring AI 멀티 프로바이더 글. + +- [ ] **Step 1: 수정 전 실패 상태를 재현한다** + +Run: \`npm run check:internal-links\` + +Expected: \`spring-ai-guide-02-multi-provider.md\`의 \`/posts/spring-ai-guide-04-production\` 누락 링크 때문에 실패한다. + +- [ ] **Step 2: 링크를 실제 타임아웃 설명 글로 변경한다** + +118행의 문장을 아래로 변경한다. + +\`\`\`md +\`DefaultCredentialsProvider\`는 AWS의 기본 인증 체인(환경변수, EC2 인스턴스 프로파일, ECS 태스크 역할 등)을 따른다. 로컬에서는 \`~/.aws/credentials\`, 배포 환경에서는 IAM 역할을 자동으로 사용한다. 연결 및 소켓 타임아웃 설정은 [Spring AI 실전 적용기](/posts/spring-ai-pipeline-real-world/)에서 다룬다. +\`\`\` + +- [ ] **Step 3: 링크 검사 통과를 확인한다** + +Run: \`npm run check:internal-links\` + +Expected: 종료 코드 0과 \`no missing internal post links\` 메시지. + +- [ ] **Step 4: 전체 품질 검증을 실행한다** + +Run: \`npm run type-check && npm run lint && npm run build && git diff --check\` + +Expected: 모든 명령이 종료 코드 0으로 완료된다. + +- [ ] **Step 5: 커밋한다** + +\`\`\`bash +git add content/posts/spring-ai-guide-02-multi-provider.md +git commit -m "fix: replace missing Spring AI post link" +\`\`\` + +### Task 3: Search Console 후속 점검 기록 + +**Files:** + +- Create: \`docs/search-console/2026-07-12-triage.md\` + +**Interfaces:** + +- Consumes: Search Console 페이지 색인 보고서의 원인별 수와 확인된 URL. +- Produces: 다음 진단 시 재사용할 수 있는 조치 우선순위 기록. + +- [ ] **Step 1: 조치 항목을 기록한다** + +\`\`\`md +| 우선순위 | 항목 | 근거 | 조치 | +| --- | --- | --- | --- | +| P0 | 미발행 Spring AI 4편 내부 링크 | \`/posts/spring-ai-guide-04-production\`이 404이고 현재 글에서 링크됨 | 실제 운영 글 링크로 교체 | +| P1 | 크롤링/발견됐지만 미색인 13개 | Search Console의 \`크롤링됨\` 8개와 \`발견됨\` 5개 | URL 목록을 내보내어 콘텐츠별 검토 | +| P2 | 다른 표준 URL을 선택한 중복 31개 | Google 표준 선택 보고서 | 예시 URL의 canonical 및 내부 링크 대조 | +| 모니터링 | 리디렉션 108개, canonical 대체 83개 | trailing slash·태그 쿼리 URL | 현재 301/canonical 동작을 유지 | +\`\`\` + +- [ ] **Step 2: 변경 범위를 확인한다** + +Run: \`git diff --check && git status --short\` + +Expected: 링크 검사기, 콘텐츠 링크, 진단 문서만 추적 대상 변경으로 보이며 기존 \`AGENTS.md\`는 미추적으로 유지된다. + +- [ ] **Step 3: 커밋한다** + +\`\`\`bash +git add docs/search-console/2026-07-12-triage.md +git commit -m "docs: record Search Console triage" +\`\`\` + diff --git a/docs/superpowers/specs/2026-07-12-search-console-link-integrity-design.md b/docs/superpowers/specs/2026-07-12-search-console-link-integrity-design.md new file mode 100644 index 0000000..d31e8f2 --- /dev/null +++ b/docs/superpowers/specs/2026-07-12-search-console-link-integrity-design.md @@ -0,0 +1,41 @@ +# Search Console 내부 링크 무결성 설계 + +## 목표 + +존재하지 않는 블로그 글을 가리키는 내부 링크를 제거하고, 이후 Markdown 글의 `/posts/:slug` 링크가 실제 게시 글과 일치하는지 빌드 전에 검증한다. + +## 범위 + +- `spring-ai-guide-02-multi-provider.md`의 미발행 4편 링크를, 실제 타임아웃 설정을 다루는 `spring-ai-pipeline-real-world` 글 링크로 바꾼다. +- `content/posts/*.md`의 상대 사이트 내부 `/posts/:slug` 링크만 검사한다. +- 외부 링크, 앵커 링크, 쿼리 문자열, 다른 라우트 및 배포 인프라의 리디렉션은 검사 대상이 아니다. + +## 설계 + +### 링크 수정 + +Spring AI 멀티 프로바이더 글은 Bedrock 타임아웃 설정의 후속 설명으로 실제 존재하는 `spring-ai-pipeline-real-world` 글을 참조한다. 이 글에는 `BedrockProxyChatModel`의 연결 및 소켓 타임아웃 설정이 포함돼 있어 원래 문맥과 일치한다. + +### 링크 검사기 + +Node ESM 스크립트가 `content/posts`에서 Markdown 파일명을 읽어 유효 slug 집합을 만든다. 각 Markdown 본문에서 Markdown 링크 목적지 중 `/posts/:slug` 형식만 수집하고, slug가 집합에 없으면 파일명과 링크를 출력하고 비정상 종료한다. + +검사기는 인수 없이 실행되며, 성공 시 검사한 파일 수와 내부 글 링크 수를 출력한다. `package.json`의 `check:internal-links` 스크립트로 노출하고, `npm run build` 전에 독립적으로 실행할 수 있게 한다. + +## 오류 처리 + +- 누락된 slug가 하나라도 있으면 모든 누락 항목을 출력하고 종료 코드 1을 반환한다. +- 게시글 디렉터리를 읽지 못하면 원본 오류를 표시하고 종료한다. + +## 검증 + +1. 존재하지 않는 `/posts/__missing-post__` 링크를 포함한 임시 Markdown 파일로 검사기가 실패하는지 확인한다. +2. 실제 문서의 링크만 남긴 뒤 검사기가 통과하는지 확인한다. +3. 타입 검사, lint, production build를 실행한다. + +## 비범위 + +- Search Console에서 발견한 리디렉션·canonical 제외 URL의 자동 교정 +- 새 Spring AI 4편 작성 +- CDN 또는 호스팅 제공자의 5xx 원인 수정 +- Google 색인 요청 또는 Search Console의 검증 버튼 실행 diff --git a/package.json b/package.json index ba5875d..205d462 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "build": "tsc -b && vite build && vite build --ssr src/entry-server.tsx --outDir dist-ssr && node scripts/prerender.mjs", "preview": "vite preview", "lint": "eslint .", - "type-check": "tsc --noEmit" + "type-check": "tsc --noEmit", + "check:internal-links": "node scripts/check-internal-post-links.mjs" }, "dependencies": { "@giscus/react": "^3.1.0", diff --git a/scripts/check-internal-post-links.mjs b/scripts/check-internal-post-links.mjs new file mode 100644 index 0000000..3448684 --- /dev/null +++ b/scripts/check-internal-post-links.mjs @@ -0,0 +1,56 @@ +import fs from "node:fs" +import path from "node:path" +import { fileURLToPath } from "node:url" + +const postLinkPattern = new RegExp( + String.raw`\[[^\]]*\]\(/posts/([^/?#)]+)/?(?:[?#][^)]*)?\)`, + "g", +) + +export function findMissingPostLinks(posts) { + const slugs = new Set(posts.map((post) => path.basename(post.path, ".md"))) + const missing = [] + + for (const post of posts) { + for (const match of post.content.matchAll(postLinkPattern)) { + const href = match[0].slice(match[0].lastIndexOf("(") + 1, -1) + const slug = match[1] + + if (!slugs.has(slug)) { + missing.push({ path: post.path, href, slug }) + } + } + } + + return missing +} + +function run() { + const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..") + const postsDir = path.join(root, "content/posts") + const posts = fs.readdirSync(postsDir, { recursive: true }) + .filter((file) => file.endsWith(".md")) + .map((file) => { + const filePath = path.join(postsDir, file) + return { + path: path.relative(root, filePath), + content: fs.readFileSync(filePath, "utf8"), + } + }) + const missing = findMissingPostLinks(posts) + + if (missing.length > 0) { + console.error("Missing internal post links:") + for (const item of missing) { + console.error(`- ${item.path}: ${item.href}`) + } + process.exitCode = 1 + return + } + + console.log(`Validated ${posts.length} posts with no missing internal post links.`) +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + run() +} diff --git a/scripts/check-internal-post-links.test.mjs b/scripts/check-internal-post-links.test.mjs new file mode 100644 index 0000000..7e87e65 --- /dev/null +++ b/scripts/check-internal-post-links.test.mjs @@ -0,0 +1,24 @@ +import assert from "node:assert/strict" +import test from "node:test" +import { findMissingPostLinks } from "./check-internal-post-links.mjs" + +test("존재하지 않는 게시글 slug를 내부 링크 오류로 보고한다", () => { + const missing = findMissingPostLinks([ + { + path: "content/posts/existing.md", + content: "[누락 글](/posts/not-published)", + }, + { + path: "content/posts/other.md", + content: "[존재 글](/posts/existing)", + }, + ]) + + assert.deepEqual(missing, [ + { + path: "content/posts/existing.md", + href: "/posts/not-published", + slug: "not-published", + }, + ]) +})