게시물 인아티클 광고 슬롯 추가

This commit is contained in:
2026-06-05 16:09:47 +09:00
parent 5c93643949
commit cc9e5949fa
14 changed files with 121 additions and 9 deletions

View File

@@ -24,6 +24,12 @@ const adSlots = [
description: '게시물 상세 본문 렌더링 직전에 표시됩니다.',
placeholder: '<script async src="..."><' + '/script>\n<ins class="adsbygoogle" ...></ins>'
},
{
key: 'adPostInArticleCode',
label: '게시물 인아티클',
description: '게시물 본문이 충분히 길 때 전체 블록 40% 근처 문단 뒤에 한 번 표시됩니다.',
placeholder: '<ins class="adsbygoogle" ...></ins>'
},
{
key: 'adPostBottomCode',
label: '게시물 본문 하단',

View File

@@ -59,6 +59,11 @@ const props = defineProps({
slashSuppressedLines: {
type: Array,
default: () => []
},
/** 공개 게시물 본문 중간에 삽입할 인아티클 광고 코드 */
inArticleAdCode: {
type: String,
default: ''
}
})
@@ -757,6 +762,71 @@ const parseMarkdownBlocks = (markdown) => {
}
const blocks = computed(() => parseMarkdownBlocks(props.content))
const normalizedInArticleAdCode = computed(() => String(props.inArticleAdCode || '').trim())
/**
* 블록의 텍스트 길이를 계산한다.
* @param {Object} block - 마크다운 블록
* @returns {number} 공백을 제외한 텍스트 길이
*/
const getBlockPlainTextLength = (block) => {
const value = Array.isArray(block.text) ? block.text.join(' ') : String(block.text || '')
return value.replace(/\s+/g, '').length
}
/**
* 본문 인아티클 광고를 삽입할 수 있는 문단 블록인지 확인한다.
* @param {Object} block - 마크다운 블록
* @returns {boolean} 삽입 후보 여부
*/
const isInArticleAdCandidateBlock = (block) => {
return block?.type === 'paragraph' && getBlockPlainTextLength(block) >= 12
}
/**
* 본문 인아티클 광고를 삽입할 블록 ID를 반환한다.
* @returns {string} 삽입 기준 블록 ID
*/
const inArticleAdAfterBlockId = computed(() => {
if (props.interactive || !normalizedInArticleAdCode.value) {
return ''
}
const currentBlocks = blocks.value
const contentBlocks = currentBlocks.filter((block) => !['spacer', 'divider'].includes(block.type))
if (contentBlocks.length < 10) {
return ''
}
const candidates = currentBlocks
.map((block, index) => ({ block, index }))
.filter((item) => isInArticleAdCandidateBlock(item.block))
if (candidates.length < 6) {
return ''
}
const minIndex = Math.max(3, Math.floor(currentBlocks.length * 0.2))
const maxIndex = Math.min(currentBlocks.length - 4, Math.floor(currentBlocks.length * 0.75))
if (maxIndex < minIndex) {
return ''
}
const targetIndex = Math.floor((currentBlocks.length - 1) * 0.4)
const rangedCandidates = candidates.filter((item) => item.index >= minIndex && item.index <= maxIndex)
if (!rangedCandidates.length) {
return ''
}
const selected = rangedCandidates.reduce((closest, item) => {
return Math.abs(item.index - targetIndex) < Math.abs(closest.index - targetIndex) ? item : closest
}, rangedCandidates[0])
return selected.block.id
})
const headingIdsByBlock = computed(() => {
const createHeadingId = createHeadingIdFactory()
const ids = {}
@@ -3259,6 +3329,13 @@ onBeforeUnmount(() => {
</template>
</template>
</p>
<SiteAdSlot
v-if="block.id === inArticleAdAfterBlockId"
class="content-markdown-renderer__in-article-ad my-8"
:code="normalizedInArticleAdCode"
location="post-in-article"
/>
</template>
<div