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:
parent
9d22d93361
commit
4bf4eb03b1
69 changed files with 4904 additions and 344 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue