Compare commits

...

10 Commits

Author SHA1 Message Date
6243898db3 feat: 添加 GitLab 仓库卡片组件支持;合并pr-518
Some checks failed
Code quality / quality (push) Has been cancelled
Build and Check / Astro Check for Node.js 22 (push) Has been cancelled
Build and Check / Astro Check for Node.js 23 (push) Has been cancelled
Build and Check / Astro Build for Node.js 22 (push) Has been cancelled
Build and Check / Astro Build for Node.js 23 (push) Has been cancelled
2026-03-22 14:27:33 +08:00
7ae6fb91a4 feat: 移除返回顶部按钮 2026-03-22 12:21:40 +08:00
cbf35beeb8 feat: 将目录(TOC)移至侧边栏并优化滚动高亮逻辑
- 重构TOC组件,移至SideBar中显示
- 改进IntersectionObserver实现,优化章节高亮行为
- 添加toc国际化翻译(中/英/繁)
- 更新Swup容器配置,支持文章页侧边栏刷新
- 移除原MainGridLayout中的独立TOC容器
2026-03-22 12:16:51 +08:00
b7ebf3bef7 feat: 添加文章置顶功能
- 添加 pinned 和 category_pinned 字段支持文章置顶
- archive 页面置顶文章独立于时间轴显示
- 主页卡片显示置顶图标
- 支持全局置顶和分类置顶两种模式
- 修复置顶文章在标签/未分类筛选时的过滤问题
2026-03-22 11:27:17 +08:00
370b421eed feat: 精简多语言支持并添加404页面 2026-03-21 15:39:09 +08:00
d97e6c66d6 feat: 迁移博客个性化配置 2026-03-21 15:16:45 +08:00
5de5876bde chore: 移除模板示例文章并添加AGENTS.md配置 2026-03-21 13:33:56 +08:00
dependabot[bot]
6d39b0dec4 chore(deps): bump the patch-updates group across 1 directory with 13 updates (#681)
Bumps the patch-updates group with 13 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@astrojs/check](https://github.com/withastro/astro/tree/HEAD/packages/language-tools/astro-check) | `0.9.4` | `0.9.6` |
| [@astrojs/rss](https://github.com/withastro/astro/tree/HEAD/packages/astro-rss) | `4.0.12` | `4.0.14` |
| [@astrojs/svelte](https://github.com/withastro/astro/tree/HEAD/packages/integrations/svelte) | `7.2.0` | `7.2.3` |
| [@expressive-code/core](https://github.com/expressive-code/expressive-code/tree/HEAD/packages/@expressive-code/core) | `0.41.3` | `0.41.4` |
| [@expressive-code/plugin-collapsible-sections](https://github.com/expressive-code/expressive-code/tree/HEAD/packages/@expressive-code/plugin-collapsible-sections) | `0.41.3` | `0.41.4` |
| [@expressive-code/plugin-line-numbers](https://github.com/expressive-code/expressive-code/tree/HEAD/packages/@expressive-code/plugin-line-numbers) | `0.41.3` | `0.41.4` |
| [@fontsource/roboto](https://github.com/fontsource/font-files/tree/HEAD/fonts/google/roboto) | `5.2.8` | `5.2.9` |
| [@iconify-json/material-symbols](https://github.com/iconify/icon-sets) | `1.2.40` | `1.2.50` |
| [astro-expressive-code](https://github.com/expressive-code/expressive-code/tree/HEAD/packages/astro-expressive-code) | `0.41.3` | `0.41.4` |
| [katex](https://github.com/KaTeX/KaTeX) | `0.16.23` | `0.16.27` |
| [sharp](https://github.com/lovell/sharp) | `0.34.4` | `0.34.5` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `3.4.18` | `3.4.19` |
| [@astrojs/ts-plugin](https://github.com/withastro/astro/tree/HEAD/packages/language-tools/ts-plugin) | `1.10.4` | `1.10.6` |



Updates `@astrojs/check` from 0.9.4 to 0.9.6
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/language-tools/astro-check/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/check@0.9.6/packages/language-tools/astro-check)

Updates `@astrojs/rss` from 4.0.12 to 4.0.14
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/astro-rss/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/rss@4.0.14/packages/astro-rss)

Updates `@astrojs/svelte` from 7.2.0 to 7.2.3
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/integrations/svelte/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/svelte@7.2.3/packages/integrations/svelte)

Updates `@expressive-code/core` from 0.41.3 to 0.41.4
- [Release notes](https://github.com/expressive-code/expressive-code/releases)
- [Changelog](https://github.com/expressive-code/expressive-code/blob/main/packages/@expressive-code/core/CHANGELOG.md)
- [Commits](https://github.com/expressive-code/expressive-code/commits/@expressive-code/core@0.41.4/packages/@expressive-code/core)

Updates `@expressive-code/plugin-collapsible-sections` from 0.41.3 to 0.41.4
- [Release notes](https://github.com/expressive-code/expressive-code/releases)
- [Changelog](https://github.com/expressive-code/expressive-code/blob/main/packages/@expressive-code/plugin-collapsible-sections/CHANGELOG.md)
- [Commits](https://github.com/expressive-code/expressive-code/commits/@expressive-code/plugin-collapsible-sections@0.41.4/packages/@expressive-code/plugin-collapsible-sections)

Updates `@expressive-code/plugin-line-numbers` from 0.41.3 to 0.41.4
- [Release notes](https://github.com/expressive-code/expressive-code/releases)
- [Changelog](https://github.com/expressive-code/expressive-code/blob/main/packages/@expressive-code/plugin-line-numbers/CHANGELOG.md)
- [Commits](https://github.com/expressive-code/expressive-code/commits/@expressive-code/plugin-line-numbers@0.41.4/packages/@expressive-code/plugin-line-numbers)

Updates `@fontsource/roboto` from 5.2.8 to 5.2.9
- [Changelog](https://github.com/fontsource/font-files/blob/main/CHANGELOG.md)
- [Commits](https://github.com/fontsource/font-files/commits/HEAD/fonts/google/roboto)

Updates `@iconify-json/material-symbols` from 1.2.40 to 1.2.50
- [Commits](https://github.com/iconify/icon-sets/commits)

Updates `astro-expressive-code` from 0.41.3 to 0.41.4
- [Release notes](https://github.com/expressive-code/expressive-code/releases)
- [Changelog](https://github.com/expressive-code/expressive-code/blob/main/packages/astro-expressive-code/CHANGELOG.md)
- [Commits](https://github.com/expressive-code/expressive-code/commits/astro-expressive-code@0.41.4/packages/astro-expressive-code)

Updates `katex` from 0.16.23 to 0.16.27
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.16.23...v0.16.27)

Updates `sharp` from 0.34.4 to 0.34.5
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.34.4...v0.34.5)

Updates `tailwindcss` from 3.4.18 to 3.4.19
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v3.4.19/packages/tailwindcss)

Updates `@astrojs/ts-plugin` from 1.10.4 to 1.10.6
- [Release notes](https://github.com/withastro/astro/releases)
- [Changelog](https://github.com/withastro/astro/blob/main/packages/language-tools/ts-plugin/CHANGELOG.md)
- [Commits](https://github.com/withastro/astro/commits/@astrojs/ts-plugin@1.10.6/packages/language-tools/ts-plugin)

---
updated-dependencies:
- dependency-name: "@astrojs/check"
  dependency-version: 0.9.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: "@astrojs/rss"
  dependency-version: 4.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: "@astrojs/svelte"
  dependency-version: 7.2.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: "@expressive-code/core"
  dependency-version: 0.41.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: "@expressive-code/plugin-collapsible-sections"
  dependency-version: 0.41.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: "@expressive-code/plugin-line-numbers"
  dependency-version: 0.41.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: "@fontsource/roboto"
  dependency-version: 5.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: "@iconify-json/material-symbols"
  dependency-version: 1.2.50
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: astro-expressive-code
  dependency-version: 0.41.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: katex
  dependency-version: 0.16.27
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: sharp
  dependency-version: 0.34.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: tailwindcss
  dependency-version: 3.4.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: "@astrojs/ts-plugin"
  dependency-version: 1.10.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-12 08:51:08 +09:00
noeFly
1c53b77312 feat: 新增支援第三方 GitLab 自架服務 2025-11-30 10:11:30 +08:00
noeFly
79332344a8 docs: 自述檔案新增 GitLab 專案卡片描述 2025-11-30 10:10:02 +08:00
44 changed files with 1812 additions and 1865 deletions

38
.vscode/settings.json vendored
View File

@@ -1,22 +1,20 @@
{
"editor.formatOnSave": true,
"editor.defaultFormatter": "biomejs.biome",
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"quickfix.biome": "always",
"source.organizeImports.biome": "always"
},
"frontMatter.dashboard.openOnStart": false
"editor.formatOnSave": false,
"editor.defaultFormatter": "biomejs.biome",
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit"
},
"frontMatter.dashboard.openOnStart": false
}

21
AGENTS.md Normal file
View File

@@ -0,0 +1,21 @@
# Project Information for AI Agents
## Build Commands
- `pnpm dev` - Start development server
- `pnpm build` - Build for production (includes pagefind search index)
- `pnpm preview` - Preview production build
## Code Quality
- `pnpm lint` - Run linter (biome)
- `pnpm format` - Format code (biome)
- `pnpm type-check` - Type check TypeScript
## Project Stack
- Framework: Astro v5
- Styling: Tailwind CSS
- Package Manager: pnpm
- Linting/Formatting: Biome
## Notes
- This is a Fuwari blog template built with Astro
- Search functionality powered by Pagefind

View File

@@ -69,24 +69,24 @@ lang: jp # Set only if the post's language differs from the site's language
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))
- GitHub and GitLab repository cards ([Preview and Usage](https://fuwari.vercel.app/posts/markdown-extended/#github-repository-cards))
- Enhanced code blocks with Expressive Code ([Preview](https://fuwari.vercel.app/posts/expressive-code/) / [Docs](https://expressive-code.com/))
## ⚡ Commands
All commands are run from the root of the project, from a terminal:
| 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 |
| 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

View File

@@ -27,7 +27,7 @@ import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-cop
// https://astro.build/config
export default defineConfig({
site: "https://fuwari.vercel.app/",
site: "https://milkfunc.top/",
base: "/",
trailingSlash: "always",
integrations: [

View File

@@ -16,28 +16,28 @@
"preinstall": "npx only-allow pnpm"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/rss": "^4.0.12",
"@astrojs/check": "^0.9.6",
"@astrojs/rss": "^4.0.14",
"@astrojs/sitemap": "^3.6.0",
"@astrojs/svelte": "7.2.0",
"@astrojs/svelte": "7.2.3",
"@astrojs/tailwind": "^6.0.2",
"@expressive-code/core": "^0.41.3",
"@expressive-code/plugin-collapsible-sections": "^0.41.3",
"@expressive-code/plugin-line-numbers": "^0.41.3",
"@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.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.40",
"@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.3",
"astro-expressive-code": "^0.41.4",
"astro-icon": "^1.1.5",
"hastscript": "^9.0.1",
"katex": "^0.16.23",
"katex": "^0.16.27",
"markdown-it": "^14.1.0",
"mdast-util-to-string": "^4.0.0",
"overlayscrollbars": "^2.12.0",
@@ -54,15 +54,15 @@
"remark-math": "^6.0.0",
"remark-sectionize": "^2.1.0",
"sanitize-html": "^2.17.0",
"sharp": "^0.34.4",
"sharp": "^0.34.5",
"stylus": "^0.64.0",
"svelte": "^5.39.8",
"tailwindcss": "^3.4.18",
"tailwindcss": "^3.4.19",
"typescript": "^5.9.3",
"unist-util-visit": "^5.0.0"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.10.4",
"@astrojs/ts-plugin": "^1.10.6",
"@biomejs/biome": "2.2.5",
"@rollup/plugin-yaml": "^4.1.2",
"@types/hast": "^3.0.4",

1606
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 KiB

View File

@@ -3,33 +3,29 @@ import { onMount } from "svelte";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import type { PostForList } from "../utils/content-utils";
import { getPostUrlBySlug } from "../utils/url-utils";
export let tags: string[];
export let categories: string[];
export let sortedPosts: Post[] = [];
export let sortedPosts: PostForList[] = [];
export let pinnedPosts: PostForList[] = [];
export let categoryPinnedPosts: PostForList[] = [];
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;
};
}
const isCategoryMode = categories.length > 0;
interface Group {
year: number;
posts: Post[];
posts: PostForList[];
}
let groups: Group[] = [];
let displayPinnedPosts: PostForList[] = [];
function formatDate(date: Date) {
const month = (date.getMonth() + 1).toString().padStart(2, "0");
@@ -42,7 +38,7 @@ function formatTag(tagList: string[]) {
}
onMount(async () => {
let filteredPosts: Post[] = sortedPosts;
let filteredPosts: PostForList[] = sortedPosts;
if (tags.length > 0) {
filteredPosts = filteredPosts.filter(
@@ -62,6 +58,39 @@ onMount(async () => {
filteredPosts = filteredPosts.filter((post) => !post.data.category);
}
if (isCategoryMode) {
displayPinnedPosts = categoryPinnedPosts.filter(
(post) => post.data.category && categories.includes(post.data.category),
);
if (tags.length > 0) {
displayPinnedPosts = displayPinnedPosts.filter(
(post) =>
Array.isArray(post.data.tags) &&
post.data.tags.some((tag) => tags.includes(tag)),
);
}
filteredPosts = filteredPosts.filter(
(post) => (post.data.category_pinned || 0) === 0,
);
} else {
displayPinnedPosts = pinnedPosts;
if (tags.length > 0) {
displayPinnedPosts = displayPinnedPosts.filter(
(post) =>
Array.isArray(post.data.tags) &&
post.data.tags.some((tag) => tags.includes(tag)),
);
}
if (uncategorized) {
displayPinnedPosts = displayPinnedPosts.filter(
(post) => !post.data.category,
);
}
filteredPosts = filteredPosts.filter(
(post) => (post.data.pinned || 0) === 0,
);
}
const grouped = filteredPosts.reduce(
(acc, post) => {
const year = post.data.published.getFullYear();
@@ -71,7 +100,7 @@ onMount(async () => {
acc[year].push(post);
return acc;
},
{} as Record<number, Post[]>,
{} as Record<number, PostForList[]>,
);
const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({
@@ -86,66 +115,121 @@ onMount(async () => {
</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>
{#if displayPinnedPosts.length > 0}
<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">
{i18n(I18nKey.pinned)}
</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">
{displayPinnedPosts.length} {i18n(displayPinnedPosts.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>
{#each displayPinnedPosts 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">
<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>
<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>
<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>
<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>
{/if}
{#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">
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
{formatDate(post.data.published)}
</div>
<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>
<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>
<div
class="hidden md:block md:w-[15%] text-left text-sm transition
whitespace-nowrap overflow-ellipsis overflow-hidden text-30"
>
{formatTag(post.data.tags)}
</div>
</div>
</a>
{/each}
</div>
{/each}
</div>

View File

@@ -21,6 +21,7 @@ interface Props {
description: string;
draft: boolean;
style: string;
pinned?: number;
}
const {
entry,
@@ -33,6 +34,7 @@ const {
image,
description,
style,
pinned = 0,
} = Astro.props;
const className = Astro.props.class;
@@ -52,6 +54,7 @@ const { remarkPluginFrontmatter } = await entry.render();
before:absolute before:top-[35px] before:left-[18px] before:hidden md:before:block
">
{title}
{pinned > 0 && <Icon name="material-symbols:push-pin" class="inline text-[1.5rem] text-[var(--primary)] ml-2 translate-y-[-2px]" />}
<Icon class="inline text-[2rem] text-[var(--primary)] md:hidden translate-y-0.5 absolute" name="material-symbols:chevron-right-rounded" ></Icon>
<Icon class="text-[var(--primary)] text-[2rem] transition hidden md:inline absolute translate-y-0.5 opacity-0 group-hover:opacity-100 -translate-x-1 group-hover:translate-x-0" name="material-symbols:chevron-right-rounded"></Icon>
</a>

View File

@@ -21,6 +21,7 @@ const interval = 50;
image={entry.data.image}
description={entry.data.description}
draft={entry.data.draft}
pinned={entry.data.pinned}
class:list="onload-animation"
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
></PostCard>

View File

@@ -1,22 +1,38 @@
---
import type { MarkdownHeading } from "astro";
import { siteConfig } from "@/config";
import Categories from "./Categories.astro";
import Profile from "./Profile.astro";
import Tag from "./Tags.astro";
import TOC from "./TOC.astro";
interface Props {
class?: string;
headings?: MarkdownHeading[];
}
const className = Astro.props.class;
const { class: className, headings } = Astro.props;
const currentPagePath = Astro.url.pathname;
const isPostPage = currentPagePath.startsWith("/posts/");
---
<div id="sidebar" class:list={[className, "w-full"]}>
<div class="flex flex-col w-full gap-4 mb-4">
<Profile></Profile>
</div>
{
!isPostPage && (
<div class="flex flex-col w-full gap-4 mb-4">
<Profile />
</div>
)
}
<div id="sidebar-sticky" class="transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4">
<Categories class="onload-animation" style="animation-delay: 150ms"></Categories>
<Tag class="onload-animation" style="animation-delay: 200ms"></Tag>
{
siteConfig.toc.enable && headings && headings.length > 0 && (
<div id="toc-wrapper" class="onload-animation card-base" style="animation-delay: 250ms">
<TOC headings={headings} />
</div>
)
}
<Categories class="onload-animation" style="animation-delay: 150ms" />
<Tag class="onload-animation" style="animation-delay: 200ms" />
</div>
</div>

View File

@@ -1,7 +1,10 @@
---
import type { MarkdownHeading } from "astro";
import { siteConfig } from "../../config";
import I18nKey from "../../i18n/i18nKey";
import { i18n } from "../../i18n/translation";
import { url } from "../../utils/url-utils";
import WidgetLayout from "./WidgetLayout.astro";
interface Props {
class?: string;
@@ -31,238 +34,249 @@ 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
hover:bg-[var(--toc-btn-hover)] active:bg-[var(--toc-btn-active)] py-2
">
<div class:list={["transition w-5 h-5 shrink-0 rounded-lg text-xs flex items-center justify-center font-bold",
{
"bg-[var(--toc-badge-bg)] text-[var(--btn-content)]": heading.depth == minDepth,
"ml-4": heading.depth == minDepth + 1,
"ml-8": heading.depth == minDepth + 2,
}
]}
<table-of-contents>
<WidgetLayout name={i18n(I18nKey.toc)} id="toc" class={className}>
<div class="flex flex-col pb-4">
<div
id="toc-wrapper"
class="onload-animation card-base"
style="animation-delay: 250ms"
>
<div
id="toc-inner-wrapper"
class="overflow-y-auto max-h-[50vh] hide-scrollbar pr-2"
>
{heading.depth == minDepth && heading1Count++}
{heading.depth == minDepth + 1 && <div class="transition w-2 h-2 rounded-[0.1875rem] bg-[var(--toc-badge-bg)]"></div>}
{heading.depth == minDepth + 2 && <div class="transition w-1.5 h-1.5 rounded-sm bg-black/5 dark:bg-white/10"></div>}
{
headings
.filter(
(heading) =>
heading.depth < minDepth + maxLevel,
)
.map((heading) => (
<a
href={`#${heading.slug}`}
class="px-2 flex gap-2 relative transition-colors duration-200 w-full min-h-9 rounded-xl hover:bg-[var(--toc-btn-hover)] active:bg-[var(--toc-btn-active)] py-2"
>
<div
class:list={[
"transition w-5 h-5 shrink-0 rounded-lg text-xs flex items-center justify-center font-bold",
{
"bg-[var(--toc-badge-bg)] text-[var(--btn-content)]":
heading.depth == minDepth,
"ml-4":
heading.depth ==
minDepth + 1,
"ml-8":
heading.depth ==
minDepth + 2,
},
]}
>
{heading.depth == minDepth &&
heading1Count++}
{heading.depth == minDepth + 1 && (
<div class="transition w-2 h-2 rounded-[0.1875rem] bg-[var(--toc-badge-bg)]" />
)}
{heading.depth == minDepth + 2 && (
<div class="transition w-1.5 h-1.5 rounded-sm bg-black/5 dark:bg-white/10" />
)}
</div>
<div
class:list={[
"transition text-sm",
{
"text-90":
heading.depth == minDepth ||
heading.depth ==
minDepth + 1,
"text-75":
heading.depth ==
minDepth + 2,
},
]}
>
{removeTailingHash(heading.text)}
</div>
</a>
))
}
</div>
<div class:list={["transition text-sm", {
"text-50": heading.depth == minDepth || heading.depth == minDepth + 1,
"text-30": heading.depth == minDepth + 2,
}]}>{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 " +
"group-hover:bg-transparent border-2 border-[var(--toc-btn-hover)] group-hover:border-[var(--toc-btn-active)] border-dashed"]}></div>
</table-of-contents>}
</div>
</div>
</WidgetLayout>
</table-of-contents>
<style>
table-of-contents a.visible {
background-color: var(--toc-btn-active) !important;
}
</style>
<script>
class TableOfContents extends HTMLElement {
tocEl: HTMLElement | null = null;
visibleClass = "visible";
observer: IntersectionObserver;
anchorNavTarget: HTMLElement | null = null;
headingIdxMap = new Map<string, number>();
headings: HTMLElement[] = [];
sections: HTMLElement[] = [];
tocEntries: HTMLAnchorElement[] = [];
active: boolean[] = [];
activeIndicator: HTMLElement | null = null;
class TableOfContents extends HTMLElement {
tocEl: HTMLElement | null = null;
visibleClass = "visible";
observer: IntersectionObserver;
anchorNavTarget: HTMLElement | null = null;
headingIdxMap = new Map<string, number>();
sections: HTMLElement[] = [];
tocEntries: HTMLAnchorElement[] = [];
// active 数组不再需要,我们将直接追踪 activeIndex
activeIndex: number | null = null;
constructor() {
super();
this.observer = new IntersectionObserver(
this.markVisibleSection, { threshold: 0 }
);
};
markVisibleSection = (entries: IntersectionObserverEntry[]) => {
entries.forEach((entry) => {
const id = entry.target.children[0]?.getAttribute("id");
const idx = id ? this.headingIdxMap.get(id) : undefined;
if (idx != undefined)
this.active[idx] = entry.isIntersecting;
if (entry.isIntersecting && this.anchorNavTarget == entry.target.firstChild)
this.anchorNavTarget = null;
});
if (!this.active.includes(true))
this.fallback();
this.update();
};
toggleActiveHeading = () => {
let i = this.active.length - 1;
let min = this.active.length - 1, max = -1;
while (i >= 0 && !this.active[i]) {
this.tocEntries[i].classList.remove(this.visibleClass);
i--;
constructor() {
super();
// 调整 IntersectionObserver 的 rootMargin使其在章节顶部进入视口时就触发
this.observer = new IntersectionObserver(
this.handleIntersection,
{ rootMargin: "0px 0px -80% 0px" }, // 关键调整
);
}
while (i >= 0 && this.active[i]) {
this.tocEntries[i].classList.add(this.visibleClass);
min = Math.min(min, i);
max = Math.max(max, i);
i--;
}
while (i >= 0) {
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`);
}
};
scrollToActiveHeading = () => {
// If the TOC widget can accommodate both the topmost
// and bottommost items, scroll to the topmost item.
// Otherwise, scroll to the bottommost one.
// --- 核心修改点 1: 新的 IntersectionObserver 回调 ---
handleIntersection = (entries: IntersectionObserverEntry[]) => {
// 当用户点击链接导航时,忽略 IntersectionObserver 的结果,防止冲突
if (this.anchorNavTarget) return;
if (this.anchorNavTarget || !this.tocEl) return;
const activeHeading =
document.querySelectorAll<HTMLDivElement>(`#toc .${this.visibleClass}`);
if (!activeHeading.length) return;
let latestVisibleEntry: IntersectionObserverEntry | undefined;
const topmost = activeHeading[0];
const bottommost = activeHeading[activeHeading.length - 1];
const tocHeight = this.tocEl.clientHeight;
let top;
if (bottommost.getBoundingClientRect().bottom -
topmost.getBoundingClientRect().top < 0.9 * tocHeight)
top = topmost.offsetTop - 32;
else
top = bottommost.offsetTop - tocHeight * 0.8;
this.tocEl.scrollTo({
top,
left: 0,
behavior: "smooth",
});
};
update = () => {
requestAnimationFrame(() => {
this.toggleActiveHeading();
// requestAnimationFrame(() => {
this.scrollToActiveHeading();
// });
});
};
fallback = () => {
if (!this.sections.length) return;
for (let i = 0; i < this.sections.length; i++) {
let offsetTop = this.sections[i].getBoundingClientRect().top;
let offsetBottom = this.sections[i].getBoundingClientRect().bottom;
if (this.isInRange(offsetTop, 0, window.innerHeight)
|| this.isInRange(offsetBottom, 0, window.innerHeight)
|| (offsetTop < 0 && offsetBottom > window.innerHeight)) {
this.markActiveHeading(i);
// 找到进入视口的、最新的(在文档中位置最靠后)的条目
for (const entry of entries) {
if (entry.isIntersecting) {
latestVisibleEntry = entry;
}
}
else if (offsetTop > window.innerHeight) break;
}
};
markActiveHeading = (idx: number)=> {
this.active[idx] = true;
};
if (latestVisibleEntry) {
const id =
latestVisibleEntry.target.children[0]?.getAttribute("id");
const idx = id ? this.headingIdxMap.get(id) : undefined;
if (idx !== undefined) {
this.setActiveIndex(idx);
}
}
};
handleAnchorClick = (event: Event) => {
const anchor = event
.composedPath()
.find((element) => element instanceof HTMLAnchorElement);
// --- 核心修改点 2: 统一设置活动索引并更新UI ---
setActiveIndex = (index: number | null) => {
// 如果索引没有变化,则不执行任何操作
if (this.activeIndex === index) return;
this.activeIndex = index;
// 更新UI
this.tocEntries.forEach((entry, i) => {
if (i === this.activeIndex) {
entry.classList.add(this.visibleClass);
} else {
entry.classList.remove(this.visibleClass);
}
});
// 只有在非用户点击导航时才自动滚动
if (!this.anchorNavTarget) {
this.scrollToActiveHeading();
}
};
scrollToActiveHeading = () => {
if (this.activeIndex === null || !this.tocEl) return;
const activeEntry = this.tocEntries[this.activeIndex];
if (!activeEntry) return;
const tocHeight = this.tocEl.clientHeight;
const entryTop = activeEntry.offsetTop;
const entryHeight = activeEntry.offsetHeight;
// 检查活动项是否在可视区域内
if (
entryTop < this.tocEl.scrollTop ||
entryTop + entryHeight > this.tocEl.scrollTop + tocHeight
) {
this.tocEl.scrollTo({
top: entryTop - tocHeight / 2 + entryHeight / 2, // 尝试滚动到中间位置
left: 0,
behavior: "smooth",
});
}
};
// --- 核心修改点 3: 处理点击事件 ---
handleAnchorClick = (event: Event) => {
// 找到被点击的 <a> 标签
const anchor = event.currentTarget as HTMLAnchorElement;
if (anchor) {
const id = decodeURIComponent(anchor.hash?.substring(1));
const idx = this.headingIdxMap.get(id);
if (idx !== undefined) {
this.anchorNavTarget = this.headings[idx];
} else {
this.anchorNavTarget = null;
// 标记正在进行锚点导航,以暂时禁用 IntersectionObserver 的更新
const targetHeading = document.getElementById(id);
if (targetHeading) {
this.anchorNavTarget = targetHeading;
// 在导航完成后重置标志
setTimeout(() => {
this.anchorNavTarget = null;
}, 1000); // 1秒后自动清除以防万一
}
// 立即更新高亮
this.setActiveIndex(idx);
}
}
};
};
isInRange(value: number, min: number, max: number) {
return min < value && value < max;
};
connectedCallback() {
// wait for the onload animation to finish, which makes the `getBoundingClientRect` return correct values
const element = document.querySelector('.prose');
if (element) {
element.addEventListener('animationend', () => {
connectedCallback() {
requestAnimationFrame(() => {
this.init();
}, { once: true });
} else {
console.debug('Animation element not found');
});
}
};
init() {
this.tocEl = document.getElementById(
"toc-inner-wrapper"
);
init() {
this.tocEl = document.getElementById("toc-inner-wrapper") || this;
if (!this.tocEl) return;
this.tocEntries = Array.from(
this.querySelectorAll<HTMLAnchorElement>("a[href^='#']"),
);
this.tocEl.addEventListener("click", this.handleAnchorClick, {
capture: true,
});
if (this.tocEntries.length === 0) return;
this.activeIndicator = document.getElementById("active-indicator");
this.sections = new Array(this.tocEntries.length);
for (let i = 0; i < this.tocEntries.length; i++) {
const entry = this.tocEntries[i];
const id = decodeURIComponent(entry.hash?.substring(1));
const heading = document.getElementById(id);
const section = heading?.parentElement;
this.tocEntries = Array.from(
document.querySelectorAll<HTMLAnchorElement>("#toc a[href^='#']")
);
if (this.tocEntries.length === 0) return;
this.sections = new Array(this.tocEntries.length);
this.headings = new Array(this.tocEntries.length);
for (let i = 0; i < this.tocEntries.length; i++) {
const id = decodeURIComponent(this.tocEntries[i].hash?.substring(1));
const heading = document.getElementById(id);
const section = heading?.parentElement;
if (heading instanceof HTMLElement && section instanceof HTMLElement) {
this.headings[i] = heading;
this.sections[i] = section;
this.headingIdxMap.set(id, i);
if (
heading instanceof HTMLElement &&
section instanceof HTMLElement
) {
this.sections[i] = section;
this.headingIdxMap.set(id, i);
// 为每个链接单独添加点击事件监听器
entry.addEventListener("click", this.handleAnchorClick);
}
}
this.sections.forEach((section) => this.observer.observe(section));
// 页面加载时,设置第一个标题为活动状态
this.setActiveIndex(0);
}
this.active = new Array(this.tocEntries.length).fill(false);
this.sections.forEach((section) =>
this.observer.observe(section)
);
disconnectedCallback() {
this.sections.forEach((section) =>
this.observer.unobserve(section),
);
this.observer.disconnect();
this.tocEntries.forEach((entry) => {
entry.removeEventListener("click", this.handleAnchorClick);
});
}
}
this.fallback();
this.update();
};
disconnectedCallback() {
this.sections.forEach((section) =>
this.observer.unobserve(section)
);
this.observer.disconnect();
this.tocEl?.removeEventListener("click", this.handleAnchorClick);
};
}
if (!customElements.get("table-of-contents")) {
customElements.define("table-of-contents", TableOfContents);
}
</script>
if (!customElements.get("table-of-contents")) {
customElements.define("table-of-contents", TableOfContents);
}
</script>

View File

@@ -8,16 +8,16 @@ import type {
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.
title: "MilkFunc",
subtitle: "Blogs",
lang: "zh_CN", // 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
fixed: true, // 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 '/'
enable: true,
src: "assets/images/banner.webp", // 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
@@ -40,40 +40,31 @@ export const siteConfig: SiteConfig = {
};
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],
};
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.",
avatar: "assets/images/avatar.webp", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
name: "CapaCake",
bio: "😨为什么我的电容炸了?",
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",
},
// {
// 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",
// },
],
};

View File

@@ -11,6 +11,8 @@ const postsCollection = defineCollection({
tags: z.array(z.string()).optional().default([]),
category: z.string().optional().nullable().default(""),
lang: z.string().optional().default(""),
pinned: z.number().optional().default(0),
category_pinned: z.number().optional().default(0),
/* For internal use */
prevTitle: z.string().default(""),

1
src/content/posts Symbolic link
View File

@@ -0,0 +1 @@
../../../blog-post/published

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 218 KiB

View File

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

View File

@@ -1,107 +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"}
```
## GitLab Repository Cards
You can also add dynamic cards that show a GitLab repository, on page load, the repository information is pulled from the GitLab API.
::gitlab{repo="gitlab-org/gitlab"}
Create a GitHub repository card with the code `::gitlab{repo="<owner>/<repo>"}`.
```mdx
::gitlab{repo="gitlab-org/gitlab"}
```
## Admonitions
Following types of admonitions are supported: `note` `tip` `important` `warning` `caution`
:::note
Highlights information that users should take into account, even when skimming.
:::
:::tip
Optional information to help a user be more successful.
:::
:::important
Crucial information necessary for users to succeed.
:::
:::warning
Critical content demanding immediate user attention due to potential risks.
:::
:::caution
Negative potential consequences of an action.
:::
### Basic Syntax
```markdown
:::note
Highlights information that users should take into account, even when skimming.
:::
:::tip
Optional information to help a user be more successful.
:::
```
### Custom Titles
The title of the admonition can be customized.
:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::
```markdown
:::note[MY CUSTOM TITLE]
This is a note with a custom title.
:::
```
### GitHub Syntax
> [!TIP]
> [The GitHub syntax](https://github.com/orgs/community/discussions/16925) is also supported.
```
> [!NOTE]
> The GitHub syntax is also supported.
> [!TIP]
> The GitHub syntax is also supported.
```
### Spoiler
You can add spoilers to your text. The text also supports **Markdown** syntax.
The content :spoiler[is hidden **ayyy**]!
```markdown
The content :spoiler[is hidden **ayyy**]!
```

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ This is the demo site for [Fuwari](https://github.com/saicaca/fuwari).
::github{repo="saicaca/fuwari"}
::gitlab{repo="CapaCake/milkblogs-fuwari" service="https://git.milkfunc.top"}
> ### 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)

View File

@@ -32,6 +32,12 @@ enum I18nKey {
author = "author",
publishedAt = "publishedAt",
license = "license",
notFound = "notFound",
notFoundDesc = "notFoundDesc",
backToHome = "backToHome",
pinned = "pinned",
toc = "toc",
}
export default I18nKey;

View File

@@ -35,4 +35,10 @@ export const en: Translation = {
[Key.author]: "Author",
[Key.publishedAt]: "Published at",
[Key.license]: "License",
[Key.notFound]: "Page Not Found",
[Key.notFoundDesc]: "The link is broken, the page has gone missing",
[Key.backToHome]: "Back to Home",
[Key.pinned]: "Pinned",
[Key.toc]: "Toc",
};

View File

@@ -1,38 +0,0 @@
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.tags]: "Etiquetas",
[Key.categories]: "Categorías",
[Key.recentPosts]: "Publicaciones recientes",
[Key.comments]: "Comentarios",
[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.themeColor]: "Color del tema",
[Key.lightMode]: "Claro",
[Key.darkMode]: "Oscuro",
[Key.systemMode]: "Sistema",
[Key.more]: "Más",
[Key.author]: "Autor",
[Key.publishedAt]: "Publicado el",
[Key.license]: "Licencia",
};

View File

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

View File

@@ -1,38 +0,0 @@
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.tags]: "タグ",
[Key.categories]: "カテゴリ",
[Key.recentPosts]: "最近の投稿",
[Key.comments]: "コメント",
[Key.untitled]: "タイトルなし",
[Key.uncategorized]: "カテゴリなし",
[Key.noTags]: "タグなし",
[Key.wordCount]: "文字",
[Key.wordsCount]: "文字",
[Key.minuteCount]: "分",
[Key.minutesCount]: "分",
[Key.postCount]: "件の投稿",
[Key.postsCount]: "件の投稿",
[Key.themeColor]: "テーマカラー",
[Key.lightMode]: "ライト",
[Key.darkMode]: "ダーク",
[Key.systemMode]: "システム",
[Key.more]: "もっと",
[Key.author]: "作者",
[Key.publishedAt]: "公開日",
[Key.license]: "ライセンス",
};

View File

@@ -1,38 +0,0 @@
import Key from "../i18nKey";
import type { Translation } from "../translation";
export const ko: Translation = {
[Key.home]: "홈",
[Key.about]: "소개",
[Key.archive]: "아카이브",
[Key.search]: "검색",
[Key.tags]: "태그",
[Key.categories]: "카테고리",
[Key.recentPosts]: "최근 게시물",
[Key.comments]: "댓글",
[Key.untitled]: "제목 없음",
[Key.uncategorized]: "분류되지 않음",
[Key.noTags]: "태그 없음",
[Key.wordCount]: "단어",
[Key.wordsCount]: "단어",
[Key.minuteCount]: "분",
[Key.minutesCount]: "분",
[Key.postCount]: "게시물",
[Key.postsCount]: "게시물",
[Key.themeColor]: "테마 색상",
[Key.lightMode]: "밝은 모드",
[Key.darkMode]: "어두운 모드",
[Key.systemMode]: "시스템 모드",
[Key.more]: "더 보기",
[Key.author]: "저자",
[Key.publishedAt]: "게시일",
[Key.license]: "라이선스",
};

View File

@@ -1,38 +0,0 @@
import Key from "../i18nKey";
import type { Translation } from "../translation";
export const th: Translation = {
[Key.home]: "หน้าแรก",
[Key.about]: "เกี่ยวกับ",
[Key.archive]: "คลัง",
[Key.search]: "ค้นหา",
[Key.tags]: "ป้ายกำกับ",
[Key.categories]: "หมวดหมู่",
[Key.recentPosts]: "โพสต์ล่าสุด",
[Key.comments]: "ความคิดเห็น",
[Key.untitled]: "ไม่ได้ตั้งชื่อ",
[Key.uncategorized]: "ไม่ได้จัดหมวดหมู่",
[Key.noTags]: "ไม่มีป้ายกำกับ",
[Key.wordCount]: "คำ",
[Key.wordsCount]: "คำ",
[Key.minuteCount]: "นาที",
[Key.minutesCount]: "นาที",
[Key.postCount]: "โพสต์",
[Key.postsCount]: "โพสต์",
[Key.themeColor]: "สีของธีม",
[Key.lightMode]: "สว่าง",
[Key.darkMode]: "มืด",
[Key.systemMode]: "ตามระบบ",
[Key.more]: "ดูเพิ่ม",
[Key.author]: "ผู้เขียน",
[Key.publishedAt]: "เผยแพร่เมื่อ",
[Key.license]: "สัญญาอนุญาต",
};

View File

@@ -1,38 +0,0 @@
import Key from "../i18nKey";
import type { Translation } from "../translation";
export const tr: Translation = {
[Key.home]: "Anasayfa",
[Key.about]: "Hakkında",
[Key.archive]: "Arşiv",
[Key.search]: "Ara",
[Key.tags]: "Taglar",
[Key.categories]: "Katagoriler",
[Key.recentPosts]: "Son Paylaşımlar",
[Key.comments]: "Yorumlar",
[Key.untitled]: "Başlıksız",
[Key.uncategorized]: "Katagorisiz",
[Key.noTags]: "Tag Bulunamadı",
[Key.wordCount]: "kelime",
[Key.wordsCount]: "kelime",
[Key.minuteCount]: "dakika",
[Key.minutesCount]: "dakika",
[Key.postCount]: "gönderi",
[Key.postsCount]: "gönderiler",
[Key.themeColor]: "Tema Rengi",
[Key.lightMode]: "Aydınlık",
[Key.darkMode]: "Koyu",
[Key.systemMode]: "Sistem",
[Key.more]: "Daha Fazla",
[Key.author]: "Yazar",
[Key.publishedAt]: "Yayınlanma:",
[Key.license]: "Lisans",
};

View File

@@ -1,38 +0,0 @@
import Key from "../i18nKey";
import type { Translation } from "../translation";
export const vi: Translation = {
[Key.home]: "Trang chủ",
[Key.about]: "Giới thiệu",
[Key.archive]: "Kho bài",
[Key.search]: "Tìm kiếm",
[Key.tags]: "Thẻ",
[Key.categories]: "Danh mục",
[Key.recentPosts]: "Bài viết mới nhất",
[Key.comments]: "Bình luận",
[Key.untitled]: "Không tiêu đề",
[Key.uncategorized]: "Chưa phân loại",
[Key.noTags]: "Chưa có thẻ",
[Key.wordCount]: "từ",
[Key.wordsCount]: "từ",
[Key.minuteCount]: "phút đọc",
[Key.minutesCount]: "phút đọc",
[Key.postCount]: "bài viết",
[Key.postsCount]: "bài viết",
[Key.themeColor]: "Màu giao diện",
[Key.lightMode]: "Sáng",
[Key.darkMode]: "Tối",
[Key.systemMode]: "Hệ thống",
[Key.more]: "Thêm",
[Key.author]: "Tác giả",
[Key.publishedAt]: "Đăng vào lúc",
[Key.license]: "Giấy phép bản quyền",
};

View File

@@ -35,4 +35,10 @@ export const zh_CN: Translation = {
[Key.author]: "作者",
[Key.publishedAt]: "发布于",
[Key.license]: "许可协议",
[Key.notFound]: "页面未找到",
[Key.notFoundDesc]: "你访问的链接已断开,页面走丢了",
[Key.backToHome]: "返回首页",
[Key.pinned]: "置顶",
[Key.toc]: "目录",
};

View File

@@ -35,4 +35,10 @@ export const zh_TW: Translation = {
[Key.author]: "作者",
[Key.publishedAt]: "發佈於",
[Key.license]: "許可協議",
[Key.notFound]: "頁面未找到",
[Key.notFoundDesc]: "你訪問的連結已斷開,頁面走丟了",
[Key.backToHome]: "返回首頁",
[Key.pinned]: "置頂",
[Key.toc]: "目錄",
};

View File

@@ -1,13 +1,6 @@
import { siteConfig } from "../config";
import type I18nKey from "./i18nKey";
import { en } from "./languages/en";
import { es } from "./languages/es";
import { id } from "./languages/id";
import { ja } from "./languages/ja";
import { ko } from "./languages/ko";
import { th } from "./languages/th";
import { tr } from "./languages/tr";
import { vi } from "./languages/vi";
import { zh_CN } from "./languages/zh_CN";
import { zh_TW } from "./languages/zh_TW";
@@ -18,24 +11,12 @@ export type Translation = {
const defaultTranslation = en;
const map: { [key: string]: Translation } = {
es: es,
en: en,
en_us: en,
en_gb: en,
en_au: en,
zh_cn: zh_CN,
zh_tw: zh_TW,
ja: ja,
ja_jp: ja,
ko: ko,
ko_kr: ko,
th: th,
th_th: th,
vi: vi,
vi_vn: vi,
id: id,
tr: tr,
tr_tr: tr,
};
export function getTranslation(lang: string): Translation {

View File

@@ -405,7 +405,19 @@ const setup = () => {
}
})
window.swup.hooks.on('content:replace', initCustomScrollbar)
window.swup.hooks.on('visit:start', (visit: {to: {url: string}}) => {
window.swup.hooks.on('visit:start', (visit: {to: {url: string};from: { url: string };containers: string[]}) => {
const postUrlPattern = "/posts/";
const isToPost = visit.to.url.includes(postUrlPattern);
const isFromPost = visit.from.url.includes(postUrlPattern);
if (isToPost || isFromPost) {
visit.containers = ["main", "#sidebar"]; // 刷新两者
} else {
// 否则(例如,从首页导航到关于页面),
// 仅刷新主内容区域。
visit.containers = ["main"];
}
// change banner height immediately when a link is clicked
const bodyElement = document.querySelector('body')
if (pathsEqual(visit.to.url, url('/'))) {
@@ -419,12 +431,6 @@ const setup = () => {
if (heightExtend) {
heightExtend.classList.remove('hidden')
}
// Hide the TOC while scrolling back to top
let toc = document.getElementById('toc-wrapper');
if (toc) {
toc.classList.add('toc-not-ready')
}
});
window.swup.hooks.on('page:view', () => {
// hide the temp high element when the transition is done
@@ -439,12 +445,6 @@ const setup = () => {
if (heightExtend) {
heightExtend.classList.add('hidden')
}
// Just make the transition looks better
const toc = document.getElementById('toc-wrapper');
if (toc) {
toc.classList.remove('toc-not-ready')
}
}, 200)
});
}
@@ -455,7 +455,6 @@ if (window?.swup?.hooks) {
}
let backToTopBtn = document.getElementById('back-to-top-btn');
let toc = document.getElementById('toc-wrapper');
let navbar = document.getElementById('navbar-wrapper')
function scrollFunction() {
let bannerHeight = window.innerHeight * (BANNER_HEIGHT / 100)
@@ -468,14 +467,6 @@ function scrollFunction() {
}
}
if (bannerEnabled && toc) {
if (document.body.scrollTop > bannerHeight || document.documentElement.scrollTop > bannerHeight) {
toc.classList.remove('toc-hide')
} else {
toc.classList.add('toc-hide')
}
}
if (!bannerEnabled) return
if (navbar) {
const NAVBAR_HEIGHT = 72

View File

@@ -1,12 +1,10 @@
---
import BackToTop from "@components/control/BackToTop.astro";
import Footer from "@components/Footer.astro";
import Navbar from "@components/Navbar.astro";
import SideBar from "@components/widget/SideBar.astro";
import type { MarkdownHeading } from "astro";
import { Icon } from "astro-icon/components";
import ImageWrapper from "../components/misc/ImageWrapper.astro";
import TOC from "../components/widget/TOC.astro";
import { siteConfig } from "../config";
import {
BANNER_HEIGHT,
@@ -98,28 +96,7 @@ const mainPanelTop = siteConfig.banner.enable
</div>
</div>
<BackToTop></BackToTop>
</div>
</div>
<!-- The things that should be under the banner, only the TOC for now -->
<div class="absolute w-full z-0 hidden 2xl:block">
<div class="relative max-w-[var(--page-width)] mx-auto">
<!-- TOC component -->
{siteConfig.toc.enable && <div id="toc-wrapper" class:list={["hidden lg:block transition absolute top-0 -right-[var(--toc-width)] w-[var(--toc-width)] items-center",
{"toc-hide": siteConfig.banner.enable}]}
>
<div id="toc-inner-wrapper" class="fixed top-14 w-[var(--toc-width)] h-[calc(100vh_-_20rem)] overflow-y-scroll overflow-x-hidden hide-scrollbar">
<div id="toc" class="w-full h-full transition-swup-fade ">
<div class="h-8 w-full"></div>
<TOC headings={headings}></TOC>
<div class="h-8 w-full"></div>
</div>
</div>
</div>}
<!-- #toc needs to exist for Swup to work normally -->
{!siteConfig.toc.enable && <div id="toc"></div>}
</div>
</div>
</Layout>

54
src/pages/404.astro Normal file
View File

@@ -0,0 +1,54 @@
---
import { Icon } from "astro-icon/components";
import I18nKey from "../i18n/i18nKey";
import { i18n } from "../i18n/translation";
import MainGridLayout from "../layouts/MainGridLayout.astro";
import { url } from "../utils/url-utils";
---
<MainGridLayout title={i18n(I18nKey.notFound)}>
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative min-h-[70vh]">
<div class="card-base z-10 px-9 py-6 relative w-full flex flex-col items-center justify-center">
<div class="onload-animation flex flex-col items-center justify-center text-center">
<h2 class="text-2xl md:text-3xl font-bold mb-8 text-90">
{i18n(I18nKey.notFound)}
</h2>
<div class="flex items-center justify-center gap-4 md:gap-6 mb-6">
<div class="relative">
<div class="absolute inset-0 blur-3xl opacity-20 bg-[var(--primary)] rounded-full"></div>
<Icon
name="fa6-solid:link-slash"
class="relative text-[8rem] md:text-[10rem] text-[var(--primary)] opacity-80 dark:opacity-60"
/>
</div>
<h1 class="text-7xl md:text-9xl font-bold text-[var(--primary)] opacity-90">
404
</h1>
</div>
<p class="text-lg text-75 mb-10 max-w-md">
{i18n(I18nKey.notFoundDesc)}
</p>
<a
href={url("/")}
class="btn-regular rounded-xl h-12 px-8 font-bold text-lg active:scale-95 transition
flex items-center hover:gap-3"
>
{i18n(I18nKey.backToHome)}
</a>
</div>
</div>
</div>
</MainGridLayout>
<style>
.btn-regular {
transition: all 200ms ease;
}
.btn-regular:hover {
gap: 0.75rem;
}
</style>

View File

@@ -3,12 +3,18 @@ import ArchivePanel from "@components/ArchivePanel.svelte";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
import { getSortedPostsList } from "../utils/content-utils";
import {
getCategoryPinnedPosts,
getPinnedPosts,
getSortedPostsList,
} from "../utils/content-utils";
const sortedPostsList = await getSortedPostsList();
const pinnedPosts = await getPinnedPosts();
const categoryPinnedPosts = await getCategoryPinnedPosts();
---
<MainGridLayout title={i18n(I18nKey.archive)}>
<ArchivePanel sortedPosts={sortedPostsList} client:only="svelte"></ArchivePanel>
<ArchivePanel sortedPosts={sortedPostsList} pinnedPosts={pinnedPosts} categoryPinnedPosts={categoryPinnedPosts} client:only="svelte"></ArchivePanel>
</MainGridLayout>

View File

@@ -6,8 +6,9 @@ import { h } from "hastscript";
*
* @param {Object} properties - The properties of the component.
* @param {string} properties.repo - The Gitlab repository in the format "owner/repo".
* @param {string} properties.service - (optional) Third-Party GitLab service provider.
* @param {import('mdast').RootContent[]} children - The children elements of the component.
* @returns {import('mdast').Parent} The created GitHub Card component.
* @returns {import('mdast').Parent} The created GitLab Card component.
*/
export function GitlabCardComponent(properties, children) {
if (Array.isArray(children) && children.length !== 0)
@@ -24,6 +25,7 @@ export function GitlabCardComponent(properties, children) {
const repo = properties.repo;
const repoE = repo.replace("/", "%2F"); // encoding by replace / to %2F
const service = properties?.service || "https://gitlab.com";
const cardUuid = `GC${Math.random().toString(36).slice(-6)}`; // Collisions are not important
const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });
@@ -43,7 +45,7 @@ export function GitlabCardComponent(properties, children) {
const nDescription = h(
`div#${cardUuid}-description`,
{ class: "gc-description" },
"Waiting for gitlab.com api...",
"Waiting for Gitlab Api...",
);
const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "00K");
@@ -53,7 +55,7 @@ export function GitlabCardComponent(properties, children) {
`script#${cardUuid}-script`,
{ type: "text/javascript", defer: true },
`
fetch('https://gitlab.com/api/v4/projects/${repoE}').then(response => response.json()).then(data => {
fetch('${service}/api/v4/projects/${repoE}').then(response => response.json()).then(data => {
document.getElementById('${cardUuid}-repo').innerText = data.name;
document.getElementById('${cardUuid}-description').innerText = data.description?.replace(/:[a-zA-Z0-9_]+:/g, '') || "Description not set";
document.getElementById('${cardUuid}-forks').innerText = Intl.NumberFormat('en-us', { notation: "compact", maximumFractionDigits: 1 }).format(data.forks_count).replaceAll("\u202f", '');
@@ -61,7 +63,7 @@ export function GitlabCardComponent(properties, children) {
const avatar_url = data.namespace.avatar_url;
if (avatar_url.startsWith('/')) {
document.getElementById('${cardUuid}-avatar').style.backgroundImage = 'url(https://gitlab.com' + avatar_url + ')';
document.getElementById('${cardUuid}-avatar').style.backgroundImage = 'url(${service}' + avatar_url + ')';
} else {
document.getElementById('${cardUuid}-avatar').style.backgroundImage = 'url(' + avatar_url + ')';
}
@@ -81,7 +83,7 @@ export function GitlabCardComponent(properties, children) {
`a#${cardUuid}-card`,
{
class: "card-github fetch-waiting no-styling",
href: `https://gitlab.com/${repo}`,
href: `${service}/${repo}`,
target: "_blank",
repo,
},

View File

@@ -65,9 +65,9 @@
@apply opacity-0 pointer-events-none
}
#toc-inner-wrapper {
/* #toc-inner-wrapper {
mask-image: linear-gradient(to bottom, transparent 0%, black 2rem, black calc(100% - 2rem), transparent 100%);
}
} */
.hide-scrollbar {
scrollbar-width: none;

View File

@@ -3,21 +3,35 @@ import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import { getCategoryUrl } from "@utils/url-utils.ts";
// // Retrieve posts and sort them by publication date
async function getRawSortedPosts() {
type Post = CollectionEntry<"posts">;
function sortPosts(posts: Post[], useCategoryPinned = false): Post[] {
return posts.sort((a, b) => {
const pinnedA = useCategoryPinned
? a.data.category_pinned || 0
: a.data.pinned || 0;
const pinnedB = useCategoryPinned
? b.data.category_pinned || 0
: b.data.pinned || 0;
const pinnedDiff = pinnedB - pinnedA;
if (pinnedDiff !== 0) return pinnedDiff;
const dateA = new Date(a.data.published);
const dateB = new Date(b.data.published);
return dateB.valueOf() - dateA.valueOf();
});
}
async function getRawSortedPosts(): Promise<Post[]> {
const allBlogPosts = await getCollection("posts", ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});
const sorted = allBlogPosts.sort((a, b) => {
const dateA = new Date(a.data.published);
const dateB = new Date(b.data.published);
return dateA > dateB ? -1 : 1;
});
return sorted;
return sortPosts(allBlogPosts);
}
export async function getSortedPosts() {
export async function getSortedPosts(): Promise<Post[]> {
const sorted = await getRawSortedPosts();
for (let i = 1; i < sorted.length; i++) {
@@ -112,3 +126,37 @@ export async function getCategoryList(): Promise<Category[]> {
}
return ret;
}
export async function getPinnedPosts(): Promise<PostForList[]> {
const allBlogPosts = await getCollection("posts", ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});
const pinnedPosts = allBlogPosts.filter(
(post) => (post.data.pinned || 0) > 0,
);
const sorted = sortPosts(pinnedPosts);
return sorted.map((post) => ({
slug: post.slug,
data: post.data,
}));
}
export async function getCategoryPinnedPosts(): Promise<PostForList[]> {
const allBlogPosts = await getCollection("posts", ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});
const categoryPinnedPosts = allBlogPosts.filter(
(post) => (post.data.category_pinned || 0) > 0,
);
const sorted = sortPosts(categoryPinnedPosts, true);
return sorted.map((post) => ({
slug: post.slug,
data: post.data,
}));
}