Skip to content
This repository was archived by the owner on Mar 7, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,28 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v1
uses: actions/setup-node@v4
with:
node-version: "18.x"
- uses: pnpm/action-setup@v2
node-version: '20'

- uses: pnpm/action-setup@v4
with:
version: 8
version: 9

- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm clean && pnpm run build
run: pnpm install --frozen-lockfile
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Regenerate lockfile before frozen install in CI

The workflow now runs pnpm install --frozen-lockfile, but this commit also replaces the dependency set in package.json without updating pnpm-lock.yaml; pnpm documents that frozen mode "fail[s] if an update is needed," and at this revision install fails with ERR_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 👍 / 👎.


- name: Build static export
run: pnpm run build
env:
NODE_ENV: production

- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v2
env:
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
PUBLISH_BRANCH: deploy
PUBLISH_DIR: ./public
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.ACCESS_TOKEN }}
publish_branch: deploy
publish_dir: ./out
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,10 @@ typings/

# gatsby files
.cache/
public

# Mac files
.DS_Store

# 비공개 게시물
@Private
@Private
.next
86 changes: 47 additions & 39 deletions README.md
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/` 디렉터리를 사용합니다.
40 changes: 40 additions & 0 deletions app/category/[category]/page.tsx
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>
);
}
26 changes: 26 additions & 0 deletions app/category/page.tsx
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>
);
}
27 changes: 27 additions & 0 deletions app/globals.css
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;
}
54 changes: 54 additions & 0 deletions app/layout.tsx
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>
);
}
16 changes: 16 additions & 0 deletions app/page.tsx
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>
);
}
38 changes: 38 additions & 0 deletions app/posts/[...slug]/page.tsx
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>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Render markdown content instead of raw source text

The post page prints post.content inside a <pre>, so markdown is displayed as literal source (e.g., headings, links, code fences, image syntax) rather than rendered article content. For this blog’s markdown-based posts, readers lose formatted content and media rendering on every post detail page, which is a major functional regression from the previous behavior.

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>
);
}
Loading