This commit is contained in:
Lio 2025-11-14 13:38:06 +01:00
commit 4df091aab5
74 changed files with 5367 additions and 1 deletions

11
.github/dependabot.yml vendored Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,6 @@
# lockfiles
package-lock.json
pnpm-lock.yaml
# dependencies
node_modules

16
.prettierrc.mjs Normal file
View 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
View 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
View 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
View 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
View 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.

View file

@ -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 ⭐️
[![Built with Astro](https://astro.badg.es/v1/built-with-astro/tiny.svg)](https://astro.build) [![Netlify Status](https://api.netlify.com/api/v1/badges/3e2c71b9-071f-4846-9321-41c949134ebf/deploy-status)](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
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/sun0225SUN/astro-air)
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](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
View 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(),
],
})

1734
bun.lock Normal file

File diff suppressed because it is too large Load diff

12
eslint.config.cjs Normal file
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

BIN
public/fonts/hwmc.otf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 KiB

23
public/links/astro.svg Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
public/preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

View file

@ -0,0 +1,32 @@
<!-- ---
const currentYear = new Date().getFullYear()
---
<footer class="flex flex-wrap items-center gap-1 py-10 opacity-25">
<p>&copy; {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> -->

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

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

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

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

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

View 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"
/>
))
}

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

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

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

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

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

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

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

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

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

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

View 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: "",
})
}

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

@ -0,0 +1,5 @@
---
import { defaultLanguage } from "~/config"
---
<meta http-equiv="refresh" content={`0;url=/${defaultLanguage}/`} />

View 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 its `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
View 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
View 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
View file

@ -0,0 +1,5 @@
article {
h2 {
margin: 2rem 0 !important;
}
}

3
src/styles/tailwind.css Normal file
View file

@ -0,0 +1,3 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@custom-variant dark (&:where(.dark, .dark *));

1954
src/styles/twikoo.css Normal file

File diff suppressed because it is too large Load diff

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

@ -0,0 +1 @@
declare module "twikoo"

29
src/utils/index.ts Normal file
View 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
View 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
View 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
View 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
}
}