v0.1.20 태그 페이지 및 Alpine 적용

This commit is contained in:
2026-04-14 10:15:13 +09:00
parent c6901a0451
commit 892dd270bc
12 changed files with 209 additions and 39 deletions

5
assets/built/alpine.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
.visible{visibility:visible}.-mb-1\.5{margin-bottom:-.375rem}.mt-1{margin-top:.25rem}.flex{display:flex}.grid{display:grid}.hidden{display:none}.flex-1{flex:1 1 0%}.cursor-pointer{cursor:pointer}.resize{resize:both}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-0\.5{row-gap:.125rem}.text-ellipsis{text-overflow:ellipsis}.rounded-\[18px\]{border-radius:18px}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pl-0{padding-left:0}.pr-3{padding-right:.75rem}.text-\[0\.8rem\]{font-size:.8rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.uppercase{text-transform:uppercase}.leading-tight{line-height:1.25}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-\[padding\2c background-color\2c color\]{transition-property:padding,background-color,color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:px-3:hover{padding-left:.75rem;padding-right:.75rem}.hover\:opacity-75:hover{opacity:.75}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:pr-3{padding-right:.75rem}}@media (min-width:1280px){.xl\:pl-0{padding-left:0}}
.visible{visibility:visible}.absolute{position:absolute}.relative{position:relative}.inset-y-0{top:0;bottom:0}.left-0{left:0}.-mb-1\.5{margin-bottom:-.375rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-auto{margin-top:auto}.line-clamp-3{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.h-11{height:2.75rem}.h-4{height:1rem}.min-h-\[128px\]{min-height:128px}.w-11{width:2.75rem}.w-4{width:1rem}.w-\[3px\]{width:3px}.min-w-0{min-width:0}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.cursor-pointer{cursor:pointer}.resize{resize:both}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-2{-moz-column-gap:.5rem;column-gap:.5rem}.gap-y-0\.5{row-gap:.125rem}.text-ellipsis{text-overflow:ellipsis}.rounded-\[14px\]{border-radius:14px}.rounded-\[15px\]{border-radius:15px}.rounded-\[18px\]{border-radius:18px}.rounded-full{border-radius:9999px}.rounded-l-\[14px\]{border-top-left-radius:14px;border-bottom-left-radius:14px}.border{border-width:1px}.border-\[var\(--border\)\]{border-color:var(--border)}.bg-\[var\(--surface\)\]{background-color:var(--surface)}.bg-\[var\(--surface-muted\)\]{background-color:var(--surface-muted)}.bg-\[var\(--tag-accent\2c var\(--accent\)\)\]{background-color:var(--tag-accent,var(--accent))}.object-cover{-o-object-fit:cover;object-fit:cover}.p-4{padding:1rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-4{padding-top:1rem;padding-bottom:1rem}.pl-0{padding-left:0}.pr-3{padding-right:.75rem}.pt-3{padding-top:.75rem}.text-left{text-align:left}.text-center{text-align:center}.text-\[0\.8rem\]{font-size:.8rem}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-tight{line-height:1.25}.tracking-\[-0\.02em\]{letter-spacing:-.02em}.text-\[var\(--text-soft\)\]{color:var(--text-soft)}.opacity-70{opacity:.7}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-\[padding\2c background-color\2c color\]{transition-property:padding,background-color,color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:bg-\[var\(--surface-muted\)\]:hover{background-color:var(--surface-muted)}.hover\:px-3:hover{padding-left:.75rem;padding-right:.75rem}.hover\:px-4:hover{padding-left:1rem;padding-right:1rem}.hover\:opacity-75:hover{opacity:.75}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:640px){.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:pr-3{padding-right:.75rem}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media (min-width:1280px){.xl\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.xl\:pl-0{padding-left:0}}

View File

@@ -6,8 +6,8 @@
<title>{{meta_title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="{{asset "built/tailwind.css"}}">
<link rel="stylesheet" type="text/css" href="{{asset "built/screen.css"}}">
<link rel="stylesheet" type="text/css" href="{{asset "built/tailwind.css"}}">
{{ghost_head}}
</head>
@@ -25,6 +25,7 @@
</div>
{{ghost_foot}}
<script defer src="{{asset "built/alpine.js"}}"></script>
<script src="{{asset "built/theme.js"}}"></script>
</body>
</html>

96
docs/deploy.md Normal file
View File

@@ -0,0 +1,96 @@
# 배포 가이드
## 현재 버전
- `v0.1.20`
## Git 기본 설정
- 저장소 작성자 정보는 아래 값으로 통일한다.
- 이름: `zenn`
- 이메일: `zenn.message@gmail.com`
```bash
git config user.name "zenn"
git config user.email "zenn.message@gmail.com"
```
## 저장소 초기화
```bash
git init
git branch -M main
git remote add origin https://git.sori.studio/zenn/sori.studio.git
```
## 작업 종료 시 업로드 절차
1. 변경 파일과 문서 반영 상태를 확인한다.
2. 작성자 정보가 올바른지 확인한다.
3. 모든 변경을 스테이징한다.
4. 한국어 커밋 메시지로 커밋한다.
5. `main` 브랜치로 원격 저장소에 푸시한다.
6. 마지막 커밋과 작업 트리가 정상인지 다시 확인한다.
```bash
git config user.name
git config user.email
git status
git add -A
git commit -m "영역: 작업 내용"
git push origin main
git status
git log -1 --oneline
```
## 로컬 실행
```bash
npm install
npm run dev
```
## 로컬 스타일 빌드
```bash
npm run build:alpine
npm run build:tailwind
```
- `npm run dev`, `npm run dev:ghost:start`, `npm run dev:ghost:restart` 실행 시 Alpine.js와 Tailwind 빌드가 먼저 수행된다.
- Alpine 결과물은 `assets/built/alpine.js`에 생성된다.
- Tailwind 결과물은 `assets/built/tailwind.css`에 생성되고, Ghost 테마에서 `screen.css`보다 먼저 로드된다.
## 로컬 빌드 검증
```bash
npm run build
```
## 저장 기능 메모
- DB 연결 환경에서는 작성/수정 API가 Prisma를 통해 실제 데이터를 저장한다.
- DB 미연결 환경에서는 샘플 콘텐츠 fallback이 프로세스 메모리에서만 갱신된다.
- 따라서 재시작 이후에도 데이터를 유지하려면 PostgreSQL 연결이 필요하다.
## 폰트 에셋
- `Pretendard` 폰트 파일은 `assets/fonts` 경로를 사용한다.
- 전역 CSS 로딩에 `assets/fonts/pretendard.css`가 포함되어 있어야 한다.
## PostgreSQL 준비
```bash
cp .env.example .env
npm run db:push
npm run db:seed-admin
```
## 데모 로그인 계정
- 이메일: `zenn.message@gmail.com`
- 비밀번호: `zenn-demo-admin`
- 실제 운영 전에는 `.env`에서 데모 인증 값을 반드시 변경한다.
## 관리자 계정 시드
- DB 연결 환경에서는 `npm run db:seed-admin`으로 초기 관리자 계정을 생성하거나 갱신한다.
- 시드 계정 정보는 `.env``DEMO_ADMIN_*` 값을 사용한다.
- 실제 운영 전에는 기본 비밀번호를 반드시 변경한다.
## 원격 저장소 정보
- 기본 원격 이름: `origin`
- 기본 저장소 주소: `https://git.sori.studio/zenn/sori.studio.git`
## 운영 메모
- 민감 정보가 포함된 파일은 커밋 전에 반드시 확인한다.
- 문서 변경이 발생한 작업은 코드와 함께 같은 커밋에 포함한다.
- 배포 절차가 확정되면 UGREEN NAS Docker 배포 방법을 이 문서에 이어서 추가한다.

View File

@@ -1,5 +1,8 @@
# 의사결정 이력
## 2026-04-14 v0.1.20
앞으로 원본 테마 섹션 코드를 더 직접적으로 참고할 수 있도록 Alpine.js를 로컬 자산으로 포함하기로 했다. 외부 CDN 의존 대신 테마 빌드 단계에서 `assets/built/alpine.js`를 생성해 함께 배포하고, 기존 `theme.js`는 즉시 제거하지 않고 공존시키면서 점진적으로 Alpine 문법을 허용하는 방향으로 정리했다.
## 2026-04-14 v0.1.19
Ghost 업로드 오류에 맞춰 `author.hbs`의 구식 `{{#author}}` 블록 헬퍼를 제거했다. 작성자 아카이브 템플릿은 이미 작성자 컨텍스트에서 렌더링되므로, 별도 블록 헬퍼 없이 현재 컨텍스트 값을 직접 사용하는 방식이 Ghost 최신 검사 기준과도 맞다.

View File

@@ -1,11 +1,13 @@
# 파일-화면 매핑 가이드
## 현재 버전
- `v0.1.18`
- `v0.1.20`
## 공통 레이아웃
- [default.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/default.hbs): 전체 3열 셸과 공통 자산 로드
- [partials/site/sidebar-left.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/partials/site/sidebar-left.hbs): 좌측 탐색/카테고리 아코디언/작성자/푸터
- [partials/site/sidebar-left.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/partials/site/sidebar-left.hbs): 좌측 탐색/직접 링크형 Tags·Authors 메뉴/카테고리 아코디언/푸터
- [page-tags.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/page-tags.hbs): `slug=tags` 페이지용 태그 디렉터리
- [page-authors.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/page-authors.hbs): `slug=authors` 페이지용 작성자 디렉터리
- [partials/site/topbar.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/partials/site/topbar.hbs): 상단 검색/CTA/다크모드
- [partials/site/sidebar-right.hbs](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/partials/site/sidebar-right.hbs): 구독/추천/작성자/푸터
@@ -26,6 +28,7 @@
## 자산
- [assets/built/screen.css](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/assets/built/screen.css): 전체 스타일
- [assets/built/tailwind.css](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/assets/built/tailwind.css): Tailwind 빌드 결과물
- [assets/built/alpine.js](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/assets/built/alpine.js): Alpine.js 로컬 배포 파일
- [assets/built/theme.js](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/assets/built/theme.js): 인터랙션 스크립트
- [assets/styles/tailwind.css](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/assets/styles/tailwind.css): Tailwind 입력 파일
- [tailwind.config.js](/Users/bicute/Desktop/UGREEN/GHOST%20THEME/tailwind.config.js): Tailwind 스캔 경로 및 테마 설정

View File

@@ -1,7 +1,7 @@
# 기술 명세
## 현재 버전
- `v0.1.19`
- `v0.1.20`
## 테마 개요
- Ghost `v5` 대응 커스텀 테마
@@ -14,8 +14,10 @@
- 검색 오버레이, 탭 전환, 다크모드 토글용 프런트 스크립트
- Ghost `navigation`, `get`, `subscribe_form`, `comments`, `pagination` 헬퍼 사용
- Tailwind CSS 빌드 결과물(`assets/built/tailwind.css`)을 기존 `screen.css`와 함께 로드
- Alpine.js 로컬 자산(`assets/built/alpine.js`)을 전역 로드
- 좌측 카테고리 영역은 `1024px` 이상에서 기본 열림, 미만에서 기본 닫힘
- `author.hbs`는 페이지 컨텍스트의 작성자 데이터를 직접 사용
- `page-tags.hbs`, `page-authors.hbs`는 각각 `slug=tags`, `slug=authors` 페이지에 연결 가능
## 주요 스타일 방향
- 밝은 크림톤 배경 + 오렌지 포인트

View File

@@ -1,5 +1,11 @@
# 업데이트 로그
## v0.1.20 - 2026-04-14
- `Tags`, `Authors` 좌측 메뉴 직접 링크형으로 변경.
- `page-tags.hbs`, `page-authors.hbs` 추가.
- Tailwind 로드 우선순위 수정.
- Alpine.js 로컬 자산 로드 추가.
## v0.1.19 - 2026-04-14
- `author.hbs` 구식 `author` 블록 헬퍼 제거.
- Ghost 업로드 오류 대응 정리.

View File

@@ -1,6 +1,6 @@
{
"name": "ghost-theme-thred-clone",
"version": "0.1.19",
"version": "0.1.20",
"private": true,
"description": "A Ghost theme inspired by the Thred reference layout.",
"keywords": [
@@ -75,9 +75,10 @@
}
},
"scripts": {
"build:alpine": "cp ./node_modules/alpinejs/dist/cdn.min.js ./assets/built/alpine.js",
"build:tailwind": "tailwindcss -c ./tailwind.config.js -i ./assets/styles/tailwind.css -o ./assets/built/tailwind.css --minify",
"dev": "npm run dev:ghost:start",
"dev:prepare": "npm run build:tailwind && npm run dev:sync",
"dev:prepare": "npm run build:alpine && npm run build:tailwind && npm run dev:sync",
"dev:sync": "sh ./scripts/sync-theme.sh",
"dev:seed": "node ./scripts/build-sample-content.js",
"dev:seed:zip": "npm run dev:seed && cd seed && zip -q -r thred-inspired-sample-content.ghost.zip thred-inspired-sample-content.ghost.json",
@@ -87,6 +88,7 @@
"zip": "zip -r theme.zip . -x '*.git*' -x 'node_modules/*' -x 'theme.zip'"
},
"devDependencies": {
"alpinejs": "^3.14.9",
"tailwindcss": "^3.4.17"
}
}

42
page-authors.hbs Normal file
View File

@@ -0,0 +1,42 @@
{{!< default}}
{{#post}}
<main class="content-area">
<section class="stack-section">
<header class="section-header text-center">
<h1 class="section-title">{{title}}</h1>
{{#if custom_excerpt}}
<p class="section-description">{{custom_excerpt}}</p>
{{else}}
<p class="section-description">Browse by author</p>
{{/if}}
</header>
{{#get "authors" limit="all" include="count.posts"}}
<div class="author-directory grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{{#foreach authors}}
<a class="author-directory__card group flex min-h-[128px] flex-col rounded-[14px] border border-[var(--border)] bg-[var(--surface)] p-4 text-left transition-colors hover:bg-[var(--surface-muted)]" href="{{url}}">
<span class="flex items-start gap-3">
{{#if profile_image}}
<img class="h-11 w-11 rounded-full object-cover" src="{{img_url profile_image size="xs"}}" alt="{{name}}">
{{else}}
<span class="flex h-11 w-11 items-center justify-center rounded-full bg-[var(--surface-muted)] text-sm font-semibold">A</span>
{{/if}}
<span class="min-w-0 flex-1">
<strong class="block text-[15px] font-semibold tracking-[-0.02em]">{{name}}</strong>
{{#if bio}}
<span class="mt-1 line-clamp-3 block text-[13px] leading-5 text-[var(--text-soft)]">{{bio}}</span>
{{else}}
<span class="mt-1 block text-[13px] leading-5 text-[var(--text-soft)]">Posts by {{name}}.</span>
{{/if}}
</span>
<img class="h-4 w-4 shrink-0 opacity-70 transition-opacity group-hover:opacity-100" src="{{asset "icons/arrow_outward.svg"}}" alt="">
</span>
<span class="mt-auto pt-3 text-[13px] font-medium text-[var(--text-soft)]">{{plural count.posts empty="0 posts" singular="% post" plural="% posts"}}</span>
</a>
{{/foreach}}
</div>
{{/get}}
</section>
</main>
{{/post}}

36
page-tags.hbs Normal file
View File

@@ -0,0 +1,36 @@
{{!< default}}
{{#post}}
<main class="content-area">
<section class="stack-section">
<header class="section-header text-center">
<h1 class="section-title">{{title}}</h1>
{{#if custom_excerpt}}
<p class="section-description">{{custom_excerpt}}</p>
{{else}}
<p class="section-description">Browse by topic</p>
{{/if}}
</header>
{{#get "tags" limit="all" include="count.posts" order="count.posts desc"}}
<div class="tag-directory grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
{{#foreach tags}}
<a class="tag-directory__card group relative flex min-h-[128px] flex-col rounded-[14px] border border-[var(--border)] bg-[var(--surface)] p-4 text-left transition-colors hover:bg-[var(--surface-muted)]" href="{{url}}"{{#if accent_color}} style="--tag-accent: {{accent_color}};"{{/if}}>
<span class="tag-directory__accent absolute inset-y-0 left-0 w-[3px] rounded-l-[14px] bg-[var(--tag-accent,var(--accent))]"></span>
<span class="flex items-start justify-between gap-3">
<strong class="text-[15px] font-semibold tracking-[-0.02em]">{{name}}</strong>
<img class="h-4 w-4 shrink-0 opacity-70 transition-opacity group-hover:opacity-100" src="{{asset "icons/arrow_outward.svg"}}" alt="">
</span>
{{#if description}}
<p class="mt-2 line-clamp-3 text-[13px] leading-5 text-[var(--text-soft)]">{{description}}</p>
{{else}}
<p class="mt-2 line-clamp-3 text-[13px] leading-5 text-[var(--text-soft)]">Posts filed under {{name}}.</p>
{{/if}}
<span class="mt-auto pt-3 text-[13px] font-medium text-[var(--text-soft)]">{{plural count.posts empty="0 posts" singular="% post" plural="% posts"}}</span>
</a>
{{/foreach}}
</div>
{{/get}}
</section>
</main>
{{/post}}

View File

@@ -17,48 +17,22 @@
</div>
</section>
<section class="menu-group">
<button class="menu-group__trigger" type="button" data-accordion aria-expanded="false">
<section class="menu-group menu-group--link">
<a class="menu-group__link px-3 py-2 flex items-center rounded-[15px] transition-[padding,background-color,color] hover:bg-[var(--surface-muted)] hover:px-4" href="/tags/">
<span class="menu-link">
<span class="menu-link__marker"></span>
<span class="menu-link__label">Tags</span>
</span>
<span class="menu-group__chevron" aria-hidden="true">
<img class="menu-group__chevron-icon menu-group__chevron-icon--down" src="{{asset "icons/keyboard_arrow_down.svg"}}" alt="">
<img class="menu-group__chevron-icon menu-group__chevron-icon--up" src="{{asset "icons/keyboard_arrow_up.svg"}}" alt="">
</span>
</button>
<div class="menu-group__content" data-accordion-content>
<ul class="link-list">
{{#get "tags" limit="6" include="count.posts"}}
{{#foreach tags}}
<li><a class="menu-sub-link" href="{{url}}">{{name}}</a></li>
{{/foreach}}
{{/get}}
</ul>
</div>
</a>
</section>
<section class="menu-group">
<button class="menu-group__trigger" type="button" data-accordion aria-expanded="false">
<section class="menu-group menu-group--link">
<a class="menu-group__link px-3 py-2 flex items-center rounded-[15px] transition-[padding,background-color,color] hover:bg-[var(--surface-muted)] hover:px-4" href="/authors/">
<span class="menu-link">
<span class="menu-link__marker"></span>
<span class="menu-link__label">Authors</span>
</span>
<span class="menu-group__chevron" aria-hidden="true">
<img class="menu-group__chevron-icon menu-group__chevron-icon--down" src="{{asset "icons/keyboard_arrow_down.svg"}}" alt="">
<img class="menu-group__chevron-icon menu-group__chevron-icon--up" src="{{asset "icons/keyboard_arrow_up.svg"}}" alt="">
</span>
</button>
<div class="menu-group__content" data-accordion-content>
<ul class="link-list">
{{#get "authors" limit="6" include="count.posts"}}
{{#foreach authors}}
<li><a class="menu-sub-link" href="{{url}}">{{name}}</a></li>
{{/foreach}}
{{/get}}
</ul>
</div>
</a>
</section>
<section class="menu-group">