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,
"editor.formatOnSave": true,
"editor.formatOnSave": false,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[astro]": {
"editor.defaultFormatter": "astro-build.astro-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "always"
"source.fixAll.eslint": "explicit"
},
"[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 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
export default defineConfig({
output: "static",
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: {
plugins: [tailwindcss()],
server: {
allowedHosts: true,
},
},
integrations: [
react(),
sitemap(),
expressiveCode({
plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
themes: ["material-theme-lighter", "material-theme-darker"],
defaultProps: {
showLineNumbers: true,
},
}),
mdx(),
robotsTxt(),
],
})
integrations: [react(), sitemap(), expressiveCode({
plugins: [pluginCollapsibleSections(), pluginLineNumbers()],
themes: ["material-theme-lighter", "material-theme-darker"],
defaultProps: {
showLineNumbers: true,
},
}), mdx({}), robotsTxt(), og()],
adapter: node({
mode: "standalone",
}),
})

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",
"version": "0.0.1",
"scripts": {
"dev": "astro dev",
"dev": "astro dev --host 0.0.0.0",
"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",
"@astrojs/check": "^0.9.5",
"@astrojs/mdx": "^4.3.11",
"@astrojs/node": "^9.5.1",
"@astrojs/react": "^4.4.2",
"@astrojs/rss": "^4.0.13",
"@astrojs/sitemap": "^3.6.0",
"@expressive-code/plugin-collapsible-sections": "^0.40.2",
"@expressive-code/plugin-line-numbers": "^0.41.3",
"@fontsource-variable/jetbrains-mono": "^5.2.8",
"@radix-ui/react-hover-card": "^1.1.15",
"@tailwindcss/vite": "^4.1.17",
"@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-og-canvas": "^0.7.0",
"astro-og": "^0.3.0",
"astro-og-canvas": "^0.7.2",
"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"
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lefthook": "^1.13.6",
"lucide-react": "^0.553.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"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": {
"@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",
"@tailwindcss/typography": "^0.5.19",
"@types/dom-view-transitions": "^1.0.6",
"@types/node": "^22.19.1",
"@types/sanitize-html": "^2.16.0",
"@typescript-eslint/parser": "^8.47.0",
"eslint": "^9.39.1",
"eslint-plugin-astro": "^1.5.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"prettier": "^3.4.2",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.11",
"sass": "^1.83.4"
"prettier-plugin-tailwindcss": "^0.6.14",
"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
---
<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">
<header class="flex h-24 w-auto flex-row items-center justify-end pr-5">
<div class="flex items-center gap-6 dark:text-amber-400 text-blue-700">
{
config.rss && (
<a
@ -27,20 +23,6 @@ const config = lang === "de" ? de : en
</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>

View file

@ -7,6 +7,6 @@ const lang = getLangFromUrl(Astro.url)
const IntroContent = lang === "de" ? IntroContentDe : IntroContentEn
---
<div class="my-10">
<div class="">
<IntroContent />
</div>

View file

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

View file

@ -6,18 +6,18 @@ interface Props {
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"
---
<a
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
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 class="line-clamp-2 text-lg hover:underline">{post.data.title}</p>
</a>

View file

@ -8,16 +8,16 @@ const lang = getLangFromUrl(Astro.url)
const t = useTranslations(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) => (
<PostList
post={post}
lang={lang}
dateFormat="YYYY-MM-DD"
dateFormat="locale"
dateWidth="w-32"
/>
))

View file

@ -1,6 +1,14 @@
export const age = new Date().getFullYear() - 2002
<p>
they/them ⋅ {age}yo digital sorcerer and pixel wizard ⋅ self-hoster ⋅ loves
automation
</p>
they/them ⋅ lion ⋅ {age}yo ⋅ digital sorcerer and pixel wizard
<br />
- coo @ [pyro]
- 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 common = {
domain: "https://lio.cat",
meta: {
favicon: "/avatar.png",
favicon: "/images/general/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,
archive: false,
custom: [
// {
// label: "CamLife",
// link: "https://camlife.cn",
// },
{
label: "bluesky",
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,
about: false,
@ -56,12 +64,6 @@ export const de = {
},
navigation: {
...common.navigation,
custom: [
// {
// label: "影集",
// link: "https://camlife.cn",
// },
],
},
pageMeta: {
archive: {
@ -87,18 +89,13 @@ export const en = {
siteName: "Lio",
meta: {
...common.meta,
title: "Lio",
slogan: "fangmarks",
title: "fangmarks",
// slogan: "fangmarks",
description: "digital sorcerer and pixel wizard",
},
navigation: {
...common.navigation,
custom: [
// {
// label: "CamLife",
// link: "https://camlife.cn",
// },
],
},
pageMeta: {
archive: {

View file

@ -6,9 +6,10 @@ const postSchema = z.object({
description: z.string(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
heroImage: z.string().optional(),
ogImage: z.string().optional(),
slug: z.string().optional(),
tags: z.array(z.string()).optional(),
published: z.boolean().optional().default(false),
lang: z.string().optional()
})
const enPostsCollection = defineCollection({
@ -18,7 +19,7 @@ const enPostsCollection = defineCollection({
const dePostsCollection = defineCollection({
loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/posts/de" }),
schema: postSchema,
schema: postSchema
})
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 = {
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",
"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",
"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

View file

@ -6,16 +6,27 @@ import { en, de } from "~/config"
import { ClientRouter } from "astro:transitions"
import "~/styles/tailwind.css"
import "~/styles/view-transition.css"
// import "~/styles/view-transition.css"
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 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>
@ -25,30 +36,38 @@ const config = lang === "de" ? de : en
<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}
</title>
<meta name="generator" content={Astro.generator} />
<meta
name="description"
content={description ? description : config.meta.description}
content={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: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:title"
content={title}
/>
<meta
property="twitter:description"
content={description}
/>
<meta property="twitter:image" content={ogImageURL} />
<script is:inline>
@ -82,21 +101,25 @@ const config = lang === "de" ? de : en
<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>
src="https://analytics.lio.systems/script.js"
data-website-id="774c3c4f-205a-4b11-b844-3e659035ee38"
data-domains="lio.cat"></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"
class="flex min-h-screen w-full justify-center bg-amber-400 px-6 dark:bg-[#121212] dark:text-white"
>
<slot />
<NoiseBackground />
<!-- <NoiseBackground /> -->
</body>
</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 Header from "~/components/astro/header.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 '@fontsource-variable/jetbrains-mono';
const { title, description, ogImage, needComment } = Astro.props
const filename = Astro.url.pathname.split("/").filter(Boolean).pop() ?? ""
const openGraphImage = !ogImage ? `/og/${filename}.png` : ogImage
const { title, description, post } = Astro.props
const lang = getLangFromUrl(Astro.url)
const config = lang === "de" ? de : en
---
<BaseLayout
title={title}
description={description}
ogImage={openGraphImage}
needComment={needComment}
post={post}
>
<main class="max-auto mb-10 w-full max-w-3xl">
<Header />
<Navigation />
<slot />
<!-- <div class="mt-20">
{needComment && <Comments />}
</div> -->
</main>
<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 />
</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>

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}>
<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" />
<img class="block" src="/images/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.

View file

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

View file

@ -27,14 +27,15 @@ const lang = getLangFromUrl(Astro.url)
const { post } = Astro.props
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">
<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)}
{formatDate(post.data.pubDate, 'locale', lang)}
</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 "tw-animate-css";
@plugin "@tailwindcss/typography";
@plugin "tailwindcss-animate";
@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 { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export const formatDate = (
date: Date | string | undefined,
format: string = "YYYY-MM-DD",
locale?:string
): string => {
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> = {
YYYY: validDate.getFullYear().toString(),
MM: String(validDate.getMonth() + 1).padStart(2, "0"),
@ -23,7 +32,15 @@ export const getPostsByLocale = async (locale: string) => {
locale === "en"
? await getCollection("enPosts")
: await getCollection("dePosts")
// Add the locale to the data of each post
posts.forEach((post: any) => {
post.data.lang = locale;
});
return posts.sort(
(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 */
"baseUrl": ".",
"paths": {
"~/*": ["./src/*"]
"~/*": ["./src/*"],
"@/*": ["./src/*"]
},
"strictNullChecks": true, // add if using `base` template
"allowJs": true // required, and included with all Astro templates