checkin
Some checks are pending
ci/woodpecker/push/build Pipeline is running

This commit is contained in:
Lio 2025-11-18 22:53:10 +01:00
parent 7470b7bff2
commit 9f62303e6f
39 changed files with 1301 additions and 628 deletions

82
.dockerignore Normal file
View 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

View file

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

View file

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

View file

@ -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(), },
sitemap(),
expressiveCode({ integrations: [react(), sitemap(), expressiveCode({
plugins: [pluginCollapsibleSections(), pluginLineNumbers()], plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
themes: ["material-theme-lighter", "material-theme-darker"], themes: ["material-theme-lighter", "material-theme-darker"],
defaultProps: { defaultProps: {
showLineNumbers: true, showLineNumbers: true,
}, },
}), mdx({}), robotsTxt(), og()],
adapter: node({
mode: "standalone",
}), }),
mdx(),
robotsTxt(),
],
}) })

993
bun.lock

File diff suppressed because it is too large Load diff

22
components.json Normal file
View 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": {}
}

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

BIN
public/fonts/jbm.ttf Normal file

Binary file not shown.

View file

Before

Width:  |  Height:  |  Size: 873 KiB

After

Width:  |  Height:  |  Size: 873 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 KiB

View file

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

View file

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

View file

@ -7,21 +7,22 @@ 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="hover:underline hover:underline-offset-4" class={linkClasses}
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"
> >
<p>{t("nav.home")}</p> {t("nav.home")}
</a> </a>
) )
} }
@ -29,39 +30,43 @@ const { home, archive, custom, links, about } =
archive && ( archive && (
<a <a
href={`/${lang}/archive`} href={`/${lang}/archive`}
class="hover:underline hover:underline-offset-4" class={linkClasses}
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"
> >
<p>{t("nav.archive")}</p> {t("nav.archive")}
</a> </a>
) )
} }
</div>
<div class="flex flex-row flex-wrap gap-4 text-lg pt-1">
{ {
custom?.map((tab) => ( custom?.map((tab) => (
<a <a
href={tab.link} href={tab.link}
class="hover:underline hover:underline-offset-4" class={linkClasses}
target="_blank" target="_blank"
aria-label={tab.label} aria-label={tab.label}
title={tab.label} title={tab.label}
data-astro-prefetch="viewport" data-astro-prefetch="viewport"
> >
<p>{tab.label}</p> {tab.label}
</a> </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>

View file

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

View file

@ -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"
/> />
)) ))

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

@ -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">
<div class=" flex w-full items-center justify-end pb-4">
<a href="/" aria-label={`${config.siteName}`} class="mr-auto">
<!-- <div class="text-4xl font-semibold">{config.siteName}</div> -->
<img src="/images/general/logo-dark.png" alt="" class="dark:hidden">
<img src="/images/general/logo-light.png" alt="" class="hidden dark:block">
</a>
<Header /> <Header />
</div>
<div class="flex w-full flex-col">
<aside class="flex flex-col ">
<Navigation /> <Navigation />
</aside>
<main class="max-auto mb-10 w-full max-w-3xl flex-1 pt-5">
<slot /> <slot />
<!-- <div class="mt-20">
{needComment && <Comments />}
</div> -->
</main> </main>
</div>
</div>
</BaseLayout> </BaseLayout>

6
src/lib/utils.ts Normal file
View 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))
}

View file

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

View file

@ -10,7 +10,7 @@ export function getStaticPaths() {
} }
--- ---
<MainLayout ogImage="/preview.png"> <MainLayout>
<Intro /> <Intro />
<RecentBlogs /> <RecentBlogs />
<Footer /> <Footer />

View file

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

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

View file

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

View file

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

View file

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

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

View file

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