82
.dockerignore
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Build outputs (will be generated during Docker build)
|
||||||
|
dist/
|
||||||
|
.astro/
|
||||||
|
|
||||||
|
# Dependencies (will be installed fresh in Docker)
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Git files
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.production
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
bun-debug.log*
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
ehthumbs.db
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# IDE files
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.settings/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
*.test.ts
|
||||||
|
*.test.tsx
|
||||||
|
*.spec.ts
|
||||||
|
*.spec.tsx
|
||||||
|
|
||||||
|
# Documentation
|
||||||
|
README.md
|
||||||
|
*.md
|
||||||
|
!CHANGELOG.md
|
||||||
|
|
||||||
|
# CI/CD (not needed in Docker image)
|
||||||
|
.github/
|
||||||
|
.gitlab-ci.yml
|
||||||
|
.travis.yml
|
||||||
|
.circleci/
|
||||||
|
|
||||||
|
# Temporary files
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
.cache/
|
||||||
|
.temp/
|
||||||
|
astro_tmp_pages_*
|
||||||
|
|
||||||
|
# Docker files (don't need to copy into itself)
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.yml
|
||||||
|
docker-compose.*.yml
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.editorconfig
|
||||||
|
.prettierignore
|
||||||
|
.eslintignore
|
||||||
|
|
||||||
11
.github/dependabot.yml
vendored
|
|
@ -1,11 +0,0 @@
|
||||||
# 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"
|
|
||||||
8
.vscode/settings.json
vendored
|
|
@ -1,12 +1,14 @@
|
||||||
{
|
{
|
||||||
"biome.enabled": false,
|
"biome.enabled": false,
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": false,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"[astro]": {
|
"[astro]": {
|
||||||
"editor.defaultFormatter": "astro-build.astro-vscode"
|
"editor.defaultFormatter": "astro-build.astro-vscode"
|
||||||
},
|
},
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit",
|
"source.fixAll.eslint": "explicit"
|
||||||
"source.organizeImports": "always"
|
},
|
||||||
|
"[mdx]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,26 +8,45 @@ import expressiveCode from "astro-expressive-code"
|
||||||
import { defineConfig } from "astro/config"
|
import { defineConfig } from "astro/config"
|
||||||
|
|
||||||
import robotsTxt from "astro-robots-txt"
|
import robotsTxt from "astro-robots-txt"
|
||||||
|
import addClasses from "rehype-class-names"
|
||||||
|
// import remarkRehype from "remark-rehype"
|
||||||
|
|
||||||
|
import og from "astro-og";
|
||||||
|
|
||||||
|
import node from "@astrojs/node";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
output: "static",
|
output: "static",
|
||||||
prefetch: true,
|
prefetch: true,
|
||||||
site: "https://astro-air.guoqi.dev",
|
site: import.meta.env.PROD ? "https://lio.cat" : 'http://localhost:4321',
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
remarkRehype: {},
|
||||||
|
rehypePlugins: [
|
||||||
|
[
|
||||||
|
addClasses, { a: 'markdown-link'}
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()],
|
plugins: [tailwindcss()],
|
||||||
|
server: {
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
|
|
||||||
},
|
},
|
||||||
integrations: [
|
|
||||||
react(),
|
integrations: [react(), sitemap(), expressiveCode({
|
||||||
sitemap(),
|
plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
|
||||||
expressiveCode({
|
themes: ["material-theme-lighter", "material-theme-darker"],
|
||||||
plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
|
defaultProps: {
|
||||||
themes: ["material-theme-lighter", "material-theme-darker"],
|
showLineNumbers: true,
|
||||||
defaultProps: {
|
},
|
||||||
showLineNumbers: true,
|
}), mdx({}), robotsTxt(), og()],
|
||||||
},
|
|
||||||
}),
|
adapter: node({
|
||||||
mdx(),
|
mode: "standalone",
|
||||||
robotsTxt(),
|
}),
|
||||||
],
|
|
||||||
})
|
})
|
||||||
22
components.json
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.mjs",
|
||||||
|
"css": "src/styles/tailwind.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "~/components",
|
||||||
|
"utils": "~/lib/utils",
|
||||||
|
"ui": "~/components/ui",
|
||||||
|
"lib": "~/lib",
|
||||||
|
"hooks": "~/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
77
package.json
|
|
@ -3,49 +3,64 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev --host 0.0.0.0",
|
||||||
"build": "astro check && astro build",
|
"build": "astro check && astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro",
|
"astro": "astro",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.5",
|
||||||
"@astrojs/mdx": "^4.0.7",
|
"@astrojs/mdx": "^4.3.11",
|
||||||
"@astrojs/react": "^4.2.0",
|
"@astrojs/node": "^9.5.1",
|
||||||
"@astrojs/rss": "^4.0.11",
|
"@astrojs/react": "^4.4.2",
|
||||||
"@astrojs/sitemap": "^3.2.1",
|
"@astrojs/rss": "^4.0.13",
|
||||||
"@expressive-code/plugin-collapsible-sections": "^0.40.1",
|
"@astrojs/sitemap": "^3.6.0",
|
||||||
"@expressive-code/plugin-line-numbers": "^0.41.2",
|
"@expressive-code/plugin-collapsible-sections": "^0.40.2",
|
||||||
"@tailwindcss/vite": "^4.0.3",
|
"@expressive-code/plugin-line-numbers": "^0.41.3",
|
||||||
"@types/react": "^19.0.8",
|
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||||
"@types/react-dom": "^19.0.3",
|
"@radix-ui/react-hover-card": "^1.1.15",
|
||||||
"astro": "^5.2.3",
|
"@tailwindcss/vite": "^4.1.17",
|
||||||
"astro-expressive-code": "^0.40.1",
|
"@types/react": "^19.2.6",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vercel/og": "^0.8.5",
|
||||||
|
"astro": "^5.15.9",
|
||||||
|
"astro-expressive-code": "^0.40.2",
|
||||||
"astro-google-analytics": "^1.0.3",
|
"astro-google-analytics": "^1.0.3",
|
||||||
"astro-og-canvas": "^0.7.0",
|
"astro-og": "^0.3.0",
|
||||||
|
"astro-og-canvas": "^0.7.2",
|
||||||
"astro-robots-txt": "^1.0.0",
|
"astro-robots-txt": "^1.0.0",
|
||||||
"canvaskit-wasm": "^0.40.0",
|
"canvaskit-wasm": "^0.40.0",
|
||||||
"lefthook": "^1.10.10",
|
"class-variance-authority": "^0.7.1",
|
||||||
"lucide-react": "^0.525.0",
|
"clsx": "^2.1.1",
|
||||||
"react": "^19.0.0",
|
"lefthook": "^1.13.6",
|
||||||
"react-dom": "^19.0.0",
|
"lucide-react": "^0.553.0",
|
||||||
"tailwindcss": "^4.0.3",
|
"react": "^19.2.0",
|
||||||
"twikoo": "^1.6.41",
|
"react-dom": "^19.2.0",
|
||||||
"typescript": "^5.7.3"
|
"rehype-class-names": "^2.0.0",
|
||||||
|
"remark-rehype": "^11.1.2",
|
||||||
|
"satori": "^0.18.3",
|
||||||
|
"satori-html": "^0.3.2",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"tw-animate-css": "^1.4.0",
|
||||||
|
"twikoo": "^1.6.44",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@types/dom-view-transitions": "^1.0.5",
|
"@types/dom-view-transitions": "^1.0.6",
|
||||||
"@types/node": "^22.13.1",
|
"@types/node": "^22.19.1",
|
||||||
"@types/sanitize-html": "^2.13.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"@typescript-eslint/parser": "^8.23.0",
|
"@typescript-eslint/parser": "^8.47.0",
|
||||||
"eslint": "^9.19.0",
|
"eslint": "^9.39.1",
|
||||||
"eslint-plugin-astro": "^1.3.1",
|
"eslint-plugin-astro": "^1.5.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.10.2",
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.6.2",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
"sass": "^1.83.4"
|
"sass": "^1.94.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 34 KiB |
BIN
public/fonts/JetBrainsMono-Bold.ttf
Normal file
BIN
public/fonts/jbm.ttf
Normal file
|
Before Width: | Height: | Size: 873 KiB After Width: | Height: | Size: 873 KiB |
BIN
public/images/general/avatar.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
public/images/general/logo-dark.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
public/images/general/logo-light.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
public/images/general/ogImage.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 448 KiB |
|
|
@ -9,12 +9,8 @@ const lang = getLangFromUrl(Astro.url)
|
||||||
const config = lang === "de" ? de : en
|
const config = lang === "de" ? de : en
|
||||||
---
|
---
|
||||||
|
|
||||||
<header class="flex h-24 w-full items-center justify-between">
|
<header class="flex h-24 w-auto flex-row items-center justify-end pr-5">
|
||||||
<a href="/" aria-label={`${config.siteName}`}>
|
<div class="flex items-center gap-6 dark:text-amber-400 text-blue-700">
|
||||||
<div class="text-2xl font-semibold">{config.siteName}</div>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="flex items-center gap-6">
|
|
||||||
{
|
{
|
||||||
config.rss && (
|
config.rss && (
|
||||||
<a
|
<a
|
||||||
|
|
@ -27,20 +23,6 @@ const config = lang === "de" ? de : en
|
||||||
</a>
|
</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 />
|
<LanguageToggle client:load />
|
||||||
<ThemeToggle client:load />
|
<ThemeToggle client:load />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,6 @@ const lang = getLangFromUrl(Astro.url)
|
||||||
const IntroContent = lang === "de" ? IntroContentDe : IntroContentEn
|
const IntroContent = lang === "de" ? IntroContentDe : IntroContentEn
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="my-10">
|
<div class="">
|
||||||
<IntroContent />
|
<IntroContent />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,61 +7,66 @@ const t = useTranslations(lang)
|
||||||
|
|
||||||
const { home, archive, custom, links, about } =
|
const { home, archive, custom, links, about } =
|
||||||
lang === "de" ? de.navigation : en.navigation
|
lang === "de" ? de.navigation : en.navigation
|
||||||
|
|
||||||
|
const linkClasses = `nav-links inline-block hover:underline hover:underline-offset-4 text-blue-700 dark:text-amber-400`
|
||||||
---
|
---
|
||||||
|
|
||||||
<div
|
<nav>
|
||||||
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"
|
<div class="flex flex-row flex-wrap gap-4 text-lg">
|
||||||
>
|
{
|
||||||
{
|
home && (
|
||||||
home && (
|
<a
|
||||||
<a
|
href={`/${lang}`}
|
||||||
href={`/${lang}`}
|
class={linkClasses}
|
||||||
class="hover:underline hover:underline-offset-4"
|
aria-label={t("nav.home")}
|
||||||
aria-label={t("nav.home")}
|
title={t("nav.home")}
|
||||||
title={t("nav.home")}
|
data-astro-prefetch="viewport"
|
||||||
data-astro-prefetch="viewport"
|
>
|
||||||
>
|
{t("nav.home")}
|
||||||
<p>{t("nav.home")}</p>
|
</a>
|
||||||
</a>
|
)
|
||||||
)
|
}
|
||||||
}
|
{
|
||||||
{
|
archive && (
|
||||||
archive && (
|
<a
|
||||||
<a
|
href={`/${lang}/archive`}
|
||||||
href={`/${lang}/archive`}
|
class={linkClasses}
|
||||||
class="hover:underline hover:underline-offset-4"
|
aria-label={t("nav.archive")}
|
||||||
aria-label={t("nav.archive")}
|
title={t("nav.archive")}
|
||||||
title={t("nav.archive")}
|
data-astro-prefetch="viewport"
|
||||||
data-astro-prefetch="viewport"
|
>
|
||||||
>
|
{t("nav.archive")}
|
||||||
<p>{t("nav.archive")}</p>
|
</a>
|
||||||
</a>
|
)
|
||||||
)
|
}
|
||||||
}
|
</div>
|
||||||
{
|
<div class="flex flex-row flex-wrap gap-4 text-lg pt-1">
|
||||||
custom?.map((tab) => (
|
{
|
||||||
<a
|
custom?.map((tab) => (
|
||||||
href={tab.link}
|
<a
|
||||||
class="hover:underline hover:underline-offset-4"
|
href={tab.link}
|
||||||
target="_blank"
|
class={linkClasses}
|
||||||
aria-label={tab.label}
|
target="_blank"
|
||||||
title={tab.label}
|
aria-label={tab.label}
|
||||||
data-astro-prefetch="viewport"
|
title={tab.label}
|
||||||
>
|
data-astro-prefetch="viewport"
|
||||||
<p>{tab.label}</p>
|
>
|
||||||
</a>
|
{tab.label}
|
||||||
))
|
</a>
|
||||||
}
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
links && (
|
links && (
|
||||||
<a
|
<a
|
||||||
href={`/${lang}/links`}
|
href={`/${lang}/links`}
|
||||||
class="hover:underline hover:underline-offset-4"
|
class={linkClasses}
|
||||||
aria-label={t("nav.links")}
|
aria-label={t("nav.links")}
|
||||||
title={t("nav.links")}
|
title={t("nav.links")}
|
||||||
data-astro-prefetch="viewport"
|
data-astro-prefetch="viewport"
|
||||||
>
|
>
|
||||||
<p>{t("nav.links")}</p>
|
{t("nav.links")}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -69,13 +74,13 @@ const { home, archive, custom, links, about } =
|
||||||
about && (
|
about && (
|
||||||
<a
|
<a
|
||||||
href={`/${lang}/about`}
|
href={`/${lang}/about`}
|
||||||
class="hover:underline hover:underline-offset-4"
|
class={linkClasses}
|
||||||
aria-label={t("nav.about")}
|
aria-label={t("nav.about")}
|
||||||
title={t("nav.about")}
|
title={t("nav.about")}
|
||||||
data-astro-prefetch="viewport"
|
data-astro-prefetch="viewport"
|
||||||
>
|
>
|
||||||
<p>{t("nav.about")}</p>
|
{t("nav.about")}
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -6,18 +6,18 @@ interface Props {
|
||||||
dateWidth?: string
|
dateWidth?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { post, lang, dateFormat = "default", dateWidth = "w-36" } = Astro.props
|
const { post, lang, dateFormat = "locale", dateWidth = "w-36" } = Astro.props
|
||||||
import { formatDate } from "~/utils"
|
import { formatDate } from "~/utils"
|
||||||
---
|
---
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href={`/${lang}/posts/${post.id}/`}
|
href={`/${lang}/posts/${post.id}/`}
|
||||||
class="my-3 flex visited:text-purple-500/90 md:my-2"
|
class="my-3 flex flex-col visited:text-purple-500/90 md:my-2 md:flex-row"
|
||||||
>
|
>
|
||||||
<p
|
<p
|
||||||
class={`flex ${dateWidth} flex-shrink-0 truncate text-gray-500 dark:text-gray-400`}
|
class={`flex ${dateWidth} flex-shrink-0 truncate text-sm text-gray-500 dark:text-gray-400 md:text-base`}
|
||||||
>
|
>
|
||||||
<time>{formatDate(post.data.pubDate, dateFormat)}</time>
|
<time>{formatDate(post.data.pubDate, dateFormat, lang)}</time>
|
||||||
</p>
|
</p>
|
||||||
<p class="line-clamp-2 text-lg hover:underline">{post.data.title}</p>
|
<p class="line-clamp-2 text-lg hover:underline">{post.data.title}</p>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,16 @@ const lang = getLangFromUrl(Astro.url)
|
||||||
const t = useTranslations(lang)
|
const t = useTranslations(lang)
|
||||||
|
|
||||||
const allPosts = await getPostsByLocale(lang)
|
const allPosts = await getPostsByLocale(lang)
|
||||||
const posts = allPosts.slice(0, common.latestPosts)
|
const posts = allPosts.slice(0, common.latestPosts).filter(p => p.data.published)
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="my-8 text-xl font-medium md:my-8">{t("blog.latest")}</div>
|
<div class="my-8 text-xl font-medium md:my-8">{(posts.length > 0) && t("blog.latest")}</div>
|
||||||
{
|
{ (posts.length > 0) &&
|
||||||
posts.map((post: any) => (
|
posts.map((post: any) => (
|
||||||
<PostList
|
<PostList
|
||||||
post={post}
|
post={post}
|
||||||
lang={lang}
|
lang={lang}
|
||||||
dateFormat="YYYY-MM-DD"
|
dateFormat="locale"
|
||||||
dateWidth="w-32"
|
dateWidth="w-32"
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,14 @@
|
||||||
export const age = new Date().getFullYear() - 2002
|
export const age = new Date().getFullYear() - 2002
|
||||||
|
|
||||||
<p>
|
they/them ⋅ lion ⋅ {age}yo ⋅ digital sorcerer and pixel wizard
|
||||||
they/them ⋅ {age}yo digital sorcerer and pixel wizard ⋅ self-hoster ⋅ loves
|
<br />
|
||||||
automation
|
- coo @ [pyro]
|
||||||
</p>
|
- writes things @ [eurofurence]
|
||||||
|
- ui @ [sofurry]
|
||||||
|
<br/>
|
||||||
|
you can find my professional work @ [datashard.work]
|
||||||
|
|
||||||
|
[pyro]:https://pyro.host
|
||||||
|
[eurofurence]:https://eurofurence.org
|
||||||
|
[sofurry]:https://sofurry.com
|
||||||
|
[datashard.work]:https://datashard.work
|
||||||
|
|
@ -1,35 +1,43 @@
|
||||||
import { Github, Twitter } from "lucide-react"
|
|
||||||
|
|
||||||
export const defaultLanguage: string = "en"
|
export const defaultLanguage: string = "en"
|
||||||
|
|
||||||
export const common = {
|
export const common = {
|
||||||
domain: "https://lio.cat",
|
domain: "https://lio.cat",
|
||||||
meta: {
|
meta: {
|
||||||
favicon: "/avatar.png",
|
favicon: "/images/general/avatar.png",
|
||||||
url: "https://lio.cat",
|
url: "https://lio.cat",
|
||||||
},
|
},
|
||||||
googleAnalyticsId: "",
|
googleAnalyticsId: "",
|
||||||
social: [
|
social: [
|
||||||
{
|
|
||||||
icon: Twitter,
|
|
||||||
label: "X",
|
|
||||||
link: "https://lio.to/twitter",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Github,
|
|
||||||
label: "GitHub",
|
|
||||||
link: "https://lio.to/github",
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
rss: true,
|
rss: true,
|
||||||
navigation: {
|
navigation: {
|
||||||
home: true,
|
home: true,
|
||||||
archive: true,
|
archive: false,
|
||||||
custom: [
|
custom: [
|
||||||
// {
|
{
|
||||||
// label: "CamLife",
|
label: "bluesky",
|
||||||
// link: "https://camlife.cn",
|
link: "https://lio.to/bluesky",
|
||||||
// },
|
},
|
||||||
|
{
|
||||||
|
label: "mastodon",
|
||||||
|
link: "https://lio.to/mastodon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "twitter",
|
||||||
|
link: "https://lio.to/twitter",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "github",
|
||||||
|
link: "https://lio.to/github",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "forge",
|
||||||
|
link: "https://lio.to/git",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "email",
|
||||||
|
link: "mailto:wrath@lio.cat",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
links: false,
|
links: false,
|
||||||
about: false,
|
about: false,
|
||||||
|
|
@ -56,12 +64,6 @@ export const de = {
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
...common.navigation,
|
...common.navigation,
|
||||||
custom: [
|
|
||||||
// {
|
|
||||||
// label: "影集",
|
|
||||||
// link: "https://camlife.cn",
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
pageMeta: {
|
pageMeta: {
|
||||||
archive: {
|
archive: {
|
||||||
|
|
@ -87,18 +89,13 @@ export const en = {
|
||||||
siteName: "Lio",
|
siteName: "Lio",
|
||||||
meta: {
|
meta: {
|
||||||
...common.meta,
|
...common.meta,
|
||||||
title: "Lio",
|
title: "fangmarks",
|
||||||
slogan: "fangmarks",
|
// slogan: "fangmarks",
|
||||||
description: "digital sorcerer and pixel wizard",
|
description: "digital sorcerer and pixel wizard",
|
||||||
},
|
},
|
||||||
navigation: {
|
navigation: {
|
||||||
...common.navigation,
|
...common.navigation,
|
||||||
custom: [
|
|
||||||
// {
|
|
||||||
// label: "CamLife",
|
|
||||||
// link: "https://camlife.cn",
|
|
||||||
// },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
pageMeta: {
|
pageMeta: {
|
||||||
archive: {
|
archive: {
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,10 @@ const postSchema = z.object({
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
pubDate: z.coerce.date(),
|
pubDate: z.coerce.date(),
|
||||||
updatedDate: z.coerce.date().optional(),
|
updatedDate: z.coerce.date().optional(),
|
||||||
heroImage: z.string().optional(),
|
slug: z.string().optional(),
|
||||||
ogImage: z.string().optional(),
|
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
|
published: z.boolean().optional().default(false),
|
||||||
|
lang: z.string().optional()
|
||||||
})
|
})
|
||||||
|
|
||||||
const enPostsCollection = defineCollection({
|
const enPostsCollection = defineCollection({
|
||||||
|
|
@ -18,7 +19,7 @@ const enPostsCollection = defineCollection({
|
||||||
|
|
||||||
const dePostsCollection = defineCollection({
|
const dePostsCollection = defineCollection({
|
||||||
loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/posts/de" }),
|
loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/posts/de" }),
|
||||||
schema: postSchema,
|
schema: postSchema
|
||||||
})
|
})
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
|
|
|
||||||
9
src/content/posts/en/friends-in-the-fandom.md
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
title: How to make Friends in the Fandom
|
||||||
|
pubDate: 2025-11-18
|
||||||
|
description: "This is the first post of my new Astro blog."
|
||||||
|
author: "@lio@pounced-on.me"
|
||||||
|
tags: ["furries", "making-friends"]
|
||||||
|
---
|
||||||
|
|
||||||
|
meow
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
---
|
|
||||||
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.
|
|
||||||
|
|
@ -9,25 +9,25 @@ export const langs = ["en", "de"]
|
||||||
|
|
||||||
export const ui = {
|
export const ui = {
|
||||||
en: {
|
en: {
|
||||||
"nav.home": "Home",
|
"nav.home": "home",
|
||||||
"nav.archive": "Archive",
|
"nav.archive": "archive",
|
||||||
"nav.about": "About",
|
"nav.about": "about",
|
||||||
"nav.links": "Links",
|
"nav.links": "links",
|
||||||
"blog.latest": "Latest Posts",
|
"blog.latest": "latest posts",
|
||||||
"archive.title": "All Posts",
|
"archive.title": "all posts",
|
||||||
"links.title": "My Friends",
|
"links.title": "my friends",
|
||||||
"tag.title": "Tag:",
|
"tag.title": "tag:",
|
||||||
"tag.no_posts": "No posts found for tag",
|
"tag.no_posts": "no posts found for tag",
|
||||||
},
|
},
|
||||||
de: {
|
de: {
|
||||||
"nav.home": "Start",
|
"nav.home": "start",
|
||||||
"nav.about": "Über mich",
|
"nav.about": "über mich",
|
||||||
"nav.archive": "Archiv",
|
"nav.archive": "archiv",
|
||||||
"nav.links": "Links",
|
"nav.links": "links",
|
||||||
"links.title": "Freunde",
|
"links.title": "freunde",
|
||||||
"blog.latest": "Letzte Posts",
|
"blog.latest": "letzte posts",
|
||||||
"archive.title": "Alle Posts",
|
"archive.title": "alle posts",
|
||||||
"tag.title": "Tag:",
|
"tag.title": "tag:",
|
||||||
"tag.no_posts": "Für diesen Tag wurden keine Posts gefunden",
|
"tag.no_posts": "für diesen tag wurden keine posts gefunden",
|
||||||
},
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
|
||||||
|
|
@ -6,16 +6,27 @@ import { en, de } from "~/config"
|
||||||
import { ClientRouter } from "astro:transitions"
|
import { ClientRouter } from "astro:transitions"
|
||||||
|
|
||||||
import "~/styles/tailwind.css"
|
import "~/styles/tailwind.css"
|
||||||
import "~/styles/view-transition.css"
|
// import "~/styles/view-transition.css"
|
||||||
|
|
||||||
const lang = getLangFromUrl(Astro.url)
|
const lang = getLangFromUrl(Astro.url)
|
||||||
|
|
||||||
const { title, description, ogImage } = Astro.props
|
const post = Astro.props.post
|
||||||
|
|
||||||
const ogImageURL = new URL(ogImage, Astro.site).href
|
const urls = {
|
||||||
|
postURL: `${Astro.url.pathname}/og.png`,
|
||||||
|
general: `/images/general/ogImage.png`,
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPost = Astro.url.pathname.includes("posts")
|
||||||
|
|
||||||
|
const ogImageURL = new URL(isPost ? urls.postURL : urls.general, Astro.site).href
|
||||||
const permalink = new URL(Astro.url.pathname, Astro.site).href
|
const permalink = new URL(Astro.url.pathname, Astro.site).href
|
||||||
|
|
||||||
const config = lang === "de" ? de : en
|
const config = lang === "de" ? de : en
|
||||||
|
const title = isPost
|
||||||
|
? `${post.title} - ${config.meta.title}`
|
||||||
|
: `${config.meta.description} - ${config.meta.title}`
|
||||||
|
const description = isPost ? post.description : config.meta.description
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
|
|
@ -25,30 +36,38 @@ const config = lang === "de" ? de : en
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="icon" type="image/png" href={config.meta.favicon} />
|
<link rel="icon" type="image/png" href={config.meta.favicon} />
|
||||||
<title>
|
<title>
|
||||||
{
|
{title}
|
||||||
title
|
|
||||||
? `${config.meta.title} - ${title}`
|
|
||||||
: `${config.meta.title} - ${config.meta.slogan}`
|
|
||||||
}
|
|
||||||
</title>
|
</title>
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
content={description ? description : config.meta.description}
|
content={description}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Open Graph / Facebook -->
|
<!-- Open Graph / Facebook -->
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content={permalink} />
|
<meta property="og:url" content={permalink} />
|
||||||
<meta property="og:title" content={title} />
|
<meta
|
||||||
<meta property="og:description" content={description} />
|
property="og:title"
|
||||||
|
content={title}
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content={description}
|
||||||
|
/>
|
||||||
<meta property="og:image" content={ogImageURL} />
|
<meta property="og:image" content={ogImageURL} />
|
||||||
|
|
||||||
<!-- Twitter -->
|
<!-- Twitter -->
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
<meta property="twitter:url" content={permalink} />
|
<meta property="twitter:url" content={permalink} />
|
||||||
<meta property="twitter:title" content={title} />
|
<meta
|
||||||
<meta property="twitter:description" content={description} />
|
property="twitter:title"
|
||||||
|
content={title}
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="twitter:description"
|
||||||
|
content={description}
|
||||||
|
/>
|
||||||
<meta property="twitter:image" content={ogImageURL} />
|
<meta property="twitter:image" content={ogImageURL} />
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
|
|
@ -82,21 +101,25 @@ const config = lang === "de" ? de : en
|
||||||
|
|
||||||
<script
|
<script
|
||||||
is:inline
|
is:inline
|
||||||
src="https://umami.guoqi.dev/script.js"
|
src="https://analytics.lio.systems/script.js"
|
||||||
data-website-id="759e9e56-20d3-463b-8d6e-abba5c53128b"
|
data-website-id="774c3c4f-205a-4b11-b844-3e659035ee38"
|
||||||
data-domains="astro-air.guoqi.dev"></script>
|
data-domains="lio.cat"></script>
|
||||||
|
|
||||||
{
|
|
||||||
config.googleAnalyticsId && (
|
|
||||||
<GoogleAnalytics id={config.googleAnalyticsId} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
<ClientRouter />
|
<ClientRouter />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class="flex min-h-screen w-full justify-center px-6 md:px-0 dark:bg-[#121212] dark:text-white"
|
class="flex min-h-screen w-full justify-center bg-amber-400 px-6 dark:bg-[#121212] dark:text-white"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
<NoiseBackground />
|
<!-- <NoiseBackground /> -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
|
<style is:global>
|
||||||
|
.markdown-link {
|
||||||
|
color: #1447e6;
|
||||||
|
}
|
||||||
|
html.dark .markdown-link {
|
||||||
|
color: #ffb900;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,37 @@
|
||||||
// import Comments from "~/components/astro/comments.astro"
|
// import Comments from "~/components/astro/comments.astro"
|
||||||
import Header from "~/components/astro/header.astro"
|
import Header from "~/components/astro/header.astro"
|
||||||
import Navigation from "~/components/astro/nav.astro"
|
import Navigation from "~/components/astro/nav.astro"
|
||||||
|
import { de, en } from "~/config"
|
||||||
|
import { getLangFromUrl } from "~/i18n/utils"
|
||||||
import BaseLayout from "~/layouts/base.astro"
|
import BaseLayout from "~/layouts/base.astro"
|
||||||
|
import '@fontsource-variable/jetbrains-mono';
|
||||||
|
|
||||||
const { title, description, ogImage, needComment } = Astro.props
|
const { title, description, post } = Astro.props
|
||||||
const filename = Astro.url.pathname.split("/").filter(Boolean).pop() ?? ""
|
const lang = getLangFromUrl(Astro.url)
|
||||||
const openGraphImage = !ogImage ? `/og/${filename}.png` : ogImage
|
const config = lang === "de" ? de : en
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout
|
<BaseLayout
|
||||||
title={title}
|
title={title}
|
||||||
description={description}
|
description={description}
|
||||||
ogImage={openGraphImage}
|
post={post}
|
||||||
needComment={needComment}
|
|
||||||
>
|
>
|
||||||
<main class="max-auto mb-10 w-full max-w-3xl">
|
<div class="flex w-full max-w-4xl flex-col pt-5 p-2">
|
||||||
<Header />
|
<div class=" flex w-full items-center justify-end pb-4">
|
||||||
<Navigation />
|
<a href="/" aria-label={`${config.siteName}`} class="mr-auto">
|
||||||
<slot />
|
<!-- <div class="text-4xl font-semibold">{config.siteName}</div> -->
|
||||||
<!-- <div class="mt-20">
|
<img src="/images/general/logo-dark.png" alt="" class="dark:hidden">
|
||||||
{needComment && <Comments />}
|
<img src="/images/general/logo-light.png" alt="" class="hidden dark:block">
|
||||||
</div> -->
|
</a>
|
||||||
</main>
|
<Header />
|
||||||
|
</div>
|
||||||
|
<div class="flex w-full flex-col">
|
||||||
|
<aside class="flex flex-col ">
|
||||||
|
<Navigation />
|
||||||
|
</aside>
|
||||||
|
<main class="max-auto mb-10 w-full max-w-3xl flex-1 pt-5">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
|
||||||
6
src/lib/utils.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@ import MainLayout from "~/layouts/main.astro"
|
||||||
<MainLayout title="404" description="Error 404 page not found." noindex={true}>
|
<MainLayout title="404" description="Error 404 page not found." noindex={true}>
|
||||||
<section class="flex min-h-[60vh] items-center justify-center">
|
<section class="flex min-h-[60vh] items-center justify-center">
|
||||||
<div class="mx-auto max-w-xl px-4 text-center">
|
<div class="mx-auto max-w-xl px-4 text-center">
|
||||||
<img class="block" src="/images/page-meta/general/404.png" />
|
<img class="block" src="/images/general/404.png" />
|
||||||
<div class="mt-4 text-gray-600 dark:text-gray-400">
|
<div class="mt-4 text-gray-600 dark:text-gray-400">
|
||||||
<p class="text-lg">
|
<p class="text-lg">
|
||||||
The page you're looking for doesn't exist or has been moved.
|
The page you're looking for doesn't exist or has been moved.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function getStaticPaths() {
|
||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<MainLayout ogImage="/preview.png">
|
<MainLayout>
|
||||||
<Intro />
|
<Intro />
|
||||||
<RecentBlogs />
|
<RecentBlogs />
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|
|
||||||
|
|
@ -27,14 +27,15 @@ const lang = getLangFromUrl(Astro.url)
|
||||||
const { post } = Astro.props
|
const { post } = Astro.props
|
||||||
|
|
||||||
const { Content } = await render(post)
|
const { Content } = await render(post)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<MainLayout {...post.data}>
|
<MainLayout post={{...post.data}}>
|
||||||
<article class="prose dark:prose-invert w-full max-w-3xl overflow-hidden">
|
<article class="prose dark:prose-invert w-full max-w-3xl overflow-hidden">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<h2 class="!my-0 text-3xl font-semibold">{post.data.title}</h2>
|
<h2 class="!my-0 text-3xl font-semibold">{post.data.title}</h2>
|
||||||
<div class="my-3 text-gray-500 dark:text-white/80">
|
<div class="my-3 text-gray-500 dark:text-white/80">
|
||||||
{formatDate(post.data.pubDate)}
|
{formatDate(post.data.pubDate, 'locale', lang)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
115
src/pages/[lang]/posts/[slug]/og.png.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
import { getCollection, type CollectionEntry } from "astro:content"
|
||||||
|
import fs from "fs"
|
||||||
|
import { join, resolve } from "node:path"
|
||||||
|
import { ImageResponse } from "@vercel/og"
|
||||||
|
import { getPostsByLocale } from "~/utils"
|
||||||
|
import type { ReactElement } from "react"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
params: { slug: string }
|
||||||
|
props: { post: CollectionEntry<"enPosts" | "dePosts"> }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const blogPostsEN = await getPostsByLocale("en")
|
||||||
|
const blogPostsDE = await getPostsByLocale("de")
|
||||||
|
const posts = [...blogPostsEN, ...blogPostsDE]
|
||||||
|
return posts.map((post) => ({
|
||||||
|
params: { slug: post.id, lang: post.data.lang },
|
||||||
|
props: { post },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET({ props }: Props) {
|
||||||
|
const options = {
|
||||||
|
width: 1200,
|
||||||
|
height: 600,
|
||||||
|
padding: 10,
|
||||||
|
color: '#FFB900'
|
||||||
|
}
|
||||||
|
const { post } = props
|
||||||
|
const path = join(process.cwd(), "public", "fonts", "JetBrainsMono-Bold.ttf")
|
||||||
|
// using custom font files
|
||||||
|
const jetbrainsMono = fs.readFileSync(path)
|
||||||
|
|
||||||
|
// Astro doesn't support tsx endpoints so usign React-element objects
|
||||||
|
const html = {
|
||||||
|
type: "div",
|
||||||
|
key: 'extra-margin',
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: "flex",
|
||||||
|
width: options.width,
|
||||||
|
height: options.height,
|
||||||
|
backgroundColor: options.color,
|
||||||
|
padding: options.padding,
|
||||||
|
boxSizing: "border-box",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "div",
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
display: "flex",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: options.color,
|
||||||
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
type: "img",
|
||||||
|
props: {
|
||||||
|
src: "https://pogge.rs/i/36b0b9ec4980.png",
|
||||||
|
alt: "logo",
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
top: 32,
|
||||||
|
right: 32,
|
||||||
|
width: 465.18,
|
||||||
|
height: 141,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "div",
|
||||||
|
props: {
|
||||||
|
style: {
|
||||||
|
position: "absolute",
|
||||||
|
left: 48,
|
||||||
|
bottom: 48,
|
||||||
|
width: 1000,
|
||||||
|
color: "#222",
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: 58,
|
||||||
|
lineHeight: 1.2,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
textAlign: "left",
|
||||||
|
display: "block",
|
||||||
|
},
|
||||||
|
children: post.data.title,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
} as unknown as ReactElement;
|
||||||
|
|
||||||
|
return new ImageResponse(html, {
|
||||||
|
width: options.width,
|
||||||
|
height: options.height,
|
||||||
|
fonts: [
|
||||||
|
{
|
||||||
|
name: "monospace",
|
||||||
|
// @ts-ignore
|
||||||
|
data: jetbrainsMono.buffer,
|
||||||
|
style: "normal",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
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"],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -1,3 +1,30 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
@plugin "@tailwindcss/typography";
|
@plugin "@tailwindcss/typography";
|
||||||
|
@plugin "tailwindcss-animate";
|
||||||
@custom-variant dark (&:where(.dark, .dark *));
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
@custom-variant light (&:where(.light, .light *));
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'JetBrains Mono Variable', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links, .markdown-link {
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
padding-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-link::after, .nav-links::after {
|
||||||
|
content: "↗";
|
||||||
|
position: absolute;
|
||||||
|
right: 0.1em;
|
||||||
|
top: 0;
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,20 @@
|
||||||
import { getCollection } from "astro:content"
|
import { getCollection } from "astro:content"
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
export const formatDate = (
|
export const formatDate = (
|
||||||
date: Date | string | undefined,
|
date: Date | string | undefined,
|
||||||
format: string = "YYYY-MM-DD",
|
format: string = "YYYY-MM-DD",
|
||||||
|
locale?:string
|
||||||
): string => {
|
): string => {
|
||||||
const validDate = date ? new Date(date) : new Date()
|
const validDate = date ? new Date(date) : new Date()
|
||||||
|
|
||||||
|
if(format === "locale") return validDate.toLocaleString(locale, {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
|
||||||
const tokens: Record<string, string> = {
|
const tokens: Record<string, string> = {
|
||||||
YYYY: validDate.getFullYear().toString(),
|
YYYY: validDate.getFullYear().toString(),
|
||||||
MM: String(validDate.getMonth() + 1).padStart(2, "0"),
|
MM: String(validDate.getMonth() + 1).padStart(2, "0"),
|
||||||
|
|
@ -23,7 +32,15 @@ export const getPostsByLocale = async (locale: string) => {
|
||||||
locale === "en"
|
locale === "en"
|
||||||
? await getCollection("enPosts")
|
? await getCollection("enPosts")
|
||||||
: await getCollection("dePosts")
|
: await getCollection("dePosts")
|
||||||
|
// Add the locale to the data of each post
|
||||||
|
posts.forEach((post: any) => {
|
||||||
|
post.data.lang = locale;
|
||||||
|
});
|
||||||
return posts.sort(
|
return posts.sort(
|
||||||
(a: any, b: any) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
(a: any, b: any) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
|
||||||
72
src/utils/plugins/components.ts
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import type { Link } from "mdast"
|
||||||
|
import type { Plugin } from "unified"
|
||||||
|
import { visit } from "unist-util-visit"
|
||||||
|
|
||||||
|
interface CustomLinkOptions {
|
||||||
|
/**
|
||||||
|
* Custom class name to add to all links
|
||||||
|
*/
|
||||||
|
className?: string
|
||||||
|
/**
|
||||||
|
* Function to determine if a link should open in a new tab
|
||||||
|
* Default: opens external links (starting with http/https) in new tab
|
||||||
|
*/
|
||||||
|
shouldOpenInNewTab?: (url: string) => boolean
|
||||||
|
/**
|
||||||
|
* Custom attributes to add to links
|
||||||
|
*/
|
||||||
|
attributes?: (url: string, title?: string | null) => Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom remark plugin that transforms link nodes in markdown
|
||||||
|
* to customize how <a> tags are rendered
|
||||||
|
*/
|
||||||
|
export function remarkCustomLinks(options: CustomLinkOptions = {}): Plugin {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
shouldOpenInNewTab = (url: string) => {
|
||||||
|
// Default: open external links in new tab
|
||||||
|
return url.startsWith("http://") || url.startsWith("https://")
|
||||||
|
},
|
||||||
|
attributes,
|
||||||
|
} = options
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
return (tree: any) => {
|
||||||
|
visit(tree, "link", (node: Link) => {
|
||||||
|
const url = node.url
|
||||||
|
const title = node.title
|
||||||
|
|
||||||
|
// Initialize data.hProperties if it doesn't exist
|
||||||
|
if (!node.data) {
|
||||||
|
node.data = {}
|
||||||
|
}
|
||||||
|
if (!node.data.hProperties) {
|
||||||
|
node.data.hProperties = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom className if provided
|
||||||
|
if (className) {
|
||||||
|
const existingClass = node.data.hProperties.class
|
||||||
|
node.data.hProperties.class = existingClass
|
||||||
|
? `${existingClass} ${className}`
|
||||||
|
: className
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add target="_blank" and rel="noopener noreferrer" for external links
|
||||||
|
if (shouldOpenInNewTab(url)) {
|
||||||
|
node.data.hProperties.target = "_blank"
|
||||||
|
node.data.hProperties.rel = "noopener noreferrer"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom attributes if provided
|
||||||
|
if (attributes) {
|
||||||
|
const customAttrs = attributes(url, title)
|
||||||
|
Object.assign(node.data.hProperties, customAttrs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -9,7 +9,8 @@
|
||||||
/* Path Aliases */
|
/* Path Aliases */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"],
|
||||||
|
"@/*": ["./src/*"]
|
||||||
},
|
},
|
||||||
"strictNullChecks": true, // add if using `base` template
|
"strictNullChecks": true, // add if using `base` template
|
||||||
"allowJs": true // required, and included with all Astro templates
|
"allowJs": true // required, and included with all Astro templates
|
||||||
|
|
|
||||||