Compare commits

...

11 Commits

Author SHA1 Message Date
Max Richter
f84704d20b chore: pnpm up 2025-07-20 14:15:44 +02:00
Max Richter
666b618a73 WIP 2025-07-20 13:37:29 +02:00
Max Richter
b32ca7d65e feat: improve some error messages
All checks were successful
Deploy to SFTP Server / build (push) Successful in 4m6s
2025-05-14 19:48:55 +02:00
Max Richter
c74b314b1e feat: cleanup some stuff
All checks were successful
Deploy to SFTP Server / build (push) Successful in 4m14s
2025-05-14 19:42:28 +02:00
Max Richter
50ce8b3ff7 feat: log imagePath when failed to generate Thumbhash 2025-05-14 19:37:24 +02:00
Max Richter
8e293c204d feat: some updates
All checks were successful
Deploy to SFTP Server / build (push) Successful in 17m5s
2025-05-14 19:23:59 +02:00
Max Richter
972c2382f3 chore: pnpm up
Some checks failed
Deploy to SFTP Server / build (push) Failing after 4m53s
2025-05-14 19:06:35 +02:00
Max Richter
e0543f2a58 chore: pnpm up
Some checks failed
Deploy to SFTP Server / build (push) Failing after 4m48s
2025-05-14 19:00:59 +02:00
Max Richter
3d78b9e56c chore: pnpm up 2025-05-14 19:00:59 +02:00
Max Richter
84e56f2668 fix(ci): increase ulimit in ci 2025-05-14 19:00:59 +02:00
Max Richter
8af8db0714 fix: dont try to use icons as img src 2025-05-14 19:00:58 +02:00
23 changed files with 1960 additions and 2344 deletions

View File

@@ -53,7 +53,6 @@ jobs:
- name: 🏗️ Build site
run: |
# Install dependencies, build, and generate site output
pnpm i && pnpm build
- name: 🔑 Configure rclone

View File

@@ -17,13 +17,12 @@ const locales = {
const DEFAULT_LAYOUT = '@layouts/Post.astro';
function setDefaultLayout() {
return function (_, file) {
return function(_, file) {
const { frontmatter } = file.data.astro;
if (!frontmatter.layout) frontmatter.layout = DEFAULT_LAYOUT;
};
}
// https://astro.build/config
export default defineConfig({
site: "https://max-richter.dev",
trailingSlash: "never",
@@ -38,6 +37,13 @@ export default defineConfig({
compiler: 'svelte',
}),
],
server: {
watch: {
// Customize watch behavior to reduce file watchers
ignored: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
usePolling: process.env.NODE_ENV === 'production',
},
},
},
markdown: {
remarkPlugins: [setDefaultLayout]

View File

@@ -11,32 +11,32 @@
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.0.8",
"@astrojs/svelte": "^7.0.4",
"@astrojs/tailwind": "^6.0.0",
"astro": "^5.3.0",
"@astrojs/mdx": "^4.3.1",
"@astrojs/svelte": "^7.1.0",
"@astrojs/tailwind": "^6.0.2",
"astro": "^5.12.0",
"astro-i18n-aut": "^0.7.3",
"exifreader": "^4.26.1",
"svelte": "^5.20.1",
"svelte-gestures": "^5.1.3",
"tailwindcss": "^4.0.6",
"exifreader": "^4.31.1",
"svelte": "^5.36.10",
"svelte-gestures": "^5.1.4",
"tailwindcss": "^4.1.11",
"thumbhash": "^0.1.1",
"typescript": "^5.7.3"
"typescript": "^5.8.3"
},
"devDependencies": {
"@astrojs/sitemap": "^3.2.1",
"@iconify-json/tabler": "^1.2.16",
"@astrojs/sitemap": "^3.4.1",
"@iconify-json/tabler": "^1.2.19",
"@types/markdown-it": "^14.1.2",
"@unocss/preset-icons": "^65.5.0",
"@unocss/reset": "^65.5.0",
"astro-font": "^1.0.0",
"@unocss/preset-icons": "^66.3.3",
"@unocss/reset": "^66.3.3",
"astro-font": "^1.1.0",
"markdown-it": "^14.1.0",
"ogl": "^1.0.11",
"prettier": "^3.5.1",
"prettier": "^3.6.2",
"prettier-plugin-astro": "^0.14.1",
"sharp": "^0.33.5",
"unocss": "^65.5.0",
"unplugin-icons": "^22.0.0",
"vite-plugin-glsl": "^1.3.1"
"sharp": "^0.34.3",
"unocss": "^66.3.3",
"unplugin-icons": "^22.1.0",
"vite-plugin-glsl": "^1.5.1"
}
}

2827
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,4 @@
onlyBuiltDependencies:
- esbuild
- exifreader
- sharp

View File

@@ -31,7 +31,14 @@ const link = translatePath(`/${collection}/${id.split("/")[0]}`);
classes={`grid gradient border-1 border-neutral overflow-hidden ${cover ? "grid-rows-[200px_1fr] xs:grid-rows-none xs:grid-cols-[1fr_200px]" : ""}`}>
<Card.Content classes="px-8 py-7 order-last xs:order-first">
<Card.Title classes="text-4xl flex items-center gap-2">
{icon && <img src={icon} class="h-6 w-6" />}
{
icon &&
(
icon?.length > 5
? <img class="h-6 w-6" src={icon} />
: <span class="p-r-4 text-md">{icon}</span>
)
}
{title}
</Card.Title>
<Card.Description>

View File

@@ -3,7 +3,7 @@ import type { ImageMetadata } from "astro";
import { Picture as AstroImage } from "astro:assets";
import { generateThumbHash, getExifData } from "@helpers/image";
interface Props {
src: ImageMetadata;
src: ImageMetadata & { fsPath?: string };
alt: string;
pictureClass?: string;
class?: string;
@@ -22,7 +22,7 @@ const {
maxWidth,
} = Astro.props;
let thumbhash = hash ? await generateThumbHash(image) : "";
let thumbhash = hash && image.fsPath ? await generateThumbHash(image) : "";
let exif = await getExifData(image);

View File

@@ -24,8 +24,8 @@ const paths = [
text: t("nav.photos"),
},
{
link: translatePath("/videos"),
text: t("nav.videos"),
link: translatePath("/resources"),
text: t("nav.resources"),
},
];
---

View File

@@ -1,4 +1,4 @@
---
title: Videos
title: Resources
menu: nav
---

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ async function getSharp(): Promise<typeof import("sharp") | undefined> {
return s;
}
export async function generateThumbHash(image: { width: number, height: number }) {
export async function generateThumbHash(image: ImageMetadata & { fsPath?: string }) {
const sharp = await getSharp();
if (!sharp) return;
@@ -19,7 +19,7 @@ export async function generateThumbHash(image: { width: number, height: number }
const smallWidth = Math.floor(image.width * scaleFactor);
const smallHeight = Math.floor(image.height * scaleFactor);
//@ts-ignore
try {
const smallImg = await sharp(image.fsPath)
.resize(smallWidth, smallHeight)
.withMetadata()
@@ -29,6 +29,11 @@ export async function generateThumbHash(image: { width: number, height: number }
const buffer = rgbaToThumbHash(smallWidth, smallHeight, smallImg);
return Buffer.from(buffer).toString("base64");
} catch (error) {
console.log(`Could not generate thumbhash for ${image.fsPath}`, error)
return ""
}
}
const allowedExif = [
@@ -48,10 +53,15 @@ const allowedExif = [
];
export async function getExifData(image: ImageMetadata) {
if (image.format === "svg") return undefined; // SVGs don't have EXIF data")
const sharp = await getSharp();
if (!sharp) return;
const imagePath = (image as ImageMetadata & { fsPath: string }).fsPath;
try {
const tags = await ExifReader.load((image as ImageMetadata & { fsPath: string }).fsPath, { async: true });
const buffer = await sharp(imagePath).toBuffer();
const tags = await ExifReader.load(buffer, { async: true });
const out: Record<string, any> = {};
let hasExif = false;
@@ -65,7 +75,7 @@ export async function getExifData(image: ImageMetadata) {
return hasExif ? out : undefined;
} catch (error) {
console.log("Error reading EXIF data", error);
console.log(`Error reading EXIF data from ${imagePath}`, error);
return undefined
}

10
src/helpers/memorium.ts Normal file
View File

@@ -0,0 +1,10 @@
export async function listResource(id: string): Promise<any[]> {
try {
const response = await fetch(
`http://localhost:8080/resources?name=${id}`,
);
return await response.json();
} catch (error) {
return []
}
}

View File

@@ -20,7 +20,7 @@ export const ui = {
'home.subtitle': 'Trained Media Designer, Blender Nerd, Developer and Hardware Tinkerer.',
'nav.blog': 'Blog',
'nav.projects': 'Projects',
'nav.videos': 'Videos',
'nav.resources': 'Resources',
'nav.photos': 'Photos',
'toc.title': 'Table of Contents',
"resume": "Resume",
@@ -38,7 +38,7 @@ export const ui = {
'home.subtitle': 'Ausgebildeter Mediengestalter, Blender Nerd, Entwickler und Hardware Bastler.',
'nav.blog': 'Blog',
'nav.projects': 'Projekte',
'nav.videos': 'Videos',
'nav.resources': 'Resources',
'nav.photos': 'Fotos',
"resume": "Lebenslauf",
'toc.title': 'Inhaltsverzeichnis',

View File

@@ -16,11 +16,10 @@ const { frontmatter, headings } = Astro.props;
const t = useTranslations(Astro.url);
const { title, url, date: dateString, links, toc, cover } = frontmatter;
const collection = url?.split("/")[2];
const date = new Date(dateString);
const path = useTranslatedPath(Astro.url);
//@ts-ignore
const backlinkContent = t(`nav.${collection}`).toLowerCase();
const date = new Date(dateString);
const path = useTranslatedPath(Astro.url);
---
<Layout title={title} image={cover}>

View File

@@ -41,10 +41,6 @@ const list = [...posts, ...photos];
list.sort((a, b) => {
return a.data.date > b.data.date ? -1 : 1;
});
// .sort((a, b) => {
// return a.data.date.getDate() > b.data.date.getDate() ? 1 : -1;
// });
console.log(list.map((post) => [post.data.date, post.data.title]));
const featuredPost = list.find((post) => post.data?.featured);
const otherPosts = list.filter((post) => featuredPost !== post).slice(0, 3);

View File

@@ -0,0 +1,64 @@
---
import Layout from "@layouts/Layout.astro";
import HeroCard from "@components/HeroCard.astro";
const collection = "resources";
const wiki = {
id: "wiki",
collection,
body: "My knowledge base",
data: {
title: "Wiki",
icon: "🧠",
},
};
const articles = {
id: "articles",
collection,
body: "Articles saved",
data: {
title: "Articles",
icon: "📰",
},
};
const recipes = {
id: "recipes",
collection,
body: "Recipes",
data: {
title: "Recipes",
icon: "🍲",
},
};
const movies = {
id: "movies",
collection,
body: "Movies",
data: {
title: "Movies",
icon: "🎥",
},
};
const series = {
id: "series",
collection,
body: "Series",
data: {
title: "Series",
icon: "📺",
},
};
---
<Layout title="Max Richter">
<HeroCard post={wiki} />
<HeroCard post={recipes} />
<HeroCard post={articles} />
<HeroCard post={movies} />
<HeroCard post={series} />
</Layout>

View File

@@ -0,0 +1,38 @@
---
import Layout from "@layouts/Layout.astro";
import * as memorium from "@helpers/memorium";
export async function getStaticPaths() {
const movieReviews = await memorium.listResource("Media/movies/*");
const paths = movieReviews.map((review: any) => {
return {
params: {
movieName: review.identifier
.replace("Media/movies/", "")
.replace(/\.md$/, ""),
},
};
});
return paths;
}
const reviews = await memorium.listResource(
//@ts-ignore
`Media/movies/${Astro.params.movieName}.md`,
);
if (reviews.length === 0) {
return new Response(null, {
status: 404,
statusText: "Not found",
});
}
const review = reviews[0];
---
<Layout title="Max Richter">
<h1>{review.itemReviewed?.name}</h1>
<p>{review.reviewBody}</p>
<!-- <pre><code>{JSON.stringify(review, null, 2)}</code></pre> -->
</Layout>

View File

@@ -0,0 +1,27 @@
---
import Layout from "@layouts/Layout.astro";
import HeroCard from "@components/HeroCard.astro";
import * as memorium from "@helpers/memorium";
const movieReviews = await memorium.listResource("Media/movies/*");
---
<Layout title="Max Richter">
{
movieReviews.map((review: any) => (
<HeroCard
post={{
collection: "resources/movies",
id: review.identifier
.replace("Media/movies/", "")
.replace(/\.md$/, ""),
data: {
title: review.itemReviewed.name,
description: review.reviewBody,
},
body: review.reviewBody,
}}
/>
))
}
</Layout>

View File

@@ -0,0 +1,71 @@
---
import Layout from "@layouts/Layout.astro";
import { useTranslatedPath } from "@i18n/utils";
import markdownToText from "@helpers/markdownToText";
import * as memorium from "@helpers/memorium";
const path = useTranslatedPath(Astro.url);
const collection = "resources/recipes";
export async function getStaticPaths() {
const recipes = await memorium.listResource("Recipes/*");
const paths = recipes.map((recipe: any) => {
return {
params: {
recipeName: recipe.identifier
.replace("Recipes/", "")
.replace(/\.md$/, ""),
},
};
});
return paths;
}
const recipes = await memorium.listResource(
//@ts-ignore
`Recipes/${Astro.params.recipeName}.md`,
);
if (recipes.length === 0) {
return new Response(null, {
status: 404,
statusText: "Not found",
});
}
const recipe = recipes[0];
---
<Layout title="Max Richter">
<div class="top-info flex items-center place-content-between m-y-2">
<a class="flex items-center gap-1 opacity-50" href={path("/" + collection)}>
<span class="i-tabler-arrow-left"></span> back
</a>
<div class="date opacity-50">
{
recipe.date?.toLocaleString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
}
</div>
</div>
<h1>{recipe.name}</h1>
<h3>Ingredients</h3>
<ol>
{
recipe.recipeIngredient?.map((ingredient: any) => (
<li>{markdownToText(ingredient)}</li>
))
}
</ol>
<h3>Instructions</h3>
<p>{recipe.recipeInstructions}</p>
</Layout>

View File

@@ -0,0 +1,23 @@
---
import Layout from "@layouts/Layout.astro";
import HeroCard from "@components/HeroCard.astro";
import * as memorium from "@helpers/memorium";
const recipes = await memorium.listResource("Recipes/*");
---
<Layout title="Max Richter">
{
recipes.map((recipe: any) => (
<HeroCard
post={{
collection: "resources/recipes",
id: recipe.identifier.replace("Recipes/", "").replace(/\.md$/, ""),
data: {
title: recipe.name,
},
}}
/>
))
}
</Layout>

View File

@@ -0,0 +1,58 @@
---
import Layout from "@layouts/Layout.astro";
import { useTranslatedPath } from "@i18n/utils";
import * as memorium from "@helpers/memorium";
const collection = "resources/series";
const path = useTranslatedPath(Astro.url);
export async function getStaticPaths() {
const seriesReviews = await memorium.listResource("Media/series/*");
const paths = seriesReviews.map((review: any) => {
return {
params: {
seriesName: review.identifier
.replace("Media/series/", "")
.replace(/\.md$/, ""),
},
};
});
return paths;
}
const reviews = await memorium.listResource(
//@ts-ignore
`Media/series/${Astro.params.seriesName}.md`,
);
if (reviews.length === 0) {
return new Response(null, {
status: 404,
statusText: "Not found",
});
}
const review = reviews[0];
---
<Layout title="Max Richter">
<div class="top-info flex items-center place-content-between m-y-2">
<a class="flex items-center gap-1 opacity-50" href={path("/" + collection)}>
<span class="i-tabler-arrow-left"></span> back
</a>
<div class="date opacity-50">
{
review.date?.toLocaleString("en-US", {
month: "long",
day: "numeric",
year: "numeric",
})
}
</div>
</div>
<h1>{review.itemReviewed?.name}</h1>
<p>{review.reviewBody}</p>
</Layout>

View File

@@ -0,0 +1,27 @@
---
import Layout from "@layouts/Layout.astro";
import HeroCard from "@components/HeroCard.astro";
import * as memorium from "@helpers/memorium";
const seriesReviewes = await memorium.listResource("Media/series/*");
---
<Layout title="Max Richter">
{
seriesReviewes.map((review: any) => (
<HeroCard
post={{
collection: "resources/series",
id: review.identifier
.replace("Media/series/", "")
.replace(/\.md$/, ""),
data: {
title: review.itemReviewed.name,
description: review.reviewBody,
},
body: review.reviewBody,
}}
/>
))
}
</Layout>

View File

@@ -1,10 +1,10 @@
// uno.config.ts
import { defineConfig, presetUno } from 'unocss'
import { defineConfig, presetWind3 } from 'unocss'
import presetIcons from '@unocss/preset-icons'
export default defineConfig({
presets: [
presetUno(),
presetWind3(),
presetIcons(),
],
shortcuts: {