Update project configuration and enhance API functionality

- Updated package.json to use npx for deploying and developing, and upgraded several dependencies including vitest and wrangler.
- Renamed the project in wrangler.jsonc to include the year 2026 and added new routes and triggers for scheduled tasks.
- Enhanced the API by adding new reload endpoints for pages, blogs, projects, and experiences, with appropriate CORS handling.
- Implemented checks in import functions for files, images, and videos to skip updates if no changes are detected.
- Improved error handling and logging across various routes and utilities.
- Refactored global headers to dynamically resolve CORS origins based on incoming requests.
This commit is contained in:
Tom Butcher 2025-11-15 19:34:41 +00:00
parent 6d5265dfe0
commit db60e43d73
19 changed files with 798 additions and 1355 deletions

View File

@ -3,25 +3,23 @@
"version": "0.0.0",
"private": true,
"scripts": {
"deploy": "wrangler deploy --env production",
"dev": "wrangler dev --test-scheduled --host 0.0.0.0",
"test": "vitest"
"deploy": "npx wrangler deploy --env production",
"dev": "npx wrangler dev --test-scheduled --host 0.0.0.0"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.6.4",
"vitest": "~2.1.9",
"wrangler": "^4.38.0"
"@cloudflare/vitest-pool-workers": "^0.10.5",
"wrangler": "^4.46.0"
},
"dependencies": {
"@napi-rs/canvas": "^0.1.80",
"@notionhq/client": "^5.1.0",
"@napi-rs/canvas": "^0.1.81",
"@notionhq/client": "^5.3.0",
"blurhash": "^2.0.5",
"dayjs": "^1.11.18",
"dayjs": "^1.11.19",
"jpeg-js": "^0.4.4",
"lodash": "^4.17.21",
"microdiff": "^1.5.0",
"remove-svg-properties": "^0.3.4",
"sharp": "^0.34.3",
"sharp": "^0.34.5",
"svgson": "^5.3.1",
"upng-js": "^2.1.0"
}

View File

@ -2,6 +2,16 @@ import { handleContactRequest } from "./routes/contact.js";
import { handleContentRequest } from "./routes/content.js";
import { handleNotionHook } from "./routes/hooks.js";
import { globalHeaders } from "./utils/api.js";
import {
handleReloadPage,
handleReloadBlog,
handleReloadProject,
handleReloadExperience,
} from "./routes/reload.js";
import { handleBlurHashUpdate } from "./utils/imageCache.js";
import { importFiles } from "./objects/files.js";
import { importVideos } from "./objects/videos.js";
import { importImages } from "./objects/images.js";
async function handleRequest(request, env) {
if (
@ -12,7 +22,7 @@ async function handleRequest(request, env) {
return new Response(null, {
status: 204, // No Content
headers: {
"Access-Control-Allow-Origin": env.CORS_ORIGIN,
...globalHeaders(request),
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
@ -39,18 +49,105 @@ async function handleRequest(request, env) {
return await handleNotionHook(request, env);
}
if (
request.method === "OPTIONS" &&
request.url.split("?")[0].endsWith("/reloadPage")
) {
console.log("Handling reloadPage OPTIONS request...");
return new Response(null, {
status: 204, // No Content
headers: {
...globalHeaders(request),
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
if (
request.method === "POST" &&
request.url.split("?")[0].endsWith("/reloadPage")
) {
return await handleReloadPage(request, env);
}
if (
request.method === "OPTIONS" &&
request.url.split("?")[0].endsWith("/reloadBlog")
) {
console.log("Handling reloadBlog OPTIONS request...");
return new Response(null, {
status: 204, // No Content
headers: {
...globalHeaders(request),
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
if (
request.method === "POST" &&
request.url.split("?")[0].endsWith("/reloadBlog")
) {
return await handleReloadBlog(request, env);
}
if (
request.method === "OPTIONS" &&
request.url.split("?")[0].endsWith("/reloadProject")
) {
console.log("Handling reloadProject OPTIONS request...");
return new Response(null, {
status: 204, // No Content
headers: {
...globalHeaders(request),
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
if (
request.method === "POST" &&
request.url.split("?")[0].endsWith("/reloadProject")
) {
return await handleReloadProject(request, env);
}
if (
request.method === "OPTIONS" &&
request.url.split("?")[0].endsWith("/reloadExperience")
) {
console.log("Handling reloadExperience OPTIONS request...");
return new Response(null, {
status: 204, // No Content
headers: {
...globalHeaders(request),
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}
if (
request.method === "POST" &&
request.url.split("?")[0].endsWith("/reloadExperience")
) {
return await handleReloadExperience(request, env);
}
// Return 404 if the route is not found
return new Response("Not Found", { status: 404, headers: globalHeaders });
return new Response("Not Found", {
status: 404,
headers: globalHeaders(request),
});
}
async function handleScheduledEvent(event, env) {
console.log("Scheduled event:", event.cron);
switch (event.cron) {
case "*/5 * * * *":
await updateAllSmoobuData(env);
break;
case "* * * * *":
await refreshBookingCache(env);
await importFiles(env);
await importVideos(env);
await importImages(env);
await handleBlurHashUpdate(env);
break;
default:
break;

View File

@ -2,7 +2,12 @@ import { getPages } from "../utils/pages.js";
import { getBlogs } from "../utils/blogs.js";
import { getProjects } from "../utils/projects.js";
import { getCompanies } from "../utils/companies.js";
import { collectFileUrls, updateFiles } from "../utils/fileCache.js";
import {
collectFileUrls,
updateFiles,
getFiles,
getFileIdFromUrl,
} from "../utils/fileCache.js";
import { getCvs } from "../utils/cv.js";
export async function importFiles(env) {
@ -21,6 +26,22 @@ export async function importFiles(env) {
...cvs,
]);
// Check if files have changed before updating
const existingFiles = (await getFiles(env)) || [];
const existingFileIds = new Set(existingFiles.map((file) => file.id));
const currentFileIds = new Set(fileUrls.map((url) => getFileIdFromUrl(url)));
// Check if there are any differences
const hasChanges =
existingFileIds.size !== currentFileIds.size ||
[...currentFileIds].some((id) => !existingFileIds.has(id)) ||
[...existingFileIds].some((id) => !currentFileIds.has(id));
if (!hasChanges) {
console.log("No file changes detected. Skipping file update.");
return existingFiles;
}
const updatedFiles = await updateFiles(env, fileUrls);
console.log("Imported files from Notion.");
return updatedFiles;

View File

@ -2,7 +2,12 @@ import { getPages } from "../utils/pages.js";
import { getBlogs } from "../utils/blogs.js";
import { getProjects } from "../utils/projects.js";
import { getCompanies } from "../utils/companies.js";
import { collectImageUrls, updateImages } from "../utils/imageCache.js";
import {
collectImageUrls,
updateImages,
getImages,
getImageIdFromUrl,
} from "../utils/imageCache.js";
export async function importImages(env) {
// Fetch caches
@ -20,6 +25,24 @@ export async function importImages(env) {
...companies,
]);
// Check if images have changed before updating
const existingImages = (await getImages(env)) || [];
const existingImageIds = new Set(existingImages.map((image) => image.id));
const currentImageIds = new Set(
imageUrls.map((url) => getImageIdFromUrl(url))
);
// Check if there are any differences
const hasChanges =
existingImageIds.size !== currentImageIds.size ||
[...currentImageIds].some((id) => !existingImageIds.has(id)) ||
[...existingImageIds].some((id) => !currentImageIds.has(id));
if (!hasChanges) {
console.log("No image changes detected. Skipping image update.");
return existingImages;
}
const updatedImages = await updateImages(env, imageUrls);
console.log("Imported images from Notion.");
return updatedImages;

View File

@ -29,12 +29,25 @@ export async function importNotionProjects(env, notionId = null) {
transformNotionProject(env, project, settingsData)
)
)
).filter(Boolean);
)
.filter(Boolean)
.sort((a, b) => {
const dateA = a.date ? new Date(a.date) : new Date(0);
const dateB = b.date ? new Date(b.date) : new Date(0);
return dateB - dateA;
});
// If notionId is not null, use lodash unionBy to replace existing Projects
if (notionId !== null) {
const cachedProjects = await getProjects(env);
const mergedProjects = unionBy(Projects, cachedProjects, "notionId");
const mergedProjects = unionBy(Projects, cachedProjects, "notionId").sort(
(a, b) => {
const dateA = a.date ? new Date(a.date) : new Date(0);
const dateB = b.date ? new Date(b.date) : new Date(0);
return dateB - dateA;
}
);
await storeProjects(env, mergedProjects);
console.log("Imported Projects from Notion and merged with cache.");
return mergedProjects;

View File

@ -2,7 +2,12 @@ import { getPages } from "../utils/pages.js";
import { getBlogs } from "../utils/blogs.js";
import { getProjects } from "../utils/projects.js";
import { getCompanies } from "../utils/companies.js";
import { collectVideoUrls, updateVideos } from "../utils/videoCache.js";
import {
collectVideoUrls,
updateVideos,
getVideos,
getVideoIdFromUrl,
} from "../utils/videoCache.js";
export async function importVideos(env) {
console.log("Importing videos from Notion...");
@ -18,6 +23,24 @@ export async function importVideos(env) {
...companies,
]);
// Check if videos have changed before updating
const existingVideos = (await getVideos(env)) || [];
const existingVideoIds = new Set(existingVideos.map((video) => video.id));
const currentVideoIds = new Set(
videoUrls.map((url) => getVideoIdFromUrl(url))
);
// Check if there are any differences
const hasChanges =
existingVideoIds.size !== currentVideoIds.size ||
[...currentVideoIds].some((id) => !existingVideoIds.has(id)) ||
[...existingVideoIds].some((id) => !currentVideoIds.has(id));
if (!hasChanges) {
console.log("No video changes detected. Skipping video update.");
return existingVideos;
}
const updatedVideos = await updateVideos(env, videoUrls);
console.log("Imported videos from Notion.");
return updatedVideos;

View File

@ -1,4 +1,4 @@
export async function handleProcessBlurhash(env) {
export async function handleProcessBlurhash(env, request) {
try {
const imagesData = await getImages(env);
@ -17,7 +17,7 @@ export async function handleProcessBlurhash(env) {
}),
{
status: 500,
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}

View File

@ -14,8 +14,8 @@ export async function handleContactRequest(request, env) {
}),
{
status: 400,
headers: globalHeaders,
},
headers: globalHeaders(request),
}
);
}
@ -28,12 +28,12 @@ export async function handleContactRequest(request, env) {
"https://challenges.cloudflare.com/turnstile/v0/siteverify",
{
method: "POST",
headers: globalHeaders,
headers: globalHeaders(request),
body: JSON.stringify({
secret: env.TURNSTILE_AUTH,
response: token,
}),
},
}
);
const verificationData = await verificationResponse.json();
@ -47,8 +47,8 @@ export async function handleContactRequest(request, env) {
JSON.stringify({ message: errorMessage, code: `captcha-${code}` }),
{
status: 400,
headers: globalHeaders,
},
headers: globalHeaders(request),
}
);
}
}
@ -62,7 +62,7 @@ export async function handleContactRequest(request, env) {
Message: message,
["IP Address"]: ip,
},
env.MESSAGES_DB,
env.MESSAGES_DB
);
return new Response(
JSON.stringify({
@ -70,16 +70,16 @@ export async function handleContactRequest(request, env) {
message: "Email processed and added to Notion",
}),
{
headers: globalHeaders,
},
headers: globalHeaders(request),
}
);
} catch (error) {
return new Response(
JSON.stringify({ message: "Error storing data.", code: "storage-error" }),
{
status: 500,
headers: globalHeaders,
},
headers: globalHeaders(request),
}
);
}
}

View File

@ -9,24 +9,35 @@ import { importNotionCompanies } from "../objects/companies.js";
import { importImages } from "../objects/images.js";
import { importNotionCvs } from "../objects/cv.js";
import { importFiles } from "../objects/files.js";
import { importNotionProjects } from "../objects/projects.js";
import { importVideos } from "../objects/videos.js";
// Fetch or return cached content
export async function getCachedContent(env) {
export async function getCachedContent(env, request) {
// Try to get combined cached content
var cachedContent = await getCombinedCachedContent(env);
const noBlogs = cachedContent.blogs?.length === 0;
const noPages = cachedContent.pages?.length === 0;
const noSettings = cachedContent.settings == {};
const noCompanies = cachedContent.companies?.length === 0;
const noCvs = cachedContent.cvs?.length === 0;
const noPositions = cachedContent.positions?.length === 0;
const noBlogs =
cachedContent.blogs?.length === 0 || cachedContent.blogs == null;
const noPages =
cachedContent.pages?.length === 0 || cachedContent.pages == null;
const noSettings =
Object.keys(cachedContent.settings).length === 0 ||
cachedContent.settings == null;
const noCompanies =
cachedContent.companies?.length === 0 || cachedContent.companies == null;
const noCvs = cachedContent.cvs?.length === 0 || cachedContent.cvs == null;
const noProjects =
cachedContent.projects?.length === 0 || cachedContent.projects == null;
if (noBlogs || noPages || noSettings || noCompanies || noCvs || noPositions) {
if (noBlogs || noPages || noSettings || noCompanies || noCvs || noProjects) {
await importNotionNavigation(env);
}
if (noSettings) {
cachedContent.settings = await importNotionSettings(env);
}
if (noBlogs) {
cachedContent.blogs = await importNotionBlogs(env);
cachedContent.images = await importImages(env);
@ -41,10 +52,6 @@ export async function getCachedContent(env) {
cachedContent.videos = await importVideos(env);
}
if (noSettings) {
cachedContent.settings = await importNotionSettings(env);
}
if (noCvs) {
cachedContent.cvs = await importNotionCvs(env);
cachedContent.files = await importFiles(env);
@ -58,6 +65,13 @@ export async function getCachedContent(env) {
cachedContent.videos = await importVideos(env);
}
if (noProjects) {
cachedContent.projects = await importNotionProjects(env);
cachedContent.images = await importImages(env);
cachedContent.files = await importFiles(env);
cachedContent.videos = await importVideos(env);
}
if (cachedContent) {
return new Response(
JSON.stringify({
@ -65,17 +79,17 @@ export async function getCachedContent(env) {
blogs: cachedContent.blogs.filter((blog) => blog.published == true),
}),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
return new Response(JSON.stringify({}), { headers: globalHeaders });
return new Response(JSON.stringify({}), { headers: globalHeaders(request) });
}
export async function handleContentRequest(request, env) {
try {
return await getCachedContent(env);
return await getCachedContent(env, request);
} catch (error) {
console.error("Error handling content request:", error);
return new Response(
@ -85,7 +99,7 @@ export async function handleContentRequest(request, env) {
}),
{
status: 500,
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}

View File

@ -6,7 +6,6 @@ import { globalHeaders } from "../utils/api";
import { importImages } from "../objects/images";
import { importFiles } from "../objects/files";
import { importVideos } from "../objects/videos";
import { handleBlurhashUpdate } from "../utils/imageCache";
import { importNotionProjects } from "../objects/projects";
import { importNotionCompanies } from "../objects/companies";
import { importNotionPositions } from "../objects/positions";
@ -33,7 +32,7 @@ export async function handleNotionHook(request, env) {
JSON.stringify({
status: "gotVerificationToken",
}),
{ headers: globalHeaders }
{ headers: globalHeaders(request) }
);
}
console.log("Notion hook received:", body.type);
@ -51,14 +50,13 @@ export async function handleNotionHook(request, env) {
await importNotionNavigation(env);
const pages = await importNotionPages(env, entityId);
const images = await importImages(env);
await handleBlurhashUpdate(request, env);
return new Response(
JSON.stringify({
status: "OK",
content: { pages, images },
}),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
@ -68,14 +66,13 @@ export async function handleNotionHook(request, env) {
const images = await importImages(env);
const files = await importFiles(env);
const videos = await importVideos(env);
await handleBlurhashUpdate(request, env);
return new Response(
JSON.stringify({
status: "OK",
content: { blogs, images, files, videos },
}),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
@ -85,14 +82,13 @@ export async function handleNotionHook(request, env) {
const images = await importImages(env);
const files = await importFiles(env);
const videos = await importVideos(env);
await handleBlurhashUpdate(request, env);
return new Response(
JSON.stringify({
status: "OK",
content: { projects, images, files, videos },
}),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
@ -102,14 +98,13 @@ export async function handleNotionHook(request, env) {
const images = await importImages(env);
const files = await importFiles(env);
const videos = await importVideos(env);
await handleBlurhashUpdate(request, env);
return new Response(
JSON.stringify({
status: "OK",
content: { companies, images, positions, files, videos },
}),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
@ -117,7 +112,6 @@ export async function handleNotionHook(request, env) {
const positions = await importNotionPositions(env, entityId);
const companies = await importNotionCompanies(env);
const images = await importImages(env);
await handleBlurhashUpdate(request, env);
const files = await importFiles(env);
const videos = await importVideos(env);
return new Response(
@ -126,7 +120,7 @@ export async function handleNotionHook(request, env) {
content: { positions, companies, images, files, videos },
}),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
@ -136,7 +130,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { cvs, files } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
@ -145,7 +139,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { settings } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
@ -154,7 +148,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { redirects } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
@ -163,16 +157,13 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { branding } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
console.log("Page Blogs with data source:", dataSourceId);
}
if (
body.type == "page.properties_updated" ||
(body.type == "page.deleted" && body.data?.parent?.data_source_id)
) {
if (body.type == "page.deleted" && body.data?.parent?.data_source_id) {
const dataSourceId = body.data.parent.data_source_id;
const entityId = body?.entity?.id || null;
console.log("Data source ID:", dataSourceId);
@ -185,7 +176,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { pages } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
case env.BLOGS_DB:
@ -194,7 +185,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { blogs } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
case env.COMPANIES_DB:
@ -202,7 +193,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { companies } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
case env.POSITIONS_DB:
@ -211,7 +202,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { positions } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
case env.PROJECTS_DB:
@ -219,7 +210,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { projects } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
case env.CV_DB:
@ -227,7 +218,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { cvs } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
case env.THEMES_DB:
@ -235,7 +226,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { settings } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
case env.REDIRECTS_DB:
@ -243,7 +234,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { redirects } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
case env.BRANDING_DB:
@ -251,7 +242,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "OK", content: { branding } }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
}
@ -263,7 +254,7 @@ export async function handleNotionHook(request, env) {
return new Response(
JSON.stringify({ status: "Unknown hook", type: body.type }),
{
headers: globalHeaders,
headers: globalHeaders(request),
}
);
} catch (error) {
@ -271,7 +262,7 @@ export async function handleNotionHook(request, env) {
console.log("No body to parse. Calling both fetch requests...");
const content = await updateAllNotionData(env);
return new Response(JSON.stringify({ status: "OK", content }), {
headers: globalHeaders,
headers: globalHeaders(request),
});
}
}

156
src/routes/reload.js Normal file
View File

@ -0,0 +1,156 @@
import { globalHeaders } from "../utils/api.js";
import { importNotionNavigation } from "../objects/navigation.js";
import { importNotionPages } from "../objects/pages.js";
import { importNotionBlogs } from "../objects/blogs.js";
import { importNotionProjects } from "../objects/projects.js";
import { importNotionPositions } from "../objects/positions.js";
import { importNotionCompanies } from "../objects/companies.js";
import { importImages } from "../objects/images.js";
import { importFiles } from "../objects/files.js";
import { importVideos } from "../objects/videos.js";
import { handleBlurHashUpdate } from "../utils/imageCache.js";
class ReloadRequestError extends Error {
constructor(message, status = 400) {
super(message);
this.status = status;
}
}
async function parseReloadRequest(request) {
try {
const body = await request.json();
if (body === null || typeof body !== "object" || Array.isArray(body)) {
throw new ReloadRequestError("Request body must be a JSON object");
}
return body;
} catch (error) {
if (error instanceof ReloadRequestError) {
throw error;
}
throw new ReloadRequestError("Invalid JSON body");
}
}
function createErrorResponse(error, request) {
return new Response(
JSON.stringify({
status: "error",
message: error.message,
}),
{
status: error.status ?? 500,
headers: globalHeaders(request),
}
);
}
export async function handleReloadPage(request, env) {
let body;
try {
body = await parseReloadRequest(request);
} catch (error) {
return createErrorResponse(error, request);
}
const notionId = body?.notionId ?? null;
await importNotionNavigation(env);
const pages = await importNotionPages(env, notionId);
const images = await importImages(env);
await handleBlurHashUpdate(env);
return new Response(
JSON.stringify({
status: "OK",
content: { pages, images },
}),
{
headers: globalHeaders(request),
}
);
}
export async function handleReloadBlog(request, env) {
let body;
try {
body = await parseReloadRequest(request);
} catch (error) {
return createErrorResponse(error, request);
}
const notionId = body?.notionId ?? null;
await importNotionNavigation(env);
const blogs = await importNotionBlogs(env, notionId);
const images = await importImages(env);
const files = await importFiles(env);
const videos = await importVideos(env);
await handleBlurHashUpdate(env);
return new Response(
JSON.stringify({
status: "OK",
content: { blogs, images, files, videos },
}),
{
headers: globalHeaders(request),
}
);
}
export async function handleReloadProject(request, env) {
let body;
try {
body = await parseReloadRequest(request);
} catch (error) {
return createErrorResponse(error, request);
}
const notionId = body?.notionId ?? null;
await importNotionNavigation(env);
const projects = await importNotionProjects(env, notionId);
const images = await importImages(env);
const files = await importFiles(env);
const videos = await importVideos(env);
await handleBlurHashUpdate(env);
return new Response(
JSON.stringify({
status: "OK",
content: { projects, images, files, videos },
}),
{
headers: globalHeaders(request),
}
);
}
export async function handleReloadExperience(request, env) {
let body;
try {
body = await parseReloadRequest(request);
} catch (error) {
return createErrorResponse(error, request);
}
const notionId = body?.notionId ?? null;
const positions = await importNotionPositions(env);
const companies = await importNotionCompanies(env, notionId);
const images = await importImages(env);
await handleBlurHashUpdate(env);
const files = await importFiles(env);
const videos = await importVideos(env);
return new Response(
JSON.stringify({
status: "OK",
content: { positions, companies, images, files, videos },
}),
{
headers: globalHeaders(request),
}
);
}

View File

@ -1,6 +1,32 @@
import { env } from "cloudflare:workers";
export const globalHeaders = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": env.CORS_ORIGIN,
const allowedOrigins = Array.isArray(env.CORS_ORIGIN)
? env.CORS_ORIGIN
: env.CORS_ORIGIN.split(",");
const resolveOrigin = (request) => {
const requestOrigin = request.headers.get("Origin") || "dev.tombutcher.work";
if (allowedOrigins.length === 0 || allowedOrigins.includes("*")) {
return "*";
}
const requestOriginWithProtocol = requestOrigin.startsWith("https://")
? requestOrigin
: "https://" + requestOrigin;
if (
requestOriginWithProtocol &&
allowedOrigins.includes(requestOriginWithProtocol)
) {
return requestOriginWithProtocol;
}
return allowedOrigins[0];
};
export const globalHeaders = (request) => ({
"Content-Type": "application/json",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Origin": resolveOrigin(request),
});

View File

@ -169,10 +169,7 @@ export async function transformNotionBlog(env, notionBlog) {
const published = properties["Published"].checkbox || false;
const dateRaw = properties["Date"].date;
const date = dateRaw?.start
? dayjs(dateRaw.start).format("DD/MM/YY h:mma")
: null;
const date = properties["Date"]?.date?.start || null;
// Extract image URLs (handle both external and file images)
const images = (properties["Images"]?.files || [])

View File

@ -302,7 +302,7 @@ export function collectImageUrls(contentObjects) {
return imageUrls;
}
export async function handleBlurhashUpdate(request, env) {
export async function handleBlurHashUpdate(env) {
// Read the image cache
const cachedImages = await env.CONTENT_KV.get(env.IMAGES_KEY, {
type: "json",

View File

@ -89,6 +89,10 @@ export async function storePages(env, pages) {
export async function transformPageData(env, notionPage, settingsData) {
const navigationItems = await getNavigation(env);
console.log(
"Navigation items:",
navigationItems.map((item) => item.notionId)
);
const icon = getItemByNotionId(navigationItems, notionPage.id).icon;

View File

@ -140,7 +140,7 @@ export async function transformNotionPosition(env, notionPosition) {
}
// Extract duration
const duration = properties["Duration"].date;
const duration = properties["Duration"]?.date || null;
// Extract name from title
const name = properties.Name?.title?.[0]?.plain_text || "Untitled";

View File

@ -166,8 +166,7 @@ export async function transformNotionProject(env, notionProject, settingsData) {
const status = toCamelCase(properties["Status"]?.status?.name || null);
const dateRaw = properties["Date"]?.date;
const date = dateRaw?.start ? dayjs(dateRaw.start).format("DD/MM/YY") : null;
const date = properties["Date"]?.date?.start || null;
// Extract image URL (handle both external and file images)
const imageFile = notionProject?.cover;

View File

@ -4,13 +4,19 @@
*/
{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "tombutcher-api",
"name": "tombutcher-api-2026",
"main": "src/index.js",
"compatibility_date": "2025-02-24",
"observability": {
"enabled": true,
"head_sampling_rate": 1
},
"routes": [
{
"pattern": "api2026.tombutcher.work",
"custom_domain": true
}
],
/**
* Smart Placement
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
@ -24,6 +30,9 @@
* https://developers.cloudflare.com/workers/runtime-apis/bindings/
*/
"triggers": {
"crons": ["*/5 * * * *"]
},
/**
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
@ -38,30 +47,35 @@
"PROJECTS_DB": "297dd26d-60b6-8031-9de2-000bb1de652b",
"COMPANIES_DB": "29fdd26d-60b6-80e8-8b70-000b0c24c7b7",
"POSITIONS_DB": "29fdd26d-60b6-8018-9fd5-000bdce63f48",
"DEVELOPERS_DB": "2a6dd26d-60b6-8041-9dc3-000bf09477ed",
"CV_DB": "2a3dd26d-60b6-8098-85c0-000b12e171c4",
"BLOGS_KEY": "th-blogs-cache",
"COMPANIES_KEY": "th-companies-cache",
"POSITIONS_KEY": "th-positions-cache",
"NAVIGATION_KEY": "th-navigation-cache",
"PAGES_KEY": "th-pages-cache",
"IMAGES_KEY": "th-images-cache",
"BOOKINGS_KEY": "th-bookings-cache",
"PROJECTS_KEY": "th-projects-cache",
"VIDEOS_KEY": "th-videos-cache",
"FILES_KEY": "th-files-cache",
"PROPERTIES_KEY": "th-properties-cache",
"SETTINGS_KEY": "th-settings-cache",
"CV_KEY": "th-cv-cache",
"BLOGS_KEY": "tb-blogs-cache",
"COMPANIES_KEY": "tb-companies-cache",
"POSITIONS_KEY": "tb-positions-cache",
"NAVIGATION_KEY": "tb-navigation-cache",
"PAGES_KEY": "tb-pages-cache",
"IMAGES_KEY": "tb-images-cache",
"BOOKINGS_KEY": "tb-bookings-cache",
"PROJECTS_KEY": "tb-projects-cache",
"VIDEOS_KEY": "tb-videos-cache",
"FILES_KEY": "tb-files-cache",
"PROPERTIES_KEY": "tb-properties-cache",
"SETTINGS_KEY": "tb-settings-cache",
"CV_KEY": "tb-cv-cache",
"CACHE_URL": "https://api.tombutcherltd.com/cache",
"R2_PUBLIC_URL": "https://cdn2026.tombutcher.work",
"BLUR_HASH": "true",
"CORS_ORIGIN": "https://tombutcher.work"
"BLUR_HASH": "false",
"CORS_ORIGIN": [
"https://tombutcher.work",
"https://2026.tombutcher.work",
"https://api2026.tombutcher.work"
]
},
"kv_namespaces": [
{
"binding": "CONTENT_KV", // the variable youll use in the Worker
"id": "05c5283d5b74488da7f90297ea350f9c" // ID from Cloudflare dashboard
"id": "1cffbf98b6e24fd3a58d5f484ecb12ef" // ID from Cloudflare dashboard
}
],
"r2_buckets": [
@ -79,7 +93,7 @@
"kv_namespaces": [
{
"binding": "CONTENT_KV", // the variable youll use in the Worker
"id": "05c5283d5b74488da7f90297ea350f9c" // ID from Cloudflare dashboard
"id": "1cffbf98b6e24fd3a58d5f484ecb12ef" // ID from Cloudflare dashboard
}
],
"r2_buckets": [
@ -92,26 +106,38 @@
"binding": "IMAGES" // i.e. available in your Worker on env.IMAGES
},
"vars": {
"THEMES_DB": "26d4d3a4-6a6f-8052-966a-000bc1c836c1",
"GLOBAL_THEMES_DB": "26d4d3a4-6a6f-80a8-b5f8-000b1b040695",
"PAGES_DB": "26d4d3a4-6a6f-80f1-a0fc-000b3c4c5013",
"PROPERTIES_DB": "26e4d3a4-6a6f-80b8-ac80-000b7b236ebc",
"BOOKINGS_DB": "26e4d3a4-6a6f-80fe-960a-000be56680fd",
"GUESTS_DB": "2764d3a4-6a6f-808b-af87-000b072894d7",
"MESSAGES_DB": "2754d3a4-6a6f-80b5-8f8e-000b2ab6ae7a",
"REDIRECTS_DB": "2754d3a4-6a6f-80ca-b30f-000b828310d2",
"BRANDING_DB": "2764d3a4-6a6f-80f6-8354-000bf2304e00",
"NAVIGATION_KEY": "th-navigation-cache",
"PAGES_KEY": "th-pages-cache",
"IMAGES_KEY": "th-images-cache",
"BOOKINGS_KEY": "th-bookings-cache",
"GUESTS_KEY": "th-guests-cache",
"PROPERTIES_KEY": "th-properties-cache",
"SETTINGS_KEY": "th-settings-cache",
"CACHE_URL": "https://api.tombutcherltd.com/cache",
"THEMES_DB": "289dd26d-60b6-8195-843b-000b65b97c0a",
"GLOBAL_THEMES_DB": "289dd26d-60b6-81d8-8823-000b24e2a783",
"PAGES_DB": "289dd26d-60b6-81d0-b598-000becb15373",
"BLOGS_DB": "289dd26d-60b6-811d-9bdb-000bc5557136",
"MESSAGES_DB": "289dd26d-60b6-818e-9f49-000b5fb2ba34",
"REDIRECTS_DB": "289dd26d-60b6-817c-9f97-000ba041fcfb",
"PROJECTS_DB": "297dd26d-60b6-8031-9de2-000bb1de652b",
"COMPANIES_DB": "29fdd26d-60b6-80e8-8b70-000b0c24c7b7",
"POSITIONS_DB": "29fdd26d-60b6-8018-9fd5-000bdce63f48",
"DEVELOPERS_DB": "2a6dd26d-60b6-8041-9dc3-000bf09477ed",
"CV_DB": "2a3dd26d-60b6-8098-85c0-000b12e171c4",
"BLOGS_KEY": "tb-blogs-cache",
"COMPANIES_KEY": "tb-companies-cache",
"POSITIONS_KEY": "tb-positions-cache",
"NAVIGATION_KEY": "tb-navigation-cache",
"PAGES_KEY": "tb-pages-cache",
"IMAGES_KEY": "tb-images-cache",
"BOOKINGS_KEY": "tb-bookings-cache",
"PROJECTS_KEY": "tb-projects-cache",
"VIDEOS_KEY": "tb-videos-cache",
"FILES_KEY": "tb-files-cache",
"PROPERTIES_KEY": "tb-properties-cache",
"SETTINGS_KEY": "tb-settings-cache",
"CV_KEY": "tb-cv-cache",
"CACHE_URL": "https://api2026.tombutcher.work/cache",
"R2_PUBLIC_URL": "https://cdn2026.tombutcher.work",
"BLUR_HASH": "true",
"CORS_ORIGIN": "https://tombutcher.work"
"CORS_ORIGIN": [
"https://tombutcher.work",
"https://2026.tombutcher.work",
"https://api2026.tombutcher.work"
]
}
}
},

1509
yarn.lock

File diff suppressed because it is too large Load Diff