diff --git a/app.vue b/app.vue index 562ec73..afc3a12 100644 --- a/app.vue +++ b/app.vue @@ -1,16 +1,7 @@ diff --git a/components/site/RightSidebar.vue b/components/site/RightSidebar.vue index b9b8285..b9adc6a 100644 --- a/components/site/RightSidebar.vue +++ b/components/site/RightSidebar.vue @@ -7,18 +7,7 @@ const tocNavRef = ref(null) const activeTocId = ref('') let tocScrollFrame = 0 -const { data: siteSettings } = await useFetch('/api/site-settings', { - default: () => ({ - title: 'sori.studio', - description: 'sori.studio 개인 블로그', - logoText: '井', - logoUrl: '', - socialLinks: [], - adSidebarCode: '', - adPostSidebarCode: '', - copyrightText: '©2026 sori.studio' - }) -}) +const { data: siteSettings } = await usePublicSiteSettings() const isPostDetailRoute = computed(() => route.path.startsWith('/post/')) const sidebarAdCode = computed(() => isPostDetailRoute.value ? siteSettings.value?.adPostSidebarCode : siteSettings.value?.adSidebarCode) diff --git a/components/site/SiteAnnouncementBar.vue b/components/site/SiteAnnouncementBar.vue index 108bd34..825175f 100644 --- a/components/site/SiteAnnouncementBar.vue +++ b/components/site/SiteAnnouncementBar.vue @@ -11,16 +11,7 @@ import { /** @type {number} 슬라이드 애니메이션 시간(ms) */ const SLIDE_MS = 320 -const { data: siteSettings } = await useFetch('/api/site-settings', { - default: () => ({ - announcementEnabled: false, - announcementText: '', - announcementUrl: '', - announcementBackgroundColor: '#15171a', - announcementAlignment: 'center', - updatedAt: null - }) -}) +const { data: siteSettings } = await usePublicSiteSettings() /** DOM에 바를 둘지(애니메이션 종료 전까지 유지) */ const inDom = ref(false) diff --git a/components/site/SiteHeader.vue b/components/site/SiteHeader.vue index 7eb1f77..be5c8b8 100644 --- a/components/site/SiteHeader.vue +++ b/components/site/SiteHeader.vue @@ -6,11 +6,7 @@ const userMenuToggleRef = ref(null) const searchOpen = ref(false) const member = ref(null) -const { data: siteSettings } = await useFetch('/api/site-settings', { - default: () => ({ - title: 'sori.studio' - }) -}) +const { data: siteSettings } = await usePublicSiteSettings() /** * 사용자 메뉴를 닫는다. diff --git a/composables/usePublicSiteSettings.js b/composables/usePublicSiteSettings.js new file mode 100644 index 0000000..66e425a --- /dev/null +++ b/composables/usePublicSiteSettings.js @@ -0,0 +1,34 @@ +import { DEFAULT_BRAND_COLOR } from '~/lib/brand-color.js' + +/** + * 공개 사이트 설정 기본값을 만든다. + * @returns {Object} 공개 사이트 설정 기본값 + */ +export const createDefaultPublicSiteSettings = () => ({ + title: 'sori.studio', + description: 'sori.studio 개인 블로그', + faviconUrl: '', + logoUrl: '', + logoText: '井', + brandColor: DEFAULT_BRAND_COLOR, + socialLinks: [], + adSidebarCode: '', + adPostSidebarCode: '', + copyrightText: '©2026 sori.studio', + announcementEnabled: false, + announcementText: '', + announcementUrl: '', + announcementBackgroundColor: '#15171a', + announcementAlignment: 'center', + updatedAt: null, + signupBlockedUsernames: [] +}) + +/** + * 공개 사이트 설정을 공통 key와 옵션으로 조회한다. + * @returns {Promise>} 사이트 설정 async data + */ +export const usePublicSiteSettings = () => useFetch('/api/site-settings', { + key: 'site-settings-public', + default: createDefaultPublicSiteSettings +}) diff --git a/docs/map.md b/docs/map.md index a613548..8da5604 100644 --- a/docs/map.md +++ b/docs/map.md @@ -26,6 +26,7 @@ | composables/useAdminFeaturedImagePicker.js | 관리자 게시물 폼 대표 이미지 선택 모달의 검색·페이지네이션·업로드·선택 상태 | | composables/useAdminPostPublishState.js | 관리자 게시물 폼 발행·예약·발행취소 상태와 날짜 입력 상태 | | composables/useAdminMediaThumbnailRegeneration.js | 관리자 미디어 화면 대표 이미지·페이지 커버·본문 이미지 재생성 가능 여부와 API 호출 상태 | +| composables/usePublicSiteSettings.js | 공개 화면의 `/api/site-settings` 조회 key(`site-settings-public`)와 기본 설정값 통일 | ## 공유 라이브러리(서버·클라이언트 공통) @@ -264,7 +265,7 @@ | components/content/ProseSignup.vue | 본문 회원가입·뉴스레터 CTA 카드 | | components/content/ProseHeaderCard.vue | 헤더 카드 | | components/content/ProseEmbed.vue | 상단 외부 여백 없이 하단 여백만 두는 YouTube 영상 iframe, X/Twitter 소셜 iframe, Mastodon `/embed` iframe, 제한된 사용자 iframe HTML, 기타 `http(s)` 외부 링크 | -| components/content/ProseHtml.vue | `:::html` 본문 HTML 렌더링, 표 기본 스타일과 허용 목록 sanitizer 적용 | +| components/content/ProseHtml.vue | `:::html` 본문 HTML 렌더링, 표 기본 스타일과 허용 목록 sanitizer 적용, 서버 전용 `sanitize-html`과 브라우저 DOM sanitizer 분리 | ## 관리자 페이지 diff --git a/docs/spec.md b/docs/spec.md index 628616c..7eb723f 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -898,6 +898,7 @@ components/content/ - 공개 헤더와 오른쪽 사이드바는 공개 사이트 설정 API 값을 사용한다. - 공개 헤더와 오른쪽 사이드바는 `logo_url`이 있으면 이미지 로고를 표시하고, 없으면 `logo_text` fallback을 쓴다. 오른쪽 사이드바 로고는 48px 정사각형으로 고정해 옆 텍스트에 밀려 줄어들지 않게 한다. `favicon_url`은 head의 icon 링크로 연결한다. - DB 연결이 없는 환경에서는 환경 변수와 기본값 기반 설정을 사용한다. +- 공개 앱·헤더·오른쪽 사이드바·어나운스 바·홈·목록·태그·상세·회원가입 화면의 공개 사이트 설정 조회는 `usePublicSiteSettings` composable의 `site-settings-public` key와 공통 기본값을 공유한다. ### 메뉴/네비게이션 diff --git a/docs/update.md b/docs/update.md index a83aaac..fae4c5b 100644 --- a/docs/update.md +++ b/docs/update.md @@ -1,5 +1,11 @@ # 업데이트 이력 +## v1.11.57 + +- 공개 사이트 설정 조회를 `usePublicSiteSettings` composable로 통합해 Nuxt `site-settings-public` async data 옵션 충돌 경고를 제거. +- HTML 블록 렌더러의 `sanitize-html` 정적 import를 서버 전용 동적 import로 바꿔 브라우저 번들 유입을 차단. +- `/posts?page=2` SSR 응답과 게시물 페이지 API 응답, 프로덕션 빌드에서 페이지네이션 회귀를 확인. + ## v1.11.56 - 공개 Markdown 렌더러의 라이트박스 이미지 목록과 갤러리 행 분할 계산을 `lib/content-gallery-images.js`로 분리. diff --git a/package-lock.json b/package-lock.json index 2a979f9..45e2231 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sori.studio", - "version": "1.11.56", + "version": "1.11.57", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sori.studio", - "version": "1.11.56", + "version": "1.11.57", "hasInstallScript": true, "dependencies": { "@nuxtjs/tailwindcss": "^6.14.0", diff --git a/package.json b/package.json index 0bf1948..62452e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sori.studio", - "version": "1.11.56", + "version": "1.11.57", "private": true, "type": "module", "imports": { diff --git a/pages/index.vue b/pages/index.vue index fb26116..f2942a1 100644 --- a/pages/index.vue +++ b/pages/index.vue @@ -6,10 +6,7 @@ import { normalizeDisplayCount } from '~/lib/display-count-settings.js' -const { data: siteSettings } = await useFetch('/api/site-settings', { - key: 'site-settings-public', - default: () => ({}) -}) +const { data: siteSettings } = await usePublicSiteSettings() const homeLatestPostLimit = computed(() => normalizeDisplayCount(siteSettings.value?.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT)) const featuredPostLimit = computed(() => normalizeDisplayCount(siteSettings.value?.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT)) diff --git a/pages/post/[slug].vue b/pages/post/[slug].vue index 517b623..d803c97 100644 --- a/pages/post/[slug].vue +++ b/pages/post/[slug].vue @@ -16,10 +16,7 @@ const { data: post } = await useFetch(() => `/api/posts/${slug.value}`) const { data: tags } = useLazyFetch('/api/tags?tagType=all', { default: () => [] }) -const { data: siteSettings } = useLazyFetch('/api/site-settings', { - key: 'site-settings-public', - default: () => ({}) -}) +const { data: siteSettings } = await usePublicSiteSettings() const postToc = useState('post-detail-toc', () => []) if (!post.value) { diff --git a/pages/posts/index.vue b/pages/posts/index.vue index f6a4e9a..924958d 100644 --- a/pages/posts/index.vue +++ b/pages/posts/index.vue @@ -7,9 +7,7 @@ import { const route = useRoute() -const { data: siteSettings } = await useFetch('/api/site-settings', { - default: () => ({}) -}) +const { data: siteSettings } = await usePublicSiteSettings() const { data: tags } = await useFetch('/api/tags?tagType=all', { default: () => [] diff --git a/pages/signup.vue b/pages/signup.vue index d35d7ce..a015da7 100644 --- a/pages/signup.vue +++ b/pages/signup.vue @@ -14,13 +14,7 @@ const signupCompleted = ref(false) const createdAdmin = ref(false) const statusMessage = ref('') const submitErrorMessage = ref('') -const { data: siteSettings } = await useFetch('/api/site-settings', { - default: () => ({ - title: 'AFFiNE', - description: 'Configure your Self Host AFFiNE with a few simple settings.', - signupBlockedUsernames: [] - }) -}) +const { data: siteSettings } = await usePublicSiteSettings() const { data: bootstrapStatus } = await useFetch('/api/auth/bootstrap-status', { default: () => ({ hasUsers: true, diff --git a/pages/tag/[slug].vue b/pages/tag/[slug].vue index 234838e..feaa575 100644 --- a/pages/tag/[slug].vue +++ b/pages/tag/[slug].vue @@ -28,9 +28,7 @@ if (!tag.value) { }) } -const { data: siteSettings } = await useFetch('/api/site-settings', { - default: () => ({}) -}) +const { data: siteSettings } = await usePublicSiteSettings() const currentPage = computed(() => normalizeListPage(route.query.page)) const tagPostListPageSize = computed(() => normalizeDisplayCount(siteSettings.value?.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE))