This commit is contained in:
Jay 2026-03-28 05:18:40 +09:00
parent 6d39b0dec4
commit 64c203a998
135 changed files with 8136 additions and 8731 deletions

View file

@ -0,0 +1 @@
{"src":{"assets":{"images":{}},"content":{"posts":{"en":{"2026":{"01":{}}}}}}}

View file

@ -0,0 +1 @@
{}

View file

@ -1,59 +0,0 @@
name: Bug Report
description: Create a report to help us improve
title: "[Bug]: "
labels: ["bug"]
assignees:
- L4Ph
- saicaca
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: textarea
id: bug-description
attributes:
label: Describe the bug
description: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: to-reproduce
attributes:
label: To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
description: A clear and concise description of what you expected to happen.
validations:
required: true
- type: dropdown
id: os
attributes:
label: OS
multiple: true
options:
- Windows
- macOS
- Linux
- Android
- iOS
- type: input
id: browser
attributes:
label: Browser
placeholder: e.g. chrome, safari
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context about the problem here.

View file

@ -1,41 +0,0 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature]: "
labels: ["enhancement"]
assignees:
- saicaca
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this feature request!
- type: textarea
id: related-problem
attributes:
label: Is your feature request related to a problem?
description: A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
- type: textarea
id: additional-context
attributes:
label: Additional context
description: Add any other context or screenshots about the feature request here.
- type: markdown
attributes:
value: |
**Disclaimer**
Please note that this feature request is at the discretion of the repository owner, @saicaca, and its implementation is not guaranteed.

View file

@ -1,11 +0,0 @@
name: Custom Issue
description: Describe your issue here.
title: "[Other]: "
body:
- type: textarea
id: issue-description
attributes:
label: Issue Description
description: Please describe your issue.
validations:
required: true

View file

@ -1,22 +0,0 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
groups:
patch-updates:
patterns:
- "*"
update-types:
- "patch"
minor-updates:
patterns:
- "*"
update-types:
- "minor"
pull-request-branch-name:
separator: "-"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major"]

View file

@ -1,37 +0,0 @@
## Type of change
- [ ] Bug fix (a non-breaking change that fixes an issue)
- [ ] New feature (a non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Other (please describe):
## Checklist
- [ ] I have read the [**CONTRIBUTING**](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md) document.
- [ ] I have checked to ensure that this Pull Request is not for personal changes.
- [ ] I have performed a self-review of my own code.
- [ ] My changes generate no new warnings.
## Related Issue
<!-- Please link to the issue that this pull request addresses. e.g. #123 -->
## Changes
<!-- Please describe the changes you made in this pull request. -->
## How To Test
<!-- Please describe how you tested your changes. -->
## Screenshots (if applicable)
<!-- If you made any UI changes, please include screenshots. -->
## Additional Notes
<!-- Any additional information that you want to share with the reviewer. -->

View file

@ -1,20 +0,0 @@
name: Code quality
on:
push:
branches: [ main ] # Adjust branches as needed
pull_request:
branches: [ main ] # Adjust branches as needed
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup Biome
uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0
with:
version: latest
- name: Run Biome
run: biome ci ./src --reporter=github

View file

@ -1,67 +0,0 @@
name: Build and Check
on:
push:
branches: [ main ] # Adjust branches as needed
pull_request:
branches: [ main ] # Adjust branches as needed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
check:
strategy:
matrix:
node: [ 22, 23 ]
runs-on: ubuntu-latest
name: Astro Check for Node.js ${{ matrix.node }}
steps:
- name: Setup Node.js
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: ${{ matrix.node }} # Use LTS
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
run_install: false # Disable auto-install
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Astro Check
run: pnpm astro check
build:
strategy:
matrix:
node: [ 22, 23 ]
runs-on: ubuntu-latest
name: Astro Build for Node.js ${{ matrix.node }} # Corrected job name
steps:
- name: Setup Node.js
uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0
with:
node-version: ${{ matrix.node }}
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Setup pnpm
uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
with:
run_install: false # Disable auto-install
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run Astro Build
run: pnpm astro build

4
.gitignore vendored
View file

@ -25,7 +25,3 @@ pnpm-debug.log*
package-lock.json package-lock.json
bun.lockb bun.lockb
yarn.lock yarn.lock
# ide
.idea
*.iml

View file

@ -1,21 +0,0 @@
# Contributing
Thank you for your interest in contributing!
## Before You Start
If you plan to make major changes (especially new features or design changes), please open an issue or discussion before starting work. This helps ensure your effort aligns with the project's direction.
## Submitting Code
Please keep each pull request focused on a single purpose. Avoid mixing unrelated changes in one PR, as this can make reviewing and merging code more difficult.
Please use the [Conventional Commits](https://www.conventionalcommits.org/) format for your commit messages whenever possible. This keeps our history clear and consistent.
Before submitting code, please run the appropriate commands to check for errors and format your code.
```bash
pnpm check
pnpm format
```

View file

@ -2,7 +2,10 @@
Un tema estático para blogs construido con [Astro](https://astro.build). Un tema estático para blogs construido con [Astro](https://astro.build).
[**🖥️ Demostración en Vivo (Vercel)**](https://fuwari.vercel.app) [**🖥️ Demostración en Vivo (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦 Versión Antigua de Hexo**](https://github.com/saicaca/hexo-theme-vivia)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
> Versión del README: `2024-04-07`
![Imagen de Vista Previa](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png) ![Imagen de Vista Previa](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
@ -15,39 +18,9 @@ Un tema estático para blogs construido con [Astro](https://astro.build).
- [x] Diseño responsivo - [x] Diseño responsivo
- [ ] Comentarios - [ ] Comentarios
- [x] Buscador - [x] Buscador
- [x] TOC (Tabla de Contenidos) - [ ] TOC (Tabla de Contenidos)
## 👀 requiere ## 🚀 Cómo Usar
- 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. 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. 2. Para editar tu blog localmente, clona tu repositorio, ejecuta `pnpm install` y `pnpm add sharp` para instalar las dependencias.

View file

@ -2,7 +2,10 @@
[Astro](https://astro.build) で構築された静的ブログテンプレート [Astro](https://astro.build) で構築された静的ブログテンプレート
[**🖥️ライブデモ (Vercel)**](https://fuwari.vercel.app) [**🖥️ライブデモ (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦旧 Hexo バージョン**](https://github.com/saicaca/hexo-theme-vivia)
> README バージョン:`2024-04-07`
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png) ![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
@ -15,39 +18,9 @@
- [x] レスポンシブデザイン - [x] レスポンシブデザイン
- [ ] コメント機能 - [ ] コメント機能
- [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をします。 1. [テンプレート](https://github.com/saicaca/fuwari/generate)から新しいリポジトリを作成するかCloneをします。
2. ブログをローカルで編集するには、リポジトリをクローンした後、`pnpm install` と `pnpm add sharp` を実行して依存関係をインストールします。 2. ブログをローカルで編集するには、リポジトリをクローンした後、`pnpm install` と `pnpm add sharp` を実行して依存関係をインストールします。

57
README.ko.md Normal file
View file

@ -0,0 +1,57 @@
# 🍥Fuwari
[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)
> README 버전: `2024-04-07`
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ 특징
- [x] [Astro](https://astro.build) 및 [Tailwind CSS](https://tailwindcss.com)로 구축됨
- [x] 부드러운 애니메이션 및 페이지 전환
- [x] 라이트 모드 / 다크 모드
- [x] 사용자 정의 가능한 테마 색상 및 배너
- [x] 반응형 디자인
- [ ] 댓글
- [x] 검색
- [ ] 목차
## 🚀 사용하는 방법
1. 이 템플릿에서 [새 저장소를 생성](https://github.com/saicaca/fuwari/generate)하거나 이 저장소를 포크하세요.
2. 블로그를 로컬에서 편집하려면 저장소를 복제하고 `pnpm install``pnpm add sharp`를 실행하여 종속성을 설치하세요.
- 아직 [pnpm](https://pnpm.io)을 설치하지 않았다면 `npm install -g pnpm`을 실행하여 [pnpm](https://pnpm.io)을 설치하세요.
3. 블로그를 사용자 정의하려면 `src/config.ts` 구성 파일을 편집하세요.
4. `pnpm new-post <filename>`을 실행하여 새 게시물을 만들고 `src/content/posts/`에서 편집하세요.
5. [가이드](https://docs.astro.build/en/guides/deploy/)에 따라 블로그를 Vercel, Netlify, GitHub 페이지 등에 배포하세요. 배포하기 전에 `astro.config.mjs`에서 사이트 구성을 편집해야 합니다.
## ⚙️ 게시물의 머리말 설정
```yaml
---
title: 내 첫 블로그 게시물
published: 2023-09-09
description: 내 새로운 Astro 블로그의 첫 번째 게시물입니다!
image: /images/cover.jpg
tags: [푸, 바, 오]
category: 앞-끝
draft: false
---
```
## 🧞 명령어
모든 명령어는 프로젝트 최상단, 터미널에서 실행됩니다:
| Command | Action |
|:------------------------------------|:-------------------------------------------------|
| `pnpm install` AND `pnpm add sharp` | 종속성을 설치합니다. |
| `pnpm dev` | `localhost:4321`에서 로컬 개발 서버를 시작합니다. |
| `pnpm build` | `./dist/`에 프로덕션 사이트를 구축합니다. |
| `pnpm preview` | 배포하기 전에 로컬에서 빌드 미리보기 |
| `pnpm new-post <filename>` | 새 게시물 작성 |
| `pnpm astro ...` | `astro add`, `astro check`와 같은 CLI 명령어 실행 |
| `pnpm astro --help` | Astro CLI를 사용하여 도움 받기 |

View file

@ -1,24 +1,19 @@
# 🍥Fuwari # 🍥Fuwari
![Node.js >= 20](https://img.shields.io/badge/node.js-%3E%3D20-brightgreen)
![pnpm >= 9](https://img.shields.io/badge/pnpm-%3E%3D9-blue)
[![DeepWiki](https://img.shields.io/badge/DeepWiki-saicaca%2Ffuwari-blue.svg?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACwAAAAyCAYAAAAnWDnqAAAAAXNSR0IArs4c6QAAA05JREFUaEPtmUtyEzEQhtWTQyQLHNak2AB7ZnyXZMEjXMGeK/AIi+QuHrMnbChYY7MIh8g01fJoopFb0uhhEqqcbWTp06/uv1saEDv4O3n3dV60RfP947Mm9/SQc0ICFQgzfc4CYZoTPAswgSJCCUJUnAAoRHOAUOcATwbmVLWdGoH//PB8mnKqScAhsD0kYP3j/Yt5LPQe2KvcXmGvRHcDnpxfL2zOYJ1mFwrryWTz0advv1Ut4CJgf5uhDuDj5eUcAUoahrdY/56ebRWeraTjMt/00Sh3UDtjgHtQNHwcRGOC98BJEAEymycmYcWwOprTgcB6VZ5JK5TAJ+fXGLBm3FDAmn6oPPjR4rKCAoJCal2eAiQp2x0vxTPB3ALO2CRkwmDy5WohzBDwSEFKRwPbknEggCPB/imwrycgxX2NzoMCHhPkDwqYMr9tRcP5qNrMZHkVnOjRMWwLCcr8ohBVb1OMjxLwGCvjTikrsBOiA6fNyCrm8V1rP93iVPpwaE+gO0SsWmPiXB+jikdf6SizrT5qKasx5j8ABbHpFTx+vFXp9EnYQmLx02h1QTTrl6eDqxLnGjporxl3NL3agEvXdT0WmEost648sQOYAeJS9Q7bfUVoMGnjo4AZdUMQku50McDcMWcBPvr0SzbTAFDfvJqwLzgxwATnCgnp4wDl6Aa+Ax283gghmj+vj7feE2KBBRMW3FzOpLOADl0Isb5587h/U4gGvkt5v60Z1VLG8BhYjbzRwyQZemwAd6cCR5/XFWLYZRIMpX39AR0tjaGGiGzLVyhse5C9RKC6ai42ppWPKiBagOvaYk8lO7DajerabOZP46Lby5wKjw1HCRx7p9sVMOWGzb/vA1hwiWc6jm3MvQDTogQkiqIhJV0nBQBTU+3okKCFDy9WwferkHjtxib7t3xIUQtHxnIwtx4mpg26/HfwVNVDb4oI9RHmx5WGelRVlrtiw43zboCLaxv46AZeB3IlTkwouebTr1y2NjSpHz68WNFjHvupy3q8TFn3Hos2IAk4Ju5dCo8B3wP7VPr/FGaKiG+T+v+TQqIrOqMTL1VdWV1DdmcbO8KXBz6esmYWYKPwDL5b5FA1a0hwapHiom0r/cKaoqr+27/XcrS5UwSMbQAAAABJRU5ErkJggg==)](https://deepwiki.com/saicaca/fuwari)
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari.svg?type=shield&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari?ref=badge_shield&issueType=license)
A static blog template built with [Astro](https://astro.build). A static blog template built with [Astro](https://astro.build).
[**🖥️ Live Demo (Vercel)**](https://fuwari.vercel.app) [**🖥️ Live Demo (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦 Old Hexo Version**](https://github.com/saicaca/hexo-theme-vivia)&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;
[**🌏 한국어**](https://github.com/saicaca/fuwari/blob/main/README.ko.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 version: `2024-09-10`
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png) ![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
🌏 README in
[**中文**](https://github.com/saicaca/fuwari/blob/main/docs/README.zh-CN.md) /
[**日本語**](https://github.com/saicaca/fuwari/blob/main/docs/README.ja.md) /
[**한국어**](https://github.com/saicaca/fuwari/blob/main/docs/README.ko.md) /
[**Español**](https://github.com/saicaca/fuwari/blob/main/docs/README.es.md) /
[**ไทย**](https://github.com/saicaca/fuwari/blob/main/docs/README.th.md) /
[**Tiếng Việt**](https://github.com/saicaca/fuwari/blob/main/docs/README.vi.md) /
[**Bahasa Indonesia**](https://github.com/saicaca/fuwari/blob/main/docs/README.id.md) (Provided by the community and may not always be up-to-date)
## ✨ Features ## ✨ Features
- [x] Built with [Astro](https://astro.build) and [Tailwind CSS](https://tailwindcss.com) - [x] Built with [Astro](https://astro.build) and [Tailwind CSS](https://tailwindcss.com)
@ -26,30 +21,20 @@ A static blog template built with [Astro](https://astro.build).
- [x] Light / dark mode - [x] Light / dark mode
- [x] Customizable theme colors & banner - [x] Customizable theme colors & banner
- [x] Responsive design - [x] Responsive design
- [x] Search functionality with [Pagefind](https://pagefind.app/) - [ ] Comments
- [x] [Markdown extended features](https://github.com/saicaca/fuwari?tab=readme-ov-file#-markdown-extended-syntax) - [x] Search
- [x] Table of contents - [ ] TOC
- [x] RSS feed
## 🚀 Getting Started ## 🚀 How to Use
1. Create your blog repository: 1. [Generate a new repository](https://github.com/saicaca/fuwari/generate) from this template or fork this repository.
- [Generate a new repository](https://github.com/saicaca/fuwari/generate) from this template or fork this repository. 2. To edit your blog locally, clone your repository, run `pnpm install` AND `pnpm add sharp` to install dependencies.
- Or run one of the following commands: - Install [pnpm](https://pnpm.io) `npm install -g pnpm` if you haven't.
```sh
npm create fuwari@latest
yarn create fuwari
pnpm create fuwari@latest
bun create fuwari@latest
deno run -A npm:create-fuwari@latest
```
2. To edit your blog locally, clone your repository, run `pnpm install` to install dependencies.
- Install [pnpm](https://pnpm.io) `npm install -g pnpm` if you haven't.
3. Edit the config file `src/config.ts` to customize your blog. 3. Edit the config file `src/config.ts` to customize your blog.
4. Run `pnpm new-post <filename>` to create a new post and edit it in `src/content/posts/`. 4. Run `pnpm new-post <filename>` to create a new post and edit it in `src/content/posts/`.
5. 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. 5. 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.
## 📝 Frontmatter of Posts ## ⚙️ Frontmatter of Posts
```yaml ```yaml
--- ---
@ -64,36 +49,16 @@ lang: jp # Set only if the post's language differs from the site's language
--- ---
``` ```
## 🧩 Markdown Extended Syntax ## 🧞 Commands
In addition to Astro's default support for [GitHub Flavored Markdown](https://github.github.com/gfm/), several extra Markdown features are included:
- Admonitions ([Preview and Usage](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- GitHub repository cards ([Preview and Usage](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- Enhanced code blocks with Expressive Code ([Preview](https://fuwari.vercel.app/posts/expressive-code/) / [Docs](https://expressive-code.com/))
## ⚡ Commands
All commands are run from the root of the project, from a terminal: All commands are run from the root of the project, from a terminal:
| Command | Action | | Command | Action |
|:---------------------------|:----------------------------------------------------| |:------------------------------------|:-------------------------------------------------|
| `pnpm install` | Installs dependencies | | `pnpm install` AND `pnpm add sharp` | Installs dependencies |
| `pnpm dev` | Starts local dev server at `localhost:4321` | | `pnpm dev` | Starts local dev server at `localhost:4321` |
| `pnpm build` | Build your production site to `./dist/` | | `pnpm build` | Build your production site to `./dist/` |
| `pnpm preview` | Preview your build locally, before deploying | | `pnpm preview` | Preview your build locally, before deploying |
| `pnpm check` | Run checks for errors in your code | | `pnpm new-post <filename>` | Create a new post |
| `pnpm format` | Format your code using Biome | | `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
| `pnpm new-post <filename>` | Create a new post | | `pnpm astro --help` | Get help using the Astro CLI |
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
| `pnpm astro --help` | Get help using the Astro CLI |
## ✏️ Contributing
Check out the [Contributing Guide](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md) for details on how to contribute to this project.
## 📄 License
This project is licensed under the MIT License.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari?ref=badge_large&issueType=license)

59
README.th.md Normal file
View file

@ -0,0 +1,59 @@
# 🍥Fuwari
แม่แบบสำหรับเว็บบล็อกแบบ static สร้างด้วย [Astro](https://astro.build)
[**🖥️ ตัวอย่างการใช้งานจริง (Vercel)**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦 เวอร์ชั่นเก่าสำหรับ Hexo**](https://github.com/saicaca/hexo-theme-vivia)
> เวอร์ชั่นของ README: `2024-09-10`
![ภาพตัวอย่าง](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ คุณสมบัติ
- [x] สร้างด้วย [Astro](https://astro.build) และ [Tailwind CSS](https://tailwindcss.com)
- [x] มีอนิเมชั่นและการเปลี่ยนหน้าอย่างลื่นไหล
- [x] รองรับโหมดสว่าง / โหมดมืด
- [x] ปรับแต่งสีธีมและแบนเนอร์ได้
- [x] Responsive design (หน้าตาเว็บปรับเปลี่ยนตามขนาดจอ)
- [ ] การแสดงความคิดเห็น
- [x] การค้นหา
- [ ] TOC (สารบัญ)
## 🚀 วิธีใช้งาน
1. [Generate repository ใหม่](https://github.com/saicaca/fuwari/generate)ขึ้นมาจากแม่แบบนี้ หรือจะ fork repository นี้ก็ได้
2. เริ่มแก้ไขบล็อกของคุณแบบ local โดยการ clone repository ของคุณ (จากข้อ 1) ไว้ในเครื่องของคุณ แล้วรันคำสั่ง `pnpm install` และ `pnpm add sharp` เพื่อติดตั้ง dependencies ที่จำเป็น
- ติดตั้ง [pnpm](https://pnpm.io) ด้วยคำสั่ง `npm install -g pnpm` ถ้ายังไม่เคยติดตั้ง
3. แก้ไขไฟล์การตั้งค่า `src/config.ts` เพื่อปรับแต่งบล็อกของคุณ
4. รันคำสั่ง `pnpm new-post <filename>` เพื่อสร้างโพสต์ใหม่ใน `src/content/posts/` และแก้ไขไฟล์โพสต์นั้นๆ ให้สมบูรณ์
5. Deploy เว็บบล็อกของคุณไปยัง Vercel, Netlify, GitHub Pages หรือบริการอื่นๆ โดยอ้างอิงวิธีการจาก[คู่มือนี้](https://docs.astro.build/en/guides/deploy/) อย่าลืมแก้ไขการตั้งค่าเว็บไซต์ในไฟล์ `astro.config.mjs` ก่อนที่คุณจะ deploy เว็บ
## ⚙️ Frontmatter ของโพสต์
```yaml
---
title: โพสต์แรกของฉัน
published: 2023-09-09
description: นี่คือโพสต์แรกของเว็บบล็อก Astro อันใหม่ของฉัน
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: jp # เขียนค่านี้เมื่อภาษาของโพสต์นั้นๆ แตกต่างจากภาษาของเว็บไซต์ที่ตั้งค่าไว้ใน `config.ts` เท่านั้น
---
```
## 🧞 คำสั่ง
คำสั่งที่รันได้ใน terminal จาก root ของโปรเจค:
| คำสั่ง | ผลที่เกิด |
|:------------------------------------|:--------------------------------------------------|
| `pnpm install` และ `pnpm add sharp` | ติดตั้ง dependencies |
| `pnpm dev` | เปิดเซิร์ฟเวอร์เพื่อพัฒนาเว็บแบบ local ที่ `localhost:4321` |
| `pnpm build` | Build เว็บไซต์แบบพร้อมใช้งานจริงไปยังโฟลเดอร์ `./dist/` |
| `pnpm preview` | ดูตัวอย่าง build ของคุณแบบ local ก่อนที่จะ deploy จริง |
| `pnpm new-post <filename>` | สร้างโพสต์ใหม่ |
| `pnpm astro ...` | รันคำสั่ง CLI เช่น `astro add`, `astro check` |
| `pnpm astro --help` | ดูข้อมูลเพิ่มเติมเกี่ยวกับ Astro CLI |

View file

@ -2,7 +2,10 @@
基于 [Astro](https://astro.build) 开发的静态博客模板。 基于 [Astro](https://astro.build) 开发的静态博客模板。
[**🖥在线预览Vercel**](https://fuwari.vercel.app) [**🖥在线预览Vercel**](https://fuwari.vercel.app)&nbsp;&nbsp;&nbsp;/&nbsp;&nbsp;&nbsp;
[**📦旧 Hexo 版本**](https://github.com/saicaca/hexo-theme-vivia)
> README 版本:`2024-09-10`
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png) ![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
@ -15,39 +18,9 @@
- [x] 响应式设计 - [x] 响应式设计
- [ ] 评论 - [ ] 评论
- [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 此仓库 1. 使用此模板[生成新仓库](https://github.com/saicaca/fuwari/generate)或 Fork 此仓库
2. 进行本地开发Clone 新的仓库,执行 `pnpm install``pnpm add sharp` 以安装依赖 2. 进行本地开发Clone 新的仓库,执行 `pnpm install``pnpm add sharp` 以安装依赖

View file

@ -1,10 +1,8 @@
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import svelte from "@astrojs/svelte"; import svelte from "@astrojs/svelte";
import tailwind from "@astrojs/tailwind"; import tailwind from "@astrojs/tailwind";
import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections";
import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers";
import swup from "@swup/astro"; import swup from "@swup/astro";
import expressiveCode from "astro-expressive-code"; import Compress from "astro-compress";
import icon from "astro-icon"; import icon from "astro-icon";
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import rehypeAutolinkHeadings from "rehype-autolink-headings"; import rehypeAutolinkHeadings from "rehype-autolink-headings";
@ -15,158 +13,120 @@ import remarkDirective from "remark-directive"; /* Handle directives */
import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-directives"; import remarkGithubAdmonitionsToDirectives from "remark-github-admonitions-to-directives";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import remarkSectionize from "remark-sectionize"; import remarkSectionize from "remark-sectionize";
import { expressiveCodeConfig } from "./src/config.ts";
import { pluginLanguageBadge } from "./src/plugins/expressive-code/language-badge.ts";
import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs"; import { AdmonitionComponent } from "./src/plugins/rehype-component-admonition.mjs";
import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs"; import { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs";
import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js"; import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js";
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js"; import { remarkExcerpt } from "./src/plugins/remark-excerpt.js";
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"; import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: "https://fuwari.vercel.app/", site: "https://blog.juyung.com",
base: "/", base: "/",
trailingSlash: "always", trailingSlash: "always",
integrations: [ integrations: [
tailwind({ tailwind(
nesting: true, {
}), nesting: true,
swup({ }
theme: false, ),
animationClass: "transition-swup-", // see https://swup.js.org/options/#animationselector swup({
// the default value `transition-` cause transition delay theme: false,
// when the Tailwind class `transition-all` is used animationClass: "transition-swup-", // see https://swup.js.org/options/#animationselector
containers: ["main", "#toc"], // the default value `transition-` cause transition delay
smoothScrolling: true, // when the Tailwind class `transition-all` is used
cache: true, containers: ["main", "#toc"],
preload: true, smoothScrolling: true,
accessibility: true, cache: true,
updateHead: true, preload: true,
updateBodyClass: false, accessibility: true,
globalInstance: true, updateHead: true,
}), updateBodyClass: false,
icon({ globalInstance: true,
include: { }),
"preprocess: vitePreprocess(),": ["*"], icon({
"fa6-brands": ["*"], include: {
"fa6-regular": ["*"], "preprocess: vitePreprocess(),": ["*"],
"fa6-solid": ["*"], "fa6-brands": ["*"],
}, "fa6-regular": ["*"],
}), "fa6-solid": ["*"],
expressiveCode({ },
themes: [expressiveCodeConfig.theme, expressiveCodeConfig.theme], }),
plugins: [ svelte(),
pluginCollapsibleSections(), sitemap(),
pluginLineNumbers(), Compress({
pluginLanguageBadge(), CSS: false,
pluginCustomCopyButton() Image: false,
], Action: {
defaultProps: { Passed: async () => true, // https://github.com/PlayForm/Compress/issues/376
wrap: true, },
overridesByLang: { }),
'shellsession': { ],
showLineNumbers: false, markdown: {
}, remarkPlugins: [
}, remarkMath,
}, remarkReadingTime,
styleOverrides: { remarkExcerpt,
codeBackground: "var(--codeblock-bg)", remarkGithubAdmonitionsToDirectives,
borderRadius: "0.75rem", remarkDirective,
borderColor: "none", remarkSectionize,
codeFontSize: "0.875rem", parseDirectiveNode,
codeFontFamily: "'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", ],
codeLineHeight: "1.5rem", rehypePlugins: [
frames: { rehypeKatex,
editorBackground: "var(--codeblock-bg)", rehypeSlug,
terminalBackground: "var(--codeblock-bg)", [
terminalTitlebarBackground: "var(--codeblock-topbar-bg)", rehypeComponents,
editorTabBarBackground: "var(--codeblock-topbar-bg)", {
editorActiveTabBackground: "none", components: {
editorActiveTabIndicatorBottomColor: "var(--primary)", github: GithubCardComponent,
editorActiveTabIndicatorTopColor: "none", note: (x, y) => AdmonitionComponent(x, y, "note"),
editorTabBarBorderBottomColor: "var(--codeblock-topbar-bg)", tip: (x, y) => AdmonitionComponent(x, y, "tip"),
terminalTitlebarBorderBottomColor: "none" important: (x, y) => AdmonitionComponent(x, y, "important"),
}, caution: (x, y) => AdmonitionComponent(x, y, "caution"),
textMarkers: { warning: (x, y) => AdmonitionComponent(x, y, "warning"),
delHue: 0, },
insHue: 180, },
markHue: 250 ],
} [
}, rehypeAutolinkHeadings,
frames: { {
showCopyToClipboardButton: false, behavior: "append",
} properties: {
}), className: ["anchor"],
svelte(), },
sitemap(), content: {
], type: "element",
markdown: { tagName: "span",
remarkPlugins: [ properties: {
remarkMath, className: ["anchor-icon"],
remarkReadingTime, "data-pagefind-ignore": true,
remarkExcerpt, },
remarkGithubAdmonitionsToDirectives, children: [
remarkDirective, {
remarkSectionize, type: "text",
parseDirectiveNode, value: "#",
], },
rehypePlugins: [ ],
rehypeKatex, },
rehypeSlug, },
[ ],
rehypeComponents, ],
{ },
components: { vite: {
github: GithubCardComponent, build: {
note: (x, y) => AdmonitionComponent(x, y, "note"), rollupOptions: {
tip: (x, y) => AdmonitionComponent(x, y, "tip"), onwarn(warning, warn) {
important: (x, y) => AdmonitionComponent(x, y, "important"), // temporarily suppress this warning
caution: (x, y) => AdmonitionComponent(x, y, "caution"), if (
warning: (x, y) => AdmonitionComponent(x, y, "warning"), warning.message.includes("is dynamically imported by") &&
}, warning.message.includes("but also statically imported by")
}, ) {
], return;
[ }
rehypeAutolinkHeadings, warn(warning);
{ },
behavior: "append", },
properties: { },
className: ["anchor"], },
},
content: {
type: "element",
tagName: "span",
properties: {
className: ["anchor-icon"],
"data-pagefind-ignore": true,
},
children: [
{
type: "text",
value: "#",
},
],
},
},
],
],
},
vite: {
build: {
rollupOptions: {
onwarn(warning, warn) {
// temporarily suppress this warning
if (
warning.message.includes("is dynamically imported by") &&
warning.message.includes("but also statically imported by")
) {
return;
}
warn(warning);
},
},
},
},
}); });

View file

@ -1,63 +1,66 @@
{ {
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json", "$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
"vcs": { "extends": [],
"enabled": false, "files": { "ignoreUnknown": true },
"clientKind": "git", "organizeImports": {
"useIgnoreFile": false "enabled": true
}, },
"files": { "formatter": {
"ignoreUnknown": false, "enabled": true,
"includes": [ "formatWithErrors": false,
"**", "ignore": ["src/config.ts"],
"!**/src/**/*.css", "indentStyle": "space",
"!**/src/public/**/*", "indentWidth": 2,
"!**/dist/**/*", "lineWidth": 80
"!**/node_modules/**/*" },
] "javascript": {
}, "parser": {
"formatter": { "unsafeParameterDecoratorsEnabled": true
"enabled": true, },
"indentStyle": "tab" "formatter": {
}, "quoteStyle": "single",
"assist": { "actions": { "source": { "organizeImports": "on" } } }, "jsxQuoteStyle": "single",
"linter": { "trailingCommas": "all",
"enabled": true, "semicolons": "asNeeded",
"rules": { "arrowParentheses": "asNeeded"
"recommended": true, }
"style": { },
"noParameterAssign": "error", "json": {
"useAsConstAssertion": "error", "parser": { "allowComments": true },
"useDefaultParameterLast": "error", "formatter": {
"useEnumInitializers": "error", "enabled": true,
"useSelfClosingElements": "error", "indentStyle": "space",
"useSingleVarDeclarator": "error", "indentWidth": 2,
"noUnusedTemplateLiteral": "error", "lineWidth": 80
"useNumberNamespace": "error", }
"noInferrableTypes": "error", },
"noUselessElse": "error" "linter": {
} "ignore": [],
} "rules": {
}, "a11y": {
"javascript": { "recommended": true
"formatter": { },
"quoteStyle": "double" "complexity": {
} "recommended": true
}, },
"overrides": [ "correctness": {
{ "recommended": true
"includes": ["**/*.svelte", "**/*.astro", "**/*.vue"], },
"linter": { "performance": {
"rules": { "recommended": true
"style": { },
"useConst": "off", "security": {
"useImportType": "off" "recommended": true
}, },
"correctness": { "style": {
"noUnusedVariables": "off", "recommended": true
"noUnusedImports": "off" },
} "suspicious": {
} "recommended": true
} },
} "nursery": {
] "recommended": true
}
}
}
} }

View file

@ -1,106 +0,0 @@
# 🍥 Fuwari
Template blog statis yang dibangun dengan [Astro](https://astro.build).
[**🖥️ Demo Langsung (Vercel)**](https://fuwari.vercel.app)
![Gambar Pratinjau](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
🌏 README dalam
[**中文**](https://github.com/saicaca/fuwari/blob/main/docs/README.zh-CN.md) /
[**日本語**](https://github.com/saicaca/fuwari/blob/main/docs/README.ja.md) /
[**한국어**](https://github.com/saicaca/fuwari/blob/main/docs/README.ko.md) /
[**Español**](https://github.com/saicaca/fuwari/blob/main/docs/README.es.md) /
[**ไทย**](https://github.com/saicaca/fuwari/blob/main/docs/README.th.md) /
[**Tiếng Việt**](https://github.com/saicaca/fuwari/blob/main/docs/README.vi.md) /
**Bahasa Indonesia (ini)** (Disediakan oleh komunitas, mungkin tidak selalu paling mutakhir)
## ✨ Fitur
- [x] Dibangun dengan [Astro](https://astro.build) dan [Tailwind CSS](https://tailwindcss.com)
- [x] Animasi dan transisi halaman yang halus
- [x] Mode terang / gelap
- [x] Warna tema & banner yang bisa dikustomisasi
- [x] Desain responsif
- [x] Fitur pencarian dengan [Pagefind](https://pagefind.app/)
- [x] [Fitur markdown tambahan](#-markdown-sintaks-ekstensi)
- [x] Daftar isi (Table of Contents)
- [x] RSS feed
## 🚀 Memulai
1. Buat repositori blog kamu:
- [Generate repositori baru](https://github.com/saicaca/fuwari/generate) dari template ini atau fork repositori ini.
- Atau jalankan salah satu perintah berikut:
```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
```
2. Untuk mengedit blog secara lokal, klon repositori kamu, jalankan `pnpm install` untuk instalasi dependensi.
- Install [pnpm](https://pnpm.io) `npm install -g pnpm` jika belum punya.
3. Edit file konfigurasi `src/config.ts` untuk menyesuaikan blog.
4. Jalankan `pnpm new-post <nama-file>` untuk membuat postingan baru dan edit di `src/content/posts/`.
5. Deploy blog ke Vercel, Netlify, GitHub Pages, dll. sesuai [panduan](https://docs.astro.build/en/guides/deploy/). Jangan lupa edit konfigurasi situs di `astro.config.mjs` sebelum deploy.
## 📝 Frontmatter Postingan
```yaml
---
title: Judul Postingan Pertama Saya
published: 2023-09-09
description: Ini adalah postingan pertama blog Astro saya.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: id # Isi hanya jika bahasa postingan berbeda dari bahasa default di `config.ts`
---
```
## 🧩 Markdown Sintaks Ekstensi
Selain dukungan default Astro untuk [GitHub Flavored Markdown](https://github.github.com/gfm/), terdapat beberapa fitur tambahan:
- Admonisi ([Pratinjau & Cara Pakai](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- Kartu repositori GitHub ([Pratinjau & Cara Pakai](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- Kode blok ekspresif lewat Expressive Code ([Pratinjau](https://fuwari.vercel.app/posts/expressive-code/) / [Dokumentasi](https://expressive-code.com/))
## ⚡ Perintah
Semua perintah dijalankan dari root proyek, via terminal:
| Perintah | Aksi |
|:-----------------------------|:----------------------------------------------------------|
| `pnpm install` | Instalasi dependensi |
| `pnpm dev` | Menjalankan server dev lokal di `localhost:4321` |
| `pnpm build` | Build untuk produksi ke folder `./dist/` |
| `pnpm preview` | Pratinjau hasil build sebelum deploy |
| `pnpm check` | Cek error atau masalah di kode |
| `pnpm format` | Format kode dengan Biome |
| `pnpm new-post <nama-file>` | Membuat postingan baru |
| `pnpm astro ...` | Jalankan perintah CLI seperti `astro add`, `astro check` |
| `pnpm astro --help` | Bantuan menggunakan Astro CLI |
## ✏️ Kontribusi
Lihat [Panduan Kontribusi](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md) untuk detail tentang cara berkontribusi ke proyek ini.
## 📄 Lisensi
Proyek ini dilisensikan di bawah MIT License.
---
> Dokumentasi ini tersedia dalam Bahasa Indonesia. Untuk bahasa lain, lihat README di direktori docs.

View file

@ -1,82 +0,0 @@
# 🍥Fuwari
[Astro](https://astro.build)로 구축된 정적 블로그 템플릿입니다.
[**🖥️미리보기 (Vercel)**](https://fuwari.vercel.app)
![Preview Image](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ 특징
- [x] [Astro](https://astro.build) 및 [Tailwind CSS](https://tailwindcss.com)로 구축됨
- [x] 부드러운 애니메이션 및 페이지 전환
- [x] 라이트 모드 / 다크 모드
- [x] 사용자 정의 가능한 테마 색상 및 배너
- [x] 반응형 디자인
- [x] [Pagefind](https://pagefind.app/)를 이용한 검색 기능
- [x] [Markdown 확장 기능](https://github.com/saicaca/fuwari?tab=readme-ov-file#-markdown-extended-syntax)
- [x] 목차
- [x] RSS 피드
## 🚀 시작하기
1. 블로그 저장소를 생성하세요:
- 이 템플릿에서 [새 저장소를 생성](https://github.com/saicaca/fuwari/generate)하거나 이 저장소를 포크하세요.
- 또는 다음 명령어 중 하나를 실행하세요:
```sh
npm create fuwari@latest
yarn create fuwari
pnpm create fuwari@latest
bun create fuwari@latest
deno run -A npm:create-fuwari@latest
```
2. 로컬에서 블로그를 수정하려면, 저장소를 복제하고 `pnpm install`을 실행하여 종속성을 설치하세요.
- [pnpm](https://pnpm.io)이 설치되어 있지 않다면 `npm install -g pnpm`을 실행하여 설치하세요.
3. `src/config.ts`설정 파일을 수정하여 블로그를 커스터마이징하세요.
4. `pnpm new-post <filename>`을 실행하여 새 게시물을 만들고 `src/content/posts/`에서 수정하세요.
5. [가이드](https://docs.astro.build/en/guides/deploy/)에 따라 블로그를 Vercel, Netlify, Github Pages 등에 배포하세요. 배포하기 전에 `astro.config.mjs`에서 사이트 구성을 수정해야 합니다.
## ⚙️ 게시물의 머리말 설정
```yaml
---
title: 내 첫 블로그 게시물
published: 2023-09-09
description: 내 새로운 Astro 블로그의 첫 번째 게시물입니다!
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: jp # 게시물의 언어가 `config.ts`의 사이트 언어와 다른 경우에만 설정합니다.
---
```
## 🧩 마크다운 확장 구문
Astro의 기본 [GitHub Flavored Markdown](https://github.github.com/gfm/) 지원 외에도 몇 가지 추가적인 마크다운 기능이 포함되어 있습니다.
- Admonitions ([미리보기 및 사용법](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- GitHub 저장소 카드 ([미리보기 및 사용법](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- Expressive Code를 사용한 향상된 코드 블록 ([미리보기](https://fuwari.vercel.app/posts/expressive-code/) / [문서](https://expressive-code.com/))
## ⚡ 명령어
모든 명령어는 프로젝트 최상단, 터미널에서 실행됩니다:
| Command | Action |
|:------------------------------------|:-------------------------------------------------|
| `pnpm install` | 종속성을 설치합니다. |
| `pnpm dev` | `localhost:4321`에서 로컬 개발 서버를 시작합니다. |
| `pnpm build` | `./dist/`에 프로덕션 사이트를 구축합니다. |
| `pnpm check` | 코드에서 오류를 확인합니다. |
| `pnpm format` | Biome을 사용하여 코드를 포멧합니다. |
| `pnpm preview` | 배포하기 전에 로컬에서 빌드 미리보기 |
| `pnpm new-post <filename>` | 새 게시물 작성 |
| `pnpm astro ...` | `astro add`, `astro check`와 같은 CLI 명령어 실행 |
| `pnpm astro --help` | Astro CLI를 사용하여 도움 받기 |
## ✏️ 기여
이 프로젝트에 기여하는 방법에 대한 자세한 내용은 [기여 가이드](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md)를 확인하세요.
## 📄 라이선스
이 프로젝트는 MIT 라이선스에 따라 라이선스가 부여됩니다.
[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari.svg?type=large&issueType=license)](https://app.fossa.com/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari?ref=badge_large&issueType=license)

View file

@ -1,84 +0,0 @@
# 🍥Fuwari
แม่แบบสำหรับเว็บบล็อกแบบ static สร้างด้วย [Astro](https://astro.build)
[**🖥️ ตัวอย่างการใช้งานจริง (Vercel)**](https://fuwari.vercel.app)
![ภาพตัวอย่าง](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ คุณสมบัติ
- [x] สร้างด้วย [Astro](https://astro.build) และ [Tailwind CSS](https://tailwindcss.com)
- [x] มีอนิเมชั่นและการเปลี่ยนหน้าอย่างลื่นไหล
- [x] โหมดสว่าง / โหมดมืด
- [x] ปรับแต่งสีธีมและแบนเนอร์ได้
- [x] Responsive design (หน้าตาเว็บปรับเปลี่ยนตามขนาดจอ)
- [x] ฟังก์ชันการค้นหา ขับเคลื่อนด้วย [Pagefind](https://pagefind.app/)
- [x] [คุณสมบัติเพิ่มเติมสำหรับมาร์กดาวน์](https://github.com/saicaca/fuwari/blob/main/docs/README.th.md#-markdown-extended-syntax)
- [x] สารบัญ
- [x] RSS feed
## 🚀 เริ่มต้นใช้งาน
1. สร้าง repository ใหม่สำหรับบล็อกของคุณ:
- [Generate repository ใหม่](https://github.com/saicaca/fuwari/generate) ขึ้นมาจากแม่แบบนี้ หรือจะ fork repository นี้ก็ได้
- หรือจะสร้างโดยการเลือกรันคำสั่งต่อไปนี้ คำสั่งใดคำสั่งหนึ่ง:
```sh
npm create fuwari@latest
yarn create fuwari
pnpm create fuwari@latest
bun create fuwari@latest
deno run -A npm:create-fuwari@latest
```
2. เริ่มแก้ไขบล็อกของคุณแบบ local โดยการ clone repository ของคุณ (จากข้อ 1) ไว้ในเครื่องของคุณ แล้วรันคำสั่ง `pnpm install` เพื่อติดตั้ง dependencies ที่จำเป็น
- ติดตั้ง [pnpm](https://pnpm.io) ด้วยคำสั่ง `npm install -g pnpm` ก่อน ถ้ายังไม่เคยติดตั้ง
3. แก้ไขไฟล์การตั้งค่า `src/config.ts` เพื่อปรับแต่งบล็อกของคุณ
4. รันคำสั่ง `pnpm new-post <filename>` เพื่อสร้างโพสต์ใหม่ใน `src/content/posts/` และแก้ไขไฟล์โพสต์นั้น ๆ ให้สมบูรณ์
5. Deploy เว็บบล็อกของคุณไปยัง Vercel, Netlify, GitHub Pages หรือบริการอื่น ๆ โดยอ้างอิงวิธีการจาก[คู่มือนี้](https://docs.astro.build/en/guides/deploy/) อย่าลืมแก้ไขการตั้งค่าเว็บไซต์ในไฟล์ `astro.config.mjs` ก่อนที่คุณจะ deploy เว็บ
## 📝 Frontmatter (ส่วนหัวไฟล์) ของโพสต์
```yaml
---
title: โพสต์แรกของฉัน
published: 2023-09-09
description: นี่คือโพสต์แรกของเว็บบล็อก Astro อันใหม่ของฉัน
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: jp # เขียนค่านี้เมื่อภาษาของโพสต์นั้น ๆ แตกต่างจากภาษาของเว็บไซต์ที่ตั้งค่าไว้ใน `config.ts` เท่านั้น
---
```
## 🧩 Markdown Extended Syntax
เดิมที Astro มีการสนับสนุน[ภาษามาร์กดาวน์แบบของ GitHub](https://github.github.com/gfm/) ไว้อยู่แล้ว แต่ Fuwari ได้เพิ่มเติมคุณสมบัติพิเศษอื่น ๆ เข้าไปอีก:
- Admonitions หรือ กล่องข้อมูลพิเศษ ([ดูตัวอย่างและการใช้งาน](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- การ์ด GitHub Repository ([ดูตัวอย่างและการใช้งาน](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- บล็อกโค้ดขั้นสูง ด้วย Expressive Code ([ดูตัวอย่าง](https://fuwari.vercel.app/posts/expressive-code/) / [เอกสารประกอบ](https://expressive-code.com/))
## ⚡ คำสั่ง
คำสั่งที่รันได้ใน terminal จาก root ของโปรเจกต์:
| คำสั่ง | การทำงาน |
|:---------------------------|:-------------------------------------------------------|
| `pnpm install` | ติดตั้ง dependencies |
| `pnpm dev` | เปิดเซิร์ฟเวอร์สำหรับการพัฒนาแบบ local ที่ `localhost:4321` |
| `pnpm build` | Build เว็บไซต์สำหรับใช้งานจริงไปยังโฟลเดอร์ `./dist/` |
| `pnpm preview` | ดูตัวอย่าง build ของคุณแบบ local ก่อนที่จะ deploy จริง |
| `pnpm check` | ดำเนินการตรวจสอบหาข้อผิดพลาดในโค้ดของคุณ |
| `pnpm format` | จัดรูปแบบโค้ดของคุณด้วย Biome |
| `pnpm new-post <filename>` | สร้างโพสต์ใหม่ |
| `pnpm astro ...` | รันคำสั่ง CLI เช่น `astro add`, `astro check` |
| `pnpm astro --help` | แสดงวิธีใช้งาน Astro CLI |
## ✏️ การมีส่วนร่วม
กรุณาอ่าน [แนวทางการมีส่วนร่วม](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md) สำหรับรายละเอียดวิธีการมีส่วนร่วมในโปรเจกต์นี้
## 📄 สัญญาอนุญาต
โปรเจกต์นี้เผยแพร่ภายใต้สัญญาอนุญาตแบบ MIT License

View file

@ -1,84 +0,0 @@
# 🍥Fuwari
Một mẫu blog tĩnh được xây bằng [Astro](https://astro.build).
[**🖥️ Xem bản dùng thử (Vercel)**](https://fuwari.vercel.app)
![Hình ảnh xem trước](https://raw.githubusercontent.com/saicaca/resource/main/fuwari/home.png)
## ✨ Tính năng
- [x] Được xây dựng bằng [Astro](https://astro.build) và [Tailwind CSS](https://tailwindcss.com)
- [x] Có hoạt ảnh đổi chuyển trang mượt mà
- [x] Chế độ sáng / tối
- [x] Màu sắc và biểu ngữ có thể tùy chỉnh được
- [x] Thiết kế nhanh nhạy
- [x] Có chức năng tìm kiếm với [Pagefind](https://pagefind.app/)
- [x] [Có các tính năng mở rộng của Markdown](https://github.com/saicaca/fuwari?tab=readme-ov-file#-markdown-extended-syntax)
- [x] Có mục lục
- [x] Nguồn cấp dữ liệu RSS
## 🚀 Bắt đầu
1. Tạo kho lưu trữ blog của bạn:
- [Tạo một kho lưu trữ mới](https://github.com/saicaca/fuwari/generate) từ mẫu này hoặc fork kho lưu trữ này.
- Hoặc chạy một trong các lệnh sau:
```sh
npm create fuwari@latest
yarn create fuwari
pnpm create fuwari@latest
bun create fuwari@latest
deno run -A npm:create-fuwari@latest
```
2. Để chỉnh sửa blog của bạn trên máy cục bộ, hãy clone kho lưu trữ của bạn, chạy lệnh `pnpm install` để cài đặt các phụ thuộc..
- Cài đặt [pnpm](https://pnpm.io) `npm install -g pnpm` nếu chưa có.
3. Chỉnh sửa tệp cấu hình `src/config.ts` để tùy chỉnh blog của bạn.
4. Chạy `pnpm new-post <filename>` để tạo một bài viết mới và chỉnh sửa nó trong `src/content/posts/`.
5. Triển khai blog của bạn lên Vercel, Netlify, GitHub Pages, etc. theo [chỉ dẫn](https://docs.astro.build/en/guides/deploy/). Bạn cần chỉnh sửa cấu hình trang web trong `astro.config.mjs` trước khi triển khai.
## 📝 Tiêu đề đầy đủ của bài viết
```yaml
---
title: Blog đầu tiên của mình
published: 2023-09-09
description: Đây là bài viết đầu tiên vủa mình trên trang blog tạo bằng Astro này.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
lang: jp # Chỉ đặt nếu ngôn ngữ của bài viết khác với ngôn ngữ của trang web trong `config.ts`
---
```
## 🧩 Cú pháp Markdown mở rộng
Ngoài việc Astro đã có hỗ trợ mặc định cho [Markdown vị Github](https://github.github.com/gfm/), một số tính năng Markdown khác cũng đã được bổ sung:
- Chêm xen ([Xem trước và Cách sử dụng](https://fuwari.vercel.app/posts/markdown-extended/#admonitions))
- Thẻ hiển thị kho lưu trữ GitHub ([Xem trước và Cách sử dụng](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- Các khối mã nâng cao với Expressive Code ([Xem trước](https://fuwari.vercel.app/posts/expressive-code/) / [Tài liệu](https://expressive-code.com/))
## ⚡ Lệnh
Tất cả các lệnh được chạy từ thư mục gốc của dự án, từ một bảng điều khiển:
| Lệnh | Mục đích |
|:---------------------------|:----------------------------------------------------|
| `pnpm install` | Cài đặt các phụ thuộc |
| `pnpm dev` | Khởi động máy chủ cục bộ tại `localhost:4321` |
| `pnpm build` | Xây dựng trang web của bạn vào `./dist/` |
| `pnpm preview` | Xem trước bản web cục bộ của bạn, trước khi triển khai |
| `pnpm check` | Chạy kiểm tra lỗi trong mã của bạn |
| `pnpm format` | Định dạng mã của bạn bằng Biome |
| `pnpm new-post <filename>` | Tạo một bài viết mới |
| `pnpm astro ...` | Chạy các lệnh CLI như `astro add`, `astro check` |
| `pnpm astro --help` | Nhận trợ giúp sử dụng Astro CLI |
## ✏️ Đóng góp
Xem [Hướng dẫn đóng góp](https://github.com/saicaca/fuwari/blob/main/CONTRIBUTING.md) để biết thêm chi tiết về cách đóng góp cho dự án này.
## 📄 Giấy phép
Dự án này đã được cấp Giấy phép MIT.

View file

@ -5,72 +5,67 @@
"scripts": { "scripts": {
"dev": "astro dev", "dev": "astro dev",
"start": "astro dev", "start": "astro dev",
"check": "astro check",
"build": "astro build && pagefind --site dist", "build": "astro build && pagefind --site dist",
"preview": "astro preview", "preview": "astro preview",
"astro": "astro", "astro": "astro",
"type-check": "tsc --noEmit --isolatedDeclarations", "type-check": "tsc --noEmit --isolatedDeclarations",
"new-post": "node scripts/new-post.js", "new-post": "node scripts/new-post.js",
"format": "biome format --write ./src", "format": "biome format --write ./src",
"lint": "biome check --write ./src", "lint": "biome check --apply ./src",
"preinstall": "npx only-allow pnpm" "preinstall": "npx only-allow pnpm"
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.6", "@astrojs/check": "^0.9.4",
"@astrojs/rss": "^4.0.14", "@astrojs/rss": "^4.0.9",
"@astrojs/sitemap": "^3.6.0", "@astrojs/sitemap": "^3.2.1",
"@astrojs/svelte": "7.2.3", "@astrojs/svelte": "^6.0.1",
"@astrojs/tailwind": "^6.0.2", "@astrojs/tailwind": "^5.1.2",
"@expressive-code/core": "^0.41.4", "@fontsource-variable/jetbrains-mono": "^5.1.1",
"@expressive-code/plugin-collapsible-sections": "^0.41.4", "@fontsource/roboto": "^5.1.0",
"@expressive-code/plugin-line-numbers": "^0.41.4", "@iconify-json/fa6-brands": "^1.2.1",
"@fontsource-variable/jetbrains-mono": "^5.2.8", "@iconify-json/fa6-regular": "^1.2.1",
"@fontsource/roboto": "^5.2.9", "@iconify-json/fa6-solid": "^1.2.1",
"@iconify-json/fa6-brands": "^1.2.6", "@iconify-json/material-symbols": "^1.2.5",
"@iconify-json/fa6-regular": "^1.2.4", "@iconify/svelte": "^4.0.2",
"@iconify-json/fa6-solid": "^1.2.4", "@sveltejs/vite-plugin-svelte": "^4.0.0",
"@iconify-json/material-symbols": "^1.2.50", "@swup/astro": "^1.4.1",
"@iconify/svelte": "^4.2.0", "@tailwindcss/typography": "^0.5.15",
"@swup/astro": "^1.7.0", "astro": "^4.16.13",
"@tailwindcss/typography": "^0.5.19", "astro-compress": "^2.3.5",
"astro": "5.13.10", "astro-icon": "^1.1.1",
"astro-expressive-code": "^0.41.4", "hastscript": "^9.0.0",
"astro-icon": "^1.1.5",
"hastscript": "^9.0.1",
"katex": "^0.16.27",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"overlayscrollbars": "^2.12.0", "overlayscrollbars": "^2.10.0",
"pagefind": "^1.4.0", "pagefind": "^1.1.1",
"photoswipe": "^5.4.4", "photoswipe": "^5.4.4",
"reading-time": "^1.5.0", "reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0", "rehype-autolink-headings": "^7.1.0",
"rehype-components": "^0.3.0", "rehype-components": "^0.3.0",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"rehype-slug": "^6.0.0", "rehype-slug": "^6.0.0",
"remark-directive": "^3.0.1", "remark-directive": "^3.0.0",
"remark-directive-rehype": "^0.4.2", "remark-directive-rehype": "^0.4.2",
"remark-github-admonitions-to-directives": "^1.0.5", "remark-github-admonitions-to-directives": "^1.0.5",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"remark-sectionize": "^2.1.0", "remark-sectionize": "^2.0.0",
"sanitize-html": "^2.17.0", "sanitize-html": "^2.13.1",
"sharp": "^0.34.5", "sharp": "^0.33.5",
"stylus": "^0.64.0", "stylus": "^0.63.0",
"svelte": "^5.39.8", "svelte": "^5.2.2",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.14",
"typescript": "^5.9.3", "typescript": "^5.6.3",
"unist-util-visit": "^5.0.0" "unist-util-visit": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/ts-plugin": "^1.10.6", "@astrojs/ts-plugin": "^1.10.4",
"@biomejs/biome": "2.2.5", "@biomejs/biome": "1.8.3",
"@rollup/plugin-yaml": "^4.1.2", "@rollup/plugin-yaml": "^4.1.2",
"@types/hast": "^3.0.4",
"@types/markdown-it": "^14.1.2", "@types/markdown-it": "^14.1.2",
"@types/mdast": "^4.0.4", "@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.16.0", "@types/sanitize-html": "^2.13.0",
"postcss-import": "^16.1.1", "postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.2" "postcss-nesting": "^13.0.1"
}, },
"packageManager": "pnpm@9.14.4" "packageManager": "pnpm@9.14.4"
} }

View file

@ -1,6 +0,0 @@
exclude_selectors:
- "span.katex"
- "span.katex-display"
- "[data-pagefind-ignore]"
- ".search-panel"
- "#search-panel"

File diff suppressed because it is too large Load diff

View file

@ -32,16 +32,10 @@ const targetDir = "./src/content/posts/"
const fullPath = path.join(targetDir, fileName) const fullPath = path.join(targetDir, fileName)
if (fs.existsSync(fullPath)) { if (fs.existsSync(fullPath)) {
console.error(`Error: File ${fullPath} already exists `) console.error(`ErrorFile ${fullPath} already exists `)
process.exit(1) process.exit(1)
} }
// recursive mode creates multi-level directories
const dirPath = path.dirname(fullPath)
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true })
}
const content = `--- const content = `---
title: ${args[0]} title: ${args[0]}
published: ${getDate()} published: ${getDate()}

BIN
src/assets/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 877 KiB

View file

@ -0,0 +1,118 @@
---
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 { keyword, 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 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>
))}
</div>
))
}
</div>

View file

@ -1,151 +0,0 @@
<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, 10),
posts: grouped[Number.parseInt(yearStr, 10)],
}));
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(group.posts.length === 1 ? I18nKey.postCount : 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

@ -1,6 +1,6 @@
--- ---
import { siteConfig } from "../config"; import { siteConfig } from '../config'
--- ---
<div id="config-carrier" data-hue={siteConfig.themeColor.hue}> <div id="config-carrier" data-hue={siteConfig.themeColor.hue}>

View file

@ -1,9 +1,8 @@
--- ---
import { profileConfig } from "../config"; import { profileConfig } from '../config'
import { url } from "../utils/url-utils"; import { url } from '../utils/url-utils'
const currentYear = new Date().getFullYear()
const currentYear = new Date().getFullYear();
--- ---
<!--<div class="border-t border-[var(&#45;&#45;primary)] mx-16 border-dashed py-8 max-w-[var(&#45;&#45;page-width)] flex flex-col items-center justify-center px-6">--> <!--<div class="border-t border-[var(&#45;&#45;primary)] mx-16 border-dashed py-8 max-w-[var(&#45;&#45;page-width)] flex flex-col items-center justify-center px-6">-->

View file

@ -1,59 +1,59 @@
<script lang="ts"> <script lang="ts">
import { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants.ts"; import type { LIGHT_DARK_MODE } from '@/types/config.ts'
import I18nKey from "@i18n/i18nKey"; import { AUTO_MODE, DARK_MODE, LIGHT_MODE } from '@constants/constants.ts'
import { i18n } from "@i18n/translation"; import I18nKey from '@i18n/i18nKey'
import Icon from "@iconify/svelte"; import { i18n } from '@i18n/translation'
import Icon from '@iconify/svelte'
import { import {
applyThemeToDocument, applyThemeToDocument,
getStoredTheme, getStoredTheme,
setTheme, setTheme,
} from "@utils/setting-utils.ts"; } from '@utils/setting-utils.ts'
import { onMount } from "svelte"; import { onMount } from 'svelte'
import type { LIGHT_DARK_MODE } from "@/types/config.ts";
const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE]; const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE]
let mode: LIGHT_DARK_MODE = $state(AUTO_MODE); let mode: LIGHT_DARK_MODE = $state(AUTO_MODE)
onMount(() => { onMount(() => {
mode = getStoredTheme(); mode = getStoredTheme()
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)"); const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)')
const changeThemeWhenSchemeChanged: Parameters< const changeThemeWhenSchemeChanged: Parameters<
typeof darkModePreference.addEventListener<"change"> typeof darkModePreference.addEventListener<'change'>
>[1] = (_e) => { >[1] = e => {
applyThemeToDocument(mode); applyThemeToDocument(mode)
}; }
darkModePreference.addEventListener("change", changeThemeWhenSchemeChanged); darkModePreference.addEventListener('change', changeThemeWhenSchemeChanged)
return () => { return () => {
darkModePreference.removeEventListener( darkModePreference.removeEventListener(
"change", 'change',
changeThemeWhenSchemeChanged, changeThemeWhenSchemeChanged,
); )
}; }
}); })
function switchScheme(newMode: LIGHT_DARK_MODE) { function switchScheme(newMode: LIGHT_DARK_MODE) {
mode = newMode; mode = newMode
setTheme(newMode); setTheme(newMode)
} }
function toggleScheme() { function toggleScheme() {
let i = 0; let i = 0
for (; i < seq.length; i++) { for (; i < seq.length; i++) {
if (seq[i] === mode) { if (seq[i] === mode) {
break; break
} }
} }
switchScheme(seq[(i + 1) % seq.length]); switchScheme(seq[(i + 1) % seq.length])
} }
function showPanel() { function showPanel() {
const panel = document.querySelector("#light-dark-panel"); const panel = document.querySelector('#light-dark-panel')
panel.classList.remove("float-panel-closed"); panel.classList.remove('float-panel-closed')
} }
function hidePanel() { function hidePanel() {
const panel = document.querySelector("#light-dark-panel"); const panel = document.querySelector('#light-dark-panel')
panel.classList.add("float-panel-closed"); panel.classList.add('float-panel-closed')
} }
</script> </script>

View file

@ -1,24 +1,23 @@
--- ---
import { Icon } from "astro-icon/components"; import { Icon } from 'astro-icon/components'
import { navBarConfig, siteConfig } from "../config"; import DisplaySettings from './widget/DisplaySettings.svelte'
import { LinkPresets } from "../constants/link-presets"; import { LinkPreset, type NavBarLink } from '../types/config'
import { LinkPreset, type NavBarLink } from "../types/config"; import { navBarConfig, siteConfig } from '../config'
import { url } from "../utils/url-utils"; import NavMenuPanel from './widget/NavMenuPanel.astro'
import LightDarkSwitch from "./LightDarkSwitch.svelte"; import Search from './Search.svelte'
import Search from "./Search.svelte"; import { LinkPresets } from '../constants/link-presets'
import DisplaySettings from "./widget/DisplaySettings.svelte"; import LightDarkSwitch from './LightDarkSwitch.svelte'
import NavMenuPanel from "./widget/NavMenuPanel.astro"; import { url } from '../utils/url-utils'
const className = Astro.props.class
const className = Astro.props.class;
let links: NavBarLink[] = navBarConfig.links.map( let links: NavBarLink[] = navBarConfig.links.map(
(item: NavBarLink | LinkPreset): NavBarLink => { (item: NavBarLink | LinkPreset): NavBarLink => {
if (typeof item === "number") { if (typeof item === 'number') {
return LinkPresets[item]; return LinkPresets[item]
} }
return item; return item
}, },
); )
--- ---
<div id="navbar" class="z-50 onload-animation"> <div id="navbar" class="z-50 onload-animation">
<div class="absolute h-8 left-0 right-0 -top-8 bg-[var(--card-bg)] transition"></div> <!-- used for onload animation --> <div class="absolute h-8 left-0 right-0 -top-8 bg-[var(--card-bg)] transition"></div> <!-- used for onload animation -->
@ -45,13 +44,13 @@ let links: NavBarLink[] = navBarConfig.links.map(
</div> </div>
<div class="flex"> <div class="flex">
<!--<SearchPanel client:load>--> <!--<SearchPanel client:load>-->
<Search client:only="svelte"></Search> <Search client:load></Search>
{!siteConfig.themeColor.fixed && ( {!siteConfig.themeColor.fixed && (
<button aria-label="Display Settings" class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="display-settings-switch"> <button aria-label="Display Settings" class="btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="display-settings-switch">
<Icon name="material-symbols:palette-outline" class="text-[1.25rem]"></Icon> <Icon name="material-symbols:palette-outline" class="text-[1.25rem]"></Icon>
</button> </button>
)} )}
<LightDarkSwitch client:only="svelte"></LightDarkSwitch> <LightDarkSwitch client:load></LightDarkSwitch>
<button aria-label="Menu" name="Nav Menu" class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 md:!hidden" id="nav-menu-switch"> <button aria-label="Menu" name="Nav Menu" class="btn-plain scale-animation rounded-lg w-11 h-11 active:scale-90 md:!hidden" id="nav-menu-switch">
<Icon name="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon> <Icon name="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon>
</button> </button>
@ -62,6 +61,7 @@ let links: NavBarLink[] = navBarConfig.links.map(
</div> </div>
<script> <script>
function switchTheme() { function switchTheme() {
if (localStorage.theme === 'dark') { if (localStorage.theme === 'dark') {
document.documentElement.classList.remove('dark'); document.documentElement.classList.remove('dark');
@ -75,67 +75,48 @@ function switchTheme() {
function loadButtonScript() { function loadButtonScript() {
let switchBtn = document.getElementById("scheme-switch"); let switchBtn = document.getElementById("scheme-switch");
if (switchBtn) { if (switchBtn) {
switchBtn.onclick = function () { switchBtn.addEventListener("click", function () {
switchTheme() switchTheme()
}; });
} }
let settingBtn = document.getElementById("display-settings-switch"); let settingBtn = document.getElementById("display-settings-switch");
if (settingBtn) { if (settingBtn) {
settingBtn.onclick = function () { settingBtn.addEventListener("click", function () {
let settingPanel = document.getElementById("display-setting"); let settingPanel = document.getElementById("display-setting");
if (settingPanel) { if (settingPanel) {
settingPanel.classList.toggle("float-panel-closed"); settingPanel.classList.toggle("float-panel-closed");
} }
}; });
} }
let menuBtn = document.getElementById("nav-menu-switch"); let menuBtn = document.getElementById("nav-menu-switch");
if (menuBtn) { if (menuBtn) {
menuBtn.onclick = function () { menuBtn.addEventListener("click", function () {
let menuPanel = document.getElementById("nav-menu-panel"); let menuPanel = document.getElementById("nav-menu-panel");
if (menuPanel) { if (menuPanel) {
menuPanel.classList.toggle("float-panel-closed"); menuPanel.classList.toggle("float-panel-closed");
} }
}; });
} }
} }
loadButtonScript(); loadButtonScript();
document.addEventListener('astro:after-swap', () => {
loadButtonScript();
}, { once: false });
</script> </script>
{import.meta.env.PROD && <script is:inline define:vars={{scriptUrl: url('/pagefind/pagefind.js')}}> {import.meta.env.PROD && <script is:inline define:vars={{scriptUrl: url('/pagefind/pagefind.js')}}>
async function loadPagefind() { async function loadPagefind() {
try { const pagefind = await import(scriptUrl)
const response = await fetch(scriptUrl, { method: 'HEAD' }); await pagefind.options({
if (!response.ok) { 'excerptLength': 20
throw new Error(`Pagefind script not found: ${response.status}`); })
} pagefind.init()
window.pagefind = pagefind
const pagefind = await import(scriptUrl); pagefind.search('') // speed up the first search
await pagefind.options({
excerptLength: 20
});
window.pagefind = pagefind;
document.dispatchEvent(new CustomEvent('pagefindready'));
console.log('Pagefind loaded and initialized successfully, event dispatched.');
} catch (error) {
console.error('Failed to load Pagefind:', error);
window.pagefind = {
search: () => Promise.resolve({ results: [] }),
options: () => Promise.resolve(),
};
document.dispatchEvent(new CustomEvent('pagefindloaderror'));
console.log('Pagefind load error, event dispatched.');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadPagefind);
} else {
loadPagefind();
} }
loadPagefind()
</script>} </script>}

View file

@ -1,46 +1,45 @@
--- ---
import type { CollectionEntry } from "astro:content"; import path from 'path'
import path from "node:path"; import PostMetadata from './PostMeta.astro'
import { Icon } from "astro-icon/components"; import ImageWrapper from './misc/ImageWrapper.astro'
import I18nKey from "../i18n/i18nKey"; import { Icon } from 'astro-icon/components'
import { i18n } from "../i18n/translation"; import { i18n } from '../i18n/translation'
import { getDir } from "../utils/url-utils"; import I18nKey from '../i18n/i18nKey'
import ImageWrapper from "./misc/ImageWrapper.astro"; import { getDir } from '../utils/url-utils'
import PostMetadata from "./PostMeta.astro";
interface Props { interface Props {
class?: string; class?: string
entry: CollectionEntry<"posts">; entry: any
title: string; title: string
url: string; url: string
published: Date; published: Date
updated?: Date; updated?: Date
tags: string[]; tags: string[]
category: string | null; category: string
image: string; image: string
description: string; description: string
draft: boolean; draft: boolean
style: string; style: string
} }
const { const {
entry, entry,
title, title,
url, url,
published, published,
updated, updated,
tags, tags,
category, category,
image, image,
description, description,
style, style,
} = Astro.props; } = Astro.props
const className = Astro.props.class; const className = Astro.props.class
const hasCover = image !== undefined && image !== null && image !== ""; const hasCover = image !== undefined && image !== null && image !== ''
const coverWidth = "28%"; const coverWidth = '28%'
const { remarkPluginFrontmatter } = await entry.render(); const { remarkPluginFrontmatter } = await entry.render()
--- ---
<div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative", className]} style={style}> <div class:list={["card-base flex flex-col-reverse md:flex-col w-full rounded-[var(--radius-large)] overflow-hidden relative", className]} style={style}>
<div class:list={["pl-6 md:pl-9 pr-6 md:pr-2 pt-6 md:pt-7 pb-6 relative", {"w-full md:w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}> <div class:list={["pl-6 md:pl-9 pr-6 md:pr-2 pt-6 md:pt-7 pb-6 relative", {"w-full md:w-[calc(100%_-_52px_-_12px)]": !hasCover, "w-full md:w-[calc(100%_-_var(--coverWidth)_-_12px)]": hasCover}]}>
@ -66,13 +65,9 @@ const { remarkPluginFrontmatter } = await entry.render();
<!-- word count and read time --> <!-- word count and read time -->
<div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition"> <div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition">
<div> <div>{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div>
{remarkPluginFrontmatter.words} {" " + i18n(remarkPluginFrontmatter.words === 1 ? I18nKey.wordCount : I18nKey.wordsCount)}
</div>
<div>|</div> <div>|</div>
<div> <div>{remarkPluginFrontmatter.minutes} {" " + i18n(I18nKey.minutesCount)}</div>
{remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
</div>
</div> </div>
</div> </div>

View file

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

View file

@ -1,28 +1,29 @@
--- ---
import type { CollectionEntry } from "astro:content"; import { getPostUrlBySlug } from '@utils/url-utils'
import { getPostUrlBySlug } from "@utils/url-utils"; import PostCard from './PostCard.astro'
import PostCard from "./PostCard.astro";
const { page } = Astro.props; const { page } = Astro.props
let delay = 0; let delay = 0
const interval = 50; const interval = 50
--- ---
<div class="transition flex flex-col rounded-[var(--radius-large)] bg-[var(--card-bg)] py-1 md:py-0 md:bg-transparent md:gap-4 mb-4"> <div class="transition flex flex-col rounded-[var(--radius-large)] bg-[var(--card-bg)] py-1 md:py-0 md:bg-transparent md:gap-4 mb-4">
{page.data.map((entry: CollectionEntry<"posts">) => ( {page.data.map((entry: { data: { draft: boolean; title: string; tags: string[]; category: string; published: Date; image: string; description: string; updated: Date; }; slug: string; }) => {
<PostCard return (
entry={entry} <PostCard
title={entry.data.title} entry={entry}
tags={entry.data.tags} title={entry.data.title}
category={entry.data.category} tags={entry.data.tags}
published={entry.data.published} category={entry.data.category}
updated={entry.data.updated} published={entry.data.published}
url={getPostUrlBySlug(entry.slug)} updated={entry.data.updated}
image={entry.data.image} url={getPostUrlBySlug(entry.slug)}
description={entry.data.description} image={entry.data.image}
draft={entry.data.draft} description={entry.data.description}
class:list="onload-animation" draft={entry.data.draft}
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`} class:list="onload-animation"
></PostCard> style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
))} ></PostCard>
);
})}
</div> </div>

View file

@ -1,141 +1,73 @@
<script lang="ts"> <script lang="ts">
import I18nKey from "@i18n/i18nKey"; import { onMount } from 'svelte'
import { i18n } from "@i18n/translation"; import { url } from '@utils/url-utils.ts'
import Icon from "@iconify/svelte"; import { i18n } from '@i18n/translation'
import { url } from "@utils/url-utils.ts"; import I18nKey from '@i18n/i18nKey'
import { onMount } from "svelte"; import Icon from '@iconify/svelte'
import type { SearchResult } from "@/global"; let keywordDesktop = ''
let keywordMobile = ''
let result = []
const fakeResult = [
{
url: url('/'),
meta: {
title: 'This Is a Fake Search Result',
},
excerpt:
'Because the search cannot work in the <mark>dev</mark> environment.',
},
{
url: url('/'),
meta: {
title: 'If You Want to Test the Search',
},
excerpt: 'Try running <mark>npm build && npm preview</mark> instead.',
},
]
let keywordDesktop = ""; let search = (keyword: string, isDesktop: boolean) => {}
let keywordMobile = "";
let result: SearchResult[] = [];
let isSearching = false;
let pagefindLoaded = false;
let initialized = false;
const fakeResult: SearchResult[] = [
{
url: url("/"),
meta: {
title: "This Is a Fake Search Result",
},
excerpt:
"Because the search cannot work in the <mark>dev</mark> environment.",
},
{
url: url("/"),
meta: {
title: "If You Want to Test the Search",
},
excerpt: "Try running <mark>npm build && npm preview</mark> instead.",
},
];
const togglePanel = () => {
const panel = document.getElementById("search-panel");
panel?.classList.toggle("float-panel-closed");
};
const setPanelVisibility = (show: boolean, isDesktop: boolean): void => {
const panel = document.getElementById("search-panel");
if (!panel || !isDesktop) return;
if (show) {
panel.classList.remove("float-panel-closed");
} else {
panel.classList.add("float-panel-closed");
}
};
const search = async (keyword: string, isDesktop: boolean): Promise<void> => {
if (!keyword) {
setPanelVisibility(false, isDesktop);
result = [];
return;
}
if (!initialized) {
return;
}
isSearching = true;
try {
let searchResults: SearchResult[] = [];
if (import.meta.env.PROD && pagefindLoaded && window.pagefind) {
const response = await window.pagefind.search(keyword);
searchResults = await Promise.all(
response.results.map((item) => item.data()),
);
} else if (import.meta.env.DEV) {
searchResults = fakeResult;
} else {
searchResults = [];
console.error("Pagefind is not available in production environment.");
}
result = searchResults;
setPanelVisibility(result.length > 0, isDesktop);
} catch (error) {
console.error("Search error:", error);
result = [];
setPanelVisibility(false, isDesktop);
} finally {
isSearching = false;
}
};
onMount(() => { onMount(() => {
const initializeSearch = () => { search = async (keyword: string, isDesktop: boolean) => {
initialized = true; let panel = document.getElementById('search-panel')
pagefindLoaded = if (!panel) return
typeof window !== "undefined" &&
!!window.pagefind &&
typeof window.pagefind.search === "function";
console.log("Pagefind status on init:", pagefindLoaded);
if (keywordDesktop) search(keywordDesktop, true);
if (keywordMobile) search(keywordMobile, false);
};
if (import.meta.env.DEV) { if (!keyword && isDesktop) {
console.log( panel.classList.add('float-panel-closed')
"Pagefind is not available in development mode. Using mock data.", return
); }
initializeSearch();
} else {
document.addEventListener("pagefindready", () => {
console.log("Pagefind ready event received.");
initializeSearch();
});
document.addEventListener("pagefindloaderror", () => {
console.warn(
"Pagefind load error event received. Search functionality will be limited.",
);
initializeSearch(); // Initialize with pagefindLoaded as false
});
// Fallback in case events are not caught or pagefind is already loaded by the time this script runs let arr = []
setTimeout(() => { if (import.meta.env.PROD) {
if (!initialized) { const ret = await pagefind.search(keyword)
console.log("Fallback: Initializing search after timeout."); for (const item of ret.results) {
initializeSearch(); arr.push(await item.data())
} }
}, 2000); // Adjust timeout as needed } else {
} // Mock data for non-production environment
}); // arr = JSON.parse('[{"url":"/","content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","word_count":187,"filters":{},"meta":{"title":"This Is a Fake Search Result"},"anchors":[{"element":"h2","id":"front-matter-of-posts","text":"Front-matter of Posts","location":34},{"element":"h2","id":"where-to-place-the-post-files","text":"Where to Place the Post Files","location":151}],"weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"raw_content":"Simple Guides for Fuwari. Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers in the Astro Docs. Front-matter of Posts. --- title: My First Blog Post published: 2023-09-09 description: This is the first post of my new Astro blog. image: ./cover.jpg tags: [Foo, Bar] category: Front-end draft: false ---AttributeDescription title. The title of the post. published. The date the post was published. description. A short description of the post. Displayed on index page. image. The cover image path of the post. 1. Start with http:// or https://: Use web image 2. Start with /: For image in public dir 3. With none of the prefixes: Relative to the markdown file. tags. The tags of the post. category. The category of the post. draft. If this post is still a draft, which wont be displayed. Where to Place the Post Files. Your post files should be placed in src/content/posts/ directory. You can also create sub-directories to better organize your posts and assets. src/content/posts/ ├── post-1.md └── post-2/ ├── cover.png └── index.md.","raw_url":"/posts/guide/","excerpt":"Because the search cannot work in the <mark>dev</mark> environment.","sub_results":[{"title":"Simple Guides for Fuwari - Fuwari","url":"/posts/guide/","weighted_locations":[{"weight":10,"balanced_score":57600,"location":3}],"locations":[3],"excerpt":"Simple Guides for <mark>Fuwari.</mark> Cover image source: Source. This blog template is built with Astro. For the things that are not mentioned in this guide, you may find the answers"}]},{"url":"/","content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","word_count":25,"filters":{},"meta":{"title":"If You Want to Test the Search"},"anchors":[{"element":"h1","id":"about","text":"About","location":0},{"element":"h3","id":"sources-of-images-used-in-this-site","text":"Sources of images used in this site","location":8}],"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"raw_content":"About. This is the demo site for Fuwari. Sources of images used in this site. Unsplash. 星と少女 by Stella. Rabbit - v1.4 Showcase by Rabbit_YourMajesty.","raw_url":"/about/","excerpt":"Try running <mark>npm build && npm preview</mark> instead.","sub_results":[{"title":"About","url":"/about/#about","anchor":{"element":"h1","id":"about","text":"About","location":0},"weighted_locations":[{"weight":1,"balanced_score":576,"location":7}],"locations":[7],"excerpt":"About. This is the demo site for <mark>Fuwari.</mark>"}]}]')
arr = fakeResult
}
$: if (initialized && keywordDesktop) { if (!arr.length && isDesktop) {
(async () => { panel.classList.add('float-panel-closed')
await search(keywordDesktop, true); return
})(); }
if (isDesktop) {
panel.classList.remove('float-panel-closed')
}
result = arr
}
})
const togglePanel = () => {
let panel = document.getElementById('search-panel')
panel?.classList.toggle('float-panel-closed')
} }
$: if (initialized && keywordMobile) { $: search(keywordDesktop, true)
(async () => { $: search(keywordMobile, false)
await search(keywordMobile, false);
})();
}
</script> </script>
<!-- search bar for desktop view --> <!-- search bar for desktop view -->
@ -191,8 +123,4 @@ top-20 left-4 md:left-[unset] right-4 shadow-2xl rounded-2xl p-2">
input:focus { input:focus {
outline: 0; outline: 0;
} }
.search-panel {
max-height: calc(100vh - 100px);
overflow-y: auto;
}
</style> </style>

View file

@ -1,5 +1,5 @@
--- ---
import { Icon } from "astro-icon/components"; import { Icon } from 'astro-icon/components'
--- ---
<!-- There can't be a filter on parent element, or it will break `fixed` --> <!-- There can't be a filter on parent element, or it will break `fixed` -->

View file

@ -1,10 +1,10 @@
--- ---
interface Props { interface Props {
badge?: string; badge?: string
url?: string; url?: string
label?: string; label?: string
} }
const { badge, url, label } = Astro.props; const { badge, url, label } = Astro.props
--- ---
<a href={url} aria-label={label}> <a href={url} aria-label={label}>
<button <button
@ -33,7 +33,7 @@ const { badge, url, label } = Astro.props;
{ badge !== undefined && badge !== null && badge !== '' && { badge !== undefined && badge !== null && badge !== '' &&
<div class="transition px-2 h-7 ml-4 min-w-[2rem] rounded-lg text-sm font-bold <div class="transition px-2 h-7 ml-4 min-w-[2rem] rounded-lg text-sm font-bold
text-[var(--btn-content)] dark:text-[var(--deep-text)] text-[var(--btn-content)] dark:text-[var(--deep-text)]
bg-[var(--btn-regular-bg)] dark:bg-[var(--primary)] bg-[oklch(0.95_0.025_var(--hue))] dark:bg-[var(--primary)]
flex items-center justify-center"> flex items-center justify-center">
{ badge } { badge }
</div> </div>

View file

@ -1,11 +1,11 @@
--- ---
interface Props { interface Props {
size?: string; size?: string
dot?: boolean; dot?: boolean
href?: string; href?: string
label?: string; label?: string
} }
const { dot, href, label }: Props = Astro.props; const { size, dot, href, label }: Props = Astro.props
--- ---
<a href={href} aria-label={label} class="btn-regular h-8 text-sm px-3 rounded-lg"> <a href={href} aria-label={label} class="btn-regular h-8 text-sm px-3 rounded-lg">
{dot && <div class="h-1 w-1 bg-[var(--btn-content)] dark:bg-[var(--card-bg)] transition rounded-md mr-2"></div>} {dot && <div class="h-1 w-1 bg-[var(--btn-content)] dark:bg-[var(--card-bg)] transition rounded-md mr-2"></div>}

View file

@ -1,58 +1,57 @@
--- ---
import type { Page } from "astro"; import type { Page } from 'astro'
import { Icon } from "astro-icon/components"; import { Icon } from 'astro-icon/components'
import { url } from "../../utils/url-utils"; import { url } from '../../utils/url-utils'
interface Props { interface Props {
page: Page; page: Page
class?: string; class?: string
style?: string; style?: string
} }
const { page, style } = Astro.props; const { page, style } = Astro.props
const HIDDEN = -1; const HIDDEN = -1
const className = Astro.props.class; const className = Astro.props.class
const ADJ_DIST = 2; const ADJ_DIST = 2
const VISIBLE = ADJ_DIST * 2 + 1; const VISIBLE = ADJ_DIST * 2 + 1
// for test // for test
let count = 1; let count = 1
let l = page.currentPage; let l = page.currentPage,
let r = page.currentPage; r = page.currentPage
while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) { while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) {
count += 2; count += 2
l--; l--
r++; r++
} }
while (0 < l - 1 && count < VISIBLE) { while (0 < l - 1 && count < VISIBLE) {
count++; count++
l--; l--
} }
while (r + 1 <= page.lastPage && count < VISIBLE) { while (r + 1 <= page.lastPage && count < VISIBLE) {
count++; count++
r++; r++
} }
let pages: number[] = []; let pages: number[] = []
if (l > 1) pages.push(1); if (l > 1) pages.push(1)
if (l === 3) pages.push(2); if (l == 3) pages.push(2)
if (l > 3) pages.push(HIDDEN); if (l > 3) pages.push(HIDDEN)
for (let i = l; i <= r; i++) pages.push(i); for (let i = l; i <= r; i++) pages.push(i)
if (r < page.lastPage - 2) pages.push(HIDDEN); if (r < page.lastPage - 2) pages.push(HIDDEN)
if (r === page.lastPage - 2) pages.push(page.lastPage - 1); if (r == page.lastPage - 2) pages.push(page.lastPage - 1)
if (r < page.lastPage) pages.push(page.lastPage); if (r < page.lastPage) pages.push(page.lastPage)
const getPageUrl = (p: number) => { const getPageUrl = (p: number) => {
if (p === 1) return "/"; if (p == 1) return '/'
return `/${p}/`; return `/${p}/`
}; }
--- ---
<div class:list={[className, "flex flex-row gap-3 justify-center"]} style={style}> <div class:list={[className, "flex flex-row gap-3 justify-center"]} style={style}>
<a href={page.url.prev || ""} aria-label={page.url.prev ? "Previous Page" : null} <a href={url(page.url.prev || "")} aria-label={page.url.prev ? "Previous Page" : null}
class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11", class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
{"disabled": page.url.prev == undefined} {"disabled": page.url.prev == undefined}
]} ]}
@ -69,12 +68,12 @@ const getPageUrl = (p: number) => {
> >
{p} {p}
</div> </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]" class="btn-card w-11 h-11 rounded-lg overflow-hidden active:scale-[0.85]"
>{p}</a> >{p}</a>
})} })}
</div> </div>
<a href={page.url.next || ""} aria-label={page.url.next ? "Next Page" : null} <a href={url(page.url.next || "")} aria-label={page.url.next ? "Next Page" : null}
class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11", class:list={["btn-card overflow-hidden rounded-lg text-[var(--primary)] w-11 h-11",
{"disabled": page.url.next == undefined} {"disabled": page.url.next == undefined}
]} ]}

View file

@ -1,54 +1,50 @@
--- ---
import path from "node:path"; import path from 'path'
interface Props { interface Props {
id?: string; id?: string
src: string; src: string
class?: string; class?: string
alt?: string; alt?: string
position?: string; position?: string
basePath?: string; basePath?: string
} }
import { Image } from 'astro:assets'
import { url } from '../../utils/url-utils'
import { Image } from "astro:assets"; const { id, src, alt, position = 'center', basePath = '/' } = Astro.props
import { url } from "../../utils/url-utils"; const className = Astro.props.class
const { id, src, alt, position = "center", basePath = "/" } = Astro.props;
const className = Astro.props.class;
const isLocal = !( const isLocal = !(
src.startsWith("/") || src.startsWith('/') ||
src.startsWith("http") || src.startsWith('http') ||
src.startsWith("https") || src.startsWith('https') ||
src.startsWith("data:") src.startsWith('data:')
); )
const isPublic = src.startsWith("/"); const isPublic = src.startsWith('/')
// TODO temporary workaround for images dynamic import // TODO temporary workaround for images dynamic import
// https://github.com/withastro/astro/issues/3373 // https://github.com/withastro/astro/issues/3373
// biome-ignore lint/suspicious/noImplicitAnyLet: <check later> let img
let img;
if (isLocal) { if (isLocal) {
const files = import.meta.glob<ImageMetadata>("../../**", { const files = import.meta.glob<ImageMetadata>('../../**', {
import: "default", import: 'default',
}); })
let normalizedPath = path let normalizedPath = path
.normalize(path.join("../../", basePath, src)) .normalize(path.join('../../', basePath, src))
.replace(/\\/g, "/"); .replace(/\\/g, '/')
const file = files[normalizedPath]; const file = files[normalizedPath]
if (!file) { if (!file) {
console.error( console.error(`\n[ERROR] Image file not found: ${normalizedPath.replace('../../', 'src/')}`)
`\n[ERROR] Image file not found: ${normalizedPath.replace("../../", "src/")}`, }
); img = await file()
}
img = await file();
} }
const imageClass = "w-full h-full object-cover"; const imageClass = 'w-full h-full object-cover'
const imageStyle = `object-position: ${position}`; const imageStyle = `object-position: ${position}`
--- ---
<div id={id} class:list={[className, 'overflow-hidden relative']}> <div id={id} class:list={[className, 'overflow-hidden relative']}>
<div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div> <div class="transition absolute inset-0 dark:bg-black/10 bg-opacity-50 pointer-events-none"></div>
{isLocal && img && <Image src={img} alt={alt || ""} class={imageClass} style={imageStyle}/>} {isLocal && img && <Image src={img} alt={alt || ""} class={imageClass} style={imageStyle}/>}
{!isLocal && <img src={isPublic ? url(src) : src} alt={alt || ""} class={imageClass} style={imageStyle}/>} {!isLocal && <img src={isPublic ? url(src) : src} alt={alt || ""} class={imageClass} style={imageStyle}/>}
</div> </div>

View file

@ -1,24 +1,24 @@
--- ---
import { Icon } from "astro-icon/components"; import { formatDateToYYYYMMDD } from '../../utils/date-utils'
import { licenseConfig, profileConfig } from "../../config"; import { Icon } from 'astro-icon/components'
import I18nKey from "../../i18n/i18nKey"; import { licenseConfig, profileConfig } from '../../config'
import { i18n } from "../../i18n/translation"; import { i18n } from '../../i18n/translation'
import { formatDateToYYYYMMDD } from "../../utils/date-utils"; import I18nKey from '../../i18n/i18nKey'
interface Props { interface Props {
title: string; title: string
slug: string; slug: string
pubDate: Date; pubDate: Date
class: string; class: string
} }
const { title, pubDate } = Astro.props; const { title, slug, pubDate } = Astro.props
const className = Astro.props.class; const className = Astro.props.class
const profileConf = profileConfig; const profileConf = profileConfig
const licenseConf = licenseConfig; const licenseConf = licenseConfig
const postUrl = decodeURIComponent(Astro.url.toString()); 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"> <div class="transition font-bold text-black/75 dark:text-white/75">
{title} {title}
</div> </div>
@ -28,16 +28,16 @@ const postUrl = decodeURIComponent(Astro.url.toString());
<div class="flex gap-6 mt-2"> <div class="flex gap-6 mt-2">
<div> <div>
<div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.author)}</div> <div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.author)}</div>
<div class="transition text-black/75 dark:text-white/75 line-clamp-2">{profileConf.name}</div> <div class="transition text-black/75 dark:text-white/75 whitespace-nowrap">{profileConf.name}</div>
</div> </div>
<div> <div>
<div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.publishedAt)}</div> <div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.publishedAt)}</div>
<div class="transition text-black/75 dark:text-white/75 line-clamp-2">{formatDateToYYYYMMDD(pubDate)}</div> <div class="transition text-black/75 dark:text-white/75 whitespace-nowrap">{formatDateToYYYYMMDD(pubDate)}</div>
</div> </div>
<div> <div>
<div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.license)}</div> <div class="transition text-black/30 dark:text-white/30 text-sm">{i18n(I18nKey.license)}</div>
<a href={licenseConf.url} target="_blank" class="link text-[var(--primary)] line-clamp-2">{licenseConf.name}</a> <a href={licenseConf.url} target="_blank" class="link text-[var(--primary)] whitespace-nowrap">{licenseConf.name}</a>
</div> </div>
</div> </div>
<Icon name="fa6-brands:creative-commons" class="transition text-[15rem] absolute pointer-events-none right-6 top-1/2 -translate-y-1/2 text-black/5 dark:text-white/5"></Icon> <Icon name="fa6-brands:creative-commons" class="transition text-[15rem] absolute pointer-events-none right-6 top-1/2 -translate-y-1/2 text-black/5 dark:text-white/5"></Icon>
</div> </div>

View file

@ -1,43 +1,66 @@
--- ---
import "@fontsource-variable/jetbrains-mono"; import '@fontsource-variable/jetbrains-mono'
import "@fontsource-variable/jetbrains-mono/wght-italic.css"; import '@fontsource-variable/jetbrains-mono/wght-italic.css'
interface Props { interface Props {
class: string; class: string
} }
const className = Astro.props.class; const className = Astro.props.class
--- ---
<div data-pagefind-body class={`prose dark:prose-invert prose-base !max-w-none custom-md ${className}`}> <div data-pagefind-body class=`prose dark:prose-invert prose-base !max-w-none custom-md ${className}`>
<!--<div class="prose dark:prose-invert max-w-none custom-md">--> <!--<div class="prose dark:prose-invert max-w-none custom-md">-->
<!--<div class="max-w-none custom-md">--> <!--<div class="max-w-none custom-md">-->
<slot/> <slot/>
</div> </div>
<script> <script>
document.addEventListener("click", function (e: MouseEvent) { const observer = new MutationObserver(addPreCopyButton);
const target = e.target as Element | null; observer.observe(document.body, { childList: true, subtree: true });
if (target && target.classList.contains("copy-btn")) {
const preEle = target.closest("pre"); document.addEventListener("DOMContentLoaded", addPreCopyButton);
const codeEle = preEle?.querySelector("code");
const code = Array.from(codeEle?.querySelectorAll(".code:not(summary *)") ?? [])
.map(el => el.textContent)
.map(t => t === "\n" ? "" : t)
.join("\n");
navigator.clipboard.writeText(code);
const timeoutId = target.getAttribute("data-timeout-id"); function addPreCopyButton() {
if (timeoutId) { observer.disconnect();
clearTimeout(parseInt(timeoutId));
let codeBlocks = Array.from(document.querySelectorAll("pre"));
for (let codeBlock of codeBlocks) {
if (codeBlock.parentElement?.nodeName === "DIV" && codeBlock.parentElement?.classList.contains("code-block")) continue
let wrapper = document.createElement("div");
wrapper.className = "relative code-block";
let copyButton = document.createElement("button");
copyButton.className = "copy-btn btn-regular-dark absolute active:scale-90 h-8 w-8 top-2 right-2 opacity-75 text-sm p-1.5 rounded-lg transition-all ease-in-out";
codeBlock.setAttribute("tabindex", "0");
if (codeBlock.parentNode) {
codeBlock.parentNode.insertBefore(wrapper, codeBlock);
}
let copyIcon = `<svg class="copy-btn-icon copy-icon" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="M368.37-237.37q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-474.26q0-34.48 24.26-58.74 24.26-24.26 58.74-24.26h378.26q34.48 0 58.74 24.26 24.26 24.26 24.26 58.74v474.26q0 34.48-24.26 58.74-24.26 24.26-58.74 24.26H368.37Zm0-83h378.26v-474.26H368.37v474.26Zm-155 238q-34.48 0-58.74-24.26-24.26-24.26-24.26-58.74v-515.76q0-17.45 11.96-29.48 11.97-12.02 29.33-12.02t29.54 12.02q12.17 12.03 12.17 29.48v515.76h419.76q17.45 0 29.48 11.96 12.02 11.97 12.02 29.33t-12.02 29.54q-12.03 12.17-29.48 12.17H213.37Zm155-238v-474.26 474.26Z"/></svg>`
let successIcon = `<svg class="copy-btn-icon success-icon" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="m389-377.13 294.7-294.7q12.58-12.67 29.52-12.67 16.93 0 29.61 12.67 12.67 12.68 12.67 29.53 0 16.86-12.28 29.14L419.07-288.41q-12.59 12.67-29.52 12.67-16.94 0-29.62-12.67L217.41-430.93q-12.67-12.68-12.79-29.45-.12-16.77 12.55-29.45 12.68-12.67 29.62-12.67 16.93 0 29.28 12.67L389-377.13Z"/></svg>`
copyButton.innerHTML = `<div>${copyIcon} ${successIcon}</div>
`
wrapper.appendChild(codeBlock);
wrapper.appendChild(copyButton);
let timeout: ReturnType<typeof setTimeout>;
copyButton.addEventListener("click", async () => {
if (timeout) {
clearTimeout(timeout);
} }
let text = codeBlock?.querySelector("code")?.innerText;
target.classList.add("success"); if (text === undefined) return;
await navigator.clipboard.writeText(text);
// 设置新的timeout并保存ID到按钮的自定义属性中 copyButton.classList.add("success");
const newTimeoutId = setTimeout(() => { timeout = setTimeout(() => {
target.classList.remove("success"); copyButton.classList.remove("success");
}, 1000); }, 1000);
});
target.setAttribute("data-timeout-id", newTimeoutId.toString());
} }
});
observer.observe(document.body, { childList: true, subtree: true });
}
</script> </script>

View file

@ -1,23 +1,25 @@
--- ---
import I18nKey from "../../i18n/i18nKey"; import WidgetLayout from './WidgetLayout.astro'
import { i18n } from "../../i18n/translation";
import { getCategoryList } from "../../utils/content-utils";
import ButtonLink from "../control/ButtonLink.astro";
import WidgetLayout from "./WidgetLayout.astro";
const categories = await getCategoryList(); import { i18n } from '../../i18n/translation'
import I18nKey from '../../i18n/i18nKey'
import { getCategoryList } from '../../utils/content-utils'
import { getCategoryUrl } from '../../utils/url-utils'
import ButtonLink from '../control/ButtonLink.astro'
const COLLAPSED_HEIGHT = "7.5rem"; const categories = await getCategoryList()
const COLLAPSE_THRESHOLD = 5;
const isCollapsed = categories.length >= COLLAPSE_THRESHOLD; const COLLAPSED_HEIGHT = '7.5rem'
const COLLAPSE_THRESHOLD = 5
const isCollapsed = categories.length >= COLLAPSE_THRESHOLD
interface Props { interface Props {
class?: string; class?: string
style?: string; style?: string
} }
const className = Astro.props.class; const className = Astro.props.class
const style = Astro.props.style; const style = Astro.props.style
--- ---
<WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} <WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}
@ -25,11 +27,11 @@ const style = Astro.props.style;
> >
{categories.map((c) => {categories.map((c) =>
<ButtonLink <ButtonLink
url={c.url} url={getCategoryUrl(c.name)}
badge={String(c.count)} badge={String(c.count)}
label={`View all posts in the ${c.name.trim()} category`} label=`View all posts in the ${c.name} category`
> >
{c.name.trim()} {c.name}
</ButtonLink> </ButtonLink>
)} )}
</WidgetLayout> </WidgetLayout>

View file

@ -1,18 +1,18 @@
<script lang="ts"> <script lang="ts">
import I18nKey from "@i18n/i18nKey"; import { i18n } from '@i18n/translation'
import { i18n } from "@i18n/translation"; import I18nKey from '@i18n/i18nKey'
import Icon from "@iconify/svelte"; import { getDefaultHue, getHue, setHue } from '@utils/setting-utils'
import { getDefaultHue, getHue, setHue } from "@utils/setting-utils"; import Icon from '@iconify/svelte'
let hue = getHue(); let hue = getHue()
const defaultHue = getDefaultHue(); const defaultHue = getDefaultHue()
function resetHue() { function resetHue() {
hue = getDefaultHue(); hue = getDefaultHue()
} }
$: if (hue || hue === 0) { $: if (hue || hue === 0) {
setHue(hue); setHue(hue)
} }
</script> </script>
@ -23,7 +23,7 @@ $: if (hue || hue === 0) {
before:absolute before:-left-3 before:top-[0.33rem]" before:absolute before:-left-3 before:top-[0.33rem]"
> >
{i18n(I18nKey.themeColor)} {i18n(I18nKey.themeColor)}
<button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90 will-change-transform" <button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90"
class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} on:click={resetHue}> class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} on:click={resetHue}>
<div class="text-[var(--btn-content)]"> <div class="text-[var(--btn-content)]">
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon> <Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>

View file

@ -1,13 +1,13 @@
--- ---
import { Icon } from "astro-icon/components"; import { type NavBarLink } from '../../types/config'
import { type NavBarLink } from "../../types/config"; import { Icon } from 'astro-icon/components'
import { url } from "../../utils/url-utils"; import { url } from '../../utils/url-utils'
interface Props { interface Props {
links: NavBarLink[]; links: NavBarLink[]
} }
const links = Astro.props.links; const links = Astro.props.links
--- ---
<div id="nav-menu-panel" class:list={["float-panel float-panel-closed absolute transition-all fixed right-4 px-2 py-2"]}> <div id="nav-menu-panel" class:list={["float-panel float-panel-closed absolute transition-all fixed right-4 px-2 py-2"]}>
{links.map((link) => ( {links.map((link) => (

View file

@ -1,10 +1,10 @@
--- ---
import { Icon } from "astro-icon/components"; import ImageWrapper from '../misc/ImageWrapper.astro'
import { profileConfig } from "../../config"; import { Icon } from 'astro-icon/components'
import { url } from "../../utils/url-utils"; import { profileConfig } from '../../config'
import ImageWrapper from "../misc/ImageWrapper.astro"; import { url } from '../../utils/url-utils'
const config = profileConfig; const config = profileConfig
--- ---
<div class="card-base p-3"> <div class="card-base p-3">
<a aria-label="Go to About Page" href={url('/about/')} <a aria-label="Go to About Page" href={url('/about/')}
@ -22,7 +22,7 @@ const config = profileConfig;
<div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{config.name}</div> <div class="font-bold text-xl text-center mb-1 dark:text-neutral-50 transition">{config.name}</div>
<div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div> <div class="h-1 w-5 bg-[var(--primary)] mx-auto rounded-full mb-2 transition"></div>
<div class="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div> <div class="text-center text-neutral-400 mb-2.5 transition">{config.bio}</div>
<div class="flex flex-wrap gap-2 justify-center mb-1"> <div class="flex gap-2 justify-center mb-1">
{config.links.length > 1 && config.links.map(item => {config.links.length > 1 && config.links.map(item =>
<a rel="me" aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90"> <a rel="me" aria-label={item.name} href={item.url} target="_blank" class="btn-regular rounded-lg h-10 w-10 active:scale-90">
<Icon name={item.icon} class="text-[1.5rem]"></Icon> <Icon name={item.icon} class="text-[1.5rem]"></Icon>

View file

@ -1,15 +1,18 @@
--- ---
import type { MarkdownHeading } from "astro"; import Profile from './Profile.astro'
import Categories from "./Categories.astro"; import Tag from './Tags.astro'
import Profile from "./Profile.astro"; import Categories from './Categories.astro'
import Tag from "./Tags.astro"; import type { MarkdownHeading } from 'astro'
import TOC from './TOC.astro'
interface Props { interface Props {
class?: string; class? : string
headings?: MarkdownHeading[]; headings? : MarkdownHeading[]
} }
const className = Astro.props.class; const className = Astro.props.class
const headings = Astro.props.headings
--- ---
<div id="sidebar" class:list={[className, "w-full"]}> <div id="sidebar" class:list={[className, "w-full"]}>
<div class="flex flex-col w-full gap-4 mb-4"> <div class="flex flex-col w-full gap-4 mb-4">

View file

@ -1,37 +1,34 @@
--- ---
import type { MarkdownHeading } from "astro"; import type { MarkdownHeading } from 'astro';
import { siteConfig } from "../../config"; import { siteConfig } from "../../config";
import { url } from "../../utils/url-utils";
interface Props { interface Props {
class?: string; class?: string
headings: MarkdownHeading[]; headings: MarkdownHeading[]
} }
let { headings = [] } = Astro.props; let { headings = [] } = Astro.props;
let minDepth = 10; let minDepth = 10;
for (const heading of headings) { for (const heading of headings) {
minDepth = Math.min(minDepth, heading.depth); minDepth = Math.min(minDepth, heading.depth);
} }
const className = Astro.props.class; const className = Astro.props.class
const isPostsRoute = Astro.url.pathname.startsWith(url("/posts/"));
const removeTailingHash = (text: string) => { const removeTailingHash = (text: string) => {
let lastIndexOfHash = text.lastIndexOf("#"); let lastIndexOfHash = text.lastIndexOf('#');
if (lastIndexOfHash !== text.length - 1) { if (lastIndexOfHash != text.length - 1) {
return text; return text;
} }
return text.substring(0, lastIndexOfHash); return text.substring(0, lastIndexOfHash);
}; }
let heading1Count = 1; let heading1Count = 1;
const maxLevel = siteConfig.toc.depth; const maxLevel = siteConfig.toc.depth;
--- ---
{isPostsRoute &&
<table-of-contents class:list={[className, "group"]}> <table-of-contents class:list={[className, "group"]}>
{headings.filter((heading) => heading.depth < minDepth + maxLevel).map((heading) => {headings.filter((heading) => heading.depth < minDepth + maxLevel).map((heading) =>
<a href={`#${heading.slug}`} class="px-2 flex gap-2 relative transition w-full min-h-9 rounded-xl <a href={`#${heading.slug}`} class="px-2 flex gap-2 relative transition w-full min-h-9 rounded-xl
@ -55,9 +52,10 @@ const maxLevel = siteConfig.toc.depth;
}]}>{removeTailingHash(heading.text)}</div> }]}>{removeTailingHash(heading.text)}</div>
</a> </a>
)} )}
<div id="active-indicator" style="opacity: 0" class:list={[{'hidden': headings.length == 0}, "-z-10 absolute bg-[var(--toc-btn-hover)] left-0 right-0 rounded-xl transition-all " + <div id="active-indicator" class:list={[{'hidden': headings.length == 0}, "-z-10 absolute bg-[var(--toc-btn-hover)] left-0 right-0 rounded-xl transition-all " +
"group-hover:bg-transparent border-2 border-[var(--toc-btn-hover)] group-hover:border-[var(--toc-btn-active)] border-dashed"]}></div> "group-hover:bg-transparent border-2 border-[var(--toc-btn-hover)] group-hover:border-[var(--toc-btn-active)] border-dashed"]}></div>
</table-of-contents>} </table-of-contents>
<script> <script>
class TableOfContents extends HTMLElement { class TableOfContents extends HTMLElement {
@ -97,7 +95,7 @@ class TableOfContents extends HTMLElement {
toggleActiveHeading = () => { toggleActiveHeading = () => {
let i = this.active.length - 1; let i = this.active.length - 1;
let min = this.active.length - 1, max = -1; let min = this.active.length - 1, max = 0;
while (i >= 0 && !this.active[i]) { while (i >= 0 && !this.active[i]) {
this.tocEntries[i].classList.remove(this.visibleClass); this.tocEntries[i].classList.remove(this.visibleClass);
i--; i--;
@ -112,15 +110,11 @@ class TableOfContents extends HTMLElement {
this.tocEntries[i].classList.remove(this.visibleClass); this.tocEntries[i].classList.remove(this.visibleClass);
i--; i--;
} }
if (min > max) { let parentOffset = this.tocEl?.getBoundingClientRect().top || 0;
this.activeIndicator?.setAttribute("style", `opacity: 0`); let scrollOffset = this.tocEl?.scrollTop || 0;
} else { let top = this.tocEntries[min].getBoundingClientRect().top - parentOffset + scrollOffset;
let parentOffset = this.tocEl?.getBoundingClientRect().top || 0; let bottom = this.tocEntries[max].getBoundingClientRect().bottom - parentOffset + scrollOffset;
let scrollOffset = this.tocEl?.scrollTop || 0; this.activeIndicator?.setAttribute("style", `top: ${top}px; height: ${bottom - top}px`);
let top = this.tocEntries[min].getBoundingClientRect().top - parentOffset + scrollOffset;
let bottom = this.tocEntries[max].getBoundingClientRect().bottom - parentOffset + scrollOffset;
this.activeIndicator?.setAttribute("style", `top: ${top}px; height: ${bottom - top}px`);
}
}; };
scrollToActiveHeading = () => { scrollToActiveHeading = () => {
@ -208,7 +202,7 @@ class TableOfContents extends HTMLElement {
this.init(); this.init();
}, { once: true }); }, { once: true });
} else { } else {
console.debug('Animation element not found'); console.warn('Animation element not found');
} }
}; };
@ -262,7 +256,6 @@ class TableOfContents extends HTMLElement {
}; };
} }
if (!customElements.get("table-of-contents")) { customElements.define("table-of-contents", TableOfContents);
customElements.define("table-of-contents", TableOfContents);
}
</script> </script>

View file

@ -1,30 +1,30 @@
--- ---
import I18nKey from "../../i18n/i18nKey"; import WidgetLayout from './WidgetLayout.astro'
import { i18n } from "../../i18n/translation"; import ButtonTag from '../control/ButtonTag.astro'
import { getTagList } from "../../utils/content-utils"; import { getTagList } from '../../utils/content-utils'
import { getTagUrl } from "../../utils/url-utils"; import { i18n } from '../../i18n/translation'
import ButtonTag from "../control/ButtonTag.astro"; import I18nKey from '../../i18n/i18nKey'
import WidgetLayout from "./WidgetLayout.astro"; import { url } from '../../utils/url-utils'
const tags = await getTagList(); const tags = await getTagList()
const COLLAPSED_HEIGHT = "7.5rem"; const COLLAPSED_HEIGHT = '7.5rem'
const isCollapsed = tags.length >= 20; const isCollapsed = tags.length >= 20
interface Props { interface Props {
class?: string; class?: string
style?: string; style?: string
} }
const className = Astro.props.class; const className = Astro.props.class
const style = Astro.props.style; const style = Astro.props.style
--- ---
<WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}> <WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}>
<div class="flex gap-2 flex-wrap"> <div class="flex gap-2 flex-wrap">
{tags.map(t => ( {tags.map(t => (
<ButtonTag href={getTagUrl(t.name)} label={`View all posts with the ${t.name.trim()} tag`}> <ButtonTag href={url(`/archive/tag/${t.name}/`)} label={`View all posts with the ${t.name} tag`}>
{t.name.trim()} {t.name}
</ButtonTag> </ButtonTag>
))} ))}
</div> </div>

View file

@ -1,18 +1,18 @@
--- ---
import { Icon } from "astro-icon/components"; import { Icon } from 'astro-icon/components'
import I18nKey from "../../i18n/i18nKey"; import { i18n } from '../../i18n/translation'
import { i18n } from "../../i18n/translation"; import I18nKey from '../../i18n/i18nKey'
interface Props { interface Props {
id: string; id: string
name?: string; name?: string
isCollapsed?: boolean; isCollapsed?: boolean
collapsedHeight?: string; collapsedHeight?: string
class?: string; class?: string
style?: string; style?: string
} }
const { id, name, isCollapsed, collapsedHeight, style } = Astro.props; const props = Astro.props
const className = Astro.props.class; const { id, name, isCollapsed, collapsedHeight, style } = Astro.props
const className = Astro.props.class
--- ---
<widget-layout data-id={id} data-is-collapsed={String(isCollapsed)} class={"pb-4 card-base " + className} style={style}> <widget-layout data-id={id} data-is-collapsed={String(isCollapsed)} class={"pb-4 card-base " + className} style={style}>
<div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2 <div class="font-bold transition text-lg text-neutral-900 dark:text-neutral-100 relative ml-8 mt-4 mb-2
@ -54,7 +54,5 @@ const className = Astro.props.class;
} }
} }
if (!customElements.get("widget-layout")) { customElements.define('widget-layout', WidgetLayout);
customElements.define("widget-layout", WidgetLayout); </script>
}
</script>

View file

@ -1,90 +1,88 @@
import type { import type {
ExpressiveCodeConfig, LicenseConfig,
LicenseConfig, NavBarConfig,
NavBarConfig, ProfileConfig,
ProfileConfig, SiteConfig,
SiteConfig, } from './types/config'
} from "./types/config"; import { LinkPreset } from './types/config'
import { LinkPreset } from "./types/config";
export const siteConfig: SiteConfig = { export const siteConfig: SiteConfig = {
title: "Fuwari", title: 'Jay',
subtitle: "Demo Site", subtitle: 'Blog',
lang: "en", // Language code, e.g. 'en', 'zh_CN', 'ja', etc. description: 'Get clear, step-by-step guidance on computer programming and coding tips. Perfect for beginners and experienced developers seeking practical solutions.',
themeColor: { lang: 'en', // 'en', 'zh_CN', 'zh_TW', 'ja', 'ko'
hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345 themeColor: {
fixed: false, // Hide the theme color picker for visitors hue: 190, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
}, fixed: false, // Hide the theme color picker for visitors
banner: { },
enable: false, banner: {
src: "assets/images/demo-banner.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/' enable: true,
position: "center", // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default src: 'assets/images/banner.jpg', // Relative to the /src directory. Relative to the /public directory if it starts with '/'
credit: { position: 'center', // Equivalent to object-position, defaults center
enable: false, // Display the credit text of the banner image credit: {
text: "", // Credit text to be displayed enable: true, // Display the credit text of the banner image
url: "", // (Optional) URL link to the original artwork or artist's page text: '60s Scifi Japan Room / DesktopHut', // Credit text to be displayed
}, url: 'https://www.desktophut.com/60s-scifi-japan-room-live-wallpaper' // (Optional) URL link to the original artwork or artist's page
}, }
toc: { },
enable: true, // Display the table of contents on the right side of the post toc: {
depth: 2, // Maximum heading depth to show in the table, from 1 to 3 enable: true, // Display the table of contents on the right side of the post
}, depth: 2 // Maximum heading depth to show in the table, from 1 to 3
favicon: [ },
// Leave this array empty to use the default favicon favicon: [ // Leave this array empty to use the default favicon
// { // {
// src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory // src: '/favicon/icon.png', // Path of the favicon, relative to the /public directory
// theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode // theme: 'light', // (Optional) Either 'light' or 'dark', set only if you have different favicons for light and dark mode
// sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes // sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes
// } // }
], ]
}; }
export const navBarConfig: NavBarConfig = { export const navBarConfig: NavBarConfig = {
links: [ links: [
LinkPreset.Home, LinkPreset.Home,
LinkPreset.Archive, LinkPreset.Archive,
LinkPreset.About, LinkPreset.About,
{ {
name: "GitHub", name: 'GitHub',
url: "https://github.com/saicaca/fuwari", // Internal links should not include the base path, as it is automatically added url: 'https://git.juyung.com/', // Internal links should not include the base path, as it is automatically added
external: true, // Show an external link icon and will open in a new tab external: true, // Show an external link icon and will open in a new tab
}, },
], ],
}; }
export const profileConfig: ProfileConfig = { export const profileConfig: ProfileConfig = {
avatar: "assets/images/demo-avatar.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/' avatar: 'assets/images/avatar.jpg', // Relative to the /src directory. Relative to the /public directory if it starts with '/'
name: "Lorem Ipsum", name: 'Jay',
bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", bio: 'Programmer, Aerospace Engineering student',
links: [ links: [
{ {
name: "Twitter", name: 'LinkTree',
icon: "fa6-brands:twitter", // Visit https://icones.js.org/ for icon codes icon: 'fa6-solid:circle-user',
// You will need to install the corresponding icon set if it's not already included url: 'https://www.juyung.com/',
// `pnpm add @iconify-json/<icon-set-name>` },
url: "https://twitter.com", {
}, name: 'GitHub',
{ icon: 'fa6-brands:github',
name: "Steam", url: 'https://git.juyung.com/',
icon: "fa6-brands:steam", },
url: "https://store.steampowered.com", {
}, name: 'Instagram',
{ icon: 'fa6-brands:instagram', // Visit https://icones.js.org/ for icon codes
name: "GitHub", // You will need to install the corresponding icon set if it's not already included
icon: "fa6-brands:github", // `pnpm add @iconify-json/<icon-set-name>`
url: "https://github.com/saicaca/fuwari", url: 'https://photos.juyung.com/',
}, },
], {
}; name: 'Twitter',
icon: 'fa6-brands:twitter',
url: 'https://social.juyung.com/',
},
],
}
export const licenseConfig: LicenseConfig = { export const licenseConfig: LicenseConfig = {
enable: true, enable: true,
name: "CC BY-NC-SA 4.0", name: 'CC BY',
url: "https://creativecommons.org/licenses/by-nc-sa/4.0/", url: 'https://creativecommons.org/licenses/by/4.0/',
}; }
export const expressiveCodeConfig: ExpressiveCodeConfig = {
// Note: Some styles (such as background color) are being overridden, see the astro.config.mjs file.
// Please select a dark theme, as this blog theme currently only supports dark background color
theme: "github-dark",
};

View file

@ -1,17 +1,19 @@
export const PAGE_SIZE = 8; export const UNCATEGORIZED = '__uncategorized__'
export const LIGHT_MODE = "light", export const PAGE_SIZE = 8
DARK_MODE = "dark",
AUTO_MODE = "auto"; export const LIGHT_MODE = 'light',
export const DEFAULT_THEME = AUTO_MODE; DARK_MODE = 'dark',
AUTO_MODE = 'auto'
export const DEFAULT_THEME = AUTO_MODE
// Banner height unit: vh // Banner height unit: vh
export const BANNER_HEIGHT = 35; export const BANNER_HEIGHT = 35
export const BANNER_HEIGHT_EXTEND = 30; export const BANNER_HEIGHT_EXTEND = 30
export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND; export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND
// The height the main panel overlaps the banner, unit: rem // The height the main panel overlaps the banner, unit: rem
export const MAIN_PANEL_OVERLAPS_BANNER_HEIGHT = 3.5; export const MAIN_PANEL_OVERLAPS_BANNER_HEIGHT = 3.5
// Page width: rem // Page width: rem
export const PAGE_WIDTH = 75; export const PAGE_WIDTH = 75

View file

@ -1,44 +1,44 @@
import type { Favicon } from "@/types/config.ts"; import type { Favicon } from '@/types/config.ts'
export const defaultFavicons: Favicon[] = [ export const defaultFavicons: Favicon[] = [
{ {
src: "/favicon/favicon-light-32.png", src: '/favicon/favicon-light-32.png',
theme: "light", theme: 'light',
sizes: "32x32", sizes: '32x32',
}, },
{ {
src: "/favicon/favicon-light-128.png", src: '/favicon/favicon-light-128.png',
theme: "light", theme: 'light',
sizes: "128x128", sizes: '128x128',
}, },
{ {
src: "/favicon/favicon-light-180.png", src: '/favicon/favicon-light-180.png',
theme: "light", theme: 'light',
sizes: "180x180", sizes: '180x180',
}, },
{ {
src: "/favicon/favicon-light-192.png", src: '/favicon/favicon-light-192.png',
theme: "light", theme: 'light',
sizes: "192x192", sizes: '192x192',
}, },
{ {
src: "/favicon/favicon-dark-32.png", src: '/favicon/favicon-dark-32.png',
theme: "dark", theme: 'dark',
sizes: "32x32", sizes: '32x32',
}, },
{ {
src: "/favicon/favicon-dark-128.png", src: '/favicon/favicon-dark-128.png',
theme: "dark", theme: 'dark',
sizes: "128x128", sizes: '128x128',
}, },
{ {
src: "/favicon/favicon-dark-180.png", src: '/favicon/favicon-dark-180.png',
theme: "dark", theme: 'dark',
sizes: "180x180", sizes: '180x180',
}, },
{ {
src: "/favicon/favicon-dark-192.png", src: '/favicon/favicon-dark-192.png',
theme: "dark", theme: 'dark',
sizes: "192x192", sizes: '192x192',
}, },
]; ]

View file

@ -1,18 +1,18 @@
import I18nKey from "@i18n/i18nKey"; import { LinkPreset, type NavBarLink } from '@/types/config'
import { i18n } from "@i18n/translation"; import I18nKey from '@i18n/i18nKey'
import { LinkPreset, type NavBarLink } from "@/types/config"; import { i18n } from '@i18n/translation'
export const LinkPresets: { [key in LinkPreset]: NavBarLink } = { export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
[LinkPreset.Home]: { [LinkPreset.Home]: {
name: i18n(I18nKey.home), name: i18n(I18nKey.home),
url: "/", url: '/',
}, },
[LinkPreset.About]: { [LinkPreset.About]: {
name: i18n(I18nKey.about), name: i18n(I18nKey.about),
url: "/about/", url: '/about/',
}, },
[LinkPreset.Archive]: { [LinkPreset.Archive]: {
name: i18n(I18nKey.archive), name: i18n(I18nKey.archive),
url: "/archive/", url: '/archive/',
}, },
}; }

View file

@ -1,28 +1,24 @@
import { defineCollection, z } from "astro:content"; import { defineCollection, z } from 'astro:content'
const postsCollection = defineCollection({ const postsCollection = defineCollection({
schema: z.object({ schema: z.object({
title: z.string(), title: z.string(),
published: z.date(), published: z.date(),
updated: z.date().optional(), updated: z.date().optional(),
draft: z.boolean().optional().default(false), draft: z.boolean().optional().default(false),
description: z.string().optional().default(""), description: z.string().optional().default(''),
image: z.string().optional().default(""), image: z.string().optional().default(''),
tags: z.array(z.string()).optional().default([]), tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""), category: z.string().optional().default(''),
lang: z.string().optional().default(""), lang: z.string().optional().default(''),
/* For internal use */ /* For internal use */
prevTitle: z.string().default(""), prevTitle: z.string().default(''),
prevSlug: z.string().default(""), prevSlug: z.string().default(''),
nextTitle: z.string().default(""), nextTitle: z.string().default(''),
nextSlug: z.string().default(""), nextSlug: z.string().default(''),
}), }),
}); })
const specCollection = defineCollection({
schema: z.object({}),
});
export const collections = { export const collections = {
posts: postsCollection, posts: postsCollection,
spec: specCollection, }
};

View file

@ -1,22 +0,0 @@
---
title: Draft Example
published: 2022-07-01
tags: [Markdown, Blogging, Demo]
category: Examples
draft: true
---
# This Article is a Draft
This article is currently in a draft state and is not published. Therefore, it will not be visible to the general audience. The content is still a work in progress and may require further editing and review.
When the article is ready for publication, you can update the "draft" field to "false" in the Frontmatter:
```markdown
---
title: Draft Example
published: 2024-01-11T04:40:26.381Z
tags: [Markdown, Blogging, Demo]
category: Examples
draft: false
---

View file

@ -0,0 +1,118 @@
---
title: Initial Server Setup and Security Essentials for Debian
lang: en
published: 2024-08-25T06:52:46.241Z
description: Secure your Debian server by setting up passwordless SSH, changing SSH port, and disabling root login.
image: ""
tags:
- Linux
- Security
- Server
category: Cybersecurity
draft: false
---
# Initial Server Setup and Security Essentials for Debian
This guide covers essentials for setting up and securing your Debian or Ubuntu VPS, including disabling root login, passwordless SSH, and changing SSH port.
## Prerequisites
- VPS with root access, such as a DigitalOcean Droplet or Hetzner Cloud instance
Connect to your server using SSH:
```bash
ssh root@1.2.3.4
```
Update your server:
```bash
apt update && apt upgrade -y
```
## 1. Disable Root Login
Using a non-root user prevents accidental changes to system files and reduces attack risks, as the root account is a common target for attackers.
Create new user:
```bash
adduser username
```
Give new user sudo privileges:
```bash
usermod -aG sudo username
```
Open the SSH configuration file:
```bash
sudo nano /etc/ssh/sshd_config
```
Locate the line with `#PermitRootLogin yes` and change it to `PermitRootLogin no`.
```
#PermitRootLogin yes
PermitRootLogin no
```
## 2. Passwordless SSH Authentication
SSH authentication lets you log in without typing a password. The server checks if the private key on your machine matches the public key on the server. If they match, you get access.
On your local machine, generate a new SSH key pair:
```bash
ssh-keygen -t ed25519 -a 100 -f ~/.ssh/id_ed25519_keyname -C "keyname"
```
Back up your SSH key, located at `~/.ssh`.
Copy your public SSH key to your server:
```bash
ssh-copy-id -i ~/.ssh/id_ed25519_keyname.pub username@1.2.3.4
```
Log in to your server and open the SSH configuration file:
```bash
sudo nano /etc/ssh/sshd_config
```
Locate the line with `#PasswordAuthentication yes` and change it to `PasswordAuthentication no`.
```
#PasswordAuthentication yes
PasswordAuthentication no
```
Run:
```bash
sudo visudo
```
Look for the line with `%sudo ALL=(ALL:ALL) ALL` and change it.
```bash
%sudo ALL=(ALL:ALL) NOPASSWD: ALL
```
Change SSH key every few years. Remove the old key from `~/.ssh/authorized_keys` on your server.
## 3. Change the Default SSH Port
By default, SSH listens on port 22. Because it's well-known, port 22 is a common target for automated attacks and brute force attempts. Changing SSH port can reduce the volume of these attacks.
Open the SSH configuration file:
```bash
sudo nano /etc/ssh/sshd_config
```
Locate the line with `#Port 22` and change it to `Port 2222`.
```
#Port 22
Port 2222
```
Restart SSH service:
```bash
sudo systemctl restart ssh
```
Use the new port to connect:
```bash
ssh username@1.2.3.4 -p 2222
```

View file

@ -0,0 +1,175 @@
---
title: Step-by-Step Guide to Installing Mailcow with Docker
lang: en
published: 2024-08-26T22:52:47.188Z
description: Learn how to install Mailcow with Docker using this guide.
image: ""
tags:
- Mail Server
- Mailcow
- Docker
category: Networking
draft: false
---
# Step-by-Step Guide to Installing Mailcow with Docker
Switch from Gmail and set up Mailcow with Docker to host your own email server.
## Prerequisites
- Debian/Ubuntu VPS from a hosting provider that allows port 25. If port 25 is blocked, you can receive but not send emails.
## Docker Installation
Download Docker:
```bash
sudo apt-get install -y ca-certificates curl && sudo mkdir -p /etc/apt/keyrings && sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && sudo chmod a+r /etc/apt/keyrings/docker.asc && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null && sudo apt-get update
```
Install Docker and Docker Compose:
```bash
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
*(Retrieved from [Docker documentation](https://docs.docker.com/engine/install/) on August 25, 2024)*
Add yourself to the Docker group to allow you running Docker commands without needing sudo every time. Replace username with your own:
```bash
sudo usermod -aG docker username
```
Start Docker on boot:
```bash
sudo systemctl enable docker
```
## Mailcow
Download Mailcow:
```bash
mkdir ~/docker; cd ~/docker
git clone https://github.com/mailcow/mailcow-dockerized; cd mailcow-dockerized
./generate_config.sh
```
When prompted, enter your mail server hostname (e.g., `mail.example.com`) and your timezone (e.g., `US/Pacific`).
Edit configuration file to reduce RAM usage.
```bash
nano mailcow.conf
```
Find and set the following lines:
```
SKIP_CLAMD=y
SKIP_SOGO=y
SKIP_SOLR=y
```
Start Mailcow:
```bash
docker compose up -d
```
Go to http://mail.example.com and log in with the username `admin` and password `moohoo`. Change your password in **System > Configuration**.
Go to **E-Mail > Configuration** and add a new domain.
On the same page, click **DNS** to view DNS records. Update these records with your hosting provider or domain registrar.
Go to **Mailboxes** tab and create a new mailbox.
## Reverse DNS
Set reverse DNS (PTR record) to `mail.example.com` for both IPv4 and IPv6 at your hosting provider. For examples, see [Hetzner Cloud](https://docs.hetzner.com/cloud/servers/cloud-server-rdns/) or [Linode](https://techdocs.akamai.com/cloud-computing/docs/configure-rdns-reverse-dns-on-a-compute-instance).
## Thunderbird
Open Thunderbird and connect to your mailbox.
- IMAP (receiving emails)
Hostname: mail.example.com
Port: **993**
Connection security: **SSL/TLS**
- SMTP (sending emails)
Hostname: mail.example.com
Port: **465**
Connection security: **SSL/TLS**
Create an OpenPGP key in Thunderbirds settings to send encrypted emails.
## Reverse Proxy
Ports 80 and 443 are often used by web servers. Change Mailcows ports to avoid conflicts with other servers on your VPS.
```bash
nano mailcow.conf
```
Change `HTTP_PORT=80` and `HTTPS_PORT=443`:
```
HTTP_PORT=8081
HTTPS_PORT=8443
```
Restart Mailcow:
```bash
docker compose down; docker compose up -d
```
Install Caddy:
```bash
sudo apt install caddy
```
Open Caddyfile:
```bash
nano /etc/caddy/Caddyfile
```
Update Caddyfile:
```
example.com {
route /mail* {
uri strip_prefix /mail
redir https://mail.{host}{uri}
}
}
mail.example.com {
reverse_proxy localhost:8081
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Mailcow admin dashboard at https://mail.example.com or at https://example.com/mail if you prefer using a subpath.
However, it's not safe to leave the admin dashboard available on the internet all the time.
To disable access to the admin dashboard, add `responnd 403` to your Caddyfile:
```
mail example.com {
reverse_proxy localhost:8081
respond 403
}
```
To enable access to the admin dashboard, comment out `respond 403` in your Caddyfile:
```
mail example.com {
reverse_proxy localhost:8081
#respond 403
}
```

View file

@ -0,0 +1,83 @@
---
title: How to Install Actual Budget with Docker on Debian/Ubuntu
lang: en
published: 2024-08-31T21:12:19.632Z
description: Quickly set up Actual Budget on Debian or Ubuntu using Docker with this easy guide.
image: ""
tags:
- Budgeting
- Actual
- Docker
category: DevOps
draft: false
---
# How to Install Actual Budget with Docker on Debian/Ubuntu
Actual Budget is a free open-source web budgeting app. Similar tools include Mint and YNAB (You Need a Budget). This easy guide will show you how to set up Actual Budget on Debian or Ubuntu using Docker, so you can get your budgeting app up and running quickly.
## Actual Budget
Download Actual Budget:
```bash
mkdir ~/docker
cd ~/docker
git clone https://github.com/actualbudget/actual-server.git actualbudget
cd actualbudget
```
## Docker
Start Actual Budget Docker containers:
```bash
docker compose up -d
```
## Reverse Proxy
Install Caddy:
```bash
sudo apt install caddy
```
Open Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
Update Caddyfile:
```
example.com {
route /cash* {
uri strip_prefix /cash
redir https://cash.{host}{uri}
}
}
cash.example.com {
reverse_proxy localhost:5006
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Actual Budget at https://cash.example.com or at https://example.com/cash if you prefer using a subpath.
Make sure to use a strong password and stop your Actual Budget when it's not in use. This will help reduce the risk of unauthorized access.
Stop Actual Budget:
```bash
docker compose down
```
Start Actual Budget:
```bash
docker compose up -d
```

View file

@ -0,0 +1,149 @@
---
title: How to Install Forgejo with Docker and Migrate from GitHub
lang: en
published: 2024-09-07T06:12:40.076Z
description: Set up Forgejo with Docker as a self-hosted alternative to GitHub. This guide will show you how to install Forgejo and migrate your GitHub repositories.
image: ""
tags:
- GitHub
- Forgejo
- Docker
category: Software Engineering
draft: false
---
# How to Install Forgejo with Docker and Migrate from GitHub
Learn how to set up Forgejo with Docker and move your code from GitHub. This simple guide makes switching to a self-hosted platform easy.
## Docker
Create a new folder named forgejo:
```bash
mkdir ~/docker
cd ~/docker
mkdir forgejo
```
Create docker-compose.yml:
```bash
nano docker-compose.yml
```
Modify docker-compose.yml and save the changes:
```yaml
version: '3'
networks:
forgejo:
external: false
services:
server:
image: codeberg.org/forgejo/forgejo:7
container_name: forgejo
environment:
- USER_UID=1000
- USER_GID=1000
restart: always
networks:
- forgejo
volumes:
- ./forgejo:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- '3000:3000'
- '222:22'
```
>[!NOTE]
>Check the official Forgejo website for the latest docker-compose file to install the most recent version.
*(Retrieved from [Forgejo documentation](https://forgejo.org/docs/latest/admin/installation-docker/) on September 6, 2024)*
Port 3000 is commonly used for web servers. Use a different port to avoid conflicts with other web servers. Change port from `3000:3000` to `3003:3003` in docker-compose.yml.
Start Docker:
```bash
docker compose up -d
```
Check if containers are running properly:
```bash
docker compose ps
```
Go to Forgejo configuration page at http://example.com:3000 and create your account. To reduce RAM usage, set the database type to SQLite and disable self-registration. Then, save the configuration.
Open Forgejo configuration file:
```bash
nano forgejo/gitea/conf/app.ini
```
Set `DISABLE_REGISTRATION` to `true`, and add `ENABLE_REVERSE_PROXY_AUTHENTICATION = true` in the [service] section and `LANDING_PAGE = explore` in the [server] section:
```ini
DISABLE_REGISTRATION = true
[service]
ENABLE_REVERSE_PROXY_AUTHENTICATION = true
[server]
LANDING_PAGE = explore
```
The `LANDING_PAGE` setting decides what users see when they go to the home page. It can be set to `home`, `explore`, `organizations`, `login` or custom URL /custom or https://example.com/custom.
*(Retrieved from [Forgejo documentation](https://forgejo.org/docs/latest/admin/config-cheat-sheet/) on September 6, 2024)*
Restart Docker:
```bash
docker compose down; docker compose up -d
```
## Reverse Proxy
Install Caddy:
```bash
sudo apt install caddy
```
Open Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
Update Caddyfile:
```
example.com {
route /git* {
uri strip_prefix /git
redir https://git.{host}{uri}
}
}
git.example.com {
reverse_proxy localhost:3003
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Forgejo at https://git.example.com or at https://example.com/git if you prefer using a subpath.
## Migrate Repositories from GitHub
1. Create an access token in GitHub settings. This [GitHub documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) provides a step-by-step guide on how to create the token.
2. Log in to Forgejo.
3. Click the plus sign (+) in the upper right corner and select **New Migration**.
4. Enter GitHub repository URL and your access token, then click **Migrate Repository** button.

View file

@ -0,0 +1,120 @@
---
title: LinkStack with Docker Self-hosted Linktree Alternative
lang: en
published: 2024-09-15T10:25:08.328Z
description: Learn how to set up LinkStack, a self-hosted Linktree alternative, using Docker for simple and easy deployment.
image: ""
tags:
- Linktree
- LinkStack
- Docker
category: DevOps
draft: false
---
# LinkStack with Docker Self-hosted Linktree Alternative
Learn how to set up LinkStack, a self-hosted Linktree alternative, using Docker for simple and easy deployment.
## Docker
Create a folder named linkstack:
```bash
mkdir ~/docker
cd ~/docker
mkdir linkstack
```
Create docker-compose.yml:
```bash
nano docker-compose.yml
```
Modify docker-compose.yml and save the changes:
```yaml
version: "3.8"
services:
linkstack:
hostname: 'linkstack'
image: 'linkstackorg/linkstack:latest'
environment:
TZ: 'America/Los_Angeles'
SERVER_ADMIN: 'admin@example.com'
HTTP_SERVER_NAME: 'www.example.com'
HTTPS_SERVER_NAME: 'www.example.com'
LOG_LEVEL: 'info'
PHP_MEMORY_LIMIT: '256M'
UPLOAD_MAX_FILESIZE: '8M'
volumes:
- 'linkstack:/htdocs'
ports:
- '8442:443'
restart: unless-stopped
volumes:
linkstack:
```
Update `TZ`, `SERVER_ADMIN`, `HTTP_SERVER_NAME`, and `HTTPS_SERVER_NAME`. Find your timezone code at [PHP List of Supported Timezones](https://www.php.net/manual/en/timezones.php).
*(Retrieved from [LinkStack Docker Hub](https://hub.docker.com/r/linkstackorg/linkstack) on September 15, 2024)*
Start Docker:
```bash
docker compose up -d
```
## Reverse Proxy
Install Caddy:
```bash
sudo apt install caddy
```
Open Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
Update Caddyfile:
```
example.com {
route / {
redir https://www.{host}
}
}
www.example.com {
reverse_proxy localhost:8442 {
transport http {
tls_insecure_skip_verify
}
}
}
```
*(Retrieved from [LinkStack documentation](https://docs.linkstack.org/docker/reverse-proxies/#caddy) on September 15, 2024)*
If someone visits https://example.com, they are automatically sent to https://www.example.com instead. It makes LinkStack the main page for your website.
Restart Caddy:
```bash
sudo systemctl restart caddy
```
## LinkStack
Go to LinkStack at https://example.com. You can access the login page at https://www.example.com/login.
1. Set database type to sqlite.
2. Disable registration and email verification.
3. Set your page as the Home Page if youre the only user. If you do, your LinkStack profile link will be https://www.example.com; otherwise, it will be https://www.example.com/@username.
4. To hide the LinkStack logo, log in and go to **Admin > Config**. In the **Footer Links** section, uncheck "Show Footer," "Display Credit on User Pages," and "Display Credit in Footer".

View file

@ -0,0 +1,170 @@
---
title: Install Vaultwarden with Docker and Harden Its Security
lang: en
published: 2024-10-05T06:35:04.819Z
description: Install Vaultwarden easily with Docker and learn basic steps to keep it secure. Create and manage unique passwords for all your online accounts safely.
image: ""
tags:
- Vaultwarden
- Docker
- Password Manager
category: Cybersecurity
draft: false
---
# Install Vaultwarden with Docker and Harden Its Security
Install Vaultwarden easily with Docker and learn basic steps to keep it secure. Create and manage unique passwords for all your online accounts safely.
>[!IMPORTANT]
>You should never store Bitcoin wallet passphrases in Vaultwarden or any digital format.
## Docker
Enable running Docker without sudo. Replace "username" with your own:
```bash
sudo usermod -aG docker username
```
Create a folder named vaultwarden:
```bash
mkdir ~/docker
cd ~/docker
mkdir vaultwarden
```
Create docker-compose.yml:
```bash
nano docker-compose.yml
```
Edit docker-compose.yml:
```yaml
version: '3.8'
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
user: 1000:1000
ports:
- "7789:80"
volumes:
- ./volumes/vw-data/:/data/
restart: unless-stopped
environment:
- ADMIN_TOKEN=insecure
```
Start Docker:
```bash
docker compose up -d
```
## Reverse Proxy
Install Caddy:
```bash
sudo apt install caddy
```
Open Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
Update Caddyfile:
```
example.com {
route /pass* {
uri strip_prefix /pass
redir https://pass.{host}{uri}
}
}
pass.example.com {
reverse_proxy localhost:7789
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Vaultwarden at https://pass.example.com or at https://example.com/pass if you prefer using a subpath.
## Security
### 1. Disable Registration
Before proceeding, create new accounts for yourself and your family.
Go to the admin panel at https://pass.example.com/admin. Enter "insecure" as the admin token.
Go to **General Settings** and uncheck **Allow new signups**.
### 2. Strong Admin Token
On your local machine, run the following commands. Replace "Insecure Password" with new admin password, like a 12-word passphrase or a password with 50+ characters.
```bash
sudo apt install argon2
echo -n "Insecure Password" | argon2 "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4
```
*(Retrieved from [Vaultwarden wiki](https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page#using-argon2) on October 5, 2024)*
The output will start with `($argon2id$v=19$m=65540,t=3,p=4$...)`, which is the salt. Go to **General Settings** and enter the salt in **Admin token/Arg2 PHC** field.
Save your changes and log out. When you log back in, use your admin password.
Comment out the environment section in docker-compose.yml:
```yml
# environment:
# - ADMIN_TOKEN=insecure
```
Restart Docker:
```bash
docker compose down; docker compose up -d
```
### 3. Restrict Admin Panel
Redirect anyone trying to access the admin panel to homepage.
Update Caddyfile:
```
pass.example.com {
reverse_proxy localhost:7789
rewrite /admin* /
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
### 4. Disallow Search Engine Indexing
Prevent your Vaultwarden site from being indexed by Google. When you search for "Vaultwarden Web", you might find other people's Vaultwarden sites and their admin panels.
Open robots.txt:
```bash
sudo nano /var/www/html/robots.txt
```
Update robots.txt:
```txt
User-agent: *
Disallow: /pass
Allow: /$
```

View file

@ -0,0 +1,142 @@
---
title: How to Install Misskey with Docker as a Twitter Alternative
lang: en
published: 2024-10-12T03:41:20.791Z
description: Discover how to easily install Misskey using Docker and explore a fresh, decentralized alternative to Twitter.
image: ""
tags:
- Misskey
- Twitter
- Docker
category: DevOps
draft: false
---
# How to Install Misskey with Docker as a Twitter Alternative
Discover how to easily install Misskey using Docker and explore a fresh, decentralized alternative to Twitter.
## Docker
Enable running Docker without sudo. Replace "username" with your own:
```bash
sudo usermod -aG docker username
```
Download Misskey:
```bash
mkdir ~/docker
cd ~/docker
git clone -b master https://github.com/misskey-dev/misskey.git misskey
cd misskey
```
Create Misskey configuration files:
```bash
cp .config/docker_example.yml .config/default.yml
cp .config/docker_example.env .config/docker.env
cp ./compose_example.yml ./docker-compose.yml
```
*(Retrieved from [Misskey documentation](https://misskey-hub.net/en/docs/for-admin/install/guides/docker/) on October 11, 2024)*
Open docker-compose.yml:
```bash
nano docker-compose.yml
```
Change ports to `4004:4004`:
```yaml
services:
web:
ports:
- "4004:4004"
```
Open docker.env:
```bash
nano .config/docker.env
```
Set database username and password. Password must be between 8 and 128 characters long and can't contain /, ", or @:
```env
db:
POSTGRES_PASSWORD=password
POSTGRES_USER=misskey-admin
```
Open default.yml:
```bash
nano .config/default.yml
```
Copy database username and password from docker.env:
```yml
db:
user: misskey-admin
pass: password
```
Build Docker image:
```bash
docker compose build
docker compose run --rm web pnpm run init
```
Start Docker:
```bash
docker compose up -d
```
## Reverse Proxy
Install Caddy:
```bash
sudo apt install caddy
```
Open Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
Update Caddyfile:
```
example.com {
route /social* {
uri strip_prefix /social
redir https://social.{host}{uri}
}
}
social.example.com {
reverse_proxy localhost:4004
# Uncomment the line below to set your profile as the homepage.
# redir / /@username
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Misskey at https://social.example.com or at https://example.com/social if you prefer using a subpath.
Create your account. Your profile link is https://social.example.com/@username and https://example.com/social/@username.
In **Control Panel** > **Moderation**, uncheck **Enable new user registration**.
## Folder Permission
Enable uploading pictures and videos:
```bash
sudo chown -hR 991:991 ./files
```
*(Retrieved from [Misskey GitHub](https://github.com/misskey-dev/misskey/issues/9564#issuecomment-1382743169) on October 11, 2024)*

View file

@ -0,0 +1,120 @@
---
title: Installing Photoprism with Docker for Photo Management
lang: en
published: 2024-10-26T13:43:55.115Z
description: Set up PhotoPrism on Docker with this simple and fast guide to easily manage your photos.
image: ""
tags:
- Photoprism
- Instagram
- Docker
category: DevOps
draft: false
---
# Installing Photoprism with Docker for Photo Management
Set up Photoprism on Docker with this simple and fast guide to easily manage your photos.
## Docker
Enable running Docker without sudo. Replace "username" with your own:
```bash
sudo usermod -aG docker username
```
Download Photoprism:
```bash
mkdir photoprism
cd photoprism
wget https://dl.photoprism.app/docker/compose.yaml -O docker-compose.yml
```
Open docker-compose.yml:
```bash
nano docker-compose.yml
```
Set ports and environment variables as follows:
```yml
services:
photoprism:
ports:
- "6342:2342"
environment:
PHOTOPRISM_ADMIN_USER: "your username"
PHOTOPRISM_ADMIN_PASSWORD: "your password"
PHOTOPRISM_BACKUP_DATABASE: "false"
PHOTOPRISM_SITE_URL: "https://photos.example.com"
PHOTOPRISM_DEFAULT_TLS: "false"
PHOTOPRISM_DISABLE_TENSORFLOW: "true"
PHOTOPRISM_DISABLE_FACES: "true"
PHOTOPRISM_DISABLE_CLASSIFICATION: "true"
PHOTOPRISM_BACKUP_DATABASE: "false"
PHOTOPRISM_DATABASE_PASSWORD: "database password"
PHOTOPRISM_SITE_CAPTION: "Photoprism"
PHOTOPRISM_SITE_AUTHOR: "your name"
```
Update database password:
```yml
mariadb:
environment:
MARIADB_PASSWORD: "same as PHOTOPRISM_DATABASE_PASSWORD above"
MARIADB_ROOT_PASSWORD: "same as PHOTOPRISM_DATABASE_PASSWORD above"
```
Comment out the entire watchtower section:
```yml
# watchtower:
# restart: unless-stopped
```
Start Photoprism:
```bash
docker compose up -d
```
## Reverse Proxy
Install Caddy:
```bash
sudo apt install caddy
```
Open Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
Update Caddyfile:
```
example.com {
route /photos* {
uri strip_prefix /photos
redir https://photos.{host}{uri}
}
}
photos.example.com {
reverse_proxy localhost:6342
rewrite / /s/gallery/album
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Photoprism at https://photos.example.com or at https://example.com/photos if you prefer using a subpath. Login using the `PHOTOPRISM_ADMIN_USER` and `PHOTOPRISM_ADMIN_PASSWORD` you set in the docker-compose.yml above.
Create a new album and share it using secret key "gallery". Anyone with your Photoprism link can view your album.

View file

@ -0,0 +1,81 @@
---
title: Docker Backup and Restore in Two Simple Steps
lang: en
published: 2025-01-02T12:40:52.203Z
description: Learn how to create a compressed backup of your Docker environment, including containers, volumes, and Caddy configurations, and easily restore it when needed.
image: ""
tags:
- Linux
- Server
- Docker
category: DevOps
draft: false
---
# Docker Backup and Restore in Two Simple Steps
Learn how to create a compressed backup of your Docker environment, including containers, volumes, and Caddy configurations, and easily restore it when needed.
## Backup
1. On your remote server, run:
```bash
(cd ~/ && sudo systemctl stop docker && sudo tar -cpf - ~/docker /var/lib/docker/volumes /etc/caddy | zstd -3 -o "backup-$(date +'%Y%m%d_%H%M%S').tar.zst" && sudo systemctl start docker)
```
This command stops Docker, creates a compressed backup of your Docker data (including containers, volumes, and Caddy configuration) in the home directory, and then restarts Docker.
- Replace `~/docker` with the path to the folder where your Docker containers are stored. You can also specify multiple folders by separating them with spaces, like `~/docker ~/astro`.
- If you don't use Caddy for reverse proxying, remove `/etc/caddy` from the command.
2. On your local computer, run:
```bash
rsync -ahPvz -e 'ssh -p 2222' root@1.2.3.4:~/backup-$(date +'%Y%m%d')*.tar.zst .
```
This command transfers the backup file from your remote server to your local computer.
- Replace `root@1.2.3.4` with your server's address.
- If you don't need to specify an SSH port, replace `'ssh -p 2222'` with `ssh`.
## Restore
Backup your remote server before proceeding, in case the restore overwrites important data and you need to recover it.
1. On your local computer, run:
```bash
sudo tar --use-compress-program=zstd -xvf backup-$(date +'%Y%m%d')*.tar.zst
```
This command extracts the backup file.
```bash
rsync -ahvzP -e 'ssh -p 2222' /local/path/ user@1.2.3.4:/remote/path/
```
This command copies the extracted files from your local computer to your remote server.
- Replace `/local/path/` with the folder on your local computer, and `/remote/path/` with the folder on your remote server.
## Quick Reference
### Backup
```bash
# On VPS
(cd ~/ && sudo systemctl stop docker && sudo tar -cpf - ~/docker /var/lib/docker/volumes /etc/caddy | zstd -3 -o "backup-$(date +'%Y%m%d_%H%M%S').tar.zst" && sudo systemctl start docker)
# On local computer
rsync -ahPvz -e 'ssh -p 2222' user@1.2.3.4:~/backup-$(date +'%Y%m%d')*.tar.zst .
```
### Restore
```bash
# On local computer
sudo tar --use-compress-program=zstd -xvf backup-$(date +'%Y%m%d')*.tar.zst
rsync -ahvzP -e 'ssh -p 2222' docker/ user@1.2.3.4:~/docker/
rsync -ahvzP -e 'ssh -p 2222' docker/forgejo/ user@1.2.3.4:~/docker/forgejo/
```

View file

@ -0,0 +1,98 @@
---
title: Install Traggo and Start Tracking Your Time Efficiently
lang: en
published: 2025-01-04T04:00:00.939Z
description: Find out how to easily set up Traggo and track your time.
image: ""
tags:
- Docker
- Time Tracker
- Traggo
category: DevOps
draft: false
---
# Install Traggo and Start Tracking Your Time Efficiently
Find out how to easily set up Traggo and track your time.
## Docker Compose
Enable running Docker without sudo. Replace "username" with your own:
```bash
sudo usermod -aG docker username
```
Create a folder named traggo:
```bash
mkdir traggo
cd traggo
```
When you need to back up Traggo, simply copy the traggo folder.
Edit docker-compose.yml:
```bash
nano docker-compose.yml
```
```yaml
version: "3.7"
services:
traggo:
image: traggo/server:latest
ports:
- 9090:3030
environment:
TRAGGO_DEFAULT_USER_NAME: "****"
TRAGGO_DEFAULT_USER_PASS: "****"
volumes:
- ./traggodata:/opt/traggo/data
```
Change `TRAGGO_DEFAULT_USER_NAME` and `TRAGGO_DEFAULT_USER_PASS`.
Start Traggo:
```bash
docker compose up -d
```
## Reverse Proxy
Install Caddy:
```bash
sudo apt install caddy
```
Edit Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
```
example.com {
route /clock* {
uri strip_prefix /clock
redir https://clock.{host}{uri}
}
}
clock.example.com {
reverse_proxy localhost:9090
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Traggo at https://clock.example.com or at https://example.com/clock if you prefer using a subpath. Log in to your account using `TRAGGO_DEFAULT_USER_NAME` and `TRAGGO_DEFAULT_USER_PASS`.
>[!TIP]
>Go to Tag menu and create categories like personal, work, social, recreation, learning, health, chores, sleep, travel, and other.

View file

@ -0,0 +1,87 @@
---
title: How to Install Homarr Personal Dashboard in Docker
lang: en
published: 2025-01-11T14:09:42.251Z
description: Install Homarr and customize your dashboard by adding widgets like a clock, weather, app shortcuts, and iframes.
image: ""
tags:
- Docker
- Dashboard
- Homarr
category: DevOps
draft: false
---
# How to Install Homarr Personal Dashboard in Docker
Install Homarr and customize your dashboard by adding widgets like a clock, weather, app shortcuts, and iframes.
## Docker Compose
Create a folder named homarr:
```bash
mkdir homarr
cd homarr
```
Edit docker-compose.yml:
```bash
nano docker-compose.yml
```
```yaml
version: '3'
services:
homarr:
container_name: homarr
image: ghcr.io/ajnart/homarr:latest
restart: unless-stopped
volumes:
- ./homarr/configs:/app/data/configs
- ./homarr/icons:/app/public/icons
- ./homarr/data:/data
ports:
- '7575:7575'
```
Start Homarr:
```bash
docker compose up -d
```
## Reverse Proxy
Install Caddy:
```bash
sudo apt install caddy
```
Edit Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
```
example.com {
route /home* {
uri strip_prefix /home
redir https://home.{host}{uri}
}
}
home.example.com {
reverse_proxy localhost:7575
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Homarr at https://home.example.com or at https://example.com/home if you prefer using a subpath.

View file

@ -0,0 +1,85 @@
---
title: How to Check Your CPU, RAM, and Disk Space Usage on Linux
lang: en
published: 2025-01-12T13:19:30.236Z
description: Learn common commands to check your CPU, RAM, and disk space usage on Linux, and keep your system running smoothly.
image: ""
tags:
- Linux
- Server
- Performance
category: Operating Systems
draft: false
---
# How to Check Your CPU, RAM, and Disk Space Usage on Linux
Learn common commands to check your CPU, RAM, and disk space usage on Linux, and keep your system running smoothly.
## CPU
```bash
htop
```
`htop` shows CPU usage for each processor in real-time in an interactive display.
Output:
```bash
0[|| 4.6%] Tasks: 267, 882 thr, 86 kthr; 1 running
1[|||| 7.9%] Load average: 0.17 0.25 0.27
Mem[|||||||||||||||||||||3.29G/3.73G] Uptime: 17:32:18
Swp[ 0K/0K]
```
## RAM
```bash
free -m
```
`free -m` shows memory usage in megabytes (MB). It displays several columns, but you usually want to look at the "total", "used" and "available" columns.
Output:
```bash
total used free shared buff/cache available
Mem: 3819 3593 115 33 385 226
Swap: 0 0 0
```
- "total" shows installed memory.
- "used" shows memory in use by processes.
- "available" shows free memory for new processes.
## Disk Space
```
df -h
```
`df -h` shows the total, used, and available disk space on your file systems.
Output:
```bash
Filesystem Size Used Avail Use% Mounted on
udev 1.9G 0 1.9G 0% /dev
tmpfs 382M 3.0M 379M 1% /run
/dev/sda1 38G 25G 12G 69% /
tmpfs 1.9G 0 1.9G 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
/dev/sda15 241M 138K 241M 1% /boot/efi
tmpfs 382M 0 382M 0% /run/user/1000
```
## Quick Reference
```bash
# CPU
htop
# RAM
free -m
# Disk space
df -h
```

View file

@ -0,0 +1,200 @@
---
title: Using Caddy to Host Websites and Docker Containers
lang: en
published: 2025-01-19T23:09:24.519Z
description: Learn how to use Caddy to host static websites and forward traffic to Docker containers.
image: ""
tags:
- Caddy
- Server
- Reverse Proxy
category: Networking
draft: false
---
# Using Caddy to Host Websites and Docker Containers
Learn how to use Caddy to host static websites and forward traffic to Docker containers.
## Installing Caddy
Install Caddy:
```bash
apt install caddy
```
Set Caddy to start automatically on boot:
```bash
sudo systemctl enable caddy
```
## Configuring Caddyfile
Create `var/www/html` directory and go into it:
```bash
mkdir -p /var/www/html
cd /var/www/html
sudo chown -R username:username /var/www/
```
### Static Website
Move your website files (HTML, CSS, JavaScript) into the `var/www/html` directory.
Open Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
Edit Caddyfile:
```
example.com {
root * /var/www/html
file_server
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
You can go to your website at https://example.com.
To use a subdomain, add this configuration to Caddyfile:
```
example.com {
root * /var/www/html
file_server
encode zstd gzip
}
subdomain.example.com {
root * /var/www/html/folder
file_server
encode zstd gzip
}
```
Restart Caddy.
### Reverse Proxy
Start a Docker application on port 4321.
Open Caddyfile:
```bash
sudo nano /etc/caddy/Caddyfile
```
Configure Caddyfile:
```
example.com {
route /subdomain* {
uri strip_prefix /subdomain
redir https://subdomain.{host}{uri}
}
root * /var/www/html
file_server
encode zstd gzip
}
subdomain.example.com {
reverse_proxy localhost:4321
encode zstd gzip
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
You can go to your Docker application at https://subdomain.example.com.
To use 'www' subdomain, add this configuration to Caddyfile:
```
example.com {
root * /var/www/html
file_server
encode zstd gzip
route / {
redir https://www.{host}{uri}
}
}
www.example.com {
reverse_proxy localhost:4321
encode zstd gzip
}
```
Restart Caddy.
## Example
Heres a Caddyfile example for serving multiple Docker containers and static websites:
```
# Ports
# 8080: docker-app1 /sub1
# 8081: docker-app2 /sub2
# : site1 /sub3
# : site2 /sub4
example.com {
route /sub1* {
uri strip_prefix /sub1
redir https://sub1.{host}{uri}
}
route /sub2* {
uri strip_prefix /sub2
redir https://sub2.{host}{uri}
}
root * /var/www/html
file_server
encode zstd gzip
}
sub1.example.com {
reverse_proxy localhost:8081
encode zstd gzip
}
sub2.example.com {
reverse_proxy localhost:8082
encode zstd gzip
}
sub3.example.com {
root * /var/www/html/site1
file_server
encode zstd gzip
}
sub4.example.com {
root * /var/www/html/site2
file_server
encode zstd gzip
}
```
> [!NOTE]
> The `encode zstd gzip` compresses your website's content. This helps your site load faster and use less bandwidth.

View file

@ -0,0 +1,117 @@
---
title: Install Docker on Debian
lang: en
published: 2025-01-20T20:40:14.958Z
description: Learn how to easily install Docker on Debian or Ubuntu to run containers and manage applications.
image: ""
tags:
- Linux
- Server
- Docker
category: Operating Systems
draft: false
---
# Install Docker on Debian
Learn how to easily install Docker on Debian or Ubuntu to run containers and manage applications.
## Docker Installation
Download Docker:
```bash
sudo apt-get install -y ca-certificates curl && sudo mkdir -p /etc/apt/keyrings && sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc && sudo chmod a+r /etc/apt/keyrings/docker.asc && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null && sudo apt-get update
```
Install Docker and Docker Compose:
```bash
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
```
Set Docker to start automatically on boot:
```bash
sudo systemctl enable docker
```
*(Retrieved from [Docker documentation](https://docs.docker.com/engine/install/) on August 25, 2024)*
Add yourself to the Docker group to allow you running Docker commands without needing sudo every time. Replace username with your own:
```bash
sudo usermod -aG docker username
```
## Create Docker Application
Create a folder to keep all your Docker applications organized:
```bash
mkdir ~/docker
cd ~/docker
```
Create application folder inside the docker folder and go into it:
```bash
mkdir app1
cd app1
```
Create docker-compose.yml:
```bash
nano docker-compose.yml
```
## Start/Stop Docker Application
Start Docker container:
```bash
docker compose up -d
```
See a list of running containers defined in docker-compose.yml file in current working directory:
```bash
docker compose ps
```
See all running containers on your server:
```bash
docker ps
```
Stop Docker container:
```bash
docker compose down
```
## Delete Docker Application
Stop Docker container:
```bash
docker compose down
```
Delete volumes:
```bash
docker volume ls
docker volume rm <volume_name>
```
Stop Docker container and remove volumes together:
```bash
docker compose down --volumes
```
A volume is a storage space where your container's data is stored. If you back up docker-compose.yml and volume, you can restore your Docker container and its data later. If you delete volume, data is lost and container is reset.
Delete Docker image:
```bash
docker rmi <repository:tag/image_id>
```
An image includes the files (HTML, CSS, JavaScript, SQL) and other components your application needs to run.
Finally, delete application folder:
```bash
rm -rf app1
```
>[!WARNING]
> You can run `docker compose down; docker system prune -a --volumes` to delete all unused containers, volumes, and images from your server. However, be careful as this command deletes all data that is not actively in use. Ensure your other containers are running before using this command.

View file

@ -0,0 +1,95 @@
---
title: Set up Swap Memory on Linux Server
lang: en
published: 2025-01-25T01:00:00.000Z
description: Find out how to create and set up a swap file, and how much swap space you need.
image: ""
tags:
- Server
- Swap Memory
- RAM
category: Operating Systems
draft: false
---
# Set up Swap Memory on Linux Server
Find out how to create and set up a swap file, and how much swap space you need.
## Create Swap
Swap memory uses storage space on your server to act as additional memory when your RAM is full. Without swap, your server might slow down or freeze if the RAM runs out. Downsides are that swap is slower than RAM and takes up storage space, so you can't use it for storing files. Swap can also wear down storage drive over time, though this happens slowly.
1. Create a swap file:
```bash
sudo dd if=/dev/zero of=/swapfile bs=1M count=8192
```
`count=8192` creates a 8GB (8192MB) swap file. Change the number to create a bigger or smaller swap file. For example, `count=4096` would create a 4GB (4096MB) swap file.
2. Set permissions:
```bash
sudo chmod 600 /swapfile
```
3. Format swap file for use:
```bash
sudo mkswap /swapfile
```
4. Start swap file:
```bash
sudo swapon /swapfile
```
5. Display swap:
```bash
sudo swapon --show
```
6. Make the swap file permanent.
Open `/etc/fstab` file:
```bash
sudo nano /etc/fstab
```
Add the folliowing line at the end of the file:
```
/swapfile none swap sw 0 0
```
Your server will automatically enable swap every time it boots up.
>[!TIP]
> You can use `free -m` and `htop` command to check your RAM and swap memory usage.
## Delete Swap
Run the following commands to delete the swap file and start over if you have any issues creating the swap file.
```bash
sudo swapoff /swapfile
sudo rm /swapfile
```
## Resize Swap
1. Delete swap file:
```bash
sudo swapoff /swapfile
sudo rm /swapfile
```
2. Create a new swap file.
## Recommended Swap Size
RAM Size | Recmmended Swap Size
-|-
~4GB | 2x the size of RAM
4~8GB | Equal to the size of RAM
8~16GB | 1~1.5x the size of RAM
16GB | 1x the size of RAM or less
This is a general guideline. The actual swap size needed depends on your server's workload.

View file

@ -0,0 +1,81 @@
---
title: Install Uptime Kuma with Docker for Simple Uptime Monitoring
lang: en
published: 2025-02-01T01:00:00.000Z
description: Discover how to install Uptime Kuma with Docker to monitor your websites and services. This guide walks you through the setup process step by step.
image: ""
tags:
- Docker
- Uptime
- Server
category: DevOps
draft: false
---
# Install Uptime Kuma with Docker for Simple Uptime Monitoring
Discover how to install Uptime Kuma with Docker to monitor your websites and services. This guide walks you through the setup process step by step.
## Docker Compose
Create a folder named "uptime-kuma":
```bash
mkdir uptime-kuma
cd uptime-kuma
```
Open compose.yml:
```bash
nano compose.yml
```
Edit compose.yml:
```yml
services:
uptime-kuma:
image: louislam/uptime-kuma:1
volumes:
- ./data:/app/data
ports:
- 11012:3001
restart: unless-stopped
```
*(Retrieved from [Uptime Kuma GitHub](https://github.com/louislam/uptime-kuma/blob/master/compose.yaml) on January 21, 2025)*
## Caddy
Install Caddy:
```bash
apt install caddy
sudo systemctl enable caddy
```
Open Caddyfile:
```bash
nano /etc/caddy/Caddyfile
```
Edit Caddyfile:
```
example.com {
route /status* {
uri strip_prefix /status
redir https://status.{host}{uri}
}
}
status.example.com {
reverse_proxy localhost:11012
encode zstd gzip
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Uptime Kuma at https://status.example.com or at https://example.com/status if you prefer using a subpath.

View file

@ -0,0 +1,42 @@
---
title: The Easiest Way to Install the Latest Node Version Using NVM
lang: en
published: 2025-02-08T01:00:00.000Z
description: Tried other tutorials to install or upgrade Node.js, but it didnt work? Try this simple and effective method using NVM to get the latest version easily.
image: ""
tags:
- Node.js
- Linux
- NVM
category: Software Engineering
draft: false
---
# The Easiest Way to Install the Latest Node Version Using NVM
Tried other tutorials to install or upgrade Node.js, but it didnt work? Try this simple and effective method using NVM to get the latest version easily.
Find 'node' directory:
```bash
which node
```
Delete the 'node' directory:
```bash
rm -rf /usr/local/node/bin/node
```
Check available Node.js versions:
```bash
nvm ls-remote
```
Install the latest version of Node.js:
```bash
nvm install <latest_version>
```
Verify the installation:
```bash
node -v
```

View file

@ -0,0 +1,131 @@
---
title: Matomo - Google Analytics Docker Alternative
lang: en
published: 2025-02-15T01:00:00.000Z
description: Follow these steps to set up Matomo, a simple and private alternative to Google Analytics, using Docker.
image: ""
tags:
- Docker
- Google Anaytics
- Web Analytics
category: Networking
draft: false
---
# Matomo - Google Analytics Docker Alternative
Follow these steps to set up Matomo, a simple and private alternative to Google Analytics, using Docker.
## Docker Compose
Create a folder named "matomo":
```bash
mkdir matomo
cd matomo
```
Create docker-compose.yml:
```bash
nano docker-compose.yml
```
Edit docker-compose.yml:
```yml
version: "3"
services:
db:
image: mariadb:10.11
command: --max-allowed-packet=64MB
restart: always
volumes:
- db:/var/lib/mysql:Z
environment:
- MYSQL_ROOT_PASSWORD=<password>
- MARIADB_AUTO_UPGRADE=1
- MARIADB_DISABLE_UPGRADE_BACKUP=1
env_file:
- ./db.env
app:
image: matomo
restart: always
volumes:
# - ./config:/var/www/html/config:z
# - ./logs:/var/www/html/logs:z
- matomo:/var/www/html:z
environment:
- MATOMO_DATABASE_HOST=db
env_file:
- ./db.env
ports:
- 10093:80
volumes:
db:
matomo:
```
Replace `<password>` with your password. If your password has a `$` symbol, write `$$` instead. This is because Docker thinks `$` is a variable name and `$$` tells it to use a normal `$`.
*(Retrieved from [Matomo GitHub](https://github.com/matomo-org/docker/blob/master/.examples/apache/docker-compose.yml) on January 22, 2025)*
Create db.env:
```bash
nano db.env
```
Edit db.env:
```env
MYSQL_PASSWORD=<password>
MYSQL_DATABASE=matomo
MYSQL_USER=matomo
MATOMO_DATABASE_ADAPTER=mysql
MATOMO_DATABASE_TABLES_PREFIX=matomo_
MATOMO_DATABASE_USERNAME=matomo
MATOMO_DATABASE_PASSWORD=<db_password>
MATOMO_DATABASE_DBNAME=matomo
MARIADB_AUTO_UPGRADE=1
MARIADB_INITDB_SKIP_TZINFO=1
```
Use the same password you set in docker-compose.yml for `MYSQL_PASSWORD`, and create new password for `MATOMO_DATABASE_PASSWORD`. Write `$$` as `$` (the reverse of what you did in the docker-compose.yml file).
## Caddy
Install Caddy:
```bash
apt install caddy
sudo systemctl enable caddy
```
Open Caddyfile:
```bash
nano /etc/caddy/Caddyfile
```
Edit Caddyfile:
```
example.com {
# You can use a different name instead of "/radar," like "/analytics" or "/traffic."
route /radar* {
uri strip_prefix /radar
redir https://radar.{host}{uri}
}
}
# To use a different port, change the "ports:" section in your docker-compose.yml file.
radar.example.com {
reverse_proxy localhost:10093
encode zstd gzip
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Matomo at https://radar.example.com or at https://example.com/radar if you prefer using a subpath.

View file

@ -0,0 +1,136 @@
---
title: YOURLS Link Shortener with Docker
lang: en
published: 2025-02-22T01:00:00.000Z
description: Set up YOURSL link shortener with Docker for a quick and simple way to manage your links.
image: ""
tags:
- Docker
- Link Shortener
- YOURLS
category: DevOps
draft: false
---
# YOURLS Link Shortener with Docker
Set up YOURLS link shortener with Docker for a quick and simple way to manage your links.
## Docker Compose
Create a folder named "yourls":
```bash
mkdir yourls
cd yourls
```
Create docker-compose.yml:
```bash
nano docker-compose.yml
```
Edit compose.yml:
```yml
version: '3.1'
services:
yourls:
image: yourls
restart: always
ports:
- 10082:80
environment:
YOURLS_DB_PASS: ****
YOURLS_SITE: https://go.example.com
YOURLS_USER: juyoung
YOURLS_PASS: ****
volumes:
- ./plugins:/var/www/html/user/plugins
- ./root:/var/www/html
mysql:
image: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ****
MYSQL_DATABASE: yourls
volumes:
- ./mysql-data:/var/lib/mysql
```
1. Change `YOURLS_SITE`.
2. Use the same password for both `YOURLS_DB_PASS` and `MYSQL_ROOT_PASSWORD`.
3. Set `YOURLS_USER` and use a unique password for `YOURLS_PASS`. These will be your username and password for logging into YOURLS dashboard.
If your password has a `$` symbol, write `$$` instead. This is because Docker thinks `$` is a variable name and `$$` tells it to use a normal `$`.
*(Retrieved from [YOURLS Docker Hub](https://hub.docker.com/_/yourls) on January 23, 2025)*
## Caddy
Install Caddy:
```bash
apt install caddy
sudo systemctl enable caddy
```
Open Caddyfile:
```bash
nano /etc/caddy/Caddyfile
```
Edit Caddyfile:
```
example.com {
# You can use a different name instead of "/go." Change "YOURLS_SITE:" in docker-compose.yml file.
route /go* {
uri strip_prefix /go
redir https://go.{host}{uri}
}
}
# To use a different port, change the "ports:" section in your docker-compose.yml file.
go.example.com {
reverse_proxy localhost:10082
encode zstd gzip
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to YOURLS dashboard at https://go.example.com/admin or at https://example.com/go/admin if you prefer using a subpath. Don't forget the "/admin" part.
## YOURLS Plugins
YOURLS comes with minimal features, but you may want to install plugins for additional functionality. You can find a list of available plugins at [YOURLS Awesome Plugins](https://github.com/YOURLS/awesome) (https://github.com/YOURLS/awesome).
To install a plugin, create a folder named after the plugin inside the "plugins" folder.
Run `ls` to view the "plugins" folder and go into it:
```bash
ls
cd plugins
```
Create folder named after the plugin:
```bash
mkdir <plugin-name>
```
Place plugin files inside this folder, then go to Plugins administration page to activate the plugin.
**Recommended Plugins**
- [404 If Not Found](https://github.com/YOURLS/404-if-not-found): Shows a 404 error page if the short URL doesn't exist.
- [QRCode Local](https://github.com/alexkolodko/yourls-local-qr-code): Adds .qr at the end of the short URL to display a QR code.
- [Every Click Counts](https://github.com/BstName/every-click-counts): Counts multiple clicks by the same client.
- [Fallback URL](https://github.com/ozh/yourls-fallback-url/): Redirects if the short URL doesn't exist.
- [JSON Response](https://github.com/tessus/yourls-json-response): Adds .json at the end of the short URL to display info as a JSON response.
- [Login Timeout](https://github.com/reanimus/yourls-login-timeout): Protects login page against brute force attacks.
- [Redirect Index](https://github.com/tomslominski/yourls-redirect-index): Redirects the user if they go to the base directory.
- [Timezones](https://github.com/YOURLS/timezones)

View file

@ -0,0 +1,127 @@
---
title: Install Flatnotes with Docker - A Markdown Notes App
lang: en
published: 2025-03-01T01:00:00.000Z
description: Easily set up FlatNotes with Docker for a fast, searchable, markdown-powered notes experience.
image: ""
tags:
- Docker
- Flatnotes
- Server
category: DevOps
draft: false
---
# Install Flatnotes with Docker - A Markdown Notes App
Easily set up FlatNotes with Docker for a fast, searchable, markdown-powered notes experience.
## Docker Compose
Create a folder named "flatnotes":
```bash
mkdir flatnotes
cd flatnotes
```
Create docker-compose.yml:
```bash
nano docker-compose.yml
```
Edit compose.yml:
```yml
version: "3"
services:
flatnotes:
container_name: flatnotes
image: dullage/flatnotes:latest
environment:
PUID: 1000
PGID: 1000
FLATNOTES_AUTH_TYPE: "totp"
FLATNOTES_USERNAME: user
FLATNOTES_PASSWORD: "<password>"
FLATNOTES_SECRET_KEY: "<32 random characters>"
FLATNOTES_TOTP_KEY: "<32 random characters>"
volumes:
- "./data:/data"
# Optional. Allows you to save the search index in a different location:
- "./index:/data/.flatnotes"
ports:
- "12005:8080"
restart: unless-stopped
```
1. Set `FLATNOTES_USERNAME` and `FLATNOTES_PASSWORD`. These will be your username and password.
2. Set 32 random characters for `FLATNOTES_SECRET_KEY` and `FLATNOTES_TOTP_KEY`.
3. If you do not want to use TOTP, change `FLATNOTES_AUTH_TYPE` to "password" and delete `FLATNOTES_TOTP_KEY`.
If you change `FLATNOTES_AUTH_TYPE` to "read_only", your Flatnotes becomes visible to anyone, but you can't login.
If your password has a `$` symbol, write `$$` instead. This is because Docker thinks `$` is a variable name and `$$` tells it to use a normal `$`.
*(Retrieved from [flatnotes GitHub](https://github.com/Dullage/flatnotes?tab=readme-ov-file#example-docker-compose) on January 25, 2025)*
Save your docker-compose.yml.
Print QR code and TOTP key for 2-step verification (skip this step if you're using FLATNOTES_AUTH_TYPE: "password"):
```bash
docker logs flatnotes
```
You can scan the QR code with your phone or manually copy TOTP key, which looks something like `ABCDEFGHIJKL====`
## Caddy
Install Caddy:
```bash
apt install caddy
sudo systemctl enable caddy
```
Open Caddyfile:
```bash
nano /etc/caddy/Caddyfile
```
Edit Caddyfile:
```
example.com {
# You can use a different name instead of "/notes."
route /notes* {
uri strip_prefix /notes
redir https://notes.{host}{uri}
}
}
# To use a different port, change the "ports:" section in your docker-compose.yml file.
notes.example.com {
reverse_proxy localhost:12005
redir / /search?term=*&sortBy=1 permanent
encode zstd gzip
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Flatnotes at https://notes.example.com or at https://example.com/notes if you prefer using a subpath.
## Flatnotes Customization
You can add more variables to "environment:" section in docker-compose.yml to customize your Flatnotes setup. After making changes to docker-compose.yml file, restart Docker by running `docker compose down; docker compose up -d`.
`FLATNOTES_SESSION_EXPIRY_DAYS`: Login session lifetime in days. The default is 30 days. Set to 365 if you don't want to be logged out automatically for a long time.
`FLATNOTES_QUICK_ACCESS_LIMIT`: Number of notes to show on homepage. Default is 4.
`FLATNOTES_QUICK_ACCESS_HIDE`: Set to "true" to hide notes on homepage. Default is "false".
`FLATNOTES_QUICK_ACCESS_SORT`: Value can be "score", "title", and "lastModified". Default is "lastModified".
You can find more variables in FlatNotes GitHub Wiki.
*(Retrieved from [flatnotes GitHub Wiki](https://github.com/dullage/flatnotes/wiki/Environment-Variables) on January 25, 2025)*

View file

@ -0,0 +1,101 @@
---
title: Easy Way to Install Lychee 6 with Docker
lang: en
published: 2025-03-08T01:00:00.000Z
description: Discover the simplest method to deploy Lychee, the easy-to-use photo-management solution, using Docker. Perfect for beginners and tech enthusiasts alike.
image: ""
tags:
- Docker
- Lychee
- Instagram
category: DevOps
draft: false
---
# Easy Way to Install Lychee 6 with Docker
Discover the simplest method to deploy Lychee, the easy-to-use photo-management solution, using Docker. Perfect for beginners and tech enthusiasts alike.
## Docker Compose
Download Lychee repository:
```bash
git clone https://github.com/LycheeOrg/Lychee-Docker lychee
```
Open compose.yml:
```bash
nano compose.yml
```
Edit compose.yml:
```bash
services:
lychee:
image: lycheeorg/lychee
container_name: lychee
ports:
- 13032:80
volumes:
- ./lychee/conf:/conf
- ./lychee/uploads:/uploads
- ./lychee/sym:/sym
- ./lychee/logs:/logs
- ./lychee/tmp:/lychee-tmp
networks:
- lychee
environment:
# Replace with your timezone (List of supported timezones: https://www.php.net/manual/en/timezones.php)
- PHP_TZ=America/Los_Angeles
- TIMEZONE=America/Los_Angeles
- APP_FORCE_HTTPS=true
- APP_URL=https://draw.example.com
- DB_CONNECTION=sqlite
restart: unless-stopped
network:
lychee:
```
## Caddy
Install Caddy:
```bash
apt install caddy
sudo systemctl enable caddy
```
Open Caddyfile:
```bash
nano /etc/caddy/Caddyfile
```
Edit Caddyfile:
```
example.com {
# You can use a different name instead of "/draw."
route /draw* {
uri strip_prefix /draw
redir https://draw.{host}{uri}
}
}
# To use a different port, change the "ports:" section in your docker-compose.yml file.
draw.example.com {
reverse_proxy localhost:13032
encode zstd gzip
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
Go to Lychee at https://draw.example.com or at https://example.com/draw if you prefer using a subpath.
In advanced settings, add `.pdf|.docx` to 'raw_formats' for uploading PDF and Word files. Other file extensions work, too.
*(Retrieved from [Lychee GitHub Issues](https://github.com/LycheeOrg/Lychee/issues/854) on February 19, 2025)*

View file

@ -0,0 +1,97 @@
---
title: Set up Navidrome with Docker for a self hosted Spotify
lang: en
published: 2025-08-21T22:08:43.679Z
description: Learn how to deploy Navidrome in Docker, a simple self-hosted Spotify alternative for streaming your music library.
image: ""
tags:
- Docker
- Navidrome
- Spotify
category: DevOps
draft: false
---
# Set up Navidrome with Docker for a self hosted Spotify
Learn how to deploy Navidrome in Docker, a simple self-hosted Spotify alternative for streaming your music library.
## Docker Compose
Create a new folder:
```bash
mkdir navidrome
cd navidrome
```
Create docker-compose.yml:
```bash
nano docker-compose.yml
```
Edit compose.yml:
```bash
services:
navidrome:
image: deluan/navidrome:latest
user: 1000:1000 # should be owner of volumes
ports:
- "4533:4533"
restart: unless-stopped
# environment:
# Optional: put your config options customization here. Examples:
# ND_LOGLEVEL: debug
volumes:
- "./navidrome/data:/data"
- "./navidrome/music:/music:ro"
```
*(Retrieved from [Navidrome official website](https://www.navidrome.org/docs/installation/docker/#using-docker-compose-) on August 16, 2025, and modified)*
Start Docker:
```bash
docker compose up -d
```
## Caddy
Install Caddy:
```bash
apt install caddy
sudo systemctl enable caddy
```
Open Caddyfile:
```bash
nano /etc/caddy/Caddyfile
```
Add the following configuration:
```
music.example.com {
reverse_proxy localhost:4533
encode zstd gzip
}
```
Restart Caddy:
```bash
sudo systemctl restart caddy
```
## Folder Permission
Enable uploading audio files:
```bash
sudo chmod -R 777 ./navidrome
```
Go to Navidrome at https://music.example.com and create a new account.
Spotify Free streams at 96 kbps on mobile, so I recommend converting your audio file to MP3 format at 96 kbps to avoid using too much of your servers bandwidth. Then, upload your MP3 file to the `./navidrome/music` folder. The following command converts a `song.ext` file (can be FLAC, OGG, etc.) to `song.mp3` at 96kbps: `ffmpeg -i song.ext -b:a 96k song.mp3`. Converts all .flac files in current folder: `for f in *.flac; do ffmpeg -y -i "$f" -b:a 96k -map_metadata 0 "${f%.flac}.mp3"; done`
If you delete everything in the `./navidrome/music` folder, they might still show up in Navidromes interface. Navidrome does this on purpose to protect your database from being competely wiped out.
*(Retrieved from [Navidrome GitHub Issues](https://github.com/navidrome/navidrome/issues/3007#issuecomment-2364647024) on August 16, 2025)*

View file

@ -0,0 +1,111 @@
---
title: How to Install Kimai with Docker Compose
lang: en
published: 2026-02-05T00:16:31.180Z
description: A simple guide for installing Kimai with Docker Compose.
image: ""
tags:
- Kimai
- Docker
- Time Management
category: DevOps
draft: false
---
# How to Install Kimai with Docker Compose
Create a new folder:
```
mkdir kimai
cd kimai
```
## Environment Variables
Create .env file:
```
nano .env
```
Add the following environment variables:
```
MYSQL_DATABASE=kimai
MYSQL_USER=kimaiuser
MYSQL_PASSWORD=****
MYSQL_ROOT_PASSWORD=****
ADMINMAIL=admin@kimai.local
ADMINPASS=****
```
**MYSQL_PASSWORD**, **MYSQL_ROOT_PASSWORD**, and **ADMINPASS** should be no longer than 64 characters and use only alphanuermic characters, with no special character.
**ADMINMAIL** and **ADMINPASS** are your admin username and password.
## Docker Compose
Create docker-compose.yml:
```
nano docker-compose.yml
```
Add the following content:
```
services:
sqldb:
image: mysql:8.3
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
restart: unless-stopped
kimai:
image: kimai/kimai2:apache
ports:
- "9093:8001"
environment:
ADMINMAIL: ${ADMINMAIL}
ADMINPASS: ${ADMINPASS}
DATABASE_URL: mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@sqldb:3306/${MYSQL_DATABASE}?charset=utf8mb4
TRUSTED_HOSTS: '^clock\.example\.com$|^localhost$|^127\.0\.0\.1$'
restart: unless-stopped
```
Update **TRUSTED_HOSTS** with your domain and leave the other settings unchanged.
Start Kimai:
```
docker compose up -d
```
## Caddy
Open Caddyfile:
```
sudo nano /etc/caddy/Caddyfile
```
Add the following content:
```
clock.example.com {
reverse_proxy localhost:9093
}
```
Restart caddy:
```
sudo systemctl restart caddy
```
## Usage
Go to https://clock.example.com and login using the **ADMINMAIL** and **ADMINPASS** values from your `.env` flie.
>[!TIP]
> Kimai may take around 1 minute to start up the first time, while initializing the database. To check if Kimai is running correctly, run: `docker compose logs -f kimai` in the same folder where your docker-compose.yml file is located.

View file

@ -0,0 +1,61 @@
---
title: Upgrade from Debian 11 Bullseye to Debian 12 Bookworm
lang: en
published: 2026-02-08T01:39:17.588Z
description: This guide explains how to upgrade from Debian 11 to Debian 12. Expect the upgrade to take about 2030 minutes, mostly waiting for packages to download.
image: ""
tags:
- Debian
- Bookworm
- Linux
category: Operating Systems
draft: false
---
# Upgrade from Debian 11 Bullseye to Debian 12 Bookworm
Expect the upgrade to take about 2030 minutes, mostly waiting for packages to download.
## Update APT sources
```
sudo nano /etc/apt/sources.list
```
Change every line that has:
```
bullseye main contrib non-free
```
to:
```
bookworm main contrib non-free non-free-firmware
```
Do this for all deb and deb-src lines. For example: `deb http://deb.debian.org/debian/ bookworm main contrib non-free non-free-firmware`
## Upgrade Debian
```
sudo apt update
sudo apt upgrade --without-new-pkgs
sudo apt full-ugrade
sudo apt autoremove
```
## Reboot and Verify
Reboot:
```
sudo reboot
```
Verify:
```
cat /etc/os-release
```
You should see `PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"`.

View file

@ -1,311 +0,0 @@
---
title: Expressive Code Example
published: 2024-04-10
description: How code blocks look in Markdown using Expressive Code.
tags: [Markdown, Blogging, Demo]
category: Examples
draft: false
---
Here, we'll explore how code blocks look using [Expressive Code](https://expressive-code.com/). The provided examples are based on the official documentation, which you can refer to for further details.
## Expressive Code
### Syntax Highlighting
[Syntax Highlighting](https://expressive-code.com/key-features/syntax-highlighting/)
#### Regular syntax highlighting
```js
console.log('This code is syntax highlighted!')
```
#### Rendering ANSI escape sequences
```ansi
ANSI colors:
- Regular: Red Green Yellow Blue Magenta Cyan
- Bold: Red Green Yellow Blue Magenta Cyan
- Dimmed: Red Green Yellow Blue Magenta Cyan
256 colors (showing colors 160-177):
160 161 162 163 164 165
166 167 168 169 170 171
172 173 174 175 176 177
Full RGB colors:
ForestGreen - RGB(34, 139, 34)
Text formatting: Bold Dimmed Italic Underline
```
### Editor & Terminal Frames
[Editor & Terminal Frames](https://expressive-code.com/key-features/frames/)
#### Code editor frames
```js title="my-test-file.js"
console.log('Title attribute example')
```
---
```html
<!-- src/content/index.html -->
<div>File name comment example</div>
```
#### Terminal frames
```bash
echo "This terminal frame has no title"
```
---
```powershell title="PowerShell terminal example"
Write-Output "This one has a title!"
```
#### Overriding frame types
```sh frame="none"
echo "Look ma, no frame!"
```
---
```ps frame="code" title="PowerShell Profile.ps1"
# Without overriding, this would be a terminal frame
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
New-Alias tail Watch-Tail
```
### Text & Line Markers
[Text & Line Markers](https://expressive-code.com/key-features/text-markers/)
#### Marking full lines & line ranges
```js {1, 4, 7-8}
// Line 1 - targeted by line number
// Line 2
// Line 3
// Line 4 - targeted by line number
// Line 5
// Line 6
// Line 7 - targeted by range "7-8"
// Line 8 - targeted by range "7-8"
```
#### Selecting line marker types (mark, ins, del)
```js title="line-markers.js" del={2} ins={3-4} {6}
function demo() {
console.log('this line is marked as deleted')
// This line and the next one are marked as inserted
console.log('this is the second inserted line')
return 'this line uses the neutral default marker type'
}
```
#### Adding labels to line markers
```jsx {"1":5} del={"2":7-8} ins={"3":10-12}
// labeled-line-markers.jsx
<button
role="button"
{...props}
value={value}
className={buttonClassName}
disabled={disabled}
active={active}
>
{children &&
!active &&
(typeof children === 'string' ? <span>{children}</span> : children)}
</button>
```
#### Adding long labels on their own lines
```jsx {"1. Provide the value prop here:":5-6} del={"2. Remove the disabled and active states:":8-10} ins={"3. Add this to render the children inside the button:":12-15}
// labeled-line-markers.jsx
<button
role="button"
{...props}
value={value}
className={buttonClassName}
disabled={disabled}
active={active}
>
{children &&
!active &&
(typeof children === 'string' ? <span>{children}</span> : children)}
</button>
```
#### Using diff-like syntax
```diff
+this line will be marked as inserted
-this line will be marked as deleted
this is a regular line
```
---
```diff
--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+this is an actual diff file
-all contents will remain unmodified
no whitespace will be removed either
```
#### Combining syntax highlighting with diff-like syntax
```diff lang="js"
function thisIsJavaScript() {
// This entire block gets highlighted as JavaScript,
// and we can still add diff markers to it!
- console.log('Old code to be removed')
+ console.log('New and shiny code!')
}
```
#### Marking individual text inside lines
```js "given text"
function demo() {
// Mark any given text inside lines
return 'Multiple matches of the given text are supported';
}
```
#### Regular expressions
```ts /ye[sp]/
console.log('The words yes and yep will be marked.')
```
#### Escaping forward slashes
```sh /\/ho.*\//
echo "Test" > /home/test.txt
```
#### Selecting inline marker types (mark, ins, del)
```js "return true;" ins="inserted" del="deleted"
function demo() {
console.log('These are inserted and deleted marker types');
// The return statement uses the default marker type
return true;
}
```
### Word Wrap
[Word Wrap](https://expressive-code.com/key-features/word-wrap/)
#### Configuring word wrap per block
```js wrap
// Example with wrap
function getLongString() {
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
```
---
```js wrap=false
// Example with wrap=false
function getLongString() {
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
```
#### Configuring indentation of wrapped lines
```js wrap preserveIndent
// Example with preserveIndent (enabled by default)
function getLongString() {
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
```
---
```js wrap preserveIndent=false
// Example with preserveIndent=false
function getLongString() {
return 'This is a very long string that will most probably not fit into the available space unless the container is extremely wide'
}
```
## Collapsible Sections
[Collapsible Sections](https://expressive-code.com/plugins/collapsible-sections/)
```js collapse={1-5, 12-14, 21-24}
// All this boilerplate setup code will be collapsed
import { someBoilerplateEngine } from '@example/some-boilerplate'
import { evenMoreBoilerplate } from '@example/even-more-boilerplate'
const engine = someBoilerplateEngine(evenMoreBoilerplate())
// This part of the code will be visible by default
engine.doSomething(1, 2, 3, calcFn)
function calcFn() {
// You can have multiple collapsed sections
const a = 1
const b = 2
const c = a + b
// This will remain visible
console.log(`Calculation result: ${a} + ${b} = ${c}`)
return c
}
// All this code until the end of the block will be collapsed again
engine.closeConnection()
engine.freeMemory()
engine.shutdown({ reason: 'End of example boilerplate code' })
```
## Line Numbers
[Line Numbers](https://expressive-code.com/plugins/line-numbers/)
### Displaying line numbers per block
```js showLineNumbers
// This code block will show line numbers
console.log('Greetings from line 2!')
console.log('I am on line 3')
```
---
```js showLineNumbers=false
// Line numbers are disabled for this block
console.log('Hello?')
console.log('Sorry, do you know what line I am on?')
```
### Changing the starting line number
```js showLineNumbers startLineNumber=5
console.log('Greetings from line 5!')
console.log('I am on line 6')
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

View file

@ -1,51 +0,0 @@
---
title: Simple Guides for Fuwari
published: 2024-04-01
description: "How to use this blog template."
image: "./cover.jpeg"
tags: ["Fuwari", "Blogging", "Customization"]
category: Guides
draft: false
---
> Cover image source: [Source](https://image.civitai.com/xG1nkqKTMzGDvpLrqFT7WA/208fc754-890d-4adb-9753-2c963332675d/width=2048/01651-1456859105-(colour_1.5),girl,_Blue,yellow,green,cyan,purple,red,pink,_best,8k,UHD,masterpiece,male%20focus,%201boy,gloves,%20ponytail,%20long%20hair,.jpeg)
This blog template is built with [Astro](https://astro.build/). For the things that are not mentioned in this guide, you may find the answers in the [Astro Docs](https://docs.astro.build/).
## Front-matter of Posts
```yaml
---
title: My First Blog Post
published: 2023-09-09
description: This is the first post of my new Astro blog.
image: ./cover.jpg
tags: [Foo, Bar]
category: Front-end
draft: false
---
```
| Attribute | Description |
|---------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `title` | The title of the post. |
| `published` | The date the post was published. |
| `description` | A short description of the post. Displayed on index page. |
| `image` | The cover image path of the post.<br/>1. Start with `http://` or `https://`: Use web image<br/>2. Start with `/`: For image in `public` dir<br/>3. With none of the prefixes: Relative to the markdown file |
| `tags` | The tags of the post. |
| `category` | The category of the post. |
| `draft` | If this post is still a draft, which won't be displayed. |
## Where to Place the Post Files
Your post files should be placed in `src/content/posts/` directory. You can also create sub-directories to better organize your posts and assets.
```
src/content/posts/
├── post-1.md
└── post-2/
├── cover.png
└── index.md
```

View file

@ -1,95 +0,0 @@
---
title: Markdown Extended Features
published: 2024-05-01
updated: 2024-11-29
description: 'Read more about Markdown features in Fuwari'
image: ''
tags: [Demo, Example, Markdown, Fuwari]
category: 'Examples'
draft: false
---
## GitHub Repository Cards
You can add dynamic cards that link to GitHub repositories, on page load, the repository information is pulled from the GitHub API.
::github{repo="Fabrizz/MMM-OnSpotify"}
Create a GitHub repository card with the code `::github{repo="<owner>/<repo>"}`.
```markdown
::github{repo="saicaca/fuwari"}
```
## Admonitions
Following types of admonitions are supported: `note` `tip` `important` `warning` `caution`
:::note
Highlights information that users should take into account, even when skimming.
:::
:::tip
Optional information to help a user be more successful.
:::
:::important
Crucial information necessary for users to succeed.
:::
:::warning
Critical content demanding immediate user attention due to potential risks.
:::
:::caution
Negative potential consequences of an action.
:::
### Basic Syntax
```markdown
:::note
Highlights information that users should take into account, even when skimming.
:::
:::tip
Optional information to help a user be more successful.
:::
```
### Custom Titles
The title of the admonition can be customized.
:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::
```markdown
:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::
```
### GitHub Syntax
> [!TIP]
> [The GitHub syntax](https://github.com/orgs/community/discussions/16925) is also supported.
```
> [!NOTE]
> The GitHub syntax is also supported.
> [!TIP]
> The GitHub syntax is also supported.
```
### Spoiler
You can add spoilers to your text. The text also supports **Markdown** syntax.
The content :spoiler[is hidden **ayyy**]!
```markdown
The content :spoiler[is hidden **ayyy**]!
```

View file

@ -1,175 +0,0 @@
---
title: Markdown Example
published: 2023-10-01
description: A simple example of a Markdown blog post.
tags: [Markdown, Blogging, Demo]
category: Examples
draft: false
---
# An h1 header
Paragraphs are separated by a blank line.
2nd paragraph. _Italic_, **bold**, and `monospace`. Itemized lists
look like:
- this one
- that one
- the other one
Note that --- not considering the asterisk --- the actual text
content starts at 4-columns in.
> Block quotes are
> written like so.
>
> They can span multiple paragraphs,
> if you like.
Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
in chapters 12--14"). Three dots ... will be converted to an ellipsis.
Unicode is supported. ☺
## An h2 header
Here's a numbered list:
1. first item
2. second item
3. third item
Note again how the actual text starts at 4 columns in (4 characters
from the left side). Here's a code sample:
# Let me re-iterate ...
for i in 1 .. 10 { do-something(i) }
As you probably guessed, indented 4 spaces. By the way, instead of
indenting the block, you can use delimited blocks, if you like:
```
define foobar() {
print "Welcome to flavor country!";
}
```
(which makes copying & pasting easier). You can optionally mark the
delimited block for Pandoc to syntax highlight it:
```python
import time
# Quick, count to ten!
for i in range(10):
# (but not *too* quick)
time.sleep(0.5)
print i
```
### An h3 header
Now a nested list:
1. First, get these ingredients:
- carrots
- celery
- lentils
2. Boil some water.
3. Dump everything in the pot and follow
this algorithm:
find wooden spoon
uncover pot
stir
cover pot
balance wooden spoon precariously on pot handle
wait 10 minutes
goto first step (or shut off burner when done)
Do not bump wooden spoon or it will fall.
Notice again how text always lines up on 4-space indents (including
that last line which continues item 3 above).
Here's a link to [a website](http://foo.bar), to a [local
doc](local-doc.html), and to a [section heading in the current
doc](#an-h2-header). Here's a footnote [^1].
[^1]: Footnote text goes here.
Tables can look like this:
size material color
---
9 leather brown
10 hemp canvas natural
11 glass transparent
Table: Shoes, their sizes, and what they're made of
(The above is the caption for the table.) Pandoc also supports
multi-line tables:
---
keyword text
---
red Sunsets, apples, and
other red or reddish
things.
green Leaves, grass, frogs
and other things it's
not easy being.
---
A horizontal rule follows.
---
Here's a definition list:
apples
: Good for making applesauce.
oranges
: Citrus!
tomatoes
: There's no "e" in tomatoe.
Again, text is indented 4 spaces. (Put a blank line between each
term/definition pair to spread things out more.)
Here's a "line block":
| Line one
| Line too
| Line tree
and images can be specified like so:
[//]: # (![example image]&#40;./demo-banner.png "An exemplary image"&#41;)
Inline math equations go in like so: $\omega = d\phi / dt$. Display
math should get its own line and be put in in double-dollarsigns:
$$I = \int \rho R^{2} dV$$
$$
\begin{equation*}
\pi
=3.1415926535
\;8979323846\;2643383279\;5028841971\;6939937510\;5820974944
\;5923078164\;0628620899\;8628034825\;3421170679\;\ldots
\end{equation*}
$$
And note that you can backslash-escape any punctuation characters
which you wish to be displayed literally, ex.: \`foo\`, \*bar\*, etc.

View file

@ -1,28 +0,0 @@
---
title: Include Video in the Posts
published: 2023-08-01
description: This post demonstrates how to include embedded video in a blog post.
tags: [Example, Video]
category: Examples
draft: false
---
Just copy the embed code from YouTube or other platforms, and paste it in the markdown file.
```yaml
---
title: Include Video in the Post
published: 2023-10-19
// ...
---
<iframe width="100%" height="468" src="https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_" title="YouTube video player" frameborder="0" allowfullscreen></iframe>
```
## YouTube
<iframe width="100%" height="468" src="https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
## Bilibili
<iframe width="100%" height="468" src="//player.bilibili.com/player.html?bvid=BV1fK4y1s7Qf&p=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>

View file

@ -1,9 +1,10 @@
# About # About
This is the demo site for [Fuwari](https://github.com/saicaca/fuwari).
::github{repo="saicaca/fuwari"} This blog is where I share notes, guides, and tutorials about programming and technology. Most posts are about coding, but sometimes I also cover tools, tech habits, or other tech topics. I write these posts as a reference for me to revisit or solve problems.
> ### Sources of images used in this site > ### Links
> - [Unsplash](https://unsplash.com/) > - [Linktree](https://www.juyung.com/)
> - [星と少女](https://www.pixiv.net/artworks/108916539) by [Stella](https://www.pixiv.net/users/93273965) > - [LinkedIn](https://job.juyung.com/)
> - [Rabbit - v1.4 Showcase](https://civitai.com/posts/586908) by [Rabbit_YourMajesty](https://civitai.com/user/Rabbit_YourMajesty) > - [Instagram](https://photos.juyung.com/)
> - [Twitter](https://social.juyung.com/)
> - [GitHub](https://git.juyung.com/)

43
src/global.d.ts vendored
View file

@ -1,41 +1,8 @@
import type { AstroIntegration } from "@swup/astro"; import type { AstroIntegration } from '@swup/astro'
declare global { declare global {
interface Window { interface Window {
// type from '@swup/astro' is incorrect // type from '@swup/astro' is incorrect
swup: AstroIntegration; swup: AstroIntegration
pagefind: { }
search: (query: string) => Promise<{
results: Array<{
data: () => Promise<SearchResult>;
}>;
}>;
};
}
}
interface SearchResult {
url: string;
meta: {
title: string;
};
excerpt: string;
content?: string;
word_count?: number;
filters?: Record<string, unknown>;
anchors?: Array<{
element: string;
id: string;
text: string;
location: number;
}>;
weighted_locations?: Array<{
weight: number;
balanced_score: number;
location: number;
}>;
locations?: number[];
raw_content?: string;
raw_url?: string;
sub_results?: SearchResult[];
} }

View file

@ -1,37 +1,37 @@
enum I18nKey { enum I18nKey {
home = "home", home = 'home',
about = "about", about = 'about',
archive = "archive", archive = 'archive',
search = "search", search = 'search',
tags = "tags", tags = 'tags',
categories = "categories", categories = 'categories',
recentPosts = "recentPosts", recentPosts = 'recentPosts',
comments = "comments", comments = 'comments',
untitled = "untitled", untitled = 'untitled',
uncategorized = "uncategorized", uncategorized = 'uncategorized',
noTags = "noTags", noTags = 'noTags',
wordCount = "wordCount", wordCount = 'wordCount',
wordsCount = "wordsCount", wordsCount = 'wordsCount',
minuteCount = "minuteCount", minuteCount = 'minuteCount',
minutesCount = "minutesCount", minutesCount = 'minutesCount',
postCount = "postCount", postCount = 'postCount',
postsCount = "postsCount", postsCount = 'postsCount',
themeColor = "themeColor", themeColor = 'themeColor',
lightMode = "lightMode", lightMode = 'lightMode',
darkMode = "darkMode", darkMode = 'darkMode',
systemMode = "systemMode", systemMode = 'systemMode',
more = "more", more = 'more',
author = "author", author = 'author',
publishedAt = "publishedAt", publishedAt = 'publishedAt',
license = "license", license = 'license',
} }
export default I18nKey; export default I18nKey

View file

@ -1,38 +1,38 @@
import Key from "../i18nKey"; import Key from '../i18nKey'
import type { Translation } from "../translation"; import type { Translation } from '../translation'
export const en: Translation = { export const en: Translation = {
[Key.home]: "Home", [Key.home]: 'Home',
[Key.about]: "About", [Key.about]: 'About',
[Key.archive]: "Archive", [Key.archive]: 'Archive',
[Key.search]: "Search", [Key.search]: 'Search',
[Key.tags]: "Tags", [Key.tags]: 'Tags',
[Key.categories]: "Categories", [Key.categories]: 'Categories',
[Key.recentPosts]: "Recent Posts", [Key.recentPosts]: 'Recent Posts',
[Key.comments]: "Comments", [Key.comments]: 'Comments',
[Key.untitled]: "Untitled", [Key.untitled]: 'Untitled',
[Key.uncategorized]: "Uncategorized", [Key.uncategorized]: 'Uncategorized',
[Key.noTags]: "No Tags", [Key.noTags]: 'No Tags',
[Key.wordCount]: "word", [Key.wordCount]: 'word',
[Key.wordsCount]: "words", [Key.wordsCount]: 'words',
[Key.minuteCount]: "minute", [Key.minuteCount]: 'minute',
[Key.minutesCount]: "minutes", [Key.minutesCount]: 'minutes',
[Key.postCount]: "post", [Key.postCount]: 'post',
[Key.postsCount]: "posts", [Key.postsCount]: 'posts',
[Key.themeColor]: "Theme Color", [Key.themeColor]: 'Theme Color',
[Key.lightMode]: "Light", [Key.lightMode]: 'Light',
[Key.darkMode]: "Dark", [Key.darkMode]: 'Dark',
[Key.systemMode]: "System", [Key.systemMode]: 'System',
[Key.more]: "More", [Key.more]: 'More',
[Key.author]: "Author", [Key.author]: 'Author',
[Key.publishedAt]: "Published at", [Key.publishedAt]: 'Published at',
[Key.license]: "License", [Key.license]: 'License',
}; }

View file

@ -1,38 +1,38 @@
import Key from "../i18nKey"; import Key from '../i18nKey'
import type { Translation } from "../translation"; import type { Translation } from '../translation'
export const es: Translation = { export const es: Translation = {
[Key.home]: "Inicio", [Key.home]: 'Inicio',
[Key.about]: "Sobre mí", [Key.about]: 'Sobre mí',
[Key.archive]: "Archivo", [Key.archive]: 'Archivo',
[Key.search]: "Buscar", [Key.search]: 'Buscar',
[Key.tags]: "Etiquetas", [Key.tags]: 'Etiquetas',
[Key.categories]: "Categorías", [Key.categories]: 'Categorías',
[Key.recentPosts]: "Publicaciones recientes", [Key.recentPosts]: 'Publicaciones recientes',
[Key.comments]: "Comentarios", [Key.comments]: 'Comentarios',
[Key.untitled]: "Sin título", [Key.untitled]: 'Sin título',
[Key.uncategorized]: "Sin categoría", [Key.uncategorized]: 'Sin categoría',
[Key.noTags]: "Sin etiquetas", [Key.noTags]: 'Sin etiquetas',
[Key.wordCount]: "palabra", [Key.wordCount]: 'palabra',
[Key.wordsCount]: "palabras", [Key.wordsCount]: 'palabras',
[Key.minuteCount]: "minuto", [Key.minuteCount]: 'minuto',
[Key.minutesCount]: "minutos", [Key.minutesCount]: 'minutos',
[Key.postCount]: "publicación", [Key.postCount]: 'publicación',
[Key.postsCount]: "publicaciones", [Key.postsCount]: 'publicaciones',
[Key.themeColor]: "Color del tema", [Key.themeColor]: 'Color del tema',
[Key.lightMode]: "Claro", [Key.lightMode]: 'Claro',
[Key.darkMode]: "Oscuro", [Key.darkMode]: 'Oscuro',
[Key.systemMode]: "Sistema", [Key.systemMode]: 'Sistema',
[Key.more]: "Más", [Key.more]: 'Más',
[Key.author]: "Autor", [Key.author]: 'Autor',
[Key.publishedAt]: "Publicado el", [Key.publishedAt]: 'Publicado el',
[Key.license]: "Licencia", [Key.license]: 'Licencia',
}; }

View file

@ -1,38 +0,0 @@
import Key from "../i18nKey";
import type { Translation } from "../translation";
export const id: Translation = {
[Key.home]: "Beranda",
[Key.about]: "Tentang",
[Key.archive]: "Arsip",
[Key.search]: "Cari",
[Key.tags]: "Tag",
[Key.categories]: "Kategori",
[Key.recentPosts]: "Postingan Terbaru",
[Key.comments]: "Komentar",
[Key.untitled]: "Tanpa Judul",
[Key.uncategorized]: "Tanpa Kategori",
[Key.noTags]: "Tanpa Tag",
[Key.wordCount]: "kata",
[Key.wordsCount]: "kata",
[Key.minuteCount]: "menit",
[Key.minutesCount]: "menit",
[Key.postCount]: "postingan",
[Key.postsCount]: "postingan",
[Key.themeColor]: "Warna Tema",
[Key.lightMode]: "Terang",
[Key.darkMode]: "Gelap",
[Key.systemMode]: "Sistem",
[Key.more]: "Lainnya",
[Key.author]: "Penulis",
[Key.publishedAt]: "Diterbitkan pada",
[Key.license]: "Lisensi",
};

View file

@ -1,38 +1,38 @@
import Key from "../i18nKey"; import Key from '../i18nKey'
import type { Translation } from "../translation"; import type { Translation } from '../translation'
export const ja: Translation = { export const ja: Translation = {
[Key.home]: "Home", [Key.home]: 'Home',
[Key.about]: "About", [Key.about]: 'About',
[Key.archive]: "Archive", [Key.archive]: 'Archive',
[Key.search]: "検索", [Key.search]: '検索',
[Key.tags]: "タグ", [Key.tags]: 'タグ',
[Key.categories]: "カテゴリ", [Key.categories]: 'カテゴリ',
[Key.recentPosts]: "最近の投稿", [Key.recentPosts]: '最近の投稿',
[Key.comments]: "コメント", [Key.comments]: 'コメント',
[Key.untitled]: "タイトルなし", [Key.untitled]: 'タイトルなし',
[Key.uncategorized]: "カテゴリなし", [Key.uncategorized]: 'カテゴリなし',
[Key.noTags]: "タグなし", [Key.noTags]: 'タグなし',
[Key.wordCount]: "文字", [Key.wordCount]: '文字',
[Key.wordsCount]: "文字", [Key.wordsCount]: '文字',
[Key.minuteCount]: "分", [Key.minuteCount]: '分',
[Key.minutesCount]: "分", [Key.minutesCount]: '分',
[Key.postCount]: "件の投稿", [Key.postCount]: '件の投稿',
[Key.postsCount]: "件の投稿", [Key.postsCount]: '件の投稿',
[Key.themeColor]: "テーマカラー", [Key.themeColor]: 'テーマカラー',
[Key.lightMode]: "ライト", [Key.lightMode]: 'ライト',
[Key.darkMode]: "ダーク", [Key.darkMode]: 'ダーク',
[Key.systemMode]: "システム", [Key.systemMode]: 'システム',
[Key.more]: "もっと", [Key.more]: 'もっと',
[Key.author]: "作者", [Key.author]: '作者',
[Key.publishedAt]: "公開日", [Key.publishedAt]: '公開日',
[Key.license]: "ライセンス", [Key.license]: 'ライセンス',
}; }

Some files were not shown because too many files have changed in this diff Show more