From 449f71511e539878af4c65f073cdc373b469c45e Mon Sep 17 00:00:00 2001 From: João Augusto Costa Branco Marado Torres Date: Sun, 6 Jul 2025 22:27:04 -0300 Subject: feat: more structured data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: João Augusto Costa Branco Marado Torres --- src/pages/blog/read/[...slug].astro | 420 +++++++++++++++++++++++------------- 1 file changed, 265 insertions(+), 155 deletions(-) (limited to 'src/pages/blog') 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; --- - - - - - - -
-
- -
-

{post.data.title}

- { - "subtitle" in post.data && ( -

- {post.data.subtitle} -

- ) - } -
+ +
+
+ +
+

{post.data.title}

{ - "description" in post.data && post.data.description && - ( -
-

Resumo

- { - post.data.description.split(new RegExp("\\s{2,}")) - .map(( - x, - ) =>

{x}

) - } -
+ "subtitle" in post.data && ( +

+ {post.data.subtitle} +

) } - {verification && } -
- { - verification?.verifications && +
+ { + "description" in post.data && post.data.description && ( - - ) - } -
-
Data de criação
-
- -
+
+

Resumo

{ - post.data.dateUpdated && ( -
Última atualização
- -
- ) - } - { - "locationCreated" in post.data && - post.data.locationCreated && ( -
- Local de criação -
- {post.data.locationCreated} -
- ) + post.data.description.split(new RegExp("\\s{2,}")) + .map(( + x, + ) =>

{x}

) } -
- - -
-
-
- { - "keywords" in original.data && ( - - ) - } + + ) + } + {verification && } +
{ - "relatedPosts" in original.data && ( - + verification?.verifications && + ( + ) } - -
-
- - +
+
Data de criação
+
+ +
+ { + post.data.dateUpdated && ( +
Última atualização
+ +
+ ) + } + { + "locationCreated" in post.data && + post.data.locationCreated && ( +
+ Local de criação +
+ {post.data.locationCreated} +
+ ) + } +
+ + +
+
+
+ { + "keywords" in original.data && ( + + ) + } + { + "relatedPosts" in original.data && ( + + ) + } + +
+
+ + +