checkin
11
.github/dependabot.yml
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for all configuration options:
|
||||||
|
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "npm" # See documentation for possible values
|
||||||
|
directory: "/" # Location of package manifests
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
27
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# generated types
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# jetbrains setting folder
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# astro tmp pages
|
||||||
|
astro_tmp_pages_*
|
||||||
57
.husky/_/pre-commit
Executable file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LEFTHOOK" = "0" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
call_lefthook()
|
||||||
|
{
|
||||||
|
if test -n "$LEFTHOOK_BIN"
|
||||||
|
then
|
||||||
|
"$LEFTHOOK_BIN" "$@"
|
||||||
|
elif lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
lefthook "$@"
|
||||||
|
else
|
||||||
|
dir="$(git rev-parse --show-toplevel)"
|
||||||
|
osArch=$(uname | tr '[:upper:]' '[:lower:]')
|
||||||
|
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
|
||||||
|
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@"
|
||||||
|
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@"
|
||||||
|
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@"
|
||||||
|
elif test -f "$dir/node_modules/lefthook/bin/index.js"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/lefthook/bin/index.js" "$@"
|
||||||
|
|
||||||
|
elif bundle exec lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
bundle exec lefthook "$@"
|
||||||
|
elif yarn lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
yarn lefthook "$@"
|
||||||
|
elif pnpm lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
pnpm lefthook "$@"
|
||||||
|
elif swift package plugin lefthook >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
swift package --disable-sandbox plugin lefthook "$@"
|
||||||
|
elif command -v mint >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
mint run csjones/lefthook-plugin "$@"
|
||||||
|
else
|
||||||
|
echo "Can't find lefthook in PATH"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
call_lefthook run "pre-commit" "$@"
|
||||||
57
.husky/_/prepare-commit-msg
Executable file
|
|
@ -0,0 +1,57 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
if [ "$LEFTHOOK_VERBOSE" = "1" -o "$LEFTHOOK_VERBOSE" = "true" ]; then
|
||||||
|
set -x
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$LEFTHOOK" = "0" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
call_lefthook()
|
||||||
|
{
|
||||||
|
if test -n "$LEFTHOOK_BIN"
|
||||||
|
then
|
||||||
|
"$LEFTHOOK_BIN" "$@"
|
||||||
|
elif lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
lefthook "$@"
|
||||||
|
else
|
||||||
|
dir="$(git rev-parse --show-toplevel)"
|
||||||
|
osArch=$(uname | tr '[:upper:]' '[:lower:]')
|
||||||
|
cpuArch=$(uname -m | sed 's/aarch64/arm64/;s/x86_64/x64/')
|
||||||
|
if test -f "$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/lefthook-${osArch}-${cpuArch}/bin/lefthook" "$@"
|
||||||
|
elif test -f "$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/@evilmartians/lefthook/bin/lefthook-${osArch}-${cpuArch}/lefthook" "$@"
|
||||||
|
elif test -f "$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/@evilmartians/lefthook-installer/bin/lefthook" "$@"
|
||||||
|
elif test -f "$dir/node_modules/lefthook/bin/index.js"
|
||||||
|
then
|
||||||
|
"$dir/node_modules/lefthook/bin/index.js" "$@"
|
||||||
|
|
||||||
|
elif bundle exec lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
bundle exec lefthook "$@"
|
||||||
|
elif yarn lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
yarn lefthook "$@"
|
||||||
|
elif pnpm lefthook -h >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
pnpm lefthook "$@"
|
||||||
|
elif swift package plugin lefthook >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
swift package --disable-sandbox plugin lefthook "$@"
|
||||||
|
elif command -v mint >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
mint run csjones/lefthook-plugin "$@"
|
||||||
|
else
|
||||||
|
echo "Can't find lefthook in PATH"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
call_lefthook run "prepare-commit-msg" "$@"
|
||||||
6
.prettierignore
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# lockfiles
|
||||||
|
package-lock.json
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules
|
||||||
16
.prettierrc.mjs
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
export default {
|
||||||
|
semi: false,
|
||||||
|
singleQuote: false,
|
||||||
|
trailingComma: "all",
|
||||||
|
endOfLine: "lf",
|
||||||
|
plugins: ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: "*.astro",
|
||||||
|
options: {
|
||||||
|
parser: "astro",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
9
.vscode/extensions.json
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"astro-build.astro-vscode",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"unifiedjs.vscode-mdx"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
||||||
11
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
12
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"biome.enabled": false,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[astro]": {
|
||||||
|
"editor.defaultFormatter": "astro-build.astro-vscode"
|
||||||
|
},
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.organizeImports": "always"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) sun0225SUN
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
99
README.md
|
|
@ -1 +1,98 @@
|
||||||
Awa
|
# Astro Air
|
||||||
|
|
||||||
|
A minimalism, personal blog theme for Astro.
|
||||||
|
|
||||||
|
> If you find this project helpful, please consider giving it a star ⭐️
|
||||||
|
|
||||||
|
[](https://astro.build) [](https://app.netlify.com/sites/astro-air/deploys)
|
||||||
|
|
||||||
|
<img style="border-radius: 10px;" src="https://cdn.jsdelivr.net/gh/sun0225SUN/astro-air/public/preview.png" alt="Astro Air">
|
||||||
|
|
||||||
|
## Showcase
|
||||||
|
|
||||||
|
- [Astro Air](https://astro-air.guoqi.dev)
|
||||||
|
- [Guoqi's blog](https://blog.sunguoqi.com)
|
||||||
|
- ...
|
||||||
|
|
||||||
|
> welcome to add your own blog to the list ❤️
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [x] 🌓 Dark mode support
|
||||||
|
- [x] 📱 Fully device responsive
|
||||||
|
- [x] 🎨 Clean and minimalist design
|
||||||
|
- [x] 📝 Markdown/MDX for content authoring
|
||||||
|
- [x] 🏄♂️ SSG static rendering, SEO friendly
|
||||||
|
- [x] 🌐 i18n support (EN/ZH)
|
||||||
|
- [x] 🔗 Social media integration
|
||||||
|
- [x] 📰 RSS feed & sitemap support
|
||||||
|
- [x] 🛠️ Google analysis integration
|
||||||
|
- [x] 💬 Commenting Integration (Twikoo)
|
||||||
|
- [x] 🎨 Enhance Transition and Animation
|
||||||
|
- [ ] 🔍 Local search functionality
|
||||||
|
- [ ] ...and more
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https://github.com/sun0225SUN/astro-air)
|
||||||
|
|
||||||
|
[](https://app.netlify.com/start/deploy?repository=https://github.com/sun0225SUN/astro-air)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
- Open `src/config/index.ts` and customize your site settings
|
||||||
|
|
||||||
|
- Open `src/config/links.ts` and customize your site links
|
||||||
|
|
||||||
|
- Open `src/config/zh(en)/about.mdx(intro.mdx、links.mdx)` and customize your pages content
|
||||||
|
|
||||||
|
## Writing Content
|
||||||
|
|
||||||
|
1. Create new blog posts in the `src/content/posts/` directory
|
||||||
|
2. Use the following frontmatter template:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
title: "Your Post Title"
|
||||||
|
description: "A brief description of your post"
|
||||||
|
pubDate: YYYY-MM-DD
|
||||||
|
updatedDate(optional): YYYY-MM-DD
|
||||||
|
tags(optional): ["tag1", "tag2"]
|
||||||
|
ogImage(optional): "cover image URL"
|
||||||
|
---
|
||||||
|
|
||||||
|
Your content here...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update Theme
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git remote add upstream https://github.com/sun0225SUN/astro-air
|
||||||
|
|
||||||
|
git fetch upstream
|
||||||
|
|
||||||
|
git merge upstream/main --allow-unrelated-histories
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome! Feel free to:
|
||||||
|
|
||||||
|
1. Fork the repository
|
||||||
|
2. Create your feature branch
|
||||||
|
3. Submit a pull request
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/sun0225SUN/astro-air
|
||||||
|
|
||||||
|
cd astro-air
|
||||||
|
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||||
|
|
|
||||||
33
astro.config.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import mdx from "@astrojs/mdx"
|
||||||
|
import react from "@astrojs/react"
|
||||||
|
import sitemap from "@astrojs/sitemap"
|
||||||
|
import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections"
|
||||||
|
import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers"
|
||||||
|
import tailwindcss from "@tailwindcss/vite"
|
||||||
|
import expressiveCode from "astro-expressive-code"
|
||||||
|
import { defineConfig } from "astro/config"
|
||||||
|
|
||||||
|
import robotsTxt from "astro-robots-txt"
|
||||||
|
|
||||||
|
// https://astro.build/config
|
||||||
|
export default defineConfig({
|
||||||
|
output: "static",
|
||||||
|
prefetch: true,
|
||||||
|
site: "https://astro-air.guoqi.dev",
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
},
|
||||||
|
integrations: [
|
||||||
|
react(),
|
||||||
|
sitemap(),
|
||||||
|
expressiveCode({
|
||||||
|
plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
|
||||||
|
themes: ["material-theme-lighter", "material-theme-darker"],
|
||||||
|
defaultProps: {
|
||||||
|
showLineNumbers: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
mdx(),
|
||||||
|
robotsTxt(),
|
||||||
|
],
|
||||||
|
})
|
||||||
12
eslint.config.cjs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
const eslintPluginAstro = require("eslint-plugin-astro")
|
||||||
|
module.exports = [
|
||||||
|
// add more generic rule sets here, such as:
|
||||||
|
// js.configs.recommended,
|
||||||
|
...eslintPluginAstro.configs["flat/recommended"], // In CommonJS, the `flat/` prefix is required.
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// override/add rules settings here, such as:
|
||||||
|
// "astro/no-set-html-directive": "error"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
14
lefthook.yml
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
pre-commit:
|
||||||
|
commands:
|
||||||
|
prettier-js:
|
||||||
|
glob: "*.{js,jsx,ts,tsx,astro}"
|
||||||
|
run: npx prettier --write {staged_files}
|
||||||
|
eslint-fix:
|
||||||
|
glob: "*.{js,jsx,ts,tsx,astro}"
|
||||||
|
run: npx eslint --fix {staged_files}
|
||||||
|
eslint:
|
||||||
|
glob: "*.{js,jsx,ts,tsx,astro}"
|
||||||
|
run: npx eslint {staged_files}
|
||||||
|
prettier-other:
|
||||||
|
glob: "*.{json,css,md}"
|
||||||
|
run: npx prettier --write {staged_files}
|
||||||
51
package.json
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"name": "astro-air",
|
||||||
|
"type": "module",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro check && astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro",
|
||||||
|
"format": "prettier --write ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
|
"@astrojs/mdx": "^4.0.7",
|
||||||
|
"@astrojs/react": "^4.2.0",
|
||||||
|
"@astrojs/rss": "^4.0.11",
|
||||||
|
"@astrojs/sitemap": "^3.2.1",
|
||||||
|
"@expressive-code/plugin-collapsible-sections": "^0.40.1",
|
||||||
|
"@expressive-code/plugin-line-numbers": "^0.41.2",
|
||||||
|
"@tailwindcss/vite": "^4.0.3",
|
||||||
|
"@types/react": "^19.0.8",
|
||||||
|
"@types/react-dom": "^19.0.3",
|
||||||
|
"astro": "^5.2.3",
|
||||||
|
"astro-expressive-code": "^0.40.1",
|
||||||
|
"astro-google-analytics": "^1.0.3",
|
||||||
|
"astro-og-canvas": "^0.7.0",
|
||||||
|
"astro-robots-txt": "^1.0.0",
|
||||||
|
"canvaskit-wasm": "^0.40.0",
|
||||||
|
"lefthook": "^1.10.10",
|
||||||
|
"lucide-react": "^0.525.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwindcss": "^4.0.3",
|
||||||
|
"twikoo": "^1.6.41",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@types/dom-view-transitions": "^1.0.5",
|
||||||
|
"@types/node": "^22.13.1",
|
||||||
|
"@types/sanitize-html": "^2.13.0",
|
||||||
|
"@typescript-eslint/parser": "^8.23.0",
|
||||||
|
"eslint": "^9.19.0",
|
||||||
|
"eslint-plugin-astro": "^1.3.1",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
|
"sass": "^1.83.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/avatar.png
Executable file
|
After Width: | Height: | Size: 34 KiB |
BIN
public/fonts/hwmc.otf
Normal file
BIN
public/images/page-meta/de/about.png
Normal file
|
After Width: | Height: | Size: 40 KiB |
BIN
public/images/page-meta/de/archive.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
public/images/page-meta/de/links.png
Normal file
|
After Width: | Height: | Size: 43 KiB |
BIN
public/images/page-meta/en/about.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/images/page-meta/en/archive.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
public/images/page-meta/en/links.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
public/images/page-meta/general/404.png
Normal file
|
After Width: | Height: | Size: 873 KiB |
23
public/links/astro.svg
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg id="logosandtypes_com" data-name="logosandtypes com" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 150 150">
|
||||||
|
<defs>
|
||||||
|
<style>
|
||||||
|
.cls-1 {
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cls-2 {
|
||||||
|
fill: url(#linear-gradient);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<linearGradient id="linear-gradient" x1="75.54" y1="113" x2="75.54" y2="143.15" gradientUnits="userSpaceOnUse">
|
||||||
|
<stop offset="0" stop-color="#ee46e0"/>
|
||||||
|
<stop offset="1" stop-color="#da3737"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g id="Layer_2" data-name="Layer 2">
|
||||||
|
<path id="Layer_3" data-name="Layer 3" class="cls-1" d="M0,0H150V150H0V0Z"/>
|
||||||
|
</g>
|
||||||
|
<path class="cls-2" d="M57.53,128.19c-5.86-5.34-7.57-16.57-5.13-24.7,4.23,5.13,10.1,6.75,16.17,7.67,10.41,1.56,21.21,.76,30.3-5.19,2.64,7.89-1.02,16.49-7.6,21.14-8.36,5.63-14.92,8.86-10.92,20.26-6.02-2.64-9.6-8.59-9.64-15-.02-1.6-.02-3.21-.24-4.79-.53-3.84-2.33-5.56-5.74-5.66-3.89-.16-6.6,2.57-7.21,6.27h0Z"/>
|
||||||
|
<path d="M24.1,102.13s17.35-8.43,34.74-8.43l13.12-40.49c.49-1.96,1.92-3.29,3.54-3.29s3.05,1.33,3.54,3.29l13.12,40.49c20.6,0,34.74,8.43,34.74,8.43,0,0-29.47-80.07-29.52-80.23-.85-2.37-2.27-3.89-4.2-3.89H57.82c-1.92,0-3.29,1.52-4.2,3.89-.06,.16-29.53,80.23-29.53,80.23Z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/noise.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/preview.png
Normal file
|
After Width: | Height: | Size: 448 KiB |
32
src/components/astro/footer.astro
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!-- ---
|
||||||
|
const currentYear = new Date().getFullYear()
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class="flex flex-wrap items-center gap-1 py-10 opacity-25">
|
||||||
|
<p>© {currentYear}</p>
|
||||||
|
|
||||||
|
{
|
||||||
|
[
|
||||||
|
{
|
||||||
|
text: "By",
|
||||||
|
link: "https://lio.to/bluesky",
|
||||||
|
label: "@lio.cat",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Based on",
|
||||||
|
link: "https://github.com/sun0225SUN/astro-air",
|
||||||
|
label: "Astro Air",
|
||||||
|
},
|
||||||
|
].map((item, index) => (
|
||||||
|
<>
|
||||||
|
<p>{item.text}</p>
|
||||||
|
<a
|
||||||
|
href={item.link}
|
||||||
|
class="text-blue-600 transition-colors duration-200 hover:text-blue-800"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</footer> -->
|
||||||
47
src/components/astro/header.astro
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
import { Rss } from "lucide-react"
|
||||||
|
import { LanguageToggle } from "~/components/react/language-toggle"
|
||||||
|
import { ThemeToggle } from "~/components/react/theme-toggle"
|
||||||
|
import { de, en } from "~/config"
|
||||||
|
import { getLangFromUrl } from "~/i18n/utils"
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const config = lang === "de" ? de : en
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="flex h-24 w-full items-center justify-between">
|
||||||
|
<a href="/" aria-label={`${config.siteName}`}>
|
||||||
|
<div class="text-2xl font-semibold">{config.siteName}</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-6">
|
||||||
|
{
|
||||||
|
config.rss && (
|
||||||
|
<a
|
||||||
|
href={"/" + lang + "/rss.xml"}
|
||||||
|
target="_blank"
|
||||||
|
aria-label="RSS"
|
||||||
|
title="RSS"
|
||||||
|
>
|
||||||
|
<Rss />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
config.social.map((social) => (
|
||||||
|
<a
|
||||||
|
href={social.link}
|
||||||
|
target="_blank"
|
||||||
|
class="hidden md:block"
|
||||||
|
aria-label={social.label}
|
||||||
|
title={social.label}
|
||||||
|
>
|
||||||
|
<social.icon />
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
<LanguageToggle client:load />
|
||||||
|
<ThemeToggle client:load />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
12
src/components/astro/intro.astro
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
import IntroContentDe from "~/config/de/intro.mdx"
|
||||||
|
import IntroContentEn from "~/config/en/intro.mdx"
|
||||||
|
import { getLangFromUrl } from "~/i18n/utils"
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const IntroContent = lang === "de" ? IntroContentDe : IntroContentEn
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="my-10">
|
||||||
|
<IntroContent />
|
||||||
|
</div>
|
||||||
32
src/components/astro/link-card.astro
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
name: string
|
||||||
|
link: string
|
||||||
|
description: string
|
||||||
|
avatar: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, link, description, avatar } = Astro.props
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
target="_blank"
|
||||||
|
class="flex items-center gap-4 rounded-lg border border-neutral-200 p-4 hover:scale-[1.02] hover:bg-neutral-100 hover:shadow-lg dark:border-neutral-800 dark:hover:bg-neutral-900"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={avatar}
|
||||||
|
alt={name}
|
||||||
|
class="h-12 w-12 rounded-full object-cover transition-transform duration-300 hover:scale-110"
|
||||||
|
/>
|
||||||
|
<div class="flex h-full flex-col">
|
||||||
|
<span class="text-lg font-medium transition-colors">
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="line-clamp-2 flex-1 text-sm text-neutral-600 dark:text-neutral-400"
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
81
src/components/astro/nav.astro
Normal file
|
|
@ -0,0 +1,81 @@
|
||||||
|
---
|
||||||
|
import { de, en } from "~/config"
|
||||||
|
import { getLangFromUrl, useTranslations } from "~/i18n/utils"
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const t = useTranslations(lang)
|
||||||
|
|
||||||
|
const { home, archive, custom, links, about } =
|
||||||
|
lang === "de" ? de.navigation : en.navigation
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mt-4 mb-10 flex gap-4 overflow-x-auto text-lg whitespace-nowrap [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
home && (
|
||||||
|
<a
|
||||||
|
href={`/${lang}`}
|
||||||
|
class="hover:underline hover:underline-offset-4"
|
||||||
|
aria-label={t("nav.home")}
|
||||||
|
title={t("nav.home")}
|
||||||
|
data-astro-prefetch="viewport"
|
||||||
|
>
|
||||||
|
<p>{t("nav.home")}</p>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
archive && (
|
||||||
|
<a
|
||||||
|
href={`/${lang}/archive`}
|
||||||
|
class="hover:underline hover:underline-offset-4"
|
||||||
|
aria-label={t("nav.archive")}
|
||||||
|
title={t("nav.archive")}
|
||||||
|
data-astro-prefetch="viewport"
|
||||||
|
>
|
||||||
|
<p>{t("nav.archive")}</p>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
custom?.map((tab) => (
|
||||||
|
<a
|
||||||
|
href={tab.link}
|
||||||
|
class="hover:underline hover:underline-offset-4"
|
||||||
|
target="_blank"
|
||||||
|
aria-label={tab.label}
|
||||||
|
title={tab.label}
|
||||||
|
data-astro-prefetch="viewport"
|
||||||
|
>
|
||||||
|
<p>{tab.label}</p>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
links && (
|
||||||
|
<a
|
||||||
|
href={`/${lang}/links`}
|
||||||
|
class="hover:underline hover:underline-offset-4"
|
||||||
|
aria-label={t("nav.links")}
|
||||||
|
title={t("nav.links")}
|
||||||
|
data-astro-prefetch="viewport"
|
||||||
|
>
|
||||||
|
<p>{t("nav.links")}</p>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
about && (
|
||||||
|
<a
|
||||||
|
href={`/${lang}/about`}
|
||||||
|
class="hover:underline hover:underline-offset-4"
|
||||||
|
aria-label={t("nav.about")}
|
||||||
|
title={t("nav.about")}
|
||||||
|
data-astro-prefetch="viewport"
|
||||||
|
>
|
||||||
|
<p>{t("nav.about")}</p>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
23
src/components/astro/post-list.astro
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
post: any
|
||||||
|
lang: string
|
||||||
|
dateFormat?: string
|
||||||
|
dateWidth?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post, lang, dateFormat = "default", dateWidth = "w-36" } = Astro.props
|
||||||
|
import { formatDate } from "~/utils"
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`/${lang}/posts/${post.id}/`}
|
||||||
|
class="my-3 flex visited:text-purple-500/90 md:my-2"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class={`flex ${dateWidth} flex-shrink-0 truncate text-gray-500 dark:text-gray-400`}
|
||||||
|
>
|
||||||
|
<time>{formatDate(post.data.pubDate, dateFormat)}</time>
|
||||||
|
</p>
|
||||||
|
<p class="line-clamp-2 text-lg hover:underline">{post.data.title}</p>
|
||||||
|
</a>
|
||||||
24
src/components/astro/recent-blogs.astro
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
---
|
||||||
|
import { common } from "~/config"
|
||||||
|
import { getLangFromUrl, useTranslations } from "~/i18n/utils"
|
||||||
|
import { getPostsByLocale } from "~/utils"
|
||||||
|
import PostList from "./post-list.astro"
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const t = useTranslations(lang)
|
||||||
|
|
||||||
|
const allPosts = await getPostsByLocale(lang)
|
||||||
|
const posts = allPosts.slice(0, common.latestPosts)
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="my-8 text-xl font-medium md:my-8">{t("blog.latest")}</div>
|
||||||
|
{
|
||||||
|
posts.map((post: any) => (
|
||||||
|
<PostList
|
||||||
|
post={post}
|
||||||
|
lang={lang}
|
||||||
|
dateFormat="YYYY-MM-DD"
|
||||||
|
dateWidth="w-32"
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
38
src/components/astro/tag.astro
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
---
|
||||||
|
import { getLangFromUrl, useTranslations } from "~/i18n/utils"
|
||||||
|
import MainLayout from "~/layouts/main.astro"
|
||||||
|
import PostList from "./post-list.astro"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
posts: any[]
|
||||||
|
tag: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts, tag } = Astro.props
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const t = useTranslations(lang)
|
||||||
|
|
||||||
|
const filteredPosts = posts.filter((post: any) => post.data.tags?.includes(tag))
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout>
|
||||||
|
<div class="my-9 mt-2 text-2xl font-semibold">#{tag}</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
filteredPosts.length > 0 ? (
|
||||||
|
<ul class="space-y-4">
|
||||||
|
{filteredPosts.map((post: any) => (
|
||||||
|
<PostList
|
||||||
|
post={post}
|
||||||
|
lang={lang}
|
||||||
|
dateFormat="YYYY-MM-DD"
|
||||||
|
dateWidth="w-32"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<p class="text-gray-500">{t("tag.no_posts")}</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</MainLayout>
|
||||||
15
src/components/react/language-toggle.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { Languages } from "lucide-react"
|
||||||
|
|
||||||
|
export function LanguageToggle() {
|
||||||
|
const handleLanguageToggle = () => {
|
||||||
|
const currentPath = window.location.pathname
|
||||||
|
|
||||||
|
const newPath = currentPath.includes("/en")
|
||||||
|
? currentPath.replace("/en", "/de")
|
||||||
|
: currentPath.replace("/de", "/en")
|
||||||
|
|
||||||
|
window.location.href = newPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Languages className="cursor-pointer" onClick={handleLanguageToggle} />
|
||||||
|
}
|
||||||
5
src/components/react/noise-background.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
export function NoiseBackground() {
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-[-1] h-full w-full bg-[url('/noise.png')] bg-[length:128px_128px] bg-repeat opacity-[0.06]"></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
src/components/react/theme-toggle.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Moon, Sun } from "lucide-react"
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const updateTheme = () => {
|
||||||
|
const isDark = document.documentElement.classList.contains("dark")
|
||||||
|
localStorage.setItem("theme", isDark ? "dark" : "light")
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-theme",
|
||||||
|
isDark ? "material-theme-darker" : "material-theme-lighter",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleToggleClick = () => {
|
||||||
|
const element = document.documentElement
|
||||||
|
|
||||||
|
// if not supported, just toggle the theme
|
||||||
|
if (!document.startViewTransition) {
|
||||||
|
element.classList.toggle("dark")
|
||||||
|
updateTheme()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
document.startViewTransition(() => {
|
||||||
|
element.classList.toggle("dark")
|
||||||
|
updateTheme()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={handleToggleClick}
|
||||||
|
aria-label="Toggle theme"
|
||||||
|
title="Toggle theme"
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<Sun className="dark:hidden" />
|
||||||
|
<Moon className="hidden dark:block" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
12
src/config/de/about.mdx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const title = "你好,我是小孙同学~"
|
||||||
|
|
||||||
|
<h3 class="my-10 text-center text-xl font-bold">{title}</h3>
|
||||||
|
|
||||||
|
<img
|
||||||
|
className="block dark:hidden"
|
||||||
|
src="https://cdn.jsdelivr.net/gh/sun0225SUN/sun0225SUN/profile-snake-contrib/github-contribution-grid-snake.svg"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className="hidden dark:block"
|
||||||
|
src="https://cdn.jsdelivr.net/gh/sun0225SUN/sun0225SUN/profile-snake-contrib/github-contribution-grid-snake-dark.svg"
|
||||||
|
/>
|
||||||
6
src/config/de/intro.mdx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const age = new Date().getFullYear() - 2002
|
||||||
|
|
||||||
|
<p>
|
||||||
|
they/them (er/ihm) ⋅ {age}yo digital sorcerer und pixel wizard ⋅ self-hoster ⋅
|
||||||
|
liebt automation
|
||||||
|
</p>
|
||||||
21
src/config/de/links.mdx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
export const title = "我的链接"
|
||||||
|
|
||||||
|
<h3 class="my-10 text-center text-xl font-bold">{title}</h3>
|
||||||
|
|
||||||
|
```json
|
||||||
|
name: "Guoqi Sun"
|
||||||
|
description: "Try, fail, retry. That's the rhythm of growth."
|
||||||
|
link: "https://blog.sunguoqi.com"
|
||||||
|
avatar: "https://assets.guoqi.dev/images/avatar.png"
|
||||||
|
```
|
||||||
|
|
||||||
|
<h4 class="my-10 text-center">
|
||||||
|
想要添加友情链接?请按照如下格式在评论区留言,我会及时添加。
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
```json
|
||||||
|
name: "your site name"
|
||||||
|
description: "your site description"
|
||||||
|
link: "your site link"
|
||||||
|
avatar: "your site avatar"
|
||||||
|
```
|
||||||
12
src/config/en/about.mdx
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
export const title = "Hello, I'm Guoqi Sun ~"
|
||||||
|
|
||||||
|
<h3 class="my-10 text-center text-xl font-bold">{title}</h3>
|
||||||
|
|
||||||
|
<img
|
||||||
|
className="block dark:hidden"
|
||||||
|
src="https://cdn.jsdelivr.net/gh/sun0225SUN/sun0225SUN/profile-snake-contrib/github-contribution-grid-snake.svg"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
className="hidden dark:block"
|
||||||
|
src="https://cdn.jsdelivr.net/gh/sun0225SUN/sun0225SUN/profile-snake-contrib/github-contribution-grid-snake-dark.svg"
|
||||||
|
/>
|
||||||
6
src/config/en/intro.mdx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const age = new Date().getFullYear() - 2002
|
||||||
|
|
||||||
|
<p>
|
||||||
|
they/them ⋅ {age}yo digital sorcerer and pixel wizard ⋅ self-hoster ⋅ loves
|
||||||
|
automation
|
||||||
|
</p>
|
||||||
22
src/config/en/links.mdx
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
export const title = "My Link"
|
||||||
|
|
||||||
|
<h3 class="my-10 text-center text-xl font-bold">{title}</h3>
|
||||||
|
|
||||||
|
```js
|
||||||
|
name: "Guoqi Sun",
|
||||||
|
description: "Try, fail, retry. That's the rhythm of growth.",
|
||||||
|
link: "https://blog.sunguoqi.com",
|
||||||
|
avatar: "https://assets.guoqi.dev/images/avatar.png",
|
||||||
|
```
|
||||||
|
|
||||||
|
<h4 class="my-10 text-center">
|
||||||
|
Want to add a friend link? Please leave a comment in the comment area below,
|
||||||
|
and I will add it as soon as possible.
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
```js
|
||||||
|
name: "your site name",
|
||||||
|
description: "your site description",
|
||||||
|
link: "your site link",
|
||||||
|
avatar: "your site avatar",
|
||||||
|
```
|
||||||
115
src/config/index.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { Github, Twitter } from "lucide-react"
|
||||||
|
|
||||||
|
export const defaultLanguage: string = "en"
|
||||||
|
|
||||||
|
export const common = {
|
||||||
|
domain: "https://lio.cat",
|
||||||
|
meta: {
|
||||||
|
favicon: "/avatar.png",
|
||||||
|
url: "https://lio.cat",
|
||||||
|
},
|
||||||
|
googleAnalyticsId: "",
|
||||||
|
social: [
|
||||||
|
{
|
||||||
|
icon: Twitter,
|
||||||
|
label: "X",
|
||||||
|
link: "https://lio.to/twitter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Github,
|
||||||
|
label: "GitHub",
|
||||||
|
link: "https://lio.to/github",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rss: true,
|
||||||
|
navigation: {
|
||||||
|
home: true,
|
||||||
|
archive: true,
|
||||||
|
custom: [
|
||||||
|
// {
|
||||||
|
// label: "CamLife",
|
||||||
|
// link: "https://camlife.cn",
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
links: false,
|
||||||
|
about: false,
|
||||||
|
},
|
||||||
|
latestPosts: 8,
|
||||||
|
comments: {
|
||||||
|
enabled: false,
|
||||||
|
twikoo: {
|
||||||
|
enabled: false,
|
||||||
|
// replace with your own envId
|
||||||
|
envId: import.meta.env.PUBLIC_TWIKOO_ENV_ID ?? "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const de = {
|
||||||
|
...common,
|
||||||
|
siteName: "Lio",
|
||||||
|
meta: {
|
||||||
|
...common.meta,
|
||||||
|
title: "Lio",
|
||||||
|
slogan: "fangmarks",
|
||||||
|
description: "digital sorcerer and pixel wizard",
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
...common.navigation,
|
||||||
|
custom: [
|
||||||
|
// {
|
||||||
|
// label: "影集",
|
||||||
|
// link: "https://camlife.cn",
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
pageMeta: {
|
||||||
|
archive: {
|
||||||
|
title: "归档",
|
||||||
|
description: "小孙同学的所有文章",
|
||||||
|
ogImage: "/images/page-meta/de/archive.png",
|
||||||
|
},
|
||||||
|
links: {
|
||||||
|
title: "朋友们",
|
||||||
|
description: "小孙同学的和他朋友们",
|
||||||
|
ogImage: "/images/page-meta/de/links.png",
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
title: "关于我",
|
||||||
|
description: "小孙同学的自我介绍",
|
||||||
|
ogImage: "/images/page-meta/de/about.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const en = {
|
||||||
|
...common,
|
||||||
|
siteName: "Lio",
|
||||||
|
meta: {
|
||||||
|
...common.meta,
|
||||||
|
title: "Lio",
|
||||||
|
slogan: "fangmarks",
|
||||||
|
description: "digital sorcerer and pixel wizard",
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
...common.navigation,
|
||||||
|
custom: [
|
||||||
|
// {
|
||||||
|
// label: "CamLife",
|
||||||
|
// link: "https://camlife.cn",
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
pageMeta: {
|
||||||
|
archive: {
|
||||||
|
title: "All Posts",
|
||||||
|
description: "All of Lio's posts",
|
||||||
|
ogImage: "/images/page-meta/en/archive.png",
|
||||||
|
},
|
||||||
|
about: {
|
||||||
|
title: "About",
|
||||||
|
description: "About Lio",
|
||||||
|
ogImage: "/images/page-meta/en/about.png",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
14
src/config/links.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
export const links = [
|
||||||
|
{
|
||||||
|
name: "Astro",
|
||||||
|
link: "https://astro.build",
|
||||||
|
description: "The web framework for content-driven websites",
|
||||||
|
avatar: "/links/astro.svg",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Guoqi Sun",
|
||||||
|
link: "https://blog.sunguoqi.com",
|
||||||
|
description: "Try, fail, retry. That's the rhythm of growth.",
|
||||||
|
avatar: "https://assets.guoqi.dev/images/avatar.png",
|
||||||
|
},
|
||||||
|
]
|
||||||
27
src/content.config.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { glob } from "astro/loaders"
|
||||||
|
import { defineCollection, z } from "astro:content"
|
||||||
|
|
||||||
|
const postSchema = z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
pubDate: z.coerce.date(),
|
||||||
|
updatedDate: z.coerce.date().optional(),
|
||||||
|
heroImage: z.string().optional(),
|
||||||
|
ogImage: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const enPostsCollection = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/posts/en" }),
|
||||||
|
schema: postSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
const dePostsCollection = defineCollection({
|
||||||
|
loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/posts/de" }),
|
||||||
|
schema: postSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
enPosts: enPostsCollection,
|
||||||
|
dePosts: dePostsCollection,
|
||||||
|
}
|
||||||
25
src/content/posts/de/post-1.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
title: "My First Blog Post"
|
||||||
|
pubDate: 2020-07-01
|
||||||
|
description: "This is the first post of my new Astro blog."
|
||||||
|
ogImage: "https://sunguoqi.com/me.png"
|
||||||
|
author: "Astro Learner"
|
||||||
|
image:
|
||||||
|
url: "https://docs.astro.build/assets/rose.webp"
|
||||||
|
alt: "The Astro logo on a dark background with a pink glow."
|
||||||
|
tags: ["astro", "blogging", "learning-in-public"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Welcome to my _new blog_ about learning Astro! Here, I will share my learning journey as I build a new website.
|
||||||
|
|
||||||
|
## What I've accomplished
|
||||||
|
|
||||||
|
1. **Installing Astro**: First, I created a new Astro project and set up my online accounts.
|
||||||
|
|
||||||
|
2. **Making Pages**: I then learned how to make pages by creating new `.astro` files and placing them in the `src/pages/` folder.
|
||||||
|
|
||||||
|
3. **Making Blog Posts**: This is my first blog post! I now have Astro pages and Markdown posts!
|
||||||
|
|
||||||
|
## What's next
|
||||||
|
|
||||||
|
I will finish the Astro tutorial, and then keep adding more posts. Watch this space for more to come.
|
||||||
25
src/content/posts/en/post-1.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
---
|
||||||
|
title: "My First Blog Post"
|
||||||
|
pubDate: 2020-07-01
|
||||||
|
description: "This is the first post of my new Astro blog."
|
||||||
|
ogImage: "https://sunguoqi.com/me.png"
|
||||||
|
author: "Astro Learner"
|
||||||
|
image:
|
||||||
|
url: "https://docs.astro.build/assets/rose.webp"
|
||||||
|
alt: "The Astro logo on a dark background with a pink glow."
|
||||||
|
tags: ["astro", "blogging", "learning-in-public"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Welcome to my _new blog_ about learning Astro! Here, I will share my learning journey as I build a new website.
|
||||||
|
|
||||||
|
## What I've accomplished
|
||||||
|
|
||||||
|
1. **Installing Astro**: First, I created a new Astro project and set up my online accounts.
|
||||||
|
|
||||||
|
2. **Making Pages**: I then learned how to make pages by creating new `.astro` files and placing them in the `src/pages/` folder.
|
||||||
|
|
||||||
|
3. **Making Blog Posts**: This is my first blog post! I now have Astro pages and Markdown posts!
|
||||||
|
|
||||||
|
## What's next
|
||||||
|
|
||||||
|
I will finish the Astro tutorial, and then keep adding more posts. Watch this space for more to come.
|
||||||
33
src/i18n/ui.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
export const languages = {
|
||||||
|
en: "English",
|
||||||
|
de: "Deutsch",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defaultLang = "en"
|
||||||
|
|
||||||
|
export const langs = ["en", "de"]
|
||||||
|
|
||||||
|
export const ui = {
|
||||||
|
en: {
|
||||||
|
"nav.home": "Home",
|
||||||
|
"nav.archive": "Archive",
|
||||||
|
"nav.about": "About",
|
||||||
|
"nav.links": "Links",
|
||||||
|
"blog.latest": "Latest Posts",
|
||||||
|
"archive.title": "All Posts",
|
||||||
|
"links.title": "My Friends",
|
||||||
|
"tag.title": "Tag:",
|
||||||
|
"tag.no_posts": "No posts found for tag",
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
"nav.home": "Start",
|
||||||
|
"nav.about": "Über mich",
|
||||||
|
"nav.archive": "Archiv",
|
||||||
|
"nav.links": "Links",
|
||||||
|
"links.title": "Freunde",
|
||||||
|
"blog.latest": "Letzte Posts",
|
||||||
|
"archive.title": "Alle Posts",
|
||||||
|
"tag.title": "Tag:",
|
||||||
|
"tag.no_posts": "Für diesen Tag wurden keine Posts gefunden",
|
||||||
|
},
|
||||||
|
} as const
|
||||||
14
src/i18n/utils.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { defaultLang, ui } from "./ui"
|
||||||
|
|
||||||
|
export function getLangFromUrl(url: URL) {
|
||||||
|
const [, lang] = url.pathname.split("/")
|
||||||
|
if (lang in ui) return lang as keyof typeof ui
|
||||||
|
return defaultLang
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTranslations(lang: keyof typeof ui) {
|
||||||
|
return function t(key: keyof (typeof ui)[typeof defaultLang]) {
|
||||||
|
// @ts-ignore
|
||||||
|
return ui[lang][key] || ui[defaultLang][key]
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/layouts/base.astro
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
---
|
||||||
|
import { NoiseBackground } from "~/components/react/noise-background"
|
||||||
|
import { getLangFromUrl } from "~/i18n/utils"
|
||||||
|
import { GoogleAnalytics } from "astro-google-analytics"
|
||||||
|
import { en, de } from "~/config"
|
||||||
|
import { ClientRouter } from "astro:transitions"
|
||||||
|
|
||||||
|
import "~/styles/tailwind.css"
|
||||||
|
import "~/styles/view-transition.css"
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
|
||||||
|
const { title, description, ogImage } = Astro.props
|
||||||
|
|
||||||
|
const ogImageURL = new URL(ogImage, Astro.site).href
|
||||||
|
const permalink = new URL(Astro.url.pathname, Astro.site).href
|
||||||
|
|
||||||
|
const config = lang === "de" ? de : en
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang={lang}>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<link rel="icon" type="image/png" href={config.meta.favicon} />
|
||||||
|
<title>
|
||||||
|
{
|
||||||
|
title
|
||||||
|
? `${config.meta.title} - ${title}`
|
||||||
|
: `${config.meta.title} - ${config.meta.slogan}`
|
||||||
|
}
|
||||||
|
</title>
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content={description ? description : config.meta.description}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Open Graph / Facebook -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content={permalink} />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:description" content={description} />
|
||||||
|
<meta property="og:image" content={ogImageURL} />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
|
<meta property="twitter:url" content={permalink} />
|
||||||
|
<meta property="twitter:title" content={title} />
|
||||||
|
<meta property="twitter:description" content={description} />
|
||||||
|
<meta property="twitter:image" content={ogImageURL} />
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
const setTheme = () => {
|
||||||
|
const theme =
|
||||||
|
localStorage.getItem("theme") ??
|
||||||
|
(window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||||
|
? "dark"
|
||||||
|
: "light")
|
||||||
|
|
||||||
|
if (theme === "dark") {
|
||||||
|
document.documentElement.classList.add("dark")
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-theme",
|
||||||
|
"material-theme-darker",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove("dark")
|
||||||
|
document.documentElement.setAttribute(
|
||||||
|
"data-theme",
|
||||||
|
"material-theme-lighter",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme()
|
||||||
|
|
||||||
|
document.addEventListener("astro:after-swap", setTheme)
|
||||||
|
document.addEventListener("astro:page-load", setTheme)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script
|
||||||
|
is:inline
|
||||||
|
src="https://umami.guoqi.dev/script.js"
|
||||||
|
data-website-id="759e9e56-20d3-463b-8d6e-abba5c53128b"
|
||||||
|
data-domains="astro-air.guoqi.dev"></script>
|
||||||
|
|
||||||
|
{
|
||||||
|
config.googleAnalyticsId && (
|
||||||
|
<GoogleAnalytics id={config.googleAnalyticsId} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<ClientRouter />
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
class="flex min-h-screen w-full justify-center px-6 md:px-0 dark:bg-[#121212] dark:text-white"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<NoiseBackground />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
26
src/layouts/main.astro
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
// import Comments from "~/components/astro/comments.astro"
|
||||||
|
import Header from "~/components/astro/header.astro"
|
||||||
|
import Navigation from "~/components/astro/nav.astro"
|
||||||
|
import BaseLayout from "~/layouts/base.astro"
|
||||||
|
|
||||||
|
const { title, description, ogImage, needComment } = Astro.props
|
||||||
|
const filename = Astro.url.pathname.split("/").filter(Boolean).pop() ?? ""
|
||||||
|
const openGraphImage = !ogImage ? `/og/${filename}.png` : ogImage
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
ogImage={openGraphImage}
|
||||||
|
needComment={needComment}
|
||||||
|
>
|
||||||
|
<main class="max-auto mb-10 w-full max-w-3xl">
|
||||||
|
<Header />
|
||||||
|
<Navigation />
|
||||||
|
<slot />
|
||||||
|
<!-- <div class="mt-20">
|
||||||
|
{needComment && <Comments />}
|
||||||
|
</div> -->
|
||||||
|
</main>
|
||||||
|
</BaseLayout>
|
||||||
16
src/pages/404.astro
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
import MainLayout from "~/layouts/main.astro"
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout title="404" description="Error 404 page not found." noindex={true}>
|
||||||
|
<section class="flex min-h-[60vh] items-center justify-center">
|
||||||
|
<div class="mx-auto max-w-xl px-4 text-center">
|
||||||
|
<img class="block" src="/images/page-meta/general/404.png" />
|
||||||
|
<div class="mt-4 text-gray-600 dark:text-gray-400">
|
||||||
|
<p class="text-lg">
|
||||||
|
The page you're looking for doesn't exist or has been moved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</MainLayout>
|
||||||
27
src/pages/[lang]/about/index.astro
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
import { de, en } from "~/config"
|
||||||
|
import AboutContentDe from "~/config/de/about.mdx"
|
||||||
|
import AboutContentEn from "~/config/en/about.mdx"
|
||||||
|
import { getLangFromUrl } from "~/i18n/utils"
|
||||||
|
import MainLayout from "~/layouts/main.astro"
|
||||||
|
import { getLanguagePaths } from "~/utils/langs"
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return getLanguagePaths()
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const pageMeta = lang === "de" ? de.pageMeta : en.pageMeta
|
||||||
|
const AboutContent = lang === "de" ? AboutContentDe : AboutContentEn
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout
|
||||||
|
title={pageMeta.about.title}
|
||||||
|
description={pageMeta.about.description}
|
||||||
|
ogImage={pageMeta.about.ogImage}
|
||||||
|
needComment={true}
|
||||||
|
>
|
||||||
|
<div class="prose dark:prose-invert max-w-3xl">
|
||||||
|
<AboutContent />
|
||||||
|
</div>
|
||||||
|
</MainLayout>
|
||||||
53
src/pages/[lang]/archive/index.astro
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
import PostList from "~/components/astro/post-list.astro"
|
||||||
|
import { de, en } from "~/config"
|
||||||
|
import { getLangFromUrl } from "~/i18n/utils"
|
||||||
|
import MainLayout from "~/layouts/main.astro"
|
||||||
|
import { getPostsByLocale } from "~/utils"
|
||||||
|
import { getLanguagePaths } from "~/utils/langs"
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
const pageMeta = lang === "de" ? de.pageMeta : en.pageMeta
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return getLanguagePaths()
|
||||||
|
}
|
||||||
|
|
||||||
|
const posts = await getPostsByLocale(lang)
|
||||||
|
|
||||||
|
const postsByYear = posts.reduce(
|
||||||
|
(acc: Record<string, any[]>, post: any) => {
|
||||||
|
const year = new Date(post.data.pubDate).getFullYear().toString()
|
||||||
|
if (!acc[year]) {
|
||||||
|
acc[year] = []
|
||||||
|
}
|
||||||
|
acc[year].push(post)
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, any[]>,
|
||||||
|
)
|
||||||
|
|
||||||
|
const years = Object.keys(postsByYear).sort((a, b) => Number(b) - Number(a))
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout
|
||||||
|
title={pageMeta.archive.title}
|
||||||
|
description={pageMeta.archive.description}
|
||||||
|
ogImage={pageMeta.archive.ogImage}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
years.map((year) => (
|
||||||
|
<div class="year-group my-8">
|
||||||
|
<h2 class="my-4 text-2xl font-bold">{year}</h2>
|
||||||
|
{postsByYear[year].map((post: any) => (
|
||||||
|
<PostList
|
||||||
|
post={post}
|
||||||
|
lang={lang}
|
||||||
|
dateFormat="MM-DD"
|
||||||
|
dateWidth="w-20"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</MainLayout>
|
||||||
17
src/pages/[lang]/index.astro
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
import Footer from "~/components/astro/footer.astro"
|
||||||
|
import Intro from "~/components/astro/intro.astro"
|
||||||
|
import RecentBlogs from "~/components/astro/recent-blogs.astro"
|
||||||
|
import MainLayout from "~/layouts/main.astro"
|
||||||
|
import { getLanguagePaths } from "~/utils/langs"
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return getLanguagePaths()
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout ogImage="/preview.png">
|
||||||
|
<Intro />
|
||||||
|
<RecentBlogs />
|
||||||
|
<Footer />
|
||||||
|
</MainLayout>
|
||||||
55
src/pages/[lang]/posts/[...slug].astro
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
import { render } from "astro:content"
|
||||||
|
import { langs } from "~/i18n/ui"
|
||||||
|
import { getLangFromUrl } from "~/i18n/utils"
|
||||||
|
import MainLayout from "~/layouts/main.astro"
|
||||||
|
import "~/styles/post.css"
|
||||||
|
import "~/styles/post.scss"
|
||||||
|
import { formatDate, getPostsByLocale } from "~/utils"
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const allPaths = []
|
||||||
|
|
||||||
|
for (const lang of langs) {
|
||||||
|
const posts = await getPostsByLocale(lang)
|
||||||
|
const paths = posts.map((post: any) => ({
|
||||||
|
params: { lang, slug: post.id },
|
||||||
|
props: { post },
|
||||||
|
}))
|
||||||
|
allPaths.push(...paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
return allPaths
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
|
||||||
|
const { post } = Astro.props
|
||||||
|
|
||||||
|
const { Content } = await render(post)
|
||||||
|
---
|
||||||
|
|
||||||
|
<MainLayout {...post.data}>
|
||||||
|
<article class="prose dark:prose-invert w-full max-w-3xl overflow-hidden">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<h2 class="!my-0 text-3xl font-semibold">{post.data.title}</h2>
|
||||||
|
<div class="my-3 text-gray-500 dark:text-white/80">
|
||||||
|
{formatDate(post.data.pubDate)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-6">
|
||||||
|
<Content />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-x-3 pb-10 text-sm text-gray-500">
|
||||||
|
{
|
||||||
|
post.data.tags.map((tag: string) => (
|
||||||
|
<a href={`/${lang}/tags/${tag}`} class="no-underline">
|
||||||
|
<p class="inline-block hover:scale-105">#{tag}</p>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</MainLayout>
|
||||||
34
src/pages/[lang]/rss.xml.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import rss from "@astrojs/rss"
|
||||||
|
import { de, en } from "~/config"
|
||||||
|
import { getPostsByLocale } from "~/utils"
|
||||||
|
import { getLanguagePaths } from "~/utils/langs"
|
||||||
|
|
||||||
|
export function getStaticPaths() {
|
||||||
|
return getLanguagePaths()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(request: { url: URL }) {
|
||||||
|
const isEn = request.url.pathname.includes("en")
|
||||||
|
|
||||||
|
const lang = isEn ? "en" : "de"
|
||||||
|
const config = isEn ? en : de
|
||||||
|
|
||||||
|
const posts = await getPostsByLocale(lang)
|
||||||
|
|
||||||
|
return rss({
|
||||||
|
title: config.meta.title,
|
||||||
|
description: config.meta.description,
|
||||||
|
site:
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? "http://localhost:4321"
|
||||||
|
: config.meta.url,
|
||||||
|
items: posts.map((post: any) => ({
|
||||||
|
title: post.data.title,
|
||||||
|
description: post.data.description,
|
||||||
|
pubDate: post.data.pubDate,
|
||||||
|
link: `/posts/${post.id}/`,
|
||||||
|
content: post.rendered ? post.rendered.html : post.data.description,
|
||||||
|
})),
|
||||||
|
customData: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
33
src/pages/[lang]/tags/[tag].astro
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
import TagComponent from "~/components/astro/tag.astro"
|
||||||
|
import { langs } from "~/i18n/ui"
|
||||||
|
import { getPostsByLocale } from "~/utils"
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
posts: any
|
||||||
|
tag: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const paths = await Promise.all(
|
||||||
|
langs.map(async (lang) => {
|
||||||
|
const posts = await getPostsByLocale(lang)
|
||||||
|
const uniqueTags = [
|
||||||
|
...new Set(posts.flatMap((post: any) => post.data.tags || [])),
|
||||||
|
]
|
||||||
|
|
||||||
|
return uniqueTags.map((tag) => ({
|
||||||
|
params: { tag, lang },
|
||||||
|
props: {
|
||||||
|
posts,
|
||||||
|
tag,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return paths.flat()
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<TagComponent {...Astro.props} />
|
||||||
5
src/pages/index.astro
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
import { defaultLanguage } from "~/config"
|
||||||
|
---
|
||||||
|
|
||||||
|
<meta http-equiv="refresh" content={`0;url=/${defaultLanguage}/`} />
|
||||||
34
src/pages/og/[...route].ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { OGImageRoute } from "astro-og-canvas"
|
||||||
|
import { defaultLanguage } from "~/config"
|
||||||
|
import { getPostsByLocale } from "~/utils"
|
||||||
|
|
||||||
|
const posts = await getPostsByLocale(defaultLanguage)
|
||||||
|
|
||||||
|
// Transform the collection into an object
|
||||||
|
// @ts-ignore
|
||||||
|
const pages = Object.fromEntries(posts.map(({ id, data }) => [id, { data }]))
|
||||||
|
|
||||||
|
export const { getStaticPaths, GET } = OGImageRoute({
|
||||||
|
// The name of your dynamic route segment.
|
||||||
|
// In this case it’s `route`, because the file is named `[...route].ts`.
|
||||||
|
param: "route",
|
||||||
|
|
||||||
|
// A collection of pages to generate images for.
|
||||||
|
pages,
|
||||||
|
// For each page, this callback will be used to customize the OG image.
|
||||||
|
getImageOptions: async (_, { data }: (typeof pages)[string]) => {
|
||||||
|
return {
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
bgGradient: [
|
||||||
|
[6, 38, 45],
|
||||||
|
[8, 3, 2],
|
||||||
|
],
|
||||||
|
logo: {
|
||||||
|
path: "./public/avatar.png",
|
||||||
|
size: [100],
|
||||||
|
},
|
||||||
|
fonts: ["./public/fonts/hwmc.otf"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
25
src/pages/rss.xml.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import rss from "@astrojs/rss"
|
||||||
|
import { de, defaultLanguage, en } from "~/config"
|
||||||
|
import { getPostsByLocale } from "~/utils"
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const posts = await getPostsByLocale(defaultLanguage)
|
||||||
|
const config = defaultLanguage === "en" ? en : de
|
||||||
|
|
||||||
|
return rss({
|
||||||
|
title: config.meta.title,
|
||||||
|
description: config.meta.description,
|
||||||
|
site:
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? "http://localhost:4321"
|
||||||
|
: config.meta.url,
|
||||||
|
items: posts.map((post: any) => ({
|
||||||
|
title: post.data.title,
|
||||||
|
description: post.data.description,
|
||||||
|
pubDate: post.data.pubDate,
|
||||||
|
link: `/posts/${post.id}/`,
|
||||||
|
content: post.rendered ? post.rendered.html : post.data.description,
|
||||||
|
})),
|
||||||
|
customData: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
15
src/styles/post.css
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
article code {
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
background-color: rgba(249, 115, 22, 0.5);
|
||||||
|
padding-left: 0.25rem;
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark article code {
|
||||||
|
background-color: rgba(249, 115, 22, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
article code::before,
|
||||||
|
article code::after {
|
||||||
|
content: none !important;
|
||||||
|
}
|
||||||
5
src/styles/post.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
article {
|
||||||
|
h2 {
|
||||||
|
margin: 2rem 0 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/styles/tailwind.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "@tailwindcss/typography";
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
1954
src/styles/twikoo.css
Normal file
27
src/styles/view-transition.css
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
/* Theme toggle effect */
|
||||||
|
/* https://theme-toggle.rdsx.dev/ */
|
||||||
|
/* https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API */
|
||||||
|
::view-transition-group(root) {
|
||||||
|
animation-timing-function: cubic-bezier(0.25, 1, 0.5, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root) {
|
||||||
|
mask: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><defs><filter id="blur"><feGaussianBlur stdDeviation="2"/></filter></defs><circle cx="0" cy="0" r="18" fill="white" filter="url(%23blur)"/></svg>')
|
||||||
|
top left / 0 no-repeat;
|
||||||
|
mask-origin: content-box;
|
||||||
|
animation: scale 1s;
|
||||||
|
transform-origin: top left;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(root),
|
||||||
|
.dark::view-transition-old(root) {
|
||||||
|
animation: scale 1s;
|
||||||
|
transform-origin: top left;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scale {
|
||||||
|
to {
|
||||||
|
mask-size: 350vmax;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/types/twikoo.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
declare module "twikoo"
|
||||||
29
src/utils/index.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { getCollection } from "astro:content"
|
||||||
|
|
||||||
|
export const formatDate = (
|
||||||
|
date: Date | string | undefined,
|
||||||
|
format: string = "YYYY-MM-DD",
|
||||||
|
): string => {
|
||||||
|
const validDate = date ? new Date(date) : new Date()
|
||||||
|
|
||||||
|
const tokens: Record<string, string> = {
|
||||||
|
YYYY: validDate.getFullYear().toString(),
|
||||||
|
MM: String(validDate.getMonth() + 1).padStart(2, "0"),
|
||||||
|
DD: String(validDate.getDate()).padStart(2, "0"),
|
||||||
|
HH: String(validDate.getHours()).padStart(2, "0"),
|
||||||
|
mm: String(validDate.getMinutes()).padStart(2, "0"),
|
||||||
|
ss: String(validDate.getSeconds()).padStart(2, "0"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) => tokens[match])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPostsByLocale = async (locale: string) => {
|
||||||
|
const posts =
|
||||||
|
locale === "en"
|
||||||
|
? await getCollection("enPosts")
|
||||||
|
: await getCollection("dePosts")
|
||||||
|
return posts.sort(
|
||||||
|
(a: any, b: any) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||||
|
)
|
||||||
|
}
|
||||||
7
src/utils/langs.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { langs } from "~/i18n/ui"
|
||||||
|
|
||||||
|
export function getLanguagePaths() {
|
||||||
|
return langs.map((lang) => ({
|
||||||
|
params: { lang },
|
||||||
|
}))
|
||||||
|
}
|
||||||
9
tailwind.config.mjs
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
|
||||||
|
darkMode: ["class"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [require("@tailwindcss/typography")],
|
||||||
|
}
|
||||||
17
tsconfig.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"include": [".astro/types.d.ts", "**/*", "eslint.config.cjs"],
|
||||||
|
"exclude": ["dist"],
|
||||||
|
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "react",
|
||||||
|
/* Path Aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"~/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"strictNullChecks": true, // add if using `base` template
|
||||||
|
"allowJs": true // required, and included with all Astro templates
|
||||||
|
}
|
||||||
|
}
|
||||||