게시글 상세 목차 사이드바 추가 v1.5.12

This commit is contained in:
2026-05-27 11:29:33 +09:00
parent 7f017a03a5
commit 602106ac9d
12 changed files with 181 additions and 8 deletions

View File

@@ -21,6 +21,7 @@ import {
import { buildCodeBlockLines, parseCodeFenceLine } from '../../lib/markdown-code-block.js'
import { buildToggleBlockLines } from '../../lib/markdown-toggle.js'
import { CALLOUT_BACKGROUND_OPTIONS, parseCalloutOptions } from '../../lib/markdown-callout.js'
import { createHeadingIdFactory } from '../../lib/markdown-toc.js'
import ContentMarkdownCodeBlockEditor from './ContentMarkdownCodeBlockEditor.vue'
import ProseCodeBlock from './ProseCodeBlock.vue'
import ContentMarkdownCalloutEditor from './ContentMarkdownCalloutEditor.vue'
@@ -731,6 +732,18 @@ const parseMarkdownBlocks = (markdown) => {
}
const blocks = computed(() => parseMarkdownBlocks(props.content))
const headingIdsByBlock = computed(() => {
const createHeadingId = createHeadingIdFactory()
const ids = {}
for (const block of blocks.value) {
if (block.type === 'heading' && block.level >= 1 && block.level <= 3) {
ids[block.id] = createHeadingId(block.text)
}
}
return ids
})
/** @type {import('vue').ComputedRef<number>} 문서 맨 아래 단일 이미지 삽입 줄 */
const tailInsertBeforeLine = computed(() => {
@@ -2323,7 +2336,7 @@ onBeforeUnmount(() => {
@delete-line="onDeleteLine"
@merge-with-previous="onMergeWithPreviousLine(block.meta.startLine, $event)"
/>
<ProseHeading v-else-if="block.type === 'heading'" :level="block.level">
<ProseHeading v-else-if="block.type === 'heading'" :id="headingIdsByBlock[block.id]" :level="block.level">
<template v-for="(segment, segmentIndex) in parseInlineSegments(block.text)" :key="`${block.id}-heading-${segmentIndex}`">
<strong v-if="segment.type === 'strong'">{{ segment.text }}</strong>
<em v-else-if="segment.type === 'em'">{{ segment.text }}</em>

View File

@@ -3,6 +3,10 @@ const props = defineProps({
level: {
type: Number,
default: 2
},
id: {
type: String,
default: ''
}
})
@@ -12,7 +16,8 @@ const tagName = computed(() => `h${Math.min(Math.max(props.level, 1), 6)}`)
<template>
<component
:is="tagName"
class="prose-heading mb-2.5 font-semibold leading-[1.25] tracking-normal first:mt-0"
:id="id || undefined"
class="prose-heading mb-2.5 scroll-mt-20 font-semibold leading-[1.25] tracking-normal first:mt-0"
:class="{
'text-[clamp(1.35rem,1.25rem+0.35vw,1.6rem)] leading-[1.15]': level === 1,
'text-[clamp(1.2rem,1.15rem+0.3vw,1.4rem)]': level === 2,

View File

@@ -1,6 +1,9 @@
<script setup>
import { getExternalFaviconUrl } from '~/lib/external-favicon-url.js'
const route = useRoute()
const postToc = useState('post-detail-toc', () => [])
const followLinks = [
{ id: 'facebook', label: 'Facebook', href: 'https://facebook.com', icon: 'facebook' },
{ id: 'x', label: 'X', href: 'https://x.com', icon: 'x' },
@@ -39,6 +42,8 @@ const recommendedSites = computed(() => {
}
return list.filter((x) => x?.isVisible !== false)
})
const isPostDetailRoute = computed(() => route.path.startsWith('/post/'))
const postTocItems = computed(() => Array.isArray(postToc.value) ? postToc.value : [])
/**
* 새 탭으로 열 외부 URL인지
@@ -202,7 +207,38 @@ const showAboutSection = false
</div>
<div
v-if="recommendedSites.length"
v-if="isPostDetailRoute"
class="right-sidebar__block right-sidebar__toc site-sidebar-section py-5 pl-5 pr-0"
>
<div class="right-sidebar__row flex items-center justify-between">
<p class="right-sidebar__eyebrow text-xs font-semibold uppercase site-muted">
TOC
</p>
</div>
<nav class="right-sidebar__toc-nav mt-4" aria-label="게시글 목차">
<ul v-if="postTocItems.length" class="right-sidebar__toc-list list-none space-y-2 p-0">
<li v-for="item in postTocItems" :key="item.id">
<a
class="right-sidebar__toc-link site-interactive block rounded-md py-1.5 pr-3 text-sm leading-snug text-[var(--site-text)] hover:text-[var(--site-accent)]"
:class="{
'pl-0 font-semibold': item.level === 1,
'pl-3': item.level === 2,
'pl-6 text-xs site-muted': item.level === 3
}"
:href="`#${item.id}`"
>
{{ item.text }}
</a>
</li>
</ul>
<p v-else class="right-sidebar__toc-empty text-sm site-muted">
목차로 표시할 제목이 없습니다.
</p>
</nav>
</div>
<div
v-else-if="recommendedSites.length"
class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-0"
>
<div class="right-sidebar__row flex items-center justify-between">