Added 404, Linktree, moved blog to /blog from index, improved markdown frontmatter to accept tags

This commit is contained in:
2026-06-07 22:51:22 +02:00
parent 87dab6e5a1
commit d0b9fd5144
12 changed files with 448 additions and 31 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+13 -3
View File
@@ -1,4 +1,12 @@
---
import type { Props as AstroProps } from 'astro';
interface Props {
tags?: string[];
}
const { tags = [] } = Astro.props;
const selectedTag = Astro.url.searchParams.get('tag');
const links = [
{ label: "github", href: "https://github.com/DerGrumpf", icon: "/github.png" },
{ label: "gitea", href: "https://git.cyperpunk.de", icon: "/git.png" },
@@ -32,9 +40,11 @@ const links = [
<div id="sidebar-topics" class="flex flex-col gap-2">
<span class="section-label text-ctp-overlay text-xs uppercase tracking-widest">topics</span>
<div id="tags" class="flex flex-wrap gap-1">
<span class="tag text-xs px-2 py-0.5 rounded-full bg-ctp-surface text-ctp-subtext hover:text-ctp-accent hover:bg-ctp-overlay cursor-pointer transition-colors">Blog</span>
<span class="tag text-xs px-2 py-0.5 rounded-full bg-ctp-surface text-ctp-subtext hover:text-ctp-accent hover:bg-ctp-overlay cursor-pointer transition-colors">Git Projects</span>
<span class="tag text-xs px-2 py-0.5 rounded-full bg-ctp-surface text-ctp-subtext hover:text-ctp-accent hover:bg-ctp-overlay cursor-pointer transition-colors">Matrix</span>
{tags.map((tag) => (
<a href={`/?tag=${tag}`} class={`tag text-xs px-2 py-0.5 rounded-full cursor-pointer transition-colors ${selectedTag === tag ? 'bg-ctp-accent text-ctp-base' : 'bg-ctp-surface text-ctp-subtext hover:text-ctp-accent'}`}>
{tag}
</a>
))}
</div>
</div>
+243
View File
@@ -0,0 +1,243 @@
---
---
<canvas id="snake-canvas" class="rounded-lg" style="
padding: 2px;
border: 2px solid transparent;
background-image: linear-gradient(var(--ctp-base), var(--ctp-base)), linear-gradient(to right, var(--ctp-red), var(--ctp-peach), var(--ctp-yellow), var(--ctp-green), var(--ctp-teal), var(--ctp-blue), var(--ctp-mauve));
background-origin: border-box;
background-clip: padding-box, border-box;
"></canvas>
<script>
const canvas = document.getElementById('snake-canvas') as HTMLCanvasElement;
const size = 15;
const cols = 27;
const gameRows = 26;
const uiHeight = 30;
canvas.width = cols * size;
canvas.height = gameRows * size + uiHeight;
const ctx = canvas.getContext('2d')!;
const rows = gameRows;
let snake: {x: number, y: number}[] = [];
let dir = { x: 1, y: 0 };
let food = { x: 0, y: 0 };
let running = false;
let aiMode = false;
let aiPath: {x: number, y: number}[] = [];
let interval: ReturnType<typeof setInterval> | null = null;
function getColor(v: string) {
return getComputedStyle(document.documentElement).getPropertyValue(v).trim();
}
function randomFood() {
let f;
do { f = { x: Math.floor(Math.random() * cols), y: Math.floor(Math.random() * rows) }; }
while (snake.some(s => s.x === f.x && s.y === f.y));
return f;
}
function astar(start: {x:number,y:number}, goal: {x:number,y:number}, blocked: Set<string>) {
const key = (p: {x:number,y:number}) => `${p.x},${p.y}`;
const h = (p: {x:number,y:number}) => Math.abs(p.x - goal.x) + Math.abs(p.y - goal.y);
const open = [{ pos: start, g: 0, f: h(start), path: [] as {x:number,y:number}[] }];
const visited = new Set<string>();
while (open.length) {
open.sort((a, b) => a.f - b.f);
const current = open.shift()!;
const k = key(current.pos);
if (visited.has(k)) continue;
visited.add(k);
if (current.pos.x === goal.x && current.pos.y === goal.y) return current.path;
for (const d of [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}]) {
const next = { x: current.pos.x + d.x, y: current.pos.y + d.y };
if (next.x < 0 || next.x >= cols || next.y < 0 || next.y >= rows) continue;
if (blocked.has(key(next))) continue;
if (visited.has(key(next))) continue;
const g = current.g + 1;
open.push({ pos: next, g, f: g + h(next), path: [...current.path, next] });
}
}
return null;
}
function aiStep() {
const blocked = new Set(snake.slice(0, -1).map(s => `${s.x},${s.y}`));
const path = astar(snake[0], food, blocked);
if (path && path.length > 0) {
aiPath = path;
const next = path[0];
dir = { x: next.x - snake[0].x, y: next.y - snake[0].y };
} else {
aiPath = [];
const options = [{x:1,y:0},{x:-1,y:0},{x:0,y:1},{x:0,y:-1}];
for (const d of options) {
const next = { x: snake[0].x + d.x, y: snake[0].y + d.y };
if (next.x >= 0 && next.x < cols && next.y >= 0 && next.y < rows && !blocked.has(`${next.x},${next.y}`)) {
dir = d;
break;
}
}
}
}
function drawGrid() {
ctx.strokeStyle = getColor('--ctp-surface');
ctx.lineWidth = 0.5;
for (let x = 0; x <= canvas.width; x += size) {
ctx.beginPath(); ctx.moveTo(x, uiHeight); ctx.lineTo(x, canvas.height); ctx.stroke();
}
for (let y = uiHeight; y <= canvas.height; y += size) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(canvas.width, y); ctx.stroke();
}
}
function drawUI() {
const btnW = 80;
const btnH = 20;
const btnY = (uiHeight - btnH) / 2;
const totalW = btnW * 2 + 10;
const startX = (canvas.width - totalW) / 2;
// Manual button
ctx.fillStyle = !aiMode ? getColor('--ctp-accent') : getColor('--ctp-surface');
ctx.beginPath();
ctx.roundRect(startX, btnY, btnW, btnH, 4);
ctx.fill();
ctx.fillStyle = !aiMode ? getColor('--ctp-base') : getColor('--ctp-subtext');
ctx.font = '12px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Manual', startX + btnW / 2, uiHeight / 2);
// AI button
const aiX = startX + btnW + 10;
ctx.fillStyle = aiMode ? getColor('--ctp-accent') : getColor('--ctp-surface');
ctx.beginPath();
ctx.roundRect(aiX, btnY, btnW, btnH, 4);
ctx.fill();
ctx.fillStyle = aiMode ? getColor('--ctp-base') : getColor('--ctp-subtext');
ctx.fillText('AI', aiX + btnW / 2, uiHeight / 2);
}
function draw() {
ctx.fillStyle = getColor('--ctp-base');
ctx.fillRect(0, uiHeight, canvas.width, canvas.height - uiHeight);
drawGrid();
drawUI();
// draw AI path
if (aiMode && aiPath.length > 0) {
ctx.fillStyle = `color-mix(in srgb, ${getColor('--ctp-teal')} 25%, transparent)`;
aiPath.forEach(p => {
ctx.fillRect(p.x * size, uiHeight + p.y * size, size - 1, size - 1);
});
}
ctx.fillStyle = getColor('--ctp-accent');
snake.forEach(s => ctx.fillRect(s.x * size, uiHeight + s.y * size, size - 1, size - 1));
ctx.fillStyle = getColor('--ctp-red');
ctx.fillRect(food.x * size, uiHeight + food.y * size, size - 1, size - 1);
}
function drawWaiting() {
ctx.fillStyle = getColor('--ctp-base');
ctx.fillRect(0, uiHeight, canvas.width, canvas.height - uiHeight);
drawGrid();
drawUI();
ctx.fillStyle = getColor('--ctp-subtext');
ctx.font = '14px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Press SPACE to start', canvas.width / 2, uiHeight + (canvas.height - uiHeight) / 2);
}
function drawGameOver() {
ctx.fillStyle = getColor('--ctp-base');
ctx.fillRect(0, uiHeight, canvas.width, canvas.height - uiHeight);
drawGrid();
drawUI();
ctx.fillStyle = getColor('--ctp-text');
ctx.font = '20px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('Game Over', canvas.width / 2, uiHeight + (canvas.height - uiHeight) / 2 - 16);
if (!aiMode) {
ctx.font = '14px monospace';
ctx.fillStyle = getColor('--ctp-subtext');
ctx.fillText('Press SPACE to restart', canvas.width / 2, uiHeight + (canvas.height - uiHeight) / 2 + 16);
}
}
function init() {
snake = [{ x: Math.floor(cols / 2), y: Math.floor(rows / 2) }];
dir = { x: 1, y: 0 };
food = randomFood();
aiPath = [];
running = true;
if (interval) clearInterval(interval);
interval = setInterval(tick, aiMode ? 50 : 120);
draw();
}
function tick() {
if (aiMode) aiStep();
const head = { x: snake[0].x + dir.x, y: snake[0].y + dir.y };
if (head.x < 0 || head.x >= cols || head.y < 0 || head.y >= rows || snake.some(s => s.x === head.x && s.y === head.y)) {
running = false;
if (interval) clearInterval(interval);
drawGameOver();
if (aiMode) setTimeout(init, 800);
return;
}
snake.unshift(head);
if (head.x === food.x && head.y === food.y) {
food = randomFood();
} else {
snake.pop();
}
draw();
}
function setMode(ai: boolean) {
aiMode = ai;
if (interval) clearInterval(interval);
running = false;
aiPath = [];
if (ai) {
init();
} else {
drawWaiting();
}
}
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const totalW = 80 * 2 + 10;
const startX = (canvas.width - totalW) / 2;
if (y < uiHeight) {
if (x >= startX && x <= startX + 80) setMode(false);
if (x >= startX + 90 && x <= startX + 170) setMode(true);
}
});
document.addEventListener('keydown', (e) => {
if (e.key === ' ') {
e.preventDefault();
if (!aiMode && !running) init();
return;
}
if (aiMode) return;
if (e.key === 'ArrowUp' && dir.y === 0) dir = { x: 0, y: -1 };
if (e.key === 'ArrowDown' && dir.y === 0) dir = { x: 0, y: 1 };
if (e.key === 'ArrowLeft' && dir.x === 0) dir = { x: -1, y: 0 };
if (e.key === 'ArrowRight' && dir.x === 0) dir = { x: 1, y: 0 };
});
requestAnimationFrame(() => drawWaiting());
</script>
+2 -1
View File
@@ -1,6 +1,7 @@
---
const navItems = [
{ label: "Home", href: "/" },
{ label: "Blog", href: "/blog" },
{ label: "About", href: "/about" },
];
@@ -8,7 +9,7 @@ const currentPath = Astro.url.pathname;
---
<div id="topbar-inner" class="flex items-center justify-between px-6 h-12 bg-ctp-mantle border-b border-ctp-surface">
<span id="logo" class="text-ctp-accent font-semibold text-sm tracking-wide">Cyperpunk.de</span>
<a href="/" id="logo" class="text-ctp-accent font-semibold text-sm tracking-wide">Cyperpunk.de</a>
<nav class="flex gap-6">
{navItems.map((item) => (
<a
+1
View File
@@ -7,6 +7,7 @@ const posts = defineCollection({
title: z.string(),
date: z.date(),
published: z.boolean().default(false),
tags: z.array(z.string()).default([]),
}),
});
+4 -1
View File
@@ -2,6 +2,7 @@
title: Markdown Test
date: 2026-06-07
published: true
tags: [blog, nix, astro]
---
# Heading 1
@@ -27,7 +28,9 @@ This is a normal paragraph with **bold text**, *italic text*, and `inline code`.
## Links and Images
[Visit Gitea](https://git.cyperpunk.de)
![Image](http://localhost:4321/avatar.jpg)
![Square Image](http://localhost:4321/avatar.jpg)
![Landscape Image](https://images8.alphacoders.com/560/thumb-1920-560986.jpg)
![Portrait Image](https://mfiles.alphacoders.com/101/thumb-1920-1012746.jpeg)
---
+23
View File
@@ -0,0 +1,23 @@
---
import '../styles/global.css';
interface Props {
title: string;
}
const { title } = Astro.props;
---
<html lang="en" data-theme="mocha">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<link rel="icon" href="/favicon.svg" />
</head>
<body class="min-h-screen flex items-center justify-center bg-ctp-base">
<slot />
<script>
const saved = localStorage.getItem('ctp-theme') ?? 'mocha';
document.documentElement.setAttribute('data-theme', saved);
</script>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
---
import Blank from '../layouts/Blank.astro';
import Snake from '../components/Snake.astro';
---
<Blank title="404 - Don't Panic">
<div class="flex flex-col items-center justify-center min-h-screen text-center gap-6">
<div id="error-code" class="font-bold text-ctp-accent leading-none" style="font-size: 5rem;">
404
</div>
<div class="text-ctp-text text-xl font-semibold">Don't Panic.</div>
<p class="text-ctp-subtext mx-auto text-sm hyphens-auto" style="max-width: 40%; text-align: justify;">
The Guide says: remain calm. This page exists somewhere in the universe — it simply cannot be located from your current position in space and time. While you wait, the Guide recommends a game of Snake. Or switch to AI mode and watch the snake find its own way — much like the universe itself, it knows where it is going, even if you do not.
</p>
<Snake />
<a href="/" class="text-ctp-accent hover:text-ctp-peach transition-colors text-sm">← Take me to the Guide</a>
</div>
</Blank>
<style>
#error-code {
animation: glitch 4s infinite;
}
@keyframes glitch {
0% { text-shadow: 4px 0 var(--ctp-red), -4px 0 var(--ctp-blue); }
20% { text-shadow: -4px 0 var(--ctp-red), 4px 0 var(--ctp-blue); }
40% { text-shadow: 4px 4px var(--ctp-mauve), -4px -4px var(--ctp-green); }
60% { text-shadow: -4px 0 var(--ctp-peach), 4px 0 var(--ctp-teal); }
80% { text-shadow: 4px 0 var(--ctp-red), -4px 0 var(--ctp-blue); }
100% { text-shadow: none; }
}
</style>
@@ -11,16 +11,27 @@ export async function getStaticPaths() {
const { post } = Astro.props;
const { Content, headings } = await render(post);
const allPosts = await getCollection('posts', ({ data }) => data.published === true);
const allTags = [...new Set(allPosts.flatMap(p => p.data.tags))].sort();
---
<Base title={post.data.title}>
<Topbar slot="topbar" />
<Sidebar slot="sidebar" />
<Sidebar slot="sidebar" tags={allTags}/>
<article class="prose max-w-none">
<Content />
</article>
<div class="flex flex-wrap gap-1 mt-6">
{post.data.tags.map((tag) => (
<a href={`/?tag=${tag}`} class="text-xs px-2 py-0.5 rounded-full bg-ctp-surface text-ctp-subtext hover:text-ctp-accent transition-colors">
{tag}
</a>
))}
</div>
<div id="toc-container" class="fixed top-16 right-4 z-50">
<button id="toc-open" class="bg-ctp-surface text-ctp-accent p-2 rounded-lg hover:bg-ctp-overlay transition-colors" style="font-family: 'NerdFontsSymbols Nerd Font'" title="Table of contents">
+47
View File
@@ -0,0 +1,47 @@
---
export const prerender = false;
import Base from "../../layouts/Base.astro";
import Topbar from "../../components/Topbar.astro";
import Sidebar from "../../components/Sidebar.astro";
import { getCollection } from 'astro:content';
const allPosts = await getCollection('posts', ({ data }) => data.published === true);
allPosts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const selectedTag = Astro.url.searchParams.get('tag');
const posts = selectedTag
? allPosts.filter(p => p.data.tags.includes(selectedTag))
: allPosts;
const allTags = [...new Set(allPosts.flatMap(p => p.data.tags))].sort();
const grouped = posts.reduce((acc, post) => {
const date = post.data.date.toDateString();
if (!acc[date]) acc[date] = [];
acc[date].push(post);
return acc;
}, {} as Record<string, typeof posts>);
const groupedEntries = Object.entries(grouped);
---
<Base title="blog">
<Topbar slot="topbar" />
<Sidebar slot="sidebar" tags={allTags} />
{selectedTag && (
<div class="mb-6">
<h2 class="text-ctp-accent text-xl font-bold mb-1"># {selectedTag.charAt(0).toUpperCase() + selectedTag.slice(1)}</h2>
</div>
)}
{groupedEntries.map(([date, datePosts]) => (
<div class="mb-6">
<div class="text-ctp-subtext text-xs uppercase tracking-widest mb-2 border-b border-ctp-surface pb-1">{date}</div>
{datePosts.map((post) => (
<div class="p-4 mb-3 rounded-lg bg-ctp-surface">
<a href={`/blog/${post.id}`} class="text-ctp-accent">{post.data.title}</a>
<div class="flex flex-wrap gap-1 mt-2">
{post.data.tags.map((tag) => (
<a href={`/blog?tag=${tag}`} class="text-xs px-2 py-0.5 rounded-full bg-ctp-mantle text-ctp-subtext hover:text-ctp-accent transition-colors">
{tag}
</a>
))}
</div>
</div>
))}
</div>
))}
</Base>
+49 -15
View File
@@ -1,21 +1,55 @@
---
import Base from "../layouts/Base.astro";
import Topbar from "../components/Topbar.astro";
import Sidebar from "../components/Sidebar.astro";
import { getCollection } from 'astro:content';
import Blank from '../layouts/Blank.astro';
const posts = await getCollection('posts', ({ data }) => data.published === true);
posts.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
const links = [
{ label: "Blog", href: "/blog", icon: "/blog.png"},
{ label: "Github", href: "https://github.com/DerGrumpf", icon: "/github.png" },
{ label: "Gitea", href: "https://git.cyperpunk.de", icon: "/git.png" },
{ label: "Instagram", href: "https://www.instagram.com/dergrumpf/", icon: "/instagram.png" },
{ label: "Matrix", href: "https://matrix.to/#/@dergrumpf:cyperpunk.de", icon: "/matrix.png" },
{ label: "Spotify", href: "https://open.spotify.com/user/fackthatshiet?si=d502739d787e497e", icon: "/spotify.png" },
{ label: "RSS", href: "/rss.xml", icon: "/rss.png" },
];
---
<Base title="home">
<Topbar slot="topbar" />
<Sidebar slot="sidebar" />
{posts.map((post) => (
<div class="p-4 mb-3 rounded-lg bg-ctp-surface">
<a href={`/posts/${post.id}`} class="text-ctp-accent">{post.data.title}</a>
<p class="text-ctp-subtext text-xs">{post.data.date.toDateString()}</p>
<Blank title="cyperpunk.de">
<div class="flex flex-col items-center gap-8">
<div class="flex flex-col items-center gap-2">
<img src="/avatar.jpg" alt="DerGrumpf" class="w-42 h-42 rounded-full object-cover" />
<h1 class="text-ctp-text font-bold text-xl">Phil Keier</h1>
<p class="text-ctp-subtext text-sm">DerGrumpf</p>
<p class="text-ctp-subtext text-xs">Media Scientist · Programmer · Linux Enthusiast</p>
</div>
<div class="flex flex-col gap-3 w-full" style="max-width: 360px;">
{links.map((link) => (
<a
href={link.href}
class="flex items-center gap-4 px-5 py-3 rounded-lg bg-ctp-surface hover:bg-ctp-overlay text-ctp-text transition-colors"
>
<div
class="w-5 h-5 shrink-0"
style={`mask-image: url(${link.icon}); mask-size: contain; mask-repeat: no-repeat; mask-position: center; background-color: var(--ctp-text);`}
></div>
<span class="text-sm font-semibold">{link.label}</span>
</a>
))}
</Base>
</div>
<div class="flex gap-2 pt-4 border-t border-ctp-surface" style="width: 360px; justify-content: center;">
<button data-flavor="mocha" class="theme-dot w-3 h-3 rounded-full bg-[#cba6f7] hover:scale-125 transition-transform" title="Mocha"></button>
<button data-flavor="macchiato" class="theme-dot w-3 h-3 rounded-full bg-[#c6a0f6] hover:scale-125 transition-transform" title="Macchiato"></button>
<button data-flavor="frappe" class="theme-dot w-3 h-3 rounded-full bg-[#ca9ee6] hover:scale-125 transition-transform" title="Frappé"></button>
<button data-flavor="latte" class="theme-dot w-3 h-3 rounded-full bg-[#8839ef] hover:scale-125 transition-transform" title="Latte"></button>
</div>
</div>
</Blank>
<script>
document.querySelectorAll('.theme-dot').forEach(btn => {
btn.addEventListener('click', () => {
const flavor = btn.getAttribute('data-flavor');
document.documentElement.setAttribute('data-theme', flavor);
localStorage.setItem('ctp-theme', flavor);
});
});
</script>
+17 -6
View File
@@ -201,14 +201,14 @@
--ctp-accent: var(--ctp-mauve);
}
[data-theme="latte"] .icon-mono {
filter: none;
}
.icon-mono {
filter: invert(1);
}
[data-theme="latte"] .icon-mono {
filter: none;
}
body {
background-color: var(--ctp-base);
color: var(--ctp-text);
@@ -284,6 +284,18 @@ article a:hover {
color: var(--ctp-peach);
}
/* Images */
article img {
max-width: 100%;
max-height: 500px;
object-fit: contain;
border-radius: 0.5rem;
display: block;
margin: 1rem auto;
padding: 2px;
background: linear-gradient(to right, var(--ctp-red), var(--ctp-peach), var(--ctp-yellow), var(--ctp-green), var(--ctp-teal), var(--ctp-blue), var(--ctp-mauve));
}
/* Horizontal Line */
article hr {
border: none;
@@ -304,8 +316,7 @@ article hr {
/* Code Block*/
article pre code {
background-color: transparent;
padding: 0;
}
padding: 0}
article code {
background-color: var(--ctp-surface);