Merge branch 'main' into tailwind-v4-poc-1

This commit is contained in:
L4Ph
2025-05-30 22:30:07 +09:00
29 changed files with 1250 additions and 975 deletions

View File

@@ -5,7 +5,7 @@ Un tema estático para blogs construido con [Astro](https://astro.build).
[**🖥️ Demostración en Vivo (Vercel)**](https://fuwari.vercel.app)   /   
[**📦 Versión Antigua de Hexo**](https://github.com/saicaca/hexo-theme-vivia)   /   
> Versión del README: `2024-04-07`
> Versión del README: `2025-04-24`
![Imagen de Vista Previa](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
@@ -18,9 +18,39 @@ Un tema estático para blogs construido con [Astro](https://astro.build).
- [x] Diseño responsivo
- [ ] Comentarios
- [x] Buscador
- [ ] TOC (Tabla de Contenidos)
- [x] TOC (Tabla de Contenidos)
## 🚀 Cómo Usar
## 👀 requiere
- Node.js <= 22
- pnpm <= 9
## 🚀 Cómo Usar 1
Inicializa el proyecto localmente usando [create-fuwari](https://github.com/L4Ph/create-fuwari).
```sh
# npm
npm create fuwari@latest.
# yarn
yarn create fuwari.
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
1. Edita el archivo de configuración `src/config.ts` para personalizar tu blog.
2. Ejecuta `pnpm new-post <nombre-de-archivo>` para crear una nueva entrada y edítala en `src/content/posts/`.
3. Despliega tu blog en Vercel, Netlify, GitHub Pages, etc., siguiendo [las guías](https://docs.astro.build/en/guides/deploy/). Necesitas editar la configuración del sitio en `astro.config.mjs` antes del despliegue.
## 🚀 Cómo Usar 2
1. [Genera un nuevo repositorio](https://github.com/saicaca/fuwari/generate) desde esta plantilla o haz un fork de este repositorio.
2. Para editar tu blog localmente, clona tu repositorio, ejecuta `pnpm install` y `pnpm add sharp` para instalar las dependencias.

View File

@@ -5,7 +5,7 @@
[**🖥️ライブデモ (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦旧 Hexo バージョン**](https://github.com/saicaca/hexo-theme-vivia)
> README バージョン:`2024-04-07`
> README バージョン:`2025-04-24`
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
@@ -18,9 +18,39 @@
- [x] レスポンシブデザイン
- [ ] コメント機能
- [x] 検索機能
- [ ] 目次
- [x] 目次
## 🚀 使用方法
## 👀 以下が必要
- Node.js <= 22
- pnpm <= 9
## 🚀 使用方法 1
[create-fuwari](https://github.com/L4Ph/create-fuwari)を使用して、ローカルにプロジェクトを初期化します。
```sh
# npm
npm create fuwari@latest
# yarn
yarn create fuwari
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
1. `src/config.ts` ファイルを編集する事でブログを自分好みにカスタマイズ出来ます。
2. `pnpm new-post <filename>` で新しい記事を作成し、`src/content/posts/`.フォルダ内で編集します。
3. 作成したブログをVercel、Netlify、GitHub Pagesなどにデプロイするには[ガイド](https://docs.astro.build/ja/guides/deploy/)に従って下さい。加えて、別途デプロイを行う前に `astro.config.mjs` を編集してサイト構成を変更する必要があります。
## 🚀 使用方法 2
1. [テンプレート](https://github.com/saicaca/fuwari/generate)から新しいリポジトリを作成するかCloneをします。
2. ブログをローカルで編集するには、リポジトリをクローンした後、`pnpm install``pnpm add sharp` を実行して依存関係をインストールします。

View File

@@ -3,9 +3,14 @@
[Astro](https://astro.build)로 구축된 정적 블로그 템플릿입니다.
[**🖥️미리보기 (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦Old Hexo Version**](https://github.com/saicaca/hexo-theme-vivia)
[**📦Old Hexo Version**](https://github.com/saicaca/hexo-theme-vivia)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**🌏 English**](https://github.com/saicaca/fuwari/blob/main/README.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**🌏 中文**](https://github.com/saicaca/fuwari/blob/main/README.zh-CN.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**🌏 日本語**](https://github.com/saicaca/fuwari/blob/main/README.ja-JP.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**🌏 Español**](https://github.com/saicaca/fuwari/blob/main/README.es.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**🌏 ไทย**](https://github.com/saicaca/fuwari/blob/main/README.th.md)
> README 버전: `2024-04-07`
> README 버전: `2025-04-24`
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
@@ -18,7 +23,38 @@
- [x] 반응형 디자인
- [ ] 댓글
- [x] 검색
- [ ] 목차
- [x] 목차
## 요구 사항
- Node.js <= 22
- pnpm <= 9
## 🚀 사용하는 방법 1
[create-fuwari](https://github.com/L4Ph/create-fuwari)를 사용하여 로컬에서 프로젝트를 초기화합니다.
```sh
# npm
npm create fuwari@latest
# yarn
yarn create fuwari
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
1. 블로그를 사용자 정의하려면 `src/config.ts` 구성 파일을 편집하세요.
2. `pnpm new-post <filename>`을 실행하여 새 게시물을 만들고 `src/content/posts/`에서 편집하세요.
3. [가이드](https://docs.astro.build/en/guides/deploy/)에 따라 블로그를 Vercel, Netlify, GitHub 페이지 등에 배포하세요. 배포하기 전에 `astro.config.mjs`에서 사이트 구성을 편집해야 합니다.
## 🚀 사용하는 방법
1. 이 템플릿에서 [새 저장소를 생성](https://github.com/saicaca/fuwari/generate)하거나 이 저장소를 포크하세요.
@@ -39,6 +75,7 @@ image: /images/cover.jpg
tags: [푸, 바, 오]
category: 앞-끝
draft: false
lang: jp # 게시물의 언어가 `config.ts`의 사이트 언어와 다른 경우에만 설정합니다.
---
```

View File

@@ -10,7 +10,7 @@ A static blog template built with [Astro](https://astro.build).
[**🌏 Español**](https://github.com/saicaca/fuwari/blob/main/README.es.md)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**🌏 ไทย**](https://github.com/saicaca/fuwari/blob/main/README.th.md)
> README version: `2024-09-10`
> README version: `2025-04-24`
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
@@ -23,7 +23,37 @@ A static blog template built with [Astro](https://astro.build).
- [x] Responsive design
- [ ] Comments
- [x] Search
- [ ] TOC
- [x] TOC
## require
- Node.js <= 22
- pnpm <= 9
## 🚀 How to Use 1
Initialize the project locally using [create-fuwari](https://github.com/L4Ph/create-fuwari).
```sh
# npm
npm create fuwari@latest
# yarn
yarn create fuwari
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
1. Edit the config file `src/config.ts` to customize your blog.
2. Run `pnpm new-post <filename>` to create a new post and edit it in `src/content/posts/`.
3. Deploy your blog to Vercel, Netlify, GitHub Pages, etc. following [the guides](https://docs.astro.build/en/guides/deploy/). You need to edit the site configuration in `astro.config.mjs` before deployment.
## 🚀 How to Use

View File

@@ -5,7 +5,7 @@
[**🖥️ ตัวอย่างการใช้งานจริง (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦 เวอร์ชั่นเก่าสำหรับ Hexo**](https://github.com/saicaca/hexo-theme-vivia)
> เวอร์ชั่นของ README: `2024-09-10`
> เวอร์ชั่นของ README: `2025-04-24`
![ภาพตัวอย่าง](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
@@ -18,9 +18,40 @@
- [x] Responsive design (หน้าตาเว็บปรับเปลี่ยนตามขนาดจอ)
- [ ] การแสดงความคิดเห็น
- [x] การค้นหา
- [ ] TOC (สารบัญ)
- [x] TOC (สารบัญ)
## 🚀 วิธีใช้งาน
## จำเป็นต้อง
- Node.js <= 22
- pnpm <= 9
## 🚀 วิธีใช้งาน 1
เริ่มต้นโปรเจ็กต์ในเครื่องโดยใช้ [create-fuwari](https://github.com/L4Ph/create-fuwari)
```sh
# npm
npm create fuwari@latest
# yarn
yarn create fuwari
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
1. แก้ไขไฟล์การตั้งค่า `src/config.ts` เพื่อปรับแต่งบล็อกของคุณ
2. รันคำสั่ง `pnpm new-post <filename>` เพื่อสร้างโพสต์ใหม่ใน `src/content/posts/` และแก้ไขไฟล์โพสต์นั้นๆ ให้สมบูรณ์
3. Deploy เว็บบล็อกของคุณไปยัง Vercel, Netlify, GitHub Pages หรือบริการอื่นๆ โดยอ้างอิงวิธีการจาก[คู่มือนี้](https://docs.astro.build/en/guides/deploy/) อย่าลืมแก้ไขการตั้งค่าเว็บไซต์ในไฟล์ `astro.config.mjs` ก่อนที่คุณจะ deploy เว็บ
## 🚀 วิธีใช้งาน 2
1. [Generate repository ใหม่](https://github.com/saicaca/fuwari/generate)ขึ้นมาจากแม่แบบนี้ หรือจะ fork repository นี้ก็ได้
2. เริ่มแก้ไขบล็อกของคุณแบบ local โดยการ clone repository ของคุณ (จากข้อ 1) ไว้ในเครื่องของคุณ แล้วรันคำสั่ง `pnpm install` และ `pnpm add sharp` เพื่อติดตั้ง dependencies ที่จำเป็น

View File

@@ -5,7 +5,7 @@
[**🖥在线预览Vercel**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦旧 Hexo 版本**](https://github.com/saicaca/hexo-theme-vivia)
> README 版本:`2024-09-10`
> README 版本:`2025-04-24`
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
@@ -18,9 +18,39 @@
- [x] 响应式设计
- [ ] 评论
- [x] 搜索
- [ ] 文内目录
- [x] 文内目录
## 🚀 使用方法
## 👀 要求
- Node.js <= 22
- pnpm <= 9
## 🚀 使用方法 1
使用 [create-fuwari](https://github.com/L4Ph/create-fuwari) 在本地初始化项目。
```sh
# npm
npm create fuwari@latest
# yarn
yarn create fuwari
# pnpm
pnpm create fuwari@latest
# bun
bun create fuwari@latest
# deno
deno run -A npm:create-fuwari@latest
```
1. 通过配置文件 `src/config.ts` 自定义博客
2. 执行 `pnpm new-post <filename>` 创建新文章,并在 `src/content/posts/` 目录中编辑
3. 参考[官方指南](https://docs.astro.build/zh-cn/guides/deploy/)将博客部署至 Vercel, Netlify, GitHub Pages 等;部署前需编辑 `astro.config.mjs` 中的站点设置。
## 🚀 使用方法 2
1. 使用此模板[生成新仓库](https://github.com/saicaca/fuwari/generate)或 Fork 此仓库
2. 进行本地开发Clone 新的仓库,执行 `pnpm install``pnpm add sharp` 以安装依赖

View File

@@ -1,71 +1,71 @@
{
"name": "fuwari",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build && pagefind --site dist",
"preview": "astro preview",
"astro": "astro",
"type-check": "tsc --noEmit --isolatedDeclarations",
"new-post": "node scripts/new-post.js",
"format": "biome format --write ./src",
"lint": "biome check --write ./src",
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.3.1",
"@astrojs/svelte": "7.0.11",
"@fontsource-variable/jetbrains-mono": "^5.2.5",
"@fontsource/roboto": "^5.2.5",
"@iconify-json/fa6-brands": "^1.2.5",
"@iconify-json/fa6-regular": "^1.2.3",
"@iconify-json/fa6-solid": "^1.2.3",
"@iconify-json/material-symbols": "^1.2.20",
"@iconify/svelte": "^4.2.0",
"@swup/astro": "^1.6.0",
"@tailwindcss/postcss": "^4.1.4",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.4",
"astro": "5.7.5",
"astro-icon": "^1.1.5",
"hastscript": "^9.0.1",
"katex": "^0.16.22",
"markdown-it": "^14.1.0",
"mdast-util-to-string": "^4.0.0",
"overlayscrollbars": "^2.11.1",
"pagefind": "^1.3.0",
"photoswipe": "^5.4.4",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-components": "^0.3.0",
"rehype-katex": "^7.0.1",
"rehype-slug": "^6.0.0",
"remark-directive": "^3.0.1",
"remark-directive-rehype": "^0.4.2",
"remark-github-admonitions-to-directives": "^1.0.5",
"remark-math": "^6.0.0",
"remark-sectionize": "^2.1.0",
"sanitize-html": "^2.16.0",
"sharp": "^0.34.1",
"stylus": "^0.64.0",
"svelte": "^5.28.2",
"tailwindcss": "^4.1.4",
"typescript": "^5.8.3",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.4",
"@biomejs/biome": "1.9.4",
"@rollup/plugin-yaml": "^4.1.2",
"@types/markdown-it": "^14.1.2",
"@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.15.0",
"postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.1"
},
"packageManager": "pnpm@9.14.4"
"name": "fuwari",
"type": "module",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro build && pagefind --site dist",
"preview": "astro preview",
"astro": "astro",
"type-check": "tsc --noEmit --isolatedDeclarations",
"new-post": "node scripts/new-post.js",
"format": "biome format --write ./src",
"lint": "biome check --write ./src",
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/rss": "^4.0.11",
"@astrojs/sitemap": "^3.4.0",
"@astrojs/svelte": "7.1.0",
"@fontsource-variable/jetbrains-mono": "^5.2.5",
"@fontsource/roboto": "^5.2.5",
"@iconify-json/fa6-brands": "^1.2.5",
"@iconify-json/fa6-regular": "^1.2.3",
"@iconify-json/fa6-solid": "^1.2.3",
"@iconify-json/material-symbols": "^1.2.22",
"@iconify/svelte": "^4.2.0",
"@swup/astro": "^1.6.0",
"@tailwindcss/postcss": "^4.1.4",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.4",
"astro": "5.8.1",
"astro-icon": "^1.1.5",
"hastscript": "^9.0.1",
"katex": "^0.16.22",
"markdown-it": "^14.1.0",
"mdast-util-to-string": "^4.0.0",
"overlayscrollbars": "^2.11.3",
"pagefind": "^1.3.0",
"photoswipe": "^5.4.4",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-components": "^0.3.0",
"rehype-katex": "^7.0.1",
"rehype-slug": "^6.0.0",
"remark-directive": "^3.0.1",
"remark-directive-rehype": "^0.4.2",
"remark-github-admonitions-to-directives": "^1.0.5",
"remark-math": "^6.0.0",
"remark-sectionize": "^2.1.0",
"sanitize-html": "^2.17.0",
"sharp": "^0.34.2",
"stylus": "^0.64.0",
"svelte": "^5.33.10",
"tailwindcss": "^4.1.4",
"typescript": "^5.8.3",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.4",
"@biomejs/biome": "1.9.4",
"@rollup/plugin-yaml": "^4.1.2",
"@types/markdown-it": "^14.1.2",
"@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.16.0",
"postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.1"
},
"packageManager": "pnpm@9.14.4"
}

1375
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,118 +0,0 @@
---
import { UNCATEGORIZED } from "@constants/constants";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { getSortedPosts } from "../utils/content-utils";
import { getPostUrlBySlug } from "../utils/url-utils";
interface Props {
keyword?: string;
tags?: string[];
categories?: string[];
}
const { tags, categories } = Astro.props;
let posts = await getSortedPosts();
if (Array.isArray(tags) && tags.length > 0) {
posts = posts.filter(
(post) =>
Array.isArray(post.data.tags) &&
post.data.tags.some((tag) => tags.includes(tag)),
);
}
if (Array.isArray(categories) && categories.length > 0) {
posts = posts.filter(
(post) =>
(post.data.category && categories.includes(post.data.category)) ||
(!post.data.category && categories.includes(UNCATEGORIZED)),
);
}
const groups: { year: number; posts: typeof posts }[] = (() => {
const groupedPosts = posts.reduce(
(grouped: { [year: number]: typeof posts }, post) => {
const year = post.data.published.getFullYear();
if (!grouped[year]) {
grouped[year] = [];
}
grouped[year].push(post);
return grouped;
},
{},
);
// convert the object to an array
const groupedPostsArray = Object.keys(groupedPosts).map((key) => ({
year: Number.parseInt(key),
posts: groupedPosts[Number.parseInt(key)],
}));
// sort years by latest first
groupedPostsArray.sort((a, b) => b.year - a.year);
return groupedPostsArray;
})();
function formatDate(date: Date) {
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${month}-${day}`;
}
function formatTag(tag: string[]) {
return tag.map((t) => `#${t}`).join(" ");
}
---
<div class="card-base px-8 py-6">
{
groups.map(group => (
<div>
<div class="flex flex-row w-full items-center h-[3.75rem]">
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">{group.year}</div>
<div class="w-[15%] md:w-[10%]">
<div class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto -outline-offset-[2px] z-50 outline-3"></div>
</div>
<div class="w-[70%] md:w-[80%] transition text-left text-50">{group.posts.length} {i18n(I18nKey.postsCount)}</div>
</div>
{group.posts.map(post => (
<a href={getPostUrlBySlug(post.slug)}
aria-label={post.data.title}
class="group btn-plain block! h-10 w-full rounded-lg hover:text-[initial]"
>
<div class="flex flex-row justify-start items-center h-full">
<!-- date -->
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
{formatDate(post.data.published)}
</div>
<!-- dot and line -->
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
<div class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
outline outline-4 z-50
outline-[var(--card-bg)]
group-hover:outline-[var(--btn-plain-bg-hover)]
group-active:outline-[var(--btn-plain-bg-active)]
"
></div>
</div>
<!-- post title -->
<div class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
text-75 pr-8 whitespace-nowrap text-ellipsis overflow-hidden"
>
{post.data.title}
</div>
<!-- tag list -->
<div class="hidden md:block md:w-[15%] text-left text-sm transition
whitespace-nowrap text-ellipsis overflow-hidden
text-30"
>{formatTag(post.data.tags)}</div>
</div>
</a>
))}
</div>
))
}
</div>

View File

@@ -0,0 +1,151 @@
<script lang="ts">
import { onMount } from "svelte";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { getPostUrlBySlug } from "../utils/url-utils";
export let tags: string[];
export let categories: string[];
export let sortedPosts: Post[] = [];
const params = new URLSearchParams(window.location.search);
tags = params.has("tag") ? params.getAll("tag") : [];
categories = params.has("category") ? params.getAll("category") : [];
const uncategorized = params.get("uncategorized");
interface Post {
slug: string;
data: {
title: string;
tags: string[];
category?: string;
published: Date;
};
}
interface Group {
year: number;
posts: Post[];
}
let groups: Group[] = [];
function formatDate(date: Date) {
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${month}-${day}`;
}
function formatTag(tagList: string[]) {
return tagList.map((t) => `#${t}`).join(" ");
}
onMount(async () => {
let filteredPosts: Post[] = sortedPosts;
if (tags.length > 0) {
filteredPosts = filteredPosts.filter(
(post) =>
Array.isArray(post.data.tags) &&
post.data.tags.some((tag) => tags.includes(tag)),
);
}
if (categories.length > 0) {
filteredPosts = filteredPosts.filter(
(post) => post.data.category && categories.includes(post.data.category),
);
}
if (uncategorized) {
filteredPosts = filteredPosts.filter((post) => !post.data.category);
}
const grouped = filteredPosts.reduce(
(acc, post) => {
const year = post.data.published.getFullYear();
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(post);
return acc;
},
{} as Record<number, Post[]>,
);
const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({
year: Number.parseInt(yearStr),
posts: grouped[Number.parseInt(yearStr)],
}));
groupedPostsArray.sort((a, b) => b.year - a.year);
groups = groupedPostsArray;
});
</script>
<div class="card-base px-8 py-6">
{#each groups as group}
<div>
<div class="flex flex-row w-full items-center h-[3.75rem]">
<div class="w-[15%] md:w-[10%] transition text-2xl font-bold text-right text-75">
{group.year}
</div>
<div class="w-[15%] md:w-[10%]">
<div
class="h-3 w-3 bg-none rounded-full outline outline-[var(--primary)] mx-auto
-outline-offset-[2px] z-50 outline-3"
></div>
</div>
<div class="w-[70%] md:w-[80%] transition text-left text-50">
{group.posts.length} {i18n(I18nKey.postsCount)}
</div>
</div>
{#each group.posts as post}
<a
href={getPostUrlBySlug(post.slug)}
aria-label={post.data.title}
class="group btn-plain !block h-10 w-full rounded-lg hover:text-[initial]"
>
<div class="flex flex-row justify-start items-center h-full">
<!-- date -->
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
{formatDate(post.data.published)}
</div>
<!-- dot and line -->
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
<div
class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
bg-[oklch(0.5_0.05_var(--hue))] group-hover:bg-[var(--primary)]
outline outline-4 z-50
outline-[var(--card-bg)]
group-hover:outline-[var(--btn-plain-bg-hover)]
group-active:outline-[var(--btn-plain-bg-active)]"
></div>
</div>
<!-- post title -->
<div
class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
text-75 pr-8 whitespace-nowrap overflow-ellipsis overflow-hidden"
>
{post.data.title}
</div>
<!-- tag list -->
<div
class="hidden md:block md:w-[15%] text-left text-sm transition
whitespace-nowrap overflow-ellipsis overflow-hidden text-30"
>
{formatTag(post.data.tags)}
</div>
</div>
</a>
{/each}
</div>
{/each}
</div>

View File

@@ -16,7 +16,7 @@ interface Props {
published: Date;
updated?: Date;
tags: string[];
category: string;
category: string | null;
image: string;
description: string;
draft: boolean;

View File

@@ -3,14 +3,14 @@ import { Icon } from "astro-icon/components";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import { formatDateToYYYYMMDD } from "../utils/date-utils";
import { url } from "../utils/url-utils";
import { getCategoryUrl, getTagUrl } from "../utils/url-utils";
interface Props {
class: string;
published: Date;
updated?: Date;
tags: string[];
category: string;
category: string | null;
hideTagsForMobile?: boolean;
hideUpdateDate?: boolean;
}
@@ -53,7 +53,7 @@ const className = Astro.props.class;
<Icon name="material-symbols:book-2-outline-rounded" class="text-xl"></Icon>
</div>
<div class="flex flex-row flex-nowrap items-center">
<a href={url(`/archive/category/${category || 'uncategorized'}/`)} aria-label=`View all posts in the ${category} category`
<a href={getCategoryUrl(category)} aria-label={`View all posts in the ${category} category`}
class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{category || i18n(I18nKey.uncategorized)}
@@ -70,10 +70,10 @@ const className = Astro.props.class;
<div class="flex flex-row flex-nowrap items-center">
{(tags && tags.length > 0) && tags.map((tag, i) => (
<div class:list={[{"hidden": i == 0}, "mx-1.5 text-[var(--meta-divider)] text-sm"]}>/</div>
<a href={url(`/archive/tag/${tag}/`)} aria-label=`View all posts with the ${tag} tag`
<a href={getTagUrl(tag)} aria-label={`View all posts with the ${tag.trim()} tag`}
class="link-lg transition text-50 text-sm font-medium
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
{tag}
{tag.trim()}
</a>
))}
{!(tags && tags.length > 0) && <div class="transition text-50 text-sm font-medium">{i18n(I18nKey.noTags)}</div>}

View File

@@ -68,7 +68,7 @@ const getPageUrl = (p: number) => {
>
{p}
</div>
return <a href={url(getPageUrl(p))} aria-label=`Page ${p}`
return <a href={url(getPageUrl(p))} aria-label={`Page ${p}`}
class="btn-card w-11 h-11 rounded-lg overflow-hidden active:scale-[0.85]"
>{p}</a>
})}

View File

@@ -18,7 +18,7 @@ const profileConf = profileConfig;
const licenseConf = licenseConfig;
const postUrl = decodeURIComponent(Astro.url.toString());
---
<div class=`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`>
<div class={`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`}>
<div class="transition font-bold text-black/75 dark:text-white/75">
{title}
</div>

View File

@@ -4,7 +4,6 @@ import WidgetLayout from "./WidgetLayout.astro";
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
import { getCategoryList } from "../../utils/content-utils";
import { getCategoryUrl } from "../../utils/url-utils";
import ButtonLink from "../control/ButtonLink.astro";
const categories = await getCategoryList();
@@ -27,11 +26,11 @@ const style = Astro.props.style;
>
{categories.map((c) =>
<ButtonLink
url={getCategoryUrl(c.name)}
url={c.url}
badge={String(c.count)}
label=`View all posts in the ${c.name} category`
label={`View all posts in the ${c.name.trim()} category`}
>
{c.name}
{c.name.trim()}
</ButtonLink>
)}
</WidgetLayout>

View File

@@ -3,7 +3,7 @@
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
import { getTagList } from "../../utils/content-utils";
import { url } from "../../utils/url-utils";
import { getTagUrl } from "../../utils/url-utils";
import ButtonTag from "../control/ButtonTag.astro";
import WidgetLayout from "./WidgetLayout.astro";
@@ -23,8 +23,8 @@ const style = Astro.props.style;
<WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}>
<div class="flex gap-2 flex-wrap">
{tags.map(t => (
<ButtonTag href={url(`/archive/tag/${t.name}/`)} label={`View all posts with the ${t.name} tag`}>
{t.name}
<ButtonTag href={getTagUrl(t.name)} label={`View all posts with the ${t.name.trim()} tag`}>
{t.name.trim()}
</ButtonTag>
))}
</div>

View File

@@ -1,5 +1,3 @@
export const UNCATEGORIZED = "__uncategorized__";
export const PAGE_SIZE = 8;
export const LIGHT_MODE = "light",

View File

@@ -9,7 +9,7 @@ const postsCollection = defineCollection({
description: z.string().optional().default(""),
image: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().default(""),
category: z.string().optional().nullable().default(""),
lang: z.string().optional().default(""),
/* For internal use */

View File

@@ -245,13 +245,49 @@ function initCustomScrollbar() {
}
});
});
const katexElements = document.querySelectorAll('.katex-display') as NodeListOf<HTMLElement>;
katexElements.forEach((ele) => {
OverlayScrollbars(ele, {
const katexObserverOptions = {
root: null,
rootMargin: '100px',
threshold: 0.1
};
const processKatexElement = (element: HTMLElement) => {
if (!element.parentNode) return;
if (element.hasAttribute('data-scrollbar-initialized')) return;
const container = document.createElement('div');
container.className = 'katex-display-container';
container.setAttribute('aria-label', 'scrollable container for formulas');
element.parentNode.insertBefore(container, element);
container.appendChild(element);
OverlayScrollbars(container, {
scrollbars: {
theme: 'scrollbar-base scrollbar-auto py-1',
theme: 'scrollbar-base scrollbar-auto',
autoHide: 'leave',
autoHideDelay: 500,
autoHideSuspend: false
}
});
element.setAttribute('data-scrollbar-initialized', 'true');
};
const katexObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
processKatexElement(entry.target as HTMLElement);
observer.unobserve(entry.target);
}
});
}, katexObserverOptions);
katexElements.forEach(element => {
katexObserver.observe(element);
});
}

View File

@@ -52,7 +52,7 @@ const mainPanelTop = siteConfig.banner.enable
</div>
<!-- Banner -->
{siteConfig.banner.enable && <div id="banner-wrapper" class=`absolute z-10 w-full transition duration-700 overflow-hidden` style=`top: -${BANNER_HEIGHT_EXTEND}vh`>
{siteConfig.banner.enable && <div id="banner-wrapper" class={`absolute z-10 w-full transition duration-700 overflow-hidden`} style={`top: -${BANNER_HEIGHT_EXTEND}vh`}>
<ImageWrapper id="banner" alt="Banner image of the blog" class:list={["object-cover h-full transition duration-700 opacity-0 scale-105"]}
src={siteConfig.banner.src} position={siteConfig.banner.position}
>
@@ -60,7 +60,7 @@ const mainPanelTop = siteConfig.banner.enable
</div>}
<!-- Main content -->
<div class="absolute w-full z-30 pointer-events-none" style=`top: ${mainPanelTop}`>
<div class="absolute w-full z-30 pointer-events-none" style={`top: ${mainPanelTop}`}>
<!-- The pointer-events-none here prevent blocking the click event of the TOC -->
<div class="relative max-w-[var(--page-width)] mx-auto pointer-events-auto">
<div id="main-grid" class="transition duration-700 w-full left-0 right-0 grid grid-cols-[17.5rem_auto] grid-rows-[auto_1fr_auto] lg:grid-rows-[auto]
@@ -106,7 +106,7 @@ const mainPanelTop = siteConfig.banner.enable
<div class="absolute w-full z-0 hidden 2xl:block">
<div class="relative max-w-[var(--page-width)] mx-auto">
<!-- TOC component -->
{siteConfig.toc.enable && <div id="toc-wrapper" class:list={["hidden lg:block transition absolute top-0 -right-[var(--toc-width)] w-[var(--toc-width)] flex items-center",
{siteConfig.toc.enable && <div id="toc-wrapper" class:list={["hidden lg:block transition absolute top-0 -right-[var(--toc-width)] w-[var(--toc-width)] items-center",
{"toc-hide": siteConfig.banner.enable}]}
>
<div id="toc-inner-wrapper" class="fixed top-14 w-[var(--toc-width)] h-[calc(100vh_-_20rem)] overflow-y-scroll overflow-x-hidden hide-scrollbar">

View File

@@ -19,5 +19,5 @@ const len = page.data.length;
<MainGridLayout>
<PostPage page={page}></PostPage>
<Pagination class="mx-auto onload-animation" page={page} style=`animation-delay: calc(var(--content-delay) + ${(len)*50}ms)`></Pagination>
<Pagination class="mx-auto onload-animation" page={page} style={`animation-delay: calc(var(--content-delay) + ${(len)*50}ms)`}></Pagination>
</MainGridLayout>

14
src/pages/archive.astro Normal file
View File

@@ -0,0 +1,14 @@
---
import ArchivePanel from "@components/ArchivePanel.svelte";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getSortedPosts } from "../utils/content-utils";
const sortedPosts = await getSortedPosts();
---
<MainGridLayout title={i18n(I18nKey.archive)}>
<ArchivePanel sortedPosts={sortedPosts} client:only="svelte"></ArchivePanel>
</MainGridLayout>

View File

@@ -1,24 +0,0 @@
---
import ArchivePanel from "@components/ArchivePanel.astro";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getCategoryList } from "@utils/content-utils";
export async function getStaticPaths() {
const categories = await getCategoryList();
return categories.map((category) => {
return {
params: {
category: category.name,
},
};
});
}
const category = Astro.params.category as string;
---
<MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}>
<ArchivePanel categories={[category]}></ArchivePanel>
</MainGridLayout>

View File

@@ -1,11 +0,0 @@
---
import ArchivePanel from "@components/ArchivePanel.astro";
import { UNCATEGORIZED } from "@constants/constants";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
---
<MainGridLayout title={i18n(I18nKey.archive)}>
<ArchivePanel categories={[UNCATEGORIZED]}></ArchivePanel>
</MainGridLayout>

View File

@@ -1,11 +0,0 @@
---
import ArchivePanel from "@components/ArchivePanel.astro";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
---
<MainGridLayout title={i18n(I18nKey.archive)}>
<ArchivePanel></ArchivePanel>
</MainGridLayout>

View File

@@ -1,32 +0,0 @@
---
import ArchivePanel from "@components/ArchivePanel.astro";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getSortedPosts } from "@utils/content-utils";
export async function getStaticPaths() {
const posts = await getSortedPosts();
// タグを集めるための Set の型を指定
const allTags = posts.reduce<Set<string>>((acc, post) => {
// biome-ignore lint/complexity/noForEach: <explanation>
post.data.tags.forEach((tag) => acc.add(tag));
return acc;
}, new Set());
const allTagsArray = Array.from(allTags);
return allTagsArray.map((tag) => ({
params: {
tag: tag,
},
}));
}
const tag = Astro.params.tag as string;
---
<MainGridLayout title={i18n(I18nKey.archive)} description={i18n(I18nKey.archive)}>
<ArchivePanel tags={[tag]}></ArchivePanel>
</MainGridLayout>

View File

@@ -91,4 +91,11 @@
}
}
.katex-display-container {
max-width: 100%;
overflow-x: auto;
margin: 1em 0;
}
}

View File

@@ -1,6 +1,7 @@
import { getCollection } from "astro:content";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import { getCategoryUrl } from "@utils/url-utils.ts";
export async function getSortedPosts() {
const allBlogPosts = await getCollection("posts", ({ data }) => {
@@ -54,6 +55,7 @@ export async function getTagList(): Promise<Tag[]> {
export type Category = {
name: string;
count: number;
url: string;
};
export async function getCategoryList(): Promise<Category[]> {
@@ -61,15 +63,19 @@ export async function getCategoryList(): Promise<Category[]> {
return import.meta.env.PROD ? data.draft !== true : true;
});
const count: { [key: string]: number } = {};
allBlogPosts.map((post: { data: { category: string | number } }) => {
allBlogPosts.map((post: { data: { category: string | null } }) => {
if (!post.data.category) {
const ucKey = i18n(I18nKey.uncategorized);
count[ucKey] = count[ucKey] ? count[ucKey] + 1 : 1;
return;
}
count[post.data.category] = count[post.data.category]
? count[post.data.category] + 1
: 1;
const categoryName =
typeof post.data.category === "string"
? post.data.category.trim()
: String(post.data.category).trim();
count[categoryName] = count[categoryName] ? count[categoryName] + 1 : 1;
});
const lst = Object.keys(count).sort((a, b) => {
@@ -78,7 +84,11 @@ export async function getCategoryList(): Promise<Category[]> {
const ret: Category[] = [];
for (const c of lst) {
ret.push({ name: c, count: count[c] });
ret.push({
name: c,
count: count[c],
url: getCategoryUrl(c),
});
}
return ret;
}

View File

@@ -1,4 +1,4 @@
import i18nKey from "@i18n/i18nKey";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
export function pathsEqual(path1: string, path2: string) {
@@ -16,10 +16,19 @@ export function getPostUrlBySlug(slug: string): string {
return url(`/posts/${slug}/`);
}
export function getCategoryUrl(category: string): string {
if (category === i18n(i18nKey.uncategorized))
return url("/archive/category/uncategorized/");
return url(`/archive/category/${category}/`);
export function getTagUrl(tag: string): string {
if (!tag) return url("/archive/");
return url(`/archive/?tag=${encodeURIComponent(tag.trim())}`);
}
export function getCategoryUrl(category: string | null): string {
if (
!category ||
category.trim() === "" ||
category.trim().toLowerCase() === i18n(I18nKey.uncategorized).toLowerCase()
)
return url("/archive/?uncategorized=true");
return url(`/archive/?category=${encodeURIComponent(category.trim())}`);
}
export function getDir(path: string): string {