Initial source commit

This commit is contained in:
Tom Butcher 2025-03-25 21:25:33 +00:00
parent 6eecd3c414
commit ec57d8f7c4
13 changed files with 1042 additions and 91 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
.DS_STORE
# Logs
logs

324
package-lock.json generated
View File

@ -7,6 +7,9 @@
"": {
"name": "web-tombutcher-work",
"version": "0.0.0",
"dependencies": {
"@notionhq/client": "^2.2.16"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.6.4",
"vitest": "~2.1.9",
@ -1027,6 +1030,19 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@notionhq/client": {
"version": "2.2.16",
"resolved": "https://registry.npmjs.org/@notionhq/client/-/client-2.2.16.tgz",
"integrity": "sha512-3GlkfhLw8+Jw8U2iFEmHA6WfCgYhZCXLxgPdqDJkYMFotELNpQO+yGSy2QWURsG8ndu21sLt+FEOfDbNcCtFMg==",
"license": "MIT",
"dependencies": {
"@types/node-fetch": "^2.5.10",
"node-fetch": "^2.6.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.9.tgz",
@ -1300,6 +1316,25 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.13.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.9.tgz",
"integrity": "sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
}
},
"node_modules/@types/node-fetch": {
"version": "2.6.12",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
"license": "MIT",
"dependencies": {
"@types/node": "*",
"form-data": "^4.0.0"
}
},
"node_modules/@vitest/expect": {
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz",
@ -1456,6 +1491,12 @@
"node": ">=12"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/birpc": {
"version": "0.2.14",
"resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.14.tgz",
@ -1483,6 +1524,19 @@
"node": ">=8"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/chai": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz",
@ -1566,6 +1620,18 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/confbox": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz",
@ -1625,6 +1691,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
@ -1643,6 +1718,38 @@
"dev": true,
"license": "MIT"
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
@ -1650,6 +1757,33 @@
"dev": true,
"license": "MIT"
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
@ -1734,6 +1868,21 @@
"node": ">=12.0.0"
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -1749,6 +1898,52 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/get-source": {
"version": "2.0.12",
"resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
@ -1767,6 +1962,57 @@
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
@ -1792,6 +2038,15 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
@ -1805,6 +2060,27 @@
"node": ">=10.0.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/miniflare": {
"version": "3.20250204.1",
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250204.1.tgz",
@ -1897,6 +2173,26 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/ohash": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.5.tgz",
@ -2266,6 +2562,12 @@
"node": ">=14.0.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -2294,6 +2596,12 @@
"node": ">=14.0"
}
},
"node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"license": "MIT"
},
"node_modules/unenv": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.1.tgz",
@ -2870,6 +3178,22 @@
}
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause"
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"license": "MIT",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",

View File

@ -12,5 +12,8 @@
"@cloudflare/vitest-pool-workers": "^0.6.4",
"vitest": "~2.1.9",
"wrangler": "^3.111.0"
},
"dependencies": {
"@notionhq/client": "^2.2.16"
}
}

View File

@ -1,15 +1,33 @@
/**
* Welcome to Cloudflare Workers! This is your first worker.
*
* - Run `npm run dev` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run `npm run deploy` to publish your worker
*
* Learn more at https://developers.cloudflare.com/workers/
*/
import { handleContactRequest } from './routes/contact.js';
import { handleSocialsRequest } from './routes/socials.js';
import { handleBlogsListRequest, handleBlogsViewRequest } from './routes/blogs.js';
import { handleFlushCacheRequest } from './routes/utils.js';
export default {
async fetch(request, env, ctx) {
return new Response('Hello World!');
},
};
async function handleRequest(request) {
if (request.method === 'POST' && request.url.split('?')[0].endsWith('/api/contact')) {
return handleContactRequest(request);
}
if (request.method === 'GET' && request.url.split('?')[0].endsWith('/api/list/socials')) {
return handleSocialsRequest(request);
}
if (request.method === 'GET' && request.url.split('?')[0].endsWith('/api/list/blogs')) {
return handleBlogsListRequest(request);
}
if (request.method === 'GET' && request.url.split('?')[0].endsWith('/api/view/blog')) {
return handleBlogsViewRequest(request);
}
if (request.method === 'POST' && request.url.split('?')[0].endsWith('/api/utils/cache')) {
return handleFlushCacheRequest(request);
}
// Return 404 if the route is not found
return new Response('Not Found', { status: 404 });
}
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request));
});

152
src/routes/blogs.js Normal file
View File

@ -0,0 +1,152 @@
import { getNotionDatabaseWithCache, getNotionBlocksWithCache } from '../utils/notion.js';
import { generateBlockHTML } from '../utils/htmlgen.js';
const blogsDB = process.env.BLOGS_DB;
export async function handleBlogsListRequest(request, env) {
console.log('Listing blogs...');
try {
// Parse URL to get query parameters
const url = new URL(request.url);
const filter = url.searchParams.get('f') || '';
let queryParam = {
filter: {
property: 'Status',
status: {
equals: 'Published', // Only show published articles
},
},
};
// Get blogs from Notion (with caching)
const blogsData = await getNotionDatabaseWithCache(blogsDB, queryParam);
if (blogsData.length <= 0) {
console.error('Invalid filter.');
return new Response(
JSON.stringify({
error: 'Invalid filter.',
}),
{
status: 404,
headers: { 'Content-Type': 'application/json' },
},
);
}
var blogsResponse = [];
for (const blog of blogsData) {
const blogTags = blog.properties['Tags'].multi_select.map((item) => item.name);
const blogObject = {
title: blog.properties['Title'].title[0].plain_text,
slug: blog.properties['Slug'].formula.string,
description: blog.properties['Description'].rich_text[0].plain_text,
author: blog.properties['Author'].created_by.name,
last_edited: blog.properties['Last edited'].last_edited_time,
tags: blogTags,
};
if (filter !== '') {
const filters = filter.split(',');
if (filters.some((tag) => blogTags.includes(tag))) {
blogsResponse.push(blogObject);
}
} else {
blogsResponse.push(blogObject);
}
}
// Return the processed data
console.log('Finished listing blogs.');
return new Response(JSON.stringify(blogsResponse), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*', // Enable CORS
'Cache-Control': 'public, max-age=600', // 10 minute browser cache
},
});
} catch (error) {
console.error('Error handling blogs request:', error);
return new Response(
JSON.stringify({
error: 'Failed to retrieve blogs',
message: error.message,
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
);
}
}
export async function handleBlogsViewRequest(request, env) {
console.log('Viewing blog...');
try {
// Parse URL to get query parameters
const url = new URL(request.url);
const slug = url.searchParams.get('b') || '';
let queryParam = {
filter: {
property: 'Status',
status: {
equals: 'Published', // Only show published articles
},
},
};
// Get blogs from Notion (with caching)
const blogsData = await getNotionDatabaseWithCache(blogsDB, queryParam);
const blog = blogsData.find((blog) => blog.properties?.['Slug']?.formula?.string == slug) || null;
if (blog == null) {
console.error('Invalid not found.');
return new Response(
JSON.stringify({
error: 'Invalid not found.',
}),
{
status: 404,
headers: { 'Content-Type': 'application/json' },
},
);
}
const blogContent = await getNotionBlocksWithCache(blog.id, queryParam);
const blogTags = blog.properties['Tags'].multi_select.map((item) => item.name);
var blogResponse = {
title: blog.properties['Title'].title[0].plain_text,
slug: blog.properties['Slug'].formula.string,
description: blog.properties['Description'].rich_text[0].plain_text,
author: blog.properties['Author'].created_by.name,
last_edited: blog.properties['Last edited'].last_edited_time,
tags: blogTags,
content: await generateBlockHTML(blogContent),
};
// Return the processed data
console.log('Finished listing blogs.');
return new Response(JSON.stringify(blogResponse), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*', // Enable CORS
'Cache-Control': 'public, max-age=600', // 10 minute browser cache
},
});
} catch (error) {
console.error('Error handling blogs request:', error);
return new Response(
JSON.stringify({
error: 'Failed to retrieve blogs',
message: error.message,
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
);
}
}

View File

@ -1,7 +1,9 @@
import { getLocation } from '../utils/geolocation.js';
import { addToNotionDatabase } from '../utils/notion.js';
import { addToNotionDatabase, getNotionDatabaseWithCache } from '../utils/notion.js';
const TURNSTILE_SECRET_KEY = 'your-turnstile-secret-key';
const TURNSTILE_SECRET_KEY = process.env.TURNSTILE_AUTH;
const TURNSTILE_ENABLED = true;
export async function handleContactRequest(request) {
const { email, token } = await request.json();
@ -15,7 +17,9 @@ export async function handleContactRequest(request) {
// Get location info
const location = await getLocation(ip);
const locationString = location.city + ' - ' + location.country;
if (TURNSTILE_ENABLED) {
// Verify the Turnstile token
const verificationResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
method: 'POST',
@ -31,10 +35,11 @@ export async function handleContactRequest(request) {
if (!verificationData.success) {
return new Response('Turnstile verification failed', { status: 400 });
}
}
// Add the email and location to Notion
try {
await addToNotionDatabase(email, location);
await addToNotionDatabase({ Name: email.split('@')[0], Email: email, Location: locationString }, '1abdd26d60b68076a886fb0525ff0a4f');
return new Response(JSON.stringify({ success: true, message: 'Email processed and added to Notion' }), {
headers: { 'Content-Type': 'application/json' },
});

View File

@ -0,0 +1,77 @@
import { getNotionDatabaseWithCache } from '../utils/notion.js';
const socialsDB = process.env.SOCIALS_DB;
const referrersDB = process.env.REFERRERS_DB;
export async function handleSocialsRequest(request, env) {
console.log('Listing socials...');
try {
// Parse URL to get query parameters
const url = new URL(request.url);
const referrer = url.searchParams.get('r') || 'unknown';
console.log('Referrer:', referrer);
// Get social links from Notion (with caching)
const socialsData = await getNotionDatabaseWithCache(socialsDB);
// Get referrers from Notion (with caching)
const referrersData = await getNotionDatabaseWithCache(referrersDB, {
filter: {
property: 'Slug',
rich_text: {
equals: referrer,
},
},
});
if (referrersData.length <= 0) {
console.error('Invalid referrer.');
return new Response(
JSON.stringify({
error: 'Invalid referrer.',
}),
{
status: 404,
headers: { 'Content-Type': 'application/json' },
},
);
}
const referrerObject = referrersData[0];
var socialsResponse = [];
// Loop through each relation
for (const relation of referrerObject.properties['Social Media'].relation) {
console.log('Social ID:', relation.id);
const socialMedia = Object.fromEntries(socialsData.map((social) => [social.id, social]))[relation.id];
const socialsResponseObject = {
name: socialMedia.properties['Name'].title[0].plain_text,
icon: socialMedia.properties['Icon'].rich_text[0].plain_text,
url: socialMedia.properties['Link'].url,
};
socialsResponse.push(socialsResponseObject);
}
// Return the processed data
console.log('Finished listing socials.');
return new Response(JSON.stringify(socialsResponse), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*', // Enable CORS
'Cache-Control': 'public, max-age=600', // 10 minute browser cache
},
});
} catch (error) {
console.error('Error handling socials request:', error);
return new Response(
JSON.stringify({
error: 'Failed to retrieve social links',
message: error.message,
}),
{
status: 500,
headers: { 'Content-Type': 'application/json' },
},
);
}
}

15
src/routes/utils.js Normal file
View File

@ -0,0 +1,15 @@
import { flushCache } from '../utils/notion.js';
export async function handleFlushCacheRequest(request) {
await flushCache();
const response = {
result: 'ok',
};
return new Response(JSON.stringify(response), {
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*', // Enable CORS
'Cache-Control': 'public, max-age=600', // 10 minute browser cache
},
});
}

View File

@ -1,4 +1,4 @@
const GEOLOCATION_API_KEY = 'your-geolocation-api-key';
const GEOLOCATION_API_KEY = '1489c8e06deb4693bf31733a18fe2351';
export async function getLocation(ip) {
const geoApiUrl = `https://api.ipgeolocation.io/ipgeo?apiKey=${GEOLOCATION_API_KEY}&ip=${ip}`;

161
src/utils/htmlgen.js Normal file
View File

@ -0,0 +1,161 @@
import { getNotionBlocksWithCache, getNotionSVGIcon } from './notion';
function richTextToHTML(richText) {
return richText
.map((item) => {
let text = item.plain_text;
let { bold, italic, strikethrough, underline, code, color } = item.annotations;
if (item.href) text = `<a href="${item.href}">${text}</a>`;
if (bold) text = `<strong>${text}</strong>`;
if (italic) text = `<em>${text}</em>`;
if (strikethrough) text = `<s>${text}</s>`;
if (underline) text = `<u>${text}</u>`;
if (code) text = `<code>${text}</code>`;
if (item.href) text = `<a href="${item.href}">${text}</a>`;
if (color && color !== 'default') text = `<span style="color:${color};">${text}</span>`;
return text;
})
.join('');
}
export async function generateBlockHTML(blocksContent) {
var blogContentHTML = '';
var olListMode = false;
var ulListMode = false;
var tableCellTag = 'td';
var index = 0;
for (const block of blocksContent) {
if (olListMode == false && block.type == 'numbered_list_item') {
blogContentHTML += `<ol>`;
olListMode = true;
} else if (olListMode == true && block.type != 'numbered_list_item') {
blogContentHTML += `</ol>`;
olListMode = false;
}
if (ulListMode == false && block.type == 'bulleted_list_item') {
blogContentHTML += `<ul>`;
ulListMode = true;
} else if (ulListMode == true && block.type != 'bulleted_list_item') {
blogContentHTML += `</ul>`;
ulListMode = false;
}
var children = '';
if (block.has_children == true) {
const getChildren = async (id) => {
console.log('Getting child:', id);
const childrenBlocksContent = await getNotionBlocksWithCache(id);
children += await generateBlockHTML(childrenBlocksContent);
};
await getChildren(block.id);
}
switch (block.type) {
case 'paragraph':
blogContentHTML += `<p>${richTextToHTML(block['paragraph'].rich_text)}</p>\n`;
break;
case 'heading_1':
blogContentHTML += `<h1>${richTextToHTML(block['heading_1'].rich_text)}</h1>\n`;
break;
case 'heading_2':
blogContentHTML += `<h2>${richTextToHTML(block['heading_2'].rich_text)}</h2>\n`;
break;
case 'heading_3':
blogContentHTML += `<h3>${richTextToHTML(block['heading_3'].rich_text)}</h3>\n`;
break;
case 'numbered_list_item':
blogContentHTML += `<li>${richTextToHTML(block['numbered_list_item'].rich_text)}\n${children}</li>\n`;
break;
case 'bulleted_list_item':
blogContentHTML += `<li>${richTextToHTML(block['bulleted_list_item'].rich_text)}\n${children}</li>\n`;
break;
case 'callout':
var calloutIcon = '';
if (block['callout'].icon != null) {
if (block['callout'].icon.type == 'emoji') {
calloutIcon = `<i class='tbemoji'>${block['callout'].icon.emoji}</i>`;
}
if (block['callout'].icon.type == 'file') {
calloutIcon = `<img class='tbicon' src='${block['callout'].icon.file.url}'></img>`;
}
if (block['callout'].icon.type == 'external') {
const svgIcon = await getNotionSVGIcon(block['callout'].icon.external.url);
calloutIcon = `<div class='tbicon'>${svgIcon}</div>`;
}
}
var richText = '';
if (block['callout'].rich_text.length != 0) {
richText = `<p>${richTextToHTML(block['callout'].rich_text)}</p>`;
}
blogContentHTML += `<div class='tbcallout'">${calloutIcon}<div class='tbcalloutcontent'>${richText}\n${children}\n</div></div>\n`;
break;
case 'divider':
blogContentHTML += `<hr>\n`;
break;
case 'quote':
blogContentHTML += `<p class='tbquote'>${richTextToHTML(block['quote'].rich_text)}</p>\n`;
break;
case 'image':
var imageSrc = '';
if (block['image'].type == 'external') {
imageSrc = block['image'].external.url;
}
if (block['image'].type == 'file') {
imageSrc = block['image'].file.url;
}
var imageCaption = '';
if (block['image'].caption.length != 0) {
imageCaption = `<p class='tbimagecaption'>${richTextToHTML(block['image'].caption)}</p>`;
}
blogContentHTML += `<div class='tbimage'><img src='${imageSrc}'></img>${imageCaption}</div>\n`;
break;
case 'column_list':
blogContentHTML += `<div class='tbcolumnlist'>${children}</div>\n`;
break;
case 'column':
blogContentHTML += `<div class='tbcolumn'>${children}</div>\n`;
break;
case 'table':
var tableClass = 'tbtable ';
if (block['table'].has_row_header == true) {
tableClass += 'tbrowheader ';
}
if (block['table'].has_column_header == true) {
tableCellTag = 'th';
tableClass += 'tbcolumnheader';
}
blogContentHTML += `<table class='${tableClass}'>${children}</table>\n`;
break;
case 'table_row':
blogContentHTML += `<tr>\n`;
block['table_row'].cells.forEach((cell) => {
blogContentHTML += `<${tableCellTag}>${richTextToHTML(cell)}</${tableCellTag}>\n`;
});
if (tableCellTag == 'th') {
tableCellTag = 'td';
}
blogContentHTML += `</tr>\n`;
break;
default:
console.log('Unhandled type:', block.type);
}
if (blocksContent.length - 1 == index) {
if (olListMode && block.type == 'numbered_list_item') {
blogContentHTML += `</ol>\n`;
}
if (ulListMode && block.type == 'bulleted_list_item') {
blogContentHTML += `</ul>\n`;
}
}
index += 1;
}
return blogContentHTML;
}

View File

@ -1,18 +1,73 @@
const NOTION_API_TOKEN = 'your-notion-api-token';
const { Client } = require('@notionhq/client');
export async function addToNotionDatabase(params, database) {
const notionApiUrl = `https://api.notion.com/v1/pages`;
let cacheVersion;
const iconColorSubstitutions = [
['#D44C47', '#FF453A'],
['#55534E', '#FFFFFF'],
['#448361', '#32D74B'],
['#337ea9', '#0A84FF'],
['#9065B0', '#BF5AF2'],
['#CB912F', '#FFD60A'],
['#C14C8A', '#FF375F'],
['#d9730d', '#FF9F0A'],
];
const notionPayload = {
parent: {
database_id: database,
},
properties: {},
};
flushCache();
// Initialize Notion client with API token
const notion = new Client({
auth: process.env.NOTION_AUTH, // Use the API token stored in environment variables
});
function fnv1aHash(str) {
let hash = 0x811c9dc5;
for (let i = 0; i < str.length; i++) {
hash ^= str.charCodeAt(i);
hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
return (hash >>> 0).toString(16);
}
export async function flushCache() {
cacheVersion = Math.random().toString(36).substring(2, 10);
console.log('New cache version:', cacheVersion);
}
export async function addToNotionDatabase(params, databaseId) {
console.log('Adding to Notion DB...');
// Construct the properties object for the page
const properties = {};
// Loop through each key-value pair in the params object
for (const [key, value] of Object.entries(params)) {
notionPayload.properties[key] = {
// Handling specific property types based on the key
if (key === 'Email') {
// Expecting the "Email" property to be of type "email"
properties[key] = {
email: value,
};
} else if (key === 'Location') {
// Expecting the "Location" property to be of type "select"
properties[key] = {
select: {
name: value, // Assume value is a valid option in the "Location" select dropdown
},
};
} else if (key === 'Name') {
// Expecting the "Name" property to be of type "title"
properties[key] = {
title: [
{
text: {
content: value,
},
},
],
};
} else {
// Default to "rich_text" for any other fields
properties[key] = {
rich_text: [
{
text: {
@ -22,20 +77,155 @@ export async function addToNotionDatabase(params, database) {
],
};
}
}
const response = await fetch(notionApiUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${NOTION_API_TOKEN}`,
'Content-Type': 'application/json',
'Notion-Version': '2021-05-13',
},
body: JSON.stringify(notionPayload),
try {
// Create the page in the Notion database
const response = await notion.pages.create({
parent: { database_id: databaseId },
properties: properties,
});
if (!response.ok) {
console.log('Added to Notion DB!');
return response;
} catch (error) {
console.error('Failed to add to Notion:', error);
throw new Error('Failed to add to Notion');
}
return response.json();
}
export async function getNotionDatabaseWithCache(databaseId, queryParams = {}, cacheTtl = 60) {
// Unique cache key based on database ID and query parameters
const queryHash = fnv1aHash(JSON.stringify(queryParams));
const cacheKey = `https://cache.api/notion-db-${databaseId}-${queryHash}-${cacheVersion}`;
// Try to get data from cache first
const cache = caches.default;
let response = await cache.match(cacheKey);
if (response) {
// Cache hit - return the cached data
const data = await response.json();
console.log(`Cache hit for database ${databaseId} query ${queryHash}`);
return data;
}
// Cache miss - fetch from Notion API
console.log(`Cache miss for database ${databaseId} query ${queryHash}, fetching from Notion API`);
try {
// Query the database
const result = await notion.databases.query({
database_id: databaseId,
...queryParams,
});
// Extract the results
const data = result.results;
// Store in cache
const newResponse = new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': `max-age=${cacheTtl}`,
},
});
await cache.put(cacheKey, newResponse);
console.log(`Stored database ${databaseId} query ${queryHash} in cache for ${cacheTtl} seconds`);
return data;
} catch (error) {
console.error(`Error fetching Notion database ${databaseId} query ${queryHash}:`, error);
throw error;
}
}
export async function getNotionBlocksWithCache(id, queryParams = {}, cacheTtl = 60) {
// Unique cache key based on ID and query parameters
const queryHash = fnv1aHash(JSON.stringify(queryParams));
const cacheKey = `https://cache.api/notion-page-${id}-${queryHash}-${cacheVersion}`;
// Try to get data from cache first
const cache = caches.default;
let response = await cache.match(cacheKey);
if (response) {
// Cache hit - return the cached data
const data = await response.json();
console.log(`Cache hit for page ${id} query ${queryHash}`);
return data;
}
// Cache miss - fetch from Notion API
console.log(`Cache miss for page ${id} query ${queryHash}, fetching from Notion API`);
try {
// Query notion blocks
const result = await notion.blocks.children.list({
block_id: id,
page_size: 50,
});
// Extract the results
const data = result.results;
// Store in cache
const newResponse = new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': `max-age=${cacheTtl}`,
},
});
await cache.put(cacheKey, newResponse);
console.log(`Stored page ${id} query ${queryHash} in cache for ${cacheTtl} seconds`);
return data;
} catch (error) {
console.error(`Error fetching Notion page ${id} query ${queryHash}:`, error);
throw error;
}
}
export async function getNotionSVGIcon(url) {
try {
// Unique cache key based on URL hash
const urlHash = fnv1aHash(url);
const cacheKey = `https://cache.api/notion-icon-${urlHash}-${cacheVersion}`;
// Try to get data from cache first
const cache = caches.default;
const cachedResponse = await cache.match(cacheKey);
console.log(cachedResponse);
if (cachedResponse) {
const svgText = await cachedResponse.text();
console.log(`Cache hit for icon ${urlHash}`);
return svgText;
}
// Cache miss - fetch from Notion API
console.log(`Cache miss for icon ${urlHash}, fetching from Notion`);
const response = await fetch(url, { method: 'GET' });
if (!response.ok) throw new Error(`Failed to fetch SVG from Notion: ${response.statusText}`);
let svgText = await response.text();
// Replace colors using direct string replacement
iconColorSubstitutions.forEach(([searchColor, replaceColor]) => {
svgText = svgText.replaceAll(searchColor, replaceColor);
});
const newResponse = new Response(svgText, {
headers: {
'Content-Type': 'application/svg+xml',
'Cache-Control': 'public, max-age=31536000, immutable',
},
});
await cache.put(cacheKey, newResponse);
console.log(`Stored icon ${urlHash} in cache for a year.`);
return svgText;
} catch (error) {
console.error(error);
return null;
}
}

0
src/utils/svgcolormap.js Normal file
View File

View File

@ -8,8 +8,8 @@
"main": "src/index.js",
"compatibility_date": "2025-02-24",
"observability": {
"enabled": true
}
"enabled": true,
},
/**
* Smart Placement
* Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement
@ -27,7 +27,11 @@
* Environment Variables
* https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables
*/
// "vars": { "MY_VARIABLE": "production_value" },
"vars": {
"BLOGS_DB": "1abdd26d60b680d68150f73a53183b6e",
"SOCIALS_DB": "1abdd26d60b6803bb3ddec9b179fa345",
"REFERRERS_DB": "1abdd26d60b680dcb677e18b76d50f26",
},
/**
* Note: Use secrets to store sensitive data.
* https://developers.cloudflare.com/workers/configuration/secrets/