diff --git a/adminSiteServer/apiRouter.ts b/adminSiteServer/apiRouter.ts index b3f18a9794d..1a7745e2902 100644 --- a/adminSiteServer/apiRouter.ts +++ b/adminSiteServer/apiRouter.ts @@ -148,6 +148,7 @@ import { match } from "ts-pattern" import { GdocDataInsight } from "../db/model/Gdoc/GdocDataInsight.js" import { GdocHomepage } from "../db/model/Gdoc/GdocHomepage.js" import { GdocAuthor } from "../db/model/Gdoc/GdocAuthor.js" +import path from "path" const apiRouter = new FunctionalRouter() @@ -2543,4 +2544,114 @@ deleteRouteWithRWTransaction( } ) +// Get an ArchieML output of all the work produced by an author. This includes +// gdoc articles, gdoc modular/linear topic pages and wordpress modular topic +// pages. Data insights are excluded. This is used to manually populate the +// [.secondary] section of the {.research-and-writing} block of author pages +// using the alternate template, which highlights topics rather than articles. +getRouteWithROTransaction(apiRouter, "/all-work", async (req, res, trx) => { + type WordpressPageRecord = { + isWordpressPage: number + } & Record< + "slug" | "title" | "subtitle" | "thumbnail" | "authors" | "publishedAt", + string + > + type GdocRecord = Pick + + const author = req.query.author || "Max Roser" + const gdocs = await db.knexRaw( + trx, + `-- sql + SELECT id, publishedAt + FROM posts_gdocs + WHERE JSON_CONTAINS(content->'$.authors', '"${author}"') + AND type NOT IN ("data-insight", "fragment") + AND published = 1 + ` + ) + + // type: page + const wpModularTopicPages = await db.knexRaw( + trx, + `-- sql + SELECT + wpApiSnapshot->>"$.slug" as slug, + wpApiSnapshot->>"$.title.rendered" as title, + wpApiSnapshot->>"$.excerpt.rendered" as subtitle, + TRUE as isWordpressPage, + wpApiSnapshot->>"$.authors_name" as authors, + wpApiSnapshot->>"$.featured_media_paths.medium_large" as thumbnail, + wpApiSnapshot->>"$.date" as publishedAt + FROM posts p + WHERE wpApiSnapshot->>"$.content" LIKE '%topic-page%' + AND JSON_CONTAINS(wpApiSnapshot->'$.authors_name', '"${author}"') + AND wpApiSnapshot->>"$.status" = 'publish' + AND NOT EXISTS ( + SELECT 1 FROM posts_gdocs pg + WHERE pg.slug = p.slug + AND pg.content->>'$.type' LIKE '%topic-page' + ) + ` + ) + + const isWordpressPage = ( + post: WordpressPageRecord | GdocRecord + ): post is WordpressPageRecord => + (post as WordpressPageRecord).isWordpressPage === 1 + + function* generateProperty(key: string, value: string) { + yield `${key}: ${value}\n` + } + + const sortByDateDesc = ( + a: GdocRecord | WordpressPageRecord, + b: GdocRecord | WordpressPageRecord + ): number => { + if (!a.publishedAt || !b.publishedAt) return 0 + return ( + new Date(b.publishedAt).getTime() - + new Date(a.publishedAt).getTime() + ) + } + + function* generateAllWorkArchieMl() { + for (const post of [...gdocs, ...wpModularTopicPages].sort( + sortByDateDesc + )) { + if (isWordpressPage(post)) { + yield* generateProperty( + "url", + `https://ourworldindata.org/${post.slug}` + ) + yield* generateProperty("title", post.title) + yield* generateProperty("subtitle", post.subtitle) + yield* generateProperty( + "authors", + JSON.parse(post.authors).join(", ") + ) + const parsedPath = path.parse(post.thumbnail) + yield* generateProperty( + "filename", + // /app/uploads/2021/09/reducing-fertilizer-768x301.png -> reducing-fertilizer.png + path.format({ + name: parsedPath.name.replace(/-\d+x\d+$/, ""), + ext: parsedPath.ext, + }) + ) + yield "\n" + } else { + // this is a gdoc + yield* generateProperty( + "url", + `https://docs.google.com/document/d/${post.id}/edit` + ) + yield "\n" + } + } + } + + res.type("text/plain") + res.send([...generateAllWorkArchieMl()].join("")) +}) + export { apiRouter } diff --git a/db/model/Gdoc/rawToEnriched.ts b/db/model/Gdoc/rawToEnriched.ts index 9557b7d4c1f..01bd1e2ce68 100644 --- a/db/model/Gdoc/rawToEnriched.ts +++ b/db/model/Gdoc/rawToEnriched.ts @@ -1636,40 +1636,6 @@ function parseExpandableParagraph( function parseResearchAndWritingBlock( raw: RawBlockResearchAndWriting ): EnrichedBlockResearchAndWriting { - const createError = ( - error: ParseError, - heading = "", - hideAuthors = false, - primary = [ - { - value: { url: "" }, - }, - ], - secondary = [ - { - value: { url: "" }, - }, - ], - more: EnrichedBlockResearchAndWritingRow = { - heading: "", - articles: [], - }, - latest: EnrichedBlockResearchAndWritingRow = { - heading: "", - articles: [], - }, - rows: EnrichedBlockResearchAndWritingRow[] = [] - ): EnrichedBlockResearchAndWriting => ({ - type: "research-and-writing", - heading, - "hide-authors": hideAuthors, - primary, - secondary, - more, - latest, - rows, - parseErrors: [error], - }) const parseErrors: ParseError[] = [] function enrichLink( @@ -1729,12 +1695,10 @@ function parseResearchAndWritingBlock( }) } - if (!raw.value.primary) - return createError({ message: "Missing primary link" }) const primary: EnrichedBlockResearchAndWritingLink[] = [] if (isArray(raw.value.primary)) { primary.push(...raw.value.primary.map((link) => enrichLink(link))) - } else { + } else if (raw.value.primary) { primary.push(enrichLink(raw.value.primary)) } diff --git a/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts b/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts index fcb2f19400a..9ef9f855798 100644 --- a/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts +++ b/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts @@ -852,8 +852,8 @@ export enum SocialLinkType { } export type RawSocialLink = { - text: string - url: string + text?: string + url?: string type?: SocialLinkType } @@ -862,7 +862,11 @@ export type RawBlockSocials = { value: RawSocialLink[] | ArchieMLUnexpectedNonObjectValue } -export type EnrichedSocialLink = RawSocialLink +export type EnrichedSocialLink = { + text: string + url: string + type?: SocialLinkType +} export type EnrichedBlockSocials = { type: "socials" diff --git a/packages/@ourworldindata/utils/src/image.ts b/packages/@ourworldindata/utils/src/image.ts index 9579cd6e693..6f7aa55b1bf 100644 --- a/packages/@ourworldindata/utils/src/image.ts +++ b/packages/@ourworldindata/utils/src/image.ts @@ -48,7 +48,7 @@ export function getFilenameWithoutExtension( export function getFilenameExtension( filename: ImageMetadata["filename"] ): string { - return filename.slice(filename.indexOf(".") + 1) + return filename.slice(filename.lastIndexOf(".") + 1) } export function getFilenameAsPng(filename: ImageMetadata["filename"]): string {