메뉴 관리 개편, 추천 사이트·1뎁스 네비, 우측 Recommended 연동(v1.1.13)
상단 네비는 평면 테이블·드래그로 편집하고 한 단계 하위만 허용한다. 추천 사이트 탭·location recommended·공개 API와 우측 사이드 카드·파비콘 URL 유틸을 추가한다. 문서·배포 마이그레이션 안내·관리자 레이아웃·설정 화면 등 누적 변경을 반영한다.
This commit is contained in:
@@ -15,7 +15,8 @@ const { data: tags } = await useFetch('/api/tags', {
|
||||
const { data: navigation } = await useFetch('/api/navigation', {
|
||||
default: () => ({
|
||||
primary: [],
|
||||
footer: []
|
||||
footer: [],
|
||||
recommended: []
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup>
|
||||
import { getExternalFaviconUrl } from '~/lib/external-favicon-url.js'
|
||||
|
||||
const followLinks = [
|
||||
{ id: 'facebook', label: 'Facebook', href: 'https://facebook.com', icon: 'facebook' },
|
||||
{ id: 'x', label: 'X', href: 'https://x.com', icon: 'x' },
|
||||
@@ -18,6 +20,33 @@ const { data: siteSettings } = await useFetch('/api/site-settings', {
|
||||
})
|
||||
})
|
||||
|
||||
const { data: navigation } = await useFetch('/api/navigation', {
|
||||
default: () => ({
|
||||
primary: [],
|
||||
footer: [],
|
||||
recommended: []
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 공개 추천 사이트 목록(비가시 제외)
|
||||
* @returns {Array<{ id: string, label: string, url: string }>}
|
||||
*/
|
||||
const recommendedSites = computed(() => {
|
||||
const list = navigation.value?.recommended
|
||||
if (!Array.isArray(list)) {
|
||||
return []
|
||||
}
|
||||
return list.filter((x) => x?.isVisible !== false)
|
||||
})
|
||||
|
||||
/**
|
||||
* 새 탭으로 열 외부 URL인지
|
||||
* @param {string} url - 링크
|
||||
* @returns {boolean}
|
||||
*/
|
||||
const isExternalNavUrl = (url) => /^https?:\/\//i.test(String(url || '').trim())
|
||||
|
||||
/** 소개 영역 공개 여부 */
|
||||
const showAboutSection = false
|
||||
</script>
|
||||
@@ -159,24 +188,44 @@ const showAboutSection = false
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-0">
|
||||
<div
|
||||
v-if="recommendedSites.length"
|
||||
class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-0"
|
||||
>
|
||||
<div class="right-sidebar__row flex items-center justify-between">
|
||||
<p class="right-sidebar__eyebrow text-xs font-semibold uppercase site-muted">
|
||||
Recommended
|
||||
</p>
|
||||
<span>↗</span>
|
||||
</div>
|
||||
<div class="right-sidebar__links mt-4 grid gap-3 text-sm">
|
||||
<NuxtLink class="right-sidebar__link site-interactive font-semibold" to="/post/hello-sori-studio">
|
||||
sori.studio 첫 글과 방향
|
||||
</NuxtLink>
|
||||
<NuxtLink class="right-sidebar__link site-interactive font-semibold" to="/pages/projects">
|
||||
Projects and services
|
||||
</NuxtLink>
|
||||
<NuxtLink class="right-sidebar__link site-interactive font-semibold" to="/pages/links">
|
||||
Links and portal
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<ul class="right-sidebar__recommended-list mt-4 list-none flex flex-col gap-2.5 p-0">
|
||||
<li v-for="item in recommendedSites" :key="item.id">
|
||||
<a
|
||||
class="right-sidebar__recommended-card site-interactive flex items-center gap-3 rounded-xl border border-[var(--site-line)] bg-[var(--site-panel)] px-3 py-2.5 transition-colors hover:border-[var(--site-accent)]"
|
||||
:href="item.url"
|
||||
:target="isExternalNavUrl(item.url) ? '_blank' : undefined"
|
||||
:rel="isExternalNavUrl(item.url) ? 'nofollow noopener noreferrer' : undefined"
|
||||
>
|
||||
<span class="right-sidebar__recommended-icon grid h-9 w-9 shrink-0 place-items-center overflow-hidden rounded-lg bg-[var(--site-paper)] text-xs font-bold text-[var(--site-text)] ring-1 ring-[var(--site-line)]">
|
||||
<img
|
||||
v-if="getExternalFaviconUrl(item.url)"
|
||||
class="h-full w-full object-cover"
|
||||
:src="getExternalFaviconUrl(item.url, 64)"
|
||||
width="36"
|
||||
height="36"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
referrerpolicy="no-referrer"
|
||||
>
|
||||
<span v-else class="px-1 text-center leading-none">{{ (item.label || '?').slice(0, 1) }}</span>
|
||||
</span>
|
||||
<span class="min-w-0 flex-1">
|
||||
<span class="block truncate text-sm font-semibold text-[var(--site-text)]">{{ item.label }}</span>
|
||||
<span class="mt-0.5 block truncate font-mono text-[11px] site-muted">{{ item.url }}</span>
|
||||
</span>
|
||||
<span class="shrink-0 text-xs site-muted" aria-hidden="true">↗</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="showAboutSection" class="right-sidebar__block site-sidebar-section py-5 pl-5 pr-0">
|
||||
|
||||
@@ -15,9 +15,9 @@ const toggleBranch = inject('sidebarPrimaryNavToggle')
|
||||
const navBarBeforeBase =
|
||||
"before:pointer-events-none before:content-[''] before:h-4 before:w-1 before:flex-none before:rounded-sm before:rounded-l-none before:transition-[width,height,border-radius,background-color] before:duration-200 hover:px-3 hover:before:h-2 hover:before:w-2 hover:before:rounded-full"
|
||||
|
||||
/** 비활성 경로: 기본 회색 막대, 호버 시 원형·믹스 색 */
|
||||
/** 비활성 경로: 테두리 톤에 가깝게 밝게 섞인 막대, 호버 시 원형·믹스 색 */
|
||||
const navBarBeforeInactive =
|
||||
`${navBarBeforeBase} before:bg-[var(--site-line)] hover:before:bg-[color:color-mix(in_srgb,var(--site-text)_28%,var(--site-line))]`
|
||||
`${navBarBeforeBase} before:bg-[color:color-mix(in_srgb,var(--site-line)_88%,var(--site-panel)_12%)] hover:before:bg-[color:color-mix(in_srgb,var(--site-text)_28%,var(--site-line))]`
|
||||
|
||||
/** 현재 페이지와 일치하는 내부 링크: 브랜드(`--site-accent`) 막대·호버 원형 */
|
||||
const navBarBeforeActive =
|
||||
@@ -122,7 +122,7 @@ const navLinkClass = (url) => {
|
||||
:class="isExpanded(node.id) ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'"
|
||||
>
|
||||
<div class="sidebar-primary-nav-list__sub-clip min-h-0 overflow-hidden">
|
||||
<ul class="sidebar-primary-nav-list__sub ml-0 border-l border-[var(--site-line)] pl-2 pt-0">
|
||||
<ul class="sidebar-primary-nav-list__sub ml-0 mt-2 pl-3 pt-0">
|
||||
<SidebarPrimaryNavList :nodes="node.children" />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user