summaryrefslogtreecommitdiff
path: root/src/pages/blog/read
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages/blog/read')
-rw-r--r--src/pages/blog/read/[...slug].astro333
1 files changed, 333 insertions, 0 deletions
diff --git a/src/pages/blog/read/[...slug].astro b/src/pages/blog/read/[...slug].astro
new file mode 100644
index 0000000..05d68e8
--- /dev/null
+++ b/src/pages/blog/read/[...slug].astro
@@ -0,0 +1,333 @@
+---
+import { type CollectionEntry, getCollection } from "astro:content";
+import { render } from "astro:content";
+import BaseHead from "@components/BaseHead.astro";
+import Translations from "@components/Translations.astro";
+import { toIso8601Full } from "@utils/datetime";
+import ReadingTime from "@components/ReadingTime.astro";
+import Keywords from "@components/Keywords.astro";
+import Citations from "@components/Citations.astro";
+import Signature from "@components/signature/Signature.astro";
+import CopyrightNotice from "@components/CopyrightNotice.astro";
+import { getEntries } from "astro:content";
+import { verifier as verifierPrototype } from "@lib/pgp/verify";
+import { defined, get } from "@utils/anonymous";
+import Authors from "@components/signature/Authors.astro";
+import { getEntry } from "astro:content";
+
+export async function getStaticPaths() {
+ const posts = await getCollection("blog");
+ return posts.map((post) => ({
+ params: { slug: post.id },
+ props: post,
+ }));
+}
+
+type Props = CollectionEntry<"blog">;
+
+const post = Astro.props;
+
+if (defined(post.data.translationOf)) {
+ const original = await getEntry(
+ post.data.translationOf as CollectionEntry<"blog">,
+ );
+
+ if (!original) {
+ throw new Error(`Original post not found for ${post.id}`);
+ }
+
+ const originalAuthor = (original.data.signers ?? []).filter(
+ (s) => s.role === "author",
+ ).map((s) => s.entity.id)?.[0];
+ const originalCoAuthors = new Set(
+ (original.data.signer ?? []).filter(
+ (s) => s.role === "co-author",
+ ).map((s) => s.entity.id),
+ );
+ const translationAuthor = (post.data.signer ?? []).filter(
+ (s) => s.role === "author",
+ ).map((s) => s.entity.id)?.[0];
+ const translationCoAuthors = new Set(
+ (post.data.signer ?? []).filter(
+ (s) => s.role === "co-author",
+ ).map((s) => s.entity.id),
+ );
+
+ if (
+ (translationAuthor !== undefined &&
+ translationAuthor !== originalAuthor) ||
+ !translationCoAuthors.isSubsetOf(originalCoAuthors)
+ ) {
+ throw new Error(
+ `Post ${post.id} has mismatched (co-)authors from original post ${original.id}`,
+ );
+ }
+
+ const translators = (post.data.signer ?? []).filter(
+ (s) => s.role === "translator",
+ ).map((s) => s.entity.id);
+
+ for (const translator of translators) {
+ if (
+ originalAuthor === translator || originalCoAuthors.has(translator)
+ ) {
+ throw new Error(
+ `Translator ${translator} in ${post.id} is already a (co-)author in original post`,
+ );
+ }
+ }
+} else {
+ if (post.data.signer?.some((x) => x.role === "translator")) {
+ throw new Error(
+ `Post ${post.id} is not a translation but has translators defined`,
+ );
+ }
+}
+
+// Add own post as a translation
+const translationsSet = new Set(
+ (await getCollection(
+ "blog",
+ (x) =>
+ x.data.translationOf?.id ===
+ (post.data.translationOf !== undefined
+ ? post.data.translationOf.id
+ : post.id),
+ ) ?? []).map(({ id }) => id),
+);
+
+translationsSet.add(post.id);
+const translations = [...translationsSet.values()].map((id) => ({
+ collection: post.collection,
+ id,
+}));
+
+const signers = await getEntries(
+ post.data.signer?.map(get("entity")) ?? [],
+).then((x) => x.filter(defined))
+ .then((x) =>
+ x.map((x) => ({
+ entity: x,
+ role: post.data.signer?.find((y) => y.entity.id === x.id)?.role,
+ }))
+ )
+ .then((x) => x.filter((x) => x.role !== undefined));
+
+const verifier = await verifierPrototype.then((x) => x.clone());
+
+// Add signers public keys to keyring
+for (const { data } of signers.map(get("entity"))) {
+ if (data.publickey.armor !== undefined) {
+ verifier.addKeyFromArmor(data.publickey.armor);
+ }
+}
+
+const verification = post.filePath !== undefined
+ ? await verifier.verify([
+ new URL(`file://${Deno.cwd()}/${post.filePath}`),
+ ])
+ : undefined;
+
+const { Content } = await render(post);
+
+const { lang } = post.data;
+
+const commit = await verification?.commit;
+---
+
+<html lang="pt-PT">
+ <head>
+ <BaseHead title={post.data.title} description={post.data.description} />
+ </head>
+
+ <body>
+ <main>
+ <article
+ itemscope
+ itemtype="http://schema.org/BlogPosting"
+ itemid={Astro.url.href}
+ >
+ <Translations {translations} {lang} />
+ <hgroup>
+ <h1 itemprop="headline">{post.data.title}</h1>
+ {
+ post.data.subtitle && (
+ <p itemprop="alternativeHeadline" class="subtitle">
+ {post.data.subtitle}
+ </p>
+ )
+ }
+ </hgroup>
+ {
+ post.data.description && (
+ <section itemprop="abstract">
+ <h2>Resumo</h2>
+ {
+ post.data.description.split(new RegExp("\\s{2,}"))
+ .map((
+ x,
+ ) => (
+ <p>{x}</p>
+ ))
+ }
+ </section>
+ )
+ }
+ {verification && <Signature {lang} {verification} />}
+ <footer>
+ {
+ verification?.verifications &&
+ (
+ <Authors
+ verifications={verification.verifications}
+ expectedSigners={signers}
+ commitSignerKey={commit?.signature?.keyFingerPrint}
+ />
+ )
+ }
+ <dl>
+ <dt>Data de criação</dt>
+ <dd>
+ <time
+ itemprop="dateCreated"
+ datetime={toIso8601Full(post.data.dateCreated)}
+ >{
+ new Intl.DateTimeFormat([lang], {}).format(
+ post.data.dateCreated,
+ )
+ }</time>
+ </dd>
+ {
+ post.data.dateUpdated && (
+ <dt>Última atualização</dt><dd>
+ <time
+ itemprop="dateUpdated"
+ datetime={toIso8601Full(post.data.dateUpdated)}
+ >{
+ new Intl.DateTimeFormat([lang], {}).format(
+ post.data.dateUpdated,
+ )
+ }</time>
+ </dd>
+ )
+ }
+ {
+ post.data.locationCreated && (
+ <dt
+ itemprop="locationCreated"
+ itemscope
+ itemtype="https://schema.org/Place"
+ >
+ Local de criação
+ </dt><dd>
+ <span itemprop="name">{post.data.locationCreated}</span>
+ </dd>
+ )
+ }
+ </dl>
+ <ReadingTime body={post.body} {lang} />
+ </footer>
+ <hr />
+ <div itemprop="articleBody text"><Content /></div>
+ <hr />
+ <Keywords keywords={post.data.keywords} />
+ <Citations citations={post.data.relatedPosts} />
+ <CopyrightNotice
+ author={signers[0]?.entity.data.website?.[0] ?? "Anonymous"}
+ website={signers[0]?.entity.data.website?.[0]}
+ email={signers[0]?.entity.data.website?.[0]}
+ title={post.data.title}
+ dateCreated={post.data.dateCreated}
+ license={post.data.license as License}
+ />
+ </article>
+ </main>
+ </body>
+</html>
+
+<script type="module" is:inline>
+ hashchange();
+
+ window.addEventListener("hashchange", hashchange);
+
+ document.addEventListener(
+ "click",
+ function (event) {
+ if (
+ event.target &&
+ event.target instanceof HTMLAnchorElement &&
+ event.target.href === location.href &&
+ location.hash.length > 1
+ ) {
+ requestIdleCallback(function () {
+ if (!event.defaultPrevented) {
+ hashchange();
+ }
+ });
+ }
+ },
+ false,
+ );
+
+ function hashchange() {
+ let hash;
+
+ try {
+ hash = decodeURIComponent(location.hash.slice(1)).toLowerCase();
+ } catch (e) {
+ return;
+ }
+
+ const name = "user-content-" + hash;
+ const target = document.getElementById(name) ||
+ document.getElementsByName(name)[0];
+
+ if (target) {
+ requestIdleCallback(function () {
+ target.scrollIntoView();
+ });
+ }
+ }
+</script>
+
+<style is:inline>
+ section[data-footnotes].footnotes {
+ word-wrap: break-word;
+ }
+</style>
+
+<style>
+ hgroup {
+ text-align: center;
+ }
+
+ .subtitle {
+ font-weight: lighter;
+ }
+
+ [itemprop~="articleBody"] {
+ line-height: 1.4;
+ font-size: 1.2em;
+ text-align: justify;
+
+ & h1,
+ & h2,
+ & h3 {
+ line-height: 1.2;
+ }
+ }
+
+ [itemprop="abstract"] {
+ margin-inline: 1em;
+ padding-block: 1em;
+ font-style: italic;
+ }
+
+ @media print {
+ body {
+ font-size: 1rem;
+ font-family: var(--ff-serif);
+ line-height: 1.62;
+ }
+ }
+</style>