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