summaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/BaseHead.astro54
-rw-r--r--src/components/Commit.astro49
-rw-r--r--src/components/CopyrightNotice.astro5
-rw-r--r--src/components/Footer.astro125
-rw-r--r--src/components/Header.astro69
-rw-r--r--src/components/Search.astro32
-rw-r--r--src/components/organisms/ActiveLink.astro (renamed from src/components/HeaderLink.astro)0
-rw-r--r--src/components/organisms/Date.astro14
-rw-r--r--src/components/organisms/KeywordsList.astro31
-rw-r--r--src/components/signature/Authors.astro8
-rw-r--r--src/components/templates/MicroBlog.astro114
-rw-r--r--src/components/templates/Search.astro67
-rw-r--r--src/components/templates/SimplePostList.astro81
13 files changed, 476 insertions, 173 deletions
diff --git a/src/components/BaseHead.astro b/src/components/BaseHead.astro
index 5ac0410..b4dbb74 100644
--- a/src/components/BaseHead.astro
+++ b/src/components/BaseHead.astro
@@ -1,8 +1,6 @@
---
-// Import the global.css file here so that it is included on
-// all pages through the use of the <BaseHead /> component.
+import { env } from "@lib/env";
import "../styles/global.css";
-import { SITE_AUTHOR, SITE_DESCRIPTION, SITE_TITLE } from "../consts";
import { ClientRouter } from "astro:transitions";
export interface Props {
@@ -12,11 +10,24 @@ export interface Props {
keywords?: string[];
}
+const {
+ PUBLIC_SITE_TITLE,
+ PUBLIC_SITE_DESCRIPTION,
+ PUBLIC_SITE_AUTHOR,
+ PUBLIC_TOR_URL,
+} = env;
+
+const isOnion = Astro.url.origin.endsWith(".onion");
+const alternate = !isOnion ? PUBLIC_TOR_URL : Astro.site;
+
const canonicalURL = new URL(Astro.url.pathname, Astro.site);
-const { title, description = SITE_DESCRIPTION, image, keywords = [] } =
- Astro.props;
-// const socialImage = image ?? Astro.site.href + 'assets/social.png'
+const {
+ title,
+ description = PUBLIC_SITE_DESCRIPTION,
+ image = new URL("favicon.svg", Astro.site),
+ keywords = [],
+} = Astro.props;
---
<!-- Global Metadata -->
@@ -28,24 +39,30 @@ const { title, description = SITE_DESCRIPTION, image, keywords = [] } =
<link
rel="alternate"
type="application/rss+xml"
- title={SITE_TITLE}
+ title={PUBLIC_SITE_TITLE}
href={new URL("rss.xml", Astro.site)}
/>
<meta name="generator" content={Astro.generator} />
<!-- Canonical URL -->
<link rel="canonical" href={canonicalURL} />
+<link
+ rel="alternate"
+ href={alternate}
+ type="text/html"
+ title={`${isOnion ? "Clearnet" : "Tor"} version`}
+>
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
-<meta name="author" content={SITE_AUTHOR} />
+<meta name="author" content={PUBLIC_SITE_AUTHOR} />
{keywords.length > 0 && <meta name="keywords" content={keywords.join(",")} />}
-<meta name="theme-color" content="#a50026" />
+<meta name="theme-color" content="oklch(0.4564 0.1835 20.81)" />
<meta
name="theme-color"
- content="#f46d43"
+ content="oklch(0.6923 0.1759 37.7)"
media="(prefers-color-scheme: dark)"
/>
@@ -54,26 +71,13 @@ const { title, description = SITE_DESCRIPTION, image, keywords = [] } =
<meta property="og:url" content={Astro.url} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
-{image && <meta property="og:image" content={new URL(image, Astro.url)} />}
+<meta property="og:image" content={image} />
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:title" content={title} />
<meta property="twitter:description" content={description} />
-{image && <meta property="twitter:image" content={new URL(image, Astro.url)} />}
+<meta property="twitter:image" content={image} />
<ClientRouter />
-
-<script is:inline>
- const root = document.documentElement;
- const theme = localStorage.getItem("theme");
- if (
- theme === "dark" ||
- (!theme && window.matchMedia("(prefers-color-scheme: dark)").matches)
- ) {
- root.classList.add("theme-dark");
- } else {
- root.classList.remove("theme-dark");
- }
-</script>
diff --git a/src/components/Commit.astro b/src/components/Commit.astro
deleted file mode 100644
index 3ee284a..0000000
--- a/src/components/Commit.astro
+++ /dev/null
@@ -1,49 +0,0 @@
----
-import type { Commit } from "@lib/git/types";
-import { gitDir } from "@lib/git";
-
-type Props = Commit;
-
-const { hash, files, author, signature } = Astro.props;
-
-const git = await gitDir;
----
-<p>Git commit info:</p>
-<dl>
- <dt>Hash</dt>
- <dd>{hash}</dd>
- <dt>Files</dt>
- {files.map((file) => <dd>{file.pathname.replace(git, "")}</dd>)}
- <dt>Author</dt>
- <dd>{author.name} &lt;{author.email}&gt;</dd>
- {
- signature && (
- <dt>Commit Signature</dt>
- <dd>
- <dl>
- <dt>Type</dt>
- <dd>{signature.type}</dd>
- <dt>Signer</dt>
- <dd>{signature.signerName}</dd>
- <dt>Key fingerprint</dt>
- <dd>{signature.keyFingerPrint}</dd>
- </dl>
- </dd>
- )
- }
-</dl>
-
-<style>
- dl {
- display: grid;
- grid-template-columns: 1fr 1fr;
- }
-
- dl > dt, dd {
- display: inline-block;
- }
-
- dt::after {
- content: ": ";
- }
-</style>
diff --git a/src/components/CopyrightNotice.astro b/src/components/CopyrightNotice.astro
index 2aa72ad..6b3bd48 100644
--- a/src/components/CopyrightNotice.astro
+++ b/src/components/CopyrightNotice.astro
@@ -1,7 +1,10 @@
---
+import {
+ CREATIVE_COMMONS_LICENSES,
+ type LICENSES,
+} from "@lib/collection/schemas";
import CC from "./licenses/CC.astro";
import WTFPL from "./licenses/WTFPL.astro";
-import { CREATIVE_COMMONS_LICENSES, LICENSES } from "../consts.ts";
export interface Props {
title: string;
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
index 11c62c4..c3dffca 100644
--- a/src/components/Footer.astro
+++ b/src/components/Footer.astro
@@ -1,62 +1,107 @@
---
+import { env } from "@lib/env";
+const {
+ PUBLIC_GIT_URL,
+ PUBLIC_TOR_URL,
+ PUBLIC_GIT_TOR_URL,
+ PUBLIC_SIMPLE_X_ADDRESS,
+} = env;
+const isOnion = Astro.url.origin.endsWith(".onion");
+const site = isOnion ? PUBLIC_TOR_URL : Astro.site;
+const git = isOnion ? PUBLIC_GIT_TOR_URL ?? PUBLIC_GIT_URL : PUBLIC_GIT_URL;
---
-<footer>
+<footer class="small">
+ {
+ !isOnion && PUBLIC_TOR_URL && (
+ <p class="mute">
+ Disponível também em: <a class="tor" href={PUBLIC_TOR_URL}>{
+ PUBLIC_TOR_URL
+ }</a>
+ </p>
+ )
+ }
<address>
- Sítio web de <a href={Astro.site} target="_blank" rel="author"
- >João Augusto Costa Branco Marado Torres</a>
+ <p>
+ Sítio web de <a href={site} target="_blank" rel="author"
+ >João Augusto Costa Branco Marado Torres</a>
+ </p>
+ {
+ PUBLIC_SIMPLE_X_ADDRESS && (
+ <p>
+ Contacte-me através do <a href={PUBLIC_SIMPLE_X_ADDRESS}>SimpleX</a>!
+ </p>
+ )
+ }
</address>
- <section id="copying">
- <h2>Licença de <span lang="en">Software</span></h2>
+ <p>
+ Isto é <abbr title="Free Libre and Open Source Software">FLOSS</abbr>, <a
+ href={git}
+ >usa as tuas liberdades</a>
+ </p>
+ <section id="copying" class="mute">
+ <h2 class="sr-only">Licença de <span lang="en">Software</span></h2>
<div lang="en">
<p>
- <small>
- &lt;<a href="/" hreflang="pt-PT">cravodeabril.pt</a>&gt; Copyright
- &copy; 2025 João Augusto Costa Branco Marado Torres
- </small>
+ &lt;<a href="/" hreflang="pt-PT">cravodeabril.pt</a>&gt; Copyright
+ &copy; 2025 João Augusto Costa Branco Marado Torres
</p>
<p>
- <small>
- This program is free software: you can redistribute it and/or modify
- it under the terms of the <a
- href="https://www.gnu.org/licenses/agpl-3.0.html"
- target="_blank"
- rel="external license"
- >GNU Affero General Public License</a> as published by the Free
- Software Foundation, either version 3 of the License, or (at your
- option) any later version.
- </small>
+ This program is free software: you can redistribute it and/or modify it
+ under the terms of the <a
+ href="https://www.gnu.org/licenses/agpl-3.0.html"
+ target="_blank"
+ rel="external license"
+ >GNU Affero General Public License</a> as published by the Free Software
+ Foundation, either version 3 of the License, or (at your option) any
+ later version.
</p>
<p>
- <small>
- This program is distributed in the hope that it will be useful, but
- WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- Affero General Public License for more details.
- </small>
+ This program is distributed in the hope that it will be useful, but
+ <strong>without any warranty</strong>; without even the implied warranty
+ of
+ <strong>merchantability</strong> or <strong>fitness for a particular
+ purpose</strong>. See the GNU Affero General Public License for more
+ details.
</p>
<p>
- <small>
- You should have received a copy of the GNU Affero General Public
- License along with this program. If not, see <a
- href="https://www.gnu.org/licenses/"
- target="_blank"
- rel="external"
- >https://www.gnu.org/licenses</a>
- </small>
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <a
+ href="https://www.gnu.org/licenses/"
+ target="_blank"
+ rel="external"
+ >https://www.gnu.org/licenses</a>
</p>
</div>
</section>
<nav>
<ul>
- <li><a>Código de Conduta</a></li>
- <li><a>Declaração de Exoneração de Responsabilidade</a></li>
- <li><a>Aviso sobre cookies</a></li>
- <li><a>Declaração de acessibilidade</a></li>
- <li><a>Apoio</a></li>
- <li><a>Contacto</a></li>
- <li><a>Código fonte</a></li>
+ <li><a href="/">Código de Conduta</a></li>
+ <li><a href="/">Declaração de Exoneração de Responsabilidade</a></li>
+ <li><a href="/">Aviso sobre cookies</a></li>
+ <li><a href="/">Declaração de acessibilidade</a></li>
+ <li><a href="/">Apoio</a></li>
</ul>
</nav>
</footer>
+
+<style>
+ footer {
+ border-block-start: 1px solid var(--color-light);
+ padding-block-start: calc(var(--size-4) * 1em);
+ }
+
+ .tor {
+ word-wrap: break-word;
+ }
+
+ nav > ul {
+ display: flex;
+ flex-direction: column;
+ gap: calc(var(--size-1) * 1em);
+ & > li {
+ padding-block: calc(var(--size-1) * 1em);
+ }
+ }
+</style>
diff --git a/src/components/Header.astro b/src/components/Header.astro
index 28ab542..496337f 100644
--- a/src/components/Header.astro
+++ b/src/components/Header.astro
@@ -1,26 +1,53 @@
---
-import HeaderLink from "./HeaderLink.astro";
-import Search from "./Search.astro";
-
-export interface Props {
- showSearch?: boolean;
- showNav?: boolean;
-}
-
-const { showSearch, showNav } = Astro.props;
+import ActiveLink from "./organisms/ActiveLink.astro";
+import Search from "./templates/Search.astro";
---
<header>
- <h1>&lt;<a href="/">cravodeabril.pt</a>&gt;</h1>
- {showSearch && <Search />}
- {
- showNav && (
- <nav>
- <ul>
- <li><HeaderLink href="/blog">Publicações</HeaderLink></li>
- <li><HeaderLink href="/blog/keywords">Palavras-Chave</HeaderLink></li>
- </ul>
- </nav>
- )
- }
+ <h1>
+ <span class="bracket">&lt;</span><a href="/">cravodeabril.pt</a><span
+ class="bracket"
+ >&gt;</span>
+ </h1>
+ <Search />
+ <nav>
+ <ul>
+ <li class="small"><ActiveLink href="/blog">Publicações</ActiveLink></li>
+ <li class="small">
+ <ActiveLink href="/blog/keywords">Palavras-Chave</ActiveLink>
+ </li>
+ <li class="small">
+ <ActiveLink href="/blog/micro/1">Micro blogue</ActiveLink>
+ </li>
+ </ul>
+ </nav>
</header>
+
+<style>
+ header {
+ margin-block-end: calc(var(--size-4) * 1em);
+ border-block-end: 1px solid var(--color-light);
+ }
+ .bracket {
+ color: var(--color-active);
+ }
+ nav {
+ display: flex;
+ max-width: max-content;
+ align-items: center;
+ justify-content: center;
+ }
+ ul {
+ display: flex;
+ flex: 1;
+ gap: calc(var(--size-0) * 1em);
+ list-style-type: none;
+ align-items: center;
+ justify-content: center;
+ padding: calc(var(--size-2) * 1em);
+ margin-block-start: 0;
+ }
+ li {
+ padding: calc(var(--size-1) * 1em);
+ }
+</style>
diff --git a/src/components/Search.astro b/src/components/Search.astro
deleted file mode 100644
index 5ca4569..0000000
--- a/src/components/Search.astro
+++ /dev/null
@@ -1,32 +0,0 @@
----
-
----
-
-<search>
- <form
- action="https://www.google.com/search"
- target="_blank"
- rel="external noreferrer search"
- role="search"
- autocomplete="on"
- name="search"
- >
- <p>
- <label>Barra de pesquisa: <input
- name="q"
- type="search"
- placeholder={`site:${Astro.site} consulta de pesquisa`}
- value={`site:${Astro.site} `}
- required
- title={`"site:${Astro.site} " é usado para que os resultados da pesquisa fiquem restritos a este website`}
- pattern={`site:${Astro.site} .+`}
- size={`site:${Astro.site} .+`.length}
- /></label>
- </p>
- <p><button type="submit">Pesquisar</button></p>
- <p>
- <small>Esta pesquisa é efectuada pelo Google e utiliza software
- proprietário.</small>
- </p>
- </form>
-</search>
diff --git a/src/components/HeaderLink.astro b/src/components/organisms/ActiveLink.astro
index 8c01f92..8c01f92 100644
--- a/src/components/HeaderLink.astro
+++ b/src/components/organisms/ActiveLink.astro
diff --git a/src/components/organisms/Date.astro b/src/components/organisms/Date.astro
new file mode 100644
index 0000000..a8b643d
--- /dev/null
+++ b/src/components/organisms/Date.astro
@@ -0,0 +1,14 @@
+---
+interface Props {
+ date: Date;
+ locales: Intl.LocalesArgument;
+ options: Intl.DateTimeFormatOptions;
+}
+
+const { date, locales, options } = Astro.props;
+
+const datetime = date.toISOString();
+const format = new Intl.DateTimeFormat(locales, options).format(date);
+---
+
+<date {datetime}>{format}</date>
diff --git a/src/components/organisms/KeywordsList.astro b/src/components/organisms/KeywordsList.astro
new file mode 100644
index 0000000..4d4b140
--- /dev/null
+++ b/src/components/organisms/KeywordsList.astro
@@ -0,0 +1,31 @@
+---
+interface Props {
+ keywords: string[];
+}
+
+const { keywords } = Astro.props;
+---
+
+<p>
+ {keywords.map((x) => <span>#<b>{x}</b></span>)}
+</p>
+
+<style>
+ p {
+ display: flex;
+ flex-direction: row-reverse;
+ flex-wrap: wrap;
+ gap: calc(var(--size-0) * 1em);
+
+ & > * {
+ border-radius: calc(infinity * 1px);
+ background-color: color-mix(
+ in srgb,
+ var(--color-active) 10%,
+ transparent
+ );
+ color: var(--color-active);
+ padding-inline: calc(var(--size-2) * 1em);
+ }
+ }
+</style>
diff --git a/src/components/signature/Authors.astro b/src/components/signature/Authors.astro
index 43a2b36..71a3d62 100644
--- a/src/components/signature/Authors.astro
+++ b/src/components/signature/Authors.astro
@@ -3,16 +3,14 @@ import { toPK } from "@lib/pgp";
import { createKeyFromArmor } from "@lib/pgp/create";
import type { Verification } from "@lib/pgp/verify";
import { defined, get, instanciate } from "@utils/anonymous";
-import { type CollectionEntry, z } from "astro:content";
+import { z } from "astro:content";
import type { EntityTypesEnum } from "src/consts";
import qrcode from "yaqrcode";
+import type { getSigners } from "@lib/collection/helpers";
interface Props {
verifications: NonNullable<Verification["verifications"]>;
- expectedSigners: {
- entity: CollectionEntry<"entity">;
- role: z.infer<typeof EntityTypesEnum>;
- }[];
+ expectedSigners: Awaited<ReturnType<typeof getSigners>>;
commitSignerKey?: string;
}
diff --git a/src/components/templates/MicroBlog.astro b/src/components/templates/MicroBlog.astro
new file mode 100644
index 0000000..b7019c5
--- /dev/null
+++ b/src/components/templates/MicroBlog.astro
@@ -0,0 +1,114 @@
+---
+import Date from "@components/organisms/Date.astro";
+import KeywordsList from "@components/organisms/KeywordsList.astro";
+import { getFirstUserID, getLastUpdate } from "@lib/collection/helpers";
+import { Micro } from "@lib/collection/schemas";
+import type { CollectionEntry, z } from "astro:content";
+
+interface Props extends CollectionEntry<"blog"> {
+ data: z.infer<typeof Micro>;
+}
+
+const micro = Astro.props;
+const { id, data, rendered } = micro;
+const { title, lang, keywords } = data;
+const date = getLastUpdate(micro);
+const user = await getFirstUserID(micro);
+const display = user?.name ?? user?.email ?? user?.entity ?? "";
+const [first, ...names] = display.split(/\s/);
+const last = names.length > 0 ? names[names.length - 1] : "";
+const little = ((first?.[0] ?? "") + (last?.[0] ?? "")).slice(0, 2);
+---
+<article>
+ <header>
+ <h3 class="title">
+ <a href={`/blog/read/${id}`}>{title}</a>
+ </h3>
+ <span class="profile_picture">{
+ user?.website ? <a href={user.website}>{little}</a> : (
+ <span>{little}</span>
+ )
+ }</span>
+ <div>
+ {first} {last} <small>· <Date
+ {date}
+ locales={lang}
+ options={{ month: "short", day: "numeric" }}
+ /></small>
+ </div>
+ </header>
+ <div class="content">
+ <small {lang}>
+ <Fragment set:html={rendered?.html} />
+ </small>
+ <footer>
+ <div class="keywords small"><KeywordsList {keywords} /></div>
+ </footer>
+ </div>
+ <aside>
+ <small><a href="/blog/micro/1">Ver todos os microposts</a></small>
+ </aside>
+</article>
+
+<style is:inline>
+ .content > [lang] > *:first-child {
+ margin-block-start: 0;
+ }
+ .content > [lang] > *:last-child {
+ margin-block-end: 0;
+ }
+</style>
+<style>
+ article {
+ border-radius: calc(var(--size-1) * 1em);
+ box-shadow: 0 0 calc(var(--size-1) * 1em) var(--color-light);
+ padding: calc(var(--size-4) * 1em);
+ display: grid;
+ grid-template-rows: repeat(3, auto);
+ grid-template-columns: calc(var(--size-9) * 1em) auto;
+ gap: calc(var(--size-1) * 1em);
+
+ & > header {
+ display: contents;
+ }
+
+ & > aside {
+ grid-row: 3 / 4;
+ grid-column: 1 / 3;
+ border-block-start: 1px solid #e7e7e7;
+ padding-block-start: calc(var(--size-1) * 1em);
+ }
+ }
+
+ .profile_picture {
+ grid-row: 1 / 3;
+ grid-column: 1 / 2;
+
+ & > * {
+ display: inline-grid;
+ place-content: center;
+ width: calc(var(--size-9) * 1em);
+ height: calc(var(--size-9) * 1em);
+ aspect-ratio: 1 / 1;
+ background-color: var(--color-active);
+ color: #fff;
+ font-weight: 950;
+ font-size: smaller;
+ border-radius: calc(infinity * 1px);
+ text-align: center;
+ text-transform: uppercase;
+ }
+ }
+
+ .title {
+ display: none;
+ }
+ .content {
+ grid-row: 2/3;
+ grid-column: 2 / 3;
+ }
+
+ .keywords {
+ margin-block-end: 0;
+ }
+</style>
diff --git a/src/components/templates/Search.astro b/src/components/templates/Search.astro
new file mode 100644
index 0000000..5245643
--- /dev/null
+++ b/src/components/templates/Search.astro
@@ -0,0 +1,67 @@
+---
+const { site } = Astro;
+---
+
+<search>
+ <link rel="dns-prefetch" href="https://www.google.com/search">
+ <form
+ action="https://www.google.com/search"
+ target="_blank"
+ rel="external noreferrer search"
+ role="search"
+ autocomplete="on"
+ name="search"
+ >
+ <details>
+ <summary>Pesquisar no website</summary>
+ <div class="details">
+ <p>
+ <label>Barra de pesquisa <input
+ name="q"
+ type="search"
+ placeholder={`site:${site} consulta de pesquisa`}
+ value={`site:${site} `}
+ required
+ title={`"site:${site} " é usado para que os resultados da pesquisa fiquem restritos a este website`}
+ pattern={`site:${site} .+`}
+ size={`site:${site} .+`.length}
+ /></label>
+ </p>
+ <p class="mute">
+ <small>Esta pesquisa é efectuada pelo Google e <strong>utiliza
+ software proprietário.</strong></small>
+ </p>
+ <p><button type="submit">🔍 Pesquisar</button></p>
+ </div>
+ </details>
+ </form>
+</search>
+
+<style>
+ search {
+ padding-block-end: calc(var(--size-4) * 1em);
+ }
+
+ summary {
+ font-size: calc(var(--size-3) * 1rem);
+ font-weight: bolder;
+ }
+
+ .details {
+ border-radius: calc(var(--size-1) * 1em);
+ border: 1px solid var(--color-light);
+ margin-block-start: calc(var(--size-1) * 1em);
+ font-size: calc(var(--size-3) * 1rem);
+ padding-inline: calc(var(--size-4) * 1em);
+ padding-block: calc(var(--size-2) * 1em);
+
+ & > p {
+ margin-block: calc(var(--size-2) * 1em);
+ line-height: calc(var(--size-8) * 1rem);
+ }
+
+ & input[type="search"] {
+ width: 100%;
+ }
+ }
+</style>
diff --git a/src/components/templates/SimplePostList.astro b/src/components/templates/SimplePostList.astro
new file mode 100644
index 0000000..0ec33e3
--- /dev/null
+++ b/src/components/templates/SimplePostList.astro
@@ -0,0 +1,81 @@
+---
+import Date from "@components/organisms/Date.astro";
+import KeywordsList from "@components/organisms/KeywordsList.astro";
+import { getFirstUserID, getLastUpdate } from "@lib/collection/helpers";
+import type { Original } from "@lib/collection/schemas";
+import type { z } from "astro:content";
+import type { CollectionEntry } from "astro:content";
+
+interface Props {
+ posts: (CollectionEntry<"blog"> & { data: z.infer<typeof Original> })[];
+}
+
+const { posts } = Astro.props;
+---
+<ol>
+ {
+ await Promise.all(posts.map(async (post) => {
+ const { id, data } = post;
+ const { title, description, lang, keywords } = data;
+ const { name, email, entity } = await getFirstUserID(post);
+ const display = name ?? email ?? entity;
+ return (
+ <li>
+ <article>
+ <h3><a href={`/blog/read/${id}`}>{title}</a></h3>
+ {
+ description &&
+ description.split("\n\n").map((paragraph) => (
+ <p class="small">{paragraph}</p>
+ ))
+ }
+
+ <footer class="small">
+ <Date
+ date={getLastUpdate(post)}
+ locales={lang}
+ options={{
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ }}
+ />{display}
+ <KeywordsList {keywords} />
+ </footer>
+ </article>
+ </li>
+ );
+ }))
+ }
+</ol>
+
+<style>
+ ol {
+ margin-inline-start: calc(var(--size-7) * 1em);
+ margin-block: calc(var(--size-7) * 1em);
+ & > li {
+ margin-block-start: calc(var(--size-2) * 1em);
+ & > article {
+ padding-inline-end: calc(var(--size-9) * 1em);
+
+ & > p:not(:first-of-type) {
+ margin-block-start: 1.5em;
+ }
+
+ & > footer {
+ display: flex;
+ flex-direction: column;
+ gap: calc(var(--size-1) * 1em);
+ }
+ }
+ }
+ }
+
+ @media (width >= 40rem) {
+ ol > li > article > footer {
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ }
+ }
+</style>