diff --git a/components/content/ContentMarkdownRenderer.vue b/components/content/ContentMarkdownRenderer.vue index 7ac9ab7..756a6c1 100644 --- a/components/content/ContentMarkdownRenderer.vue +++ b/components/content/ContentMarkdownRenderer.vue @@ -29,7 +29,8 @@ const createBlock = (type = 'paragraph', text = '', level = null, id = '', optio variant: options.variant || '', ordered: options.ordered || false, width: options.width || 'regular', - images: options.images || [] + images: options.images || [], + meta: options.meta && typeof options.meta === 'object' ? { ...options.meta } : {} }) /** @@ -72,6 +73,96 @@ const collectFencedLines = (lines, startIndex) => { } } +/** + * 북마크 fenced 블록 본문에서 URL·제목·설명·썸네일을 파싱한다. + * @param {string} raw - fenced 내부 텍스트 + * @returns {{url: string, title: string, description: string, thumbnail: string}} 북마크 메타 + */ +const parseBookmarkMeta = (raw) => { + const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean) + const meta = { + url: '', + title: '', + description: '', + thumbnail: '' + } + + for (const line of lines) { + const kv = line.match(/^(\w+)=(.*)$/) + + if (kv) { + const key = kv[1].toLowerCase() + const val = kv[2].trim() + + if (key === 'url') { + meta.url = val + } else if (key === 'title') { + meta.title = val + } else if (key === 'description' || key === 'desc') { + meta.description = val + } else if (key === 'thumbnail' || key === 'image') { + meta.thumbnail = val + } + + continue + } + + if (!meta.url && /^https?:\/\//i.test(line)) { + meta.url = line + continue + } + + if (meta.url && !meta.title) { + meta.title = line + continue + } + + if (meta.url && meta.title && !meta.description) { + meta.description = line + } + } + + return meta +} + +/** + * 회원가입(CTA) fenced 블록 본문에서 표시 문구를 파싱한다. + * @param {string} raw - fenced 내부 텍스트 + * @returns {{title: string, description: string, button: string, placeholder: string}} CTA 메타 + */ +const parseSignupMeta = (raw) => { + const meta = { + title: '뉴스레터에 가입하세요', + description: '새 글이 올라오면 받아보실 수 있어요.', + button: '구독하기', + placeholder: 'you@example.com' + } + const lines = raw.split('\n').map((l) => l.trim()).filter(Boolean) + + for (const line of lines) { + const kv = line.match(/^(\w+)=(.*)$/) + + if (!kv) { + continue + } + + const key = kv[1].toLowerCase() + const val = kv[2].trim() + + if (key === 'title') { + meta.title = val + } else if (key === 'description' || key === 'desc') { + meta.description = val + } else if (key === 'button') { + meta.button = val + } else if (key === 'placeholder') { + meta.placeholder = val + } + } + + return meta +} + /** * 마크다운 문자열을 렌더링용 블록 목록으로 변환 * @param {string} markdown - 마크다운 문자열 @@ -105,6 +196,26 @@ const parseMarkdownBlocks = (markdown) => { continue } + if (trimmedLine === ':::bookmark') { + const { contentLines, nextIndex } = collectFencedLines(lines, index + 1) + const bookmarkMeta = parseBookmarkMeta(contentLines.join('\n')) + + if (bookmarkMeta.url) { + blocks.push(createBlock('bookmark', '', null, `block-${blocks.length}`, { meta: bookmarkMeta })) + } + + index = nextIndex + continue + } + + if (trimmedLine === ':::signup') { + const { contentLines, nextIndex } = collectFencedLines(lines, index + 1) + const signupMeta = parseSignupMeta(contentLines.join('\n')) + blocks.push(createBlock('signup', '', null, `block-${blocks.length}`, { meta: signupMeta })) + index = nextIndex + continue + } + if (trimmedLine === ':::gallery') { const { contentLines, nextIndex } = collectFencedLines(lines, index + 1) const images = [] @@ -287,6 +398,20 @@ const showNextImage = () => { {{ block.text }} + +