diff --git a/.gitignore b/.gitignore
index 4c55f13..9bafa4d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -39,4 +39,9 @@ next-env.d.ts
/.vscode/
# serena
-/.serena/
\ No newline at end of file
+/.serena/
+
+# codex
+/.codex/
+
+skills-lock.json
\ No newline at end of file
diff --git a/README.md b/README.md
index 30e42ba..9b065a8 100644
--- a/README.md
+++ b/README.md
@@ -17,45 +17,68 @@
-Next.jsの学習用としてこのプロジェクトを作成しました。プロジェクトの構成としては以下になります。
+Next.js App Router と microCMS を使った技術ブログプロジェクトです。
+記事一覧、記事詳細、検索ページを備えたシンプルな構成で、Tailwind CSS を使って UI を実装しています。
-Next.js + directory + microCMS + TailWind CSS
+## Tech Stack
-このプロジェクトでは以下の機能を実装しています。
-* microCMSに投稿した記事の検索
-* microCMSに投稿した記事のカテゴリ絞り込み
-* microCMSに投稿した記事の内容取得
+- Next.js
+- React
+- TypeScript
+- microCMS
+- Tailwind CSS
+- ESLint
+## Features
-## Usage
+- microCMS から取得した記事一覧の表示
+- 記事詳細ページの表示
+- キーワード検索とカテゴリ絞り込み
+- sitemap / robots.txt の生成
+
+## Setup
+
+依存パッケージをインストールします。
-パッケージのインストール。
```bash
npm ci
```
-プロジェクトのルート配下に「.env.local」を新規作成して、以下を追加します。
+プロジェクトルートに `.env.local` を作成し、microCMS の設定を追加します。
+
```bash
-# microCMSのAPIキーを記載します
-API_KEY=XXXXXXXXXXXXXXXXXX
-# microCMSのサービス名を記載します
-SERVICE_DOMAIN=hoge
+API_KEY=YOUR_MICROCMS_API_KEY
+SERVICE_DOMAIN=YOUR_MICROCMS_SERVICE_DOMAIN
```
-ローカルサーバーの起動。
+## Development
+
+ローカルサーバーを起動します。
+
```bash
npm run dev
```
-サーバー起動後は以下のURLより、アプリの動作確認が可能です。
+起動後、以下の URL で確認できます。
+
+```text
http://localhost:3000
+```
+
+## Build
+
+本番ビルドを作成します。
+```bash
+npm run build
+```
## Demo
-* 記事検索
+
+### 記事一覧
-* 記事閲覧
+### 記事詳細
-
\ No newline at end of file
+
diff --git a/package-lock.json b/package-lock.json
index 5d146e8..3f3ea18 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,24 +12,18 @@
"@types/node": "^25.5.0",
"@types/react-dom": "19.2.3",
"@vercel/analytics": "^2.0.1",
- "axios": "^1.13.6",
"clsx": "^2.1.1",
"eslint": "^9.39.3",
"html-react-parser": "^5.2.17",
- "lucide-react": "^0.577.0",
"microcms-js-sdk": "^3.3.0",
"next": "^16.1.7",
"next-sitemap": "^4.2.3",
"react": "^19.2.4",
- "react-device-detect": "^2.2.3",
"react-dom": "^19.2.4",
"server-only": "^0.0.1",
- "simplebar-react": "^3.3.2",
- "swr": "^2.4.1",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
- "vercel": "^50.32.5",
- "zod": "^4.3.6"
+ "vercel": "^50.32.5"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^5.10.0",
@@ -4788,17 +4782,6 @@
"node": ">=4"
}
},
- "node_modules/axios": {
- "version": "1.13.6",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
- "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
- "license": "MIT",
- "dependencies": {
- "follow-redirects": "^1.15.11",
- "form-data": "^4.0.5",
- "proxy-from-env": "^1.1.0"
- }
- },
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -5312,15 +5295,6 @@
"node": ">= 0.6"
}
},
- "node_modules/dequal": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -6430,26 +6404,6 @@
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"license": "ISC"
},
- "node_modules/follow-redirects": {
- "version": "1.15.11",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
- "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
- "funding": [
- {
- "type": "individual",
- "url": "https://github.com/sponsors/RubenVerborgh"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=4.0"
- },
- "peerDependenciesMeta": {
- "debug": {
- "optional": true
- }
- }
- },
"node_modules/for-each": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
@@ -7975,18 +7929,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/lodash": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
- "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
- "license": "MIT"
- },
- "node_modules/lodash-es": {
- "version": "4.17.23",
- "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz",
- "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
- "license": "MIT"
- },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -8014,15 +7956,6 @@
"node": "20 || >=22"
}
},
- "node_modules/lucide-react": {
- "version": "0.577.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz",
- "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==",
- "license": "ISC",
- "peerDependencies": {
- "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/luxon": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
@@ -9032,18 +8965,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/react-device-detect": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/react-device-detect/-/react-device-detect-2.2.3.tgz",
- "integrity": "sha512-buYY3qrCnQVlIFHrC5UcUoAj7iANs/+srdkwsnNjI7anr3Tt7UY6MqNxtMLlr0tMBied0O49UZVK8XKs3ZIiPw==",
- "dependencies": {
- "ua-parser-js": "^1.0.33"
- },
- "peerDependencies": {
- "react": ">= 0.14.0",
- "react-dom": ">= 0.14.0"
- }
- },
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
@@ -9543,28 +9464,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/simplebar-core": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/simplebar-core/-/simplebar-core-1.3.2.tgz",
- "integrity": "sha512-qKgTTuTqapjsFGkNhCjyPhysnbZGpQqNmjk0nOYjFN5ordC/Wjvg+RbYCyMSnW60l/Z0ZS82GbNltly6PMUH1w==",
- "license": "MIT",
- "dependencies": {
- "lodash": "^4.17.21",
- "lodash-es": "^4.17.21"
- }
- },
- "node_modules/simplebar-react": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-3.3.2.tgz",
- "integrity": "sha512-ZsgcQhKLtt5ra0BRIJeApfkTBQCa1vUPA/WXI4HcYReFt+oCEOvdVz6rR/XsGJcKxTlCRPmdGx1uJIUChupo+A==",
- "license": "MIT",
- "dependencies": {
- "simplebar-core": "^1.3.2"
- },
- "peerDependencies": {
- "react": ">=16.8.0"
- }
- },
"node_modules/smart-buffer": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
@@ -9930,19 +9829,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/swr": {
- "version": "2.4.1",
- "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz",
- "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==",
- "license": "MIT",
- "dependencies": {
- "dequal": "^2.0.3",
- "use-sync-external-store": "^1.6.0"
- },
- "peerDependencies": {
- "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/tailwindcss": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
@@ -10288,28 +10174,6 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/ua-parser-js": {
- "version": "1.0.37",
- "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.37.tgz",
- "integrity": "sha512-bhTyI94tZofjo+Dn8SN6Zv8nBDvyXTymAdM3LDI/0IboIUwTu1rEhW7v2TfiVsoYWgkQ4kOVqnI8APUFbIQIFQ==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/ua-parser-js"
- },
- {
- "type": "paypal",
- "url": "https://paypal.me/faisalman"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/faisalman"
- }
- ],
- "engines": {
- "node": "*"
- }
- },
"node_modules/uid-promise": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/uid-promise/-/uid-promise-1.0.0.tgz",
@@ -10449,15 +10313,6 @@
"punycode": "^2.1.0"
}
},
- "node_modules/use-sync-external-store": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
- "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
"node_modules/vercel": {
"version": "50.32.5",
"resolved": "https://registry.npmjs.org/vercel/-/vercel-50.32.5.tgz",
@@ -10713,6 +10568,7 @@
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index 3f8131a..025ae82 100644
--- a/package.json
+++ b/package.json
@@ -15,24 +15,18 @@
"@types/node": "^25.5.0",
"@types/react-dom": "19.2.3",
"@vercel/analytics": "^2.0.1",
- "axios": "^1.13.6",
"clsx": "^2.1.1",
"eslint": "^9.39.3",
"html-react-parser": "^5.2.17",
- "lucide-react": "^0.577.0",
"microcms-js-sdk": "^3.3.0",
"next": "^16.1.7",
"next-sitemap": "^4.2.3",
"react": "^19.2.4",
- "react-device-detect": "^2.2.3",
"react-dom": "^19.2.4",
"server-only": "^0.0.1",
- "simplebar-react": "^3.3.2",
- "swr": "^2.4.1",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
- "vercel": "^50.32.5",
- "zod": "^4.3.6"
+ "vercel": "^50.32.5"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^5.10.0",
diff --git a/src/app/[others]/page.tsx b/src/app/[others]/page.tsx
index 0a601f6..4e0c2e7 100644
--- a/src/app/[others]/page.tsx
+++ b/src/app/[others]/page.tsx
@@ -2,7 +2,7 @@ import Link from 'next/link'
export const metadata = {
title: 'Not Found | N-LAB',
- description: 'ページが見つかりません。',
+ description: 'ページが見つかりませんでした。',
}
export default function NonexistentPage() {
@@ -14,18 +14,20 @@ export default function NonexistentPage() {
404
- Page Not Found.
+
+ ページが見つかりません
+
- お探しのページは削除されたか、
+ お探しのページは見つかりませんでした。
- URLが変更された可能性があります。
+ URL が変更されたか、ページが削除された可能性があります。
- Back to Top
+ トップへ戻る
→
diff --git a/src/app/api/list/route.ts b/src/app/api/list/route.ts
deleted file mode 100644
index 860831a..0000000
--- a/src/app/api/list/route.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import 'server-only'
-import { htmlspecialchars } from '@/features/common/sanitize'
-import { NextRequest, NextResponse } from 'next/server'
-import { z } from 'zod'
-import { Article } from '@/types'
-import { getArticleList } from '@/libs/microcms/client'
-
-export type ArticleListResponse = {
- articleList: Article[]
-}
-
-const userSchema = z.object({
- keyword: z.string(),
-})
-
-type RequestBody = {
- keyword: string
-}
-
-export const POST = async (request: NextRequest) => {
- const body = (await request.json()) as RequestBody
- const result = userSchema.safeParse(body)
-
- if (!result.success) {
- return NextResponse.json([], { status: 400 })
- }
-
- const keyword = htmlspecialchars(result.data.keyword.trim())
- const articleList = (
- await getArticleList('id,title,overview,svgPath,category,createdDate', keyword)
- )?.contents
-
- if (!articleList) {
- return NextResponse.json([], { status: 500 })
- }
-
- return NextResponse.json(
- { articleList },
- {
- status: 200,
- },
- )
-}
diff --git a/src/app/articles/[id]/page.tsx b/src/app/articles/[id]/page.tsx
index 724cacc..6a4cd95 100644
--- a/src/app/articles/[id]/page.tsx
+++ b/src/app/articles/[id]/page.tsx
@@ -5,11 +5,12 @@ import { htmlspecialchars } from '@/features/common/sanitize'
import { ArticleHeader } from '@/features/articles/components/ArticleHeader'
import { ArticleBody } from '@/features/articles/components/ArticleBody'
+export const revalidate = 3600
+
type Props = {
params: Promise<{ id: string }>
}
-// Dynamic Route使用時にSSGでビルドする
export async function generateStaticParams() {
try {
const response = await getArticleList('id')
@@ -47,7 +48,7 @@ export async function generateMetadata(props: Props): Promise {
catch {
return {
title: 'Not Found | N-LAB',
- description: '記事が見つかりません',
+ description: '記事が見つかりませんでした。',
}
}
}
diff --git a/src/app/error/page.tsx b/src/app/error/page.tsx
index 9037bf6..8ca6305 100644
--- a/src/app/error/page.tsx
+++ b/src/app/error/page.tsx
@@ -13,12 +13,12 @@ export default function Error() {
- Something went wrong.
+ エラーが発生しました
- 予期せぬエラーが発生しました。
+ 予期しないエラーが発生しました。
- しばらく時間をおいてから、再度お試しください。
+ 時間をおいて再度お試しいただくか、トップページへお戻りください。
@@ -26,7 +26,7 @@ export default function Error() {
href="/"
className="group inline-flex items-center gap-3 px-8 py-3 rounded-full border border-border bg-white/5 text-[#e2e8f0] font-semibold transition-all duration-300 hover:border-white hover:bg-white/10"
>
- Back to Top
+ トップへ戻る
→
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 77921ef..bc5d60e 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -2,17 +2,15 @@ import './globals.css'
import { Inter, Fira_Code } from 'next/font/google'
import Link from 'next/link'
import type { Metadata } from 'next'
-import { Analytics } from '@vercel/analytics/react'
+import AnalyticsProvider from '@/features/common/components/AnalyticsProvider'
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' })
const firaCode = Fira_Code({ subsets: ['latin'], variable: '--font-fira' })
export const metadata: Metadata = {
title: 'N-Laboratory | Engineering the Modern Web',
-
description:
- 'Next.jsやNuxt.js、Spring BootなどのフレームワークやAWSを中心とした技術のナレッジを発信するサイトです',
-
+ 'Next.js、Nuxt.js、Spring Boot、AWS などを中心に、モダンな Web 開発の知見を発信するサイトです。',
icons: {
icon: '/favicon.ico',
shortcut: '/favicon.ico',
@@ -33,7 +31,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })