From ec57d8f7c4485f4dbfae946b791abc36c01c87a7 Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Tue, 25 Mar 2025 21:25:33 +0000 Subject: [PATCH] Initial source commit --- .gitignore | 2 + package-lock.json | 324 +++++++++++++++++++++++++++++++++++++++ package.json | 3 + src/index.js | 46 ++++-- src/routes/blogs.js | 152 ++++++++++++++++++ src/routes/contact.js | 35 +++-- src/routes/socials.js | 77 ++++++++++ src/routes/utils.js | 15 ++ src/utils/geolocation.js | 2 +- src/utils/htmlgen.js | 161 +++++++++++++++++++ src/utils/notion.js | 248 ++++++++++++++++++++++++++---- src/utils/svgcolormap.js | 0 wrangler.jsonc | 68 ++++---- 13 files changed, 1042 insertions(+), 91 deletions(-) create mode 100644 src/routes/blogs.js create mode 100644 src/routes/utils.js create mode 100644 src/utils/htmlgen.js create mode 100644 src/utils/svgcolormap.js diff --git a/.gitignore b/.gitignore index 3b0fe33..90ee3e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_STORE + # Logs logs diff --git a/package-lock.json b/package-lock.json index 5f0be4d..be38337 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 00d2634..a474dbe 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/index.js b/src/index.js index de66b62..f469372 100644 --- a/src/index.js +++ b/src/index.js @@ -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)); +}); diff --git a/src/routes/blogs.js b/src/routes/blogs.js new file mode 100644 index 0000000..364c135 --- /dev/null +++ b/src/routes/blogs.js @@ -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' }, + }, + ); + } +} diff --git a/src/routes/contact.js b/src/routes/contact.js index cb3e850..7bd340e 100644 --- a/src/routes/contact.js +++ b/src/routes/contact.js @@ -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,26 +17,29 @@ export async function handleContactRequest(request) { // Get location info const location = await getLocation(ip); + const locationString = location.city + ' - ' + location.country; - // Verify the Turnstile token - const verificationResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - secret: TURNSTILE_SECRET_KEY, - response: token, - }), - }); + if (TURNSTILE_ENABLED) { + // Verify the Turnstile token + const verificationResponse = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + secret: TURNSTILE_SECRET_KEY, + response: token, + }), + }); - const verificationData = await verificationResponse.json(); + const verificationData = await verificationResponse.json(); - if (!verificationData.success) { - return new Response('Turnstile verification failed', { status: 400 }); + 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' }, }); diff --git a/src/routes/socials.js b/src/routes/socials.js index e69de29..65887ac 100644 --- a/src/routes/socials.js +++ b/src/routes/socials.js @@ -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' }, + }, + ); + } +} diff --git a/src/routes/utils.js b/src/routes/utils.js new file mode 100644 index 0000000..5ca1e85 --- /dev/null +++ b/src/routes/utils.js @@ -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 + }, + }); +} diff --git a/src/utils/geolocation.js b/src/utils/geolocation.js index 48ba0cc..566e604 100644 --- a/src/utils/geolocation.js +++ b/src/utils/geolocation.js @@ -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}`; diff --git a/src/utils/htmlgen.js b/src/utils/htmlgen.js new file mode 100644 index 0000000..c12c15b --- /dev/null +++ b/src/utils/htmlgen.js @@ -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 = `${text}`; + if (bold) text = `${text}`; + if (italic) text = `${text}`; + if (strikethrough) text = `${text}`; + if (underline) text = `${text}`; + if (code) text = `${text}`; + if (item.href) text = `${text}`; + if (color && color !== 'default') text = `${text}`; + + 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 += `
    `; + olListMode = true; + } else if (olListMode == true && block.type != 'numbered_list_item') { + blogContentHTML += `
`; + olListMode = false; + } + if (ulListMode == false && block.type == 'bulleted_list_item') { + blogContentHTML += ``; + 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 += `

${richTextToHTML(block['paragraph'].rich_text)}

\n`; + break; + case 'heading_1': + blogContentHTML += `

${richTextToHTML(block['heading_1'].rich_text)}

\n`; + break; + case 'heading_2': + blogContentHTML += `

${richTextToHTML(block['heading_2'].rich_text)}

\n`; + break; + case 'heading_3': + blogContentHTML += `

${richTextToHTML(block['heading_3'].rich_text)}

\n`; + break; + case 'numbered_list_item': + blogContentHTML += `
  • ${richTextToHTML(block['numbered_list_item'].rich_text)}\n${children}
  • \n`; + break; + case 'bulleted_list_item': + blogContentHTML += `
  • ${richTextToHTML(block['bulleted_list_item'].rich_text)}\n${children}
  • \n`; + break; + case 'callout': + var calloutIcon = ''; + if (block['callout'].icon != null) { + if (block['callout'].icon.type == 'emoji') { + calloutIcon = `${block['callout'].icon.emoji}`; + } + if (block['callout'].icon.type == 'file') { + calloutIcon = ``; + } + if (block['callout'].icon.type == 'external') { + const svgIcon = await getNotionSVGIcon(block['callout'].icon.external.url); + calloutIcon = `
    ${svgIcon}
    `; + } + } + var richText = ''; + if (block['callout'].rich_text.length != 0) { + richText = `

    ${richTextToHTML(block['callout'].rich_text)}

    `; + } + blogContentHTML += `
    ${calloutIcon}
    ${richText}\n${children}\n
    \n`; + break; + case 'divider': + blogContentHTML += `
    \n`; + break; + case 'quote': + blogContentHTML += `

    ${richTextToHTML(block['quote'].rich_text)}

    \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 = `

    ${richTextToHTML(block['image'].caption)}

    `; + } + blogContentHTML += `
    ${imageCaption}
    \n`; + break; + case 'column_list': + blogContentHTML += `
    ${children}
    \n`; + break; + case 'column': + blogContentHTML += `
    ${children}
    \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 += `${children}
    \n`; + break; + case 'table_row': + blogContentHTML += `\n`; + block['table_row'].cells.forEach((cell) => { + blogContentHTML += `<${tableCellTag}>${richTextToHTML(cell)}\n`; + }); + if (tableCellTag == 'th') { + tableCellTag = 'td'; + } + blogContentHTML += `\n`; + break; + default: + console.log('Unhandled type:', block.type); + } + + if (blocksContent.length - 1 == index) { + if (olListMode && block.type == 'numbered_list_item') { + blogContentHTML += `\n`; + } + if (ulListMode && block.type == 'bulleted_list_item') { + blogContentHTML += `\n`; + } + } + + index += 1; + } + + return blogContentHTML; +} diff --git a/src/utils/notion.js b/src/utils/notion.js index 712de15..07d8a5d 100644 --- a/src/utils/notion.js +++ b/src/utils/notion.js @@ -1,41 +1,231 @@ -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] = { - rich_text: [ - { - text: { - content: value, - }, + // 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: { + content: value, + }, + }, + ], + }; + } } - 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; + } } diff --git a/src/utils/svgcolormap.js b/src/utils/svgcolormap.js new file mode 100644 index 0000000..e69de29 diff --git a/wrangler.jsonc b/wrangler.jsonc index 970db7a..4447fdf 100644 --- a/wrangler.jsonc +++ b/wrangler.jsonc @@ -8,40 +8,44 @@ "main": "src/index.js", "compatibility_date": "2025-02-24", "observability": { - "enabled": true - } - /** - * Smart Placement - * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement - */ - // "placement": { "mode": "smart" }, + "enabled": true, + }, + /** + * Smart Placement + * Docs: https://developers.cloudflare.com/workers/configuration/smart-placement/#smart-placement + */ + // "placement": { "mode": "smart" }, - /** - * Bindings - * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including - * databases, object storage, AI inference, real-time communication and more. - * https://developers.cloudflare.com/workers/runtime-apis/bindings/ - */ + /** + * Bindings + * Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform, including + * databases, object storage, AI inference, real-time communication and more. + * https://developers.cloudflare.com/workers/runtime-apis/bindings/ + */ - /** - * Environment Variables - * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables - */ - // "vars": { "MY_VARIABLE": "production_value" }, - /** - * Note: Use secrets to store sensitive data. - * https://developers.cloudflare.com/workers/configuration/secrets/ - */ + /** + * Environment Variables + * https://developers.cloudflare.com/workers/wrangler/configuration/#environment-variables + */ + "vars": { + "BLOGS_DB": "1abdd26d60b680d68150f73a53183b6e", + "SOCIALS_DB": "1abdd26d60b6803bb3ddec9b179fa345", + "REFERRERS_DB": "1abdd26d60b680dcb677e18b76d50f26", + }, + /** + * Note: Use secrets to store sensitive data. + * https://developers.cloudflare.com/workers/configuration/secrets/ + */ - /** - * Static Assets - * https://developers.cloudflare.com/workers/static-assets/binding/ - */ - // "assets": { "directory": "./public/", "binding": "ASSETS" }, + /** + * Static Assets + * https://developers.cloudflare.com/workers/static-assets/binding/ + */ + // "assets": { "directory": "./public/", "binding": "ASSETS" }, - /** - * Service Bindings (communicate between multiple Workers) - * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings - */ - // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] + /** + * Service Bindings (communicate between multiple Workers) + * https://developers.cloudflare.com/workers/wrangler/configuration/#service-bindings + */ + // "services": [{ "binding": "MY_SERVICE", "service": "my-service" }] }