diff options
Diffstat (limited to 'src/pages/blog/read')
-rw-r--r-- | src/pages/blog/read/[...slug].astro | 420 |
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> |