| name | nextjs-page-migration |
| description | Migrate static HTML guide pages from legacy/ to web-next/ App Router page.tsx using TDD. Phase AâF is fully complete. Use this skill for new guide pages or maintenance of existing migrated pages. TRIGGER when the user says any of the following (Japanese or English): - "æ°èŠã¬ã€ãããŒãžã远å " / "ã¬ã€ãããŒãžãç§»è¡" / "ããŒãžãä¿å®" - "add new guide page" / "migrate guide page" / "nextjs page migration" Applies project-specific patterns: SiteHeader, DisclaimerBanner, nav-links.ts, CSS Modules, shiki build-time highlighting, Mermaid lazy loading.
|
| invocation | explicit |
| allowed-tools | ["Read","Grep","Glob","Edit","Write","Bash"] |
Next.js ã¬ã€ãããŒãžè¿œå ã»ä¿å®ã¹ãã«
æŠèŠ
Phase AâF ã§ 18 æã®ã¬ã€ãããŒãžã web-next/ App Router ã«å
šç§»è¡å®äºã
æ¬ã¹ãã«ã¯ æ°èŠã¬ã€ãããŒãžã®è¿œå ãŸãã¯æ¢åããŒãžã®ä¿å®ã»æ¹åã TDD ã§æšæºåããã
åæ: SiteHeader / DisclaimerBanner 㯠app/layout.tsx ã«ããŠã³ãæžã¿ã
ããŒãžåŽã§åã€ã³ã¯ã«ãŒãããªããæ°ããŒãžã¯ nav-links.ts ã«è¿œèšããŠç»é²ããã
ã»ãã·ã§ã³éå§æã«å¿
ãèªããã¡ã€ã«ïŒé åºåºå®ïŒ
docs/PROGRESS.md â çŸåšã®ä¿å®ãã§ãŒãºã®ã¹ããŒã¿ã¹ã»ãã¹ãæ°ã»ãã¯ã¹ãã¢ã¯ã·ã§ã³
- ãã®ãã¡ã€ã« â æšæºæé ãšå®è£
ãã¿ãŒã³
.claude/rules/tdd-mandatory-cycle.md â TDD å¿
é ãµã€ã¯ã« & ã³ãããåå²ã«ãŒã«
TDD å¿
é ãµã€ã¯ã«ã®é©çšïŒæéèŠïŒ
åžžã« TDD ãµã€ã¯ã«ïŒRed â Green â Refactor â DocsïŒãæåªå
ã§é©çšããã
- task.md äœææ: åã³ãããããRedããGreenããRefactorããDocs Syncãåäœã«æ§é åãã
- å®è£
åïŒRedïŒ:
page.tsx ãå®è£
ããåã«å€±æãããŠããããã¹ããäœæããŠã³ããããã
- äžæ¬ã³ãããçŠæ¢: ãã¹ãã»å®è£
ã»ã«ãã¬ããžæŽæ°ã»ããã¥ã¡ã³ãæŽæ°ã 1 ã³ãããã«ãŸãšããªã
æ°èŠããŒãžè¿œå ã®æšæºæé
Step 1: [Red] å¥çŽãã¹ãã®äœæ
app/<provider>/<slug>/page.test.tsx ãäœæãæäœé以äžã® 5 å¥çŽãæžã:
render(<Page />) ã§ã¿ã€ãã«ïŒ<h1>ïŒã®ããã¹ãå®å
šäžèŽ
- äž»èŠã»ã¯ã·ã§ã³æ°ïŒ
<h2> ã® countïŒ
- å€éšãªã³ã¯ã«
target="_blank" ãš rel="noopener noreferrer" ãäž¡æ¹ä»äžãããŠãã
- å
éšãªã³ã¯ã clean URLïŒ
.html ãªãïŒã§ãã
- ã³ãŒããããã¯ã«
language-* ã¯ã©ã¹ãä»äžãããŠããïŒshiki é©çšã®å段確èªïŒ
åèå®è£
: web-next/components/Hero.test.tsx ã® render() + querySelector ãã¿ãŒã³ã
Step 2: [Green] page.tsx ã®å®è£
faithful å¿
é : ãœãŒã¹ HTML ã® å
šãªã¹ãé
ç®ã»å
šã³ãŒããããã¯ã»å
š SVGã»å
š alertã»å
š table ã
JSX ã«è»¢åããããšãèŠçŽã»çç¥ã»çž®çŽã¯çŠæ¢ã
- Server Component ããã©ã«ãã
"use client" 㯠useState ãå¿
èŠãªå Žåã®ã¿
- ã¹ã¿ã€ã«åªå
é äœ: Tailwind ãŠãŒãã£ãªã㣠â CSS ModulesïŒ
page.module.cssïŒ â global CSSïŒé¿ããïŒ
- ãã¡ã€ã«ã¬ã€ã¢ãŠã:
app/<provider>/<slug>/ é
äžã« page.tsx / page.module.css / page.test.tsx ã® 3 ç¹ã»ãããã³ãã±ãŒã·ã§ã³é
眮
- æ°ãã« i18n ããŒã远å ããå Žåã
lib/i18n.test.ts ã® expect(Object.keys(T).length).toBe(N) ãåãã³ãããå
ã§æŽæ°ãã
JSX 倿 Pitfalls ãã§ãã¯ãªã¹ã
| åé¡ | NG äŸ | OK äŸ |
|---|
class 屿§ | class="foo" | className="foo" |
for 屿§ | for="id" | htmlFor="id" |
| void èŠçŽ ã®éã | <br> <img> | <br /> <img /> |
| HTML ã³ã¡ã³ã | <!-- comment --> | {/* comment */} |
| ã€ã³ã©ã€ã³ã¹ã¿ã€ã« | style="font-family: var(--f)" | style={{ fontFamily: 'var(--f)' }} |
{"\n"} æ¹è¡ | <span>A</span>{"\n"}<span>B</span> | <div className={styles.codeLine}>âŠ</div> ã§ã©ãã |
| ãã·ãžã§ã³ããŒãã«ã®ã¹ããŒã¹æã | ã¹ããŒã¹ã§åå¹
ãåããã | <table> èŠçŽ ãžå€æ |
<main> ã©ãããŒè¿œå | <main>âŠ</main> | äžèŠïŒlayout.tsx ã管çïŒ |
| ç HTML 泚å
¥ | dangerouslySetInnerHTML | ãã¹ããã JSX <span> ã§è¡šçŸïŒäžèšåç
§ïŒ |
| SVG ã« title/aria ãªã | <svg>âŠ</svg> | <svg role="img" aria-label="âŠ"><title>âŠ</title>âŠ</svg> |
ã³ãŒããããã¯å
ã®è¡åºåããã¿ãŒã³
.code-block ã®ããã©ã«ã white-space 㯠normal ã®ãã {"\n"} ã¯ã¹ããŒã¹ã«æ£èŠåãããã
åè¡ã <div className={styles.codeLine}> ã§ã©ããããããšïŒ.codeLine ã«ã¯ white-space: pre ãå®çŸ©æžã¿ïŒã
{}
<div className={styles.codeBody}>
<span className={styles.ck}>const</span>{"\n"}
<span className={styles.cv}>value</span>
</div>
{}
<div className={styles.codeBody}>
<div className={styles.codeLine}><span className={styles.ck}>const</span><span className={styles.cv}> value</span></div>
<div className={styles.codeLine}><span className={styles.cv}>= 42</span></div>
</div>
SVG 屿§å€æèŠå
| HTML 屿§ | JSX 屿§ | HTML 屿§ | JSX 屿§ |
|---|
text-anchor | textAnchor | stroke-width | strokeWidth |
font-family | fontFamily | fill-opacity | fillOpacity |
font-size | fontSize | pointer-events | pointerEvents |
font-weight | fontWeight | stop-color in style= | style={{ stopColor: '...' }} |
letter-spacing | letterSpacing | style="display:block;" | style={{ display: 'block' }} |
SVG ã«ã¯å¿
ã role="img" + aria-label + <title> ãä»äžïŒBiome noSvgWithoutTitle 察å¿ïŒã
Step 3: ã³ãŒããããã¯ïŒshikiïŒ
build-time ãã€ã©ã€ããšã㊠shiki ãæ¡çšããã
- RSC å
ã®
async ããŒãžã³ã³ããŒãã³ããã shiki highlighter ãååŸãäºå HTML åãã
- å®å
šãªæµã蟌㿠API ã®äœ¿ãæ¹ã¯æ¢åã®
web-next/components/CodeBlock.tsx ãåç
§ããããš
ïŒSKILL.md å
ã«ãªãã©ã«èšè¿°ããªã â XSS ç£æ»ã® false positive 鲿¢ïŒ
- å
¥åã¯ãã«ãæã«ç¢ºå®ããå€ã®ã¿ïŒãŠãŒã¶ãŒå
¥åã¯ãã€ã©ã€ã察象ã«å«ããªãïŒ
ã³ãŒããã€ã©ã€ãçšã¯ã©ã¹æ©èŠè¡šïŒstyles. ãå眮ããŠäœ¿çšïŒ:
| ã¯ã©ã¹ | è² | çšé | ã¯ã©ã¹ | è² | çšé |
|---|
ck | èµ€ #ff7b72 | ããŒã¯ãŒã | cm | ç·å€ªå #7ee787 | ããŒã«ãŒã»ã»ã¯ã·ã§ã³ |
cs | èé #a5d6ff | æååã»åºåã | cw | é» #d29922 | èŠåã»éèŠèª |
cv | é #79c0ff | å€ | ce | 玫 #bc8cff | åæã»ç¹æ® |
cc | ã°ã¬ãŒæäœ #3b4750 | ã³ã¡ã³ã | cg | æç· #3fb950 | æåã»è¯å® |
ch | ãªã¬ã³ãžå€ªå #f0883e | èŠåºã | | | |
Step 4: Mermaid ãã€ã¢ã°ã©ã
components/docs/MermaidDiagram.tsx ãçŽæ¥ã€ã³ããŒãããŠéåžžéã䜿çšããïŒå
éšã§åçã€ã³ããŒãæžã¿ïŒ
- ããŒãã¯
theme: "dark" ãèšå®ãã
- èšè¿°ã¯ 巊端æãå¿
é ïŒã€ã³ãã³ããæ··ãããšæ§æãšã©ãŒïŒ
- 衚瀺ã¬ã€ã¢ãŠã: ãã€ã¢ã°ã©ã ã¯å¿
ãäžå€®å¯ãã«é
眮ããããšïŒCSS Module å
ã§
:global(.mermaid) ãã³ã³ããã«å¯Ÿã㊠display: flex; justify-content: center; ãé©çšããïŒã
globals.css ã« Mermaid è£æ£ CSS é©çšæžã¿ãæ°èŠå³è§£ã§èªã¿ã«ããå Žåã®ã¿è¿œèšãã
Step 5: [Refactor] å
±éå倿
- 3 ããŒãžä»¥äžã§éè€ãã UI ãã¿ãŒã³ â
components/docs/ ãžã®æœåºãæ€èš
- 2 ããŒãžä»¥äž â ããŒãžåºæã§æ®ã
Step 6: ããŒã«ã«æ€èšŒ
cd web-next
bun run lint
bun run typecheck
bun run test
bun run build
å
šéé ãå¿
é ãéšå pass ã§ã³ãããããªãã
Step 7: nav-links.ts / ããã¥ã¡ã³ãåæ
nav-links.ts ãžã®ç»é²ïŒå¿
é ïŒ:
{ href: '/<provider>/<slug>', label: 'ããŒãžè¡šç€ºå', category: '<category>' },
CLAUDE.md / GEMINI.md ãžã®è¿œèšïŒå¿
é ïŒ:
- `app/<provider>/<slug>/page.tsx` â ããŒãžã®èª¬æ
åå©çšãã¿ãŒã³é
Ext ãã«ããŒïŒå€éšãªã³ã¯ïŒ
function Ext({ href, children }: { href: string; children: React.ReactNode }) {
return (
<a href={href} target="_blank" rel="noopener noreferrer">
{children}
</a>
);
}
ã³ãŒããããã¯æ§é
<div className={styles.codeWrap}>
<div className={styles.codeBar}>
<span>ãã¡ã€ã«å.md</span>
<span className={styles.codeLang}>YAML</span>
</div>
<div className={styles.codeBody}>
<div className={styles.codeLine}><span className={styles.cs}>---</span></div>
<div className={styles.codeLine}>
<span className={styles.cm}>name</span>
<span>{": "}</span>
<span className={styles.cv}>{'My Agent'}</span>
</div>
</div>
</div>
[!IMPORTANT]
ã³ãŒããããã¯ãæ§æãã .codeBar, .codeBody, .codeLine ãªã©ã®CSSã¢ãžã¥ãŒã«ã¯ã©ã¹ã«ã¯ãå¿
ãçå¹
ãã©ã³ãèšå®ïŒfont-family: var(--font-mono), "JetBrains Mono", monospaceïŒãé©çšããããšããŸãã.codeBody ã«ã¯é©å㪠line-heightïŒäŸ: 1.65ïŒãæå®ããŠèŠèªæ§ã確ä¿ããã
ããã¿ãŒæ§é
ããã¿ãŒã¯ã¡ã€ã³ã³ã³ãã³ã (<main>) ã®æ«å°Ÿã«é
眮ããäžå€®å¯ããšçå¹
ãã©ã³ããé©çšããã
.pageFooter {
max-width: 100%;
margin: 0 auto;
padding: 40px 56px 80px;
color: var(--text-tertiary);
font-size: 12.5px;
font-family: var(--font-mono), "JetBrains Mono", monospace;
text-align: center;
border-top: 1px solid var(--border);
line-height: 1.8;
background: #050b12;
}
TOC nav ãšã¹ã¯ããŒã«èªåè¿œåŸ (Intersection Observer)
ã¹ã¯ããŒã«ã«å¿ããŠç®æ¬¡ïŒTOCïŒã®ã¢ã¯ãã£ãé
ç®ããã€ã©ã€ãããIntersection Observerãå°å
¥ããå Žåãpage.tsx ã 'use client' åããŠã¯ãããªãïŒNext.js App Routerã®ä»æ§ã«ãã metadata ããšã¯ã¹ããŒãã§ããªããªãããïŒã
æšå¥šèšèšãã¿ãŒã³:
- åäžãã£ã¬ã¯ããªå
ã«
TocObserver.tsx ãšãã軜éã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ããæ°èŠäœæããã
TocObserver å
ã§ useEffect ãš IntersectionObserver ãçšããŠãå .chapter ã®äº€å·®å€å®ãè¡ããTOCãªã³ã¯ã«å¯ŸããŠçŽæ¥ classList.add(styles.tocLinkActive) / classList.remove(styles.tocLinkActive) ãæäœããã
- ãµãŒããŒã³ã³ããŒãã³ãã§ãã
page.tsx ãã TocObserver ãã€ã³ããŒãããã¬ã€ã¢ãŠãå
ã«é
眮ãããããã«ãããmetadata éçãšã¯ã¹ããŒãã®ç¶æãšã¹ã¯ããŒã«è¿œåŸæ©èœã®äž¡ç«ãå¯èœã«ãªãã
"use client";
import { useEffect } from "react";
import styles from "./page.module.css";
export default function TocObserver() {
useEffect(() => {
const sections = document.querySelectorAll("section.chapter");
const links = Array.from(document.querySelectorAll(`.${styles.tocLink}`));
if (links.length > 0) links[0].classList.add(styles.tocLinkActive);
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const id = entry.target.id;
for (const l of links) {
if (l.getAttribute("href") === `#${id}`) {
l.classList.add(styles.tocLinkActive);
} else {
l.classList.remove(styles.tocLinkActive);
}
}
}
}
}, { rootMargin: "-15% 0px -70% 0px", threshold: 0 });
for (const sec of sections) observer.observe(sec);
return () => observer.disconnect();
}, []);
return null;
}
CSS Module è€åã¯ã©ã¹
<span className={`${styles.navPill} ${styles.green}`}>Agent Mode</span>
WAI-ARIA ãã¿ãŒã³ïŒã€ã³ã¿ã©ã¯ãã£ã UIïŒ
䜿ãåãæ©èŠè¡š:
| UI ãã¿ãŒã³ | æ£ãã ARIA | 誀ããããä»£æ¿ |
|---|
| ãã§ãã¯ãã¿ã³ (on/off) | aria-pressed={bool} | aria-checked, aria-selected |
| ã¿ãåãæ¿ã | role="tab" + aria-selected | aria-pressed, aria-current |
| ã¹ãããçŸåšå° | aria-current="step" | aria-selected, aria-pressed |
ã¿ã UIïŒRoving tabindex ãã¿ãŒã³ïŒ:
<div role="tablist">
{TABS.map((t) => (
<button
key={t.id}
id={`tab-${t.id}`}
role="tab"
aria-selected={active === t.id}
aria-controls={`panel-${t.id}`}
tabIndex={active === t.id ? 0 : -1}
onClick={() => setActive(t.id)}
>
{t.label}
</button>
))}
</div>
<div id={`panel-${active}`} role="tabpanel" aria-labelledby={`tab-${active}`}>
...
</div>
ã¹ãããçŸåšå°: aria-current={isActive ? "step" : undefined} â undefined ã§å±æ§èªäœãæ¶ãïŒfalse ã ãš aria-current="false" ãåºåãããïŒã
ã»ãã·ã§ã³çµäºåã®ä»æ§æžåæïŒå¿
é ïŒ
<ai_agent_directive>
AI ãšãŒãžã§ã³ããžã®å³æ Œãªæç€º: 以äžã®ããã»ã¹ã¯ã²ãŒãæ¡ä»¶ïŒGate ConditionïŒã§ããã¿ã¹ã¯ã®å ±åãè¡ãåã«ããŠãŒã¶ãŒã®èš±å¯ãåŸ
ããã«èªåŸçãã€èªåçã«ãã¹ããããã€ã¹ãããã§ã³ããããŸã§å®äºãããŠãã ãããã«ãŒã«ã«åããŠã³ããããåŸåãã«ããããšã¯çŠæ¢ãããŠããŸãã
</ai_agent_directive>
1 ããŒãžã® git commit å®äºåŸã次ã®äœæ¥ãå§ããåã«å¿
ã宿œãã:
cd web-next && bun run build && bun run lint && bun run test
å
šééåŸãdocs/PROGRESS.md ã®ãã¹ãæ°ã»æ¬¡ã®äœæ¥ã»åéããã³ãããæŽæ°ããŠã³ãããããã
詳现㯠.claude/rules/migration-progress-sync.md ãåç
§ã
å€å®åºæº
| çµæ | ã¢ã¯ã·ã§ã³ |
|---|
| å
šã¹ãããæå + ãã¹ãå
šéé | ã³ããã OK ãšå ±åããæ¬¡ããŒãžã«é²ã |
| åäœãã¹ã倱æ | ãã¹ãã®æå³ã確èªããå®è£
ããã¹ããã©ã¡ãã誀ãã倿 |
| ãã«ã倱æ | 忢ãimport / åãšã©ãŒãæå°å·®åã§ä¿®æ£ |
| lint ãšã©ãŒ | 倿Žãã¡ã€ã«åäœã§ãã¹æå®ããŠä¿®æ£ïŒbun run lint:fix åŒæ°ãªãçŠæ¢ïŒ |
| èšå®ãã¡ã€ã«ã®æå³ããªãå€æŽ | 忢ããŠãŠãŒã¶ãŒç¢ºèª |
ConstraintsïŒçŠæ¢äºé
ïŒ
<SiteHeader> / <DisclaimerBanner> ãããŒãžåŽã§åã€ã³ã¯ã«ãŒãããªã â layout.tsx ãæäŸ
"use client" ãäžå¿
èŠã«äœ¿ããªã â Server Component ããã©ã«ã
- ç HTML 泚å
¥ prop ã䜿ããªã â JSX
<span> ã§ã·ã³ã¿ãã¯ã¹ãã€ã©ã€ãã衚çŸ
{"\n"} ã .code-block å
ã®æ¹è¡ã«äœ¿ããªã â <div className={styles.codeLine}> ã§ã©ãã
- ã¹ããŒã¹æãã§ tabular data ã衚çŸããªã â
<table> èŠçŽ ãžå€æããŸããè¡šã®æåã¯ãã¹ãŠã®ããããŒïŒthïŒããã³ã»ã«ïŒtdïŒã§å¿
ãå·Šå¯ãïŒtext-align: left !importantïŒã«ããŠè¡šç€ºããããšã
- Mermaidã®å³è§£ãå·Šå¯ãã«ããªã â å¿
ã
:global(.mermaid) ãã³ã³ããã®ãã¬ãã¯ã¹ããã¯ã¹çãå©çšããŠäžå€®å¯ãïŒdisplay: flex; justify-content: center;ïŒã«ããããšã
bun run lint:fixïŒåŒæ°ãªãïŒãå®è¡ããªã â 倿Žãã¡ã€ã«åäœã§ãã¹æå®ïŒR1 ã«ãŒã«ïŒ
- å€éšãã©ã³ãã
<link> ã¿ã°ã§èªã¿èŸŒãŸãªã â next/font/google ã®ã¿ïŒlayout.tsxïŒ
@layer components ã page-specific styles ã«äœ¿ããªã â plain CSS ã§ specificity ã確ä¿
@keyframes ã«ãã£ã¡ã«ã±ãŒã¹ã䜿ããªã â kebab-case å¿
é ïŒfadeUp â fade-upïŒ
- z-index ã CSS ãš Tailwind ã¯ã©ã¹ã®äž¡æ¹ã§æå®ããªã â åäžãœãŒã¹åå
- SVG ã«
role="img" + aria-label + <title> ãçç¥ããªã â Biome noSvgWithoutTitle éå
- Playwright MCP ããŒã«ã䜿ããªã â èŠèŠç¢ºèªã¯ãŠãŒã¶ãŒãæåã§å®æœ
- æ°èŠã¬ã€ãããŒãžã
legacy/ é
äžã«äœæããªã â web-next/app/ åŽã®ã¿ã«äœæãã
- ã³ãŒããããã¯ãããã¿ãŒã® monospace ãã©ã³ãæå®ãçç¥ããªã â
.codeBody, .codeLine, .codeBar, .pageFooter ã«ã¯å¿
ãçå¹
ãã©ã³ãïŒfont-family: var(--font-mono), ...ïŒãæç€ºçã«æå®ããããšã
- RSC ã®
page.tsx ãçŽæ¥ã¯ã©ã€ã¢ã³ãã³ã³ããŒãã³ãåïŒ'use client'ïŒããªã â Intersection Observer çãå¿
èŠãªå Žåã¯ã軜é㪠<TocObserver /> ãªã©ã«åå²ããpage.tsx èªäœã¯ Server Component ã®ãŸãŸã§ metadata éçãšã¯ã¹ããŒããã§ããç¶æ
ãç¶æããã