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].astro420
1 files changed, 265 insertions, 155 deletions
diff --git a/src/pages/blog/read/[...slug].astro b/src/pages/blog/read/[...slug].astro
index 5b42e86..71d0929 100644
--- a/src/pages/blog/read/[...slug].astro
+++ b/src/pages/blog/read/[...slug].astro
@@ -1,7 +1,6 @@
---
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";
@@ -10,10 +9,17 @@ import Citations from "@components/Citations.astro";
import Signature from "@components/signature/Signature.astro";
import CopyrightNotice from "@components/CopyrightNotice.astro";
import { verifier as verifierPrototype } from "@lib/pgp/verify";
-import { getSigners } from "@lib/collection/helpers";
+import { getSigners, isTranslation } from "@lib/collection/helpers";
import { get } from "@utils/anonymous";
import Authors from "@components/signature/Authors.astro";
import { getEntry } from "astro:content";
+import Base from "@layouts/Base.astro";
+import readingTime from "reading-time";
+import type {
+ Entry,
+ MicroEntry,
+ OriginalEntry,
+} from "@lib/collection/schemas";
export async function getStaticPaths() {
const posts = await getCollection("blog");
@@ -27,11 +33,11 @@ type Props = CollectionEntry<"blog">;
const post = Astro.props;
-let original: CollectionEntry<"blog">;
-if (post.data.kind === "translation") {
- original = await getEntry(
- post.data.translationOf as CollectionEntry<"blog">,
- );
+let original: OriginalEntry | MicroEntry;
+if (isTranslation(post)) {
+ original = await getEntry(post.data.translationOf) as
+ | OriginalEntry
+ | MicroEntry;
if (!original) {
throw new Error(`Original post not found for ${post.id}`);
@@ -126,174 +132,278 @@ const { Content } = await render(post);
const { lang } = post.data;
const commit = await verification?.commit;
+
+const reading = post.body ? readingTime(post.body, {}) : undefined;
+const minutes = reading === undefined
+ ? undefined
+ : Math.ceil(reading.minutes);
+const estimative = reading === undefined
+ ? undefined
+ : new Intl.DurationFormat(lang, {
+ style: "long",
+ }).format({ minutes });
+const duration = minutes === undefined
+ ? undefined
+ : `PT${Math.floor(minutes / 60) > 0 ? Math.floor(minutes / 60) + "H" : ""}${
+ minutes % 60 > 0 ? minutes % 60 + "M" : ""
+ }`;
+
+const getOrUndefined = (k: string) =>
+ k in post.data ? post.data[k as keyof typeof post.data] : undefined;
+const author = {
+ "@type": "Person",
+} as const;
+const contributor = post.data.signers.filter(({ role }) =>
+ role === "co-author"
+).map(() => {
+ return {
+ "@type": "Person",
+ } as const;
+});
+const translator = post.data.signers.filter(({ role }) =>
+ role === "translator"
+).map(() => {
+ return {
+ "@type": "Person",
+ } as const;
+});
+const JSONLD = {
+ "@context": "https://schema.org",
+ "@type": "BlogPosting",
+ "@id": Astro.url.href,
+ articleBody: post.rendered?.html ?? post.body,
+ abstract: getOrUndefined("description"),
+ alternativeHeadline: getOrUndefined("subtitle"),
+ author,
+ citation: [].map(() => {
+ return {
+ "@type": "CreativeWork",
+ };
+ }),
+ contributor,
+ copyrightHolder: [author, ...contributor, ...translator],
+ // copyrightNotice: post.data.license, // WORKAROUND
+ copyrightYear: post.data.dateCreated.getFullYear(),
+ creativeWorkStatus: "Published",
+ dateCreated: post.data.dateCreated.toISOString(),
+ dateModified: "dateUpdated" in post.data
+ ? post.data.dateUpdated?.toISOString()
+ : undefined,
+ // datePublished: undefined, // from git commit commit date
+ encodingFormat: "text/html",
+ headline: post.data.title,
+ inLanguage: post.data.lang,
+ isAccessibleForFree: true,
+ isBasedOn: isTranslation(post)
+ ? {
+ "@type": "BlogPosting",
+ "@id": new URL(`blog/read/${post.data.translationOf}`, Astro.site).href,
+ }
+ : undefined,
+ keywords: original.data.keywords,
+ license: post.data.license, // WORKAROUND
+ locationCreated: {
+ "@type": "Place",
+ // XXX: getOrUndefined("locationCreated"),
+ },
+ mentions: [].map(() => {
+ return {
+ "@type": "Thing",
+ };
+ }),
+ // publication: {
+ // "@type": "PublicationEvent",
+ // }, // from git commit
+ // publisher: {
+ // "@type": "Person",
+ // }, // from git commit
+ text: post.rendered?.html ?? post.body,
+ timeRequired: post.body !== undefined ? duration : undefined,
+ translationOf: isTranslation(post)
+ ? {
+ "@type": "BlogPosting",
+ "@id": new URL(`blog/read/${post.data.translationOf}`, Astro.site).href,
+ }
+ : undefined,
+ translator,
+ // version: undefined // TODO
+ wordCount: reading?.words,
+ workTranslations: translations.filter(({ id }) => id !== post.id).map((
+ { id },
+ ) => ({
+ "@type": "BlogPosting",
+ "@id": new URL(`blog/read/${id}`, Astro.site).href,
+ })),
+ description: getOrUndefined("description"),
+ name: post.data.title,
+ url: Astro.url.href,
+} as const;
---
-<html lang="pt-PT">
- <head>
- <BaseHead
- title={post.data.title}
- description={"description" in post.data
- ? post.data.description
- : post.data.title}
- />
- </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>
- {
- "subtitle" in post.data && (
- <p itemprop="alternativeHeadline" class="subtitle">
- {post.data.subtitle}
- </p>
- )
- }
- </hgroup>
+<Base
+ title={post.data.title}
+ description={"description" in post.data ? post.data.description : post.data.title}
+>
+ <main>
+ <article
+ itemscope
+ itemtype="http://schema.org/BlogPosting"
+ itemid={Astro.url.href}
+ >
+ <Translations {translations} {lang} />
+ <hgroup>
+ <h1 itemprop="headline">{post.data.title}</h1>
{
- "description" in post.data && post.data.description &&
- (
- <section itemprop="abstract">
- <h2>Resumo</h2>
- {
- post.data.description.split(new RegExp("\\s{2,}"))
- .map((
- x,
- ) => <p>{x}</p>)
- }
- </section>
+ "subtitle" in post.data && (
+ <p itemprop="alternativeHeadline" class="subtitle">
+ {post.data.subtitle}
+ </p>
)
}
- {verification && <Signature {lang} {verification} />}
- <footer>
- {
- verification?.verifications &&
+ </hgroup>
+ {
+ "description" in post.data && post.data.description &&
(
- <Authors
- verifications={verification.verifications}
- expectedSigners={signers}
- commitSignerKey={commit?.signature?.signer}
- />
- )
- }
- <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>
+ <section itemprop="abstract">
+ <h2>Resumo</h2>
{
- post.data.dateUpdated && (
- <dt>Última atualização</dt><dd>
- <time
- itemprop="dateModified"
- datetime={toIso8601Full(post.data.dateUpdated)}
- >{
- new Intl.DateTimeFormat([lang], {}).format(
- post.data.dateUpdated,
- )
- }</time>
- </dd>
- )
- }
- {
- "locationCreated" in post.data &&
- 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>
- )
+ post.data.description.split(new RegExp("\\s{2,}"))
+ .map((
+ x,
+ ) => <p>{x}</p>)
}
- </dl>
- <ReadingTime body={post.body} {lang} />
- </footer>
- <hr />
- <div itemprop="articleBody text"><Content /></div>
- <hr />
- {
- "keywords" in original.data && (
- <Keywords keywords={original.data.keywords} />
- )
- }
+ </section>
+ )
+ }
+ {verification && <Signature {lang} {verification} />}
+ <footer>
{
- "relatedPosts" in original.data && (
- <Citations citations={original.data.relatedPosts} />
+ verification?.verifications &&
+ (
+ <Authors
+ verifications={verification.verifications}
+ expectedSigners={signers}
+ commitSignerKey={commit?.signature?.signer}
+ />
)
}
- <CopyrightNotice
- author={signers[0]?.entity.data.websites?.[0] ?? "Anonymous"}
- website={signers[0]?.entity.data.websites?.[0]}
- email={signers[0]?.entity.data.websites?.[0]}
- title={post.data.title}
- dateCreated={post.data.dateCreated}
- license={post.data.license}
- />
- </article>
- </main>
- </body>
-</html>
+ <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="dateModified"
+ datetime={toIso8601Full(post.data.dateUpdated)}
+ >{
+ new Intl.DateTimeFormat([lang], {}).format(
+ post.data.dateUpdated,
+ )
+ }</time>
+ </dd>
+ )
+ }
+ {
+ "locationCreated" in post.data &&
+ 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" in original.data && (
+ <Keywords keywords={original.data.keywords} />
+ )
+ }
+ {
+ "relatedPosts" in original.data && (
+ <Citations citations={original.data.relatedPosts} />
+ )
+ }
+ <CopyrightNotice
+ author={signers[0]?.entity.data.websites?.[0] ?? "Anonymous"}
+ website={signers[0]?.entity.data.websites?.[0]}
+ email={signers[0]?.entity.data.websites?.[0]}
+ title={post.data.title}
+ dateCreated={post.data.dateCreated}
+ license={post.data.license}
+ />
+ </article>
+ </main>
+</Base>
+
+<script
+ type="application/ld+json"
+ is:inline
+ set:html={JSON.stringify(JSONLD)}
+/>
<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,
- );
+ 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;
+ function hashchange() {
+ let hash;
- try {
- hash = decodeURIComponent(location.hash.slice(1)).toLowerCase();
- } catch (e) {
- return;
- }
+ 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];
+ const name = "user-content-" + hash;
+ const target = document.getElementById(name) ||
+ document.getElementsByName(name)[0];
- if (target) {
- requestIdleCallback(function () {
- target.scrollIntoView();
- });
- }
+ if (target) {
+ requestIdleCallback(function () {
+ target.scrollIntoView();
+ });
}
+ }
</script>
<style is:inline>