From ca7d15fe668894ed57e10cda1b8e37e14a984d5c Mon Sep 17 00:00:00 2001 From: JeongwooSeo <98446924+ShipFriend0516@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:12:51 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20youtube=20embed=20rehype=20?= =?UTF-8?q?=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/post/detail/PostBody.tsx | 158 +++++++++++++++++++++++++- app/globals.css | 42 +++++++ 2 files changed, 199 insertions(+), 1 deletion(-) diff --git a/app/entities/post/detail/PostBody.tsx b/app/entities/post/detail/PostBody.tsx index 53031c4..c61d88e 100644 --- a/app/entities/post/detail/PostBody.tsx +++ b/app/entities/post/detail/PostBody.tsx @@ -35,6 +35,156 @@ const PostBody = ({ content, tags, loading }: Props) => { } } }; + + const renderOpenGraph = (node: any, index?: number, parent?: Element) => { + if (node.type === 'element' && node.tagName === 'p' && node.children) { + const aTag = node.children.find( + (node: any) => node.type === 'element' && node.tagName === 'a' + ); + if (!aTag) return; + + const href = aTag.properties?.href; + if (href && href.startsWith('/')) { + const opengraph: Element = { + type: 'element', + tagName: 'a', + properties: { + className: 'open-graph', + href: href, + }, + children: [ + { + type: 'element', + tagName: 'img', + properties: { + src: `${href}`, + alt: 'Open Graph Image', + className: 'og-image', + }, + children: [], + }, + { + type: 'element', + tagName: 'div', + properties: { + className: 'og-container', + }, + children: [ + { + type: 'element', + tagName: 'h4', + properties: { + className: 'og-title', + }, + children: [ + { + type: 'text', + value: decodeURIComponent( + href.split('/').pop() + ).replaceAll('-', ' '), + }, + ], + }, + { + type: 'element', + tagName: 'span', + properties: { + className: 'og-content', + }, + children: [ + { + type: 'text', + value: decodeURIComponent( + href.split('/').pop() + ).replaceAll('-', ' '), + }, + ], + }, + { + type: 'element', + tagName: 'span', + properties: { + className: 'og-domain', + }, + children: [ + { + type: 'text', + value: '', + }, + ], + }, + ], + }, + ], + }; + // 부모가 존재하고 children 배열이 있는 경우 + if (parent && parent.children && Array.isArray(parent.children)) { + // 현재 a 태그 다음 위치에 div 삽입 s + parent.children.splice(index + 1, 0, opengraph); + } else return; + } + } + }; + + const renderYoutubeEmbed = (node: any, index?: number, parent?: Element) => { + if (node.type === 'element' && node.tagName === 'p' && node.children) { + const aTag = node.children.find( + (node: any) => node.type === 'element' && node.tagName === 'a' + ); + if (!aTag) return; + + const href = aTag.properties?.href; + const isYoutubeLink = + href && + (href.startsWith('https://www.youtube.com/watch') || + href.startsWith('https://youtu.be/')); + + if (isYoutubeLink) { + const urlType = href.startsWith('https://www.youtube.com/watch') + ? 'watch' + : 'be'; + + const videoId = + urlType === 'watch' + ? new URL(href).searchParams.get('v') + : href.split('/').pop(); + + if (videoId) { + const youtubeEmbed = createYoutubeIframe(videoId, 736, 414); + // 부모가 존재하고 children 배열이 있는 경우 + if ( + index && + parent && + parent.children && + Array.isArray(parent.children) + ) { + parent.children.splice(index + 1, 0, youtubeEmbed); + } else return; + } + } + } + }; + + const createYoutubeIframe = ( + videoId: string, + width: number, + height: number + ) => { + return { + type: 'element', + tagName: 'iframe', + properties: { + src: `https://www.youtube.com/embed/${videoId}`, + width: width.toString(), + height: height.toString(), + frameBorder: '0', + allowFullScreen: true, + className: 'youtube-embed', + }, + children: [], + }; + }; + return (
{ wrapperElement={{ 'data-color-mode': theme, }} - rehypeRewrite={(node) => { + rehypeRewrite={(node, index?, parent?) => { asideStyleRewrite(node); + renderOpenGraph(node, index || 0, parent as Element | undefined); + renderYoutubeEmbed( + node, + index || 0, + parent as Element | undefined + ); }} /> diff --git a/app/globals.css b/app/globals.css index 1c1660b..744269a 100644 --- a/app/globals.css +++ b/app/globals.css @@ -173,6 +173,47 @@ article.post .post-body hr::before { 0 -2px 4px -1px rgba(0, 0, 0, 0.06); } +.post-body .open-graph { + display: flex; + height: 120px; + background-color: #ededed; + overflow: hidden; + flex-direction: row; + justify-content: center; + border-radius: 8px; + margin-bottom: 0.5em; + box-shadow: 0 0 2px rgba(0, 0, 0, 0.1); + text-decoration: none; + color: var(--text-default); +} + +.post-body .open-graph img.og-image { + flex-grow: 1; + border-radius: 0; + margin: 0; + padding: 0.25em; + background-color: white; +} + +.open-graph .og-container { + flex-grow: 2; + display: flex; + flex-direction: column; + justify-content: center; + padding: 0.5em; + gap: 4px; +} + +.post-body .youtube-embed { + position: relative; + margin: 1em auto; + width: 100%; + height: auto; + aspect-ratio: 16 / 9; + border-radius: 1em; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.05); +} + .post-body aside { background-color: rgba(231, 241, 231); border-radius: 4px; @@ -193,6 +234,7 @@ article.post .post-body hr::before { .post-body aside p { margin: 0; } + .dark .post-body p > code, .dark .post-body li > code, .dark .post-body strong > code, From c30480332a60e97b7a211a083630b05271202d85 Mon Sep 17 00:00:00 2001 From: JeongwooSeo <98446924+ShipFriend0516@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:14:23 +0900 Subject: [PATCH 2/4] style: code block shadow --- app/globals.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/globals.css b/app/globals.css index 744269a..1e60717 100644 --- a/app/globals.css +++ b/app/globals.css @@ -167,6 +167,10 @@ article.post .post-body hr::before { content: ''; } +article.post .post-body pre { + box-shadow: 0 0 4px rgba(0, 0, 0, 0.05); +} + .shadow-top { box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1), From ec8c49825c4bb8dd4f8fa60bd6e05cb67c7ad6cf Mon Sep 17 00:00:00 2001 From: JeongwooSeo <98446924+ShipFriend0516@users.noreply.github.com> Date: Wed, 30 Jul 2025 19:18:04 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20element=20=EA=B4=80=EB=A0=A8=20type?= =?UTF-8?q?=20error=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/post/detail/PostBody.tsx | 157 ++++++++++++++------------ 1 file changed, 84 insertions(+), 73 deletions(-) diff --git a/app/entities/post/detail/PostBody.tsx b/app/entities/post/detail/PostBody.tsx index c61d88e..3f80ffa 100644 --- a/app/entities/post/detail/PostBody.tsx +++ b/app/entities/post/detail/PostBody.tsx @@ -45,80 +45,14 @@ const PostBody = ({ content, tags, loading }: Props) => { const href = aTag.properties?.href; if (href && href.startsWith('/')) { - const opengraph: Element = { - type: 'element', - tagName: 'a', - properties: { - className: 'open-graph', - href: href, - }, - children: [ - { - type: 'element', - tagName: 'img', - properties: { - src: `${href}`, - alt: 'Open Graph Image', - className: 'og-image', - }, - children: [], - }, - { - type: 'element', - tagName: 'div', - properties: { - className: 'og-container', - }, - children: [ - { - type: 'element', - tagName: 'h4', - properties: { - className: 'og-title', - }, - children: [ - { - type: 'text', - value: decodeURIComponent( - href.split('/').pop() - ).replaceAll('-', ' '), - }, - ], - }, - { - type: 'element', - tagName: 'span', - properties: { - className: 'og-content', - }, - children: [ - { - type: 'text', - value: decodeURIComponent( - href.split('/').pop() - ).replaceAll('-', ' '), - }, - ], - }, - { - type: 'element', - tagName: 'span', - properties: { - className: 'og-domain', - }, - children: [ - { - type: 'text', - value: '', - }, - ], - }, - ], - }, - ], - }; // 부모가 존재하고 children 배열이 있는 경우 - if (parent && parent.children && Array.isArray(parent.children)) { + const opengraph = createOpenGraph(href); + if ( + index && + parent && + parent.children && + Array.isArray(parent.children) + ) { // 현재 a 태그 다음 위치에 div 삽입 s parent.children.splice(index + 1, 0, opengraph); } else return; @@ -185,6 +119,83 @@ const PostBody = ({ content, tags, loading }: Props) => { }; }; + const createOpenGraph = (href: string) => { + return { + type: 'element', + tagName: 'a', + properties: { + className: 'open-graph', + href: href, + }, + children: [ + { + type: 'element', + tagName: 'img', + properties: { + src: `${href}`, + alt: 'Open Graph Image', + className: 'og-image', + }, + children: [], + }, + { + type: 'element', + tagName: 'div', + properties: { + className: 'og-container', + }, + children: [ + { + type: 'element', + tagName: 'h4', + properties: { + className: 'og-title', + }, + children: [ + { + type: 'text', + value: decodeURIComponent(href.split('/').pop()!).replaceAll( + '-', + ' ' + ), + }, + ], + }, + { + type: 'element', + tagName: 'span', + properties: { + className: 'og-content', + }, + children: [ + { + type: 'text', + value: decodeURIComponent(href.split('/').pop()!).replaceAll( + '-', + ' ' + ), + }, + ], + }, + { + type: 'element', + tagName: 'span', + properties: { + className: 'og-domain', + }, + children: [ + { + type: 'text', + value: '', + }, + ], + }, + ], + }, + ], + }; + }; + return (
Date: Wed, 30 Jul 2025 19:56:14 +0900 Subject: [PATCH 4/4] =?UTF-8?q?chore:=20og=20=EC=9D=BC=EB=8B=A8=20?= =?UTF-8?q?=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/entities/post/detail/PostBody.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/entities/post/detail/PostBody.tsx b/app/entities/post/detail/PostBody.tsx index 3f80ffa..b2fd342 100644 --- a/app/entities/post/detail/PostBody.tsx +++ b/app/entities/post/detail/PostBody.tsx @@ -48,7 +48,7 @@ const PostBody = ({ content, tags, loading }: Props) => { // 부모가 존재하고 children 배열이 있는 경우 const opengraph = createOpenGraph(href); if ( - index && + index !== undefined && parent && parent.children && Array.isArray(parent.children) @@ -168,13 +168,13 @@ const PostBody = ({ content, tags, loading }: Props) => { className: 'og-content', }, children: [ - { - type: 'text', - value: decodeURIComponent(href.split('/').pop()!).replaceAll( - '-', - ' ' - ), - }, + // { + // type: 'text', + // value: decodeURIComponent(href.split('/').pop()!).replaceAll( + // '-', + // ' ' + // ), + // }, ], }, { @@ -221,7 +221,7 @@ const PostBody = ({ content, tags, loading }: Props) => { }} rehypeRewrite={(node, index?, parent?) => { asideStyleRewrite(node); - renderOpenGraph(node, index || 0, parent as Element | undefined); + // renderOpenGraph(node, index || 0, parent as Element | undefined); renderYoutubeEmbed( node, index || 0,