홈 중앙 메인 영역을 Thred 간격 기준으로 재구성.
Hero/Featured/Latest 섹션을 내부 컨테이너 기준 보더 정렬로 바꾸고, Latest 목록 카드를 원본 패턴의 리스트 메타 구조로 정리해 중앙 메인 영역의 시각 리듬을 맞췄다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -75,7 +75,7 @@
|
||||
|
||||
| 파일 | 화면 |
|
||||
|------|------|
|
||||
| pages/index.vue | 홈 |
|
||||
| pages/index.vue | 홈, 중앙 Hero/Featured/Latest 섹션의 내부 컨테이너 보더 정렬과 리스트형 latest 카드 |
|
||||
| pages/posts/index.vue | 게시물 전체 목록 |
|
||||
| pages/posts/[slug].vue | `/post/:slug` 리다이렉트 |
|
||||
| pages/post/[slug].vue | 블로그 글 상세, 게시물 SEO/OG 메타 출력 |
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
- 태그 상세 페이지 게시물 메타 영역에 featured 강조, 태그 컬러 배지, 구분자 스타일을 원본 패턴에 맞춰 보정.
|
||||
- 태그 상세 페이지에서 복수 태그 글은 첫 번째 태그만 배지로 표시하고, 배지와 `/` 구분자가 겹치지 않도록 메타 구조 수정.
|
||||
- 오른쪽 사이드바 Follow 영역을 원본 패턴의 소셜 아이콘 링크 행으로 교체.
|
||||
- 홈 중앙 메인 영역을 원본 Thred 구조에 맞춰 Hero/Featured/Latest 섹션 간격과 내부 보더 정렬 기준으로 재구성.
|
||||
- 기술 명세 현재 버전을 v0.0.45로 갱신.
|
||||
|
||||
## v0.0.44
|
||||
|
||||
229
pages/index.vue
229
pages/index.vue
@@ -3,6 +3,10 @@ const { data: posts } = await useFetch('/api/posts', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
const { data: tags } = await useFetch('/api/tags', {
|
||||
default: () => []
|
||||
})
|
||||
|
||||
/**
|
||||
* 날짜 표시 형식 변환
|
||||
* @param {string | null} value - ISO 날짜 문자열
|
||||
@@ -13,90 +17,177 @@ const formatPostDate = (value) => {
|
||||
return ''
|
||||
}
|
||||
|
||||
const date = new Date(value)
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
return `${year}.${month}.${day}`
|
||||
return new Intl.DateTimeFormat('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).format(new Date(value))
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시물 카드 데이터 변환
|
||||
* @param {Object} post - API 게시물
|
||||
* @returns {Object} 게시물 카드 데이터
|
||||
* 태그 슬러그로 태그 정보 조회
|
||||
* @param {string | undefined} slug - 태그 슬러그
|
||||
* @returns {{name: string, color: string}} 태그 정보
|
||||
*/
|
||||
const mapPostCard = (post) => ({
|
||||
title: post.title,
|
||||
excerpt: post.excerpt,
|
||||
featuredImage: post.featuredImage,
|
||||
tag: post.tags?.[0]?.toUpperCase() || 'POST',
|
||||
publishedAt: formatPostDate(post.publishedAt),
|
||||
to: `/post/${post.slug}`
|
||||
})
|
||||
const getTagMeta = (slug) => {
|
||||
const matchedTag = tags.value.find((item) => item.slug === slug)
|
||||
|
||||
const postCards = computed(() => posts.value.map(mapPostCard))
|
||||
return {
|
||||
name: matchedTag?.name || (slug ? slug.toUpperCase() : 'POST'),
|
||||
color: matchedTag?.color || '#4d4d4d'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest 목록 데이터 변환
|
||||
* @param {Object} post - API 게시물
|
||||
* @param {number} index - 목록 인덱스
|
||||
* @returns {Object} 화면 표시 데이터
|
||||
*/
|
||||
const mapLatestPost = (post, index) => {
|
||||
const primaryTagSlug = post.tags?.[0]
|
||||
const tagMeta = getTagMeta(primaryTagSlug)
|
||||
|
||||
return {
|
||||
title: post.title,
|
||||
excerpt: post.excerpt,
|
||||
featuredImage: post.featuredImage,
|
||||
tagName: tagMeta.name,
|
||||
tagColor: tagMeta.color,
|
||||
publishedAt: formatPostDate(post.publishedAt),
|
||||
to: `/post/${post.slug}`,
|
||||
isFeatured: index === 0
|
||||
}
|
||||
}
|
||||
|
||||
const featuredPosts = computed(() => posts.value.slice(0, 6))
|
||||
const latestPosts = computed(() => posts.value.map(mapLatestPost))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MainColumn>
|
||||
<section class="home-hero site-section">
|
||||
<div class="home-hero__inner site-section-header text-center">
|
||||
<h1 class="home-hero__title mx-auto max-w-[620px] text-3xl font-semibold leading-tight tracking-normal md:text-[28px]">
|
||||
Ideas <em>published</em> for meaningful conversation, discussed and shaped by the community
|
||||
</h1>
|
||||
<p class="home-hero__description mx-auto mt-3 max-w-[500px] text-base leading-7 site-muted">
|
||||
글을 쌓고, 프로젝트와 링크를 연결하고, 오래 쓰기 좋은 개인 블로그를 직접 구축합니다.
|
||||
</p>
|
||||
<form class="home-hero__subscribe mx-auto mt-5 flex max-w-[345px] gap-2">
|
||||
<input class="home-hero__input min-w-0 flex-1 rounded-lg px-3 py-2 text-sm site-input" placeholder="Your email">
|
||||
<button class="home-hero__button rounded-lg px-5 py-2 text-sm font-semibold site-button" type="button">
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home-featured site-section">
|
||||
<div class="home-featured__header site-section-body flex items-center justify-between">
|
||||
<h2 class="home-featured__title text-sm font-semibold uppercase site-muted">
|
||||
Featured
|
||||
</h2>
|
||||
<div class="home-featured__controls flex gap-4">
|
||||
<span>‹</span>
|
||||
<span>›</span>
|
||||
<section class="px-5 py-6 sm:px-6 md:py-8">
|
||||
<div class="mx-auto flex max-w-[720px] flex-col-reverse gap-6">
|
||||
<div class="z-[2] flex flex-col items-center justify-center gap-2 text-center">
|
||||
<h1 class="text-xl font-semibold leading-[1.125] md:text-2xl">
|
||||
Ideas <em>published</em> for meaningful conversation, <em>discussed</em> and shaped by the community
|
||||
</h1>
|
||||
<p class="max-w-md text-base leading-snug site-muted">
|
||||
A modern Ghost theme for curated, community-driven publishing, where members join the conversation.
|
||||
</p>
|
||||
<form class="group relative mt-1 flex w-full max-w-xs flex-col items-start">
|
||||
<fieldset class="flex w-full flex-wrap gap-2 text-sm">
|
||||
<legend class="sr-only">Personal information</legend>
|
||||
<input class="site-input flex-[2] rounded-[10px] px-3 py-1.5 text-sm" type="email" placeholder="Your email" aria-label="Your email">
|
||||
<button class="site-button flex-1 cursor-pointer rounded-[10px] border border-[var(--site-invert)] bg-gradient-to-b from-[rgba(17,17,17,0.75)] to-[rgba(17,17,17,0.95)] px-3 py-1.5 font-medium text-[var(--site-invert-text)] hover:opacity-90" type="button">
|
||||
Subscribe
|
||||
</button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="home-featured__items grid grid-cols-1 gap-4 px-6 pb-6 md:grid-cols-3">
|
||||
<article class="home-featured__card h-[146px] rounded-lg bg-[linear-gradient(135deg,#071b22,#0f827c)] p-4 text-white">
|
||||
<h3 class="mt-20 text-sm font-semibold leading-tight">
|
||||
Essential tools and techniques for getting started
|
||||
</h3>
|
||||
</article>
|
||||
<article class="home-featured__card h-[146px] rounded-lg bg-[linear-gradient(135deg,#182434,#d4b06b)] p-4 text-white">
|
||||
<h3 class="mt-20 text-sm font-semibold leading-tight">
|
||||
Setting up your first home server from scratch
|
||||
</h3>
|
||||
</article>
|
||||
<article class="home-featured__card h-[146px] rounded-lg bg-[linear-gradient(135deg,#141414,#8a5a44)] p-4 text-white">
|
||||
<h3 class="mt-20 text-sm font-semibold leading-tight">
|
||||
Writing notes that stay useful over time
|
||||
</h3>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="px-5 py-4 sm:px-6">
|
||||
<div class="mx-auto max-w-[720px]">
|
||||
<div class="flex items-end justify-between gap-2 border-b border-[var(--site-line)] pb-2">
|
||||
<h2 class="text-sm font-medium uppercase site-muted">Featured</h2>
|
||||
<div class="flex justify-between gap-2">
|
||||
<button class="cursor-pointer p-1 hover:opacity-75" type="button" aria-label="Previous">‹</button>
|
||||
<button class="cursor-pointer p-1 hover:opacity-75" type="button" aria-label="Next">›</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<NuxtLink
|
||||
v-for="post in featuredPosts"
|
||||
:key="`featured-${post.slug}`"
|
||||
:to="`/post/${post.slug}`"
|
||||
class="group relative block aspect-video overflow-hidden rounded-[10px]"
|
||||
>
|
||||
<img
|
||||
v-if="post.featuredImage"
|
||||
:src="post.featuredImage"
|
||||
:alt="post.title"
|
||||
class="h-full w-full object-cover brightness-75 contrast-125 transition-all duration-200 group-hover:brightness-90 group-hover:contrast-110"
|
||||
loading="lazy"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="h-full w-full bg-[linear-gradient(135deg,#071b22,#5f6f85)]"
|
||||
/>
|
||||
<h3 class="absolute right-0 bottom-2.5 left-0 px-3 text-sm font-medium leading-tight text-white line-clamp-2">
|
||||
{{ post.title }}
|
||||
</h3>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="home-latest site-section">
|
||||
<div class="home-latest__header site-section-body flex items-center justify-between">
|
||||
<h2 class="home-latest__title text-sm font-semibold uppercase site-muted">
|
||||
Latest
|
||||
</h2>
|
||||
<button class="home-latest__view site-interactive rounded-lg px-3 py-2 text-sm site-input" type="button">
|
||||
목록
|
||||
</button>
|
||||
<section class="px-5 py-4 sm:px-6">
|
||||
<div class="mx-auto max-w-[720px]">
|
||||
<div class="flex items-end justify-between gap-2 border-b border-[var(--site-line)] pb-2">
|
||||
<h2 class="text-sm font-medium uppercase site-muted">Latest</h2>
|
||||
<button class="site-input flex cursor-pointer items-center justify-center gap-1 rounded-[10px] border px-2 py-1.5 text-sm hover:bg-[var(--site-panel)]" type="button">
|
||||
<span>☷</span>
|
||||
<span>⌄</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-8 flex flex-col">
|
||||
<div class="flex flex-col divide-y divide-[var(--site-line)]">
|
||||
<article
|
||||
v-for="post in latestPosts"
|
||||
:key="post.to"
|
||||
class="group relative flex flex-row gap-3 py-4"
|
||||
>
|
||||
<NuxtLink :to="post.to" class="relative aspect-square min-w-16 flex-1 sm:aspect-video">
|
||||
<figure class="overflow-hidden rounded-[10px]">
|
||||
<img
|
||||
v-if="post.featuredImage"
|
||||
:src="post.featuredImage"
|
||||
:alt="post.title"
|
||||
class="aspect-square w-full rounded-[inherit] object-cover transition-opacity duration-200 group-hover:opacity-90 sm:aspect-video"
|
||||
loading="lazy"
|
||||
>
|
||||
<div
|
||||
v-else
|
||||
class="aspect-square w-full rounded-[inherit] bg-[linear-gradient(135deg,#253444,#8f9dad)] sm:aspect-video"
|
||||
/>
|
||||
</figure>
|
||||
</NuxtLink>
|
||||
|
||||
<div class="relative flex-[3] md:flex-[4]">
|
||||
<div class="flex h-full flex-col gap-1.5">
|
||||
<h2 class="max-w-[90%] text-sm font-medium leading-tight">
|
||||
<NuxtLink :to="post.to" class="transition-opacity duration-200 hover:opacity-75">
|
||||
<span v-if="post.isFeatured" class="mr-1 inline-flex text-[var(--site-accent)]">⚡</span>
|
||||
{{ post.title }}
|
||||
</NuxtLink>
|
||||
</h2>
|
||||
|
||||
<p class="line-clamp-2 flex-1 text-[0.8rem] leading-tight site-muted">
|
||||
{{ post.excerpt }}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs site-muted sm:gap-1.5">
|
||||
<time>{{ post.publishedAt }}</time>
|
||||
<span class="text-[var(--site-line)]">/</span>
|
||||
<span
|
||||
class="rounded-sm px-1.5 py-px font-medium text-[var(--site-text)]"
|
||||
:style="{ backgroundColor: `${post.tagColor}1a` }"
|
||||
>
|
||||
{{ post.tagName }}
|
||||
</span>
|
||||
<span class="text-[var(--site-line)]">/</span>
|
||||
<span class="flex items-center gap-0.5">
|
||||
<span>◌</span>
|
||||
<span>0</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<PostCard v-for="post in postCards" :key="post.to" :post="post" />
|
||||
</MainColumn>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user