A simple, privacy-focused embeddable widget service for Notion dashboards, hosted on Cloudflare Workers.
Note
This code is a result of heavy collaboration with Cursor AI. It's not 100% generated, as it needed a lot of tweaking to do exactly what I wanted, but the bones of it were suggestions from chat
- π Privacy-first: Host your own widgets without third-party tracking
- π Fast: Powered by Cloudflare Workers edge computing
- π¨ Extensible: Easy to add new widget types
- π± Notion-compatible: Works seamlessly with Notion's embed blocks
- πΎ Smart Caching: Reduces API calls with configurable cache times per widget
- π Monitoring with BetterStack
- π Metrics Tracking: Monitor cache hits vs misses to optimize costs
- Install dependencies:
npm install- Configure your Cloudflare account (if not already done):
npx wrangler login-
(Optional) Configure BetterStack logging:
a. Create a source in BetterStack Logs
b. Add your source token as a Cloudflare Workers secret:
npx wrangler secret put BETTERSTACK_SOURCE_TOKEN
c. Paste your source token when prompted
-
Run locally for development:
npm run dev- Deploy to Cloudflare Workers:
npm run deploy- After deploying, visit your worker URL (e.g.,
https://personal-embeds.your-subdomain.workers.dev) - Copy the widget URL for the widget you want (e.g.,
/widget/weather) - In Notion, type
/embed - Paste the full widget URL
- The widget will load in your Notion page
- URL:
/widget/weather - Optional Parameters:
location: The location identifier (default: "leith")label: Display label for the location (default: "leith, edinburgh")days: Number of days to show (default: "3")
- Example:
/widget/weather?location=london&label=London, UK&days=5 - Note: This widget uses weatherwidget.io for weather data
- URL:
/widget/clock - Optional Parameters:
timezone: IANA timezone (default: "Europe/London")format: Time format - "12" or "24" (default: "24")showDate: Show date - "true" or "false" (default: "false")theme: Widget theme - "light" or "dark" (default: "light")location: Location label to display (default: "Leith, Edinburgh")showLocation: Show location label - "true" or "false" (default: "true")
- Example:
/widget/clock?timezone=America/New_York&format=12&showDate=true&theme=dark&location=New York
- Create a new file in
src/widgets/(e.g.,src/widgets/clock.js):
export async function clockHandler(request, url) {
const params = url.searchParams;
const timezone = params.get("timezone") || "UTC";
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
margin: 0;
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
}
#clock {
font-size: 2em;
font-weight: 300;
}
</style>
</head>
<body>
<div id="clock"></div>
<script>
function updateClock() {
const now = new Date();
const options = {
timeZone: '${timezone}',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
};
document.getElementById('clock').textContent =
now.toLocaleTimeString('en-US', options);
}
updateClock();
setInterval(updateClock, 1000);
</script>
</body>
</html>
`;
}- Import and register the handler in
src/index.js:
import { weatherHandler } from "./widgets/weather.js";
import { clockHandler } from "./widgets/clock.js"; // Add this
const widgetHandlers = {
weather: weatherHandler,
clock: clockHandler, // Add this
};- Deploy the changes:
npm run deployWhen creating widgets:
- Keep them lightweight and fast-loading
- Ensure they work well in iframes
- Include proper responsive design
- Handle errors gracefully
- Consider adding query parameter support for customization
The service implements intelligent caching to minimize Cloudflare Workers invocations:
- Edge Cache: Responses are cached at Cloudflare's edge locations
- Browser Cache: Configurable browser caching per widget type
- Cache Headers: Automatic cache status headers (HIT/MISS)
- Weather widget: 10 minutes (edge), 5 minutes (browser)
- Clock widget: 1 second (minimal caching due to real-time nature)
- Home page: 1 hour (edge), 30 minutes (browser)
You can customize cache times in src/cache-config.js.
- The service uses permissive CORS headers to work with Notion
- Be cautious about what data you expose through widgets
- Consider adding authentication if hosting sensitive information
- Review third-party scripts before including them in widgets
- Ensure your worker is deployed and accessible
- Check that the URL is correct and includes the full path
- Try refreshing the Notion page
- Check browser console for any errors
- Make sure you're logged into Cloudflare:
npx wrangler login - Check that port 8787 is not in use
- Try clearing your browser cache
When BetterStack logging is configured, the service tracks the following metrics for each request:
cache_status: Whether the request was served from cache ("HIT") or required a new invocation ("MISS")is_invocation: Boolean flag indicating if this request cost a Cloudflare Workers invocationwidget_type: Which widget was requested (weather, clock, home, etc.)response_time_ms: How long the request took to processstatus_code: HTTP response status codepath: The requested URL path
In BetterStack, you can create queries to analyze your Cloudflare Workers usage:
-
Cache Hit Rate by Widget:
SELECT widget_type, COUNT(*) as total_requests, SUM(CASE WHEN cache_status = 'HIT' THEN 1 ELSE 0 END) as cache_hits, ROUND(SUM(CASE WHEN cache_status = 'HIT' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 2) as hit_rate_percent FROM logs WHERE metric = 'widget_request' GROUP BY widget_type
-
Invocations vs Cached Requests Over Time:
SELECT DATE_TRUNC('hour', timestamp) as hour, SUM(CASE WHEN is_invocation THEN 1 ELSE 0 END) as invocations, SUM(CASE WHEN NOT is_invocation THEN 1 ELSE 0 END) as cached_requests FROM logs WHERE metric = 'widget_request' GROUP BY hour ORDER BY hour DESC
-
Average Response Time by Cache Status:
SELECT cache_status, AVG(response_time_ms) as avg_response_time, PERCENTILE(response_time_ms, 0.95) as p95_response_time FROM logs WHERE metric = 'widget_request' GROUP BY cache_status
MIT