From bdfb2f8fefa14c812e5c622c5c8ba66cc658639d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ra=C4=8D=C3=A1k?= Date: Wed, 26 Feb 2025 15:38:27 +0100 Subject: [PATCH] Add catalogPath to mdims (#4576) * Catalog path will now be the main ID used by ETL * It's temporarily allowed to be null; we'll disallow that once existing data is migrated * Slug is now allowed to be null, since it will be only set in admin at some point * For that reason we'll use the catalog path as a slug for previews of mdims in the admin, i.e. `/admin/grapher/{catalogPath}` --- adminSiteClient/MultiDimIndexPage.tsx | 57 ++++++++++----- adminSiteServer/adminRouter.tsx | 14 ++-- adminSiteServer/apiRouter.ts | 6 +- adminSiteServer/apiRoutes/mdims.ts | 53 ++++++++------ adminSiteServer/app.test.ts | 8 +-- adminSiteServer/multiDim.ts | 72 ++++++++++++------- adminSiteServer/validation.ts | 8 ++- baker/MultiDimBaker.tsx | 30 ++++++-- .../1738919205864-AddCatalogPathToMultiDim.ts | 24 +++++++ db/model/MultiDimDataPage.ts | 34 +++++---- .../types/src/dbTypes/MultiDimDataPages.ts | 3 +- .../types/src/siteTypes/MultiDimDataPage.ts | 2 +- site/multiDim/MultiDimDataPage.tsx | 5 +- site/multiDim/MultiDimDataPageContent.tsx | 2 +- 14 files changed, 220 insertions(+), 98 deletions(-) create mode 100644 db/migration/1738919205864-AddCatalogPathToMultiDim.ts diff --git a/adminSiteClient/MultiDimIndexPage.tsx b/adminSiteClient/MultiDimIndexPage.tsx index 028f33f382..3cf7d8645d 100644 --- a/adminSiteClient/MultiDimIndexPage.tsx +++ b/adminSiteClient/MultiDimIndexPage.tsx @@ -19,7 +19,7 @@ import { AdminLayout } from "./AdminLayout.js" import { AdminAppContext } from "./AdminAppContext.js" import urljoin from "url-join" import { - BAKED_BASE_URL, + ADMIN_BASE_URL, BAKED_GRAPHER_URL, GRAPHER_DYNAMIC_THUMBNAIL_URL, } from "../settings/clientSettings.js" @@ -28,6 +28,7 @@ import { Link } from "./Link.js" type ApiMultiDim = { id: number + catalogPath: string | null title: string slug: string updatedAt: string @@ -86,21 +87,31 @@ function createColumns( render: (_, record) => { return ( - Preview + {record.slug ? ( + Preview + ) : ( + "Preview" + )} ) }, @@ -109,7 +120,6 @@ function createColumns( title: "Title", dataIndex: "title", key: "title", - width: 280, render: (text, record) => record.published ? ( a.title.localeCompare(b.title), }, + { + title: "Catalog path", + dataIndex: "catalogPath", + key: "catalogPath", + render: (catalogPath) => + catalogPath && ( + {catalogPath} + ), + sorter: (a, b) => { + if (a.catalogPath === null) return 1 + if (b.catalogPath === null) return -1 + return a.catalogPath.localeCompare(b.catalogPath) + }, + }, { title: "Slug", dataIndex: "slug", @@ -164,8 +188,9 @@ function createColumns( diff --git a/adminSiteServer/adminRouter.tsx b/adminSiteServer/adminRouter.tsx index 7d7a52f41b..b68af50c50 100644 --- a/adminSiteServer/adminRouter.tsx +++ b/adminSiteServer/adminRouter.tsx @@ -44,7 +44,10 @@ import { getChartConfigBySlug } from "../db/model/Chart.js" import { getVariableMetadata } from "../db/model/Variable.js" import { DbPlainDatasetFile, DbPlainDataset } from "@ourworldindata/types" import { getPlainRouteWithROTransaction } from "./plainRouterHelpers.js" -import { getMultiDimDataPageBySlug } from "../db/model/MultiDimDataPage.js" +import { + getMultiDimDataPageByCatalogPath, + getMultiDimDataPageBySlug, +} from "../db/model/MultiDimDataPage.js" import { renderMultiDimDataPageFromConfig } from "../baker/MultiDimBaker.js" // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -356,13 +359,14 @@ getPlainRouteWithROTransaction( return } - const mdd = await getMultiDimDataPageBySlug(trx, slug, { - onlyPublished: false, - }) + const mdd = + (await getMultiDimDataPageBySlug(trx, slug, { + onlyPublished: false, + })) ?? (await getMultiDimDataPageByCatalogPath(trx, slug)) if (mdd) { const renderedPage = await renderMultiDimDataPageFromConfig({ knex: trx, - slug, + slug: mdd.slug, config: mdd.config, isPreviewing: true, }) diff --git a/adminSiteServer/apiRouter.ts b/adminSiteServer/apiRouter.ts index 3bb0d1dd9b..7731a71f1b 100644 --- a/adminSiteServer/apiRouter.ts +++ b/adminSiteServer/apiRouter.ts @@ -291,7 +291,11 @@ getRouteWithROTransaction(apiRouter, "/images/usage", getImageUsageHandler) // Mdim routes getRouteWithROTransaction(apiRouter, "/multi-dims.json", handleGetMultiDims) -putRouteWithRWTransaction(apiRouter, "/multi-dim/:slug", handlePutMultiDim) +putRouteWithRWTransaction( + apiRouter, + "/multi-dims/:catalogPath", + handlePutMultiDim +) patchRouteWithRWTransaction(apiRouter, "/multi-dims/:id", handlePatchMultiDim) // Misc routes diff --git a/adminSiteServer/apiRoutes/mdims.ts b/adminSiteServer/apiRoutes/mdims.ts index 93ff81ae10..b5697777df 100644 --- a/adminSiteServer/apiRoutes/mdims.ts +++ b/adminSiteServer/apiRoutes/mdims.ts @@ -1,22 +1,24 @@ import { + DbPlainMultiDimDataPage, JsonError, MultiDimDataPageConfigRaw, MultiDimDataPagesTableName, } from "@ourworldindata/types" +import { getMultiDimDataPageById } from "../../db/model/MultiDimDataPage.js" +import { expectInt } from "../../serverUtils/serverUtil.js" import { - getMultiDimDataPageById, - multiDimDataPageExists, -} from "../../db/model/MultiDimDataPage.js" -import { expectInt, isValidSlug } from "../../serverUtils/serverUtil.js" -import { - upsertMultiDimConfig, + upsertMultiDim, setMultiDimPublished, setMultiDimSlug, } from "../multiDim.js" import { triggerStaticBuild } from "./routeUtils.js" import { Request } from "../authentication.js" import * as db from "../../db/db.js" -import { validateNewGrapherSlug, validateMultiDimSlug } from "../validation.js" +import { + validateNewGrapherSlug, + validateMultiDimSlug, + isValidCatalogPath, +} from "../validation.js" import e from "express" export async function handleGetMultiDims( @@ -25,17 +27,17 @@ export async function handleGetMultiDims( trx: db.KnexReadonlyTransaction ) { try { - const results = await db.knexRaw<{ - id: number - slug: string - title: string - updatedAt: string - published: number - }>( + const results = await db.knexRaw< + Pick< + DbPlainMultiDimDataPage, + "id" | "catalogPath" | "slug" | "updatedAt" | "published" + > & { title: string } + >( trx, `-- sql SELECT id, + catalogPath, slug, config->>'$.title.title' as title, updatedAt, @@ -57,20 +59,25 @@ export async function handlePutMultiDim( res: e.Response>, trx: db.KnexReadWriteTransaction ) { - const { slug } = req.params - if (!isValidSlug(slug)) { - throw new JsonError(`Invalid multi-dim slug ${slug}`) + const { catalogPath } = req.params + if (!isValidCatalogPath(catalogPath)) { + throw new JsonError(`Invalid multi-dim catalog path ${catalogPath}`) } - if (!(await multiDimDataPageExists(trx, { slug }))) { - await validateNewGrapherSlug(trx, slug) + const { config: rawConfig } = req.body as { + config: MultiDimDataPageConfigRaw } - const rawConfig = req.body as MultiDimDataPageConfigRaw - const id = await upsertMultiDimConfig(trx, slug, rawConfig) + const id = await upsertMultiDim(trx, catalogPath, rawConfig) - if (await multiDimDataPageExists(trx, { slug, published: true })) { + const { slug: publishedSlug } = + (await trx(MultiDimDataPagesTableName) + .select("slug") + .where("catalogPath", catalogPath) + .where("published", true) + .first()) ?? {} + if (publishedSlug) { await triggerStaticBuild( res.locals.user, - `Publishing multidimensional chart ${slug}` + `Publishing multidimensional chart ${publishedSlug}` ) } return { success: true, id } diff --git a/adminSiteServer/app.test.ts b/adminSiteServer/app.test.ts index db594d2878..a6b690bce9 100644 --- a/adminSiteServer/app.test.ts +++ b/adminSiteServer/app.test.ts @@ -492,11 +492,12 @@ describe("OwidAdminApp: indicator-level chart configs", () => { // create mdim config that uses both of the variables await makeRequestAgainstAdminApi({ method: "PUT", - path: "/multi-dim/energy", - body: JSON.stringify(testMultiDimConfig), + path: "/multi-dims/test%2Fcatalog%23path", + body: JSON.stringify({ config: testMultiDimConfig }), }) const mdim = await testKnexInstance!(MultiDimDataPagesTableName).first() - expect(mdim.slug).toBe("energy") + expect(mdim.catalogPath).toBe("test/catalog#path") + expect(mdim.slug).toBe(null) const savedMdimConfig = JSON.parse(mdim.config) // variableId should be normalized to an array expect(savedMdimConfig.views[0].indicators.y).toBeInstanceOf(Array) @@ -516,7 +517,6 @@ describe("OwidAdminApp: indicator-level chart configs", () => { ...mergedGrapherConfig, title: "Total energy use", selectedEntityNames: [], // mdims define their own default entities - slug: "energy", } const fullViewConfig1 = await testKnexInstance!(ChartConfigsTableName) .where("id", mdxcc1.chartConfigId) diff --git a/adminSiteServer/multiDim.ts b/adminSiteServer/multiDim.ts index 8389ec21c7..76d927935a 100644 --- a/adminSiteServer/multiDim.ts +++ b/adminSiteServer/multiDim.ts @@ -5,7 +5,6 @@ import { migrateGrapherConfigToLatestVersion, } from "@ourworldindata/grapher" import { - Base64String, ChartConfigsTableName, DbEnrichedMultiDimDataPage, DbPlainMultiDimDataPage, @@ -23,6 +22,7 @@ import { MultiDimDimensionChoices, MultiDimXChartConfigsTableName, parseChartConfigsRow, + R2GrapherConfigDirectory, View, } from "@ourworldindata/types" import { @@ -38,6 +38,7 @@ import { getVariableIdsByCatalogPath, } from "../db/model/Variable.js" import { + deleteGrapherConfigFromR2, deleteGrapherConfigFromR2ByUUID, saveMultiDimConfigToR2, } from "./chartConfigR2Helpers.js" @@ -166,7 +167,7 @@ async function resolveMultiDimDataPageCatalogPathsToIndicatorIds( async function getViewIdToChartConfigIdMap( knex: db.KnexReadonlyTransaction, - slug: string + catalogPath: string ) { const rows = await db.knexRaw( knex, @@ -174,45 +175,54 @@ async function getViewIdToChartConfigIdMap( SELECT viewId, chartConfigId FROM multi_dim_x_chart_configs mdxcc JOIN multi_dim_data_pages mddp ON mddp.id = mdxcc.multiDimId - WHERE mddp.slug = ?`, - [slug] + WHERE mddp.catalogPath = ?`, + [catalogPath] ) - return new Map( - rows.map((row) => [row.viewId, row.chartConfigId as Base64String]) + return new Map(rows.map((row) => [row.viewId, row.chartConfigId])) +} + +async function retrieveMultiDimConfigFromDbAndSaveToR2( + knex: db.KnexReadonlyTransaction, + id: number +) { + // We need to get the full config and the md5 hash from the database instead of + // computing our own md5 hash because MySQL normalizes JSON and our + // client computed md5 would be different from the ones computed by and stored in R2 + const result = await knex( + MultiDimDataPagesTableName ) + .select("slug", "config", "configMd5") + .where({ id }) + .first() + const { slug, config: normalizedConfig, configMd5 } = result! + if (slug) { + await saveMultiDimConfigToR2(normalizedConfig, slug, configMd5) + } } -async function saveMultiDimConfig( +async function upsertMultiDimConfig( knex: db.KnexReadWriteTransaction, - slug: string, + catalogPath: string, config: MultiDimDataPageConfigEnriched ) { const id = await upsertMultiDimDataPage(knex, { - slug, + catalogPath, config: JSON.stringify(config), }) if (id === 0) { // There are no updates to the config, return the existing id. - console.debug(`There are no changes to multi dim config slug=${slug}`) + console.debug( + `There are no changes to multi dim config catalogPath=${catalogPath}` + ) const result = await knex( MultiDimDataPagesTableName ) .select("id") - .where({ slug }) + .where({ catalogPath }) .first() return result!.id } - // We need to get the full config and the md5 hash from the database instead of - // computing our own md5 hash because MySQL normalizes JSON and our - // client computed md5 would be different from the ones computed by and stored in R2 - const result = await knex( - MultiDimDataPagesTableName - ) - .select("config", "configMd5") - .where({ id }) - .first() - const { config: normalizedConfig, configMd5 } = result! - await saveMultiDimConfigToR2(normalizedConfig, slug, configMd5) + await retrieveMultiDimConfigFromDbAndSaveToR2(knex, id) return id } @@ -231,9 +241,9 @@ async function cleanUpOrphanedChartConfigs( } } -export async function upsertMultiDimConfig( +export async function upsertMultiDim( knex: db.KnexReadWriteTransaction, - slug: string, + catalogPath: string, rawConfig: MultiDimDataPageConfigRaw ): Promise { const config = await resolveMultiDimDataPageCatalogPathsToIndicatorIds( @@ -246,7 +256,7 @@ export async function upsertMultiDimConfig( ) const existingViewIdsToChartConfigIds = await getViewIdToChartConfigIdMap( knex, - slug + catalogPath ) const reusedChartConfigIds = new Set() const { grapherConfigSchema } = config @@ -259,7 +269,6 @@ export async function upsertMultiDimConfig( $schema: defaultGrapherConfig.$schema, dimensions: MultiDimDataPageConfig.viewToDimensionsConfig(view), selectedEntityNames: config.defaultSelection ?? [], - slug, } let viewGrapherConfig = {} if (view.config) { @@ -313,7 +322,11 @@ export async function upsertMultiDimConfig( await cleanUpOrphanedChartConfigs(knex, orphanedChartConfigIds) const enrichedConfig = { ...config, views: enrichedViews } - const multiDimId = await saveMultiDimConfig(knex, slug, enrichedConfig) + const multiDimId = await upsertMultiDimConfig( + knex, + catalogPath, + enrichedConfig + ) for (const view of enrichedConfig.views) { await upsertMultiDimXChartConfigs(knex, { multiDimId, @@ -376,5 +389,10 @@ export async function setMultiDimSlug( await knex(MultiDimDataPagesTableName) .where({ id: multiDim.id }) .update({ slug }) + await deleteGrapherConfigFromR2( + R2GrapherConfigDirectory.multiDim, + `${multiDim.slug}.json` + ) + await retrieveMultiDimConfigFromDbAndSaveToR2(knex, multiDim.id) return { ...multiDim, slug } } diff --git a/adminSiteServer/validation.ts b/adminSiteServer/validation.ts index 9bfc3c9636..3a0b07f769 100644 --- a/adminSiteServer/validation.ts +++ b/adminSiteServer/validation.ts @@ -7,6 +7,8 @@ import * as db from "../db/db.js" import { multiDimDataPageExists } from "../db/model/MultiDimDataPage.js" import { isValidSlug } from "../serverUtils/serverUtil.js" +const CATALOG_PATH_PATTERN = /^[\w\d_/-]+#[\w\d_/-]+$/ + async function isSlugUsedInRedirect( knex: db.KnexReadonlyTransaction, slug: string, @@ -77,7 +79,7 @@ export async function validateNewGrapherSlug( */ export async function validateMultiDimSlug( knex: db.KnexReadonlyTransaction, - slug: string, + slug: string | null, existingConfigId?: number ) { if (!isValidSlug(slug)) { @@ -95,3 +97,7 @@ export async function validateMultiDimSlug( } return slug } + +export function isValidCatalogPath(catalogPath: string) { + return CATALOG_PATH_PATTERN.test(catalogPath) +} diff --git a/baker/MultiDimBaker.tsx b/baker/MultiDimBaker.tsx index 130c6201a3..d91a212281 100644 --- a/baker/MultiDimBaker.tsx +++ b/baker/MultiDimBaker.tsx @@ -35,7 +35,8 @@ import { import { logErrorAndMaybeCaptureInSentry } from "../serverUtils/errorLog.js" import { getAllPublishedChartSlugs } from "../db/model/Chart.js" import { - getAllMultiDimDataPages, + getAllPublishedMultiDimDataPages, + getMultiDimDataPageByCatalogPath, getMultiDimDataPageBySlug, } from "../db/model/MultiDimDataPage.js" @@ -125,7 +126,7 @@ export async function renderMultiDimDataPageFromConfig({ isPreviewing = false, }: { knex: db.KnexReadonlyTransaction - slug: string + slug: string | null config: MultiDimDataPageConfigEnriched imageMetadataDictionary?: Record isPreviewing?: boolean @@ -142,7 +143,11 @@ export async function renderMultiDimDataPageFromConfig({ const faqEntries = await getFaqEntries(knex, config, variableMetaDict) // PRIMARY TOPIC - const primaryTopic = await getPrimaryTopic(knex, config.topicTags, slug) + const primaryTopic = await getPrimaryTopic( + knex, + config.topicTags, + slug ?? undefined + ) // Related research const relatedResearchCandidates = @@ -198,6 +203,23 @@ export const renderMultiDimDataPageBySlug = async ( }) } +export async function renderMultiDimDataPageByCatalogPath( + knex: db.KnexReadonlyTransaction, + catalogPath: string +) { + const dbRow = await getMultiDimDataPageByCatalogPath(knex, catalogPath) + if (!dbRow) + throw new Error( + `No multi-dim site found for catalog path: ${catalogPath}` + ) + + return renderMultiDimDataPageFromConfig({ + knex, + slug: dbRow.slug, + config: dbRow.config, + }) +} + export const renderMultiDimDataPageFromProps = async ( props: MultiDimDataPageProps ) => { @@ -226,7 +248,7 @@ export const bakeAllMultiDimDataPages = async ( bakedSiteDir: string, imageMetadata: Record ) => { - const multiDimsBySlug = await getAllMultiDimDataPages(knex) + const multiDimsBySlug = await getAllPublishedMultiDimDataPages(knex) const progressBar = new ProgressBar( "bake multi-dim page [:bar] :current/:total :elapseds :rate/s :name\n", { diff --git a/db/migration/1738919205864-AddCatalogPathToMultiDim.ts b/db/migration/1738919205864-AddCatalogPathToMultiDim.ts new file mode 100644 index 0000000000..7a6ab3e57b --- /dev/null +++ b/db/migration/1738919205864-AddCatalogPathToMultiDim.ts @@ -0,0 +1,24 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class AddCatalogPathToMultiDim1738919205864 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `-- sql + ALTER TABLE multi_dim_data_pages + MODIFY COLUMN slug VARCHAR(255) NULL, + ADD COLUMN catalogPath VARCHAR(767) NULL AFTER id, + ADD UNIQUE INDEX idx_multi_dim_data_pages_catalog_path (catalogPath)` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `-- sql + ALTER TABLE multi_dim_data_pages + MODIFY COLUMN slug VARCHAR(255) NOT NULL, + DROP COLUMN catalogPath` + ) + } +} diff --git a/db/model/MultiDimDataPage.ts b/db/model/MultiDimDataPage.ts index b740ea88c2..015fe35206 100644 --- a/db/model/MultiDimDataPage.ts +++ b/db/model/MultiDimDataPage.ts @@ -52,24 +52,22 @@ const enrichRow = ( config: JSON.parse(row.config), }) -export const getAllMultiDimDataPages = async ( - knex: KnexReadonlyTransaction, - { onlyPublished = true }: { onlyPublished?: boolean } = {} +export const getAllPublishedMultiDimDataPages = async ( + knex: KnexReadonlyTransaction ): Promise> => { const rows = await knex( MultiDimDataPagesTableName - ).where({ - ...createOnlyPublishedFilter(onlyPublished), - }) + ).where("published", true) - return new Map(rows.map((row) => [row.slug, enrichRow(row)])) + // Published mdims must have a slug. + return new Map(rows.map((row) => [row.slug!, enrichRow(row)])) } export async function getAllLinkedPublishedMultiDimDataPages( knex: KnexReadonlyTransaction -): Promise[]> { +): Promise<{ slug: string; config: MultiDimDataPageConfigEnriched }[]> { const rows = await knexRaw< - Pick + Pick & { slug: string } >( knex, `-- sql @@ -87,10 +85,10 @@ export async function getAllLinkedPublishedMultiDimDataPages( export async function getAllMultiDimDataPageSlugs( knex: KnexReadonlyTransaction ): Promise { - const rows = await knex( - MultiDimDataPagesTableName - ).select("slug") - return rows.map((row) => row.slug) + const rows = await knex(MultiDimDataPagesTableName) + .select("slug") + .whereNotNull("slug") + return rows.map((row) => row.slug!) } export const getMultiDimDataPageBySlug = async ( @@ -114,3 +112,13 @@ export async function getMultiDimDataPageById( .first() return row ? enrichRow(row) : undefined } + +export async function getMultiDimDataPageByCatalogPath( + knex: KnexReadonlyTransaction, + catalogPath: string +): Promise { + const row = await knex(MultiDimDataPagesTableName) + .where({ catalogPath }) + .first() + return row ? enrichRow(row) : undefined +} diff --git a/packages/@ourworldindata/types/src/dbTypes/MultiDimDataPages.ts b/packages/@ourworldindata/types/src/dbTypes/MultiDimDataPages.ts index 731e8c93ab..2275ae5f06 100644 --- a/packages/@ourworldindata/types/src/dbTypes/MultiDimDataPages.ts +++ b/packages/@ourworldindata/types/src/dbTypes/MultiDimDataPages.ts @@ -3,7 +3,8 @@ import { MultiDimDataPageConfigEnriched } from "../siteTypes/MultiDimDataPage.js export const MultiDimDataPagesTableName = "multi_dim_data_pages" export interface DbInsertMultiDimDataPage { - slug: string + catalogPath?: string + slug?: string | null config: JsonString published?: boolean createdAt?: Date diff --git a/packages/@ourworldindata/types/src/siteTypes/MultiDimDataPage.ts b/packages/@ourworldindata/types/src/siteTypes/MultiDimDataPage.ts index 14729086e9..e2e371db09 100644 --- a/packages/@ourworldindata/types/src/siteTypes/MultiDimDataPage.ts +++ b/packages/@ourworldindata/types/src/siteTypes/MultiDimDataPage.ts @@ -111,7 +111,7 @@ export type FaqEntryKeyedByGdocIdAndFragmentId = { export interface MultiDimDataPageProps { baseUrl: string baseGrapherUrl: string - slug: string + slug: string | null configObj: MultiDimDataPageConfigEnriched tagToSlugMap?: Record faqEntries?: FaqEntryKeyedByGdocIdAndFragmentId diff --git a/site/multiDim/MultiDimDataPage.tsx b/site/multiDim/MultiDimDataPage.tsx index e3a2cc6f26..b9c3f3ec55 100644 --- a/site/multiDim/MultiDimDataPage.tsx +++ b/site/multiDim/MultiDimDataPage.tsx @@ -21,7 +21,10 @@ export function MultiDimDataPage({ imageMetadata, isPreviewing, }: MultiDimDataPageProps) { - const canonicalUrl = `${baseGrapherUrl}/${slug}` + if (!slug && !isPreviewing) { + throw new Error("Missing slug for multidimensional data page") + } + const canonicalUrl = slug ? `${baseGrapherUrl}/${slug}` : "" const contentProps: MultiDimDataPageContentProps = { canonicalUrl, slug, diff --git a/site/multiDim/MultiDimDataPageContent.tsx b/site/multiDim/MultiDimDataPageContent.tsx index 57eb7d1e3e..aedd9ca3fd 100644 --- a/site/multiDim/MultiDimDataPageContent.tsx +++ b/site/multiDim/MultiDimDataPageContent.tsx @@ -211,7 +211,7 @@ const analytics = new GrapherAnalytics() export type MultiDimDataPageContentProps = { canonicalUrl: string - slug: string + slug: string | null configObj: MultiDimDataPageConfigEnriched tagToSlugMap?: Record faqEntries?: FaqEntryKeyedByGdocIdAndFragmentId