diff --git a/adminSiteServer/mockSiteRouter.tsx b/adminSiteServer/mockSiteRouter.tsx index c593d5e4249..8cb903a6c50 100644 --- a/adminSiteServer/mockSiteRouter.tsx +++ b/adminSiteServer/mockSiteRouter.tsx @@ -20,6 +20,7 @@ import { renderThankYouPage, makeDataInsightsAtomFeed, renderGdocTombstone, + renderExplorerIndexPage, } from "../baker/siteRenderers.js" import { BAKED_BASE_URL, @@ -164,6 +165,14 @@ mockSiteRouter.get("/assets/embedCharts.js", async (req, res) => { const explorerAdminServer = new ExplorerAdminServer(GIT_CMS_DIR) +getPlainRouteWithROTransaction( + mockSiteRouter, + `/${EXPLORERS_ROUTE_FOLDER}`, + async (_, res, trx) => { + return res.send(await renderExplorerIndexPage(trx)) + } +) + getPlainRouteWithROTransaction( mockSiteRouter, `/${EXPLORERS_ROUTE_FOLDER}/:slug`, diff --git a/baker/SiteBaker.tsx b/baker/SiteBaker.tsx index a66b1c91115..894f85fc9ed 100644 --- a/baker/SiteBaker.tsx +++ b/baker/SiteBaker.tsx @@ -31,6 +31,7 @@ import { renderThankYouPage, makeDataInsightsAtomFeed, renderGdocTombstone, + renderExplorerIndexPage, } from "../baker/siteRenderers.js" import { bakeGrapherUrls, @@ -704,6 +705,10 @@ export class SiteBaker { `${this.bakedSiteDir}/search.html`, await renderSearchPage() ) + await this.stageWrite( + `${this.bakedSiteDir}/explorers.html`, + await renderExplorerIndexPage(knex) + ) await this.stageWrite( `${this.bakedSiteDir}/collection/custom.html`, await renderDynamicCollectionPage() diff --git a/baker/siteRenderers.tsx b/baker/siteRenderers.tsx index 201eea72a36..a6c46848bfb 100644 --- a/baker/siteRenderers.tsx +++ b/baker/siteRenderers.tsx @@ -6,6 +6,7 @@ import { StaticCollectionPage } from "../site/collections/StaticCollectionPage.j import { SearchPage } from "../site/search/SearchPage.js" import NotFoundPage from "../site/NotFoundPage.js" import { DonatePage } from "../site/DonatePage.js" +import { ExplorerIndexPage } from "../site/ExplorerIndexPage.js" import { ThankYouPage } from "../site/ThankYouPage.js" import TombstonePage from "../site/TombstonePage.js" import OwidGdocPage from "../site/gdocs/OwidGdocPage.js" @@ -64,6 +65,7 @@ import { KnexReadonlyTransaction, getHomepageId, getFlatTagGraph, + getPublishedExplorersBySlug, } from "../db/db.js" import { getPageOverrides, isPageOverridesCitable } from "./pageOverrides.js" import { ProminentLink } from "../site/blocks/ProminentLink.js" @@ -677,6 +679,19 @@ export const renderReusableBlock = async ( return cheerioEl("body").html() ?? undefined } +export const renderExplorerIndexPage = async ( + knex: KnexReadonlyTransaction +): Promise => { + const explorersBySlug = await getPublishedExplorersBySlug(knex) + const explorers = Object.values(explorersBySlug).sort((a, b) => + a.title.localeCompare(b.title) + ) + + return renderToHtmlPage( + + ) +} + interface ExplorerRenderOpts { urlMigrationSpec?: ExplorerPageUrlMigrationSpec isPreviewing?: boolean diff --git a/baker/sitemap.ts b/baker/sitemap.ts index 48a2518925f..b4fc385da29 100644 --- a/baker/sitemap.ts +++ b/baker/sitemap.ts @@ -98,6 +98,8 @@ export const makeSitemap = async ( ` ) + const STATIC_PAGES = ["/explorers", "/data", "/search", "/donate"] + const explorers = await explorerAdminServer.getAllPublishedExplorers() let urls = countries.map((c) => ({ @@ -105,6 +107,11 @@ export const makeSitemap = async ( })) as SitemapUrl[] urls = urls + .concat( + ...STATIC_PAGES.map((path) => { + return [{ loc: urljoin(BAKED_BASE_URL, path) }] + }) + ) .concat( ...countryProfileSpecs.map((spec) => { return countries.map((c) => ({ diff --git a/site/ExplorerIndexPage.scss b/site/ExplorerIndexPage.scss new file mode 100644 index 00000000000..66906cd3f7b --- /dev/null +++ b/site/ExplorerIndexPage.scss @@ -0,0 +1,53 @@ +.explorer-index-page { + .explorer-index-page__header { + background-color: #f0f4fa; + margin-bottom: 16px; + padding-bottom: 24px; + .h1-semibold { + margin-top: 24px; + margin-bottom: 8px; + @include sm-up { + font-size: 2rem; + } + } + p { + margin: 0; + color: #46688f; + } + } + .explorer-index-page-list { + background-color: #fff; + list-style: none; + row-gap: var(--grid-gap); + .explorer-index-page__card { + display: block; + height: 100%; + padding: 8px; + display: flex; + flex-direction: column; + &:hover { + h2 { + text-decoration: none; + } + img { + box-shadow: 0px 0px 20px 0px rgba(49, 37, 2, 0.07); + } + } + img { + transition: 150ms; + box-shadow: 0px 0px 12px 0px rgba(49, 37, 2, 0.05); + } + h2 { + @include owid-link-90; + @include h3-bold; + font-weight: bold; + margin-top: 8px; + margin-bottom: 8px; + } + p { + color: $blue-90; + margin-top: 0; + } + } + } +} diff --git a/site/ExplorerIndexPage.tsx b/site/ExplorerIndexPage.tsx new file mode 100644 index 00000000000..8acfa1bbc21 --- /dev/null +++ b/site/ExplorerIndexPage.tsx @@ -0,0 +1,63 @@ +import React from "react" +import { Head } from "./Head.js" +import { Html } from "./Html.js" +import { EXPLORERS_ROUTE_FOLDER } from "@ourworldindata/explorer" +import { SiteHeader } from "./SiteHeader.js" +import { SiteFooter } from "./SiteFooter.js" +import { MinimalExplorerInfo } from "@ourworldindata/types" +import { EXPLORER_DYNAMIC_THUMBNAIL_URL } from "../settings/clientSettings.js" + +interface ExplorerIndexPageProps { + baseUrl: string + explorers: MinimalExplorerInfo[] +} + +export const ExplorerIndexPage = ({ + baseUrl, + explorers, +}: ExplorerIndexPageProps) => { + return ( + + + + +
+
+

+ Data Explorers +

+

+ Our data explorers gather many indicators together + to provide comprehensive overviews of their topics. +

+
+ +
+ + + + ) +} diff --git a/site/gdocs/components/ExplorerTiles.tsx b/site/gdocs/components/ExplorerTiles.tsx index 769ea8352b9..c01a5e32641 100644 --- a/site/gdocs/components/ExplorerTiles.tsx +++ b/site/gdocs/components/ExplorerTiles.tsx @@ -60,7 +60,7 @@ export function ExplorerTiles({