Upload
This commit is contained in:
parent
6d39b0dec4
commit
64c203a998
135 changed files with 8136 additions and 8731 deletions
1
.frontmatter/database/mediaDb.json
Normal file
1
.frontmatter/database/mediaDb.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"src":{"assets":{"images":{}},"content":{"posts":{"en":{"2026":{"01":{}}}}}}}
|
||||
1
.frontmatter/database/taxonomyDb.json
Normal file
1
.frontmatter/database/taxonomyDb.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
59
.github/ISSUE_TEMPLATE/01-bug_report.yml
vendored
59
.github/ISSUE_TEMPLATE/01-bug_report.yml
vendored
|
|
@ -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.
|
||||
41
.github/ISSUE_TEMPLATE/02-feature_request.yml
vendored
41
.github/ISSUE_TEMPLATE/02-feature_request.yml
vendored
|
|
@ -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.
|
||||
11
.github/ISSUE_TEMPLATE/03-custom_issue.yml
vendored
11
.github/ISSUE_TEMPLATE/03-custom_issue.yml
vendored
|
|
@ -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
|
||||
22
.github/dependabot.yml
vendored
22
.github/dependabot.yml
vendored
|
|
@ -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"]
|
||||
37
.github/pull_request_template.md
vendored
37
.github/pull_request_template.md
vendored
|
|
@ -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. -->
|
||||
20
.github/workflows/biome.yml
vendored
20
.github/workflows/biome.yml
vendored
|
|
@ -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
|
||||
67
.github/workflows/build.yml
vendored
67
.github/workflows/build.yml
vendored
|
|
@ -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
4
.gitignore
vendored
|
|
@ -25,7 +25,3 @@ pnpm-debug.log*
|
|||
package-lock.json
|
||||
bun.lockb
|
||||
yarn.lock
|
||||
|
||||
# ide
|
||||
.idea
|
||||
*.iml
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
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) /
|
||||
[**📦 Versión Antigua de Hexo**](https://github.com/saicaca/hexo-theme-vivia) /
|
||||
|
||||
> Versión del README: `2024-04-07`
|
||||
|
||||

|
||||
|
||||
|
|
@ -15,39 +18,9 @@ Un tema estático para blogs construido con [Astro](https://astro.build).
|
|||
- [x] Diseño responsivo
|
||||
- [ ] Comentarios
|
||||
- [x] Buscador
|
||||
- [x] TOC (Tabla de Contenidos)
|
||||
- [ ] TOC (Tabla de Contenidos)
|
||||
|
||||
## 👀 requiere
|
||||
|
||||
- Node.js <= 22
|
||||
- pnpm <= 9
|
||||
|
||||
## 🚀 Cómo Usar 1
|
||||
|
||||
Inicializa el proyecto localmente usando [create-fuwari](https://github.com/L4Ph/create-fuwari).
|
||||
|
||||
```sh
|
||||
# npm
|
||||
npm create fuwari@latest.
|
||||
|
||||
# yarn
|
||||
yarn create fuwari.
|
||||
|
||||
# pnpm
|
||||
pnpm create fuwari@latest
|
||||
|
||||
# bun
|
||||
bun create fuwari@latest
|
||||
|
||||
# deno
|
||||
deno run -A npm:create-fuwari@latest
|
||||
```
|
||||
|
||||
1. Edita el archivo de configuración `src/config.ts` para personalizar tu blog.
|
||||
2. Ejecuta `pnpm new-post <nombre-de-archivo>` para crear una nueva entrada y edítala en `src/content/posts/`.
|
||||
3. Despliega tu blog en Vercel, Netlify, GitHub Pages, etc., siguiendo [las guías](https://docs.astro.build/en/guides/deploy/). Necesitas editar la configuración del sitio en `astro.config.mjs` antes del despliegue.
|
||||
|
||||
## 🚀 Cómo Usar 2
|
||||
## 🚀 Cómo Usar
|
||||
|
||||
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,7 +2,10 @@
|
|||
|
||||
[Astro](https://astro.build) で構築された静的ブログテンプレート
|
||||
|
||||
[**🖥️ライブデモ (Vercel)**](https://fuwari.vercel.app)
|
||||
[**🖥️ライブデモ (Vercel)**](https://fuwari.vercel.app) /
|
||||
[**📦旧 Hexo バージョン**](https://github.com/saicaca/hexo-theme-vivia)
|
||||
|
||||
> README バージョン:`2024-04-07`
|
||||
|
||||

|
||||
|
||||
|
|
@ -15,39 +18,9 @@
|
|||
- [x] レスポンシブデザイン
|
||||
- [ ] コメント機能
|
||||
- [x] 検索機能
|
||||
- [x] 目次
|
||||
- [ ] 目次
|
||||
|
||||
## 👀 以下が必要
|
||||
|
||||
- Node.js <= 22
|
||||
- pnpm <= 9
|
||||
|
||||
## 🚀 使用方法 1
|
||||
|
||||
[create-fuwari](https://github.com/L4Ph/create-fuwari)を使用して、ローカルにプロジェクトを初期化します。
|
||||
|
||||
```sh
|
||||
# npm
|
||||
npm create fuwari@latest
|
||||
|
||||
# yarn
|
||||
yarn create fuwari
|
||||
|
||||
# pnpm
|
||||
pnpm create fuwari@latest
|
||||
|
||||
# bun
|
||||
bun create fuwari@latest
|
||||
|
||||
# deno
|
||||
deno run -A npm:create-fuwari@latest
|
||||
```
|
||||
|
||||
1. `src/config.ts` ファイルを編集する事でブログを自分好みにカスタマイズ出来ます。
|
||||
2. `pnpm new-post <filename>` で新しい記事を作成し、`src/content/posts/`.フォルダ内で編集します。
|
||||
3. 作成したブログをVercel、Netlify、GitHub Pagesなどにデプロイするには[ガイド](https://docs.astro.build/ja/guides/deploy/)に従って下さい。加えて、別途デプロイを行う前に `astro.config.mjs` を編集してサイト構成を変更する必要があります。
|
||||
|
||||
## 🚀 使用方法 2
|
||||
## 🚀 使用方法
|
||||
|
||||
1. [テンプレート](https://github.com/saicaca/fuwari/generate)から新しいリポジトリを作成するかCloneをします。
|
||||
2. ブログをローカルで編集するには、リポジトリをクローンした後、`pnpm install` と `pnpm add sharp` を実行して依存関係をインストールします。
|
||||
57
README.ko.md
Normal file
57
README.ko.md
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# 🍥Fuwari
|
||||
|
||||
[Astro](https://astro.build)로 구축된 정적 블로그 템플릿입니다.
|
||||
|
||||
[**🖥️미리보기 (Vercel)**](https://fuwari.vercel.app) /
|
||||
[**📦Old Hexo Version**](https://github.com/saicaca/hexo-theme-vivia)
|
||||
|
||||
> README 버전: `2024-04-07`
|
||||
|
||||

|
||||
|
||||
## ✨ 특징
|
||||
|
||||
- [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를 사용하여 도움 받기 |
|
||||
89
README.md
89
README.md
|
|
@ -1,24 +1,19 @@
|
|||
# 🍥Fuwari
|
||||

|
||||

|
||||
[](https://deepwiki.com/saicaca/fuwari)
|
||||
[](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).
|
||||
|
||||
[**🖥️ Live Demo (Vercel)**](https://fuwari.vercel.app)
|
||||
[**🖥️ Live Demo (Vercel)**](https://fuwari.vercel.app) /
|
||||
[**📦 Old Hexo Version**](https://github.com/saicaca/hexo-theme-vivia) /
|
||||
[**🌏 中文**](https://github.com/saicaca/fuwari/blob/main/README.zh-CN.md) /
|
||||
[**🌏 日本語**](https://github.com/saicaca/fuwari/blob/main/README.ja-JP.md) /
|
||||
[**🌏 한국어**](https://github.com/saicaca/fuwari/blob/main/README.ko.md) /
|
||||
[**🌏 Español**](https://github.com/saicaca/fuwari/blob/main/README.es.md) /
|
||||
[**🌏 ไทย**](https://github.com/saicaca/fuwari/blob/main/README.th.md)
|
||||
|
||||
> README version: `2024-09-10`
|
||||
|
||||

|
||||
|
||||
🌏 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
|
||||
|
||||
- [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] Customizable theme colors & banner
|
||||
- [x] Responsive design
|
||||
- [x] Search functionality with [Pagefind](https://pagefind.app/)
|
||||
- [x] [Markdown extended features](https://github.com/saicaca/fuwari?tab=readme-ov-file#-markdown-extended-syntax)
|
||||
- [x] Table of contents
|
||||
- [x] RSS feed
|
||||
- [ ] Comments
|
||||
- [x] Search
|
||||
- [ ] TOC
|
||||
|
||||
## 🚀 Getting Started
|
||||
## 🚀 How to Use
|
||||
|
||||
1. Create your blog repository:
|
||||
- [Generate a new repository](https://github.com/saicaca/fuwari/generate) from this template or fork this repository.
|
||||
- Or run one of the following commands:
|
||||
```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.
|
||||
1. [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.
|
||||
- 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.
|
||||
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.
|
||||
|
||||
## 📝 Frontmatter of Posts
|
||||
## ⚙️ Frontmatter of Posts
|
||||
|
||||
```yaml
|
||||
---
|
||||
|
|
@ -64,36 +49,16 @@ lang: jp # Set only if the post's language differs from the site's language
|
|||
---
|
||||
```
|
||||
|
||||
## 🧩 Markdown Extended Syntax
|
||||
|
||||
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
|
||||
## 🧞 Commands
|
||||
|
||||
All commands are run from the root of the project, from a terminal:
|
||||
|
||||
| Command | Action |
|
||||
|:---------------------------|:----------------------------------------------------|
|
||||
| `pnpm install` | Installs dependencies |
|
||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||
| `pnpm build` | Build your production site to `./dist/` |
|
||||
| `pnpm preview` | Preview your build locally, before deploying |
|
||||
| `pnpm check` | Run checks for errors in your code |
|
||||
| `pnpm format` | Format your code using Biome |
|
||||
| `pnpm new-post <filename>` | Create a new post |
|
||||
| `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.
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari?ref=badge_large&issueType=license)
|
||||
| Command | Action |
|
||||
|:------------------------------------|:-------------------------------------------------|
|
||||
| `pnpm install` AND `pnpm add sharp` | Installs dependencies |
|
||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||
| `pnpm build` | Build your production site to `./dist/` |
|
||||
| `pnpm preview` | Preview your build locally, before deploying |
|
||||
| `pnpm new-post <filename>` | Create a new post |
|
||||
| `pnpm astro ...` | Run CLI commands like `astro add`, `astro check` |
|
||||
| `pnpm astro --help` | Get help using the Astro CLI |
|
||||
|
|
|
|||
59
README.th.md
Normal file
59
README.th.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# 🍥Fuwari
|
||||
|
||||
แม่แบบสำหรับเว็บบล็อกแบบ static สร้างด้วย [Astro](https://astro.build)
|
||||
|
||||
[**🖥️ ตัวอย่างการใช้งานจริง (Vercel)**](https://fuwari.vercel.app) /
|
||||
[**📦 เวอร์ชั่นเก่าสำหรับ Hexo**](https://github.com/saicaca/hexo-theme-vivia)
|
||||
|
||||
> เวอร์ชั่นของ README: `2024-09-10`
|
||||
|
||||

|
||||
|
||||
## ✨ คุณสมบัติ
|
||||
|
||||
- [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 |
|
||||
|
|
@ -2,7 +2,10 @@
|
|||
|
||||
基于 [Astro](https://astro.build) 开发的静态博客模板。
|
||||
|
||||
[**🖥️在线预览(Vercel)**](https://fuwari.vercel.app)
|
||||
[**🖥️在线预览(Vercel)**](https://fuwari.vercel.app) /
|
||||
[**📦旧 Hexo 版本**](https://github.com/saicaca/hexo-theme-vivia)
|
||||
|
||||
> README 版本:`2024-09-10`
|
||||
|
||||

|
||||
|
||||
|
|
@ -15,39 +18,9 @@
|
|||
- [x] 响应式设计
|
||||
- [ ] 评论
|
||||
- [x] 搜索
|
||||
- [x] 文内目录
|
||||
- [ ] 文内目录
|
||||
|
||||
## 👀 要求
|
||||
|
||||
- Node.js <= 22
|
||||
- pnpm <= 9
|
||||
|
||||
## 🚀 使用方法 1
|
||||
|
||||
使用 [create-fuwari](https://github.com/L4Ph/create-fuwari) 在本地初始化项目。
|
||||
|
||||
```sh
|
||||
# npm
|
||||
npm create fuwari@latest
|
||||
|
||||
# yarn
|
||||
yarn create fuwari
|
||||
|
||||
# pnpm
|
||||
pnpm create fuwari@latest
|
||||
|
||||
# bun
|
||||
bun create fuwari@latest
|
||||
|
||||
# deno
|
||||
deno run -A npm:create-fuwari@latest
|
||||
```
|
||||
|
||||
1. 通过配置文件 `src/config.ts` 自定义博客
|
||||
2. 执行 `pnpm new-post <filename>` 创建新文章,并在 `src/content/posts/` 目录中编辑
|
||||
3. 参考[官方指南](https://docs.astro.build/zh-cn/guides/deploy/)将博客部署至 Vercel, Netlify, GitHub Pages 等;部署前需编辑 `astro.config.mjs` 中的站点设置。
|
||||
|
||||
## 🚀 使用方法 2
|
||||
## 🚀 使用方法
|
||||
|
||||
1. 使用此模板[生成新仓库](https://github.com/saicaca/fuwari/generate)或 Fork 此仓库
|
||||
2. 进行本地开发,Clone 新的仓库,执行 `pnpm install` 和 `pnpm add sharp` 以安装依赖
|
||||
258
astro.config.mjs
258
astro.config.mjs
|
|
@ -1,10 +1,8 @@
|
|||
import sitemap from "@astrojs/sitemap";
|
||||
import svelte from "@astrojs/svelte";
|
||||
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 expressiveCode from "astro-expressive-code";
|
||||
import Compress from "astro-compress";
|
||||
import icon from "astro-icon";
|
||||
import { defineConfig } from "astro/config";
|
||||
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 remarkMath from "remark-math";
|
||||
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 { GithubCardComponent } from "./src/plugins/rehype-component-github-card.mjs";
|
||||
import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js";
|
||||
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js";
|
||||
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
|
||||
import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-copy-button.js";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
site: "https://fuwari.vercel.app/",
|
||||
base: "/",
|
||||
trailingSlash: "always",
|
||||
integrations: [
|
||||
tailwind({
|
||||
nesting: true,
|
||||
}),
|
||||
swup({
|
||||
theme: false,
|
||||
animationClass: "transition-swup-", // see https://swup.js.org/options/#animationselector
|
||||
// the default value `transition-` cause transition delay
|
||||
// when the Tailwind class `transition-all` is used
|
||||
containers: ["main", "#toc"],
|
||||
smoothScrolling: true,
|
||||
cache: true,
|
||||
preload: true,
|
||||
accessibility: true,
|
||||
updateHead: true,
|
||||
updateBodyClass: false,
|
||||
globalInstance: true,
|
||||
}),
|
||||
icon({
|
||||
include: {
|
||||
"preprocess: vitePreprocess(),": ["*"],
|
||||
"fa6-brands": ["*"],
|
||||
"fa6-regular": ["*"],
|
||||
"fa6-solid": ["*"],
|
||||
},
|
||||
}),
|
||||
expressiveCode({
|
||||
themes: [expressiveCodeConfig.theme, expressiveCodeConfig.theme],
|
||||
plugins: [
|
||||
pluginCollapsibleSections(),
|
||||
pluginLineNumbers(),
|
||||
pluginLanguageBadge(),
|
||||
pluginCustomCopyButton()
|
||||
],
|
||||
defaultProps: {
|
||||
wrap: true,
|
||||
overridesByLang: {
|
||||
'shellsession': {
|
||||
showLineNumbers: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
styleOverrides: {
|
||||
codeBackground: "var(--codeblock-bg)",
|
||||
borderRadius: "0.75rem",
|
||||
borderColor: "none",
|
||||
codeFontSize: "0.875rem",
|
||||
codeFontFamily: "'JetBrains Mono Variable', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
codeLineHeight: "1.5rem",
|
||||
frames: {
|
||||
editorBackground: "var(--codeblock-bg)",
|
||||
terminalBackground: "var(--codeblock-bg)",
|
||||
terminalTitlebarBackground: "var(--codeblock-topbar-bg)",
|
||||
editorTabBarBackground: "var(--codeblock-topbar-bg)",
|
||||
editorActiveTabBackground: "none",
|
||||
editorActiveTabIndicatorBottomColor: "var(--primary)",
|
||||
editorActiveTabIndicatorTopColor: "none",
|
||||
editorTabBarBorderBottomColor: "var(--codeblock-topbar-bg)",
|
||||
terminalTitlebarBorderBottomColor: "none"
|
||||
},
|
||||
textMarkers: {
|
||||
delHue: 0,
|
||||
insHue: 180,
|
||||
markHue: 250
|
||||
}
|
||||
},
|
||||
frames: {
|
||||
showCopyToClipboardButton: false,
|
||||
}
|
||||
}),
|
||||
svelte(),
|
||||
sitemap(),
|
||||
],
|
||||
markdown: {
|
||||
remarkPlugins: [
|
||||
remarkMath,
|
||||
remarkReadingTime,
|
||||
remarkExcerpt,
|
||||
remarkGithubAdmonitionsToDirectives,
|
||||
remarkDirective,
|
||||
remarkSectionize,
|
||||
parseDirectiveNode,
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeKatex,
|
||||
rehypeSlug,
|
||||
[
|
||||
rehypeComponents,
|
||||
{
|
||||
components: {
|
||||
github: GithubCardComponent,
|
||||
note: (x, y) => AdmonitionComponent(x, y, "note"),
|
||||
tip: (x, y) => AdmonitionComponent(x, y, "tip"),
|
||||
important: (x, y) => AdmonitionComponent(x, y, "important"),
|
||||
caution: (x, y) => AdmonitionComponent(x, y, "caution"),
|
||||
warning: (x, y) => AdmonitionComponent(x, y, "warning"),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
rehypeAutolinkHeadings,
|
||||
{
|
||||
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);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
site: "https://blog.juyung.com",
|
||||
base: "/",
|
||||
trailingSlash: "always",
|
||||
integrations: [
|
||||
tailwind(
|
||||
{
|
||||
nesting: true,
|
||||
}
|
||||
),
|
||||
swup({
|
||||
theme: false,
|
||||
animationClass: "transition-swup-", // see https://swup.js.org/options/#animationselector
|
||||
// the default value `transition-` cause transition delay
|
||||
// when the Tailwind class `transition-all` is used
|
||||
containers: ["main", "#toc"],
|
||||
smoothScrolling: true,
|
||||
cache: true,
|
||||
preload: true,
|
||||
accessibility: true,
|
||||
updateHead: true,
|
||||
updateBodyClass: false,
|
||||
globalInstance: true,
|
||||
}),
|
||||
icon({
|
||||
include: {
|
||||
"preprocess: vitePreprocess(),": ["*"],
|
||||
"fa6-brands": ["*"],
|
||||
"fa6-regular": ["*"],
|
||||
"fa6-solid": ["*"],
|
||||
},
|
||||
}),
|
||||
svelte(),
|
||||
sitemap(),
|
||||
Compress({
|
||||
CSS: false,
|
||||
Image: false,
|
||||
Action: {
|
||||
Passed: async () => true, // https://github.com/PlayForm/Compress/issues/376
|
||||
},
|
||||
}),
|
||||
],
|
||||
markdown: {
|
||||
remarkPlugins: [
|
||||
remarkMath,
|
||||
remarkReadingTime,
|
||||
remarkExcerpt,
|
||||
remarkGithubAdmonitionsToDirectives,
|
||||
remarkDirective,
|
||||
remarkSectionize,
|
||||
parseDirectiveNode,
|
||||
],
|
||||
rehypePlugins: [
|
||||
rehypeKatex,
|
||||
rehypeSlug,
|
||||
[
|
||||
rehypeComponents,
|
||||
{
|
||||
components: {
|
||||
github: GithubCardComponent,
|
||||
note: (x, y) => AdmonitionComponent(x, y, "note"),
|
||||
tip: (x, y) => AdmonitionComponent(x, y, "tip"),
|
||||
important: (x, y) => AdmonitionComponent(x, y, "important"),
|
||||
caution: (x, y) => AdmonitionComponent(x, y, "caution"),
|
||||
warning: (x, y) => AdmonitionComponent(x, y, "warning"),
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
rehypeAutolinkHeadings,
|
||||
{
|
||||
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);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
|||
125
biome.json
125
biome.json
|
|
@ -1,63 +1,66 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"includes": [
|
||||
"**",
|
||||
"!**/src/**/*.css",
|
||||
"!**/src/public/**/*",
|
||||
"!**/dist/**/*",
|
||||
"!**/node_modules/**/*"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "tab"
|
||||
},
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"style": {
|
||||
"noParameterAssign": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["**/*.svelte", "**/*.astro", "**/*.vue"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"useConst": "off",
|
||||
"useImportType": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"noUnusedVariables": "off",
|
||||
"noUnusedImports": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
|
||||
"extends": [],
|
||||
"files": { "ignoreUnknown": true },
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"ignore": ["src/config.ts"],
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80
|
||||
},
|
||||
"javascript": {
|
||||
"parser": {
|
||||
"unsafeParameterDecoratorsEnabled": true
|
||||
},
|
||||
"formatter": {
|
||||
"quoteStyle": "single",
|
||||
"jsxQuoteStyle": "single",
|
||||
"trailingCommas": "all",
|
||||
"semicolons": "asNeeded",
|
||||
"arrowParentheses": "asNeeded"
|
||||
}
|
||||
},
|
||||
"json": {
|
||||
"parser": { "allowComments": true },
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"lineWidth": 80
|
||||
}
|
||||
},
|
||||
"linter": {
|
||||
"ignore": [],
|
||||
"rules": {
|
||||
"a11y": {
|
||||
"recommended": true
|
||||
},
|
||||
"complexity": {
|
||||
"recommended": true
|
||||
},
|
||||
"correctness": {
|
||||
"recommended": true
|
||||
},
|
||||
"performance": {
|
||||
"recommended": true
|
||||
},
|
||||
"security": {
|
||||
"recommended": true
|
||||
},
|
||||
"style": {
|
||||
"recommended": true
|
||||
},
|
||||
"suspicious": {
|
||||
"recommended": true
|
||||
},
|
||||
"nursery": {
|
||||
"recommended": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,106 +0,0 @@
|
|||
# 🍥 Fuwari
|
||||
|
||||
Template blog statis yang dibangun dengan [Astro](https://astro.build).
|
||||
|
||||
[**🖥️ Demo Langsung (Vercel)**](https://fuwari.vercel.app)
|
||||
|
||||

|
||||
|
||||
🌏 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.
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
# 🍥Fuwari
|
||||
|
||||
[Astro](https://astro.build)로 구축된 정적 블로그 템플릿입니다.
|
||||
|
||||
[**🖥️미리보기 (Vercel)**](https://fuwari.vercel.app)
|
||||
|
||||

|
||||
|
||||
## ✨ 특징
|
||||
|
||||
- [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 라이선스에 따라 라이선스가 부여됩니다.
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Fsaicaca%2Ffuwari?ref=badge_large&issueType=license)
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
# 🍥Fuwari
|
||||
|
||||
แม่แบบสำหรับเว็บบล็อกแบบ static สร้างด้วย [Astro](https://astro.build)
|
||||
|
||||
[**🖥️ ตัวอย่างการใช้งานจริง (Vercel)**](https://fuwari.vercel.app)
|
||||
|
||||

|
||||
|
||||
## ✨ คุณสมบัติ
|
||||
|
||||
- [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
|
||||
|
|
@ -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)
|
||||
|
||||

|
||||
|
||||
## ✨ 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.
|
||||
75
package.json
75
package.json
|
|
@ -5,72 +5,67 @@
|
|||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"check": "astro check",
|
||||
"build": "astro build && pagefind --site dist",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro",
|
||||
"type-check": "tsc --noEmit --isolatedDeclarations",
|
||||
"new-post": "node scripts/new-post.js",
|
||||
"format": "biome format --write ./src",
|
||||
"lint": "biome check --write ./src",
|
||||
"lint": "biome check --apply ./src",
|
||||
"preinstall": "npx only-allow pnpm"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.6",
|
||||
"@astrojs/rss": "^4.0.14",
|
||||
"@astrojs/sitemap": "^3.6.0",
|
||||
"@astrojs/svelte": "7.2.3",
|
||||
"@astrojs/tailwind": "^6.0.2",
|
||||
"@expressive-code/core": "^0.41.4",
|
||||
"@expressive-code/plugin-collapsible-sections": "^0.41.4",
|
||||
"@expressive-code/plugin-line-numbers": "^0.41.4",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@fontsource/roboto": "^5.2.9",
|
||||
"@iconify-json/fa6-brands": "^1.2.6",
|
||||
"@iconify-json/fa6-regular": "^1.2.4",
|
||||
"@iconify-json/fa6-solid": "^1.2.4",
|
||||
"@iconify-json/material-symbols": "^1.2.50",
|
||||
"@iconify/svelte": "^4.2.0",
|
||||
"@swup/astro": "^1.7.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"astro": "5.13.10",
|
||||
"astro-expressive-code": "^0.41.4",
|
||||
"astro-icon": "^1.1.5",
|
||||
"hastscript": "^9.0.1",
|
||||
"katex": "^0.16.27",
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@astrojs/rss": "^4.0.9",
|
||||
"@astrojs/sitemap": "^3.2.1",
|
||||
"@astrojs/svelte": "^6.0.1",
|
||||
"@astrojs/tailwind": "^5.1.2",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.1.1",
|
||||
"@fontsource/roboto": "^5.1.0",
|
||||
"@iconify-json/fa6-brands": "^1.2.1",
|
||||
"@iconify-json/fa6-regular": "^1.2.1",
|
||||
"@iconify-json/fa6-solid": "^1.2.1",
|
||||
"@iconify-json/material-symbols": "^1.2.5",
|
||||
"@iconify/svelte": "^4.0.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@swup/astro": "^1.4.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"astro": "^4.16.13",
|
||||
"astro-compress": "^2.3.5",
|
||||
"astro-icon": "^1.1.1",
|
||||
"hastscript": "^9.0.0",
|
||||
"markdown-it": "^14.1.0",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"overlayscrollbars": "^2.12.0",
|
||||
"pagefind": "^1.4.0",
|
||||
"overlayscrollbars": "^2.10.0",
|
||||
"pagefind": "^1.1.1",
|
||||
"photoswipe": "^5.4.4",
|
||||
"reading-time": "^1.5.0",
|
||||
"rehype-autolink-headings": "^7.1.0",
|
||||
"rehype-components": "^0.3.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-directive": "^3.0.1",
|
||||
"remark-directive": "^3.0.0",
|
||||
"remark-directive-rehype": "^0.4.2",
|
||||
"remark-github-admonitions-to-directives": "^1.0.5",
|
||||
"remark-math": "^6.0.0",
|
||||
"remark-sectionize": "^2.1.0",
|
||||
"sanitize-html": "^2.17.0",
|
||||
"sharp": "^0.34.5",
|
||||
"stylus": "^0.64.0",
|
||||
"svelte": "^5.39.8",
|
||||
"tailwindcss": "^3.4.19",
|
||||
"typescript": "^5.9.3",
|
||||
"remark-sectionize": "^2.0.0",
|
||||
"sanitize-html": "^2.13.1",
|
||||
"sharp": "^0.33.5",
|
||||
"stylus": "^0.63.0",
|
||||
"svelte": "^5.2.2",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"typescript": "^5.6.3",
|
||||
"unist-util-visit": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/ts-plugin": "^1.10.6",
|
||||
"@biomejs/biome": "2.2.5",
|
||||
"@astrojs/ts-plugin": "^1.10.4",
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"@rollup/plugin-yaml": "^4.1.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"@types/markdown-it": "^14.1.2",
|
||||
"@types/mdast": "^4.0.4",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"postcss-import": "^16.1.1",
|
||||
"postcss-nesting": "^13.0.2"
|
||||
"@types/sanitize-html": "^2.13.0",
|
||||
"postcss-import": "^16.1.0",
|
||||
"postcss-nesting": "^13.0.1"
|
||||
},
|
||||
"packageManager": "pnpm@9.14.4"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
exclude_selectors:
|
||||
- "span.katex"
|
||||
- "span.katex-display"
|
||||
- "[data-pagefind-ignore]"
|
||||
- ".search-panel"
|
||||
- "#search-panel"
|
||||
8244
pnpm-lock.yaml
8244
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -32,16 +32,10 @@ const targetDir = "./src/content/posts/"
|
|||
const fullPath = path.join(targetDir, fileName)
|
||||
|
||||
if (fs.existsSync(fullPath)) {
|
||||
console.error(`Error: File ${fullPath} already exists `)
|
||||
console.error(`Error:File ${fullPath} already exists `)
|
||||
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 = `---
|
||||
title: ${args[0]}
|
||||
published: ${getDate()}
|
||||
|
|
|
|||
BIN
src/assets/banner.jpg
Normal file
BIN
src/assets/banner.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
BIN
src/assets/images/avatar.jpg
Normal file
BIN
src/assets/images/avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.6 KiB |
BIN
src/assets/images/banner.jpg
Normal file
BIN
src/assets/images/banner.jpg
Normal file
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 |
118
src/components/ArchivePanel.astro
Normal file
118
src/components/ArchivePanel.astro
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
|
||||
import { siteConfig } from "../config";
|
||||
import { siteConfig } from '../config'
|
||||
---
|
||||
|
||||
<div id="config-carrier" data-hue={siteConfig.themeColor.hue}>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
---
|
||||
|
||||
import { profileConfig } from "../config";
|
||||
import { url } from "../utils/url-utils";
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
import { profileConfig } from '../config'
|
||||
import { url } from '../utils/url-utils'
|
||||
const currentYear = new Date().getFullYear()
|
||||
---
|
||||
|
||||
<!--<div class="border-t border-[var(--primary)] mx-16 border-dashed py-8 max-w-[var(--page-width)] flex flex-col items-center justify-center px-6">-->
|
||||
|
|
|
|||
|
|
@ -1,59 +1,59 @@
|
|||
<script lang="ts">
|
||||
import { AUTO_MODE, DARK_MODE, LIGHT_MODE } from "@constants/constants.ts";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import Icon from "@iconify/svelte";
|
||||
import type { LIGHT_DARK_MODE } from '@/types/config.ts'
|
||||
import { AUTO_MODE, DARK_MODE, LIGHT_MODE } from '@constants/constants.ts'
|
||||
import I18nKey from '@i18n/i18nKey'
|
||||
import { i18n } from '@i18n/translation'
|
||||
import Icon from '@iconify/svelte'
|
||||
import {
|
||||
applyThemeToDocument,
|
||||
getStoredTheme,
|
||||
setTheme,
|
||||
} from "@utils/setting-utils.ts";
|
||||
import { onMount } from "svelte";
|
||||
import type { LIGHT_DARK_MODE } from "@/types/config.ts";
|
||||
applyThemeToDocument,
|
||||
getStoredTheme,
|
||||
setTheme,
|
||||
} from '@utils/setting-utils.ts'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE];
|
||||
let mode: LIGHT_DARK_MODE = $state(AUTO_MODE);
|
||||
const seq: LIGHT_DARK_MODE[] = [LIGHT_MODE, DARK_MODE, AUTO_MODE]
|
||||
let mode: LIGHT_DARK_MODE = $state(AUTO_MODE)
|
||||
|
||||
onMount(() => {
|
||||
mode = getStoredTheme();
|
||||
const darkModePreference = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const changeThemeWhenSchemeChanged: Parameters<
|
||||
typeof darkModePreference.addEventListener<"change">
|
||||
>[1] = (_e) => {
|
||||
applyThemeToDocument(mode);
|
||||
};
|
||||
darkModePreference.addEventListener("change", changeThemeWhenSchemeChanged);
|
||||
return () => {
|
||||
darkModePreference.removeEventListener(
|
||||
"change",
|
||||
changeThemeWhenSchemeChanged,
|
||||
);
|
||||
};
|
||||
});
|
||||
mode = getStoredTheme()
|
||||
const darkModePreference = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const changeThemeWhenSchemeChanged: Parameters<
|
||||
typeof darkModePreference.addEventListener<'change'>
|
||||
>[1] = e => {
|
||||
applyThemeToDocument(mode)
|
||||
}
|
||||
darkModePreference.addEventListener('change', changeThemeWhenSchemeChanged)
|
||||
return () => {
|
||||
darkModePreference.removeEventListener(
|
||||
'change',
|
||||
changeThemeWhenSchemeChanged,
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
function switchScheme(newMode: LIGHT_DARK_MODE) {
|
||||
mode = newMode;
|
||||
setTheme(newMode);
|
||||
mode = newMode
|
||||
setTheme(newMode)
|
||||
}
|
||||
|
||||
function toggleScheme() {
|
||||
let i = 0;
|
||||
for (; i < seq.length; i++) {
|
||||
if (seq[i] === mode) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
switchScheme(seq[(i + 1) % seq.length]);
|
||||
let i = 0
|
||||
for (; i < seq.length; i++) {
|
||||
if (seq[i] === mode) {
|
||||
break
|
||||
}
|
||||
}
|
||||
switchScheme(seq[(i + 1) % seq.length])
|
||||
}
|
||||
|
||||
function showPanel() {
|
||||
const panel = document.querySelector("#light-dark-panel");
|
||||
panel.classList.remove("float-panel-closed");
|
||||
const panel = document.querySelector('#light-dark-panel')
|
||||
panel.classList.remove('float-panel-closed')
|
||||
}
|
||||
|
||||
function hidePanel() {
|
||||
const panel = document.querySelector("#light-dark-panel");
|
||||
panel.classList.add("float-panel-closed");
|
||||
const panel = document.querySelector('#light-dark-panel')
|
||||
panel.classList.add('float-panel-closed')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { navBarConfig, siteConfig } from "../config";
|
||||
import { LinkPresets } from "../constants/link-presets";
|
||||
import { LinkPreset, type NavBarLink } from "../types/config";
|
||||
import { url } from "../utils/url-utils";
|
||||
import LightDarkSwitch from "./LightDarkSwitch.svelte";
|
||||
import Search from "./Search.svelte";
|
||||
import DisplaySettings from "./widget/DisplaySettings.svelte";
|
||||
import NavMenuPanel from "./widget/NavMenuPanel.astro";
|
||||
|
||||
const className = Astro.props.class;
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import DisplaySettings from './widget/DisplaySettings.svelte'
|
||||
import { LinkPreset, type NavBarLink } from '../types/config'
|
||||
import { navBarConfig, siteConfig } from '../config'
|
||||
import NavMenuPanel from './widget/NavMenuPanel.astro'
|
||||
import Search from './Search.svelte'
|
||||
import { LinkPresets } from '../constants/link-presets'
|
||||
import LightDarkSwitch from './LightDarkSwitch.svelte'
|
||||
import { url } from '../utils/url-utils'
|
||||
const className = Astro.props.class
|
||||
|
||||
let links: NavBarLink[] = navBarConfig.links.map(
|
||||
(item: NavBarLink | LinkPreset): NavBarLink => {
|
||||
if (typeof item === "number") {
|
||||
return LinkPresets[item];
|
||||
}
|
||||
return item;
|
||||
},
|
||||
);
|
||||
(item: NavBarLink | LinkPreset): NavBarLink => {
|
||||
if (typeof item === 'number') {
|
||||
return LinkPresets[item]
|
||||
}
|
||||
return item
|
||||
},
|
||||
)
|
||||
---
|
||||
<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 -->
|
||||
|
|
@ -45,13 +44,13 @@ let links: NavBarLink[] = navBarConfig.links.map(
|
|||
</div>
|
||||
<div class="flex">
|
||||
<!--<SearchPanel client:load>-->
|
||||
<Search client:only="svelte"></Search>
|
||||
<Search client:load></Search>
|
||||
{!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">
|
||||
<Icon name="material-symbols:palette-outline" class="text-[1.25rem]"></Icon>
|
||||
</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">
|
||||
<Icon name="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon>
|
||||
</button>
|
||||
|
|
@ -62,6 +61,7 @@ let links: NavBarLink[] = navBarConfig.links.map(
|
|||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
function switchTheme() {
|
||||
if (localStorage.theme === 'dark') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
|
|
@ -75,67 +75,48 @@ function switchTheme() {
|
|||
function loadButtonScript() {
|
||||
let switchBtn = document.getElementById("scheme-switch");
|
||||
if (switchBtn) {
|
||||
switchBtn.onclick = function () {
|
||||
switchBtn.addEventListener("click", function () {
|
||||
switchTheme()
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let settingBtn = document.getElementById("display-settings-switch");
|
||||
if (settingBtn) {
|
||||
settingBtn.onclick = function () {
|
||||
settingBtn.addEventListener("click", function () {
|
||||
let settingPanel = document.getElementById("display-setting");
|
||||
if (settingPanel) {
|
||||
settingPanel.classList.toggle("float-panel-closed");
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
let menuBtn = document.getElementById("nav-menu-switch");
|
||||
if (menuBtn) {
|
||||
menuBtn.onclick = function () {
|
||||
menuBtn.addEventListener("click", function () {
|
||||
let menuPanel = document.getElementById("nav-menu-panel");
|
||||
if (menuPanel) {
|
||||
menuPanel.classList.toggle("float-panel-closed");
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
loadButtonScript();
|
||||
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
loadButtonScript();
|
||||
}, { once: false });
|
||||
</script>
|
||||
|
||||
{import.meta.env.PROD && <script is:inline define:vars={{scriptUrl: url('/pagefind/pagefind.js')}}>
|
||||
async function loadPagefind() {
|
||||
try {
|
||||
const response = await fetch(scriptUrl, { method: 'HEAD' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Pagefind script not found: ${response.status}`);
|
||||
}
|
||||
|
||||
const pagefind = await import(scriptUrl);
|
||||
|
||||
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();
|
||||
const pagefind = await import(scriptUrl)
|
||||
await pagefind.options({
|
||||
'excerptLength': 20
|
||||
})
|
||||
pagefind.init()
|
||||
window.pagefind = pagefind
|
||||
pagefind.search('') // speed up the first search
|
||||
}
|
||||
loadPagefind()
|
||||
</script>}
|
||||
|
|
|
|||
|
|
@ -1,46 +1,45 @@
|
|||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import path from "node:path";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import { getDir } from "../utils/url-utils";
|
||||
import ImageWrapper from "./misc/ImageWrapper.astro";
|
||||
import PostMetadata from "./PostMeta.astro";
|
||||
import path from 'path'
|
||||
import PostMetadata from './PostMeta.astro'
|
||||
import ImageWrapper from './misc/ImageWrapper.astro'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { i18n } from '../i18n/translation'
|
||||
import I18nKey from '../i18n/i18nKey'
|
||||
import { getDir } from '../utils/url-utils'
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
entry: CollectionEntry<"posts">;
|
||||
title: string;
|
||||
url: string;
|
||||
published: Date;
|
||||
updated?: Date;
|
||||
tags: string[];
|
||||
category: string | null;
|
||||
image: string;
|
||||
description: string;
|
||||
draft: boolean;
|
||||
style: string;
|
||||
class?: string
|
||||
entry: any
|
||||
title: string
|
||||
url: string
|
||||
published: Date
|
||||
updated?: Date
|
||||
tags: string[]
|
||||
category: string
|
||||
image: string
|
||||
description: string
|
||||
draft: boolean
|
||||
style: string
|
||||
}
|
||||
const {
|
||||
entry,
|
||||
title,
|
||||
url,
|
||||
published,
|
||||
updated,
|
||||
tags,
|
||||
category,
|
||||
image,
|
||||
description,
|
||||
style,
|
||||
} = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
entry,
|
||||
title,
|
||||
url,
|
||||
published,
|
||||
updated,
|
||||
tags,
|
||||
category,
|
||||
image,
|
||||
description,
|
||||
style,
|
||||
} = Astro.props
|
||||
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={["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 -->
|
||||
<div class="text-sm text-black/30 dark:text-white/30 flex gap-4 transition">
|
||||
<div>
|
||||
{remarkPluginFrontmatter.words} {" " + i18n(remarkPluginFrontmatter.words === 1 ? I18nKey.wordCount : I18nKey.wordsCount)}
|
||||
</div>
|
||||
<div>{remarkPluginFrontmatter.words} {" " + i18n(I18nKey.wordsCount)}</div>
|
||||
<div>|</div>
|
||||
<div>
|
||||
{remarkPluginFrontmatter.minutes} {" " + i18n(remarkPluginFrontmatter.minutes === 1 ? I18nKey.minuteCount : I18nKey.minutesCount)}
|
||||
</div>
|
||||
<div>{remarkPluginFrontmatter.minutes} {" " + i18n(I18nKey.minutesCount)}</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,28 +1,21 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import { i18n } from "../i18n/translation";
|
||||
import { formatDateToYYYYMMDD } from "../utils/date-utils";
|
||||
import { getCategoryUrl, getTagUrl } from "../utils/url-utils";
|
||||
import { formatDateToYYYYMMDD } from '../utils/date-utils'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { i18n } from '../i18n/translation'
|
||||
import I18nKey from '../i18n/i18nKey'
|
||||
import { url } from '../utils/url-utils'
|
||||
|
||||
interface Props {
|
||||
class: string;
|
||||
published: Date;
|
||||
updated?: Date;
|
||||
tags: string[];
|
||||
category: string | null;
|
||||
hideTagsForMobile?: boolean;
|
||||
hideUpdateDate?: boolean;
|
||||
class: string
|
||||
published: Date
|
||||
updated?: Date
|
||||
tags: string[]
|
||||
category: string
|
||||
hideTagsForMobile?: boolean
|
||||
hideUpdateDate?: boolean
|
||||
}
|
||||
const {
|
||||
published,
|
||||
updated,
|
||||
tags,
|
||||
category,
|
||||
hideTagsForMobile = false,
|
||||
hideUpdateDate = false,
|
||||
} = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
const { published, 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]}>
|
||||
|
|
@ -53,7 +46,7 @@ const className = Astro.props.class;
|
|||
<Icon name="material-symbols:book-2-outline-rounded" class="text-xl"></Icon>
|
||||
</div>
|
||||
<div class="flex flex-row flex-nowrap items-center">
|
||||
<a href={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
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
|
||||
{category || i18n(I18nKey.uncategorized)}
|
||||
|
|
@ -70,10 +63,10 @@ const className = Astro.props.class;
|
|||
<div class="flex flex-row flex-nowrap items-center">
|
||||
{(tags && tags.length > 0) && tags.map((tag, i) => (
|
||||
<div class:list={[{"hidden": i == 0}, "mx-1.5 text-[var(--meta-divider)] text-sm"]}>/</div>
|
||||
<a href={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
|
||||
hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap">
|
||||
{tag.trim()}
|
||||
{tag}
|
||||
</a>
|
||||
))}
|
||||
{!(tags && tags.length > 0) && <div class="transition text-50 text-sm font-medium">{i18n(I18nKey.noTags)}</div>}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,29 @@
|
|||
---
|
||||
import type { CollectionEntry } from "astro:content";
|
||||
import { getPostUrlBySlug } from "@utils/url-utils";
|
||||
import PostCard from "./PostCard.astro";
|
||||
import { getPostUrlBySlug } from '@utils/url-utils'
|
||||
import PostCard from './PostCard.astro'
|
||||
|
||||
const { page } = Astro.props;
|
||||
const { page } = Astro.props
|
||||
|
||||
let delay = 0;
|
||||
const interval = 50;
|
||||
let delay = 0
|
||||
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">
|
||||
{page.data.map((entry: CollectionEntry<"posts">) => (
|
||||
<PostCard
|
||||
entry={entry}
|
||||
title={entry.data.title}
|
||||
tags={entry.data.tags}
|
||||
category={entry.data.category}
|
||||
published={entry.data.published}
|
||||
updated={entry.data.updated}
|
||||
url={getPostUrlBySlug(entry.slug)}
|
||||
image={entry.data.image}
|
||||
description={entry.data.description}
|
||||
draft={entry.data.draft}
|
||||
class:list="onload-animation"
|
||||
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
|
||||
></PostCard>
|
||||
))}
|
||||
{page.data.map((entry: { data: { draft: boolean; title: string; tags: string[]; category: string; published: Date; image: string; description: string; updated: Date; }; slug: string; }) => {
|
||||
return (
|
||||
<PostCard
|
||||
entry={entry}
|
||||
title={entry.data.title}
|
||||
tags={entry.data.tags}
|
||||
category={entry.data.category}
|
||||
published={entry.data.published}
|
||||
updated={entry.data.updated}
|
||||
url={getPostUrlBySlug(entry.slug)}
|
||||
image={entry.data.image}
|
||||
description={entry.data.description}
|
||||
draft={entry.data.draft}
|
||||
class:list="onload-animation"
|
||||
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
|
||||
></PostCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
@ -1,141 +1,73 @@
|
|||
<script lang="ts">
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { url } from "@utils/url-utils.ts";
|
||||
import { onMount } from "svelte";
|
||||
import type { SearchResult } from "@/global";
|
||||
import { onMount } from 'svelte'
|
||||
import { url } from '@utils/url-utils.ts'
|
||||
import { i18n } from '@i18n/translation'
|
||||
import I18nKey from '@i18n/i18nKey'
|
||||
import Icon from '@iconify/svelte'
|
||||
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 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;
|
||||
}
|
||||
};
|
||||
let search = (keyword: string, isDesktop: boolean) => {}
|
||||
|
||||
onMount(() => {
|
||||
const initializeSearch = () => {
|
||||
initialized = true;
|
||||
pagefindLoaded =
|
||||
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);
|
||||
};
|
||||
search = async (keyword: string, isDesktop: boolean) => {
|
||||
let panel = document.getElementById('search-panel')
|
||||
if (!panel) return
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
console.log(
|
||||
"Pagefind is not available in development mode. Using mock data.",
|
||||
);
|
||||
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
|
||||
});
|
||||
if (!keyword && isDesktop) {
|
||||
panel.classList.add('float-panel-closed')
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback in case events are not caught or pagefind is already loaded by the time this script runs
|
||||
setTimeout(() => {
|
||||
if (!initialized) {
|
||||
console.log("Fallback: Initializing search after timeout.");
|
||||
initializeSearch();
|
||||
}
|
||||
}, 2000); // Adjust timeout as needed
|
||||
}
|
||||
});
|
||||
let arr = []
|
||||
if (import.meta.env.PROD) {
|
||||
const ret = await pagefind.search(keyword)
|
||||
for (const item of ret.results) {
|
||||
arr.push(await item.data())
|
||||
}
|
||||
} 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 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.","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 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.","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) {
|
||||
(async () => {
|
||||
await search(keywordDesktop, true);
|
||||
})();
|
||||
if (!arr.length && isDesktop) {
|
||||
panel.classList.add('float-panel-closed')
|
||||
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) {
|
||||
(async () => {
|
||||
await search(keywordMobile, false);
|
||||
})();
|
||||
}
|
||||
$: search(keywordDesktop, true)
|
||||
$: search(keywordMobile, false)
|
||||
</script>
|
||||
|
||||
<!-- 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 {
|
||||
outline: 0;
|
||||
}
|
||||
.search-panel {
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -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` -->
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
---
|
||||
interface Props {
|
||||
badge?: string;
|
||||
url?: string;
|
||||
label?: string;
|
||||
badge?: string
|
||||
url?: string
|
||||
label?: string
|
||||
}
|
||||
const { badge, url, label } = Astro.props;
|
||||
const { badge, url, label } = Astro.props
|
||||
---
|
||||
<a href={url} aria-label={label}>
|
||||
<button
|
||||
|
|
@ -33,7 +33,7 @@ const { badge, url, label } = Astro.props;
|
|||
{ badge !== undefined && badge !== null && badge !== '' &&
|
||||
<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)]
|
||||
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">
|
||||
{ badge }
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
---
|
||||
interface Props {
|
||||
size?: string;
|
||||
dot?: boolean;
|
||||
href?: string;
|
||||
label?: string;
|
||||
size?: string
|
||||
dot?: boolean
|
||||
href?: 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">
|
||||
{dot && <div class="h-1 w-1 bg-[var(--btn-content)] dark:bg-[var(--card-bg)] transition rounded-md mr-2"></div>}
|
||||
|
|
|
|||
|
|
@ -1,58 +1,57 @@
|
|||
---
|
||||
import type { Page } from "astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { url } from "../../utils/url-utils";
|
||||
|
||||
import type { Page } from 'astro'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { url } from '../../utils/url-utils'
|
||||
interface Props {
|
||||
page: Page;
|
||||
class?: string;
|
||||
style?: string;
|
||||
page: Page
|
||||
class?: 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 VISIBLE = ADJ_DIST * 2 + 1;
|
||||
const ADJ_DIST = 2
|
||||
const VISIBLE = ADJ_DIST * 2 + 1
|
||||
|
||||
// for test
|
||||
let count = 1;
|
||||
let l = page.currentPage;
|
||||
let r = page.currentPage;
|
||||
let count = 1
|
||||
let l = page.currentPage,
|
||||
r = page.currentPage
|
||||
while (0 < l - 1 && r + 1 <= page.lastPage && count + 2 <= VISIBLE) {
|
||||
count += 2;
|
||||
l--;
|
||||
r++;
|
||||
count += 2
|
||||
l--
|
||||
r++
|
||||
}
|
||||
while (0 < l - 1 && count < VISIBLE) {
|
||||
count++;
|
||||
l--;
|
||||
count++
|
||||
l--
|
||||
}
|
||||
while (r + 1 <= page.lastPage && count < VISIBLE) {
|
||||
count++;
|
||||
r++;
|
||||
count++
|
||||
r++
|
||||
}
|
||||
|
||||
let pages: number[] = [];
|
||||
if (l > 1) pages.push(1);
|
||||
if (l === 3) pages.push(2);
|
||||
if (l > 3) pages.push(HIDDEN);
|
||||
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(page.lastPage - 1);
|
||||
if (r < page.lastPage) pages.push(page.lastPage);
|
||||
let pages: number[] = []
|
||||
if (l > 1) pages.push(1)
|
||||
if (l == 3) pages.push(2)
|
||||
if (l > 3) pages.push(HIDDEN)
|
||||
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(page.lastPage - 1)
|
||||
if (r < page.lastPage) pages.push(page.lastPage)
|
||||
|
||||
const getPageUrl = (p: number) => {
|
||||
if (p === 1) return "/";
|
||||
return `/${p}/`;
|
||||
};
|
||||
if (p == 1) return '/'
|
||||
return `/${p}/`
|
||||
}
|
||||
---
|
||||
|
||||
<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",
|
||||
{"disabled": page.url.prev == undefined}
|
||||
]}
|
||||
|
|
@ -69,12 +68,12 @@ const getPageUrl = (p: number) => {
|
|||
>
|
||||
{p}
|
||||
</div>
|
||||
return <a href={url(getPageUrl(p))} aria-label={`Page ${p}`}
|
||||
return <a href={url(getPageUrl(p))} aria-label=`Page ${p}`
|
||||
class="btn-card w-11 h-11 rounded-lg overflow-hidden active:scale-[0.85]"
|
||||
>{p}</a>
|
||||
})}
|
||||
</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",
|
||||
{"disabled": page.url.next == undefined}
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -1,54 +1,50 @@
|
|||
---
|
||||
import path from "node:path";
|
||||
|
||||
import path from 'path'
|
||||
interface Props {
|
||||
id?: string;
|
||||
src: string;
|
||||
class?: string;
|
||||
alt?: string;
|
||||
position?: string;
|
||||
basePath?: string;
|
||||
id?: string
|
||||
src: string
|
||||
class?: string
|
||||
alt?: string
|
||||
position?: string
|
||||
basePath?: string
|
||||
}
|
||||
import { Image } from 'astro:assets'
|
||||
import { url } from '../../utils/url-utils'
|
||||
|
||||
import { Image } from "astro:assets";
|
||||
import { url } from "../../utils/url-utils";
|
||||
|
||||
const { id, src, alt, position = "center", basePath = "/" } = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
const { id, src, alt, position = 'center', basePath = '/' } = Astro.props
|
||||
const className = Astro.props.class
|
||||
|
||||
const isLocal = !(
|
||||
src.startsWith("/") ||
|
||||
src.startsWith("http") ||
|
||||
src.startsWith("https") ||
|
||||
src.startsWith("data:")
|
||||
);
|
||||
const isPublic = src.startsWith("/");
|
||||
src.startsWith('/') ||
|
||||
src.startsWith('http') ||
|
||||
src.startsWith('https') ||
|
||||
src.startsWith('data:')
|
||||
)
|
||||
const isPublic = src.startsWith('/')
|
||||
|
||||
// TODO temporary workaround for images dynamic import
|
||||
// https://github.com/withastro/astro/issues/3373
|
||||
// biome-ignore lint/suspicious/noImplicitAnyLet: <check later>
|
||||
let img;
|
||||
let img
|
||||
if (isLocal) {
|
||||
const files = import.meta.glob<ImageMetadata>("../../**", {
|
||||
import: "default",
|
||||
});
|
||||
let normalizedPath = path
|
||||
.normalize(path.join("../../", basePath, src))
|
||||
.replace(/\\/g, "/");
|
||||
const file = files[normalizedPath];
|
||||
if (!file) {
|
||||
console.error(
|
||||
`\n[ERROR] Image file not found: ${normalizedPath.replace("../../", "src/")}`,
|
||||
);
|
||||
}
|
||||
img = await file();
|
||||
const files = import.meta.glob<ImageMetadata>('../../**', {
|
||||
import: 'default',
|
||||
})
|
||||
let normalizedPath = path
|
||||
.normalize(path.join('../../', basePath, src))
|
||||
.replace(/\\/g, '/')
|
||||
const file = files[normalizedPath]
|
||||
if (!file) {
|
||||
console.error(`\n[ERROR] Image file not found: ${normalizedPath.replace('../../', 'src/')}`)
|
||||
}
|
||||
img = await file()
|
||||
}
|
||||
|
||||
const imageClass = "w-full h-full object-cover";
|
||||
const imageStyle = `object-position: ${position}`;
|
||||
const imageClass = 'w-full h-full object-cover'
|
||||
const imageStyle = `object-position: ${position}`
|
||||
---
|
||||
<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>
|
||||
{isLocal && img && <Image src={img} alt={alt || ""} class={imageClass} style={imageStyle}/>}
|
||||
{!isLocal && <img src={isPublic ? url(src) : src} alt={alt || ""} class={imageClass} style={imageStyle}/>}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { licenseConfig, profileConfig } from "../../config";
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
import { formatDateToYYYYMMDD } from "../../utils/date-utils";
|
||||
import { formatDateToYYYYMMDD } from '../../utils/date-utils'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { licenseConfig, profileConfig } from '../../config'
|
||||
import { i18n } from '../../i18n/translation'
|
||||
import I18nKey from '../../i18n/i18nKey'
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
slug: string;
|
||||
pubDate: Date;
|
||||
class: string;
|
||||
title: string
|
||||
slug: string
|
||||
pubDate: Date
|
||||
class: string
|
||||
}
|
||||
|
||||
const { title, pubDate } = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
const profileConf = profileConfig;
|
||||
const licenseConf = licenseConfig;
|
||||
const postUrl = decodeURIComponent(Astro.url.toString());
|
||||
const { title, slug, pubDate } = Astro.props
|
||||
const className = Astro.props.class
|
||||
const profileConf = profileConfig
|
||||
const licenseConf = licenseConfig
|
||||
const postUrl = decodeURIComponent(Astro.url.toString())
|
||||
---
|
||||
<div class={`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`}>
|
||||
<div class=`relative transition overflow-hidden bg-[var(--license-block-bg)] py-5 px-6 ${className}`>
|
||||
<div class="transition font-bold text-black/75 dark:text-white/75">
|
||||
{title}
|
||||
</div>
|
||||
|
|
@ -28,15 +28,15 @@ const postUrl = decodeURIComponent(Astro.url.toString());
|
|||
<div class="flex gap-6 mt-2">
|
||||
<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 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 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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,43 +1,66 @@
|
|||
---
|
||||
import "@fontsource-variable/jetbrains-mono";
|
||||
import "@fontsource-variable/jetbrains-mono/wght-italic.css";
|
||||
import '@fontsource-variable/jetbrains-mono'
|
||||
import '@fontsource-variable/jetbrains-mono/wght-italic.css'
|
||||
|
||||
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="max-w-none custom-md">-->
|
||||
<slot/>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("click", function (e: MouseEvent) {
|
||||
const target = e.target as Element | null;
|
||||
if (target && target.classList.contains("copy-btn")) {
|
||||
const preEle = target.closest("pre");
|
||||
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 observer = new MutationObserver(addPreCopyButton);
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
|
||||
const timeoutId = target.getAttribute("data-timeout-id");
|
||||
if (timeoutId) {
|
||||
clearTimeout(parseInt(timeoutId));
|
||||
document.addEventListener("DOMContentLoaded", addPreCopyButton);
|
||||
|
||||
function addPreCopyButton() {
|
||||
observer.disconnect();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
target.classList.add("success");
|
||||
|
||||
// 设置新的timeout并保存ID到按钮的自定义属性中
|
||||
const newTimeoutId = setTimeout(() => {
|
||||
target.classList.remove("success");
|
||||
let text = codeBlock?.querySelector("code")?.innerText;
|
||||
if (text === undefined) return;
|
||||
await navigator.clipboard.writeText(text);
|
||||
copyButton.classList.add("success");
|
||||
timeout = setTimeout(() => {
|
||||
copyButton.classList.remove("success");
|
||||
}, 1000);
|
||||
|
||||
target.setAttribute("data-timeout-id", newTimeoutId.toString());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
---
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
import { getCategoryList } from "../../utils/content-utils";
|
||||
import ButtonLink from "../control/ButtonLink.astro";
|
||||
import WidgetLayout from "./WidgetLayout.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 COLLAPSE_THRESHOLD = 5;
|
||||
const categories = await getCategoryList()
|
||||
|
||||
const isCollapsed = categories.length >= COLLAPSE_THRESHOLD;
|
||||
const COLLAPSED_HEIGHT = '7.5rem'
|
||||
const COLLAPSE_THRESHOLD = 5
|
||||
|
||||
const isCollapsed = categories.length >= COLLAPSE_THRESHOLD
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
style?: string;
|
||||
class?: string
|
||||
style?: string
|
||||
}
|
||||
const className = Astro.props.class;
|
||||
const style = Astro.props.style;
|
||||
const className = Astro.props.class
|
||||
const style = Astro.props.style
|
||||
---
|
||||
|
||||
<WidgetLayout name={i18n(I18nKey.categories)} id="categories" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT}
|
||||
|
|
@ -25,11 +27,11 @@ const style = Astro.props.style;
|
|||
>
|
||||
{categories.map((c) =>
|
||||
<ButtonLink
|
||||
url={c.url}
|
||||
url={getCategoryUrl(c.name)}
|
||||
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>
|
||||
)}
|
||||
</WidgetLayout>
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
<script lang="ts">
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import Icon from "@iconify/svelte";
|
||||
import { getDefaultHue, getHue, setHue } from "@utils/setting-utils";
|
||||
import { i18n } from '@i18n/translation'
|
||||
import I18nKey from '@i18n/i18nKey'
|
||||
import { getDefaultHue, getHue, setHue } from '@utils/setting-utils'
|
||||
import Icon from '@iconify/svelte'
|
||||
|
||||
let hue = getHue();
|
||||
const defaultHue = getDefaultHue();
|
||||
let hue = getHue()
|
||||
const defaultHue = getDefaultHue()
|
||||
|
||||
function resetHue() {
|
||||
hue = getDefaultHue();
|
||||
hue = getDefaultHue()
|
||||
}
|
||||
|
||||
$: if (hue || hue === 0) {
|
||||
setHue(hue);
|
||||
setHue(hue)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ $: if (hue || hue === 0) {
|
|||
before:absolute before:-left-3 before:top-[0.33rem]"
|
||||
>
|
||||
{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}>
|
||||
<div class="text-[var(--btn-content)]">
|
||||
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { type NavBarLink } from "../../types/config";
|
||||
import { url } from "../../utils/url-utils";
|
||||
import { type NavBarLink } from '../../types/config'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { url } from '../../utils/url-utils'
|
||||
|
||||
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"]}>
|
||||
{links.map((link) => (
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import { profileConfig } from "../../config";
|
||||
import { url } from "../../utils/url-utils";
|
||||
import ImageWrapper from "../misc/ImageWrapper.astro";
|
||||
import ImageWrapper from '../misc/ImageWrapper.astro'
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { profileConfig } from '../../config'
|
||||
import { url } from '../../utils/url-utils'
|
||||
|
||||
const config = profileConfig;
|
||||
const config = profileConfig
|
||||
---
|
||||
<div class="card-base p-3">
|
||||
<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="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="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 =>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,18 @@
|
|||
---
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import Categories from "./Categories.astro";
|
||||
import Profile from "./Profile.astro";
|
||||
import Tag from "./Tags.astro";
|
||||
import Profile from './Profile.astro'
|
||||
import Tag from './Tags.astro'
|
||||
import Categories from './Categories.astro'
|
||||
import type { MarkdownHeading } from 'astro'
|
||||
import TOC from './TOC.astro'
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
headings?: MarkdownHeading[];
|
||||
class? : string
|
||||
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 class="flex flex-col w-full gap-4 mb-4">
|
||||
|
|
|
|||
|
|
@ -1,37 +1,34 @@
|
|||
---
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import type { MarkdownHeading } from 'astro';
|
||||
import { siteConfig } from "../../config";
|
||||
import { url } from "../../utils/url-utils";
|
||||
|
||||
interface Props {
|
||||
class?: string;
|
||||
headings: MarkdownHeading[];
|
||||
class?: string
|
||||
headings: MarkdownHeading[]
|
||||
}
|
||||
|
||||
let { headings = [] } = Astro.props;
|
||||
let { headings = [] } = Astro.props;
|
||||
|
||||
let minDepth = 10;
|
||||
for (const heading of headings) {
|
||||
minDepth = Math.min(minDepth, heading.depth);
|
||||
minDepth = Math.min(minDepth, heading.depth);
|
||||
}
|
||||
|
||||
const className = Astro.props.class;
|
||||
const isPostsRoute = Astro.url.pathname.startsWith(url("/posts/"));
|
||||
const className = Astro.props.class
|
||||
|
||||
const removeTailingHash = (text: string) => {
|
||||
let lastIndexOfHash = text.lastIndexOf("#");
|
||||
if (lastIndexOfHash !== text.length - 1) {
|
||||
return text;
|
||||
}
|
||||
let lastIndexOfHash = text.lastIndexOf('#');
|
||||
if (lastIndexOfHash != text.length - 1) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.substring(0, lastIndexOfHash);
|
||||
};
|
||||
return text.substring(0, lastIndexOfHash);
|
||||
}
|
||||
|
||||
let heading1Count = 1;
|
||||
|
||||
const maxLevel = siteConfig.toc.depth;
|
||||
---
|
||||
{isPostsRoute &&
|
||||
<table-of-contents class:list={[className, "group"]}>
|
||||
{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
|
||||
|
|
@ -55,9 +52,10 @@ const maxLevel = siteConfig.toc.depth;
|
|||
}]}>{removeTailingHash(heading.text)}</div>
|
||||
</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>
|
||||
</table-of-contents>}
|
||||
</table-of-contents>
|
||||
|
||||
|
||||
<script>
|
||||
class TableOfContents extends HTMLElement {
|
||||
|
|
@ -97,7 +95,7 @@ class TableOfContents extends HTMLElement {
|
|||
|
||||
toggleActiveHeading = () => {
|
||||
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]) {
|
||||
this.tocEntries[i].classList.remove(this.visibleClass);
|
||||
i--;
|
||||
|
|
@ -112,15 +110,11 @@ class TableOfContents extends HTMLElement {
|
|||
this.tocEntries[i].classList.remove(this.visibleClass);
|
||||
i--;
|
||||
}
|
||||
if (min > max) {
|
||||
this.activeIndicator?.setAttribute("style", `opacity: 0`);
|
||||
} else {
|
||||
let parentOffset = this.tocEl?.getBoundingClientRect().top || 0;
|
||||
let scrollOffset = this.tocEl?.scrollTop || 0;
|
||||
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`);
|
||||
}
|
||||
let parentOffset = this.tocEl?.getBoundingClientRect().top || 0;
|
||||
let scrollOffset = this.tocEl?.scrollTop || 0;
|
||||
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 = () => {
|
||||
|
|
@ -208,7 +202,7 @@ class TableOfContents extends HTMLElement {
|
|||
this.init();
|
||||
}, { once: true });
|
||||
} 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>
|
||||
|
|
@ -1,30 +1,30 @@
|
|||
---
|
||||
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
import { getTagList } from "../../utils/content-utils";
|
||||
import { getTagUrl } from "../../utils/url-utils";
|
||||
import ButtonTag from "../control/ButtonTag.astro";
|
||||
import WidgetLayout from "./WidgetLayout.astro";
|
||||
import WidgetLayout from './WidgetLayout.astro'
|
||||
import ButtonTag from '../control/ButtonTag.astro'
|
||||
import { getTagList } from '../../utils/content-utils'
|
||||
import { i18n } from '../../i18n/translation'
|
||||
import I18nKey from '../../i18n/i18nKey'
|
||||
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 {
|
||||
class?: string;
|
||||
style?: string;
|
||||
class?: string
|
||||
style?: string
|
||||
}
|
||||
const className = Astro.props.class;
|
||||
const style = Astro.props.style;
|
||||
const className = Astro.props.class
|
||||
const style = Astro.props.style
|
||||
---
|
||||
<WidgetLayout name={i18n(I18nKey.tags)} id="tags" isCollapsed={isCollapsed} collapsedHeight={COLLAPSED_HEIGHT} class={className} style={style}>
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
{tags.map(t => (
|
||||
<ButtonTag href={getTagUrl(t.name)} label={`View all posts with the ${t.name.trim()} tag`}>
|
||||
{t.name.trim()}
|
||||
<ButtonTag href={url(`/archive/tag/${t.name}/`)} label={`View all posts with the ${t.name} tag`}>
|
||||
{t.name}
|
||||
</ButtonTag>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
---
|
||||
import { Icon } from "astro-icon/components";
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import { i18n } from "../../i18n/translation";
|
||||
|
||||
import { Icon } from 'astro-icon/components'
|
||||
import { i18n } from '../../i18n/translation'
|
||||
import I18nKey from '../../i18n/i18nKey'
|
||||
interface Props {
|
||||
id: string;
|
||||
name?: string;
|
||||
isCollapsed?: boolean;
|
||||
collapsedHeight?: string;
|
||||
class?: string;
|
||||
style?: string;
|
||||
id: string
|
||||
name?: string
|
||||
isCollapsed?: boolean
|
||||
collapsedHeight?: string
|
||||
class?: string
|
||||
style?: string
|
||||
}
|
||||
const { id, name, isCollapsed, collapsedHeight, style } = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
const props = Astro.props
|
||||
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}>
|
||||
<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>
|
||||
160
src/config.ts
160
src/config.ts
|
|
@ -1,90 +1,88 @@
|
|||
import type {
|
||||
ExpressiveCodeConfig,
|
||||
LicenseConfig,
|
||||
NavBarConfig,
|
||||
ProfileConfig,
|
||||
SiteConfig,
|
||||
} from "./types/config";
|
||||
import { LinkPreset } from "./types/config";
|
||||
LicenseConfig,
|
||||
NavBarConfig,
|
||||
ProfileConfig,
|
||||
SiteConfig,
|
||||
} from './types/config'
|
||||
import { LinkPreset } from './types/config'
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
title: "Fuwari",
|
||||
subtitle: "Demo Site",
|
||||
lang: "en", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.
|
||||
themeColor: {
|
||||
hue: 250, // 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,
|
||||
src: "assets/images/demo-banner.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
position: "center", // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default
|
||||
credit: {
|
||||
enable: false, // Display the credit text of the banner image
|
||||
text: "", // Credit text to be displayed
|
||||
url: "", // (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
|
||||
depth: 2, // Maximum heading depth to show in the table, from 1 to 3
|
||||
},
|
||||
favicon: [
|
||||
// Leave this array empty to use the default favicon
|
||||
// {
|
||||
// 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
|
||||
// sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes
|
||||
// }
|
||||
],
|
||||
};
|
||||
title: 'Jay',
|
||||
subtitle: 'Blog',
|
||||
description: 'Get clear, step-by-step guidance on computer programming and coding tips. Perfect for beginners and experienced developers seeking practical solutions.',
|
||||
lang: 'en', // 'en', 'zh_CN', 'zh_TW', 'ja', 'ko'
|
||||
themeColor: {
|
||||
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: true,
|
||||
src: 'assets/images/banner.jpg', // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
position: 'center', // Equivalent to object-position, defaults center
|
||||
credit: {
|
||||
enable: true, // Display the credit text of the banner image
|
||||
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
|
||||
depth: 2 // Maximum heading depth to show in the table, from 1 to 3
|
||||
},
|
||||
favicon: [ // Leave this array empty to use the default favicon
|
||||
// {
|
||||
// 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
|
||||
// sizes: '32x32', // (Optional) Size of the favicon, set only if you have favicons of different sizes
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
export const navBarConfig: NavBarConfig = {
|
||||
links: [
|
||||
LinkPreset.Home,
|
||||
LinkPreset.Archive,
|
||||
LinkPreset.About,
|
||||
{
|
||||
name: "GitHub",
|
||||
url: "https://github.com/saicaca/fuwari", // 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
|
||||
},
|
||||
],
|
||||
};
|
||||
links: [
|
||||
LinkPreset.Home,
|
||||
LinkPreset.Archive,
|
||||
LinkPreset.About,
|
||||
{
|
||||
name: 'GitHub',
|
||||
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
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const profileConfig: ProfileConfig = {
|
||||
avatar: "assets/images/demo-avatar.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
name: "Lorem Ipsum",
|
||||
bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
links: [
|
||||
{
|
||||
name: "Twitter",
|
||||
icon: "fa6-brands:twitter", // Visit https://icones.js.org/ for icon codes
|
||||
// You will need to install the corresponding icon set if it's not already included
|
||||
// `pnpm add @iconify-json/<icon-set-name>`
|
||||
url: "https://twitter.com",
|
||||
},
|
||||
{
|
||||
name: "Steam",
|
||||
icon: "fa6-brands:steam",
|
||||
url: "https://store.steampowered.com",
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
icon: "fa6-brands:github",
|
||||
url: "https://github.com/saicaca/fuwari",
|
||||
},
|
||||
],
|
||||
};
|
||||
avatar: 'assets/images/avatar.jpg', // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||
name: 'Jay',
|
||||
bio: 'Programmer, Aerospace Engineering student',
|
||||
links: [
|
||||
{
|
||||
name: 'LinkTree',
|
||||
icon: 'fa6-solid:circle-user',
|
||||
url: 'https://www.juyung.com/',
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
icon: 'fa6-brands:github',
|
||||
url: 'https://git.juyung.com/',
|
||||
},
|
||||
{
|
||||
name: 'Instagram',
|
||||
icon: 'fa6-brands:instagram', // Visit https://icones.js.org/ for icon codes
|
||||
// You will need to install the corresponding icon set if it's not already included
|
||||
// `pnpm add @iconify-json/<icon-set-name>`
|
||||
url: 'https://photos.juyung.com/',
|
||||
},
|
||||
{
|
||||
name: 'Twitter',
|
||||
icon: 'fa6-brands:twitter',
|
||||
url: 'https://social.juyung.com/',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const licenseConfig: LicenseConfig = {
|
||||
enable: true,
|
||||
name: "CC BY-NC-SA 4.0",
|
||||
url: "https://creativecommons.org/licenses/by-nc-sa/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",
|
||||
};
|
||||
enable: true,
|
||||
name: 'CC BY',
|
||||
url: 'https://creativecommons.org/licenses/by/4.0/',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
export const PAGE_SIZE = 8;
|
||||
export const UNCATEGORIZED = '__uncategorized__'
|
||||
|
||||
export const LIGHT_MODE = "light",
|
||||
DARK_MODE = "dark",
|
||||
AUTO_MODE = "auto";
|
||||
export const DEFAULT_THEME = AUTO_MODE;
|
||||
export const PAGE_SIZE = 8
|
||||
|
||||
export const LIGHT_MODE = 'light',
|
||||
DARK_MODE = 'dark',
|
||||
AUTO_MODE = 'auto'
|
||||
export const DEFAULT_THEME = AUTO_MODE
|
||||
|
||||
// Banner height unit: vh
|
||||
export const BANNER_HEIGHT = 35;
|
||||
export const BANNER_HEIGHT_EXTEND = 30;
|
||||
export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND;
|
||||
export const BANNER_HEIGHT = 35
|
||||
export const BANNER_HEIGHT_EXTEND = 30
|
||||
export const BANNER_HEIGHT_HOME = BANNER_HEIGHT + BANNER_HEIGHT_EXTEND
|
||||
|
||||
// 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
|
||||
export const PAGE_WIDTH = 75;
|
||||
export const PAGE_WIDTH = 75
|
||||
|
|
@ -1,44 +1,44 @@
|
|||
import type { Favicon } from "@/types/config.ts";
|
||||
import type { Favicon } from '@/types/config.ts'
|
||||
|
||||
export const defaultFavicons: Favicon[] = [
|
||||
{
|
||||
src: "/favicon/favicon-light-32.png",
|
||||
theme: "light",
|
||||
sizes: "32x32",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-light-128.png",
|
||||
theme: "light",
|
||||
sizes: "128x128",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-light-180.png",
|
||||
theme: "light",
|
||||
sizes: "180x180",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-light-192.png",
|
||||
theme: "light",
|
||||
sizes: "192x192",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-dark-32.png",
|
||||
theme: "dark",
|
||||
sizes: "32x32",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-dark-128.png",
|
||||
theme: "dark",
|
||||
sizes: "128x128",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-dark-180.png",
|
||||
theme: "dark",
|
||||
sizes: "180x180",
|
||||
},
|
||||
{
|
||||
src: "/favicon/favicon-dark-192.png",
|
||||
theme: "dark",
|
||||
sizes: "192x192",
|
||||
},
|
||||
];
|
||||
{
|
||||
src: '/favicon/favicon-light-32.png',
|
||||
theme: 'light',
|
||||
sizes: '32x32',
|
||||
},
|
||||
{
|
||||
src: '/favicon/favicon-light-128.png',
|
||||
theme: 'light',
|
||||
sizes: '128x128',
|
||||
},
|
||||
{
|
||||
src: '/favicon/favicon-light-180.png',
|
||||
theme: 'light',
|
||||
sizes: '180x180',
|
||||
},
|
||||
{
|
||||
src: '/favicon/favicon-light-192.png',
|
||||
theme: 'light',
|
||||
sizes: '192x192',
|
||||
},
|
||||
{
|
||||
src: '/favicon/favicon-dark-32.png',
|
||||
theme: 'dark',
|
||||
sizes: '32x32',
|
||||
},
|
||||
{
|
||||
src: '/favicon/favicon-dark-128.png',
|
||||
theme: 'dark',
|
||||
sizes: '128x128',
|
||||
},
|
||||
{
|
||||
src: '/favicon/favicon-dark-180.png',
|
||||
theme: 'dark',
|
||||
sizes: '180x180',
|
||||
},
|
||||
{
|
||||
src: '/favicon/favicon-dark-192.png',
|
||||
theme: 'dark',
|
||||
sizes: '192x192',
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import I18nKey from "@i18n/i18nKey";
|
||||
import { i18n } from "@i18n/translation";
|
||||
import { LinkPreset, type NavBarLink } from "@/types/config";
|
||||
import { LinkPreset, type NavBarLink } from '@/types/config'
|
||||
import I18nKey from '@i18n/i18nKey'
|
||||
import { i18n } from '@i18n/translation'
|
||||
|
||||
export const LinkPresets: { [key in LinkPreset]: NavBarLink } = {
|
||||
[LinkPreset.Home]: {
|
||||
name: i18n(I18nKey.home),
|
||||
url: "/",
|
||||
},
|
||||
[LinkPreset.About]: {
|
||||
name: i18n(I18nKey.about),
|
||||
url: "/about/",
|
||||
},
|
||||
[LinkPreset.Archive]: {
|
||||
name: i18n(I18nKey.archive),
|
||||
url: "/archive/",
|
||||
},
|
||||
};
|
||||
[LinkPreset.Home]: {
|
||||
name: i18n(I18nKey.home),
|
||||
url: '/',
|
||||
},
|
||||
[LinkPreset.About]: {
|
||||
name: i18n(I18nKey.about),
|
||||
url: '/about/',
|
||||
},
|
||||
[LinkPreset.Archive]: {
|
||||
name: i18n(I18nKey.archive),
|
||||
url: '/archive/',
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,24 @@
|
|||
import { defineCollection, z } from "astro:content";
|
||||
import { defineCollection, z } from 'astro:content'
|
||||
|
||||
const postsCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
published: z.date(),
|
||||
updated: z.date().optional(),
|
||||
draft: z.boolean().optional().default(false),
|
||||
description: z.string().optional().default(""),
|
||||
image: z.string().optional().default(""),
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
category: z.string().optional().nullable().default(""),
|
||||
lang: z.string().optional().default(""),
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
published: z.date(),
|
||||
updated: z.date().optional(),
|
||||
draft: z.boolean().optional().default(false),
|
||||
description: z.string().optional().default(''),
|
||||
image: z.string().optional().default(''),
|
||||
tags: z.array(z.string()).optional().default([]),
|
||||
category: z.string().optional().default(''),
|
||||
lang: z.string().optional().default(''),
|
||||
|
||||
/* For internal use */
|
||||
prevTitle: z.string().default(""),
|
||||
prevSlug: z.string().default(""),
|
||||
nextTitle: z.string().default(""),
|
||||
nextSlug: z.string().default(""),
|
||||
}),
|
||||
});
|
||||
const specCollection = defineCollection({
|
||||
schema: z.object({}),
|
||||
});
|
||||
/* For internal use */
|
||||
prevTitle: z.string().default(''),
|
||||
prevSlug: z.string().default(''),
|
||||
nextTitle: z.string().default(''),
|
||||
nextSlug: z.string().default(''),
|
||||
}),
|
||||
})
|
||||
export const collections = {
|
||||
posts: postsCollection,
|
||||
spec: specCollection,
|
||||
};
|
||||
posts: postsCollection,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
---
|
||||
118
src/content/posts/en/2024/08/24.md
Normal file
118
src/content/posts/en/2024/08/24.md
Normal 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
|
||||
```
|
||||
175
src/content/posts/en/2024/08/25.md
Normal file
175
src/content/posts/en/2024/08/25.md
Normal 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 Thunderbird’s settings to send encrypted emails.
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
Ports 80 and 443 are often used by web servers. Change Mailcow’s 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
|
||||
}
|
||||
```
|
||||
83
src/content/posts/en/2024/08/31.md
Normal file
83
src/content/posts/en/2024/08/31.md
Normal 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
|
||||
```
|
||||
149
src/content/posts/en/2024/09/06.md
Normal file
149
src/content/posts/en/2024/09/06.md
Normal 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.
|
||||
120
src/content/posts/en/2024/09/15.md
Normal file
120
src/content/posts/en/2024/09/15.md
Normal 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 you’re 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".
|
||||
170
src/content/posts/en/2024/10/05.md
Normal file
170
src/content/posts/en/2024/10/05.md
Normal 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: /$
|
||||
```
|
||||
142
src/content/posts/en/2024/10/11.md
Normal file
142
src/content/posts/en/2024/10/11.md
Normal 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)*
|
||||
120
src/content/posts/en/2024/10/26.md
Normal file
120
src/content/posts/en/2024/10/26.md
Normal 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.
|
||||
81
src/content/posts/en/2025/01/02.md
Normal file
81
src/content/posts/en/2025/01/02.md
Normal 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/
|
||||
```
|
||||
98
src/content/posts/en/2025/01/03.md
Normal file
98
src/content/posts/en/2025/01/03.md
Normal 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.
|
||||
87
src/content/posts/en/2025/01/11.md
Normal file
87
src/content/posts/en/2025/01/11.md
Normal 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.
|
||||
85
src/content/posts/en/2025/01/12.md
Normal file
85
src/content/posts/en/2025/01/12.md
Normal 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
|
||||
```
|
||||
200
src/content/posts/en/2025/01/19.md
Normal file
200
src/content/posts/en/2025/01/19.md
Normal 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
|
||||
|
||||
Here’s 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.
|
||||
117
src/content/posts/en/2025/01/20.md
Normal file
117
src/content/posts/en/2025/01/20.md
Normal 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.
|
||||
95
src/content/posts/en/2025/01/25.md
Normal file
95
src/content/posts/en/2025/01/25.md
Normal 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.
|
||||
81
src/content/posts/en/2025/02/01.md
Normal file
81
src/content/posts/en/2025/02/01.md
Normal 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.
|
||||
42
src/content/posts/en/2025/02/08.md
Normal file
42
src/content/posts/en/2025/02/08.md
Normal 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 didn’t 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 didn’t 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
|
||||
```
|
||||
131
src/content/posts/en/2025/02/15.md
Normal file
131
src/content/posts/en/2025/02/15.md
Normal 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.
|
||||
136
src/content/posts/en/2025/02/22.md
Normal file
136
src/content/posts/en/2025/02/22.md
Normal 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)
|
||||
127
src/content/posts/en/2025/03/01.md
Normal file
127
src/content/posts/en/2025/03/01.md
Normal 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)*
|
||||
101
src/content/posts/en/2025/03/08.md
Normal file
101
src/content/posts/en/2025/03/08.md
Normal 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)*
|
||||
97
src/content/posts/en/2025/08/22.md
Normal file
97
src/content/posts/en/2025/08/22.md
Normal 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 server’s 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 Navidrome’s 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)*
|
||||
111
src/content/posts/en/2026/02/05.md
Normal file
111
src/content/posts/en/2026/02/05.md
Normal 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.
|
||||
61
src/content/posts/en/2026/02/08.md
Normal file
61
src/content/posts/en/2026/02/08.md
Normal 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 20–30 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 20–30 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)"`.
|
||||
|
|
@ -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: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
|
||||
- Bold: [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
|
||||
- Dimmed: [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m
|
||||
|
||||
256 colors (showing colors 160-177):
|
||||
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
|
||||
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
|
||||
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m
|
||||
|
||||
Full RGB colors:
|
||||
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m
|
||||
|
||||
Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
|
||||
```
|
||||
|
||||
### 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 |
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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**]!
|
||||
|
||||
```
|
||||
|
|
@ -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:
|
||||
|
||||
[//]: # ()
|
||||
|
||||
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.
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
# 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
|
||||
> - [Unsplash](https://unsplash.com/)
|
||||
> - [星と少女](https://www.pixiv.net/artworks/108916539) by [Stella](https://www.pixiv.net/users/93273965)
|
||||
> - [Rabbit - v1.4 Showcase](https://civitai.com/posts/586908) by [Rabbit_YourMajesty](https://civitai.com/user/Rabbit_YourMajesty)
|
||||
> ### Links
|
||||
> - [Linktree](https://www.juyung.com/)
|
||||
> - [LinkedIn](https://job.juyung.com/)
|
||||
> - [Instagram](https://photos.juyung.com/)
|
||||
> - [Twitter](https://social.juyung.com/)
|
||||
> - [GitHub](https://git.juyung.com/)
|
||||
|
|
|
|||
43
src/global.d.ts
vendored
43
src/global.d.ts
vendored
|
|
@ -1,41 +1,8 @@
|
|||
import type { AstroIntegration } from "@swup/astro";
|
||||
import type { AstroIntegration } from '@swup/astro'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
// type from '@swup/astro' is incorrect
|
||||
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[];
|
||||
interface Window {
|
||||
// type from '@swup/astro' is incorrect
|
||||
swup: AstroIntegration
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +1,37 @@
|
|||
enum I18nKey {
|
||||
home = "home",
|
||||
about = "about",
|
||||
archive = "archive",
|
||||
search = "search",
|
||||
home = 'home',
|
||||
about = 'about',
|
||||
archive = 'archive',
|
||||
search = 'search',
|
||||
|
||||
tags = "tags",
|
||||
categories = "categories",
|
||||
recentPosts = "recentPosts",
|
||||
tags = 'tags',
|
||||
categories = 'categories',
|
||||
recentPosts = 'recentPosts',
|
||||
|
||||
comments = "comments",
|
||||
comments = 'comments',
|
||||
|
||||
untitled = "untitled",
|
||||
uncategorized = "uncategorized",
|
||||
noTags = "noTags",
|
||||
untitled = 'untitled',
|
||||
uncategorized = 'uncategorized',
|
||||
noTags = 'noTags',
|
||||
|
||||
wordCount = "wordCount",
|
||||
wordsCount = "wordsCount",
|
||||
minuteCount = "minuteCount",
|
||||
minutesCount = "minutesCount",
|
||||
postCount = "postCount",
|
||||
postsCount = "postsCount",
|
||||
wordCount = 'wordCount',
|
||||
wordsCount = 'wordsCount',
|
||||
minuteCount = 'minuteCount',
|
||||
minutesCount = 'minutesCount',
|
||||
postCount = 'postCount',
|
||||
postsCount = 'postsCount',
|
||||
|
||||
themeColor = "themeColor",
|
||||
themeColor = 'themeColor',
|
||||
|
||||
lightMode = "lightMode",
|
||||
darkMode = "darkMode",
|
||||
systemMode = "systemMode",
|
||||
lightMode = 'lightMode',
|
||||
darkMode = 'darkMode',
|
||||
systemMode = 'systemMode',
|
||||
|
||||
more = "more",
|
||||
more = 'more',
|
||||
|
||||
author = "author",
|
||||
publishedAt = "publishedAt",
|
||||
license = "license",
|
||||
author = 'author',
|
||||
publishedAt = 'publishedAt',
|
||||
license = 'license',
|
||||
}
|
||||
|
||||
export default I18nKey;
|
||||
export default I18nKey
|
||||
|
|
|
|||
|
|
@ -1,38 +1,38 @@
|
|||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
import Key from '../i18nKey'
|
||||
import type { Translation } from '../translation'
|
||||
|
||||
export const en: Translation = {
|
||||
[Key.home]: "Home",
|
||||
[Key.about]: "About",
|
||||
[Key.archive]: "Archive",
|
||||
[Key.search]: "Search",
|
||||
[Key.home]: 'Home',
|
||||
[Key.about]: 'About',
|
||||
[Key.archive]: 'Archive',
|
||||
[Key.search]: 'Search',
|
||||
|
||||
[Key.tags]: "Tags",
|
||||
[Key.categories]: "Categories",
|
||||
[Key.recentPosts]: "Recent Posts",
|
||||
[Key.tags]: 'Tags',
|
||||
[Key.categories]: 'Categories',
|
||||
[Key.recentPosts]: 'Recent Posts',
|
||||
|
||||
[Key.comments]: "Comments",
|
||||
[Key.comments]: 'Comments',
|
||||
|
||||
[Key.untitled]: "Untitled",
|
||||
[Key.uncategorized]: "Uncategorized",
|
||||
[Key.noTags]: "No Tags",
|
||||
[Key.untitled]: 'Untitled',
|
||||
[Key.uncategorized]: 'Uncategorized',
|
||||
[Key.noTags]: 'No Tags',
|
||||
|
||||
[Key.wordCount]: "word",
|
||||
[Key.wordsCount]: "words",
|
||||
[Key.minuteCount]: "minute",
|
||||
[Key.minutesCount]: "minutes",
|
||||
[Key.postCount]: "post",
|
||||
[Key.postsCount]: "posts",
|
||||
[Key.wordCount]: 'word',
|
||||
[Key.wordsCount]: 'words',
|
||||
[Key.minuteCount]: 'minute',
|
||||
[Key.minutesCount]: 'minutes',
|
||||
[Key.postCount]: 'post',
|
||||
[Key.postsCount]: 'posts',
|
||||
|
||||
[Key.themeColor]: "Theme Color",
|
||||
[Key.themeColor]: 'Theme Color',
|
||||
|
||||
[Key.lightMode]: "Light",
|
||||
[Key.darkMode]: "Dark",
|
||||
[Key.systemMode]: "System",
|
||||
[Key.lightMode]: 'Light',
|
||||
[Key.darkMode]: 'Dark',
|
||||
[Key.systemMode]: 'System',
|
||||
|
||||
[Key.more]: "More",
|
||||
[Key.more]: 'More',
|
||||
|
||||
[Key.author]: "Author",
|
||||
[Key.publishedAt]: "Published at",
|
||||
[Key.license]: "License",
|
||||
};
|
||||
[Key.author]: 'Author',
|
||||
[Key.publishedAt]: 'Published at',
|
||||
[Key.license]: 'License',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,38 @@
|
|||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
import Key from '../i18nKey'
|
||||
import type { Translation } from '../translation'
|
||||
|
||||
export const es: Translation = {
|
||||
[Key.home]: "Inicio",
|
||||
[Key.about]: "Sobre mí",
|
||||
[Key.archive]: "Archivo",
|
||||
[Key.search]: "Buscar",
|
||||
[Key.home]: 'Inicio',
|
||||
[Key.about]: 'Sobre mí',
|
||||
[Key.archive]: 'Archivo',
|
||||
[Key.search]: 'Buscar',
|
||||
|
||||
[Key.tags]: "Etiquetas",
|
||||
[Key.categories]: "Categorías",
|
||||
[Key.recentPosts]: "Publicaciones recientes",
|
||||
[Key.tags]: 'Etiquetas',
|
||||
[Key.categories]: 'Categorías',
|
||||
[Key.recentPosts]: 'Publicaciones recientes',
|
||||
|
||||
[Key.comments]: "Comentarios",
|
||||
[Key.comments]: 'Comentarios',
|
||||
|
||||
[Key.untitled]: "Sin título",
|
||||
[Key.uncategorized]: "Sin categoría",
|
||||
[Key.noTags]: "Sin etiquetas",
|
||||
[Key.untitled]: 'Sin título',
|
||||
[Key.uncategorized]: 'Sin categoría',
|
||||
[Key.noTags]: 'Sin etiquetas',
|
||||
|
||||
[Key.wordCount]: "palabra",
|
||||
[Key.wordsCount]: "palabras",
|
||||
[Key.minuteCount]: "minuto",
|
||||
[Key.minutesCount]: "minutos",
|
||||
[Key.postCount]: "publicación",
|
||||
[Key.postsCount]: "publicaciones",
|
||||
[Key.wordCount]: 'palabra',
|
||||
[Key.wordsCount]: 'palabras',
|
||||
[Key.minuteCount]: 'minuto',
|
||||
[Key.minutesCount]: 'minutos',
|
||||
[Key.postCount]: 'publicación',
|
||||
[Key.postsCount]: 'publicaciones',
|
||||
|
||||
[Key.themeColor]: "Color del tema",
|
||||
[Key.themeColor]: 'Color del tema',
|
||||
|
||||
[Key.lightMode]: "Claro",
|
||||
[Key.darkMode]: "Oscuro",
|
||||
[Key.systemMode]: "Sistema",
|
||||
[Key.lightMode]: 'Claro',
|
||||
[Key.darkMode]: 'Oscuro',
|
||||
[Key.systemMode]: 'Sistema',
|
||||
|
||||
[Key.more]: "Más",
|
||||
[Key.more]: 'Más',
|
||||
|
||||
[Key.author]: "Autor",
|
||||
[Key.publishedAt]: "Publicado el",
|
||||
[Key.license]: "Licencia",
|
||||
};
|
||||
[Key.author]: 'Autor',
|
||||
[Key.publishedAt]: 'Publicado el',
|
||||
[Key.license]: 'Licencia',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
|
|
@ -1,38 +1,38 @@
|
|||
import Key from "../i18nKey";
|
||||
import type { Translation } from "../translation";
|
||||
import Key from '../i18nKey'
|
||||
import type { Translation } from '../translation'
|
||||
|
||||
export const ja: Translation = {
|
||||
[Key.home]: "Home",
|
||||
[Key.about]: "About",
|
||||
[Key.archive]: "Archive",
|
||||
[Key.search]: "検索",
|
||||
[Key.home]: 'Home',
|
||||
[Key.about]: 'About',
|
||||
[Key.archive]: 'Archive',
|
||||
[Key.search]: '検索',
|
||||
|
||||
[Key.tags]: "タグ",
|
||||
[Key.categories]: "カテゴリ",
|
||||
[Key.recentPosts]: "最近の投稿",
|
||||
[Key.tags]: 'タグ',
|
||||
[Key.categories]: 'カテゴリ',
|
||||
[Key.recentPosts]: '最近の投稿',
|
||||
|
||||
[Key.comments]: "コメント",
|
||||
[Key.comments]: 'コメント',
|
||||
|
||||
[Key.untitled]: "タイトルなし",
|
||||
[Key.uncategorized]: "カテゴリなし",
|
||||
[Key.noTags]: "タグなし",
|
||||
[Key.untitled]: 'タイトルなし',
|
||||
[Key.uncategorized]: 'カテゴリなし',
|
||||
[Key.noTags]: 'タグなし',
|
||||
|
||||
[Key.wordCount]: "文字",
|
||||
[Key.wordsCount]: "文字",
|
||||
[Key.minuteCount]: "分",
|
||||
[Key.minutesCount]: "分",
|
||||
[Key.postCount]: "件の投稿",
|
||||
[Key.postsCount]: "件の投稿",
|
||||
[Key.wordCount]: '文字',
|
||||
[Key.wordsCount]: '文字',
|
||||
[Key.minuteCount]: '分',
|
||||
[Key.minutesCount]: '分',
|
||||
[Key.postCount]: '件の投稿',
|
||||
[Key.postsCount]: '件の投稿',
|
||||
|
||||
[Key.themeColor]: "テーマカラー",
|
||||
[Key.themeColor]: 'テーマカラー',
|
||||
|
||||
[Key.lightMode]: "ライト",
|
||||
[Key.darkMode]: "ダーク",
|
||||
[Key.systemMode]: "システム",
|
||||
[Key.lightMode]: 'ライト',
|
||||
[Key.darkMode]: 'ダーク',
|
||||
[Key.systemMode]: 'システム',
|
||||
|
||||
[Key.more]: "もっと",
|
||||
[Key.more]: 'もっと',
|
||||
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "公開日",
|
||||
[Key.license]: "ライセンス",
|
||||
};
|
||||
[Key.author]: '作者',
|
||||
[Key.publishedAt]: '公開日',
|
||||
[Key.license]: 'ライセンス',
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue