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 Aug 21, 2024
1 parent 7c74a5b commit 1dd7bdb
Show file tree
Hide file tree
Showing 5 changed files with 128 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 @@ -164,17 +166,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 @@ -258,17 +260,14 @@ export async function fetchGrapherConfig(
}

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

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

if (grapherConfigResponse.status !== 200) {
return null
Expand Down Expand Up @@ -308,20 +307,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")

if (!svg) {
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 "../grapher/thumbnail/[slug].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
}
38 changes: 34 additions & 4 deletions functions/grapher/[slug].ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,20 @@ import { Env } from "../_common/env.js"
import {
getOptionalRedirectForSlug,
createRedirectResponse,
Etag,
fetchUnparsedGrapherConfig,
} from "../_common/grapherRenderer.js"
import { IRequestStrict, Router, error, cors } from "itty-router"
import { handleThumbnailRequest } from "../_common/reusableHandlers.js"

const { preflight, corsify } = cors({
allowMethods: "GET",
})

const router = Router<IRequestStrict, [URL, Env, string]>({
const router = Router<
IRequestStrict,
[URL, Env, Etag, EventContext<unknown, any, Record<string, unknown>>]
>({
before: [preflight],
finally: [corsify],
})
Expand All @@ -20,6 +25,30 @@ router
async ({ params: { slug } }, { searchParams }, env, etag) =>
handleConfigRequest(slug, searchParams, env, etag)
)
.get(
"/grapher/:slug.png",
async ({ params: { slug } }, { searchParams }, env, etag, ctx) =>
handleThumbnailRequest(
{ type: "slug", id: slug },
searchParams,
env,
etag,
ctx,
"png"
)
)
.get(
"/grapher/:slug.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 @@ -39,7 +68,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) => error(500, e))
}
Expand Down Expand Up @@ -99,10 +129,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/${lowerCaseSlug}.png?imType=og${
const openGraphThumbnailUrl = `/grapher/${lowerCaseSlug}.png?imType=og${
url.search ? "&" + url.search.slice(1) : ""
}`
const twitterThumbnailUrl = `/grapher/thumbnail/${lowerCaseSlug}.png?imType=twitter${
const twitterThumbnailUrl = `/grapher/${lowerCaseSlug}.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 "../thumbnail/[slug].js"
import { fetchGrapherConfig } from "../../_common/grapherRenderer.js"
import { IRequestStrict, Router, error, cors } from "itty-router"
import { handleThumbnailRequest } from "../../_common/reusableHandlers.js"
const { preflight, corsify } = cors({
allowMethods: "GET",
})
Expand All @@ -15,6 +16,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 onRequestGet: 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) => error(500, e))
}
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 1dd7bdb

Please sign in to comment.