- 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.
225 lines
6.2 KiB
JavaScript
225 lines
6.2 KiB
JavaScript
import {
|
|
getItemByNotionId,
|
|
getPlainText,
|
|
transformContent,
|
|
toCamelCase,
|
|
} from "./notion.js";
|
|
import _ from "lodash";
|
|
import diff from "microdiff";
|
|
import { getNavigation } from "../utils/navigation.js";
|
|
import dayjs from "dayjs";
|
|
|
|
export async function getProjects(env, cached = false) {
|
|
try {
|
|
// If cached=true, try to get from Cloudflare Cache API first
|
|
if (cached) {
|
|
try {
|
|
const cache = caches.default;
|
|
const cacheKey = new Request(
|
|
`https://${env.CACHE_URL || "cache"}/Projects`
|
|
);
|
|
const cachedResponse = await cache.match(cacheKey);
|
|
|
|
if (cachedResponse) {
|
|
const cachedData = await cachedResponse.json();
|
|
console.log("Projects retrieved from Cloudflare cache");
|
|
return cachedData;
|
|
}
|
|
} catch (cacheError) {
|
|
console.log("Cache miss or error, falling back to KV:", cacheError);
|
|
}
|
|
}
|
|
|
|
// Fall back to KV storage
|
|
const kvData = await env.CONTENT_KV.get(env.PROJECTS_KEY, {
|
|
type: "json",
|
|
});
|
|
|
|
// If we were trying to use cache but it failed, store the KV data in cache
|
|
if (cached && kvData) {
|
|
try {
|
|
const cache = caches.default;
|
|
const cacheKey = new Request(
|
|
`https://${env.CACHE_URL || "cache"}/Projects`
|
|
);
|
|
|
|
const response = new Response(JSON.stringify(kvData), {
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"Cache-Control": "max-age=60", // 1 minute TTL
|
|
ETag: `"Projects-${Date.now()}"`, // Add ETag for cache validation
|
|
},
|
|
});
|
|
|
|
await cache.put(cacheKey, response);
|
|
console.log("Projects stored in Cloudflare cache after KV fallback");
|
|
} catch (cacheError) {
|
|
console.warn("Error storing in cache after KV fallback:", cacheError);
|
|
}
|
|
}
|
|
|
|
return kvData || [];
|
|
} catch (error) {
|
|
console.log("Error fetching Projects cache:", error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function storeProjects(env, Projects) {
|
|
try {
|
|
// Always store in KV first
|
|
await env.CONTENT_KV.put(env.PROJECTS_KEY, JSON.stringify(Projects));
|
|
console.log("Projects stored in KV.");
|
|
|
|
// Purge the Cloudflare Cache API
|
|
try {
|
|
const cache = caches.default;
|
|
const cacheKey = new Request(
|
|
`https://${env.CACHE_URL || "cache"}/Projects`
|
|
);
|
|
|
|
await cache.delete(cacheKey);
|
|
console.log("Projects cache purged successfully.");
|
|
} catch (cacheError) {
|
|
console.warn(
|
|
"Error purging Cloudflare cache, but KV was updated successfully:",
|
|
cacheError
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error storing Projects cache:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export function diffProjects(newList, oldList) {
|
|
// Keys to check for changes
|
|
const keysToCheck = [
|
|
"name",
|
|
"date",
|
|
"image",
|
|
"content",
|
|
"published",
|
|
"externalLink",
|
|
"type",
|
|
"tools",
|
|
"client",
|
|
];
|
|
|
|
// Helper: index by notionId using lodash
|
|
const oldById = _.keyBy(oldList || [], "notionId");
|
|
const newById = _.keyBy(newList || [], "notionId");
|
|
|
|
// toAdd: in newList but not in oldList
|
|
const toAdd = newList.filter((p) => !oldById[p.notionId]);
|
|
|
|
// toDelete: in oldList but not in newList
|
|
const toDelete = oldList.filter((p) => !newById[p.notionId]);
|
|
|
|
// toUpdate: in both, but with different content (using microdiff)
|
|
const toUpdate = newList.filter((p) => {
|
|
const old = oldById[p.notionId];
|
|
if (!old) return false;
|
|
|
|
// Prepare objects for comparison
|
|
const { notionId, ...restNew } = p;
|
|
const { notionId: _, ...restOld } = old;
|
|
|
|
// Use microdiff to get differences
|
|
const differences = diff(restOld, restNew);
|
|
|
|
// Filter differences to only include keys we care about
|
|
const relevantDifferences = differences.filter((change) => {
|
|
// Check if the changed path starts with any of our keysToCheck
|
|
const path = change.path.join(".");
|
|
return keysToCheck.some((key) => path.startsWith(key));
|
|
});
|
|
|
|
return relevantDifferences.length > 0;
|
|
});
|
|
|
|
return { toAdd, toUpdate, toDelete };
|
|
}
|
|
|
|
// Transform Notion project data to desired format
|
|
export async function transformNotionProject(env, notionProject, settingsData) {
|
|
const navigationItems = await getNavigation(env);
|
|
|
|
const properties = notionProject.properties;
|
|
|
|
console.log("Notion Project:", notionProject);
|
|
|
|
// Extract theme information
|
|
let theme = undefined; // default
|
|
if (properties.Theme?.relation?.[0]?.id) {
|
|
const themeId = properties.Theme.relation[0].id;
|
|
console.log(settingsData.themes);
|
|
const themeData = getItemByNotionId(settingsData.themes, themeId);
|
|
if (themeData?.name) {
|
|
theme = themeData.name;
|
|
}
|
|
}
|
|
|
|
const published = properties["Published"]?.checkbox || false;
|
|
|
|
const slug = properties["Slug"]?.formula?.string || "unknown";
|
|
|
|
const status = toCamelCase(properties["Status"]?.status?.name || null);
|
|
|
|
const date = properties["Date"]?.date?.start || null;
|
|
|
|
// Extract image URL (handle both external and file images)
|
|
const imageFile = notionProject?.cover;
|
|
const image = imageFile?.external?.url || imageFile?.file?.url || null;
|
|
|
|
const subTitle = getPlainText(properties["Subtitle"]);
|
|
|
|
// Extract name from title
|
|
const name = properties.Name?.title?.[0]?.plain_text || "Untitled";
|
|
|
|
// Extract external link
|
|
const externalLink = properties["External Link"]?.url || null;
|
|
|
|
// Extract type (select)
|
|
const type = properties["Type"]?.select?.name || null;
|
|
|
|
// Extract tools (multi-select)
|
|
const tools = (properties["Tools"]?.multi_select || []).map(
|
|
(item) => item.name
|
|
);
|
|
|
|
// Extract client (text)
|
|
const client = getPlainText(properties["Client"]);
|
|
|
|
// Fetch and transform the actual page content from Notion blocks
|
|
|
|
const content = [{ type: "image", url: image, caption: subTitle }];
|
|
const projectContent = await transformContent(
|
|
env,
|
|
notionProject.id,
|
|
navigationItems
|
|
);
|
|
content.push(...projectContent);
|
|
|
|
if (published == false) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
notionId: notionProject.id,
|
|
name: name,
|
|
date: date,
|
|
image: image,
|
|
subTitle: subTitle,
|
|
content,
|
|
published,
|
|
externalLink: externalLink,
|
|
type: type,
|
|
tools: tools,
|
|
client: client,
|
|
slug: slug,
|
|
status: status,
|
|
theme: theme,
|
|
};
|
|
}
|