Add new posts for Image Voice Memos, Initial VPS Setup on Debian, Local Webmention Avatars, Security Headers for Astro with Caddy, and Setting up Forgejo Actions Runner

- Created a new post on Image Voice Memos detailing a macOS app for browsing photos and recording voice memos with automatic transcription.
- Added a guide for Initial VPS Setup on Debian covering system updates, user creation, and SSH hardening.
- Introduced a post on caching webmention avatars locally at build time to enhance privacy and comply with CSP.
- Documented the implementation of security headers for an Astro site behind Caddy, focusing on GDPR compliance and CSP.
- Set up a Forgejo Actions runner for self-hosted CI/CD, detailing the installation and configuration process for automated deployments.
This commit is contained in:
Adrian Altner 2026-04-22 23:00:10 +02:00
parent 9d22d93361
commit 4bf4eb03b1
69 changed files with 4904 additions and 344 deletions

View file

@ -6,7 +6,7 @@ import FormattedDate from '~/components/FormattedDate.astro';
import Webmentions from '~/components/Webmentions.astro';
import BaseLayout from '~/layouts/BaseLayout.astro';
import { DEFAULT_LOCALE, type Locale } from '~/consts';
import { categoryHref, entryHref, findTranslation, tagHref } from '~/i18n/posts';
import { categoryHref, entryHref, entrySlug, findTranslation, getSeries, postHref, tagHref } from '~/i18n/posts';
import { getLocaleFromUrl, t } from '~/i18n/ui';
type Props = CollectionEntry<'posts'>['data'] & {
@ -20,8 +20,11 @@ const {
pubDate,
updatedDate,
heroImage,
hideHero,
category,
tags,
url,
repo,
entry,
locale = getLocaleFromUrl(Astro.url) ?? DEFAULT_LOCALE,
} = Astro.props;
@ -29,6 +32,9 @@ const {
const categoryEntry = category ? await getEntry(category) : undefined;
const otherLocale: Locale = locale === 'de' ? 'en' : 'de';
const translation = entry ? await findTranslation(entry, otherLocale) : undefined;
const series = entry ? await getSeries(entry) : undefined;
const currentSlug = entry ? entrySlug(entry) : undefined;
const seriesPosition = series && currentSlug ? series.entries.findIndex((e) => entrySlug(e) === currentSlug) : -1;
---
<BaseLayout title={title} description={description} image={heroImage} locale={locale} entry={entry}>
@ -36,7 +42,7 @@ const translation = entry ? await findTranslation(entry, otherLocale) : undefine
<a href={Astro.url.pathname} class="u-url" hidden></a>
<div class="hero-image">
{
heroImage && (
heroImage && !hideHero && (
<Image
class="u-photo"
width={1020}
@ -79,19 +85,59 @@ const translation = entry ? await findTranslation(entry, otherLocale) : undefine
tags && tags.length > 0 && (
<p class="tags">
{t(locale, 'post.tags')}:{' '}
{tags.map((name, i) => (
<>
{i > 0 && ', '}
<a href={tagHref(locale, name)} class="p-category">
{name}
</a>
</>
))}
{tags.map((name, i) => (<>{i > 0 && ', '}<a href={tagHref(locale, name)} class="p-category">{name}</a></>))}
</p>
)
}
{
(url || repo) && (
<p class="project-links">
{url && (
<a href={url} rel="noopener" class="u-url">
{t(locale, 'post.projectDemo')}
</a>
)}
{url && repo && ' · '}
{repo && (
<a href={repo} rel="noopener">
{t(locale, 'post.projectRepo')}
</a>
)}
</p>
)
}
<hr />
</div>
{
series && seriesPosition >= 0 && (
<aside class="series-box">
<p class="series-heading">
{t(locale, 'post.series')}{' '}
<a href={postHref(series.parent)}>{series.parent.data.title}</a>
{' · '}
<span class="series-position">
{t(locale, 'post.seriesPart')
.replace('{n}', String(seriesPosition + 1))
.replace('{total}', String(series.entries.length))}
</span>
</p>
<ol class="series-list">
{series.entries.map((e) => {
const isCurrent = entrySlug(e) === currentSlug;
return (
<li class={isCurrent ? 'is-current' : ''}>
{isCurrent ? (
<span>{e.data.title}</span>
) : (
<a href={postHref(e)}>{e.data.title}</a>
)}
</li>
);
})}
</ol>
</aside>
)
}
{
translation && (
<aside class="translation-notice" lang={otherLocale}>
@ -160,4 +206,42 @@ const translation = entry ? await findTranslation(entry, otherLocale) : undefine
color: var(--accent);
font-weight: 600;
}
.series-box {
margin: 0 0 2em 0;
padding: 1em 1.25em;
background: rgba(var(--gray-light), 0.5);
border-left: 3px solid var(--accent);
border-radius: 4px;
font-size: 0.95em;
}
.series-heading {
margin: 0 0 0.5em 0;
font-weight: 600;
}
.series-heading a {
color: var(--accent);
}
.series-position {
font-weight: 400;
color: rgb(var(--gray));
}
.series-list {
margin: 0;
padding-left: 1.5em;
}
.series-list li {
margin: 0.2em 0;
}
.series-list li.is-current {
font-weight: 600;
color: rgb(var(--gray-dark));
}
.project-links {
margin: 0.5em 0 0 0;
font-size: 0.95em;
}
.project-links a {
color: var(--accent);
font-weight: 600;
}
</style>