Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6243898db3 | |||
| 7ae6fb91a4 | |||
| cbf35beeb8 | |||
| b7ebf3bef7 | |||
| 370b421eed | |||
| d97e6c66d6 | |||
| 5de5876bde | |||
| 6d39b0dec4 | |||
| 1c53b77312 | |||
| 79332344a8 |
Vendored
+2
-4
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": false,
|
||||||
"editor.defaultFormatter": "biomejs.biome",
|
"editor.defaultFormatter": "biomejs.biome",
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
@@ -14,9 +14,7 @@
|
|||||||
"editor.defaultFormatter": "biomejs.biome"
|
"editor.defaultFormatter": "biomejs.biome"
|
||||||
},
|
},
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit",
|
"source.fixAll.biome": "explicit"
|
||||||
"quickfix.biome": "always",
|
|
||||||
"source.organizeImports.biome": "always"
|
|
||||||
},
|
},
|
||||||
"frontMatter.dashboard.openOnStart": false
|
"frontMatter.dashboard.openOnStart": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -69,7 +69,7 @@ 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:
|
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))
|
- 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/))
|
- Enhanced code blocks with Expressive Code ([Preview](https://fuwari.vercel.app/posts/expressive-code/) / [Docs](https://expressive-code.com/))
|
||||||
|
|
||||||
## ⚡ Commands
|
## ⚡ Commands
|
||||||
@@ -77,7 +77,7 @@ In addition to Astro's default support for [GitHub Flavored Markdown](https://gi
|
|||||||
All commands are run from the root of the project, from a terminal:
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
| Command | Action |
|
| Command | Action |
|
||||||
|:---------------------------|:----------------------------------------------------|
|
| :------------------------- | :----------------------------------------------- |
|
||||||
| `pnpm install` | Installs dependencies |
|
| `pnpm install` | Installs dependencies |
|
||||||
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
| `pnpm dev` | Starts local dev server at `localhost:4321` |
|
||||||
| `pnpm build` | Build your production site to `./dist/` |
|
| `pnpm build` | Build your production site to `./dist/` |
|
||||||
|
|||||||
+1
-1
@@ -27,7 +27,7 @@ import { pluginCustomCopyButton } from "./src/plugins/expressive-code/custom-cop
|
|||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: "https://fuwari.vercel.app/",
|
site: "https://milkfunc.top/",
|
||||||
base: "/",
|
base: "/",
|
||||||
trailingSlash: "always",
|
trailingSlash: "always",
|
||||||
integrations: [
|
integrations: [
|
||||||
|
|||||||
+13
-13
@@ -16,28 +16,28 @@
|
|||||||
"preinstall": "npx only-allow pnpm"
|
"preinstall": "npx only-allow pnpm"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.6",
|
||||||
"@astrojs/rss": "^4.0.12",
|
"@astrojs/rss": "^4.0.14",
|
||||||
"@astrojs/sitemap": "^3.6.0",
|
"@astrojs/sitemap": "^3.6.0",
|
||||||
"@astrojs/svelte": "7.2.0",
|
"@astrojs/svelte": "7.2.3",
|
||||||
"@astrojs/tailwind": "^6.0.2",
|
"@astrojs/tailwind": "^6.0.2",
|
||||||
"@expressive-code/core": "^0.41.3",
|
"@expressive-code/core": "^0.41.4",
|
||||||
"@expressive-code/plugin-collapsible-sections": "^0.41.3",
|
"@expressive-code/plugin-collapsible-sections": "^0.41.4",
|
||||||
"@expressive-code/plugin-line-numbers": "^0.41.3",
|
"@expressive-code/plugin-line-numbers": "^0.41.4",
|
||||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
"@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-brands": "^1.2.6",
|
||||||
"@iconify-json/fa6-regular": "^1.2.4",
|
"@iconify-json/fa6-regular": "^1.2.4",
|
||||||
"@iconify-json/fa6-solid": "^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",
|
"@iconify/svelte": "^4.2.0",
|
||||||
"@swup/astro": "^1.7.0",
|
"@swup/astro": "^1.7.0",
|
||||||
"@tailwindcss/typography": "^0.5.19",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"astro": "5.13.10",
|
"astro": "5.13.10",
|
||||||
"astro-expressive-code": "^0.41.3",
|
"astro-expressive-code": "^0.41.4",
|
||||||
"astro-icon": "^1.1.5",
|
"astro-icon": "^1.1.5",
|
||||||
"hastscript": "^9.0.1",
|
"hastscript": "^9.0.1",
|
||||||
"katex": "^0.16.23",
|
"katex": "^0.16.27",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"mdast-util-to-string": "^4.0.0",
|
"mdast-util-to-string": "^4.0.0",
|
||||||
"overlayscrollbars": "^2.12.0",
|
"overlayscrollbars": "^2.12.0",
|
||||||
@@ -54,15 +54,15 @@
|
|||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"remark-sectionize": "^2.1.0",
|
"remark-sectionize": "^2.1.0",
|
||||||
"sanitize-html": "^2.17.0",
|
"sanitize-html": "^2.17.0",
|
||||||
"sharp": "^0.34.4",
|
"sharp": "^0.34.5",
|
||||||
"stylus": "^0.64.0",
|
"stylus": "^0.64.0",
|
||||||
"svelte": "^5.39.8",
|
"svelte": "^5.39.8",
|
||||||
"tailwindcss": "^3.4.18",
|
"tailwindcss": "^3.4.19",
|
||||||
"typescript": "^5.9.3",
|
"typescript": "^5.9.3",
|
||||||
"unist-util-visit": "^5.0.0"
|
"unist-util-visit": "^5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/ts-plugin": "^1.10.4",
|
"@astrojs/ts-plugin": "^1.10.6",
|
||||||
"@biomejs/biome": "2.2.5",
|
"@biomejs/biome": "2.2.5",
|
||||||
"@rollup/plugin-yaml": "^4.1.2",
|
"@rollup/plugin-yaml": "^4.1.2",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
|
|||||||
Generated
+1149
-457
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 |
@@ -3,33 +3,29 @@ import { onMount } from "svelte";
|
|||||||
|
|
||||||
import I18nKey from "../i18n/i18nKey";
|
import I18nKey from "../i18n/i18nKey";
|
||||||
import { i18n } from "../i18n/translation";
|
import { i18n } from "../i18n/translation";
|
||||||
|
import type { PostForList } from "../utils/content-utils";
|
||||||
import { getPostUrlBySlug } from "../utils/url-utils";
|
import { getPostUrlBySlug } from "../utils/url-utils";
|
||||||
|
|
||||||
export let tags: string[];
|
export let tags: string[];
|
||||||
export let categories: 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);
|
const params = new URLSearchParams(window.location.search);
|
||||||
tags = params.has("tag") ? params.getAll("tag") : [];
|
tags = params.has("tag") ? params.getAll("tag") : [];
|
||||||
categories = params.has("category") ? params.getAll("category") : [];
|
categories = params.has("category") ? params.getAll("category") : [];
|
||||||
const uncategorized = params.get("uncategorized");
|
const uncategorized = params.get("uncategorized");
|
||||||
|
|
||||||
interface Post {
|
const isCategoryMode = categories.length > 0;
|
||||||
slug: string;
|
|
||||||
data: {
|
|
||||||
title: string;
|
|
||||||
tags: string[];
|
|
||||||
category?: string;
|
|
||||||
published: Date;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Group {
|
interface Group {
|
||||||
year: number;
|
year: number;
|
||||||
posts: Post[];
|
posts: PostForList[];
|
||||||
}
|
}
|
||||||
|
|
||||||
let groups: Group[] = [];
|
let groups: Group[] = [];
|
||||||
|
let displayPinnedPosts: PostForList[] = [];
|
||||||
|
|
||||||
function formatDate(date: Date) {
|
function formatDate(date: Date) {
|
||||||
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
const month = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||||
@@ -42,7 +38,7 @@ function formatTag(tagList: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
let filteredPosts: Post[] = sortedPosts;
|
let filteredPosts: PostForList[] = sortedPosts;
|
||||||
|
|
||||||
if (tags.length > 0) {
|
if (tags.length > 0) {
|
||||||
filteredPosts = filteredPosts.filter(
|
filteredPosts = filteredPosts.filter(
|
||||||
@@ -62,6 +58,39 @@ onMount(async () => {
|
|||||||
filteredPosts = filteredPosts.filter((post) => !post.data.category);
|
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(
|
const grouped = filteredPosts.reduce(
|
||||||
(acc, post) => {
|
(acc, post) => {
|
||||||
const year = post.data.published.getFullYear();
|
const year = post.data.published.getFullYear();
|
||||||
@@ -71,7 +100,7 @@ onMount(async () => {
|
|||||||
acc[year].push(post);
|
acc[year].push(post);
|
||||||
return acc;
|
return acc;
|
||||||
},
|
},
|
||||||
{} as Record<number, Post[]>,
|
{} as Record<number, PostForList[]>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({
|
const groupedPostsArray = Object.keys(grouped).map((yearStr) => ({
|
||||||
@@ -86,6 +115,65 @@ onMount(async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="card-base px-8 py-6">
|
<div class="card-base px-8 py-6">
|
||||||
|
{#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 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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#each groups as group}
|
{#each groups as group}
|
||||||
<div>
|
<div>
|
||||||
<div class="flex flex-row w-full items-center h-[3.75rem]">
|
<div class="flex flex-row w-full items-center h-[3.75rem]">
|
||||||
@@ -110,12 +198,10 @@ onMount(async () => {
|
|||||||
class="group btn-plain !block h-10 w-full rounded-lg hover:text-[initial]"
|
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="flex flex-row justify-start items-center h-full">
|
||||||
<!-- date -->
|
|
||||||
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
|
<div class="w-[15%] md:w-[10%] transition text-sm text-right text-50">
|
||||||
{formatDate(post.data.published)}
|
{formatDate(post.data.published)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- dot and line -->
|
|
||||||
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
|
<div class="w-[15%] md:w-[10%] relative dash-line h-full flex items-center">
|
||||||
<div
|
<div
|
||||||
class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
|
class="transition-all mx-auto w-1 h-1 rounded group-hover:h-5
|
||||||
@@ -127,7 +213,6 @@ onMount(async () => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- post title -->
|
|
||||||
<div
|
<div
|
||||||
class="w-[70%] md:max-w-[65%] md:w-[65%] text-left font-bold
|
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)]
|
group-hover:translate-x-1 transition-all group-hover:text-[var(--primary)]
|
||||||
@@ -136,7 +221,6 @@ onMount(async () => {
|
|||||||
{post.data.title}
|
{post.data.title}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- tag list -->
|
|
||||||
<div
|
<div
|
||||||
class="hidden md:block md:w-[15%] text-left text-sm transition
|
class="hidden md:block md:w-[15%] text-left text-sm transition
|
||||||
whitespace-nowrap overflow-ellipsis overflow-hidden text-30"
|
whitespace-nowrap overflow-ellipsis overflow-hidden text-30"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface Props {
|
|||||||
description: string;
|
description: string;
|
||||||
draft: boolean;
|
draft: boolean;
|
||||||
style: string;
|
style: string;
|
||||||
|
pinned?: number;
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
entry,
|
entry,
|
||||||
@@ -33,6 +34,7 @@ const {
|
|||||||
image,
|
image,
|
||||||
description,
|
description,
|
||||||
style,
|
style,
|
||||||
|
pinned = 0,
|
||||||
} = Astro.props;
|
} = Astro.props;
|
||||||
const className = Astro.props.class;
|
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
|
before:absolute before:top-[35px] before:left-[18px] before:hidden md:before:block
|
||||||
">
|
">
|
||||||
{title}
|
{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="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>
|
<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>
|
</a>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const interval = 50;
|
|||||||
image={entry.data.image}
|
image={entry.data.image}
|
||||||
description={entry.data.description}
|
description={entry.data.description}
|
||||||
draft={entry.data.draft}
|
draft={entry.data.draft}
|
||||||
|
pinned={entry.data.pinned}
|
||||||
class:list="onload-animation"
|
class:list="onload-animation"
|
||||||
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
|
style={`animation-delay: calc(var(--content-delay) + ${delay++ * interval}ms);`}
|
||||||
></PostCard>
|
></PostCard>
|
||||||
|
|||||||
@@ -1,22 +1,38 @@
|
|||||||
---
|
---
|
||||||
import type { MarkdownHeading } from "astro";
|
import type { MarkdownHeading } from "astro";
|
||||||
|
import { siteConfig } from "@/config";
|
||||||
import Categories from "./Categories.astro";
|
import Categories from "./Categories.astro";
|
||||||
import Profile from "./Profile.astro";
|
import Profile from "./Profile.astro";
|
||||||
import Tag from "./Tags.astro";
|
import Tag from "./Tags.astro";
|
||||||
|
import TOC from "./TOC.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
headings?: MarkdownHeading[];
|
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 id="sidebar" class:list={[className, "w-full"]}>
|
||||||
|
{
|
||||||
|
!isPostPage && (
|
||||||
<div class="flex flex-col w-full gap-4 mb-4">
|
<div class="flex flex-col w-full gap-4 mb-4">
|
||||||
<Profile></Profile>
|
<Profile />
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
<div id="sidebar-sticky" class="transition-all duration-700 flex flex-col w-full gap-4 top-4 sticky top-4">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+175
-161
@@ -1,7 +1,10 @@
|
|||||||
---
|
---
|
||||||
import type { MarkdownHeading } from "astro";
|
import type { MarkdownHeading } from "astro";
|
||||||
import { siteConfig } from "../../config";
|
import { siteConfig } from "../../config";
|
||||||
|
import I18nKey from "../../i18n/i18nKey";
|
||||||
|
import { i18n } from "../../i18n/translation";
|
||||||
import { url } from "../../utils/url-utils";
|
import { url } from "../../utils/url-utils";
|
||||||
|
import WidgetLayout from "./WidgetLayout.astro";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
class?: string;
|
class?: string;
|
||||||
@@ -31,238 +34,249 @@ let heading1Count = 1;
|
|||||||
|
|
||||||
const maxLevel = siteConfig.toc.depth;
|
const maxLevel = siteConfig.toc.depth;
|
||||||
---
|
---
|
||||||
{isPostsRoute &&
|
|
||||||
<table-of-contents class:list={[className, "group"]}>
|
<table-of-contents>
|
||||||
{headings.filter((heading) => heading.depth < minDepth + maxLevel).map((heading) =>
|
<WidgetLayout name={i18n(I18nKey.toc)} id="toc" class={className}>
|
||||||
<a href={`#${heading.slug}`} class="px-2 flex gap-2 relative transition w-full min-h-9 rounded-xl
|
<div class="flex flex-col pb-4">
|
||||||
hover:bg-[var(--toc-btn-hover)] active:bg-[var(--toc-btn-active)] py-2
|
<div
|
||||||
">
|
id="toc-wrapper"
|
||||||
<div class:list={["transition w-5 h-5 shrink-0 rounded-lg text-xs flex items-center justify-center font-bold",
|
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"
|
||||||
|
>
|
||||||
{
|
{
|
||||||
"bg-[var(--toc-badge-bg)] text-[var(--btn-content)]": heading.depth == minDepth,
|
headings
|
||||||
"ml-4": heading.depth == minDepth + 1,
|
.filter(
|
||||||
"ml-8": heading.depth == minDepth + 2,
|
(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 &&
|
||||||
{heading.depth == minDepth + 1 && <div class="transition w-2 h-2 rounded-[0.1875rem] bg-[var(--toc-badge-bg)]"></div>}
|
heading1Count++}
|
||||||
{heading.depth == minDepth + 2 && <div class="transition w-1.5 h-1.5 rounded-sm bg-black/5 dark:bg-white/10"></div>}
|
{heading.depth == minDepth + 1 && (
|
||||||
</div>
|
<div class="transition w-2 h-2 rounded-[0.1875rem] bg-[var(--toc-badge-bg)]" />
|
||||||
<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 " +
|
{heading.depth == minDepth + 2 && (
|
||||||
"group-hover:bg-transparent border-2 border-[var(--toc-btn-hover)] group-hover:border-[var(--toc-btn-active)] border-dashed"]}></div>
|
<div class="transition w-1.5 h-1.5 rounded-sm bg-black/5 dark:bg-white/10" />
|
||||||
</table-of-contents>}
|
)}
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</WidgetLayout>
|
||||||
|
</table-of-contents>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
table-of-contents a.visible {
|
||||||
|
background-color: var(--toc-btn-active) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
class TableOfContents extends HTMLElement {
|
class TableOfContents extends HTMLElement {
|
||||||
tocEl: HTMLElement | null = null;
|
tocEl: HTMLElement | null = null;
|
||||||
visibleClass = "visible";
|
visibleClass = "visible";
|
||||||
observer: IntersectionObserver;
|
observer: IntersectionObserver;
|
||||||
anchorNavTarget: HTMLElement | null = null;
|
anchorNavTarget: HTMLElement | null = null;
|
||||||
headingIdxMap = new Map<string, number>();
|
headingIdxMap = new Map<string, number>();
|
||||||
headings: HTMLElement[] = [];
|
|
||||||
sections: HTMLElement[] = [];
|
sections: HTMLElement[] = [];
|
||||||
tocEntries: HTMLAnchorElement[] = [];
|
tocEntries: HTMLAnchorElement[] = [];
|
||||||
active: boolean[] = [];
|
// active 数组不再需要,我们将直接追踪 activeIndex
|
||||||
activeIndicator: HTMLElement | null = null;
|
activeIndex: number | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
// 调整 IntersectionObserver 的 rootMargin,使其在章节顶部进入视口时就触发
|
||||||
this.observer = new IntersectionObserver(
|
this.observer = new IntersectionObserver(
|
||||||
this.markVisibleSection, { threshold: 0 }
|
this.handleIntersection,
|
||||||
|
{ rootMargin: "0px 0px -80% 0px" }, // 关键调整
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 核心修改点 1: 新的 IntersectionObserver 回调 ---
|
||||||
|
handleIntersection = (entries: IntersectionObserverEntry[]) => {
|
||||||
|
// 当用户点击链接导航时,忽略 IntersectionObserver 的结果,防止冲突
|
||||||
|
if (this.anchorNavTarget) return;
|
||||||
|
|
||||||
|
let latestVisibleEntry: IntersectionObserverEntry | undefined;
|
||||||
|
|
||||||
|
// 找到进入视口的、最新的(在文档中位置最靠后)的条目
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
latestVisibleEntry = entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestVisibleEntry) {
|
||||||
|
const id =
|
||||||
|
latestVisibleEntry.target.children[0]?.getAttribute("id");
|
||||||
|
const idx = id ? this.headingIdxMap.get(id) : undefined;
|
||||||
|
if (idx !== undefined) {
|
||||||
|
this.setActiveIndex(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
markVisibleSection = (entries: IntersectionObserverEntry[]) => {
|
// --- 核心修改点 2: 统一设置活动索引并更新UI ---
|
||||||
entries.forEach((entry) => {
|
setActiveIndex = (index: number | null) => {
|
||||||
const id = entry.target.children[0]?.getAttribute("id");
|
// 如果索引没有变化,则不执行任何操作
|
||||||
const idx = id ? this.headingIdxMap.get(id) : undefined;
|
if (this.activeIndex === index) return;
|
||||||
if (idx != undefined)
|
|
||||||
this.active[idx] = entry.isIntersecting;
|
|
||||||
|
|
||||||
if (entry.isIntersecting && this.anchorNavTarget == entry.target.firstChild)
|
this.activeIndex = index;
|
||||||
this.anchorNavTarget = null;
|
|
||||||
|
// 更新UI
|
||||||
|
this.tocEntries.forEach((entry, i) => {
|
||||||
|
if (i === this.activeIndex) {
|
||||||
|
entry.classList.add(this.visibleClass);
|
||||||
|
} else {
|
||||||
|
entry.classList.remove(this.visibleClass);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.active.includes(true))
|
// 只有在非用户点击导航时才自动滚动
|
||||||
this.fallback();
|
if (!this.anchorNavTarget) {
|
||||||
this.update();
|
this.scrollToActiveHeading();
|
||||||
};
|
|
||||||
|
|
||||||
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--;
|
|
||||||
}
|
|
||||||
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 = () => {
|
scrollToActiveHeading = () => {
|
||||||
// If the TOC widget can accommodate both the topmost
|
if (this.activeIndex === null || !this.tocEl) return;
|
||||||
// and bottommost items, scroll to the topmost item.
|
|
||||||
// Otherwise, scroll to the bottommost one.
|
|
||||||
|
|
||||||
if (this.anchorNavTarget || !this.tocEl) return;
|
const activeEntry = this.tocEntries[this.activeIndex];
|
||||||
const activeHeading =
|
if (!activeEntry) return;
|
||||||
document.querySelectorAll<HTMLDivElement>(`#toc .${this.visibleClass}`);
|
|
||||||
if (!activeHeading.length) return;
|
|
||||||
|
|
||||||
const topmost = activeHeading[0];
|
|
||||||
const bottommost = activeHeading[activeHeading.length - 1];
|
|
||||||
const tocHeight = this.tocEl.clientHeight;
|
const tocHeight = this.tocEl.clientHeight;
|
||||||
|
const entryTop = activeEntry.offsetTop;
|
||||||
|
const entryHeight = activeEntry.offsetHeight;
|
||||||
|
|
||||||
let top;
|
// 检查活动项是否在可视区域内
|
||||||
if (bottommost.getBoundingClientRect().bottom -
|
if (
|
||||||
topmost.getBoundingClientRect().top < 0.9 * tocHeight)
|
entryTop < this.tocEl.scrollTop ||
|
||||||
top = topmost.offsetTop - 32;
|
entryTop + entryHeight > this.tocEl.scrollTop + tocHeight
|
||||||
else
|
) {
|
||||||
top = bottommost.offsetTop - tocHeight * 0.8;
|
|
||||||
|
|
||||||
this.tocEl.scrollTo({
|
this.tocEl.scrollTo({
|
||||||
top,
|
top: entryTop - tocHeight / 2 + entryHeight / 2, // 尝试滚动到中间位置
|
||||||
left: 0,
|
left: 0,
|
||||||
behavior: "smooth",
|
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);
|
|
||||||
}
|
|
||||||
else if (offsetTop > window.innerHeight) break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
markActiveHeading = (idx: number)=> {
|
// --- 核心修改点 3: 处理点击事件 ---
|
||||||
this.active[idx] = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
handleAnchorClick = (event: Event) => {
|
handleAnchorClick = (event: Event) => {
|
||||||
const anchor = event
|
// 找到被点击的 <a> 标签
|
||||||
.composedPath()
|
const anchor = event.currentTarget as HTMLAnchorElement;
|
||||||
.find((element) => element instanceof HTMLAnchorElement);
|
|
||||||
|
|
||||||
if (anchor) {
|
|
||||||
const id = decodeURIComponent(anchor.hash?.substring(1));
|
const id = decodeURIComponent(anchor.hash?.substring(1));
|
||||||
const idx = this.headingIdxMap.get(id);
|
const idx = this.headingIdxMap.get(id);
|
||||||
if (idx !== undefined) {
|
|
||||||
this.anchorNavTarget = this.headings[idx];
|
|
||||||
} else {
|
|
||||||
this.anchorNavTarget = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
isInRange(value: number, min: number, max: number) {
|
if (idx !== undefined) {
|
||||||
return min < value && value < max;
|
// 标记正在进行锚点导航,以暂时禁用 IntersectionObserver 的更新
|
||||||
|
const targetHeading = document.getElementById(id);
|
||||||
|
if (targetHeading) {
|
||||||
|
this.anchorNavTarget = targetHeading;
|
||||||
|
// 在导航完成后重置标志
|
||||||
|
setTimeout(() => {
|
||||||
|
this.anchorNavTarget = null;
|
||||||
|
}, 1000); // 1秒后自动清除,以防万一
|
||||||
|
}
|
||||||
|
// 立即更新高亮
|
||||||
|
this.setActiveIndex(idx);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
// wait for the onload animation to finish, which makes the `getBoundingClientRect` return correct values
|
requestAnimationFrame(() => {
|
||||||
const element = document.querySelector('.prose');
|
|
||||||
if (element) {
|
|
||||||
element.addEventListener('animationend', () => {
|
|
||||||
this.init();
|
this.init();
|
||||||
}, { once: true });
|
});
|
||||||
} else {
|
|
||||||
console.debug('Animation element not found');
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.tocEl = document.getElementById(
|
this.tocEl = document.getElementById("toc-inner-wrapper") || this;
|
||||||
"toc-inner-wrapper"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this.tocEl) return;
|
|
||||||
|
|
||||||
this.tocEl.addEventListener("click", this.handleAnchorClick, {
|
|
||||||
capture: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.activeIndicator = document.getElementById("active-indicator");
|
|
||||||
|
|
||||||
this.tocEntries = Array.from(
|
this.tocEntries = Array.from(
|
||||||
document.querySelectorAll<HTMLAnchorElement>("#toc a[href^='#']")
|
this.querySelectorAll<HTMLAnchorElement>("a[href^='#']"),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.tocEntries.length === 0) return;
|
if (this.tocEntries.length === 0) return;
|
||||||
|
|
||||||
this.sections = new Array(this.tocEntries.length);
|
this.sections = new Array(this.tocEntries.length);
|
||||||
this.headings = new Array(this.tocEntries.length);
|
|
||||||
for (let i = 0; i < this.tocEntries.length; i++) {
|
for (let i = 0; i < this.tocEntries.length; i++) {
|
||||||
const id = decodeURIComponent(this.tocEntries[i].hash?.substring(1));
|
const entry = this.tocEntries[i];
|
||||||
|
const id = decodeURIComponent(entry.hash?.substring(1));
|
||||||
const heading = document.getElementById(id);
|
const heading = document.getElementById(id);
|
||||||
const section = heading?.parentElement;
|
const section = heading?.parentElement;
|
||||||
if (heading instanceof HTMLElement && section instanceof HTMLElement) {
|
|
||||||
this.headings[i] = heading;
|
if (
|
||||||
|
heading instanceof HTMLElement &&
|
||||||
|
section instanceof HTMLElement
|
||||||
|
) {
|
||||||
this.sections[i] = section;
|
this.sections[i] = section;
|
||||||
this.headingIdxMap.set(id, i);
|
this.headingIdxMap.set(id, i);
|
||||||
|
// 为每个链接单独添加点击事件监听器
|
||||||
|
entry.addEventListener("click", this.handleAnchorClick);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.active = new Array(this.tocEntries.length).fill(false);
|
|
||||||
|
|
||||||
this.sections.forEach((section) =>
|
this.sections.forEach((section) => this.observer.observe(section));
|
||||||
this.observer.observe(section)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.fallback();
|
// 页面加载时,设置第一个标题为活动状态
|
||||||
this.update();
|
this.setActiveIndex(0);
|
||||||
};
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this.sections.forEach((section) =>
|
this.sections.forEach((section) =>
|
||||||
this.observer.unobserve(section)
|
this.observer.unobserve(section),
|
||||||
);
|
);
|
||||||
this.observer.disconnect();
|
this.observer.disconnect();
|
||||||
this.tocEl?.removeEventListener("click", this.handleAnchorClick);
|
this.tocEntries.forEach((entry) => {
|
||||||
};
|
entry.removeEventListener("click", this.handleAnchorClick);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!customElements.get("table-of-contents")) {
|
if (!customElements.get("table-of-contents")) {
|
||||||
customElements.define("table-of-contents", TableOfContents);
|
customElements.define("table-of-contents", TableOfContents);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
+27
-36
@@ -8,16 +8,16 @@ import type {
|
|||||||
import { LinkPreset } from "./types/config";
|
import { LinkPreset } from "./types/config";
|
||||||
|
|
||||||
export const siteConfig: SiteConfig = {
|
export const siteConfig: SiteConfig = {
|
||||||
title: "Fuwari",
|
title: "MilkFunc",
|
||||||
subtitle: "Demo Site",
|
subtitle: "Blogs",
|
||||||
lang: "en", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.
|
lang: "zh_CN", // Language code, e.g. 'en', 'zh_CN', 'ja', etc.
|
||||||
themeColor: {
|
themeColor: {
|
||||||
hue: 250, // Default hue for the theme color, from 0 to 360. e.g. red: 0, teal: 200, cyan: 250, pink: 345
|
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: {
|
banner: {
|
||||||
enable: false,
|
enable: true,
|
||||||
src: "assets/images/demo-banner.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
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
|
position: "center", // Equivalent to object-position, only supports 'top', 'center', 'bottom'. 'center' by default
|
||||||
credit: {
|
credit: {
|
||||||
enable: false, // Display the credit text of the banner image
|
enable: false, // Display the credit text of the banner image
|
||||||
@@ -40,40 +40,31 @@ export const siteConfig: SiteConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const navBarConfig: NavBarConfig = {
|
export const navBarConfig: NavBarConfig = {
|
||||||
links: [
|
links: [LinkPreset.Home, LinkPreset.Archive, LinkPreset.About],
|
||||||
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
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const profileConfig: ProfileConfig = {
|
export const profileConfig: ProfileConfig = {
|
||||||
avatar: "assets/images/demo-avatar.png", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
avatar: "assets/images/avatar.webp", // Relative to the /src directory. Relative to the /public directory if it starts with '/'
|
||||||
name: "Lorem Ipsum",
|
name: "CapaCake",
|
||||||
bio: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
bio: "😨为什么我的电容炸了?",
|
||||||
links: [
|
links: [
|
||||||
{
|
// {
|
||||||
name: "Twitter",
|
// name: "Twitter",
|
||||||
icon: "fa6-brands:twitter", // Visit https://icones.js.org/ for icon codes
|
// 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
|
// // You will need to install the corresponding icon set if it's not already included
|
||||||
// `pnpm add @iconify-json/<icon-set-name>`
|
// // `pnpm add @iconify-json/<icon-set-name>`
|
||||||
url: "https://twitter.com",
|
// url: "https://twitter.com",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "Steam",
|
// name: "Steam",
|
||||||
icon: "fa6-brands:steam",
|
// icon: "fa6-brands:steam",
|
||||||
url: "https://store.steampowered.com",
|
// url: "https://store.steampowered.com",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
name: "GitHub",
|
// name: "GitHub",
|
||||||
icon: "fa6-brands:github",
|
// icon: "fa6-brands:github",
|
||||||
url: "https://github.com/saicaca/fuwari",
|
// url: "https://github.com/saicaca/fuwari",
|
||||||
},
|
// },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ const postsCollection = defineCollection({
|
|||||||
tags: z.array(z.string()).optional().default([]),
|
tags: z.array(z.string()).optional().default([]),
|
||||||
category: z.string().optional().nullable().default(""),
|
category: z.string().optional().nullable().default(""),
|
||||||
lang: z.string().optional().default(""),
|
lang: z.string().optional().default(""),
|
||||||
|
pinned: z.number().optional().default(0),
|
||||||
|
category_pinned: z.number().optional().default(0),
|
||||||
|
|
||||||
/* For internal use */
|
/* For internal use */
|
||||||
prevTitle: z.string().default(""),
|
prevTitle: z.string().default(""),
|
||||||
|
|||||||
Symlink
+1
@@ -0,0 +1 @@
|
|||||||
|
../../../blog-post/published
|
||||||
@@ -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
|
|
||||||
---
|
|
||||||
@@ -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,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**]!
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -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>
|
|
||||||
@@ -3,6 +3,7 @@ This is the demo site for [Fuwari](https://github.com/saicaca/fuwari).
|
|||||||
|
|
||||||
::github{repo="saicaca/fuwari"}
|
::github{repo="saicaca/fuwari"}
|
||||||
|
|
||||||
|
::gitlab{repo="CapaCake/milkblogs-fuwari" service="https://git.milkfunc.top"}
|
||||||
> ### Sources of images used in this site
|
> ### Sources of images used in this site
|
||||||
> - [Unsplash](https://unsplash.com/)
|
> - [Unsplash](https://unsplash.com/)
|
||||||
> - [星と少女](https://www.pixiv.net/artworks/108916539) by [Stella](https://www.pixiv.net/users/93273965)
|
> - [星と少女](https://www.pixiv.net/artworks/108916539) by [Stella](https://www.pixiv.net/users/93273965)
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ enum I18nKey {
|
|||||||
author = "author",
|
author = "author",
|
||||||
publishedAt = "publishedAt",
|
publishedAt = "publishedAt",
|
||||||
license = "license",
|
license = "license",
|
||||||
|
|
||||||
|
notFound = "notFound",
|
||||||
|
notFoundDesc = "notFoundDesc",
|
||||||
|
backToHome = "backToHome",
|
||||||
|
pinned = "pinned",
|
||||||
|
toc = "toc",
|
||||||
}
|
}
|
||||||
|
|
||||||
export default I18nKey;
|
export default I18nKey;
|
||||||
|
|||||||
@@ -35,4 +35,10 @@ export const en: Translation = {
|
|||||||
[Key.author]: "Author",
|
[Key.author]: "Author",
|
||||||
[Key.publishedAt]: "Published at",
|
[Key.publishedAt]: "Published at",
|
||||||
[Key.license]: "License",
|
[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",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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",
|
|
||||||
};
|
|
||||||
@@ -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 +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]: "ライセンス",
|
|
||||||
};
|
|
||||||
@@ -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]: "라이선스",
|
|
||||||
};
|
|
||||||
@@ -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]: "สัญญาอนุญาต",
|
|
||||||
};
|
|
||||||
@@ -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",
|
|
||||||
};
|
|
||||||
@@ -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",
|
|
||||||
};
|
|
||||||
@@ -35,4 +35,10 @@ export const zh_CN: Translation = {
|
|||||||
[Key.author]: "作者",
|
[Key.author]: "作者",
|
||||||
[Key.publishedAt]: "发布于",
|
[Key.publishedAt]: "发布于",
|
||||||
[Key.license]: "许可协议",
|
[Key.license]: "许可协议",
|
||||||
|
|
||||||
|
[Key.notFound]: "页面未找到",
|
||||||
|
[Key.notFoundDesc]: "你访问的链接已断开,页面走丢了",
|
||||||
|
[Key.backToHome]: "返回首页",
|
||||||
|
[Key.pinned]: "置顶",
|
||||||
|
[Key.toc]: "目录",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,4 +35,10 @@ export const zh_TW: Translation = {
|
|||||||
[Key.author]: "作者",
|
[Key.author]: "作者",
|
||||||
[Key.publishedAt]: "發佈於",
|
[Key.publishedAt]: "發佈於",
|
||||||
[Key.license]: "許可協議",
|
[Key.license]: "許可協議",
|
||||||
|
|
||||||
|
[Key.notFound]: "頁面未找到",
|
||||||
|
[Key.notFoundDesc]: "你訪問的連結已斷開,頁面走丟了",
|
||||||
|
[Key.backToHome]: "返回首頁",
|
||||||
|
[Key.pinned]: "置頂",
|
||||||
|
[Key.toc]: "目錄",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import { siteConfig } from "../config";
|
import { siteConfig } from "../config";
|
||||||
import type I18nKey from "./i18nKey";
|
import type I18nKey from "./i18nKey";
|
||||||
import { en } from "./languages/en";
|
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_CN } from "./languages/zh_CN";
|
||||||
import { zh_TW } from "./languages/zh_TW";
|
import { zh_TW } from "./languages/zh_TW";
|
||||||
|
|
||||||
@@ -18,24 +11,12 @@ export type Translation = {
|
|||||||
const defaultTranslation = en;
|
const defaultTranslation = en;
|
||||||
|
|
||||||
const map: { [key: string]: Translation } = {
|
const map: { [key: string]: Translation } = {
|
||||||
es: es,
|
|
||||||
en: en,
|
en: en,
|
||||||
en_us: en,
|
en_us: en,
|
||||||
en_gb: en,
|
en_gb: en,
|
||||||
en_au: en,
|
en_au: en,
|
||||||
zh_cn: zh_CN,
|
zh_cn: zh_CN,
|
||||||
zh_tw: zh_TW,
|
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 {
|
export function getTranslation(lang: string): Translation {
|
||||||
|
|||||||
+13
-22
@@ -405,7 +405,19 @@ const setup = () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
window.swup.hooks.on('content:replace', initCustomScrollbar)
|
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
|
// change banner height immediately when a link is clicked
|
||||||
const bodyElement = document.querySelector('body')
|
const bodyElement = document.querySelector('body')
|
||||||
if (pathsEqual(visit.to.url, url('/'))) {
|
if (pathsEqual(visit.to.url, url('/'))) {
|
||||||
@@ -419,12 +431,6 @@ const setup = () => {
|
|||||||
if (heightExtend) {
|
if (heightExtend) {
|
||||||
heightExtend.classList.remove('hidden')
|
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', () => {
|
window.swup.hooks.on('page:view', () => {
|
||||||
// hide the temp high element when the transition is done
|
// hide the temp high element when the transition is done
|
||||||
@@ -439,12 +445,6 @@ const setup = () => {
|
|||||||
if (heightExtend) {
|
if (heightExtend) {
|
||||||
heightExtend.classList.add('hidden')
|
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)
|
}, 200)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -455,7 +455,6 @@ if (window?.swup?.hooks) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let backToTopBtn = document.getElementById('back-to-top-btn');
|
let backToTopBtn = document.getElementById('back-to-top-btn');
|
||||||
let toc = document.getElementById('toc-wrapper');
|
|
||||||
let navbar = document.getElementById('navbar-wrapper')
|
let navbar = document.getElementById('navbar-wrapper')
|
||||||
function scrollFunction() {
|
function scrollFunction() {
|
||||||
let bannerHeight = window.innerHeight * (BANNER_HEIGHT / 100)
|
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 (!bannerEnabled) return
|
||||||
if (navbar) {
|
if (navbar) {
|
||||||
const NAVBAR_HEIGHT = 72
|
const NAVBAR_HEIGHT = 72
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
---
|
---
|
||||||
import BackToTop from "@components/control/BackToTop.astro";
|
|
||||||
import Footer from "@components/Footer.astro";
|
import Footer from "@components/Footer.astro";
|
||||||
import Navbar from "@components/Navbar.astro";
|
import Navbar from "@components/Navbar.astro";
|
||||||
import SideBar from "@components/widget/SideBar.astro";
|
import SideBar from "@components/widget/SideBar.astro";
|
||||||
import type { MarkdownHeading } from "astro";
|
import type { MarkdownHeading } from "astro";
|
||||||
import { Icon } from "astro-icon/components";
|
import { Icon } from "astro-icon/components";
|
||||||
import ImageWrapper from "../components/misc/ImageWrapper.astro";
|
import ImageWrapper from "../components/misc/ImageWrapper.astro";
|
||||||
import TOC from "../components/widget/TOC.astro";
|
|
||||||
import { siteConfig } from "../config";
|
import { siteConfig } from "../config";
|
||||||
import {
|
import {
|
||||||
BANNER_HEIGHT,
|
BANNER_HEIGHT,
|
||||||
@@ -98,28 +96,7 @@ const mainPanelTop = siteConfig.banner.enable
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BackToTop></BackToTop>
|
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</Layout>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -3,12 +3,18 @@ import ArchivePanel from "@components/ArchivePanel.svelte";
|
|||||||
import I18nKey from "@i18n/i18nKey";
|
import I18nKey from "@i18n/i18nKey";
|
||||||
import { i18n } from "@i18n/translation";
|
import { i18n } from "@i18n/translation";
|
||||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
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 sortedPostsList = await getSortedPostsList();
|
||||||
|
const pinnedPosts = await getPinnedPosts();
|
||||||
|
const categoryPinnedPosts = await getCategoryPinnedPosts();
|
||||||
---
|
---
|
||||||
|
|
||||||
<MainGridLayout title={i18n(I18nKey.archive)}>
|
<MainGridLayout title={i18n(I18nKey.archive)}>
|
||||||
<ArchivePanel sortedPosts={sortedPostsList} client:only="svelte"></ArchivePanel>
|
<ArchivePanel sortedPosts={sortedPostsList} pinnedPosts={pinnedPosts} categoryPinnedPosts={categoryPinnedPosts} client:only="svelte"></ArchivePanel>
|
||||||
</MainGridLayout>
|
</MainGridLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { h } from "hastscript";
|
|||||||
*
|
*
|
||||||
* @param {Object} properties - The properties of the component.
|
* @param {Object} properties - The properties of the component.
|
||||||
* @param {string} properties.repo - The Gitlab repository in the format "owner/repo".
|
* @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.
|
* @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) {
|
export function GitlabCardComponent(properties, children) {
|
||||||
if (Array.isArray(children) && children.length !== 0)
|
if (Array.isArray(children) && children.length !== 0)
|
||||||
@@ -24,6 +25,7 @@ export function GitlabCardComponent(properties, children) {
|
|||||||
|
|
||||||
const repo = properties.repo;
|
const repo = properties.repo;
|
||||||
const repoE = repo.replace("/", "%2F"); // encoding by replace / to %2F
|
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 cardUuid = `GC${Math.random().toString(36).slice(-6)}`; // Collisions are not important
|
||||||
|
|
||||||
const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });
|
const nAvatar = h(`div#${cardUuid}-avatar`, { class: "gc-avatar" });
|
||||||
@@ -43,7 +45,7 @@ export function GitlabCardComponent(properties, children) {
|
|||||||
const nDescription = h(
|
const nDescription = h(
|
||||||
`div#${cardUuid}-description`,
|
`div#${cardUuid}-description`,
|
||||||
{ class: "gc-description" },
|
{ class: "gc-description" },
|
||||||
"Waiting for gitlab.com api...",
|
"Waiting for Gitlab Api...",
|
||||||
);
|
);
|
||||||
|
|
||||||
const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "00K");
|
const nStars = h(`div#${cardUuid}-stars`, { class: "gc-stars" }, "00K");
|
||||||
@@ -53,7 +55,7 @@ export function GitlabCardComponent(properties, children) {
|
|||||||
`script#${cardUuid}-script`,
|
`script#${cardUuid}-script`,
|
||||||
{ type: "text/javascript", defer: true },
|
{ 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}-repo').innerText = data.name;
|
||||||
document.getElementById('${cardUuid}-description').innerText = data.description?.replace(/:[a-zA-Z0-9_]+:/g, '') || "Description not set";
|
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", '');
|
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;
|
const avatar_url = data.namespace.avatar_url;
|
||||||
if (avatar_url.startsWith('/')) {
|
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 {
|
} else {
|
||||||
document.getElementById('${cardUuid}-avatar').style.backgroundImage = 'url(' + avatar_url + ')';
|
document.getElementById('${cardUuid}-avatar').style.backgroundImage = 'url(' + avatar_url + ')';
|
||||||
}
|
}
|
||||||
@@ -81,7 +83,7 @@ export function GitlabCardComponent(properties, children) {
|
|||||||
`a#${cardUuid}-card`,
|
`a#${cardUuid}-card`,
|
||||||
{
|
{
|
||||||
class: "card-github fetch-waiting no-styling",
|
class: "card-github fetch-waiting no-styling",
|
||||||
href: `https://gitlab.com/${repo}`,
|
href: `${service}/${repo}`,
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
repo,
|
repo,
|
||||||
},
|
},
|
||||||
|
|||||||
+2
-2
@@ -65,9 +65,9 @@
|
|||||||
@apply opacity-0 pointer-events-none
|
@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%);
|
mask-image: linear-gradient(to bottom, transparent 0%, black 2rem, black calc(100% - 2rem), transparent 100%);
|
||||||
}
|
} */
|
||||||
|
|
||||||
.hide-scrollbar {
|
.hide-scrollbar {
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|||||||
@@ -3,21 +3,35 @@ import I18nKey from "@i18n/i18nKey";
|
|||||||
import { i18n } from "@i18n/translation";
|
import { i18n } from "@i18n/translation";
|
||||||
import { getCategoryUrl } from "@utils/url-utils.ts";
|
import { getCategoryUrl } from "@utils/url-utils.ts";
|
||||||
|
|
||||||
// // Retrieve posts and sort them by publication date
|
type Post = CollectionEntry<"posts">;
|
||||||
async function getRawSortedPosts() {
|
|
||||||
|
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 }) => {
|
const allBlogPosts = await getCollection("posts", ({ data }) => {
|
||||||
return import.meta.env.PROD ? data.draft !== true : true;
|
return import.meta.env.PROD ? data.draft !== true : true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const sorted = allBlogPosts.sort((a, b) => {
|
return sortPosts(allBlogPosts);
|
||||||
const dateA = new Date(a.data.published);
|
|
||||||
const dateB = new Date(b.data.published);
|
|
||||||
return dateA > dateB ? -1 : 1;
|
|
||||||
});
|
|
||||||
return sorted;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSortedPosts() {
|
export async function getSortedPosts(): Promise<Post[]> {
|
||||||
const sorted = await getRawSortedPosts();
|
const sorted = await getRawSortedPosts();
|
||||||
|
|
||||||
for (let i = 1; i < sorted.length; i++) {
|
for (let i = 1; i < sorted.length; i++) {
|
||||||
@@ -112,3 +126,37 @@ export async function getCategoryList(): Promise<Category[]> {
|
|||||||
}
|
}
|
||||||
return ret;
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user