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:
L4Ph 2024-01-21 13:54:41 +09:00 committed by GitHub
parent f9a78b3e3b
commit 197d524b53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 2714 additions and 12795 deletions

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1,14 @@
{
"taxonomy": {
"tags": [
"Blogging",
"Customization",
"Demo",
"Example",
"Fuwari",
"Markdown",
"Video"
],
"categories": []
}
}

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome", "astro-build.astro-vscode"]
}

22
.vscode/settings.json vendored Normal file
View File

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

View File

@ -25,7 +25,7 @@ Fuwari (not the final name maybe) is a static blog template built with [Astro](h
1. [Generate a new repository](https://github.com/saicaca/fuwari/generate) from this template.
2. Edit the config file `src/config.ts` to customize your blog.
3. Run `npm run new-post -- <filename>` to create a new post and edit it in `src/content/posts/`.
3. Run `pnpm run new-post -- <filename>` to create a new post and edit it in `src/content/posts/`.
4. Deploy your blog to Vercel, Netlify, GitHub Pages, etc. following [the guides](https://docs.astro.build/en/guides/deploy/).
## ⚙️ Frontmatter of Posts
@ -47,10 +47,10 @@ All commands are run from the root of the project, from a terminal:
| Command | Action |
|:---------------------------------|:-------------------------------------------------|
| `npm install` | Installs dependencies |
| `npm run dev` | Starts local dev server at `localhost:4321` |
| `npm run build` | Build your production site to `./dist/` |
| `npm run preview` | Preview your build locally, before deploying |
| `npm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `npm run astro -- --help` | Get help using the Astro CLI |
| `npm run new-post -- <filename>` | Create a new post |
| `pnpm install` | Installs dependencies |
| `pnpm run dev` | Starts local dev server at `localhost:4321` |
| `pnpm run build` | Build your production site to `./dist/` |
| `pnpm run preview` | Preview your build locally, before deploying |
| `pnpm run astro ...` | Run CLI commands like `astro add`, `astro check` |
| `pnpm run astro -- --help` | Get help using the Astro CLI |
| `pnpm run new-post -- <filename>` | Create a new post |

View File

@ -1,57 +1,74 @@
import { defineConfig } from 'astro/config';
import yaml from '@rollup/plugin-yaml';
import icon from "astro-icon";
import tailwind from "@astrojs/tailwind"
import yaml from "@rollup/plugin-yaml"
import Compress from "astro-compress"
import icon from "astro-icon"
import { defineConfig } from "astro/config"
import Color from "colorjs.io"
import rehypeAutolinkHeadings from "rehype-autolink-headings"
import rehypeKatex from "rehype-katex"
import rehypeSlug from "rehype-slug"
import remarkMath from "remark-math"
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"
import tailwind from "@astrojs/tailwind";
import {remarkReadingTime} from "./src/plugins/remark-reading-time.mjs";
import rehypeKatex from "rehype-katex";
import Color from 'colorjs.io';
import remarkMath from "remark-math";
import rehypeAutolinkHeadings from "rehype-autolink-headings";
import rehypeSlug from "rehype-slug";
// https://astro.build/config
const oklchToHex = function (str) {
const DEFAULT_HUE = 250;
const regex = /-?\d+(\.\d+)?/g;
const matches = str.string.match(regex);
const lch = [matches[0], matches[1], DEFAULT_HUE];
return new Color("oklch", lch).to("srgb").toString({format: "hex"});
const oklchToHex = (str) => {
const DEFAULT_HUE = 250
const regex = /-?\d+(\.\d+)?/g
const matches = str.string.match(regex)
const lch = [matches[0], matches[1], DEFAULT_HUE]
return new Color("oklch", lch).to("srgb").toString({
format: "hex",
})
}
// https://astro.build/config
export default defineConfig({
site: 'https://fuwari.vercel.app/',
base: '/',
site: "https://fuwari.vercel.app/",
base: "/",
integrations: [
tailwind(),
icon({
include: {
'material-symbols': ['*'],
'fa6-brands': ['*'],
'fa6-regular': ['*'],
'fa6-solid': ['*']
}
})
"material-symbols": ["*"],
"fa6-brands": ["*"],
"fa6-regular": ["*"],
"fa6-solid": ["*"],
},
}),
Compress({
Image: false,
}),
],
markdown: {
remarkPlugins: [remarkMath, remarkReadingTime],
rehypePlugins: [rehypeKatex, rehypeSlug,
[rehypeAutolinkHeadings, {
behavior: 'append',
properties: {className: ['anchor']},
rehypePlugins: [
rehypeKatex,
rehypeSlug,
[
rehypeAutolinkHeadings,
{
behavior: "append",
properties: {
className: ["anchor"],
},
content: {
type: 'element',
tagName: 'span',
properties: {className: ['anchor-icon']},
children: [{type: 'text', value: '#'}]
}}]]
type: "element",
tagName: "span",
properties: {
className: ["anchor-icon"],
},
children: [
{
type: "text",
value: "#",
},
],
},
},
],
],
},
redirects: {
'/': '/page/1',
"/": "/page/1",
},
vite: {
plugins: [yaml()],
@ -59,10 +76,10 @@ export default defineConfig({
preprocessorOptions: {
stylus: {
define: {
oklchToHex: oklchToHex
}
}
}
}
oklchToHex: oklchToHex,
},
});
},
},
},
},
})

66
biome.json Normal file
View File

@ -0,0 +1,66 @@
{
"$schema": "https://biomejs.dev/schemas/1.4.1/schema.json",
"extends": [],
"files": { "ignoreUnknown": true },
"organizeImports": {
"enabled": true
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"ignore": [],
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"javascript": {
"parser": {
"unsafeParameterDecoratorsEnabled": true
},
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "single",
"trailingComma": "all",
"semicolons": "asNeeded",
"arrowParentheses": "asNeeded"
}
},
"json": {
"parser": { "allowComments": true },
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
}
},
"linter": {
"ignore": [],
"rules": {
"a11y": {
"recommended": true
},
"complexity": {
"recommended": true
},
"correctness": {
"recommended": true
},
"performance": {
"recommended": true
},
"security": {
"recommended": true
},
"style": {
"recommended": true
},
"suspicious": {
"recommended": true
},
"nursery": {
"recommended": true
}
}
}
}

62
frontmatter.json Normal file
View File

@ -0,0 +1,62 @@
{
"$schema": "https://frontmatter.codes/frontmatter.schema.json",
"frontMatter.framework.id": "astro",
"frontMatter.preview.host": "http://localhost:4321",
"frontMatter.content.publicFolder": "public",
"frontMatter.content.pageFolders": [
{
"title": "posts",
"path": "[[workspace]]/src/content/posts"
}
],
"frontMatter.taxonomy.contentTypes": [
{
"name": "default",
"pageBundle": true,
"previewPath": "'blog'",
"filePrefix": null,
"clearEmpty": true,
"fields": [
{
"title": "title",
"name": "title",
"type": "string",
"single": true
},
{
"title": "description",
"name": "description",
"type": "string"
},
{
"title": "published",
"name": "published",
"type": "datetime",
"default": "{{now}}",
"isPublishDate": true
},
{
"title": "preview",
"name": "image",
"type": "image",
"isPreviewImage": true
},
{
"title": "tags",
"name": "tags",
"type": "list"
},
{
"title": "category",
"name": "category",
"type": "string"
},
{
"title": "draft",
"name": "draft",
"type": "boolean"
}
]
}
]
}

10688
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -8,37 +8,41 @@
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"new-post": "node scripts/new-post.js"
"new-post": "node scripts/new-post.js",
"format": "biome format --write ./src",
"lint": "biome check --apply ./src"
},
"dependencies": {
"@astrojs/check": "^0.2.0",
"@astrojs/tailwind": "^5.0.2",
"@astrojs/vue": "^3.0.1",
"@fontsource-variable/jetbrains-mono": "^5.0.18",
"@astrojs/check": "^0.3.4",
"@astrojs/tailwind": "^5.1.0",
"@astrojs/vue": "^4.0.8",
"@fontsource-variable/jetbrains-mono": "^5.0.19",
"@fontsource/roboto": "^5.0.8",
"astro": "^3.5.0",
"astro-icon": "1.0.0-next.2",
"astro": "^4.1.1",
"astro-icon": "1.0.2",
"colorjs.io": "^0.4.5",
"mdast-util-to-string": "^4.0.0",
"overlayscrollbars": "^2.4.4",
"overlayscrollbars": "^2.4.6",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-katex": "^7.0.0",
"rehype-slug": "^6.0.0",
"remark-math": "^6.0.0",
"sharp": "^0.32.6",
"tailwindcss": "^3.3.3",
"tailwindcss": "^3.3.7",
"typescript": "^5.2.2",
"valine": "^1.5.1"
},
"devDependencies": {
"@astrojs/ts-plugin": "^1.1.3",
"@iconify-json/fa6-brands": "^1.1.13",
"@iconify-json/fa6-regular": "^1.1.13",
"@iconify-json/fa6-solid": "^1.1.15",
"@iconify-json/material-symbols": "^1.1.59",
"@astrojs/ts-plugin": "^1.3.1",
"@biomejs/biome": "1.4.1",
"@iconify-json/fa6-brands": "^1.1.18",
"@iconify-json/fa6-regular": "^1.1.18",
"@iconify-json/fa6-solid": "^1.1.20",
"@iconify-json/material-symbols": "^1.1.69",
"@rollup/plugin-yaml": "^4.1.2",
"@tailwindcss/typography": "^0.5.10",
"astro-compress": "github:astro-community/AstroCompress#no-sharp",
"stylus": "^0.59.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +1,40 @@
import fs from 'fs';
import path from 'path';
import fs from "fs"
import path from "path"
function getDate() {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0'); //月份从0开始所以要加1
const day = String(today.getDate()).padStart(2, '0');
const today = new Date()
const year = today.getFullYear()
const month = String(today.getMonth() + 1).padStart(2, "0") //月份从0开始所以要加1
const day = String(today.getDate()).padStart(2, "0")
return `${year}-${month}-${day}`;
return `${year}-${month}-${day}`
}
const args = process.argv.slice(2);
const args = process.argv.slice(2)
if (args.length === 0) {
console.error(`Error: No filename argument provided
Usage: npm run new-post -- <filename>`);
process.exit(1); // Terminate the script and return error code 1
Usage: npm run new-post -- <filename>`)
process.exit(1) // Terminate the script and return error code 1
}
let fileName = args[0];
let fileName = args[0]
// Add .md extension if not present
const fileExtensionRegex = /\.(md|mdx)$/i;
const fileExtensionRegex = /\.(md|mdx)$/i
if (!fileExtensionRegex.test(fileName)) {
fileName += '.md';
fileName += ".md"
}
const targetDir = './src/content/posts/';
const fullPath = path.join(targetDir, fileName);
const targetDir = "./src/content/posts/"
const fullPath = path.join(targetDir, fileName)
if (fs.existsSync(fullPath)) {
console.error(`ErrorFile ${fullPath} already exists `);
process.exit(1);
console.error(`ErrorFile ${fullPath} already exists `)
process.exit(1)
}
const content =
`---
const content = `---
title: ${args[0]}
published: ${getDate()}
description:
@ -43,8 +42,8 @@ image:
tags: []
category:
---
`;
`
fs.writeFileSync(path.join(targetDir, fileName), content);
fs.writeFileSync(path.join(targetDir, fileName), content)
console.log(`Post ${fullPath} created`);
console.log(`Post ${fullPath} created`)

View File

@ -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;

View File

@ -1,5 +1,10 @@
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',
@ -9,7 +14,7 @@ export const siteConfig: SiteConfig = {
banner: {
enable: true,
src: 'assets/images/demo-banner.png',
}
},
}
export const navBarConfig: NavBarConfig = {
@ -21,8 +26,8 @@ export const navBarConfig: NavBarConfig = {
name: 'GitHub',
url: 'https://github.com/saicaca/fuwari',
external: true,
}
]
},
],
}
export const profileConfig: ProfileConfig = {
@ -34,16 +39,18 @@ export const profileConfig: ProfileConfig = {
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 = {

View File

@ -1,15 +1,16 @@
import { z, defineCollection } from "astro:content";
import { defineCollection, z } from 'astro:content'
const blogCollection = defineCollection({
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,
}

View File

@ -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.

View 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
---

View File

@ -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,35 +48,33 @@ 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.
@ -109,7 +103,9 @@ 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
@ -119,9 +115,12 @@ 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.
@ -129,11 +128,12 @@ things.
green Leaves, grass, frogs
and other things it's
not easy being.
-------- -----------------------
---
A horizontal rule follows.
***
---
Here's a definition list:

View File

@ -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>

2
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="../.astro/types.d.ts" />

6
src/files.d.ts vendored
View File

@ -1,4 +1,4 @@
declare module "*.yml" {
const value: any;
export default value;
declare module '*.yml' {
const value: unknown
export default value
}

View File

@ -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

View File

@ -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
View 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]: 'ライセンス',
}

View File

@ -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]: '许可协议',
}

View File

@ -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]: '許可協議',
}

View File

@ -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]
}

View File

@ -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;

View File

@ -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;

View File

@ -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')

View File

@ -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() {

View File

@ -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)}>

View File

@ -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() {

View File

@ -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,7 +17,13 @@ const {page} = Astro.props;
<!-- 显示当前页面。也可以使用 Astro.params.page -->
<MainGridLayout>
<div class="flex flex-col gap-4 mb-4">
{page.data.map(entry =>
{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}
@ -27,8 +33,19 @@ const {page} = Astro.props;
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>
<script>
if (import.meta.env.DEV) {
console.log("開発環境");
} else {
console.log("本番環境");
}
</script>

View File

@ -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 },
}));

View File

@ -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
}
}

View File

@ -1,45 +1,45 @@
export type SiteConfig = {
title: string,
subtitle: string,
title: string
subtitle: string
lang: string,
lang: string
themeHue: number,
themeHue: number
banner: {
enable: boolean,
src: string,
enable: boolean
src: string
}
}
};
export enum LinkPreset {
Home,
Archive,
About,
Home = 0,
Archive = 1,
About = 2,
}
export type NavBarLink = {
name: string,
url: string,
name: string
url: string
external?: boolean
}
export type NavBarConfig = {
links: (NavBarLink | LinkPreset)[],
links: (NavBarLink | LinkPreset)[]
}
export type ProfileConfig = {
avatar?: string,
name: string,
bio?: string,
avatar?: string
name: string
bio?: string
links: {
name: string,
url: string,
icon: string,
}[],
};
name: string
url: string
icon: string
}[]
}
export type LicenseConfig = {
enable: boolean;
name: string,
url: string,
enable: boolean
name: string
url: string
}

View File

@ -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 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 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;
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;
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) => {
const countMap: { [key: string]: number } = {}
allBlogPosts.map(post => {
post.data.tags.map((tag: string) => {
if (!countMap[tag]) countMap[tag] = 0;
countMap[tag]++;
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());
});
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) => {
const allBlogPosts = await getCollection('posts')
const count: { [key: string]: number } = {}
allBlogPosts.map(post => {
if (!post.data.category) {
return;
return
}
if (!count[post.data.category]) {
count[post.data.category] = 0;
count[post.data.category] = 0
}
count[post.data.category]++;
});
count[post.data.category]++
})
let lst = Object.keys(count).sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase());
});
const lst = Object.keys(count).sort((a, b) => {
return a.toLowerCase().localeCompare(b.toLowerCase())
})
let ret : Category[] = [];
const ret: Category[] = []
for (const c of lst) {
ret.push({name: c, count: count[c]});
ret.push({ name: c, count: count[c] })
}
return ret;
return ret
}

View File

@ -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}`
}

View File

@ -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}`
}

View File

@ -1,16 +1,14 @@
/** @type {import('tailwindcss').Config} */
const defaultTheme = require("tailwindcss/defaultTheme");
const defaultTheme = require("tailwindcss/defaultTheme")
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
darkMode: 'class', // allows toggling dark mode manually
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
darkMode: "class", // allows toggling dark mode manually
theme: {
extend: {
fontFamily: {
sans: ['Roboto', 'sans-serif', ...defaultTheme.fontFamily.sans],
}
sans: ["Roboto", "sans-serif", ...defaultTheme.fontFamily.sans],
},
},
plugins: [
require('@tailwindcss/typography'),
],
},
plugins: [require("@tailwindcss/typography")],
}

View File

@ -1,16 +1,22 @@
{
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"baseUrl": ".",
"strictNullChecks": true,
"allowJs": true,
"plugins": [
{
"name": "@astrojs/ts-plugin"
}
]
},
"include": [
"src/**/**",
"src/**/**/**",
]
],
"paths": {
"@components/*": ["src/components/*"],
"@assets/*": ["src/assets/*"],
"@utils/*": ["src/utils/*"],
"@i18n/*": ["src/i18n/*"],
"@layouts/*": ["src/layouts/*"],
"@/*": ["src/*"]
}
},
"include": ["src/**/*"]
}