Skip to content

Commit

Permalink
✨ Render thumbnails for graphers by uuid
Browse files Browse the repository at this point in the history
  • Loading branch information
danyx23 committed Sep 3, 2024
1 parent eb04d8d commit e09348a
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 24 deletions.
26 changes: 10 additions & 16 deletions functions/_common/grapherRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ declare global {
var window: any
}

export type Etag = string

const grapherBaseUrl = "https://ourworldindata.org/grapher"

// Lots of defaults; these are mostly the same as they are in owid-grapher.
Expand Down Expand Up @@ -166,17 +168,17 @@ interface FetchGrapherConfigResult {
etag: string | undefined
}

interface GrapherSlug {
export interface GrapherSlug {
type: "slug"
id: string
}

interface GrapherUuid {
export interface GrapherUuid {
type: "uuid"
id: string
}

type GrapherIdentifier = GrapherSlug | GrapherUuid
export type GrapherIdentifier = GrapherSlug | GrapherUuid

export async function fetchUnparsedGrapherConfig(
identifier: GrapherIdentifier,
Expand Down Expand Up @@ -267,17 +269,14 @@ export async function fetchGrapherConfig(
}

async function fetchAndRenderGrapherToSvg(
slug: string,
id: GrapherIdentifier,
options: ImageOptions,
searchParams: URLSearchParams,
env: Env
): Promise<string> {
const grapherLogger = new TimeLogger("grapher")

const grapherConfigResponse = await fetchGrapherConfig(
{ type: "slug", id: slug },
env
)
const grapherConfigResponse = await fetchGrapherConfig(id, env)

if (grapherConfigResponse.status === 404) {
// we throw 404 errors instad of returning a 404 response so that the router
Expand Down Expand Up @@ -320,20 +319,15 @@ async function fetchAndRenderGrapherToSvg(
}

export const fetchAndRenderGrapher = async (
slug: string,
id: GrapherIdentifier,
searchParams: URLSearchParams,
outType: "png" | "svg",
env: Env
) => {
const options = extractOptions(searchParams)

console.log("Rendering", slug, outType, options)
const svg = await fetchAndRenderGrapherToSvg(
slug,
options,
searchParams,
env
)
console.log("Rendering", id.id, outType, options)
const svg = await fetchAndRenderGrapherToSvg(id, options, searchParams, env)
console.log("fetched svg")

switch (outType) {
Expand Down
37 changes: 37 additions & 0 deletions functions/_common/reusableHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Env } from "./env.js"
import {
Etag,
GrapherIdentifier,
fetchAndRenderGrapher,
} from "./grapherRenderer.js"

export async function handleThumbnailRequest(
id: GrapherIdentifier,
searchParams: URLSearchParams,
env: Env,
_etag: Etag,
ctx: EventContext<unknown, any, Record<string, unknown>>,
extension: "png" | "svg"
) {
const url = new URL(env.url)
const shouldCache = !url.searchParams.has("nocache")

const cache = caches.default
console.log("Handling", env.url, ctx.request.headers.get("User-Agent"))
if (shouldCache) {
console.log("Checking cache")
const maybeCached = await cache.match(ctx.request)
console.log("Cache check result", maybeCached ? "hit" : "miss")
if (maybeCached) return maybeCached
}
const resp = await fetchAndRenderGrapher(id, searchParams, extension, env)
if (shouldCache) {
resp.headers.set("Cache-Control", "public, s-maxage=3600, max-age=3600")
ctx.waitUntil(caches.default.put(ctx.request, resp.clone()))
} else
resp.headers.set(
"Cache-Control",
"public, s-maxage=0, max-age=0, must-revalidate"
)
return resp
}
42 changes: 38 additions & 4 deletions functions/grapher/[slug].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,54 @@ import { Env } from "../_common/env.js"
import {
getOptionalRedirectForSlug,
createRedirectResponse,
Etag,
fetchUnparsedGrapherConfig,
} from "../_common/grapherRenderer.js"
import { IRequestStrict, Router, StatusError, error } from "itty-router"
import { handleThumbnailRequest } from "../_common/reusableHandlers.js"

// We collect the possible extensions here so we can easily take them into account
// when handling redirects
const extensions = {
configJson: ".config.json",
png: ".png",
svg: ".svg",
}

const router = Router<IRequestStrict, [URL, Env, string]>()
const router = Router<
IRequestStrict,
[URL, Env, Etag, EventContext<unknown, any, Record<string, unknown>>]
>()
router
.get(
`/grapher/:slug${extensions.configJson}`,
async ({ params: { slug } }, { searchParams }, env, etag) =>
handleConfigRequest(slug, searchParams, env, etag)
)
.get(
`/grapher/:slug${extensions.png}`,
async ({ params: { slug } }, { searchParams }, env, etag, ctx) =>
handleThumbnailRequest(
{ type: "slug", id: slug },
searchParams,
env,
etag,
ctx,
"png"
)
)
.get(
`/grapher/:slug${extensions.svg}`,
async ({ params: { slug } }, { searchParams }, env, etag, ctx) =>
handleThumbnailRequest(
{ type: "slug", id: slug },
searchParams,
env,
etag,
ctx,
"svg"
)
)
.get(
"/grapher/:slug",
async ({ params: { slug } }, { searchParams }, env) =>
Expand All @@ -42,7 +75,8 @@ export const onRequestGet: PagesFunction = async (context) => {
request,
url,
{ ...env, url },
request.headers.get("if-none-match")
request.headers.get("if-none-match"),
context
)
.catch(async (e) => {
// Here we do a unified after the fact handling of 404s to check
Expand Down Expand Up @@ -112,10 +146,10 @@ async function handleHtmlPageRequest(
// In the case of the redirect, the browser will then request the new URL which will again be handled by this worker.
if (grapherPageResp.status !== 200) return grapherPageResp

const openGraphThumbnailUrl = `/grapher/thumbnail/${slug}.png?imType=og${
const openGraphThumbnailUrl = `/grapher/${slug}.png?imType=og${
url.search ? "&" + url.search.slice(1) : ""
}`
const twitterThumbnailUrl = `/grapher/thumbnail/${slug}.png?imType=twitter${
const twitterThumbnailUrl = `/grapher/${slug}.png?imType=twitter${
url.search ? "&" + url.search.slice(1) : ""
}`

Expand Down
28 changes: 27 additions & 1 deletion functions/grapher/by-uuid/[uuid].ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Env } from "../../_common/env.js"
import { fetchGrapherConfig } from "../../_common/grapherRenderer.js"
import { IRequestStrict, Router, error, StatusError } from "itty-router"
import { handleThumbnailRequest } from "../../_common/reusableHandlers.js"

const router = Router<IRequestStrict, [URL, Env, string]>()
router
Expand All @@ -9,6 +10,30 @@ router
async ({ params: { uuid } }, { searchParams }, env, etag) =>
handleConfigRequest(uuid, searchParams, env, etag)
)
.get(
"/grapher/by-uuid/:uuid.png",
async ({ params: { uuid } }, { searchParams }, env, etag, ctx) =>
handleThumbnailRequest(
{ type: "uuid", id: uuid },
searchParams,
env,
etag,
ctx,
"png"
)
)
.get(
"/grapher/by-uuid/:uuid.svg",
async ({ params: { uuid } }, { searchParams }, env, etag, ctx) =>
handleThumbnailRequest(
{ type: "uuid", id: uuid },
searchParams,
env,
etag,
ctx,
"svg"
)
)
.all("*", () => error(404, "Route not defined"))

export const onRequestOptions: PagesFunction = async (_context) => {
Expand All @@ -29,7 +54,8 @@ export const onRequestGet: PagesFunction = async (context) => {
request,
url,
{ ...env, url },
request.headers.get("if-none-match")
request.headers.get("if-none-match"),
context
)
.catch((e) => {
if (e instanceof StatusError) {
Expand Down
23 changes: 20 additions & 3 deletions functions/grapher/thumbnail/[slug].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,39 @@ import { Env } from "../../_common/env.js"
import { fetchAndRenderGrapher } from "../../_common/grapherRenderer.js"
import { IRequestStrict, Router, error } from "itty-router"

// TODO: remove the /grapher/thumbnail route two weeks or so after the change to use /grapher/:slug.png is deployed
// We keep this around for another two weeks so that cached html pages etc can still fetch the correct thumbnail
const router = Router<IRequestStrict, [URL, Env, ExecutionContext]>()
router
.get(
"/grapher/thumbnail/:slug.png",
async ({ params: { slug } }, { searchParams }, env) =>
fetchAndRenderGrapher(slug, searchParams, "png", env)
fetchAndRenderGrapher(
{ type: "slug", id: slug },
searchParams,
"png",
env
)
)
.get(
"/grapher/thumbnail/:slug.svg",
async ({ params: { slug } }, { searchParams }, env) =>
fetchAndRenderGrapher(slug, searchParams, "svg", env)
fetchAndRenderGrapher(
{ type: "slug", id: slug },
searchParams,
"svg",
env
)
)
.get(
"/grapher/thumbnail/:slug",
async ({ params: { slug } }, { searchParams }, env) =>
fetchAndRenderGrapher(slug, searchParams, "svg", env)
fetchAndRenderGrapher(
{ type: "slug", id: slug },
searchParams,
"svg",
env
)
)
.all("*", () => error(404, "Route not defined"))

Expand Down

0 comments on commit e09348a

Please sign in to comment.