@@ -176,7 +175,7 @@ export default async function BlogPostPage({
{/* Comments */}
@@ -206,8 +205,6 @@ export default async function BlogPostPage({
isAccessibleForFree: true,
}}
/>
-
-
>
);
}
diff --git a/docs/README.md b/docs/README.md
index 79695e0..5c283d9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,6 +1,6 @@
# 문서 인덱스
-Last updated: 2026-07-24
+Last updated: 2026-07-28
이 인덱스는 현재 코드베이스와 함께 유지해야 하는 문서만 추적한다. 계속
업데이트할 문서가 아니라면 삭제하거나, 오래 남겨야 하는 결정만 ADR로 옮긴다.
@@ -50,6 +50,7 @@ Last updated: 2026-07-24
- `docs/adr/0049-share-publication-policy-data-with-audit.md`
- `docs/adr/0050-retire-series-navigation-and-enforce-listed-post-policy.md`
- `docs/adr/0052-use-repo-local-public-writing-review-workflow.md`
+- `docs/adr/0053-use-inline-table-of-contents-for-article-reading.md`
- `docs/blog-quality-guide.md`
- `docs/content-publication-candidates.md`
- `docs/database/db-schema.md`
diff --git a/docs/adr/0053-use-inline-table-of-contents-for-article-reading.md b/docs/adr/0053-use-inline-table-of-contents-for-article-reading.md
new file mode 100644
index 0000000..d24868f
--- /dev/null
+++ b/docs/adr/0053-use-inline-table-of-contents-for-article-reading.md
@@ -0,0 +1,51 @@
+# 0053. 글 읽기 흐름에 인라인 목차를 사용한다
+
+Date: 2026-07-28
+Status: Accepted
+
+## 배경
+
+기존 글 상세 화면은 본문 바깥의 Portal 목차를 사용했다. 이 방식은 목차를
+항상 화면에 노출할 수 있지만, 본문과 목차의 관계가 분리되어 글의 실제
+읽기 흐름과 반응형 레이아웃을 함께 이해하기 어렵다. 본문 안에 목차를
+배치하면 짧은 글에서 목차를 숨길 수 있고, 독자가 글을 시작하는 위치에서
+구조를 바로 확인할 수 있다.
+
+## 결정
+
+- 목차는 글 본문 컨테이너 안에 인라인으로 렌더링한다.
+- 목차 항목이 2개 미만이면 렌더링하지 않는다.
+- 목차는 번호, 계층 들여쓰기, 현재 활성 heading 상태를 제공한다.
+- 데스크톱 콘텐츠 레이아웃에서는 본문과 댓글에 같은
+ `ark-article-container` 정렬 정책을 적용한다.
+- 기존 viewport별 rail과 타이포그래피 규칙은 ADR 0051을 유지하고, 이 ADR은
+ 글 내부 탐색 방식에 대한 결정을 추가한다.
+
+## 결과
+
+- 목차가 본문 시작과 가까워져 글의 구조를 빠르게 파악할 수 있다.
+- 짧은 글에는 불필요한 목차가 나타나지 않는다.
+- 본문과 댓글이 같은 콘텐츠 열에 정렬되어 넓은 데스크톱 화면에서도
+ 읽기 흐름과 대화 영역의 시작점이 일치한다.
+- 목차가 본문 흐름에 포함되므로 Portal 기반의 고정 목차보다 화면에
+ 항상 노출된다는 보장은 줄어든다.
+
+## 검토한 대안
+
+- **기존 Portal 목차 유지**: 항상 접근할 수 있지만 본문과 구조적으로
+ 분리되고 짧은 글에도 별도 탐색 UI가 남는다.
+- **데스크톱에서만 Portal 목차 유지**: 데스크톱과 모바일의 탐색 모델이
+ 달라지고, 본문과 목차의 위치 관계를 일관되게 설명하기 어렵다.
+- **본문과 댓글의 서로 다른 정렬 유지**: 넓은 화면에서 댓글이 본문보다
+ 오른쪽으로 밀려 콘텐츠 열의 시각적 기준선이 어긋난다.
+
+## 검증
+
+- 목차 컴포넌트의 2개 미만 항목, 계층, 활성 상태 테스트
+- viewport stylesheet selector 및 정렬 정책 단위 테스트
+- 전체 lint, CSS syntax 검사, production build
+
+## Related History
+
+- `7f3f6ec`: 본문 Portal 목차를 인라인 목차로 변경한 초기 구현
+- `a79f661`: 커밋 메시지 및 PR 메타데이터 정리
diff --git a/docs/adr/README.md b/docs/adr/README.md
index a41a146..48a9402 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -70,6 +70,7 @@ ADR은 AI 협업 가이드와 별개의 문서다. 사람이 결정했든 AI가
| [0050](0050-retire-series-navigation-and-enforce-listed-post-policy.md) | Accepted | 시리즈 탐색을 제거하고 발행 정책을 repository에서 강제한다 |
| [0051](0051-use-viewport-specific-reading-layout-and-type-scale.md) | Accepted | viewport별 읽기 레이아웃과 타이포그래피 스케일을 사용한다 |
| [0052](0052-use-repo-local-public-writing-review-workflow.md) | Accepted | 공개 글 검토에 repo-local writing review workflow를 사용한다 |
+| [0053](0053-use-inline-table-of-contents-for-article-reading.md) | Accepted | 글 읽기 흐름에 인라인 목차를 사용한다 |
## 작성 조건
diff --git a/styles/globals.css b/styles/globals.css
index f4c5816..da59496 100644
--- a/styles/globals.css
+++ b/styles/globals.css
@@ -184,11 +184,77 @@
.ark-site-external-link {
color: var(--color-text-primary);
- font-size: var(--text-base);
+ font-size: var(--text-sm);
font-weight: var(--font-normal);
line-height: 1.5rem;
}
+.ark-article-toc {
+ margin: var(--space-12) 0;
+ padding: var(--space-6) 0;
+ border-top: 1px solid var(--color-divider);
+ border-bottom: 1px solid var(--color-divider);
+}
+
+.ark-article-toc-title {
+ margin: 0 0 var(--space-4);
+ color: var(--color-grey-500);
+ font-family: var(--font-mono);
+ font-size: var(--text-xs);
+ font-weight: var(--font-medium);
+ letter-spacing: var(--tracking-wide);
+}
+
+.ark-article-toc-list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-1);
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.ark-article-toc-link {
+ display: flex;
+ gap: var(--space-3);
+ padding: var(--space-1) 0;
+ color: var(--color-grey-600);
+ font-family: var(--font-sans-emoji);
+ font-size: var(--text-sm);
+ line-height: 1.5rem;
+ transition: color var(--duration-150) var(--ease-default);
+}
+
+.ark-article-toc-link:hover {
+ color: var(--color-grey-900);
+}
+
+.ark-article-toc-link:focus-visible {
+ outline: 2px solid var(--color-accent);
+ outline-offset: 0.25rem;
+}
+
+.ark-article-toc-link-level-3 {
+ padding-left: var(--space-4);
+}
+
+.ark-article-toc-link-level-4 {
+ padding-left: var(--space-8);
+}
+
+.ark-article-toc-link-active {
+ color: var(--color-accent);
+ font-weight: var(--font-medium);
+}
+
+.ark-article-toc-number {
+ flex-shrink: 0;
+ color: var(--color-grey-400);
+ font-family: var(--font-mono);
+ font-size: var(--text-xs);
+ line-height: 1.5rem;
+}
+
/* ===== Article Entry ===== */
.ark-article {
@@ -636,12 +702,17 @@
.prose code {
font-family: var(--font-mono);
font-size: 0.875em;
- background-color: var(--color-bg-secondary);
- color: var(--color-text-primary);
- padding: 0.125rem 0.375rem;
+ background-color: var(--color-code-inline-bg);
+ color: var(--color-code-inline-fg);
+ padding: 0.1rem 0.35rem;
border-radius: var(--radius-action);
}
+.prose :not(pre) > code {
+ background-color: var(--color-code-inline-bg);
+ color: var(--color-code-inline-fg);
+}
+
/* Inline code should not have background if it's inside pre */
.prose pre code {
background-color: transparent;
diff --git a/styles/globals.test.ts b/styles/globals.test.ts
index e323fe7..bf953d1 100644
--- a/styles/globals.test.ts
+++ b/styles/globals.test.ts
@@ -84,6 +84,20 @@ describe('globals styles', () => {
expect(globalsContent).toContain('max-width: 34rem;');
});
+ it('keeps external links visually secondary to primary navigation', () => {
+ expect(globalsContent).toContain('.ark-site-external-link {');
+ expect(globalsContent).toContain('font-size: var(--text-sm);');
+ });
+
+ it('keeps inline code lighter than fenced code blocks', () => {
+ expect(tokensContent).toContain('--color-code-inline-bg: #d8d8dc;');
+ expect(tokensContent).toContain('--color-code-inline-fg: #52525b;');
+ expect(tokensContent).toContain('--color-code-bg: #3f3f46;');
+ expect(globalsContent).toContain(
+ '.prose :not(pre) > code {\n background-color: var(--color-code-inline-bg);'
+ );
+ });
+
it('gives the mobile home page a split first-entry layout', () => {
expect(mobileViewportContent).toContain(
".ark-site-grid[data-page-layout='home']"
@@ -134,6 +148,12 @@ describe('globals styles', () => {
expect(contentViewport).toContain(
".ark-site-grid[data-page-layout='content'] .ark-article {\n padding-top: 0;"
);
+ expect(contentViewport).toContain('position: sticky;');
+ expect(contentViewport).toContain('top: 2.5rem;');
+ expect(contentViewport).toContain(
+ ".ark-site-grid[data-page-layout='content'] .ark-article-container {"
+ );
+ expect(contentViewport).toContain('margin-left: 0;');
});
it('matches hero and primary navigation sizes at intermediate widths', () => {
diff --git a/styles/tokens.css b/styles/tokens.css
index bc839e5..39f1d67 100644
--- a/styles/tokens.css
+++ b/styles/tokens.css
@@ -53,9 +53,11 @@
--mobile-nav-focus-offset: var(--mobile-nav-bg);
/* Code Block Colors */
- --color-code-bg: #252525;
- --color-code-fg: #eaebea;
- --color-code-title-bg: #3f3f46;
+ --color-code-bg: #3f3f46;
+ --color-code-fg: #e2e2e5;
+ --color-code-inline-bg: #d8d8dc;
+ --color-code-inline-fg: #52525b;
+ --color-code-title-bg: #52525b;
--color-code-title-fg: #eaebea;
--color-code-highlight: rgba(234, 235, 234, 0.12);
--color-code-divider: rgba(234, 235, 234, 0.16);
diff --git a/styles/tokens.test.ts b/styles/tokens.test.ts
index d11c578..55f2e14 100644
--- a/styles/tokens.test.ts
+++ b/styles/tokens.test.ts
@@ -13,6 +13,7 @@ describe('Ark paper token definitions', () => {
expect(tokensContent).toContain('--text-meta: 0.75rem;');
expect(tokensContent).toContain('--text-reading: 0.875rem;');
expect(tokensContent).toContain('--text-prose-h1: 1.375rem;');
+ expect(tokensContent).toContain('--color-code-inline-bg: #d8d8dc;');
});
it('defines semantic radius roles for actions, content, and selections', () => {
diff --git a/styles/viewport/content.css b/styles/viewport/content.css
index 9e60984..73bf556 100644
--- a/styles/viewport/content.css
+++ b/styles/viewport/content.css
@@ -7,6 +7,9 @@
.ark-site-grid[data-page-layout='content'] .ark-site-navigation {
grid-column: 1;
grid-row: 1;
+ position: sticky;
+ top: 2.5rem;
+ align-self: start;
padding-top: 4.5rem;
}
@@ -18,4 +21,9 @@
.ark-site-grid[data-page-layout='content'] .ark-article {
padding-top: 0;
}
+
+ .ark-site-grid[data-page-layout='content'] .ark-article-container {
+ margin-right: 0;
+ margin-left: 0;
+ }
}