mirror of
https://github.com/saicaca/fuwari.git
synced 2026-01-11 23:02:53 +01:00
feat: add FrontMatter CMS, biome, translation, etc.
* add Frontmatter CMS * add biome * update * update * fixed & add docs * fix translation.ts * fix translation
This commit is contained in:
@@ -11,6 +11,7 @@ interface Props {
|
||||
image: string;
|
||||
description: string;
|
||||
words: number;
|
||||
draft: boolean;
|
||||
}
|
||||
const { entry, title, url, published, tags, category, image, description, words } = Astro.props;
|
||||
const className = Astro.props.class;
|
||||
|
||||
@@ -1,53 +1,60 @@
|
||||
import type {LicenseConfig, NavBarConfig, ProfileConfig, SiteConfig} from "./types/config.ts";
|
||||
import {LinkPreset} from "./types/config.ts";
|
||||
import type {
|
||||
LicenseConfig,
|
||||
NavBarConfig,
|
||||
ProfileConfig,
|
||||
SiteConfig,
|
||||
} from './types/config.ts'
|
||||
import { LinkPreset } from './types/config.ts'
|
||||
|
||||
export const siteConfig: SiteConfig = {
|
||||
title: 'Fuwari',
|
||||
subtitle: 'Demo Site',
|
||||
lang: 'en',
|
||||
themeHue: 250,
|
||||
banner: {
|
||||
enable: true,
|
||||
src: 'assets/images/demo-banner.png',
|
||||
}
|
||||
title: 'Fuwari',
|
||||
subtitle: 'Demo Site',
|
||||
lang: 'en',
|
||||
themeHue: 250,
|
||||
banner: {
|
||||
enable: true,
|
||||
src: 'assets/images/demo-banner.png',
|
||||
},
|
||||
}
|
||||
|
||||
export const navBarConfig: NavBarConfig = {
|
||||
links: [
|
||||
LinkPreset.Home,
|
||||
LinkPreset.Archive,
|
||||
LinkPreset.About,
|
||||
{
|
||||
name: 'GitHub',
|
||||
url: 'https://github.com/saicaca/fuwari',
|
||||
external: true,
|
||||
}
|
||||
]
|
||||
links: [
|
||||
LinkPreset.Home,
|
||||
LinkPreset.Archive,
|
||||
LinkPreset.About,
|
||||
{
|
||||
name: 'GitHub',
|
||||
url: 'https://github.com/saicaca/fuwari',
|
||||
external: true,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const profileConfig: ProfileConfig = {
|
||||
avatar: 'assets/images/demo-avatar.png',
|
||||
name: 'Lorem Ipsum',
|
||||
bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
links: [
|
||||
{
|
||||
name: 'Twitter',
|
||||
icon: 'fa6-brands:twitter',
|
||||
url: 'https://twitter.com',
|
||||
}, {
|
||||
name: 'Steam',
|
||||
icon: 'fa6-brands:steam',
|
||||
url: 'https://store.steampowered.com',
|
||||
}, {
|
||||
name: 'GitHub',
|
||||
icon: 'fa6-brands:github',
|
||||
url: 'https://github.com/saicaca/fuwari',
|
||||
}
|
||||
]
|
||||
avatar: 'assets/images/demo-avatar.png',
|
||||
name: 'Lorem Ipsum',
|
||||
bio: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
||||
links: [
|
||||
{
|
||||
name: 'Twitter',
|
||||
icon: 'fa6-brands:twitter',
|
||||
url: 'https://twitter.com',
|
||||
},
|
||||
{
|
||||
name: 'Steam',
|
||||
icon: 'fa6-brands:steam',
|
||||
url: 'https://store.steampowered.com',
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
icon: 'fa6-brands:github',
|
||||
url: 'https://github.com/saicaca/fuwari',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
export const licenseConfig: LicenseConfig = {
|
||||
enable: true,
|
||||
name: 'CC BY-NC-SA 4.0',
|
||||
url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
|
||||
}
|
||||
enable: true,
|
||||
name: 'CC BY-NC-SA 4.0',
|
||||
url: 'https://creativecommons.org/licenses/by-nc-sa/4.0/',
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { z, defineCollection } from "astro:content";
|
||||
import { defineCollection, z } from 'astro:content'
|
||||
|
||||
const blogCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
published: z.date(),
|
||||
description: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
category: z.string().optional(),
|
||||
})
|
||||
const postsCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
published: z.date(),
|
||||
draft: z.boolean(),
|
||||
description: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
category: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
export const collections = {
|
||||
'blog': blogCollection,
|
||||
}
|
||||
posts: postsCollection,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
title: 'Cover Image Example'
|
||||
title: "Cover Image Example"
|
||||
published: 2023-09-01
|
||||
description: 'How to set a cover image using the cover attribute.'
|
||||
image: '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'
|
||||
description: "How to set a cover image using the cover attribute."
|
||||
image: "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"
|
||||
tags: ["Fuwari", "Blogging", "Customization"]
|
||||
category:
|
||||
category: test
|
||||
draft: false
|
||||
---
|
||||
|
||||
## Set the cover image using the `image` attribute
|
||||
@@ -20,4 +21,5 @@ published: 2023-10-05
|
||||
image: "/images/my-cover-image.jpg"
|
||||
---
|
||||
```
|
||||
|
||||
Web URLs are also supported.
|
||||
|
||||
24
src/content/posts/draftexample/index.md
Normal file
24
src/content/posts/draftexample/index.md
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
title: Draft Example
|
||||
published: 2024-01-11T04:40:26.381Z
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Example
|
||||
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: Example
|
||||
draft: false
|
||||
---
|
||||
@@ -2,22 +2,21 @@
|
||||
title: Markdown Example
|
||||
published: 2023-10-01
|
||||
description: A simple example of a Markdown blog post.
|
||||
image:
|
||||
tags: [Markdown, Blogging, Demo]
|
||||
category: Example
|
||||
draft: false
|
||||
---
|
||||
|
||||
An h1 header
|
||||
============
|
||||
# An h1 header
|
||||
|
||||
Paragraphs are separated by a blank line.
|
||||
|
||||
2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists
|
||||
2nd paragraph. _Italic_, **bold**, and `monospace`. Itemized lists
|
||||
look like:
|
||||
|
||||
* this one
|
||||
* that one
|
||||
* the other one
|
||||
- this one
|
||||
- that one
|
||||
- the other one
|
||||
|
||||
Note that --- not considering the asterisk --- the actual text
|
||||
content starts at 4-columns in.
|
||||
@@ -32,10 +31,7 @@ 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
|
||||
------------
|
||||
## An h2 header
|
||||
|
||||
Here's a numbered list:
|
||||
|
||||
@@ -52,50 +48,48 @@ from the left side). Here's a code sample:
|
||||
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
|
||||
```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 ###
|
||||
### An h3 header
|
||||
|
||||
Now a nested list:
|
||||
|
||||
1. First, get these ingredients:
|
||||
|
||||
* carrots
|
||||
* celery
|
||||
* lentils
|
||||
- carrots
|
||||
- celery
|
||||
- lentils
|
||||
|
||||
2. Boil some water.
|
||||
|
||||
3. Dump everything in the pot and follow
|
||||
this algorithm:
|
||||
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)
|
||||
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.
|
||||
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).
|
||||
@@ -108,32 +102,38 @@ doc](#an-h2-header). Here's a footnote [^1].
|
||||
|
||||
Tables can look like this:
|
||||
|
||||
size material color
|
||||
---- ------------ ------------
|
||||
9 leather brown
|
||||
10 hemp canvas natural
|
||||
11 glass transparent
|
||||
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
|
||||
---
|
||||
|
||||
keyword text
|
||||
|
||||
---
|
||||
|
||||
red Sunsets, apples, and
|
||||
other red or reddish
|
||||
things.
|
||||
|
||||
green Leaves, grass, frogs
|
||||
green Leaves, grass, frogs
|
||||
and other things it's
|
||||
not easy being.
|
||||
-------- -----------------------
|
||||
|
||||
---
|
||||
|
||||
A horizontal rule follows.
|
||||
|
||||
***
|
||||
---
|
||||
|
||||
Here's a definition list:
|
||||
|
||||
@@ -150,7 +150,7 @@ term/definition pair to spread things out more.)
|
||||
Here's a "line block":
|
||||
|
||||
| Line one
|
||||
| Line too
|
||||
| Line too
|
||||
| Line tree
|
||||
|
||||
and images can be specified like so:
|
||||
@@ -163,4 +163,4 @@ math should get its own line and be put in in double-dollarsigns:
|
||||
$$I = \int \rho R^{2} dV$$
|
||||
|
||||
And note that you can backslash-escape any punctuation characters
|
||||
which you wish to be displayed literally, ex.: \`foo\`, \*bar\*, etc.
|
||||
which you wish to be displayed literally, ex.: \`foo\`, \*bar\*, etc.
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
title: Include Video in the Posts
|
||||
published: 2022-08-01
|
||||
description: This post demonstrates how to include embedded video in a blog post.
|
||||
image:
|
||||
tags: [Example, Video]
|
||||
category: Example
|
||||
draft: false
|
||||
---
|
||||
|
||||
Just copy the embed code from YouTube or other platforms, and paste it in the markdown file.
|
||||
|
||||
```yaml
|
||||
@@ -19,7 +20,9 @@ published: 2023-10-19
|
||||
```
|
||||
|
||||
## 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>
|
||||
|
||||
<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>
|
||||
|
||||
2
src/env.d.ts
vendored
Normal file
2
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/// <reference types="astro/client" />
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
8
src/files.d.ts
vendored
8
src/files.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
declare module "*.yml" {
|
||||
const value: any;
|
||||
export default value;
|
||||
}
|
||||
declare module '*.yml' {
|
||||
const value: unknown
|
||||
export default value
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
enum I18nKey {
|
||||
home = "home",
|
||||
about = "about",
|
||||
archive = "archive",
|
||||
home = 'home',
|
||||
about = 'about',
|
||||
archive = 'archive',
|
||||
|
||||
tags = "tags",
|
||||
categories = "categories",
|
||||
recentPosts = "recentPosts",
|
||||
tags = 'tags',
|
||||
categories = 'categories',
|
||||
recentPosts = 'recentPosts',
|
||||
|
||||
comments = "comments",
|
||||
comments = 'comments',
|
||||
|
||||
untitled = "untitled",
|
||||
uncategorized = "uncategorized",
|
||||
noTags = "noTags",
|
||||
untitled = 'untitled',
|
||||
uncategorized = 'uncategorized',
|
||||
noTags = 'noTags',
|
||||
|
||||
wordCount = "wordCount",
|
||||
wordsCount = "wordsCount",
|
||||
minuteCount = "minuteCount",
|
||||
minutesCount = "minutesCount",
|
||||
postCount = "postCount",
|
||||
postsCount = "postsCount",
|
||||
wordCount = 'wordCount',
|
||||
wordsCount = 'wordsCount',
|
||||
minuteCount = 'minuteCount',
|
||||
minutesCount = 'minutesCount',
|
||||
postCount = 'postCount',
|
||||
postsCount = 'postsCount',
|
||||
|
||||
primaryColor = "primaryColor",
|
||||
primaryColor = 'primaryColor',
|
||||
|
||||
more = "more",
|
||||
more = 'more',
|
||||
|
||||
author = "author",
|
||||
publishedAt = "publishedAt",
|
||||
license = "license",
|
||||
author = 'author',
|
||||
publishedAt = 'publishedAt',
|
||||
license = 'license',
|
||||
}
|
||||
|
||||
export default I18nKey;
|
||||
export default I18nKey
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import type { Translation } from "../translation.ts";
|
||||
import Key from "../i18nKey.ts";
|
||||
import Key from '../i18nKey.ts'
|
||||
import type { Translation } from '../translation.ts'
|
||||
|
||||
export const en: Translation = {
|
||||
[Key.home]: "Home",
|
||||
[Key.about]: "About",
|
||||
[Key.archive]: "Archive",
|
||||
[Key.home]: 'Home',
|
||||
[Key.about]: 'About',
|
||||
[Key.archive]: 'Archive',
|
||||
|
||||
[Key.tags]: "Tags",
|
||||
[Key.categories]: "Categories",
|
||||
[Key.recentPosts]: "Recent Posts",
|
||||
[Key.tags]: 'Tags',
|
||||
[Key.categories]: 'Categories',
|
||||
[Key.recentPosts]: 'Recent Posts',
|
||||
|
||||
[Key.comments]: "Comments",
|
||||
[Key.comments]: 'Comments',
|
||||
|
||||
[Key.untitled]: "Untitled",
|
||||
[Key.uncategorized]: "Uncategorized",
|
||||
[Key.noTags]: "No Tags",
|
||||
[Key.untitled]: 'Untitled',
|
||||
[Key.uncategorized]: 'Uncategorized',
|
||||
[Key.noTags]: 'No Tags',
|
||||
|
||||
[Key.wordCount]: "word",
|
||||
[Key.wordsCount]: "words",
|
||||
[Key.minuteCount]: "minute",
|
||||
[Key.minutesCount]: "minutes",
|
||||
[Key.postCount]: "post",
|
||||
[Key.postsCount]: "posts",
|
||||
[Key.wordCount]: 'word',
|
||||
[Key.wordsCount]: 'words',
|
||||
[Key.minuteCount]: 'minute',
|
||||
[Key.minutesCount]: 'minutes',
|
||||
[Key.postCount]: 'post',
|
||||
[Key.postsCount]: 'posts',
|
||||
|
||||
[Key.primaryColor]: "Primary Color",
|
||||
[Key.primaryColor]: 'Primary Color',
|
||||
|
||||
[Key.more]: "More",
|
||||
[Key.more]: 'More',
|
||||
|
||||
[Key.author]: "Author",
|
||||
[Key.publishedAt]: "Published at",
|
||||
[Key.license]: "License",
|
||||
};
|
||||
[Key.author]: 'Author',
|
||||
[Key.publishedAt]: 'Published at',
|
||||
[Key.license]: 'License',
|
||||
}
|
||||
|
||||
33
src/i18n/languages/ja.ts
Normal file
33
src/i18n/languages/ja.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import Key from '../i18nKey.ts'
|
||||
import type { Translation } from '../translation.ts'
|
||||
|
||||
export const ja: Translation = {
|
||||
[Key.home]: 'Home',
|
||||
[Key.about]: 'About',
|
||||
[Key.archive]: 'Archive',
|
||||
|
||||
[Key.tags]: 'タグ',
|
||||
[Key.categories]: 'カテゴリ',
|
||||
[Key.recentPosts]: '最近の投稿',
|
||||
|
||||
[Key.comments]: 'コメント',
|
||||
|
||||
[Key.untitled]: 'タイトルなし',
|
||||
[Key.uncategorized]: 'カテゴリなし',
|
||||
[Key.noTags]: 'タグなし',
|
||||
|
||||
[Key.wordCount]: '文字',
|
||||
[Key.wordsCount]: '文字',
|
||||
[Key.minuteCount]: '分',
|
||||
[Key.minutesCount]: '分',
|
||||
[Key.postCount]: 'post',
|
||||
[Key.postsCount]: 'posts',
|
||||
|
||||
[Key.primaryColor]: '原色',
|
||||
|
||||
[Key.more]: 'もっと',
|
||||
|
||||
[Key.author]: '作者',
|
||||
[Key.publishedAt]: '公開日',
|
||||
[Key.license]: 'ライセンス',
|
||||
}
|
||||
@@ -1,33 +1,33 @@
|
||||
import type { Translation } from "../translation.ts";
|
||||
import Key from "../i18nKey.ts";
|
||||
import Key from '../i18nKey.ts'
|
||||
import type { Translation } from '../translation.ts'
|
||||
|
||||
export const zh_CN: Translation = {
|
||||
[Key.home]: "主页",
|
||||
[Key.about]: "关于",
|
||||
[Key.archive]: "归档",
|
||||
[Key.home]: '主页',
|
||||
[Key.about]: '关于',
|
||||
[Key.archive]: '归档',
|
||||
|
||||
[Key.tags]: "标签",
|
||||
[Key.categories]: "分类",
|
||||
[Key.recentPosts]: "最新文章",
|
||||
[Key.tags]: '标签',
|
||||
[Key.categories]: '分类',
|
||||
[Key.recentPosts]: '最新文章',
|
||||
|
||||
[Key.comments]: "评论",
|
||||
[Key.comments]: '评论',
|
||||
|
||||
[Key.untitled]: "无标题",
|
||||
[Key.uncategorized]: "未分类",
|
||||
[Key.noTags]: "无标签",
|
||||
[Key.untitled]: '无标题',
|
||||
[Key.uncategorized]: '未分类',
|
||||
[Key.noTags]: '无标签',
|
||||
|
||||
[Key.wordCount]: "字",
|
||||
[Key.wordsCount]: "字",
|
||||
[Key.minuteCount]: "分钟",
|
||||
[Key.minutesCount]: "分钟",
|
||||
[Key.postCount]: "篇文章",
|
||||
[Key.postsCount]: "篇文章",
|
||||
[Key.wordCount]: '字',
|
||||
[Key.wordsCount]: '字',
|
||||
[Key.minuteCount]: '分钟',
|
||||
[Key.minutesCount]: '分钟',
|
||||
[Key.postCount]: '篇文章',
|
||||
[Key.postsCount]: '篇文章',
|
||||
|
||||
[Key.primaryColor]: "主题色",
|
||||
[Key.primaryColor]: '主题色',
|
||||
|
||||
[Key.more]: "更多",
|
||||
[Key.more]: '更多',
|
||||
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "发布于",
|
||||
[Key.license]: "许可协议",
|
||||
};
|
||||
[Key.author]: '作者',
|
||||
[Key.publishedAt]: '发布于',
|
||||
[Key.license]: '许可协议',
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import type { Translation } from "../translation.ts";
|
||||
import Key from "../i18nKey.ts";
|
||||
import Key from '../i18nKey.ts'
|
||||
import type { Translation } from '../translation.ts'
|
||||
|
||||
export const zh_TW: Translation = {
|
||||
[Key.home]: "首頁",
|
||||
[Key.about]: "關於",
|
||||
[Key.archive]: "彙整",
|
||||
[Key.home]: '首頁',
|
||||
[Key.about]: '關於',
|
||||
[Key.archive]: '彙整',
|
||||
|
||||
[Key.tags]: "標籤",
|
||||
[Key.categories]: "分類",
|
||||
[Key.recentPosts]: "最新文章",
|
||||
[Key.tags]: '標籤',
|
||||
[Key.categories]: '分類',
|
||||
[Key.recentPosts]: '最新文章',
|
||||
|
||||
[Key.comments]: "評論",
|
||||
[Key.comments]: '評論',
|
||||
|
||||
[Key.untitled]: "無標題",
|
||||
[Key.uncategorized]: "未分類",
|
||||
[Key.noTags]: "無標籤",
|
||||
[Key.untitled]: '無標題',
|
||||
[Key.uncategorized]: '未分類',
|
||||
[Key.noTags]: '無標籤',
|
||||
|
||||
[Key.wordCount]: "字",
|
||||
[Key.wordsCount]: "字",
|
||||
[Key.minuteCount]: "分鐘",
|
||||
[Key.minutesCount]: "分鐘",
|
||||
[Key.postCount]: "篇文章",
|
||||
[Key.postsCount]: "篇文章",
|
||||
[Key.wordCount]: '字',
|
||||
[Key.wordsCount]: '字',
|
||||
[Key.minuteCount]: '分鐘',
|
||||
[Key.minutesCount]: '分鐘',
|
||||
[Key.postCount]: '篇文章',
|
||||
[Key.postsCount]: '篇文章',
|
||||
|
||||
[Key.primaryColor]: "主題色",
|
||||
[Key.primaryColor]: '主題色',
|
||||
|
||||
[Key.more]: "更多",
|
||||
[Key.more]: '更多',
|
||||
|
||||
[Key.author]: "作者",
|
||||
[Key.publishedAt]: "發佈於",
|
||||
[Key.license]: "許可協議",
|
||||
};
|
||||
[Key.author]: '作者',
|
||||
[Key.publishedAt]: '發佈於',
|
||||
[Key.license]: '許可協議',
|
||||
}
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import {en} from "./languages/en.ts";
|
||||
import {zh_TW} from "./languages/zh_TW.ts";
|
||||
import {zh_CN} from "./languages/zh_CN.ts";
|
||||
import type I18nKey from "./i18nKey.ts";
|
||||
import {siteConfig} from "../config.ts";
|
||||
import { siteConfig } from '../config.ts'
|
||||
import type I18nKey from './i18nKey.ts'
|
||||
import { en } from './languages/en.ts'
|
||||
import { ja } from './languages/ja.ts'
|
||||
import { zh_CN } from './languages/zh_CN.ts'
|
||||
import { zh_TW } from './languages/zh_TW.ts'
|
||||
|
||||
export type Translation = {
|
||||
[K in I18nKey]: string;
|
||||
[K in I18nKey]: string
|
||||
}
|
||||
|
||||
const defaultTranslation = en;
|
||||
const defaultTranslation = en
|
||||
|
||||
const map: { [key: string]: Translation } = {
|
||||
"en": en,
|
||||
"en_us": en,
|
||||
"en_gb": en,
|
||||
"en_au": en,
|
||||
"zh_cn": zh_CN,
|
||||
"zh_tw": zh_TW,
|
||||
en: en,
|
||||
en_us: en,
|
||||
en_gb: en,
|
||||
en_au: en,
|
||||
zh_cn: zh_CN,
|
||||
zh_tw: zh_TW,
|
||||
ja: ja,
|
||||
ja_jp: ja,
|
||||
}
|
||||
|
||||
export function getTranslation(lang: string): Translation {
|
||||
lang = lang.toLowerCase();
|
||||
return map[lang] || defaultTranslation;
|
||||
lang = lang.toLowerCase()
|
||||
return map[lang] || defaultTranslation
|
||||
}
|
||||
|
||||
export function i18n(key: I18nKey): string {
|
||||
const lang = siteConfig.lang || "en";
|
||||
return getTranslation(lang)[key];
|
||||
}
|
||||
const lang = siteConfig.lang || 'en'
|
||||
return getTranslation(lang)[key]
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
---
|
||||
import GlobalStyles from "../components/GlobalStyles.astro";
|
||||
import GlobalStyles from "@components/GlobalStyles.astro";
|
||||
import '@fontsource/roboto/400.css';
|
||||
import '@fontsource/roboto/500.css';
|
||||
import '@fontsource/roboto/700.css';
|
||||
import { ViewTransitions } from 'astro:transitions';
|
||||
import ImageBox from "../components/misc/ImageBox.astro";
|
||||
import ImageBox from "@components/misc/ImageBox.astro";
|
||||
|
||||
import { fade } from 'astro:transitions';
|
||||
import {pathsEqual} from "../utils/url-utils";
|
||||
import ConfigCarrier from "../components/ConfigCarrier.astro";
|
||||
import {siteConfig} from "../config";
|
||||
import {pathsEqual} from "@utils/url-utils";
|
||||
import ConfigCarrier from "@components/ConfigCarrier.astro";
|
||||
import {siteConfig} from "@/config";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
import Layout from "./Layout.astro";
|
||||
import Navbar from "../components/Navbar.astro";
|
||||
import SideBar from "../components/widget/SideBar.astro";
|
||||
import {pathsEqual} from "../utils/url-utils";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import BackToTop from "../components/control/BackToTop.astro";
|
||||
import DisplaySetting from "../components/widget/DisplaySetting.astro";
|
||||
import {siteConfig} from "../config";
|
||||
import Navbar from "@components/Navbar.astro";
|
||||
import SideBar from "@components/widget/SideBar.astro";
|
||||
import {pathsEqual} from "@utils/url-utils";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import BackToTop from "@components/control/BackToTop.astro";
|
||||
import DisplaySetting from "@components/widget/DisplaySetting.astro";
|
||||
import {siteConfig} from "@/config";
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
|
||||
@@ -5,7 +5,7 @@ import MainGridLayout from "../layouts/MainGridLayout.astro";
|
||||
import { getEntry } from 'astro:content'
|
||||
import {i18n} from "../i18n/translation";
|
||||
import I18nKey from "../i18n/i18nKey";
|
||||
import Markdown from "../components/misc/Markdown.astro";
|
||||
import Markdown from "@components/misc/Markdown.astro";
|
||||
|
||||
const aboutPost = await getEntry('spec', 'about')
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
import {getCategoryList, getSortedPosts} from "../../../utils/content-utils";
|
||||
import MainGridLayout from "../../../layouts/MainGridLayout.astro";
|
||||
import ArchivePanel from "../../../components/ArchivePanel.astro";
|
||||
import {i18n} from "../../../i18n/translation";
|
||||
import I18nKey from "../../../i18n/i18nKey";
|
||||
import {getCategoryList, getSortedPosts} from "@utils/content-utils";
|
||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||||
import ArchivePanel from "@components/ArchivePanel.astro";
|
||||
import {i18n} from "@i18n/translation";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
|
||||
|
||||
export async function getStaticPaths() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
import { getCollection, getEntry } from "astro:content";
|
||||
import MainGridLayout from "../../layouts/MainGridLayout.astro";
|
||||
import ArchivePanel from "../../components/ArchivePanel.astro";
|
||||
import {i18n} from "../../i18n/translation";
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||||
import ArchivePanel from "@components/ArchivePanel.astro";
|
||||
import {i18n} from "@i18n/translation";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
---
|
||||
|
||||
<MainGridLayout title={i18n(I18nKey.archive)}>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
---
|
||||
|
||||
import {getSortedPosts} from "../../../utils/content-utils";
|
||||
import MainGridLayout from "../../../layouts/MainGridLayout.astro";
|
||||
import ArchivePanel from "../../../components/ArchivePanel.astro";
|
||||
import {i18n} from "../../../i18n/translation";
|
||||
import I18nKey from "../../../i18n/i18nKey";
|
||||
import {getSortedPosts} from "@utils/content-utils";
|
||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||||
import ArchivePanel from "@components/ArchivePanel.astro";
|
||||
import {i18n} from "@i18n/translation";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
|
||||
|
||||
export async function getStaticPaths() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
---
|
||||
import MainGridLayout from "../../layouts/MainGridLayout.astro";
|
||||
import TitleCard from "../../components/TitleCardNew.astro";
|
||||
import Pagination from "../../components/control/Pagination.astro";
|
||||
import {getSortedPosts} from "../../utils/content-utils";
|
||||
import {getPostUrlBySlug} from "../../utils/url-utils";
|
||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||||
import TitleCard from "@components/TitleCardNew.astro";
|
||||
import Pagination from "@components/control/Pagination.astro";
|
||||
import {getSortedPosts} from "@utils/content-utils";
|
||||
import {getPostUrlBySlug} from "@utils/url-utils";
|
||||
|
||||
export async function getStaticPaths({ paginate }) {
|
||||
const allBlogPosts = await getSortedPosts();
|
||||
@@ -17,18 +17,35 @@ const {page} = Astro.props;
|
||||
<!-- 显示当前页面。也可以使用 Astro.params.page -->
|
||||
<MainGridLayout>
|
||||
<div class="flex flex-col gap-4 mb-4">
|
||||
{page.data.map(entry =>
|
||||
<TitleCard
|
||||
entry={entry}
|
||||
title={entry.data.title}
|
||||
tags={entry.data.tags}
|
||||
category={entry.data.category}
|
||||
published={entry.data.published}
|
||||
url={getPostUrlBySlug(entry.slug)}
|
||||
image={entry.data.image}
|
||||
description={entry.data.description}
|
||||
></TitleCard>
|
||||
)}
|
||||
{page.data.map((entry: { data: { draft: boolean; title: string; tags: string[]; category: string; published: Date; image: string; description: string; }; slug: string; }) => {
|
||||
// ここで draft が true の場合は何もレンダリングしない
|
||||
if (import.meta.env.PROD && entry.data.draft) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TitleCard
|
||||
entry={entry}
|
||||
title={entry.data.title}
|
||||
tags={entry.data.tags}
|
||||
category={entry.data.category}
|
||||
published={entry.data.published}
|
||||
url={getPostUrlBySlug(entry.slug)}
|
||||
image={entry.data.image}
|
||||
description={entry.data.description}
|
||||
draft={entry.data.draft}
|
||||
></TitleCard>
|
||||
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Pagination class="mx-auto" page={page}></Pagination>
|
||||
</MainGridLayout>
|
||||
</MainGridLayout>
|
||||
|
||||
<script>
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("開発環境");
|
||||
} else {
|
||||
console.log("本番環境");
|
||||
}
|
||||
</script>
|
||||
@@ -1,19 +1,21 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import MainGridLayout from "../../layouts/MainGridLayout.astro";
|
||||
import ImageBox from "../../components/misc/ImageBox.astro";
|
||||
import MainGridLayout from "@layouts/MainGridLayout.astro";
|
||||
import ImageBox from "@components/misc/ImageBox.astro";
|
||||
import {Icon} from "astro-icon/components";
|
||||
import PostMetadata from "../../components/PostMetadata.astro";
|
||||
import Button from "../../components/control/Button.astro";
|
||||
import {i18n} from "../../i18n/translation";
|
||||
import I18nKey from "../../i18n/i18nKey";
|
||||
import {getPostUrlBySlug} from "../../utils/url-utils";
|
||||
import License from "../../components/misc/License.astro";
|
||||
import {licenseConfig} from "../../config";
|
||||
import Markdown from "../../components/misc/Markdown.astro";
|
||||
import PostMetadata from "@components/PostMetadata.astro";
|
||||
import Button from "@components/control/Button.astro";
|
||||
import {i18n} from "@i18n/translation";
|
||||
import I18nKey from "@i18n/i18nKey";
|
||||
import {getPostUrlBySlug} from "@utils/url-utils";
|
||||
import License from "@components/misc/License.astro";
|
||||
import {licenseConfig} from "src/config";
|
||||
import Markdown from "@components/misc/Markdown.astro";
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const blogEntries = await getCollection('posts');
|
||||
const blogEntries = await getCollection('posts', ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
return blogEntries.map(entry => ({
|
||||
params: { slug: entry.slug }, props: { entry },
|
||||
}));
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import getReadingTime from 'reading-time';
|
||||
import { toString } from 'mdast-util-to-string';
|
||||
// biome-ignore lint/suspicious/noShadowRestrictedNames: <explanation>
|
||||
import { toString } from 'mdast-util-to-string'
|
||||
import getReadingTime from 'reading-time'
|
||||
|
||||
export function remarkReadingTime() {
|
||||
return function (tree, { data }) {
|
||||
const textOnPage = toString(tree);
|
||||
const readingTime = getReadingTime(textOnPage);
|
||||
data.astro.frontmatter.minutes = Math.max(1, Math.round(readingTime.minutes));
|
||||
data.astro.frontmatter.words = readingTime.words;
|
||||
};
|
||||
}
|
||||
return (tree, { data }) => {
|
||||
const textOnPage = toString(tree)
|
||||
const readingTime = getReadingTime(textOnPage)
|
||||
data.astro.frontmatter.minutes = Math.max(
|
||||
1,
|
||||
Math.round(readingTime.minutes),
|
||||
)
|
||||
data.astro.frontmatter.words = readingTime.words
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
export type SiteConfig = {
|
||||
title: string,
|
||||
subtitle: string,
|
||||
title: string
|
||||
subtitle: string
|
||||
|
||||
lang: string,
|
||||
lang: string
|
||||
|
||||
themeHue: number,
|
||||
banner: {
|
||||
enable: boolean,
|
||||
src: string,
|
||||
}
|
||||
};
|
||||
themeHue: number
|
||||
banner: {
|
||||
enable: boolean
|
||||
src: string
|
||||
}
|
||||
}
|
||||
|
||||
export enum LinkPreset {
|
||||
Home,
|
||||
Archive,
|
||||
About,
|
||||
Home = 0,
|
||||
Archive = 1,
|
||||
About = 2,
|
||||
}
|
||||
|
||||
export type NavBarLink = {
|
||||
name: string,
|
||||
url: string,
|
||||
external?: boolean
|
||||
name: string
|
||||
url: string
|
||||
external?: boolean
|
||||
}
|
||||
|
||||
export type NavBarConfig = {
|
||||
links: (NavBarLink | LinkPreset)[],
|
||||
links: (NavBarLink | LinkPreset)[]
|
||||
}
|
||||
|
||||
export type ProfileConfig = {
|
||||
avatar?: string,
|
||||
name: string,
|
||||
bio?: string,
|
||||
links: {
|
||||
name: string,
|
||||
url: string,
|
||||
icon: string,
|
||||
}[],
|
||||
};
|
||||
avatar?: string
|
||||
name: string
|
||||
bio?: string
|
||||
links: {
|
||||
name: string
|
||||
url: string
|
||||
icon: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export type LicenseConfig = {
|
||||
enable: boolean;
|
||||
name: string,
|
||||
url: string,
|
||||
}
|
||||
enable: boolean
|
||||
name: string
|
||||
url: string
|
||||
}
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
import {CollectionEntry, getCollection} from "astro:content";
|
||||
import { getCollection } from 'astro:content'
|
||||
|
||||
export async function getSortedPosts() {
|
||||
const allBlogPosts = await getCollection("posts");
|
||||
const sorted = allBlogPosts.sort((a, b) => {
|
||||
const dateA = new Date(a.data.published);
|
||||
const dateB = new Date(b.data.published);
|
||||
return dateA > dateB ? -1 : 1;
|
||||
});
|
||||
const allBlogPosts = await getCollection('posts')
|
||||
const sorted = allBlogPosts.sort((a, b) => {
|
||||
const dateA = new Date(a.data.published)
|
||||
const dateB = new Date(b.data.published)
|
||||
return dateA > dateB ? -1 : 1
|
||||
})
|
||||
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
sorted[i].data.nextSlug = sorted[i - 1].slug;
|
||||
sorted[i].data.nextTitle = sorted[i - 1].data.title;
|
||||
}
|
||||
for (let i = 0; i < sorted.length - 1; i++) {
|
||||
sorted[i].data.prevSlug = sorted[i + 1].slug;
|
||||
sorted[i].data.prevTitle = sorted[i + 1].data.title;
|
||||
}
|
||||
for (let i = 1; i < sorted.length; i++) {
|
||||
sorted[i].data.nextSlug = sorted[i - 1].slug
|
||||
sorted[i].data.nextTitle = sorted[i - 1].data.title
|
||||
}
|
||||
for (let i = 0; i < sorted.length - 1; i++) {
|
||||
sorted[i].data.prevSlug = sorted[i + 1].slug
|
||||
sorted[i].data.prevTitle = sorted[i + 1].data.title
|
||||
}
|
||||
|
||||
return sorted;
|
||||
return sorted
|
||||
}
|
||||
|
||||
export type Tag = {
|
||||
name: string;
|
||||
count: number;
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export async function getTagList(): Promise<Tag[]> {
|
||||
const allBlogPosts = await getCollection("posts");
|
||||
const allBlogPosts = await getCollection('posts')
|
||||
|
||||
const countMap: { [key: string]: number } = {};
|
||||
allBlogPosts.map((post) => {
|
||||
post.data.tags.map((tag: string) => {
|
||||
if (!countMap[tag]) countMap[tag] = 0;
|
||||
countMap[tag]++;
|
||||
})
|
||||
});
|
||||
const countMap: { [key: string]: number } = {}
|
||||
allBlogPosts.map(post => {
|
||||
post.data.tags.map((tag: string) => {
|
||||
if (!countMap[tag]) countMap[tag] = 0
|
||||
countMap[tag]++
|
||||
})
|
||||
})
|
||||
|
||||
// sort tags
|
||||
const keys: string[] = Object.keys(countMap).sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
// sort tags
|
||||
const keys: string[] = Object.keys(countMap).sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase())
|
||||
})
|
||||
|
||||
return keys.map((key) => ({name: key, count: countMap[key]}));
|
||||
return keys.map(key => ({ name: key, count: countMap[key] }))
|
||||
}
|
||||
|
||||
export type Category = {
|
||||
name: string;
|
||||
count: number;
|
||||
name: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export async function getCategoryList(): Promise<Category[]> {
|
||||
const allBlogPosts = await getCollection("posts");
|
||||
let count : {[key: string]: number} = {};
|
||||
allBlogPosts.map((post) => {
|
||||
if (!post.data.category) {
|
||||
return;
|
||||
}
|
||||
if (!count[post.data.category]) {
|
||||
count[post.data.category] = 0;
|
||||
}
|
||||
count[post.data.category]++;
|
||||
});
|
||||
|
||||
let lst = Object.keys(count).sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase());
|
||||
});
|
||||
|
||||
let ret : Category[] = [];
|
||||
for (const c of lst) {
|
||||
ret.push({name: c, count: count[c]});
|
||||
const allBlogPosts = await getCollection('posts')
|
||||
const count: { [key: string]: number } = {}
|
||||
allBlogPosts.map(post => {
|
||||
if (!post.data.category) {
|
||||
return
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
if (!count[post.data.category]) {
|
||||
count[post.data.category] = 0
|
||||
}
|
||||
count[post.data.category]++
|
||||
})
|
||||
|
||||
const lst = Object.keys(count).sort((a, b) => {
|
||||
return a.toLowerCase().localeCompare(b.toLowerCase())
|
||||
})
|
||||
|
||||
const ret: Category[] = []
|
||||
for (const c of lst) {
|
||||
ret.push({ name: c, count: count[c] })
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export function formatDateToYYYYMMDD(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0');
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = date.getDate().toString().padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day}`;
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
|
||||
export function pathsEqual(path1: string, path2: string) {
|
||||
const normalizedPath1 = path1.replace(/^\/|\/$/g, '').toLowerCase();
|
||||
const normalizedPath2 = path2.replace(/^\/|\/$/g, '').toLowerCase();
|
||||
return normalizedPath1 === normalizedPath2;
|
||||
const normalizedPath1 = path1.replace(/^\/|\/$/g, '').toLowerCase()
|
||||
const normalizedPath2 = path2.replace(/^\/|\/$/g, '').toLowerCase()
|
||||
return normalizedPath1 === normalizedPath2
|
||||
}
|
||||
|
||||
function joinUrl(...parts: string[]): string {
|
||||
const joined = parts.join('/');
|
||||
return joined.replace(/([^:]\/)\/+/g, '$1');
|
||||
const joined = parts.join('/')
|
||||
return joined.replace(/([^:]\/)\/+/g, '$1')
|
||||
}
|
||||
|
||||
export function getPostUrlBySlug(slug: string): string | null {
|
||||
if (!slug)
|
||||
return null;
|
||||
return `/posts/${slug}`;
|
||||
if (!slug) return null
|
||||
return `/posts/${slug}`
|
||||
}
|
||||
|
||||
export function getCategoryUrl(category: string): string | null {
|
||||
if (!category)
|
||||
return null;
|
||||
return `/archive/category/${category}`;
|
||||
if (!category) return null
|
||||
return `/archive/category/${category}`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user