Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ add CF function for getting SLUG.config.json #3870

Merged
merged 1 commit into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions functions/_common/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface Env {
ASSETS: {
fetch: typeof fetch
}
url: URL
GRAPHER_CONFIG_R2_BUCKET_URL: string
GRAPHER_CONFIG_R2_BUCKET_FALLBACK_URL: string
GRAPHER_CONFIG_R2_BUCKET_PATH: string
GRAPHER_CONFIG_R2_BUCKET_FALLBACK_PATH: string
CF_PAGES_BRANCH: string
ENV: string
}
84 changes: 73 additions & 11 deletions functions/_common/grapherRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import LatoRegular from "../_common/fonts/LatoLatin-Regular.ttf.bin"
import LatoMedium from "../_common/fonts/LatoLatin-Medium.ttf.bin"
import LatoBold from "../_common/fonts/LatoLatin-Bold.ttf.bin"
import PlayfairSemiBold from "../_common/fonts/PlayfairDisplayLatin-SemiBold.ttf.bin"
import { Env } from "../grapher/thumbnail/[slug].js"
import { Env } from "./env.js"

declare global {
// eslint-disable-next-line no-var
Expand Down Expand Up @@ -158,14 +158,17 @@ async function fetchFromR2(
return primaryResponse
}

async function fetchAndRenderGrapherToSvg(
interface FetchGrapherConfigResult {
grapherConfig: GrapherInterface | null
status: number
etag: string | undefined
}

export async function fetchUnparsedGrapherConfig(
slug: string,
options: ImageOptions,
searchParams: URLSearchParams,
env: Env
env: Env,
etag?: string
) {
const grapherLogger = new TimeLogger("grapher")

// The top level directory is either the bucket path (should be set in dev environments and production)
// or the branch name on preview staging environments
console.log("branch", env.CF_PAGES_BRANCH)
Expand Down Expand Up @@ -202,19 +205,54 @@ async function fetchAndRenderGrapherToSvg(
}

// Fetch grapher config
const fetchResponse = await fetchFromR2(requestUrl, undefined, fallbackUrl)
return fetchFromR2(requestUrl, etag, fallbackUrl)
}

export async function fetchGrapherConfig(
slug: string,
env: Env,
etag?: string
): Promise<FetchGrapherConfigResult> {
const fetchResponse = await fetchUnparsedGrapherConfig(slug, env, etag)

if (fetchResponse.status !== 200) {
console.log("Failed to fetch grapher config", fetchResponse.status)
return null
console.log(
"Status code is not 200, returning empty response with status code",
fetchResponse.status
)
return {
grapherConfig: null,
status: fetchResponse.status,
etag: fetchResponse.headers.get("etag"),
}
}

const grapherConfig: GrapherInterface = await fetchResponse.json()
console.log("grapher title", grapherConfig.title)
return {
grapherConfig,
status: 200,
etag: fetchResponse.headers.get("etag"),
}
}

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

const grapherConfigResponse = await fetchGrapherConfig(slug, env)

if (grapherConfigResponse.status !== 200) {
return null
}

const bounds = new Bounds(0, 0, options.svgWidth, options.svgHeight)
const grapher = new Grapher({
...grapherConfig,
...grapherConfigResponse.grapherConfig,
bakedGrapherURL: grapherBaseUrl,
queryStr: "?" + searchParams.toString(),
bounds,
Expand Down Expand Up @@ -298,3 +336,27 @@ async function renderSvgToPng(svg: string, options: ImageOptions) {
pngLogger.log("svg2png")
return pngData
}

export async function getOptionalRedirectForSlug(
slug: string,
baseUrl: URL,
env: Env
) {
const redirects: Record<string, string> = await env.ASSETS.fetch(
new URL("/grapher/_grapherRedirects.json", baseUrl),
{ cf: { cacheTtl: 2 * 60 } }
)
.then((r): Promise<Record<string, string>> => r.json())
.catch((e) => {
console.error("Error fetching redirects", e)
return {}
})
return redirects[slug]
}

export function createRedirectResponse(redirSlug: string, currentUrl: URL) {
new Response(null, {
status: 302,
headers: { Location: `/grapher/${redirSlug}${currentUrl.search}` },
})
}
154 changes: 124 additions & 30 deletions functions/grapher/[slug].ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,57 @@
export const onRequestGet: PagesFunction = async (context) => {
import { Env } from "../_common/env.js"
import {
getOptionalRedirectForSlug,
createRedirectResponse,
fetchUnparsedGrapherConfig,
} from "../_common/grapherRenderer.js"
import { IRequestStrict, Router, error, cors } from "itty-router"

const { preflight, corsify } = cors({
allowMethods: ["GET", "OPTIONS", "HEAD"],
})

const router = Router<IRequestStrict, [URL, Env, string]>({
before: [preflight],
finally: [corsify],
})
router
.get(
"/grapher/:slug.config.json",
async ({ params: { slug } }, { searchParams }, env, etag) =>
handleConfigRequest(slug, searchParams, env, etag)
)
.get(
"/grapher/:slug",
async ({ params: { slug } }, { searchParams }, env) =>
handleHtmlPageRequest(slug, searchParams, env)
)
.all("*", () => error(404, "Route not defined"))

export const onRequest: PagesFunction = async (context) => {
// Makes it so that if there's an error, we will just deliver the original page before the HTML rewrite.
// Only caveat is that redirects will not be taken into account for some reason; but on the other hand the worker is so simple that it's unlikely to fail.
context.passThroughOnException()
const { request, env } = context
const url = new URL(request.url)

// Redirects handling is performed by the worker, and is done by fetching the (baked) _grapherRedirects.json file.
// That file is a mapping from old slug to new slug.
const getOptionalRedirectForSlug = async (slug: string, baseUrl: URL) => {
const redirects: Record<string, string> = await env.ASSETS.fetch(
new URL("/grapher/_grapherRedirects.json", baseUrl),
{ cf: { cacheTtl: 2 * 60 } }
return router
.fetch(
request,
url,
{ ...env, url },
request.headers.get("if-none-match")
)
.then((r): Promise<Record<string, string>> => r.json())
.catch((e) => {
console.error("Error fetching redirects", e)
return {}
})
return redirects[slug]
}

const createRedirectResponse = (redirSlug: string, currentUrl: URL) =>
new Response(null, {
status: 302,
headers: { Location: `/grapher/${redirSlug}${currentUrl.search}` },
})

const { request, env, params } = context
.catch((e) => error(500, e))
}

const originalSlug = params.slug as string
const url = new URL(request.url)
async function handleHtmlPageRequest(
slug: string,
searchParams: URLSearchParams,
env: Env
) {
const url = env.url
// Redirects handling is performed by the worker, and is done by fetching the (baked) _grapherRedirects.json file.
// That file is a mapping from old slug to new slug.

/**
* REDIRECTS HANDLING:
Expand All @@ -40,11 +64,12 @@ export const onRequestGet: PagesFunction = async (context) => {

// All our grapher slugs are lowercase by convention.
// To allow incoming links that may contain uppercase characters to work, we redirect to the lowercase version.
const lowerCaseSlug = originalSlug.toLowerCase()
if (lowerCaseSlug !== originalSlug) {
const lowerCaseSlug = slug.toLowerCase()
if (lowerCaseSlug !== slug) {
const redirectSlug = await getOptionalRedirectForSlug(
lowerCaseSlug,
url
url,
env
)

return createRedirectResponse(redirectSlug ?? lowerCaseSlug, url)
Expand All @@ -61,8 +86,8 @@ export const onRequestGet: PagesFunction = async (context) => {
if (grapherPageResp.status === 404) {
// If the request is a 404, we check if there's a redirect for it.
// If there is, we redirect to the new page.
const redirectSlug = await getOptionalRedirectForSlug(originalSlug, url)
if (redirectSlug && redirectSlug !== originalSlug) {
const redirectSlug = await getOptionalRedirectForSlug(slug, url, env)
if (redirectSlug && redirectSlug !== slug) {
return createRedirectResponse(redirectSlug, url)
} else {
// Otherwise we just return the 404 page.
Expand Down Expand Up @@ -110,5 +135,74 @@ export const onRequestGet: PagesFunction = async (context) => {
},
})

return rewriter.transform(grapherPageResp)
return rewriter.transform(grapherPageResp as unknown as Response)
}

async function handleConfigRequest(
slug: string,
searchParams: URLSearchParams,
env: Env,
etag: string | undefined
) {
const shouldCache = searchParams.get("nocache") === null
console.log("Preparing json response for ", slug)
// All our grapher slugs are lowercase by convention.
// To allow incoming links that may contain uppercase characters to work, we redirect to the lowercase version.
const lowerCaseSlug = slug.toLowerCase()
if (lowerCaseSlug !== slug) {
const redirectSlug = await getOptionalRedirectForSlug(
lowerCaseSlug,
env.url,
env
)

return createRedirectResponse(
`${redirectSlug ?? lowerCaseSlug}.config.json`,
env.url
)
}

const grapherPageResp = await fetchUnparsedGrapherConfig(slug, env, etag)

if (grapherPageResp.status === 304) {
console.log("Returning 304 for ", slug)
return new Response(null, { status: 304 })
}

if (grapherPageResp.status !== 200) {
// If the request is a 404, we check if there's a redirect for it.
// If there is, we redirect to the new page.
const redirectSlug = await getOptionalRedirectForSlug(
slug,
env.url,
env
)
if (redirectSlug && redirectSlug !== slug) {
console.log("Redirecting to ", redirectSlug)
return createRedirectResponse(
`${redirectSlug}.config.json`,
env.url
)
} else {
console.log("Returning 404 for ", slug)
// Otherwise we just return the status code.
return new Response(null, { status: grapherPageResp.status })
}
}

console.log("Returning 200 for ", slug)

const cacheControl = shouldCache
? "public, s-maxage=3600, max-age=0, must-revalidate"
: "public, s-maxage=0, max-age=0, must-revalidate"

//grapherPageResp.headers.set("Cache-Control", cacheControl)
return new Response(grapherPageResp.body as any, {
status: 200,
headers: {
"Content-Type": "application/json",
"Cache-Control": cacheControl,
ETag: grapherPageResp.headers.get("ETag") ?? "",
},
})
}
14 changes: 1 addition & 13 deletions functions/grapher/thumbnail/[slug].ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,7 @@
import { Env } from "../../_common/env.js"
import { fetchAndRenderGrapher } from "../../_common/grapherRenderer.js"
import { IRequestStrict, Router, error } from "itty-router"

export interface Env {
ASSETS: {
fetch: typeof fetch
}
url: URL
GRAPHER_CONFIG_R2_BUCKET_URL: string
GRAPHER_CONFIG_R2_BUCKET_FALLBACK_URL: string
GRAPHER_CONFIG_R2_BUCKET_PATH: string
GRAPHER_CONFIG_R2_BUCKET_FALLBACK_PATH: string
CF_PAGES_BRANCH: string
ENV: string
}

const router = Router<IRequestStrict, [URL, Env, ExecutionContext]>()
router
.get(
Expand Down