메인 인피드 광고 슬롯 추가
This commit is contained in:
@@ -6,6 +6,12 @@ const adSlots = [
|
||||
description: '메인 화면 Featured와 Latest 목록 사이에 표시됩니다.',
|
||||
placeholder: '<ins class="adsbygoogle" ...></ins>'
|
||||
},
|
||||
{
|
||||
key: 'adHomeInfeedCode',
|
||||
label: '메인 인피드',
|
||||
description: '메인 화면 Latest 게시물 목록 사이 한 곳에 무작위로 표시됩니다.',
|
||||
placeholder: '<ins class="adsbygoogle" ...></ins>'
|
||||
},
|
||||
{
|
||||
key: 'adSidebarCode',
|
||||
label: '오른쪽 사이드',
|
||||
|
||||
2
db/migrations/051_site_settings_home_infeed_ad.sql
Normal file
2
db/migrations/051_site_settings_home_infeed_ad.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE site_settings
|
||||
ADD COLUMN IF NOT EXISTS ad_home_infeed_code TEXT NOT NULL DEFAULT '';
|
||||
@@ -1,5 +1,9 @@
|
||||
# 의사결정 이력
|
||||
|
||||
## 2026-06-05 v1.5.72 — 메인 인피드 광고는 클라이언트에서 한 번 무작위 배치
|
||||
|
||||
메인 Latest 목록 사이 광고는 매번 같은 위치보다 자연스럽게 보이도록 무작위 삽입을 허용한다. 다만 SSR 단계에서 무작위 값을 만들면 서버 HTML과 클라이언트 hydration 결과가 달라질 수 있으므로, 브라우저 마운트 이후 게시물 사이 한 지점을 한 번 산출해 삽입한다. 광고는 첫 항목 앞이나 마지막 항목 뒤가 아니라 게시물 사이에만 들어가도록 제한한다.
|
||||
|
||||
## 2026-06-05 v1.5.71 — 화면 위치 광고 슬롯은 사이트 코드와 분리 관리
|
||||
|
||||
사이트 검증·공통 스크립트용 사이트 코드 카드와, 실제 화면 위치에 삽입되는 광고 단위를 분리했다. 이유는 ads.txt/head/footer는 전역 설정이고, 메인 피드·사이드바·게시물 본문 상단·하단 광고는 위치별 표시 여부와 레이아웃 책임이 다르기 때문이다. 광고 HTML은 관리자 신뢰 콘텐츠로 저장하되, 빈 값은 DOM을 만들지 않고 클라이언트에서만 삽입해 AdSense 스크립트 중복 실행 가능성을 줄인다.
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
| components/site/LeftSidebar.vue | 왼쪽 사이드바, `lg+`는 `sticky`+고정 높이+내부 무스크롤바 스크롤, `lg` 미만은 고정 슬라이드 패널, 상단 메뉴는 `SidebarPrimaryNavList`+`provide`로 트리·펼침 상태(`sori-primary-nav-expanded`), Authors 영역은 비공개, 푸터 `footer` 링크는 `flex-wrap`·테마 버튼 `shrink-0`, 태그 카테고리·테마 점은 `site-sidebar-nav-row` 호버 |
|
||||
| components/site/SidebarPrimaryNavList.vue | 상단 네비: 부모·리프 동일 `before` 막대/호버 원형, 내부 현재 경로 `--site-accent`, 행 `w-full`+`site-sidebar-nav-row` 호버(`#F7F4EF` 라이트), `inject`·`localStorage` 펼침 |
|
||||
| components/site/RightSidebar.vue | 오른쪽 사이드바, 일반 화면은 공개 `GET /api/navigation`의 `recommended` 카드 목록(대체 텍스트·썸네일 우선, 외부 URL은 Google 파비콘 fallback), 게시글 상세 데스크톱은 Recommended 대신 H1~H3 TOC(모바일 숨김, 스크롤 위치 기반 활성 표시·내부 자동 스크롤), 설정 기반 SNS Follow(프리셋·사용자 SVG, 16px 아이콘 중앙 정렬)·구독 폼, About 영역은 비공개, `lg+` 스티키 |
|
||||
| components/site/SiteAdSlot.vue | 사이트 설정 Ads HTML 코드 렌더링, 메인 피드·오른쪽 사이드·게시물 본문 상단·하단 광고 슬롯 |
|
||||
| components/site/SiteAdSlot.vue | 사이트 설정 Ads HTML 코드 렌더링, 메인 피드·메인 인피드·오른쪽 사이드·게시물 본문 상단·하단 광고 슬롯 |
|
||||
| components/site/MainColumn.vue | 메인 화면 중앙, `lg:max-w-[720px]`로 본문 상한 |
|
||||
| components/site/HomeHero.vue | 홈 상단 720px 커버 배너, 라이트·다크 테마별 이미지 교체, 왼쪽 하단 오버레이 제목·본문 |
|
||||
| components/site/PostCard.vue | 목록의 게시물 카드, 카드 hover 인터랙션, 태그는 있을 때만 메타에 표시 |
|
||||
@@ -91,7 +91,7 @@
|
||||
|------|-----------|
|
||||
| components/admin/AdminSettingsNavIcon.vue | 사이트 설정 좌측 내비 항목 아이콘(`iconId`별 SVG·미구현 placeholder) |
|
||||
| components/admin/AdminSiteCodeSettingsCard.vue | 관리자 사이트 설정의 ads.txt·공통 헤더 코드·공통 푸터 코드 카드 |
|
||||
| components/admin/AdminAdsSettingsCard.vue | 관리자 사이트 설정의 위치별 Ads 코드 카드(메인 피드·오른쪽 사이드·게시물 본문 상단·하단) |
|
||||
| components/admin/AdminAdsSettingsCard.vue | 관리자 사이트 설정의 위치별 Ads 코드 카드(메인 피드·메인 인피드·오른쪽 사이드·게시물 본문 상단·하단) |
|
||||
| components/admin/AdminPostExportFileRow.vue | 관리자 사이트 설정 내보내기 작업의 분할 파일 선택 행 |
|
||||
| components/admin/AdminMediaVideoThumbnail.vue | 관리자 미디어 목록 비디오 항목의 초반 프레임 캔버스 썸네일 |
|
||||
| components/admin/AdminPostForm.vue | 관리자 글 작성/수정 폼, Ghost형 툴바(왼쪽 상태 텍스트·Publish/Update/Unpublish/Unschedule, 서버 반영 상태 기준 분기), 초안만 서버 디바운스 자동 저장·신규 임시 슬러그·발행·예약·멤버십·비공개 상태 저장, 발행 모달(중앙 배치), 좌우 설정 패널(작은 화면은 오른쪽 고정 오버레이), 오른쪽 `View Post` 링크, 미리보기 emit·미저장 이탈 가드, 추천 글 토글, 태그 색상 배지 다중 입력·메인 태그 드롭다운·부분 검색 추천 |
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
- 기본 보기 방식은 `compact`이며, Default 선택 시에도 `compact`로 복원한다.
|
||||
- `compact`는 썸네일을 포함한 짧은 행 형태, `list`는 텍스트 중심 목록 형태, `cards`는 카드 그리드 형태로 표시한다.
|
||||
- Latest 섹션은 게시물이 적어도 보기 방식 선택 메뉴가 아래쪽에서 잘리지 않도록 최소 높이를 둔다.
|
||||
- 사이트 설정 Ads의 메인 피드 광고 코드가 있으면 Featured 영역과 Latest 목록 사이에 광고 슬롯을 표시한다. 비어 있으면 슬롯 자체를 렌더링하지 않는다.
|
||||
- 사이트 설정 Ads의 메인 피드 광고 코드가 있으면 Featured 영역과 Latest 목록 사이에 광고 슬롯을 표시한다. 메인 인피드 광고 코드가 있으면 Latest 게시물 목록 사이 한 곳에 브라우저 렌더 시점 기준으로 무작위 삽입한다. 비어 있으면 슬롯 자체를 렌더링하지 않는다.
|
||||
|
||||
### Post 페이지
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
### 사이트 광고 슬롯
|
||||
|
||||
- 관리자 사이트 설정의 Ads 카드는 `adHomeFeedCode`, `adSidebarCode`, `adPostTopCode`, `adPostBottomCode` 네 위치의 HTML 코드를 저장한다.
|
||||
- 관리자 사이트 설정의 Ads 카드는 `adHomeFeedCode`, `adHomeInfeedCode`, `adSidebarCode`, `adPostTopCode`, `adPostBottomCode` 다섯 위치의 HTML 코드를 저장한다.
|
||||
- 각 값은 관리자만 입력하는 신뢰 콘텐츠를 전제로 하며, 애드센스에서 제공하는 반응형 또는 고정 크기 HTML 코드를 그대로 붙여 넣는다.
|
||||
- 공개 화면은 `SiteAdSlot` 컴포넌트로 광고 슬롯을 렌더링한다. 값이 비어 있으면 DOM을 만들지 않고, 값이 있으면 클라이언트에서 HTML을 삽입한 뒤 내부 script를 실행 가능한 노드로 교체해 Nuxt 클라이언트 라우팅 후에도 광고 코드가 동작하게 한다.
|
||||
- 공통 헤더·푸터 코드와 ads.txt는 기존 사이트 코드 카드에서 관리하고, 화면 위치가 필요한 광고 단위는 Ads 카드에서 관리한다.
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 업데이트 이력
|
||||
|
||||
## v1.5.72
|
||||
|
||||
- 사이트 설정 Ads: 메인 인피드 광고 슬롯 추가.
|
||||
- 메인 화면: Latest 게시물 목록 사이 한 곳에 인피드 광고를 무작위 삽입하도록 추가.
|
||||
- 사이트 설정 저장소: 메인 인피드 광고 코드 컬럼과 관리자 입력 검증 추가.
|
||||
|
||||
## v1.5.71
|
||||
|
||||
- 사이트 설정: 위치별 광고 코드를 관리하는 Ads 카드 추가.
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.5.71",
|
||||
"version": "1.5.72",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sori.studio",
|
||||
"version": "1.5.71",
|
||||
"version": "1.5.72",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sori.studio",
|
||||
"version": "1.5.71",
|
||||
"version": "1.5.72",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"imports": {
|
||||
|
||||
@@ -137,6 +137,7 @@ const siteCodeSnapshot = reactive({
|
||||
/** 편집 시작 시점의 광고 슬롯(취소 시 복원용) */
|
||||
const adsSnapshot = reactive({
|
||||
adHomeFeedCode: '',
|
||||
adHomeInfeedCode: '',
|
||||
adSidebarCode: '',
|
||||
adPostTopCode: '',
|
||||
adPostBottomCode: ''
|
||||
@@ -178,6 +179,7 @@ const form = reactive({
|
||||
customHeadCode: settings.value?.customHeadCode || '',
|
||||
customFooterCode: settings.value?.customFooterCode || '',
|
||||
adHomeFeedCode: settings.value?.adHomeFeedCode || '',
|
||||
adHomeInfeedCode: settings.value?.adHomeInfeedCode || '',
|
||||
adSidebarCode: settings.value?.adSidebarCode || '',
|
||||
adPostTopCode: settings.value?.adPostTopCode || '',
|
||||
adPostBottomCode: settings.value?.adPostBottomCode || ''
|
||||
@@ -271,6 +273,7 @@ const hasSiteCodeChanges = computed(() => editSiteCode.value && (
|
||||
*/
|
||||
const hasAdsChanges = computed(() => editAds.value && (
|
||||
form.adHomeFeedCode !== adsSnapshot.adHomeFeedCode
|
||||
|| form.adHomeInfeedCode !== adsSnapshot.adHomeInfeedCode
|
||||
|| form.adSidebarCode !== adsSnapshot.adSidebarCode
|
||||
|| form.adPostTopCode !== adsSnapshot.adPostTopCode
|
||||
|| form.adPostBottomCode !== adsSnapshot.adPostBottomCode
|
||||
@@ -1128,6 +1131,7 @@ const buildSiteSettingsPayload = () => ({
|
||||
customHeadCode: form.customHeadCode || '',
|
||||
customFooterCode: form.customFooterCode || '',
|
||||
adHomeFeedCode: form.adHomeFeedCode || '',
|
||||
adHomeInfeedCode: form.adHomeInfeedCode || '',
|
||||
adSidebarCode: form.adSidebarCode || '',
|
||||
adPostTopCode: form.adPostTopCode || '',
|
||||
adPostBottomCode: form.adPostBottomCode || ''
|
||||
@@ -1720,6 +1724,7 @@ const saveSiteCodeSection = async () => {
|
||||
*/
|
||||
const beginEditAds = () => {
|
||||
adsSnapshot.adHomeFeedCode = form.adHomeFeedCode
|
||||
adsSnapshot.adHomeInfeedCode = form.adHomeInfeedCode
|
||||
adsSnapshot.adSidebarCode = form.adSidebarCode
|
||||
adsSnapshot.adPostTopCode = form.adPostTopCode
|
||||
adsSnapshot.adPostBottomCode = form.adPostBottomCode
|
||||
@@ -1732,6 +1737,7 @@ const beginEditAds = () => {
|
||||
*/
|
||||
const cancelEditAds = () => {
|
||||
form.adHomeFeedCode = adsSnapshot.adHomeFeedCode
|
||||
form.adHomeInfeedCode = adsSnapshot.adHomeInfeedCode
|
||||
form.adSidebarCode = adsSnapshot.adSidebarCode
|
||||
form.adPostTopCode = adsSnapshot.adPostTopCode
|
||||
form.adPostBottomCode = adsSnapshot.adPostBottomCode
|
||||
@@ -1754,6 +1760,7 @@ const saveAdsSection = async () => {
|
||||
|
||||
if (ok) {
|
||||
adsSnapshot.adHomeFeedCode = form.adHomeFeedCode
|
||||
adsSnapshot.adHomeInfeedCode = form.adHomeInfeedCode
|
||||
adsSnapshot.adSidebarCode = form.adSidebarCode
|
||||
adsSnapshot.adPostTopCode = form.adPostTopCode
|
||||
adsSnapshot.adPostBottomCode = form.adPostBottomCode
|
||||
|
||||
191
pages/index.vue
191
pages/index.vue
@@ -15,6 +15,7 @@ const postFeedStyleStorageKey = 'POST_FEED_STYLE'
|
||||
|
||||
const postFeedStyleOpen = ref(false)
|
||||
const postFeedStyle = ref('compact')
|
||||
const homeInfeedAdInsertIndex = ref(-1)
|
||||
|
||||
/** @typedef {'list' | 'compact' | 'cards'} PostFeedStyle */
|
||||
|
||||
@@ -165,6 +166,7 @@ const mapLatestPost = (post) => {
|
||||
|
||||
const featuredPosts = computed(() => posts.value.filter((post) => post.isFeatured).slice(0, 6))
|
||||
const latestPosts = computed(() => posts.value.map(mapLatestPost))
|
||||
const hasHomeInfeedAd = computed(() => Boolean(String(siteSettings.value?.adHomeInfeedCode || '').trim()))
|
||||
|
||||
const featuredTrackRef = ref(null)
|
||||
/** Featured 트랙이 스크롤 시작에 붙었는지 — 이전 화살표 비활성 */
|
||||
@@ -236,6 +238,7 @@ onMounted(() => {
|
||||
|
||||
const storedStyle = localStorage.getItem(postFeedStyleStorageKey)
|
||||
postFeedStyle.value = normalizePostFeedStyle(storedStyle)
|
||||
randomizeHomeInfeedAdInsertIndex()
|
||||
|
||||
document.addEventListener('pointerdown', onDocumentPointerDown)
|
||||
})
|
||||
@@ -277,6 +280,29 @@ const scrollFeatured = (direction) => {
|
||||
behavior: 'smooth'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 메인 인피드 광고 삽입 위치를 현재 브라우저 렌더에서 한 번 정한다.
|
||||
* @returns {void}
|
||||
*/
|
||||
const randomizeHomeInfeedAdInsertIndex = () => {
|
||||
if (!hasHomeInfeedAd.value || latestPosts.value.length < 2) {
|
||||
homeInfeedAdInsertIndex.value = -1
|
||||
return
|
||||
}
|
||||
|
||||
const minIndex = 0
|
||||
const maxIndex = latestPosts.value.length - 2
|
||||
homeInfeedAdInsertIndex.value = minIndex + Math.floor(Math.random() * (maxIndex - minIndex + 1))
|
||||
}
|
||||
|
||||
watch([latestPosts, hasHomeInfeedAd], () => {
|
||||
if (!import.meta.client) {
|
||||
return
|
||||
}
|
||||
|
||||
randomizeHomeInfeedAdInsertIndex()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -465,89 +491,100 @@ const scrollFeatured = (direction) => {
|
||||
data-post-feed="latest"
|
||||
:class="getPostFeedContainerClass(postFeedStyle)"
|
||||
>
|
||||
<article
|
||||
v-for="post in latestPosts"
|
||||
<template
|
||||
v-for="(post, index) in latestPosts"
|
||||
:key="post.to"
|
||||
data-post-card
|
||||
:data-featured="post.isFeatured ? '' : undefined"
|
||||
:class="getPostFeedArticleClass(postFeedStyle)"
|
||||
>
|
||||
<PostCardMedia
|
||||
v-if="showPostFeedMedia"
|
||||
:to="post.to"
|
||||
:title="post.title"
|
||||
:featured-image="post.featuredImage"
|
||||
:link-class="isPostFeedCards ? 'post-feed__media post-feed__media--cards mb-3 block aspect-video w-full' : 'post-feed__media post-feed__media--list relative flex-1 aspect-square min-w-16 sm:aspect-video'"
|
||||
:aspect-class="isPostFeedCards ? 'aspect-video' : 'aspect-square sm:aspect-video'"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="post-feed__content relative min-w-0"
|
||||
:class="isPostFeedCards ? 'flex flex-col' : 'flex flex-[3] flex-col gap-1.5 md:flex-[4]'"
|
||||
<article
|
||||
data-post-card
|
||||
:data-featured="post.isFeatured ? '' : undefined"
|
||||
:class="getPostFeedArticleClass(postFeedStyle)"
|
||||
>
|
||||
<div class="post-feed__content-inner flex min-h-0 flex-1 flex-col gap-1">
|
||||
<h2 class="max-w-[90%] text-sm font-medium leading-tight">
|
||||
<NuxtLink :to="post.to" class="flex items-center transition-opacity duration-200 hover:opacity-75">
|
||||
<span v-if="post.isFeatured" class="post-feed__featured-icon mr-1 inline-flex h-4 w-4 items-center justify-center text-[var(--site-accent)]">
|
||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 3v7h6l-8 11v-7H5l8-11" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ post.title }}
|
||||
</NuxtLink>
|
||||
</h2>
|
||||
<PostCardMedia
|
||||
v-if="showPostFeedMedia"
|
||||
:to="post.to"
|
||||
:title="post.title"
|
||||
:featured-image="post.featuredImage"
|
||||
:link-class="isPostFeedCards ? 'post-feed__media post-feed__media--cards mb-3 block aspect-video w-full' : 'post-feed__media post-feed__media--list relative flex-1 aspect-square min-w-16 sm:aspect-video'"
|
||||
:aspect-class="isPostFeedCards ? 'aspect-video' : 'aspect-square sm:aspect-video'"
|
||||
/>
|
||||
|
||||
<p
|
||||
v-if="post.excerpt"
|
||||
class="flex-1 text-[0.8rem] leading-tight site-muted text-[#6E6661]"
|
||||
:class="getPostFeedExcerptClass(postFeedStyle)"
|
||||
>
|
||||
{{ post.excerpt }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs site-muted sm:gap-1.5">
|
||||
<time v-if="post.publishedAt" :datetime="post.publishedAtIso">{{ post.publishedAt }}</time>
|
||||
<template v-if="post.tagName">
|
||||
<span v-if="post.publishedAt" class="text-[var(--site-line)]">/</span>
|
||||
<span
|
||||
class="rounded-md px-1.5 py-px font-medium text-[var(--site-text)]"
|
||||
:style="{ backgroundColor: `${post.tagColor}1a` }"
|
||||
>
|
||||
{{ post.tagName }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-if="post.publishedAt || post.tagName" class="text-[var(--site-line)]">/</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="-mt-px">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||
</svg>
|
||||
<span>{{ post.commentCount }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="absolute top-0 right-0 flex cursor-pointer items-center gap-1 hover:opacity-75"
|
||||
:class="isPostFeedCards ? '' : 'md:top-auto md:right-0 md:bottom-0 md:invisible md:opacity-0 md:transition-[opacity,visibility] md:duration-200 md:group-hover:visible md:group-hover:opacity-100'"
|
||||
type="button"
|
||||
aria-label="Share this post"
|
||||
<div
|
||||
class="post-feed__content relative min-w-0"
|
||||
:class="isPostFeedCards ? 'flex flex-col' : 'flex flex-[3] flex-col gap-1.5 md:flex-[4]'"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="h-4 w-4"
|
||||
<div class="post-feed__content-inner flex min-h-0 flex-1 flex-col gap-1">
|
||||
<h2 class="max-w-[90%] text-sm font-medium leading-tight">
|
||||
<NuxtLink :to="post.to" class="flex items-center transition-opacity duration-200 hover:opacity-75">
|
||||
<span v-if="post.isFeatured" class="post-feed__featured-icon mr-1 inline-flex h-4 w-4 items-center justify-center text-[var(--site-accent)]">
|
||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 3v7h6l-8 11v-7H5l8-11" />
|
||||
</svg>
|
||||
</span>
|
||||
{{ post.title }}
|
||||
</NuxtLink>
|
||||
</h2>
|
||||
|
||||
<p
|
||||
v-if="post.excerpt"
|
||||
class="flex-1 text-[0.8rem] leading-tight site-muted text-[#6E6661]"
|
||||
:class="getPostFeedExcerptClass(postFeedStyle)"
|
||||
>
|
||||
{{ post.excerpt }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs site-muted sm:gap-1.5">
|
||||
<time v-if="post.publishedAt" :datetime="post.publishedAtIso">{{ post.publishedAt }}</time>
|
||||
<template v-if="post.tagName">
|
||||
<span v-if="post.publishedAt" class="text-[var(--site-line)]">/</span>
|
||||
<span
|
||||
class="rounded-md px-1.5 py-px font-medium text-[var(--site-text)]"
|
||||
:style="{ backgroundColor: `${post.tagColor}1a` }"
|
||||
>
|
||||
{{ post.tagName }}
|
||||
</span>
|
||||
</template>
|
||||
<span v-if="post.publishedAt || post.tagName" class="text-[var(--site-line)]">/</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="-mt-px">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||
</svg>
|
||||
<span>{{ post.commentCount }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="absolute top-0 right-0 flex cursor-pointer items-center gap-1 hover:opacity-75"
|
||||
:class="isPostFeedCards ? '' : 'md:top-auto md:right-0 md:bottom-0 md:invisible md:opacity-0 md:transition-[opacity,visibility] md:duration-200 md:group-hover:visible md:group-hover:opacity-100'"
|
||||
type="button"
|
||||
aria-label="Share this post"
|
||||
>
|
||||
<path d="M17 7 7 17" />
|
||||
<path d="M8 7h9v9" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="h-4 w-4"
|
||||
>
|
||||
<path d="M17 7 7 17" />
|
||||
<path d="M8 7h9v9" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<SiteAdSlot
|
||||
v-if="index === homeInfeedAdInsertIndex"
|
||||
class="home-page__infeed-ad-slot py-3"
|
||||
:class="isPostFeedCards ? 'sm:col-span-2' : ''"
|
||||
:code="siteSettings?.adHomeInfeedCode"
|
||||
location="home-infeed"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -125,6 +125,7 @@ const mapSiteSettingsRow = (row) => ({
|
||||
customHeadCode: row.custom_head_code || '',
|
||||
customFooterCode: row.custom_footer_code || '',
|
||||
adHomeFeedCode: row.ad_home_feed_code || '',
|
||||
adHomeInfeedCode: row.ad_home_infeed_code || '',
|
||||
adSidebarCode: row.ad_sidebar_code || '',
|
||||
adPostTopCode: row.ad_post_top_code || '',
|
||||
adPostBottomCode: row.ad_post_bottom_code || '',
|
||||
@@ -899,6 +900,7 @@ export const updateSiteSettings = async (input) => {
|
||||
custom_head_code,
|
||||
custom_footer_code,
|
||||
ad_home_feed_code,
|
||||
ad_home_infeed_code,
|
||||
ad_sidebar_code,
|
||||
ad_post_top_code,
|
||||
ad_post_bottom_code,
|
||||
@@ -930,6 +932,7 @@ export const updateSiteSettings = async (input) => {
|
||||
${input.customHeadCode || ''},
|
||||
${input.customFooterCode || ''},
|
||||
${input.adHomeFeedCode || ''},
|
||||
${input.adHomeInfeedCode || ''},
|
||||
${input.adSidebarCode || ''},
|
||||
${input.adPostTopCode || ''},
|
||||
${input.adPostBottomCode || ''},
|
||||
@@ -961,6 +964,7 @@ export const updateSiteSettings = async (input) => {
|
||||
custom_head_code = EXCLUDED.custom_head_code,
|
||||
custom_footer_code = EXCLUDED.custom_footer_code,
|
||||
ad_home_feed_code = EXCLUDED.ad_home_feed_code,
|
||||
ad_home_infeed_code = EXCLUDED.ad_home_infeed_code,
|
||||
ad_sidebar_code = EXCLUDED.ad_sidebar_code,
|
||||
ad_post_top_code = EXCLUDED.ad_post_top_code,
|
||||
ad_post_bottom_code = EXCLUDED.ad_post_bottom_code,
|
||||
|
||||
@@ -47,6 +47,7 @@ export const adminSiteSettingsInputSchema = z.object({
|
||||
customHeadCode: z.string().max(50000).optional().default(''),
|
||||
customFooterCode: z.string().max(50000).optional().default(''),
|
||||
adHomeFeedCode: z.string().max(50000).optional().default(''),
|
||||
adHomeInfeedCode: z.string().max(50000).optional().default(''),
|
||||
adSidebarCode: z.string().max(50000).optional().default(''),
|
||||
adPostTopCode: z.string().max(50000).optional().default(''),
|
||||
adPostBottomCode: z.string().max(50000).optional().default('')
|
||||
|
||||
@@ -38,6 +38,7 @@ export const getDefaultSiteSettings = () => {
|
||||
customHeadCode: '',
|
||||
customFooterCode: '',
|
||||
adHomeFeedCode: '',
|
||||
adHomeInfeedCode: '',
|
||||
adSidebarCode: '',
|
||||
adPostTopCode: '',
|
||||
adPostBottomCode: '',
|
||||
|
||||
Reference in New Issue
Block a user