공개 페이지 전환 경고 정리
This commit is contained in:
11
app.vue
11
app.vue
@@ -1,16 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { DEFAULT_BRAND_COLOR, normalizeBrandColor } from './lib/brand-color.js'
|
import { DEFAULT_BRAND_COLOR, normalizeBrandColor } from './lib/brand-color.js'
|
||||||
|
|
||||||
const { data: appSiteSettings } = await useFetch('/api/site-settings', {
|
const { data: appSiteSettings } = await usePublicSiteSettings()
|
||||||
key: 'site-settings-public',
|
|
||||||
default: () => ({
|
|
||||||
title: 'sori.studio',
|
|
||||||
faviconUrl: '',
|
|
||||||
logoUrl: '',
|
|
||||||
logoText: '井',
|
|
||||||
brandColor: DEFAULT_BRAND_COLOR
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const siteAccentColor = computed(() => normalizeBrandColor(appSiteSettings.value?.brandColor || DEFAULT_BRAND_COLOR))
|
const siteAccentColor = computed(() => normalizeBrandColor(appSiteSettings.value?.brandColor || DEFAULT_BRAND_COLOR))
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { sanitizeHtmlBlock } from '../../lib/markdown-html-block.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
html: {
|
html: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -8,6 +6,203 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const allowedHtmlBlockTags = new Set([
|
||||||
|
'a',
|
||||||
|
'abbr',
|
||||||
|
'b',
|
||||||
|
'blockquote',
|
||||||
|
'br',
|
||||||
|
'caption',
|
||||||
|
'code',
|
||||||
|
'col',
|
||||||
|
'colgroup',
|
||||||
|
'dd',
|
||||||
|
'details',
|
||||||
|
'div',
|
||||||
|
'dl',
|
||||||
|
'dt',
|
||||||
|
'em',
|
||||||
|
'figcaption',
|
||||||
|
'figure',
|
||||||
|
'h2',
|
||||||
|
'h3',
|
||||||
|
'h4',
|
||||||
|
'h5',
|
||||||
|
'h6',
|
||||||
|
'hr',
|
||||||
|
'i',
|
||||||
|
'img',
|
||||||
|
'li',
|
||||||
|
'mark',
|
||||||
|
'ol',
|
||||||
|
'p',
|
||||||
|
'pre',
|
||||||
|
's',
|
||||||
|
'small',
|
||||||
|
'span',
|
||||||
|
'strong',
|
||||||
|
'sub',
|
||||||
|
'summary',
|
||||||
|
'sup',
|
||||||
|
'table',
|
||||||
|
'tbody',
|
||||||
|
'td',
|
||||||
|
'tfoot',
|
||||||
|
'th',
|
||||||
|
'thead',
|
||||||
|
'time',
|
||||||
|
'tr',
|
||||||
|
'u',
|
||||||
|
'ul'
|
||||||
|
])
|
||||||
|
const globalAllowedAttributes = new Set(['class', 'title', 'aria-label', 'aria-hidden'])
|
||||||
|
const allowedAttributesByTag = {
|
||||||
|
a: new Set(['href', 'target', 'rel']),
|
||||||
|
col: new Set(['span', 'width']),
|
||||||
|
img: new Set(['src', 'alt', 'width', 'height', 'loading']),
|
||||||
|
li: new Set(['value']),
|
||||||
|
ol: new Set(['start', 'type']),
|
||||||
|
td: new Set(['colspan', 'rowspan', 'headers']),
|
||||||
|
th: new Set(['colspan', 'rowspan', 'headers', 'scope']),
|
||||||
|
time: new Set(['datetime'])
|
||||||
|
}
|
||||||
|
const urlAttributeNames = new Set(['href', 'src'])
|
||||||
|
const allowedUrlSchemes = new Set(['http:', 'https:', 'mailto:', 'tel:'])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML 텍스트를 escape한다.
|
||||||
|
* @param {unknown} value - 원본 값
|
||||||
|
* @returns {string} HTML escape 문자열
|
||||||
|
*/
|
||||||
|
const escapeHtmlText = (value) => String(value || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 브라우저에서 URL 속성 값을 허용할지 확인한다.
|
||||||
|
* @param {string} value - URL 값
|
||||||
|
* @returns {boolean} 허용 여부
|
||||||
|
*/
|
||||||
|
const isAllowedBrowserUrl = (value) => {
|
||||||
|
const trimmedValue = String(value || '').trim()
|
||||||
|
|
||||||
|
if (!trimmedValue || trimmedValue.startsWith('//')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedValue.startsWith('/') || trimmedValue.startsWith('#')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return allowedUrlSchemes.has(new URL(trimmedValue, window.location.origin).protocol)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 브라우저에서 HTML 노드를 허용 목록 기준으로 정리한다.
|
||||||
|
* @param {Node} node - 정리할 노드
|
||||||
|
* @returns {Node|null} 정리된 노드
|
||||||
|
*/
|
||||||
|
const sanitizeBrowserNode = (node) => {
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
return document.createTextNode(node.textContent || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = /** @type {Element} */ (node)
|
||||||
|
const tagName = element.tagName.toLowerCase()
|
||||||
|
|
||||||
|
if (!allowedHtmlBlockTags.has(tagName)) {
|
||||||
|
const fragment = document.createDocumentFragment()
|
||||||
|
Array.from(element.childNodes).forEach((childNode) => {
|
||||||
|
const sanitizedChild = sanitizeBrowserNode(childNode)
|
||||||
|
if (sanitizedChild) {
|
||||||
|
fragment.appendChild(sanitizedChild)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedElement = document.createElement(tagName)
|
||||||
|
const tagAllowedAttributes = allowedAttributesByTag[tagName] || new Set()
|
||||||
|
|
||||||
|
Array.from(element.attributes).forEach((attribute) => {
|
||||||
|
const attributeName = attribute.name.toLowerCase()
|
||||||
|
|
||||||
|
if (!globalAllowedAttributes.has(attributeName) && !tagAllowedAttributes.has(attributeName)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (urlAttributeNames.has(attributeName) && !isAllowedBrowserUrl(attribute.value)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizedElement.setAttribute(attributeName, attribute.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (tagName === 'a' && sanitizedElement.getAttribute('target') === '_blank') {
|
||||||
|
const relValues = new Set(String(sanitizedElement.getAttribute('rel') || '').split(/\s+/).filter(Boolean))
|
||||||
|
relValues.add('noopener')
|
||||||
|
relValues.add('noreferrer')
|
||||||
|
sanitizedElement.setAttribute('rel', [...relValues].join(' '))
|
||||||
|
}
|
||||||
|
|
||||||
|
Array.from(element.childNodes).forEach((childNode) => {
|
||||||
|
const sanitizedChild = sanitizeBrowserNode(childNode)
|
||||||
|
if (sanitizedChild) {
|
||||||
|
sanitizedElement.appendChild(sanitizedChild)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return sanitizedElement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 브라우저에서 HTML 블록을 정리한다.
|
||||||
|
* @param {unknown} value - 원본 HTML
|
||||||
|
* @returns {string} 정리된 HTML
|
||||||
|
*/
|
||||||
|
const sanitizeHtmlBlockInBrowser = (value) => {
|
||||||
|
const html = String(value || '').trim()
|
||||||
|
|
||||||
|
if (!html) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!import.meta.client || typeof DOMParser === 'undefined') {
|
||||||
|
return escapeHtmlText(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
const parser = new DOMParser()
|
||||||
|
const documentFragment = document.createDocumentFragment()
|
||||||
|
const parsedDocument = parser.parseFromString(`<template>${html}</template>`, 'text/html')
|
||||||
|
const template = parsedDocument.querySelector('template')
|
||||||
|
|
||||||
|
Array.from(template?.content.childNodes || []).forEach((node) => {
|
||||||
|
const sanitizedNode = sanitizeBrowserNode(node)
|
||||||
|
if (sanitizedNode) {
|
||||||
|
documentFragment.appendChild(sanitizedNode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const container = document.createElement('div')
|
||||||
|
container.appendChild(documentFragment)
|
||||||
|
return container.innerHTML.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizeHtmlBlock = import.meta.server
|
||||||
|
? (await import('../../lib/markdown-html-block.js')).sanitizeHtmlBlock
|
||||||
|
: sanitizeHtmlBlockInBrowser
|
||||||
|
|
||||||
const sanitizedHtml = computed(() => sanitizeHtmlBlock(props.html))
|
const sanitizedHtml = computed(() => sanitizeHtmlBlock(props.html))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,7 @@ const tocNavRef = ref(null)
|
|||||||
const activeTocId = ref('')
|
const activeTocId = ref('')
|
||||||
let tocScrollFrame = 0
|
let tocScrollFrame = 0
|
||||||
|
|
||||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
const { data: siteSettings } = await usePublicSiteSettings()
|
||||||
default: () => ({
|
|
||||||
title: 'sori.studio',
|
|
||||||
description: 'sori.studio 개인 블로그',
|
|
||||||
logoText: '井',
|
|
||||||
logoUrl: '',
|
|
||||||
socialLinks: [],
|
|
||||||
adSidebarCode: '',
|
|
||||||
adPostSidebarCode: '',
|
|
||||||
copyrightText: '©2026 sori.studio'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const isPostDetailRoute = computed(() => route.path.startsWith('/post/'))
|
const isPostDetailRoute = computed(() => route.path.startsWith('/post/'))
|
||||||
const sidebarAdCode = computed(() => isPostDetailRoute.value ? siteSettings.value?.adPostSidebarCode : siteSettings.value?.adSidebarCode)
|
const sidebarAdCode = computed(() => isPostDetailRoute.value ? siteSettings.value?.adPostSidebarCode : siteSettings.value?.adSidebarCode)
|
||||||
|
|||||||
@@ -11,16 +11,7 @@ import {
|
|||||||
/** @type {number} 슬라이드 애니메이션 시간(ms) */
|
/** @type {number} 슬라이드 애니메이션 시간(ms) */
|
||||||
const SLIDE_MS = 320
|
const SLIDE_MS = 320
|
||||||
|
|
||||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
const { data: siteSettings } = await usePublicSiteSettings()
|
||||||
default: () => ({
|
|
||||||
announcementEnabled: false,
|
|
||||||
announcementText: '',
|
|
||||||
announcementUrl: '',
|
|
||||||
announcementBackgroundColor: '#15171a',
|
|
||||||
announcementAlignment: 'center',
|
|
||||||
updatedAt: null
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/** DOM에 바를 둘지(애니메이션 종료 전까지 유지) */
|
/** DOM에 바를 둘지(애니메이션 종료 전까지 유지) */
|
||||||
const inDom = ref(false)
|
const inDom = ref(false)
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ const userMenuToggleRef = ref(null)
|
|||||||
const searchOpen = ref(false)
|
const searchOpen = ref(false)
|
||||||
const member = ref(null)
|
const member = ref(null)
|
||||||
|
|
||||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
const { data: siteSettings } = await usePublicSiteSettings()
|
||||||
default: () => ({
|
|
||||||
title: 'sori.studio'
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 메뉴를 닫는다.
|
* 사용자 메뉴를 닫는다.
|
||||||
|
|||||||
34
composables/usePublicSiteSettings.js
Normal file
34
composables/usePublicSiteSettings.js
Normal file
@@ -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<import('nuxt/app').AsyncData<Object, Error | null>>} 사이트 설정 async data
|
||||||
|
*/
|
||||||
|
export const usePublicSiteSettings = () => useFetch('/api/site-settings', {
|
||||||
|
key: 'site-settings-public',
|
||||||
|
default: createDefaultPublicSiteSettings
|
||||||
|
})
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
| composables/useAdminFeaturedImagePicker.js | 관리자 게시물 폼 대표 이미지 선택 모달의 검색·페이지네이션·업로드·선택 상태 |
|
| composables/useAdminFeaturedImagePicker.js | 관리자 게시물 폼 대표 이미지 선택 모달의 검색·페이지네이션·업로드·선택 상태 |
|
||||||
| composables/useAdminPostPublishState.js | 관리자 게시물 폼 발행·예약·발행취소 상태와 날짜 입력 상태 |
|
| composables/useAdminPostPublishState.js | 관리자 게시물 폼 발행·예약·발행취소 상태와 날짜 입력 상태 |
|
||||||
| composables/useAdminMediaThumbnailRegeneration.js | 관리자 미디어 화면 대표 이미지·페이지 커버·본문 이미지 재생성 가능 여부와 API 호출 상태 |
|
| 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/ProseSignup.vue | 본문 회원가입·뉴스레터 CTA 카드 |
|
||||||
| components/content/ProseHeaderCard.vue | 헤더 카드 |
|
| components/content/ProseHeaderCard.vue | 헤더 카드 |
|
||||||
| components/content/ProseEmbed.vue | 상단 외부 여백 없이 하단 여백만 두는 YouTube 영상 iframe, X/Twitter 소셜 iframe, Mastodon `/embed` iframe, 제한된 사용자 iframe HTML, 기타 `http(s)` 외부 링크 |
|
| 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 분리 |
|
||||||
|
|
||||||
## 관리자 페이지
|
## 관리자 페이지
|
||||||
|
|
||||||
|
|||||||
@@ -898,6 +898,7 @@ components/content/
|
|||||||
- 공개 헤더와 오른쪽 사이드바는 공개 사이트 설정 API 값을 사용한다.
|
- 공개 헤더와 오른쪽 사이드바는 공개 사이트 설정 API 값을 사용한다.
|
||||||
- 공개 헤더와 오른쪽 사이드바는 `logo_url`이 있으면 이미지 로고를 표시하고, 없으면 `logo_text` fallback을 쓴다. 오른쪽 사이드바 로고는 48px 정사각형으로 고정해 옆 텍스트에 밀려 줄어들지 않게 한다. `favicon_url`은 head의 icon 링크로 연결한다.
|
- 공개 헤더와 오른쪽 사이드바는 `logo_url`이 있으면 이미지 로고를 표시하고, 없으면 `logo_text` fallback을 쓴다. 오른쪽 사이드바 로고는 48px 정사각형으로 고정해 옆 텍스트에 밀려 줄어들지 않게 한다. `favicon_url`은 head의 icon 링크로 연결한다.
|
||||||
- DB 연결이 없는 환경에서는 환경 변수와 기본값 기반 설정을 사용한다.
|
- DB 연결이 없는 환경에서는 환경 변수와 기본값 기반 설정을 사용한다.
|
||||||
|
- 공개 앱·헤더·오른쪽 사이드바·어나운스 바·홈·목록·태그·상세·회원가입 화면의 공개 사이트 설정 조회는 `usePublicSiteSettings` composable의 `site-settings-public` key와 공통 기본값을 공유한다.
|
||||||
|
|
||||||
### 메뉴/네비게이션
|
### 메뉴/네비게이션
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
## v1.11.56
|
||||||
|
|
||||||
- 공개 Markdown 렌더러의 라이트박스 이미지 목록과 갤러리 행 분할 계산을 `lib/content-gallery-images.js`로 분리.
|
- 공개 Markdown 렌더러의 라이트박스 이미지 목록과 갤러리 행 분할 계산을 `lib/content-gallery-images.js`로 분리.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.56",
|
"version": "1.11.57",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.56",
|
"version": "1.11.57",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/tailwindcss": "^6.14.0",
|
"@nuxtjs/tailwindcss": "^6.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sori.studio",
|
"name": "sori.studio",
|
||||||
"version": "1.11.56",
|
"version": "1.11.57",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import {
|
|||||||
normalizeDisplayCount
|
normalizeDisplayCount
|
||||||
} from '~/lib/display-count-settings.js'
|
} from '~/lib/display-count-settings.js'
|
||||||
|
|
||||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
const { data: siteSettings } = await usePublicSiteSettings()
|
||||||
key: 'site-settings-public',
|
|
||||||
default: () => ({})
|
|
||||||
})
|
|
||||||
|
|
||||||
const homeLatestPostLimit = computed(() => normalizeDisplayCount(siteSettings.value?.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT))
|
const homeLatestPostLimit = computed(() => normalizeDisplayCount(siteSettings.value?.homeLatestPostLimit, DEFAULT_HOME_LATEST_POST_LIMIT))
|
||||||
const featuredPostLimit = computed(() => normalizeDisplayCount(siteSettings.value?.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT))
|
const featuredPostLimit = computed(() => normalizeDisplayCount(siteSettings.value?.featuredPostLimit, DEFAULT_FEATURED_POST_LIMIT))
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ const { data: post } = await useFetch(() => `/api/posts/${slug.value}`)
|
|||||||
const { data: tags } = useLazyFetch('/api/tags?tagType=all', {
|
const { data: tags } = useLazyFetch('/api/tags?tagType=all', {
|
||||||
default: () => []
|
default: () => []
|
||||||
})
|
})
|
||||||
const { data: siteSettings } = useLazyFetch('/api/site-settings', {
|
const { data: siteSettings } = await usePublicSiteSettings()
|
||||||
key: 'site-settings-public',
|
|
||||||
default: () => ({})
|
|
||||||
})
|
|
||||||
const postToc = useState('post-detail-toc', () => [])
|
const postToc = useState('post-detail-toc', () => [])
|
||||||
|
|
||||||
if (!post.value) {
|
if (!post.value) {
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import {
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
const { data: siteSettings } = await usePublicSiteSettings()
|
||||||
default: () => ({})
|
|
||||||
})
|
|
||||||
|
|
||||||
const { data: tags } = await useFetch('/api/tags?tagType=all', {
|
const { data: tags } = await useFetch('/api/tags?tagType=all', {
|
||||||
default: () => []
|
default: () => []
|
||||||
|
|||||||
@@ -14,13 +14,7 @@ const signupCompleted = ref(false)
|
|||||||
const createdAdmin = ref(false)
|
const createdAdmin = ref(false)
|
||||||
const statusMessage = ref('')
|
const statusMessage = ref('')
|
||||||
const submitErrorMessage = ref('')
|
const submitErrorMessage = ref('')
|
||||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
const { data: siteSettings } = await usePublicSiteSettings()
|
||||||
default: () => ({
|
|
||||||
title: 'AFFiNE',
|
|
||||||
description: 'Configure your Self Host AFFiNE with a few simple settings.',
|
|
||||||
signupBlockedUsernames: []
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const { data: bootstrapStatus } = await useFetch('/api/auth/bootstrap-status', {
|
const { data: bootstrapStatus } = await useFetch('/api/auth/bootstrap-status', {
|
||||||
default: () => ({
|
default: () => ({
|
||||||
hasUsers: true,
|
hasUsers: true,
|
||||||
|
|||||||
@@ -28,9 +28,7 @@ if (!tag.value) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: siteSettings } = await useFetch('/api/site-settings', {
|
const { data: siteSettings } = await usePublicSiteSettings()
|
||||||
default: () => ({})
|
|
||||||
})
|
|
||||||
|
|
||||||
const currentPage = computed(() => normalizeListPage(route.query.page))
|
const currentPage = computed(() => normalizeListPage(route.query.page))
|
||||||
const tagPostListPageSize = computed(() => normalizeDisplayCount(siteSettings.value?.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE))
|
const tagPostListPageSize = computed(() => normalizeDisplayCount(siteSettings.value?.tagPostListPageSize, DEFAULT_TAG_POST_LIST_PAGE_SIZE))
|
||||||
|
|||||||
Reference in New Issue
Block a user