diff --git a/baker/SiteBaker.tsx b/baker/SiteBaker.tsx index b21f0add55d..73ccf9cd4cb 100644 --- a/baker/SiteBaker.tsx +++ b/baker/SiteBaker.tsx @@ -104,6 +104,7 @@ import { } from "../db/model/Variable.js" import { Knex } from "knex" import { getBakePath } from "@ourworldindata/components" +import { GdocAuthor } from "../db/model/Gdoc/GdocAuthor.js" type PrefetchedAttachments = { linkedDocuments: Record @@ -137,6 +138,7 @@ const nonWordpressSteps = [ "gdriveImages", "dods", "dataInsights", + "authors", ] as const const otherSteps = ["removeDeletedPosts"] as const @@ -302,7 +304,7 @@ export class SiteBaker { picks?: [string[], string[], string[], string[]] ): Promise { if (!this._prefetchedAttachmentsCache) { - const publishedGdocs = await GdocPost.getPublishedGdocs().then( + const publishedGdocs = await GdocPost.getPublishedGdocPosts().then( (fullGdocs) => fullGdocs.map(fullGdocToMinimalGdoc) ) const publishedGdocsDictionary = keyBy(publishedGdocs, "id") @@ -441,7 +443,7 @@ export class SiteBaker { postSlugs.push(post.slug) } - const gdocPosts = await GdocPost.getPublishedGdocs() + const gdocPosts = await GdocPost.getPublishedGdocPosts() for (const post of gdocPosts) { postSlugs.push(post.slug) @@ -483,7 +485,7 @@ export class SiteBaker { async bakeGDocPosts(slugs?: string[]) { await db.getConnection() if (!this.bakeSteps.has("gdocPosts")) return - const publishedGdocs = await GdocPost.getPublishedGdocs() + const publishedGdocs = await GdocPost.getPublishedGdocPosts() const gdocsToBake = slugs !== undefined @@ -765,6 +767,63 @@ export class SiteBaker { await this.stageWrite(outPath, html) } } + + private async bakeAuthors() { + if (!this.bakeSteps.has("authors")) return + + const publishedAuthors = await GdocAuthor.getPublishedAuthors() + + for (const publishedAuthor of publishedAuthors) { + const attachments = await this.getPrefetchedGdocAttachments([ + publishedAuthor.linkedDocumentIds, + publishedAuthor.linkedImageFilenames, + publishedAuthor.linkedChartSlugs.grapher, + publishedAuthor.linkedChartSlugs.explorer, + ]) + + // We don't need these to be attached to the gdoc in the current + // state of author pages. We'll keep them here as documentation + // of intent, until we need them. + // publishedAuthor.linkedCharts = { + // ...attachments.linkedCharts.graphers, + // ...attachments.linkedCharts.explorers, + // } + + // Attach documents metadata linked to in the "featured work" section + publishedAuthor.linkedDocuments = attachments.linkedDocuments + + // Attach image metadata for the profile picture and the "featured work" images + publishedAuthor.imageMetadata = attachments.imageMetadata + + // Attach image metadata for the “latest work" images + await publishedAuthor.loadLatestWorkImages() + + await publishedAuthor.validate() + if ( + publishedAuthor.errors.filter( + (e) => e.type === OwidGdocErrorMessageType.Error + ).length + ) { + await logErrorAndMaybeSendToBugsnag( + `Error(s) baking "${ + publishedAuthor.slug + }" :\n ${publishedAuthor.errors + .map((error) => error.message) + .join("\n ")}` + ) + } + try { + await this.bakeOwidGdoc(publishedAuthor) + } catch (e) { + logErrorAndMaybeSendToBugsnag( + `Error baking author with id "${publishedAuthor.id}" and slug "${publishedAuthor.slug}": ${e}` + ) + } + } + + this.progressBar.tick({ name: "✅ baked author pages" }) + } + // Pages that are expected by google scholar for indexing private async bakeGoogleScholar() { if (!this.bakeSteps.has("googleScholar")) return @@ -927,6 +986,7 @@ export class SiteBaker { await this.validateGrapherDodReferences() await this.bakeGDocPosts() await this.bakeDataInsights() + await this.bakeAuthors() await this.bakeDriveImages() } diff --git a/baker/algolia/indexToAlgolia.tsx b/baker/algolia/indexToAlgolia.tsx index d2f9e55841f..a894d19450f 100644 --- a/baker/algolia/indexToAlgolia.tsx +++ b/baker/algolia/indexToAlgolia.tsx @@ -197,7 +197,7 @@ function generateGdocRecords( // Generate records for countries, WP posts (not including posts that have been succeeded by Gdocs equivalents), and Gdocs const getPagesRecords = async (knex: Knex) => { const pageviews = await getAnalyticsPageviewsByUrlObj(knex) - const gdocs = await GdocPost.getPublishedGdocs() + const gdocs = await GdocPost.getPublishedGdocPosts() const publishedGdocsBySlug = keyBy(gdocs, "slug") // TODO: the knex instance should be handed down as a parameter const slugsWithPublishedGdocsSuccessors = diff --git a/baker/sitemap.ts b/baker/sitemap.ts index db812fe39b6..9b0a36efead 100644 --- a/baker/sitemap.ts +++ b/baker/sitemap.ts @@ -70,7 +70,7 @@ export const makeSitemap = async ( undefined, (postrow) => !alreadyPublishedViaGdocsSlugsSet.has(postrow.slug) ) - const gdocPosts = await GdocPost.getPublishedGdocs() + const gdocPosts = await GdocPost.getPublishedGdocPosts() const publishedDataInsights = await db.getPublishedDataInsights(knex) const dataInsightFeedPageCount = calculateDataInsightIndexPageCount( diff --git a/db/model/Gdoc/GdocAuthor.ts b/db/model/Gdoc/GdocAuthor.ts index 6bfe7327f5e..eb22ec33395 100644 --- a/db/model/Gdoc/GdocAuthor.ts +++ b/db/model/Gdoc/GdocAuthor.ts @@ -1,4 +1,4 @@ -import { Entity, Column } from "typeorm" +import { Entity, Column, Raw } from "typeorm" import { OwidGdocErrorMessage, OwidGdocAuthorInterface, @@ -8,6 +8,7 @@ import { OwidGdocErrorMessageType, DbEnrichedLatestWork, DEFAULT_GDOC_FEATURED_IMAGE, + OwidGdocType, } from "@ourworldindata/utils" import { GdocBase } from "./GdocBase.js" import { htmlToEnrichedTextBlock } from "./htmlToEnriched.js" @@ -35,7 +36,11 @@ export class GdocAuthor extends GdocBase implements OwidGdocAuthorInterface { return blocks } - _loadSubclassAttachments = async (): Promise => { + _loadSubclassAttachments = (): Promise => { + return this.loadLatestWorkImages() + } + + loadLatestWorkImages = async (): Promise => { if (!this.content.title) return const knex = db.knexInstance() @@ -109,4 +114,16 @@ export class GdocAuthor extends GdocBase implements OwidGdocAuthorInterface { } return errors } + + static async getPublishedAuthors(): Promise { + return GdocAuthor.find({ + where: { + published: true, + content: Raw( + (content) => + `${content}->"$.type" = '${OwidGdocType.Author}'` + ), + }, + }) + } } diff --git a/db/model/Gdoc/GdocPost.ts b/db/model/Gdoc/GdocPost.ts index d48172bea96..2dcea9ce152 100644 --- a/db/model/Gdoc/GdocPost.ts +++ b/db/model/Gdoc/GdocPost.ts @@ -210,7 +210,7 @@ export class GdocPost extends GdocBase implements OwidGdocPostInterface { return parseDetails(gdoc.content.details) } - static async getPublishedGdocs(): Promise { + static async getPublishedGdocPosts(): Promise { // #gdocsvalidation this cast means that we trust the admin code and // workflow to provide published articles that have all the required content // fields (see #gdocsvalidationclient and pending #gdocsvalidationserver). @@ -246,7 +246,7 @@ export class GdocPost extends GdocBase implements OwidGdocPostInterface { /** * Excludes published listed Gdocs with a publication date in the future */ - static async getListedGdocs(): Promise { + static async getListedGdocPosts(): Promise { return GdocPost.findBy({ published: true, publicationContext: OwidGdocPublicationContext.listed, diff --git a/db/model/Post.ts b/db/model/Post.ts index 638205ba8cc..15ea71de098 100644 --- a/db/model/Post.ts +++ b/db/model/Post.ts @@ -243,7 +243,7 @@ const selectHomepagePosts: FilterFnPostRestApi = (post) => export const getBlogIndex = memoize( async (knex: Knex): Promise => { await db.getConnection() // side effect: ensure connection is established - const gdocPosts = await GdocPost.getListedGdocs() + const gdocPosts = await GdocPost.getListedGdocPosts() const wpPosts = await Promise.all( await getPostsFromSnapshots( knex,