diff --git a/adminSiteClient/gdocsDeploy.ts b/adminSiteClient/gdocsDeploy.ts index 990703d748a..5c21b68e21e 100644 --- a/adminSiteClient/gdocsDeploy.ts +++ b/adminSiteClient/gdocsDeploy.ts @@ -60,6 +60,7 @@ export const checkIsLightningUpdate = ( "cover-color": true, "cover-image": true, "hide-citation": true, + "sidebar-toc": true, body: true, dateline: true, details: true, diff --git a/baker/SiteBaker.tsx b/baker/SiteBaker.tsx index 5dcf3c4bd64..d73fa5fcddb 100644 --- a/baker/SiteBaker.tsx +++ b/baker/SiteBaker.tsx @@ -308,6 +308,7 @@ export class SiteBaker { // Bake all GDoc posts async bakeGDocPosts() { + await db.getConnection() if (!this.bakeSteps.has("gdocPosts")) return const publishedGdocs = await Gdoc.getPublishedGdocs() diff --git a/db/migrateWpPostsToArchieMl.ts b/db/migrateWpPostsToArchieMl.ts index 3527df446a3..d38a1d3b707 100644 --- a/db/migrateWpPostsToArchieMl.ts +++ b/db/migrateWpPostsToArchieMl.ts @@ -170,6 +170,7 @@ const migrate = async (): Promise => { // Provide an empty array to prevent the sticky nav from rendering at all // Because if it isn't defined, it tries to automatically populate itself "sticky-nav": isEntry ? [] : undefined, + "sidebar-toc": isEntry, }, relatedCharts, published: false, diff --git a/db/model/Gdoc/archieToEnriched.ts b/db/model/Gdoc/archieToEnriched.ts index e9cd4ce04c4..d52dc1d2079 100644 --- a/db/model/Gdoc/archieToEnriched.ts +++ b/db/model/Gdoc/archieToEnriched.ts @@ -121,31 +121,67 @@ function generateStickyNav( } function generateToc( - body: OwidEnrichedGdocBlock[] + body: OwidEnrichedGdocBlock[], + isTocForSidebar: boolean = false ): TocHeadingWithTitleSupertitle[] { + // For linear topic pages, we record h1s & h2s + // For the sdg-toc, we record h2s & h3s (as it was developed before we decided to use h1s as our top level heading) + // It would be nice to standardise this but it would require a migration, updating CSS, updating Gdocs, etc. + const [primary, secondary] = isTocForSidebar ? [1, 2] : [2, 3] const toc: TocHeadingWithTitleSupertitle[] = [] - // track h2s and h3s for the SDG table of contents body.forEach((block) => traverseEnrichedBlocks(block, (child) => { if (child.type === "heading") { const { level, text, supertitle } = child const titleString = spansToSimpleString(text) - const supertitleString = - supertitle && spansToSimpleString(supertitle) - if (titleString && (level === 2 || level === 3)) { + const supertitleString = supertitle + ? spansToSimpleString(supertitle) + : "" + if (titleString && (level === primary || level === secondary)) { toc.push({ title: titleString, supertitle: supertitleString, text: titleString, slug: urlSlug(`${supertitleString} ${titleString}`), - isSubheading: level === 3, + isSubheading: level === secondary, }) } } + if (isTocForSidebar && child.type === "all-charts") { + toc.push({ + title: child.heading, + text: child.heading, + slug: ALL_CHARTS_ID, + isSubheading: false, + }) + } }) ) + if (isTocForSidebar) { + toc.push( + { + title: "Endnotes", + text: "Endnotes", + slug: "article-endnotes", + isSubheading: false, + }, + { + title: "Citation", + text: "Citation", + slug: "article-citation", + isSubheading: false, + }, + { + title: "Licence", + text: "Licence", + slug: "article-licence", + isSubheading: false, + } + ) + } + return toc } @@ -255,7 +291,8 @@ export const archieToEnriched = (text: string): OwidGdocContent => { // Parse elements of the ArchieML into enrichedBlocks parsed.body = compact(parsed.body.map(parseRawBlocksToEnrichedBlocks)) - parsed.toc = generateToc(parsed.body) + const isTocForSidebar = parsed["sidebar-toc"] === "true" + parsed.toc = generateToc(parsed.body, isTocForSidebar) const parsedRefs = parseRefs({ refs: [...(parsed.refs ?? []), ...rawInlineRefs], diff --git a/db/model/Gdoc/archieToGdoc.ts b/db/model/Gdoc/archieToGdoc.ts index 8af069e48c5..cc61fe8e8c1 100644 --- a/db/model/Gdoc/archieToGdoc.ts +++ b/db/model/Gdoc/archieToGdoc.ts @@ -49,6 +49,7 @@ function* owidArticleToArchieMLStringGenerator( } yield "[]" } + yield* propertyToArchieMLString("sidebar-toc", article) // TODO: inline refs yieldMultiBlockPropertyIfDefined("summary", article, article.summary) yield* propertyToArchieMLString("hide-citation", article) diff --git a/db/model/Gdoc/rawToArchie.ts b/db/model/Gdoc/rawToArchie.ts index 0619718d1fc..651bd49db76 100644 --- a/db/model/Gdoc/rawToArchie.ts +++ b/db/model/Gdoc/rawToArchie.ts @@ -39,8 +39,9 @@ import { import { match } from "ts-pattern" export function appendDotEndIfMultiline( - line: string | null | undefined + line: string | boolean | null | undefined ): string { + if (typeof line === "boolean") return line ? "true" : "false" if (line && line.includes("\n")) return line + "\n:end" return line ?? "" } diff --git a/packages/@ourworldindata/utils/src/owidTypes.ts b/packages/@ourworldindata/utils/src/owidTypes.ts index ffc0dc249c1..32feba1f6cc 100644 --- a/packages/@ourworldindata/utils/src/owidTypes.ts +++ b/packages/@ourworldindata/utils/src/owidTypes.ts @@ -1414,6 +1414,7 @@ export interface OwidGdocContent { "featured-image"?: string "atom-title"?: string "atom-excerpt"?: string + "sidebar-toc"?: boolean "cover-color"?: | "sdg-color-1" | "sdg-color-2" diff --git a/site/TableOfContents.tsx b/site/TableOfContents.tsx index fc833735944..1b283bb4f83 100644 --- a/site/TableOfContents.tsx +++ b/site/TableOfContents.tsx @@ -12,6 +12,10 @@ interface TableOfContentsData { headings: TocHeading[] pageTitle: string hideSubheadings?: boolean + headingLevels?: { + primary: number + secondary: number + } } const isRecordTopViewport = (record: IntersectionObserverEntry) => { @@ -34,17 +38,36 @@ export const TableOfContents = ({ headings, pageTitle, hideSubheadings, + // Original WP articles used a hierarchy of h2 and h3 headings + // New Gdoc articles use a hierarchy of h1 and h2 headings + headingLevels = { + primary: 2, + secondary: 3, + }, }: TableOfContentsData) => { const [isOpen, setIsOpen] = useState(false) const [activeHeading, setActiveHeading] = useState("") + const { primary, secondary } = headingLevels const tocRef = useRef(null) const toggleIsOpen = () => { setIsOpen(!isOpen) } + // The Gdocs sidebar can't rely on the same CSS logic that old-style entries use, so we need to + // explicitly trigger these toggles based on screen width + const toggleIsOpenOnMobile = () => { + if (window.innerWidth < 1536) { + toggleIsOpen() + } + } useTriggerWhenClickOutside(tocRef, isOpen, setIsOpen) + // Open the sidebar on desktop by default when mounting + useEffect(() => { + setIsOpen(window.innerWidth >= 1536) + }, []) + useEffect(() => { if ("IntersectionObserver" in window) { const previousHeadings = headings.map((heading, i) => ({ @@ -107,16 +130,26 @@ export const TableOfContents = ({ ) let contentHeadings = null + // In Gdocs articles, these sections are ID'd via unique elements + const appendixDivs = + ", h3#article-endnotes, section#article-citation, section#article-licence" if (hideSubheadings) { - contentHeadings = document.querySelectorAll("h2") + contentHeadings = document.querySelectorAll( + `h${secondary} ${appendixDivs}` + ) } else { - contentHeadings = document.querySelectorAll("h2, h3") + contentHeadings = document.querySelectorAll( + `h${primary}, h${secondary} ${appendixDivs}` + ) } contentHeadings.forEach((contentHeading) => { observer.observe(contentHeading) }) + + return () => observer.disconnect() } - }, [headings, hideSubheadings]) + return + }, [headings, hideSubheadings, primary, secondary]) return (
@@ -131,7 +164,7 @@ export const TableOfContents = ({
  • { - toggleIsOpen() + toggleIsOpenOnMobile() setActiveHeading("") }} href="#" @@ -159,7 +192,7 @@ export const TableOfContents = ({ } > diff --git a/site/gdocs/OwidGdoc.tsx b/site/gdocs/OwidGdoc.tsx index 8687373f8ad..8e60cd2489e 100644 --- a/site/gdocs/OwidGdoc.tsx +++ b/site/gdocs/OwidGdoc.tsx @@ -21,6 +21,7 @@ import { DebugProvider } from "./DebugContext.js" import { OwidGdocHeader } from "./OwidGdocHeader.js" import StickyNav from "../blocks/StickyNav.js" import { getShortPageCitation } from "./utils.js" +import { TableOfContents } from "../TableOfContents.js" export const AttachmentsContext = createContext<{ linkedCharts: Record linkedDocuments: Record @@ -70,6 +71,7 @@ export function OwidGdoc({ publishedAt ) const citationText = `${shortPageCitation} Published online at OurWorldInData.org. Retrieved from: '${`${BAKED_BASE_URL}/${slug}`}' [Online Resource]` + const hasSidebarToc = content["sidebar-toc"] const bibtex = `@article{owid-${slug.replace(/\//g, "-")}, author = {${formatAuthors({ @@ -119,6 +121,13 @@ export function OwidGdoc({ publishedAt={publishedAt} breadcrumbs={breadcrumbs ?? undefined} /> + {hasSidebarToc && content.toc ? ( + + ) : null} {content.type === "topic-page" && stickyNavLinks?.length ? (