This repository was archived by the owner on Mar 7, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Migrate blog from Gatsby to Next.js (App Router) and update deploy workflow #22
Closed
Closed
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -56,10 +56,10 @@ typings/ | |
|
|
||
| # gatsby files | ||
| .cache/ | ||
| public | ||
|
|
||
| # Mac files | ||
| .DS_Store | ||
|
|
||
| # 비공개 게시물 | ||
| @Private | ||
| @Private | ||
| .next | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,44 +1,52 @@ | ||
| Start Template: gastby-starter [official and community-created starters](https://www.gatsbyjs.com/docs/gatsby-starters/) | ||
| # suu3.github.io (Next.js) | ||
|
|
||
| ## 🚀 Quick start | ||
| 기존 Gatsby 블로그를 Next.js App Router 기반으로 마이그레이션한 버전입니다. | ||
|
|
||
| Site: `http://localhost:8000` | ||
| Querying your data: `http://localhost:8000/___graphql` [Gatsby Tutorial](https://www.gatsbyjs.com/docs/tutorial/getting-started/part-4/#use-graphiql-to-explore-the-data-layer-and-write-graphql-queries). | ||
| ## 실행 | ||
|
|
||
| ```shell | ||
| # 실행 | ||
| pnpm start | ||
| # 배포 | ||
| pnpm run deploy | ||
| ```bash | ||
| # 1) pnpm 활성화 | ||
| corepack enable | ||
|
|
||
| # 2) (사내망/미러 환경이라면) npm registry 강제 지정 | ||
| pnpm config set registry https://registry.npmjs.org/ | ||
|
|
||
| # 3) 설치 및 실행 | ||
| pnpm install | ||
| pnpm dev | ||
| ``` | ||
|
|
||
| - 접속: `http://localhost:3000` | ||
|
|
||
| ## 주요 기능 | ||
|
|
||
| - 최신 Next.js(App Router) 구조 적용 | ||
| - Gatsby 레거시 컴포넌트/설정(`src/*`, `gatsby-*.ts(x)`) 제거로 Next.js 전용 구조 정리 | ||
| - Tailwind CSS v4 기반 UI 스타일링(기존 카드형 디자인 톤 유지) | ||
| - 전체 포스트 검색(제목/설명/카테고리/태그) | ||
| - 카테고리 모아보기 및 카테고리별 포스트 목록 | ||
| - PWA 메타데이터/manifest 및 favicon 연결 | ||
|
|
||
| ## 빌드 | ||
|
|
||
| ```bash | ||
| pnpm build | ||
| ``` | ||
|
|
||
| ## 🧐 What's inside? | ||
|
|
||
| 디렉터리 구조 | ||
|
|
||
| . | ||
| ├── node_modules | ||
| └── blog | ||
| └── @Private :비공개 게시물 => 대신 포스트는 따로 노션에 관리. (어드민을 둘 생각이 없으므로) | ||
| └── ... | ||
| ├── src | ||
| ├── .gitignore | ||
| ├── gatsby-browser.js | ||
| ├── gatsby-config.js | ||
| ├── gatsby-node.js | ||
| ├── gatsby-ssr.js | ||
| ├── LICENSE | ||
| ├── package.json | ||
| └── README.md | ||
|
|
||
| [Gatsby browser APIs](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/) | ||
| [Gatsby Config](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-config/) | ||
| [Gatsby Node APIs](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-node/) | ||
| [Gatsby server-side rendering APIs](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-ssr/) | ||
| [Gatsby website](https://www.gatsbyjs.com/) | ||
| [Tutorial](https://www.gatsbyjs.com/docs/tutorial/getting-started/) | ||
| [Documentation](https://www.gatsbyjs.com/docs/) | ||
|
|
||
| ## 💫 Deploy | ||
|
|
||
| gh-pages 배포 | ||
| 정적 배포를 위해 `next.config.ts`에서 `output: 'export'`를 사용합니다. | ||
|
|
||
| ## 실행이 안 될 때 | ||
|
|
||
| - `ERR_PNPM_FETCH_403`가 나면, 프로젝트 또는 전역 `.npmrc`에 사설 레지스트리 설정이 있는지 확인하세요. | ||
| - 아래 명령으로 현재 레지스트리를 확인/초기화할 수 있습니다. | ||
|
|
||
| ```bash | ||
| pnpm config get registry | ||
| pnpm config set registry https://registry.npmjs.org/ | ||
| ``` | ||
|
|
||
|
|
||
| ## 배포 | ||
|
|
||
| GitHub Actions `deploy.yml`은 Next.js 정적 출력 폴더인 `out/`를 배포합니다. | ||
| 정적 에셋은 Next.js 표준 위치인 `public/` 디렉터리를 사용합니다. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import Link from 'next/link'; | ||
| import { getAllPosts, splitSlugToSegments } from '@/lib/posts'; | ||
|
|
||
| type Props = { | ||
| params: Promise<{ category: string }>; | ||
| }; | ||
|
|
||
| export async function generateStaticParams() { | ||
| const categories = [...new Set(getAllPosts().map((post) => post.category))]; | ||
| return categories.map((category) => ({ category: encodeURIComponent(category) })); | ||
| } | ||
|
|
||
| export default async function CategoryDetailPage({ params }: Props) { | ||
| const { category: categoryParam } = await params; | ||
| const category = decodeURIComponent(categoryParam); | ||
| const posts = getAllPosts().filter((post) => post.category === category); | ||
|
|
||
| return ( | ||
| <section className="space-y-4"> | ||
| <h1 className="text-3xl font-black tracking-tight">{category}</h1> | ||
| <p className="text-sm text-gray-500">{posts.length}개의 포스트</p> | ||
| <ul className="space-y-2"> | ||
| {posts.map((post) => ( | ||
| <li key={post.slug}> | ||
| <Link | ||
| href={`/posts/${splitSlugToSegments(post.slug).join('/')}`} | ||
| className="block rounded-lg border border-[#2a2b31] bg-white px-4 py-3 hover:bg-[#ffddca]" | ||
| > | ||
| <p className="font-semibold">{post.title}</p> | ||
| <p className="mt-1 text-sm text-gray-500">{post.date}</p> | ||
| </Link> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| <Link href="/category" className="inline-block rounded border border-[#2a2b31] px-3 py-2 text-sm hover:bg-[#ffddca]"> | ||
| ← 카테고리 목록 | ||
| </Link> | ||
| </section> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import Link from 'next/link'; | ||
| import { getAllPosts, getCategoriesWithCount } from '@/lib/posts'; | ||
|
|
||
| export default function CategoryPage() { | ||
| const posts = getAllPosts(); | ||
| const categories = Object.entries(getCategoriesWithCount(posts)).sort((a, b) => b[1] - a[1]); | ||
|
|
||
| return ( | ||
| <section className="space-y-4"> | ||
| <h1 className="text-3xl font-black tracking-tight">카테고리 모아보기</h1> | ||
| <ul className="grid gap-3 md:grid-cols-2"> | ||
| {categories.map(([category, count]) => ( | ||
| <li key={category}> | ||
| <Link | ||
| href={`/category/${encodeURIComponent(category)}`} | ||
| className="flex items-center justify-between rounded-xl border border-[#2a2b31] bg-white px-4 py-3 transition hover:bg-[#ffddca] hover:shadow-[4px_4px_0_0_#2a2b31]" | ||
| > | ||
| <span className="font-semibold">{category}</span> | ||
| <span className="font-mono text-sm text-gray-500">{count}</span> | ||
| </Link> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| </section> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| @import url("https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"); | ||
| @import "tailwindcss"; | ||
|
|
||
| :root { | ||
| --surface: #ffffff; | ||
| --text: #2a2b31; | ||
| --muted: #6b7280; | ||
| --line: #2a2b31; | ||
| --theme: #ff6737; | ||
| --theme-soft: #ffddca; | ||
| --bg: #f7f7f7; | ||
| } | ||
|
|
||
| * { | ||
| box-sizing: border-box; | ||
| } | ||
|
|
||
| body { | ||
| margin: 0; | ||
| font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | ||
| color: var(--text); | ||
| background: var(--bg); | ||
| } | ||
|
|
||
| a { | ||
| color: inherit; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import type { Metadata, Viewport } from 'next'; | ||
| import Link from 'next/link'; | ||
| import './globals.css'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Suu.Blog', | ||
| description: 'Suu3 기술 블로그', | ||
| metadataBase: new URL('https://suu3.github.io'), | ||
| manifest: '/site.webmanifest', | ||
| icons: { | ||
| icon: [ | ||
| { url: '/favicon.ico' }, | ||
| { url: '/favicons/favicon-48x48.png', sizes: '48x48', type: 'image/png' }, | ||
| { url: '/favicons/favicon-96x96.png', sizes: '96x96', type: 'image/png' }, | ||
| { url: '/favicons/favicon-144x144.png', sizes: '144x144', type: 'image/png' }, | ||
| { url: '/favicons/favicon-192x192.png', sizes: '192x192', type: 'image/png' }, | ||
| { url: '/favicons/favicon-512x512.png', sizes: '512x512', type: 'image/png' }, | ||
| ], | ||
| apple: [{ url: '/favicons/favicon-192x192.png' }], | ||
| }, | ||
| }; | ||
|
|
||
| export const viewport: Viewport = { | ||
| themeColor: '#ff6737', | ||
| }; | ||
|
|
||
| export default function RootLayout({ children }: { children: React.ReactNode }) { | ||
| return ( | ||
| <html lang="ko"> | ||
| <body> | ||
| <header className="sticky top-0 z-20 border-b border-[#2a2b31] bg-white/95 backdrop-blur"> | ||
| <div className="mx-auto flex h-16 w-full max-w-5xl items-center justify-between px-4"> | ||
| <Link href="/" className="text-xl font-black tracking-tight"> | ||
| Suu.Blog | ||
| </Link> | ||
| <nav className="flex items-center gap-2 text-sm font-medium"> | ||
| <Link href="/" className="rounded-md border border-[#2a2b31] px-3 py-1.5 hover:bg-[#ffddca]"> | ||
| 홈 | ||
| </Link> | ||
| <Link | ||
| href="/category" | ||
| className="rounded-md border border-[#2a2b31] px-3 py-1.5 hover:bg-[#ffddca]" | ||
| > | ||
| 카테고리 | ||
| </Link> | ||
| </nav> | ||
| </div> | ||
| </header> | ||
|
|
||
| <main className="mx-auto w-full max-w-5xl px-4 py-8">{children}</main> | ||
| </body> | ||
| </html> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| import PostSearch from '@/components/post-search'; | ||
| import { getAllPosts } from '@/lib/posts'; | ||
|
|
||
| export default function HomePage() { | ||
| const posts = getAllPosts(); | ||
|
|
||
| return ( | ||
| <section className="space-y-6"> | ||
| <div className="space-y-2"> | ||
| <p className="text-sm font-medium text-gray-500">전체 포스트</p> | ||
| <h1 className="text-3xl font-black tracking-tight">기록하고 공유합니다</h1> | ||
| </div> | ||
| <PostSearch posts={posts} /> | ||
| </section> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import Link from 'next/link'; | ||
| import { notFound } from 'next/navigation'; | ||
| import { getAllPosts, getPostBySlug } from '@/lib/posts'; | ||
|
|
||
| type Props = { | ||
| params: Promise<{ slug: string[] }>; | ||
| }; | ||
|
|
||
| export async function generateStaticParams() { | ||
| return getAllPosts().map((post) => ({ | ||
| slug: post.slug.split('/').map(encodeURIComponent), | ||
| })); | ||
| } | ||
|
|
||
| export default async function PostDetailPage({ params }: Props) { | ||
| const { slug: slugSegments } = await params; | ||
| const slug = slugSegments.map(decodeURIComponent).join('/'); | ||
| const post = getPostBySlug(slug); | ||
|
|
||
| if (!post) { | ||
| notFound(); | ||
| } | ||
|
|
||
| return ( | ||
| <article className="rounded-xl border border-[#2a2b31] bg-white p-6 shadow-[4px_4px_0_0_#2a2b31]"> | ||
| <p className="mb-2 inline-block rounded-md bg-[#ffddca] px-2 py-1 font-mono text-xs">{post.category}</p> | ||
| <h1 className="text-3xl font-black tracking-tight">{post.title}</h1> | ||
| <p className="mt-2 text-sm text-gray-500">{post.date}</p> | ||
| <p className="mt-4 leading-7 text-gray-700">{post.description}</p> | ||
| <p className="mt-3 text-sm text-gray-600">태그: {post.tags.join(', ') || '없음'}</p> | ||
| <hr className="my-6 border-[#2a2b31]" /> | ||
| <pre className="overflow-x-auto whitespace-pre-wrap rounded-lg bg-gray-100 p-4 text-sm leading-6">{post.content}</pre> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The post page prints Useful? React with 👍 / 👎. |
||
| <Link href="/" className="mt-6 inline-block rounded border border-[#2a2b31] px-3 py-2 text-sm hover:bg-[#ffddca]"> | ||
| ← 목록으로 | ||
| </Link> | ||
| </article> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The workflow now runs
pnpm install --frozen-lockfile, but this commit also replaces the dependency set inpackage.jsonwithout updatingpnpm-lock.yaml; pnpm documents that frozen mode "fail[s] if an update is needed," and at this revision install fails withERR_PNPM_OUTDATED_LOCKFILE. That means every deploy job will stop at dependency installation and no static build/deploy can complete until the lockfile is regenerated and committed.Useful? React with 👍 / 👎.