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

View File

@ -2,6 +2,16 @@ import { handleContactRequest } from "./routes/contact.js";
import { handleContentRequest } from "./routes/content.js"; import { handleContentRequest } from "./routes/content.js";
import { handleNotionHook } from "./routes/hooks.js"; import { handleNotionHook } from "./routes/hooks.js";
import { globalHeaders } from "./utils/api.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) { async function handleRequest(request, env) {
if ( if (
@ -12,7 +22,7 @@ async function handleRequest(request, env) {
return new Response(null, { return new Response(null, {
status: 204, // No Content status: 204, // No Content
headers: { headers: {
"Access-Control-Allow-Origin": env.CORS_ORIGIN, ...globalHeaders(request),
"Access-Control-Allow-Methods": "POST, OPTIONS", "Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type", "Access-Control-Allow-Headers": "Content-Type",
}, },
@ -39,18 +49,105 @@ async function handleRequest(request, env) {
return await handleNotionHook(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 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) { async function handleScheduledEvent(event, env) {
console.log("Scheduled event:", event.cron); console.log("Scheduled event:", event.cron);
switch (event.cron) { switch (event.cron) {
case "*/5 * * * *": case "*/5 * * * *":
await updateAllSmoobuData(env); await importFiles(env);
break; await importVideos(env);
case "* * * * *": await importImages(env);
await refreshBookingCache(env); await handleBlurHashUpdate(env);
break; break;
default: default:
break; break;

View File

@ -2,7 +2,12 @@ import { getPages } from "../utils/pages.js";
import { getBlogs } from "../utils/blogs.js"; import { getBlogs } from "../utils/blogs.js";
import { getProjects } from "../utils/projects.js"; import { getProjects } from "../utils/projects.js";
import { getCompanies } from "../utils/companies.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"; import { getCvs } from "../utils/cv.js";
export async function importFiles(env) { export async function importFiles(env) {
@ -21,6 +26,22 @@ export async function importFiles(env) {
...cvs, ...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); const updatedFiles = await updateFiles(env, fileUrls);
console.log("Imported files from Notion."); console.log("Imported files from Notion.");
return updatedFiles; return updatedFiles;

View File

@ -2,7 +2,12 @@ import { getPages } from "../utils/pages.js";
import { getBlogs } from "../utils/blogs.js"; import { getBlogs } from "../utils/blogs.js";
import { getProjects } from "../utils/projects.js"; import { getProjects } from "../utils/projects.js";
import { getCompanies } from "../utils/companies.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) { export async function importImages(env) {
// Fetch caches // Fetch caches
@ -20,6 +25,24 @@ export async function importImages(env) {
...companies, ...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); const updatedImages = await updateImages(env, imageUrls);
console.log("Imported images from Notion."); console.log("Imported images from Notion.");
return updatedImages; return updatedImages;

View File

@ -29,12 +29,25 @@ export async function importNotionProjects(env, notionId = null) {
transformNotionProject(env, project, settingsData) 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 is not null, use lodash unionBy to replace existing Projects
if (notionId !== null) { if (notionId !== null) {
const cachedProjects = await getProjects(env); 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); await storeProjects(env, mergedProjects);
console.log("Imported Projects from Notion and merged with cache."); console.log("Imported Projects from Notion and merged with cache.");
return mergedProjects; return mergedProjects;

View File

@ -2,7 +2,12 @@ import { getPages } from "../utils/pages.js";
import { getBlogs } from "../utils/blogs.js"; import { getBlogs } from "../utils/blogs.js";
import { getProjects } from "../utils/projects.js"; import { getProjects } from "../utils/projects.js";
import { getCompanies } from "../utils/companies.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) { export async function importVideos(env) {
console.log("Importing videos from Notion..."); console.log("Importing videos from Notion...");
@ -18,6 +23,24 @@ export async function importVideos(env) {
...companies, ...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); const updatedVideos = await updateVideos(env, videoUrls);
console.log("Imported videos from Notion."); console.log("Imported videos from Notion.");
return updatedVideos; return updatedVideos;

View File

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

View File

@ -14,8 +14,8 @@ export async function handleContactRequest(request, env) {
}), }),
{ {
status: 400, status: 400,
headers: globalHeaders, headers: globalHeaders(request),
}, }
); );
} }
@ -28,12 +28,12 @@ export async function handleContactRequest(request, env) {
"https://challenges.cloudflare.com/turnstile/v0/siteverify", "https://challenges.cloudflare.com/turnstile/v0/siteverify",
{ {
method: "POST", method: "POST",
headers: globalHeaders, headers: globalHeaders(request),
body: JSON.stringify({ body: JSON.stringify({
secret: env.TURNSTILE_AUTH, secret: env.TURNSTILE_AUTH,
response: token, response: token,
}), }),
}, }
); );
const verificationData = await verificationResponse.json(); const verificationData = await verificationResponse.json();
@ -47,8 +47,8 @@ export async function handleContactRequest(request, env) {
JSON.stringify({ message: errorMessage, code: `captcha-${code}` }), JSON.stringify({ message: errorMessage, code: `captcha-${code}` }),
{ {
status: 400, status: 400,
headers: globalHeaders, headers: globalHeaders(request),
}, }
); );
} }
} }
@ -62,7 +62,7 @@ export async function handleContactRequest(request, env) {
Message: message, Message: message,
["IP Address"]: ip, ["IP Address"]: ip,
}, },
env.MESSAGES_DB, env.MESSAGES_DB
); );
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
@ -70,16 +70,16 @@ export async function handleContactRequest(request, env) {
message: "Email processed and added to Notion", message: "Email processed and added to Notion",
}), }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
}, }
); );
} catch (error) { } catch (error) {
return new Response( return new Response(
JSON.stringify({ message: "Error storing data.", code: "storage-error" }), JSON.stringify({ message: "Error storing data.", code: "storage-error" }),
{ {
status: 500, 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 { importImages } from "../objects/images.js";
import { importNotionCvs } from "../objects/cv.js"; import { importNotionCvs } from "../objects/cv.js";
import { importFiles } from "../objects/files.js"; import { importFiles } from "../objects/files.js";
import { importNotionProjects } from "../objects/projects.js";
import { importVideos } from "../objects/videos.js"; import { importVideos } from "../objects/videos.js";
// Fetch or return cached content // Fetch or return cached content
export async function getCachedContent(env) { export async function getCachedContent(env, request) {
// Try to get combined cached content // Try to get combined cached content
var cachedContent = await getCombinedCachedContent(env); var cachedContent = await getCombinedCachedContent(env);
const noBlogs = cachedContent.blogs?.length === 0; const noBlogs =
const noPages = cachedContent.pages?.length === 0; cachedContent.blogs?.length === 0 || cachedContent.blogs == null;
const noSettings = cachedContent.settings == {}; const noPages =
const noCompanies = cachedContent.companies?.length === 0; cachedContent.pages?.length === 0 || cachedContent.pages == null;
const noCvs = cachedContent.cvs?.length === 0; const noSettings =
const noPositions = cachedContent.positions?.length === 0; 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); await importNotionNavigation(env);
} }
if (noSettings) {
cachedContent.settings = await importNotionSettings(env);
}
if (noBlogs) { if (noBlogs) {
cachedContent.blogs = await importNotionBlogs(env); cachedContent.blogs = await importNotionBlogs(env);
cachedContent.images = await importImages(env); cachedContent.images = await importImages(env);
@ -41,10 +52,6 @@ export async function getCachedContent(env) {
cachedContent.videos = await importVideos(env); cachedContent.videos = await importVideos(env);
} }
if (noSettings) {
cachedContent.settings = await importNotionSettings(env);
}
if (noCvs) { if (noCvs) {
cachedContent.cvs = await importNotionCvs(env); cachedContent.cvs = await importNotionCvs(env);
cachedContent.files = await importFiles(env); cachedContent.files = await importFiles(env);
@ -58,6 +65,13 @@ export async function getCachedContent(env) {
cachedContent.videos = await importVideos(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) { if (cachedContent) {
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
@ -65,17 +79,17 @@ export async function getCachedContent(env) {
blogs: cachedContent.blogs.filter((blog) => blog.published == true), 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) { export async function handleContentRequest(request, env) {
try { try {
return await getCachedContent(env); return await getCachedContent(env, request);
} catch (error) { } catch (error) {
console.error("Error handling content request:", error); console.error("Error handling content request:", error);
return new Response( return new Response(
@ -85,7 +99,7 @@ export async function handleContentRequest(request, env) {
}), }),
{ {
status: 500, status: 500,
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
} }

View File

@ -6,7 +6,6 @@ import { globalHeaders } from "../utils/api";
import { importImages } from "../objects/images"; import { importImages } from "../objects/images";
import { importFiles } from "../objects/files"; import { importFiles } from "../objects/files";
import { importVideos } from "../objects/videos"; import { importVideos } from "../objects/videos";
import { handleBlurhashUpdate } from "../utils/imageCache";
import { importNotionProjects } from "../objects/projects"; import { importNotionProjects } from "../objects/projects";
import { importNotionCompanies } from "../objects/companies"; import { importNotionCompanies } from "../objects/companies";
import { importNotionPositions } from "../objects/positions"; import { importNotionPositions } from "../objects/positions";
@ -33,7 +32,7 @@ export async function handleNotionHook(request, env) {
JSON.stringify({ JSON.stringify({
status: "gotVerificationToken", status: "gotVerificationToken",
}), }),
{ headers: globalHeaders } { headers: globalHeaders(request) }
); );
} }
console.log("Notion hook received:", body.type); console.log("Notion hook received:", body.type);
@ -51,14 +50,13 @@ export async function handleNotionHook(request, env) {
await importNotionNavigation(env); await importNotionNavigation(env);
const pages = await importNotionPages(env, entityId); const pages = await importNotionPages(env, entityId);
const images = await importImages(env); const images = await importImages(env);
await handleBlurhashUpdate(request, env);
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
status: "OK", status: "OK",
content: { pages, images }, content: { pages, images },
}), }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
} }
@ -68,14 +66,13 @@ export async function handleNotionHook(request, env) {
const images = await importImages(env); const images = await importImages(env);
const files = await importFiles(env); const files = await importFiles(env);
const videos = await importVideos(env); const videos = await importVideos(env);
await handleBlurhashUpdate(request, env);
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
status: "OK", status: "OK",
content: { blogs, images, files, videos }, 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 images = await importImages(env);
const files = await importFiles(env); const files = await importFiles(env);
const videos = await importVideos(env); const videos = await importVideos(env);
await handleBlurhashUpdate(request, env);
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
status: "OK", status: "OK",
content: { projects, images, files, videos }, 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 images = await importImages(env);
const files = await importFiles(env); const files = await importFiles(env);
const videos = await importVideos(env); const videos = await importVideos(env);
await handleBlurhashUpdate(request, env);
return new Response( return new Response(
JSON.stringify({ JSON.stringify({
status: "OK", status: "OK",
content: { companies, images, positions, files, videos }, 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 positions = await importNotionPositions(env, entityId);
const companies = await importNotionCompanies(env); const companies = await importNotionCompanies(env);
const images = await importImages(env); const images = await importImages(env);
await handleBlurhashUpdate(request, env);
const files = await importFiles(env); const files = await importFiles(env);
const videos = await importVideos(env); const videos = await importVideos(env);
return new Response( return new Response(
@ -126,7 +120,7 @@ export async function handleNotionHook(request, env) {
content: { positions, companies, images, files, videos }, content: { positions, companies, images, files, videos },
}), }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
} }
@ -136,7 +130,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { cvs, files } }), 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( return new Response(
JSON.stringify({ status: "OK", content: { settings } }), JSON.stringify({ status: "OK", content: { settings } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
} }
@ -154,7 +148,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { redirects } }), JSON.stringify({ status: "OK", content: { redirects } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
} }
@ -163,16 +157,13 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { branding } }), JSON.stringify({ status: "OK", content: { branding } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
} }
console.log("Page Blogs with data source:", dataSourceId); console.log("Page Blogs with data source:", dataSourceId);
} }
if ( if (body.type == "page.deleted" && body.data?.parent?.data_source_id) {
body.type == "page.properties_updated" ||
(body.type == "page.deleted" && body.data?.parent?.data_source_id)
) {
const dataSourceId = body.data.parent.data_source_id; const dataSourceId = body.data.parent.data_source_id;
const entityId = body?.entity?.id || null; const entityId = body?.entity?.id || null;
console.log("Data source ID:", dataSourceId); console.log("Data source ID:", dataSourceId);
@ -185,7 +176,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { pages } }), JSON.stringify({ status: "OK", content: { pages } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
case env.BLOGS_DB: case env.BLOGS_DB:
@ -194,7 +185,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { blogs } }), JSON.stringify({ status: "OK", content: { blogs } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
case env.COMPANIES_DB: case env.COMPANIES_DB:
@ -202,7 +193,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { companies } }), JSON.stringify({ status: "OK", content: { companies } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
case env.POSITIONS_DB: case env.POSITIONS_DB:
@ -211,7 +202,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { positions } }), JSON.stringify({ status: "OK", content: { positions } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
case env.PROJECTS_DB: case env.PROJECTS_DB:
@ -219,7 +210,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { projects } }), JSON.stringify({ status: "OK", content: { projects } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
case env.CV_DB: case env.CV_DB:
@ -227,7 +218,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { cvs } }), JSON.stringify({ status: "OK", content: { cvs } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
case env.THEMES_DB: case env.THEMES_DB:
@ -235,7 +226,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { settings } }), JSON.stringify({ status: "OK", content: { settings } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
case env.REDIRECTS_DB: case env.REDIRECTS_DB:
@ -243,7 +234,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { redirects } }), JSON.stringify({ status: "OK", content: { redirects } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
case env.BRANDING_DB: case env.BRANDING_DB:
@ -251,7 +242,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "OK", content: { branding } }), JSON.stringify({ status: "OK", content: { branding } }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
} }
@ -263,7 +254,7 @@ export async function handleNotionHook(request, env) {
return new Response( return new Response(
JSON.stringify({ status: "Unknown hook", type: body.type }), JSON.stringify({ status: "Unknown hook", type: body.type }),
{ {
headers: globalHeaders, headers: globalHeaders(request),
} }
); );
} catch (error) { } catch (error) {
@ -271,7 +262,7 @@ export async function handleNotionHook(request, env) {
console.log("No body to parse. Calling both fetch requests..."); console.log("No body to parse. Calling both fetch requests...");
const content = await updateAllNotionData(env); const content = await updateAllNotionData(env);
return new Response(JSON.stringify({ status: "OK", content }), { 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"; import { env } from "cloudflare:workers";
export const globalHeaders = { const allowedOrigins = Array.isArray(env.CORS_ORIGIN)
"Content-Type": "application/json", ? env.CORS_ORIGIN
"Access-Control-Allow-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 published = properties["Published"].checkbox || false;
const dateRaw = properties["Date"].date; const date = properties["Date"]?.date?.start || null;
const date = dateRaw?.start
? dayjs(dateRaw.start).format("DD/MM/YY h:mma")
: null;
// Extract image URLs (handle both external and file images) // Extract image URLs (handle both external and file images)
const images = (properties["Images"]?.files || []) const images = (properties["Images"]?.files || [])

View File

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

View File

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

View File

@ -140,7 +140,7 @@ export async function transformNotionPosition(env, notionPosition) {
} }
// Extract duration // Extract duration
const duration = properties["Duration"].date; const duration = properties["Duration"]?.date || null;
// Extract name from title // Extract name from title
const name = properties.Name?.title?.[0]?.plain_text || "Untitled"; 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 status = toCamelCase(properties["Status"]?.status?.name || null);
const dateRaw = properties["Date"]?.date; const date = properties["Date"]?.date?.start || null;
const date = dateRaw?.start ? dayjs(dateRaw.start).format("DD/MM/YY") : null;
// Extract image URL (handle both external and file images) // Extract image URL (handle both external and file images)
const imageFile = notionProject?.cover; const imageFile = notionProject?.cover;

View File

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