diff --git a/db/model/Gdoc/GdocBase.ts b/db/model/Gdoc/GdocBase.ts
index 5e7a0fb7a33..e2f847524e5 100644
--- a/db/model/Gdoc/GdocBase.ts
+++ b/db/model/Gdoc/GdocBase.ts
@@ -312,6 +312,17 @@ export class GdocBase implements OwidGdocBaseInterface {
block: OwidEnrichedGdocBlock
): DbInsertPostGdocLink[] | void {
const links: DbInsertPostGdocLink[] = match(block)
+ .with({ type: "person" }, (block) => {
+ if (!block.url) return []
+ return [
+ createLinkFromUrl({
+ url: block.url,
+ source: this,
+ componentType: block.type,
+ text: block.name,
+ }),
+ ]
+ })
.with({ type: "prominent-link" }, (block) => [
createLinkFromUrl({
url: block.url,
@@ -559,7 +570,6 @@ export class GdocBase implements OwidGdocBaseInterface {
"numbered-list",
"people",
"people-rows",
- "person",
"pull-quote",
"sdg-grid",
"sdg-toc",
diff --git a/db/model/Gdoc/enrichedToRaw.ts b/db/model/Gdoc/enrichedToRaw.ts
index db819e092e4..5227f4263b8 100644
--- a/db/model/Gdoc/enrichedToRaw.ts
+++ b/db/model/Gdoc/enrichedToRaw.ts
@@ -228,7 +228,13 @@ export function enrichedBlockToRawBlock(
image: b.image,
name: b.name,
title: b.title,
+ url: b.url,
text: b.text.map(enrichedBlockToRawBlock) as RawBlockText[],
+ socials: b.socials?.map((social) => ({
+ type: social.type,
+ url: social.url,
+ text: social.text,
+ })),
},
})
)
diff --git a/db/model/Gdoc/exampleEnrichedBlocks.ts b/db/model/Gdoc/exampleEnrichedBlocks.ts
index f6189a6cc4b..10be34737f0 100644
--- a/db/model/Gdoc/exampleEnrichedBlocks.ts
+++ b/db/model/Gdoc/exampleEnrichedBlocks.ts
@@ -53,7 +53,36 @@ const enrichedBlockPerson: EnrichedBlockPerson = {
type: "person",
image: "example.png",
name: "Max Roser",
+ title: "Founder and Executive Co-Director",
+ url: "https://docs.google.com/document/d/1NfXOk8HVohVYjzJ1rtZYuw8h7kB9cWd5Kqxj4Dg1-WQ/edit",
text: [enrichedBlockText],
+ socials: [
+ {
+ type: SocialLinkType.X,
+ url: "https://x.com/MaxCRoser",
+ text: "@MaxCRoser",
+ parseErrors: [],
+ },
+ {
+ type: SocialLinkType.Mastodon,
+ url: "https://mas.to/@maxroser",
+ text: "@maxroser",
+ parseErrors: [],
+ },
+ {
+ type: SocialLinkType.Bluesky,
+ url: "https://bsky.app/profile/maxroser.bsky.social",
+ text: "@maxroser.bsky.social",
+ parseErrors: [],
+ },
+ {
+ type: SocialLinkType.Threads,
+ url: "https://www.threads.net/@max.roser.ox",
+ text: "@max.roser.ox",
+ parseErrors: [],
+ },
+ ],
+
parseErrors: [],
}
diff --git a/db/model/Gdoc/rawToArchie.ts b/db/model/Gdoc/rawToArchie.ts
index d151cb0f40e..c2c9b50803d 100644
--- a/db/model/Gdoc/rawToArchie.ts
+++ b/db/model/Gdoc/rawToArchie.ts
@@ -269,11 +269,21 @@ function* rawBlockPersonToArchieMLString(
yield* propertyToArchieMLString("image", block.value)
yield* propertyToArchieMLString("name", block.value)
yield* propertyToArchieMLString("title", block.value)
+ yield* propertyToArchieMLString("url", block.value)
yield "[.+text]"
for (const b of block.value.text) {
yield* OwidRawGdocBlockToArchieMLStringGenerator(b)
}
yield "[]"
+ if (block.value.socials?.length) {
+ yield "[.socials]"
+ for (const b of block.value.socials) {
+ yield* propertyToArchieMLString("type", b)
+ yield* propertyToArchieMLString("url", b)
+ yield* propertyToArchieMLString("text", b)
+ }
+ yield "[]"
+ }
yield "{}"
}
diff --git a/db/model/Gdoc/rawToEnriched.ts b/db/model/Gdoc/rawToEnriched.ts
index 5cb8230ea2a..4831c4c29c2 100644
--- a/db/model/Gdoc/rawToEnriched.ts
+++ b/db/model/Gdoc/rawToEnriched.ts
@@ -843,6 +843,7 @@ const parsePerson = (raw: RawBlockPerson): EnrichedBlockPerson => {
image: raw.value.image,
name: raw.value.name,
title: raw.value.title,
+ url: extractUrl(raw.value.url),
text: raw.value.text.map(parseText),
socials: raw.value.socials?.map(parseSocialLink),
parseErrors: [],
@@ -1394,6 +1395,7 @@ function parseCallout(raw: RawBlockCallout): EnrichedBlockCallout {
return {
type: "callout",
+ icon: raw.value.icon,
parseErrors: [],
text: excludeNullish(enrichedTextBlocks),
title: raw.value.title,
diff --git a/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts b/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts
index 90ea4c7c8ef..d1a89ff651d 100644
--- a/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts
+++ b/packages/@ourworldindata/types/src/gdocTypes/ArchieMlComponents.ts
@@ -284,6 +284,7 @@ export type RawBlockPerson = {
image?: string
name: string
title?: string
+ url?: string
text: RawBlockText[]
socials?: RawSocialLink[]
}
@@ -305,6 +306,7 @@ export type EnrichedBlockPerson = {
image?: string
name: string
title?: string
+ url?: string
text: EnrichedBlockText[]
socials?: EnrichedSocialLink[]
} & EnrichedBlockWithParseErrors
@@ -511,6 +513,7 @@ export type EnrichedBlockProminentLink = {
export type RawBlockCallout = {
type: "callout"
value: {
+ icon?: "info"
title?: string
text: (RawBlockText | RawBlockHeading | RawBlockList)[]
}
@@ -518,6 +521,7 @@ export type RawBlockCallout = {
export type EnrichedBlockCallout = {
type: "callout"
+ icon?: "info"
title?: string
text: (EnrichedBlockText | EnrichedBlockHeading | EnrichedBlockList)[]
} & EnrichedBlockWithParseErrors
diff --git a/packages/@ourworldindata/utils/src/index.ts b/packages/@ourworldindata/utils/src/index.ts
index 520058b256e..1a98410e6ce 100644
--- a/packages/@ourworldindata/utils/src/index.ts
+++ b/packages/@ourworldindata/utils/src/index.ts
@@ -294,7 +294,12 @@ export { Url, setWindowUrl, getWindowUrl } from "./urls/Url.js"
export { type UrlMigration, performUrlMigrations } from "./urls/UrlMigration.js"
-export { camelCaseProperties, titleCase, toAsciiQuotes } from "./string.js"
+export {
+ camelCaseProperties,
+ titleCase,
+ toAsciiQuotes,
+ removeDiacritics,
+} from "./string.js"
export { serializeJSONForHTML, deserializeJSONFromHTML } from "./serializers.js"
diff --git a/packages/@ourworldindata/utils/src/string.ts b/packages/@ourworldindata/utils/src/string.ts
index b6dae99eaee..056fe1e7c8e 100644
--- a/packages/@ourworldindata/utils/src/string.ts
+++ b/packages/@ourworldindata/utils/src/string.ts
@@ -35,3 +35,8 @@ export const titleCase = (str: string): string => {
export function toAsciiQuotes(str: string): string {
return str.replace(/[“”]/g, '"').replace(/[‘’]/g, "'")
}
+
+// https://stackoverflow.com/a/37511463/9846837
+export function removeDiacritics(str: string): string {
+ return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "")
+}
diff --git a/site/gdocs/components/ArticleBlock.tsx b/site/gdocs/components/ArticleBlock.tsx
index c644ec1c3cc..6f3b20da71a 100644
--- a/site/gdocs/components/ArticleBlock.tsx
+++ b/site/gdocs/components/ArticleBlock.tsx
@@ -1,6 +1,7 @@
import React from "react"
import cx from "classnames"
+import Callout from "./Callout.js"
import ChartStory from "./ChartStory.js"
import Scroller from "./Scroller.js"
import Chart from "./Chart.js"
@@ -261,14 +262,10 @@ export default function ArticleBlock({
/>
))
.with({ type: "callout" }, (block) => (
-
- {block.title ? (
-
{block.title}
- ) : null}
- {block.text.map((textBlock, i) => (
-
- ))}
-
+
))
.with({ type: "chart-story" }, (block) => (
+ {block.icon ? (
+
+ ) : null}
+ {block.title ? (
+ {block.title}
+ ) : null}
+
+
+ )
+}
diff --git a/site/gdocs/components/Donors.tsx b/site/gdocs/components/Donors.tsx
index 34970caffb3..f3d05db2475 100644
--- a/site/gdocs/components/Donors.tsx
+++ b/site/gdocs/components/Donors.tsx
@@ -1,12 +1,14 @@
import * as React from "react"
-import { groupBy } from "@ourworldindata/utils"
+import { groupBy, removeDiacritics } from "@ourworldindata/utils"
import { useDonors } from "../utils.js"
export default function Donors({ className }: { className?: string }) {
const donors = useDonors()
if (!donors) return null
- const donorsByLetter = groupBy(donors, (donor) => donor[0])
+ const donorsByLetter = groupBy(donors, (donor) =>
+ removeDiacritics(donor[0].toUpperCase())
+ )
return (
diff --git a/site/gdocs/components/People.scss b/site/gdocs/components/People.scss
index 2537255eea7..84dc616b66c 100644
--- a/site/gdocs/components/People.scss
+++ b/site/gdocs/components/People.scss
@@ -1,5 +1,9 @@
.people {
- row-gap: 16px;
+ .article-block__text,
+ .article-block__list,
+ .article-block__numbered-list {
+ font-size: 16px;
+ }
}
.people-cols-2 {
@@ -15,6 +19,10 @@
}
.people-cols-4 {
+ .person-heading {
+ font-size: 18px;
+ }
+
@include lg-up {
.article-block__text {
@include body-3-medium;
diff --git a/site/gdocs/components/Person.scss b/site/gdocs/components/Person.scss
index 6f49bc591d5..e5453d5c3cb 100644
--- a/site/gdocs/components/Person.scss
+++ b/site/gdocs/components/Person.scss
@@ -9,6 +9,12 @@
}
}
+@include sm-only {
+ .person + .person {
+ margin-top: 16px;
+ }
+}
+
.person-image-container {
display: flex;
flex: 0 0 193px;
@@ -21,6 +27,10 @@
}
.person-image {
+ picture {
+ display: flex; // Fix extra padding at the bottom.
+ }
+
img {
border-radius: 50%;
}
@@ -35,19 +45,31 @@
flex-direction: column;
gap: 2px;
margin-bottom: 8px;
+
+ a {
+ color: inherit;
+ }
}
.person-heading {
+ @include h2-bold;
margin: 0;
+
+ @include sm-only {
+ @include h3-bold;
+ margin: 0;
+ }
}
.person-title {
+ line-height: 19px;
color: $blue-60;
}
.person-socials {
margin-top: 16px;
margin-bottom: 16px;
+ font-size: 14px;
ul {
display: flex;
diff --git a/site/gdocs/components/Person.tsx b/site/gdocs/components/Person.tsx
index e3c9f93dbc9..c64750b2260 100644
--- a/site/gdocs/components/Person.tsx
+++ b/site/gdocs/components/Person.tsx
@@ -1,18 +1,31 @@
import * as React from "react"
+import { useMediaQuery } from "usehooks-ts"
-import { EnrichedBlockPerson } from "@ourworldindata/types"
+import { getCanonicalUrl } from "@ourworldindata/components"
+import { EnrichedBlockPerson, OwidGdocType } from "@ourworldindata/types"
+import { SMALL_BREAKPOINT_MEDIA_QUERY } from "../../SiteConstants.js"
+import { useLinkedDocument } from "../utils.js"
import { ArticleBlocks } from "./ArticleBlocks.js"
import Image from "./Image.js"
-import { useMediaQuery } from "usehooks-ts"
-import { SMALL_BREAKPOINT_MEDIA_QUERY } from "../../SiteConstants.js"
import { Socials } from "./Socials.js"
export default function Person({ person }: { person: EnrichedBlockPerson }) {
+ const { linkedDocument } = useLinkedDocument(person.url ?? "")
const isSmallScreen = useMediaQuery(SMALL_BREAKPOINT_MEDIA_QUERY)
+ const slug = linkedDocument?.slug
+ const url = slug
+ ? getCanonicalUrl("", {
+ slug,
+ content: { type: OwidGdocType.Author },
+ })
+ : undefined
+
+ const heading =
{person.name}
+
const header = (
-
{person.name}
+ {url ?
{heading} : heading}
{person.title && (
{person.title}
)}
diff --git a/site/gdocs/components/centered-article.scss b/site/gdocs/components/centered-article.scss
index d91a4cf958c..abcb1ce0f5c 100644
--- a/site/gdocs/components/centered-article.scss
+++ b/site/gdocs/components/centered-article.scss
@@ -464,14 +464,11 @@ h3.article-block__heading {
}
}
-.image--has-outline + .article-block__image-caption {
- margin-top: 8px;
-}
-
.article-block__image-caption {
@include body-3-medium-italic;
color: $blue-60;
text-align: center;
+ margin-top: 16px;
}
.article-block__chart + .article-block__heading {
@@ -1053,6 +1050,7 @@ div.raw-html-table__container {
// A small grey block
.article-block__callout {
h4 {
+ display: inline;
color: $blue-90;
margin-bottom: 8px;
margin-top: 8px;
@@ -1142,6 +1140,12 @@ div.raw-html-table__container {
}
}
+@include sm-only {
+ .article-block__text + .article-block__prominent-link {
+ margin-top: 8px;
+ }
+}
+
.article-block__key-insights {
.slide[data-active="true"] {
// Have to override the WP styles, which we can undo once they're removed
diff --git a/site/gdocs/pages/AboutPage.scss b/site/gdocs/pages/AboutPage.scss
index 7a32487fd0a..565e99fe9cf 100644
--- a/site/gdocs/pages/AboutPage.scss
+++ b/site/gdocs/pages/AboutPage.scss
@@ -54,10 +54,6 @@
@include h1-semibold;
color: $blue-60;
margin-top: 0;
-
- &:has(+ .article-block__side-by-side) {
- margin-bottom: 8px;
- }
}
h3.article-block__heading {
@@ -95,20 +91,31 @@
.article-block__callout {
margin-top: 0;
margin-bottom: 24px;
- }
- .article-block__chart {
- margin-bottom: 24px;
+ .article-block__text {
+ font-size: 14px;
+ }
}
.article-block__code-snippet {
margin-bottom: 16px;
}
+ .article-block__chart,
.article-block__image {
margin: 0 0 24px;
}
+ .article-block__image-download-button-container {
+ @include touch-device {
+ display: none;
+ }
+ }
+
+ .article-block__people + .article-block__heading {
+ margin-top: 40px;
+ }
+
.article-block__prominent-link {
margin-bottom: 24px;
}
@@ -122,6 +129,12 @@
}
}
+ .article-block__side-by-side {
+ :first-child {
+ margin-top: 0;
+ }
+ }
+
.article-block__donors {
margin-top: 32px;
padding-top: 40px;
diff --git a/site/gdocs/pages/AboutPage.tsx b/site/gdocs/pages/AboutPage.tsx
index 9b7c30336ef..f5cc3d44821 100644
--- a/site/gdocs/pages/AboutPage.tsx
+++ b/site/gdocs/pages/AboutPage.tsx
@@ -1,6 +1,7 @@
import cx from "classnames"
import { isEmpty } from "lodash"
import * as React from "react"
+import { useEffect, useRef } from "react"
import { OwidGdocAboutInterface } from "@ourworldindata/types"
import { ABOUT_LINKS } from "../../SiteAbout.js"
@@ -28,6 +29,25 @@ export default function AboutPage({ content, slug }: OwidGdocAboutInterface) {
}
function AboutNav({ slug }: { slug: string }) {
+ const activeLinkRef = useRef
(null)
+
+ // Scroll the nav to the active link, since it might not be visible
+ // on mobile.
+ useEffect(() => {
+ const activeLink = activeLinkRef.current
+ if (activeLink) {
+ const nav = activeLink.closest("nav")
+ if (nav) {
+ const activeLinkOffset = activeLink.offsetLeft
+ const navWidth = nav.offsetWidth
+ const activeLinkWidth = activeLink.offsetWidth
+ // Center the active link.
+ nav.scrollLeft =
+ activeLinkOffset - (navWidth - activeLinkWidth) / 2
+ }
+ }
+ }, [])
+
return (