From 5c899b4a5c2c07398151960b2b86d238b49d5bf4 Mon Sep 17 00:00:00 2001 From: Volpeon Date: Sun, 26 Jul 2026 11:57:13 +0200 Subject: Better Strapi integration --- src/deps/strapi.tsx | 416 ---------------------------------------------------- 1 file changed, 416 deletions(-) delete mode 100644 src/deps/strapi.tsx (limited to 'src/deps/strapi.tsx') diff --git a/src/deps/strapi.tsx b/src/deps/strapi.tsx deleted file mode 100644 index 127dffc..0000000 --- a/src/deps/strapi.tsx +++ /dev/null @@ -1,416 +0,0 @@ -import type { Pipeline } from "corvos"; -import type { StandardSchemaV1 } from "corvos/deps/standard_schema.ts"; - -import { type } from "arktype"; -import { Page, PipelineStageError } from "corvos"; -import { Severity } from "corvos/core/error.ts"; -import { replaceExtension } from "corvos/core/utils/path.ts"; - -import { ContentImage } from "$/ui/objects/ContentImage.tsx"; - -export enum HeadingBlockLevel { - L1 = 1, - L2 = 2, - L3 = 3, - L4 = 4, - L5 = 5, - L6 = 6, -} - -export const fileAttributeSchema = type({ - "alternativeText?": "string | null", - "caption?": "string | null", - "createdAt": "string", - "documentId": "string", - "ext": "string", - "formats?": "Record | null", - "hash": "string", - "height?": "number | null", - "mime": "string", - "name": "string", - "previewUrl?": "string | null", - "provider": "string", - "provider_metadata?": "unknown", - "size": "number", - "updatedAt": "string", - "url": "string", - "width?": "number | null", -}); - -export type FileAttribute = typeof fileAttributeSchema.infer; - -export const imageAttributeSchema = fileAttributeSchema.narrow( - (o): o is Omit & { width: number; height: number } => - !!o.width && !!o.height, -); - -export type ImageAttribute = typeof imageAttributeSchema.infer; - -const blocksAttributeScope = type - .scope({ - codeBlockNode: { - "children": "defaultInlineNode[]", - "language?": "string", - "type": "'code'", - }, - - defaultInlineNode: "textInlineNode | linkInlineNode", - - headingBlockNode: { - children: "defaultInlineNode[]", - level: "1 | 2 | 3 | 4 | 5 | 6", - type: "'heading'", - }, - - imageBlockNode: { - children: type({ - text: "string", - type: "'text'", - }).array(), - image: fileAttributeSchema.omit("documentId"), - type: "'image'", - }, - - linkInlineNode: { - children: "textInlineNode[]", - type: "'link'", - url: "string", - }, - - listBlockNode: { - children: "(listItemInlineNode | listBlockNode)[]", - format: "'ordered' | 'unordered'", - type: "'list'", - }, - - listItemInlineNode: { - children: "defaultInlineNode[]", - type: "'list-item'", - }, - - node: "rootNode | nonTextInlineNode", - - nonTextInlineNode: "linkInlineNode | listItemInlineNode", - - paragraphBlockNode: { - children: "defaultInlineNode[]", - type: "'paragraph'", - }, - - quoteBlockNode: { - children: "defaultInlineNode[]", - type: "'quote'", - }, - - rootNode: - "paragraphBlockNode | quoteBlockNode | codeBlockNode | headingBlockNode | listBlockNode | imageBlockNode", - - textInlineNode: { - "bold?": "boolean", - "code?": "boolean", - "italic?": "boolean", - "strikethrough?": "boolean", - "text": "string", - "type": "'text'", - "underline?": "boolean", - }, - }) - .export(); - -export const defaultInlineNodeSchema = blocksAttributeScope.defaultInlineNode; - -export type DefaultInlineNode = typeof defaultInlineNodeSchema.infer; - -export const nodeSchema = blocksAttributeScope.node; - -export type Node = typeof nodeSchema.infer; - -export const strapiItemSchema = type({ - "[string]": "unknown", - "documentId": "string", - "publishedAt": "string.date.parse | null", - "updatedAt": "string.date.parse", -}); - -export type StrapiItem = typeof strapiItemSchema.infer; - -export function walkBlocks( - content: (Node | DefaultInlineNode)[], - fn: (_: Node | DefaultInlineNode) => unknown, -): void { - for (const node of content) { - fn(node); - - switch (node.type) { - case "heading": - case "code": - case "quote": - case "paragraph": - case "list": - case "list-item": - case "link": { - return walkBlocks(node.children, fn); - } - } - } -} - -function blocksNodeToTSX(node: Node | DefaultInlineNode) { - switch (true) { - case node.type === "heading": { - switch (node.level) { - case 1: - return

{blocksToTSX(node.children)}

; - case 2: - return

{blocksToTSX(node.children)}

; - case 3: - return

{blocksToTSX(node.children)}

; - case 4: - return

{blocksToTSX(node.children)}

; - case 5: - return
{blocksToTSX(node.children)}
; - default: - return
{blocksToTSX(node.children)}
; - } - } - - case node.type === "image" && !!node.image.caption: { - return ( -
- -
{node.image.caption}
-
- ); - } - - case node.type === "image": { - return ; - } - - case node.type === "code": { - return ( -
-					{blocksToTSX(node.children)}
-				
- ); - } - - case node.type === "quote": { - return
{blocksToTSX(node.children)}
; - } - - case node.type === "paragraph" && - node.children.length === 1 && - node.children[0]?.type === "text" && - node.children[0].text === "---": { - return
; - } - - case node.type === "paragraph": { - return

{blocksToTSX(node.children)}

; - } - - case node.type === "list" && node.format === "ordered": { - return
    ${blocksToTSX(node.children)}
; - } - - case node.type === "list": { - return
    ${blocksToTSX(node.children)}
; - } - - case node.type === "list-item": { - return
  • ${blocksToTSX(node.children)}
  • ; - } - - case node.type === "link": { - return {blocksToTSX(node.children)}; - } - - case node.type === "text": { - return [ - node.bold ? "strong" : undefined, - node.code ? "code" : undefined, - node.italic ? "em" : undefined, - node.strikethrough ? "del" : undefined, - node.underline ? "u" : undefined, - ].reduce((res: JSX.Element, Tag) => (Tag ? {res} : res), node.text); - } - } -} - -export function blocksToTSX( - content: (Node | DefaultInlineNode)[], - filter: (node: Node | DefaultInlineNode) => boolean = () => true, -): JSX.Element { - return content - .filter(filter) - .map(blocksNodeToTSX) - .filter((s) => s !== undefined); -} - -export const strapiCollectionResponseSchema = type({ - data: strapiItemSchema.array(), - meta: { - pagination: { - page: "number", - pageCount: "number", - pageSize: "number", - total: "number", - }, - }, -}); - -export const strapiItemResponseSchema = type({ - data: strapiItemSchema, -}); - -async function* loadPaginated(url: URL | string, init?: RequestInit) { - let pageSize: number | undefined; - let pages = 1; - - for (let i = 0; i < pages; ++i) { - const pageUrl = new URL(url); - - if (pageSize) { - pageUrl.searchParams.set("pagination[page]", `${i + 1}`); - pageUrl.searchParams.set("pagination[pageSize]", `${pageSize}`); - } - - const res = await fetch(pageUrl, init); - const json = await res.json(); - - const dec = strapiCollectionResponseSchema(json); - - if (dec instanceof type.errors) { - throw new Error(dec.summary); - } - - pages = dec.meta.pagination.pageCount; - pageSize = dec.meta.pagination.pageSize; - - for (const item of dec.data) { - yield item; - } - } -} - -export class StrapiLoader { - init: RequestInit; - - constructor( - readonly host: string | URL, - token: string, - readonly publicHost = host, - ) { - this.init = { - headers: { - Authorization: `Bearer ${token}`, - }, - keepalive: true, - }; - } - - async loadSingle( - collection: string, - schema: StandardSchemaV1, - query: Record = {}, - ) { - const url = new URL(`/api/${collection}`, this.host); - Object.entries(query).forEach(([key, value]) => { - url.searchParams.append(key, value); - }); - - const res = await fetch(url, this.init); - const json = await res.json(); - - const dec = strapiItemResponseSchema(json); - - if (dec instanceof type.errors) { - throw new Error(`Schema mismatch in ${url}:\n${dec.summary}`); - } - - const dec2 = await schema["~standard"].validate(dec.data); - - if (dec2.issues) { - throw new Error(`Schema mismatch in ${url}:\n${dec2.issues.join("\n")}`); - } - - return { - documentId: dec.data.documentId, - publishedAt: dec.data.publishedAt, - updatedAt: dec.data.updatedAt, - ...dec2.value, - }; - } - - async *loadCollection( - collection: string, - schema: StandardSchemaV1, - query: Record = {}, - ) { - const url = new URL(`/api/${collection}`, this.host); - Object.entries(query).forEach(([key, value]) => { - url.searchParams.append(key, value); - }); - - for await (const item of loadPaginated(url, this.init)) { - const dec = await schema["~standard"].validate(item); - - if (dec.issues) { - yield new PipelineStageError( - Severity.ERROR, - `Schema mismatch in ${url}`, - dec.issues.join("\n"), - ); - } else { - yield { - documentId: item.documentId, - publishedAt: item.publishedAt, - updatedAt: item.updatedAt, - ...dec.value, - }; - } - } - } - - readonly resolvedFileAttributeSchema = fileAttributeSchema.pipe(({ url, previewUrl, ...o }) => ({ - ...o, - url: new URL(url, this.publicHost).href, - ...(previewUrl ? { previewUrl: new URL(previewUrl, this.publicHost).href } : {}), - })); - - readonly resolvedImageAttributeSchema = imageAttributeSchema.pipe((o) => ({ - ...o, - previewUrl: o.previewUrl ? new URL(o.previewUrl, this.publicHost).href : undefined, - url: new URL(o.url, this.publicHost).href, - })); -} - -export function strapi() { - return ( - pipeline: Pipeline, - ) => - pipeline.process("strapi", ".strapi", function* (page) { - yield new Page( - ".jsxelement", - blocksToTSX(page.content), - page.data as Out[".strapi"][1], - replaceExtension(page.url, ".jsxelement"), - page, - ); - }); -} - -export const Blocks = ({ - content, - filter = () => true, -}: { - content: (Node | DefaultInlineNode)[]; - filter?: (node: Node | DefaultInlineNode) => boolean; -}) => blocksToTSX(content, filter); - -const strapiUrl = Deno.env.get("STRAPI_URL") ?? ""; -const strapiPublicUrl = Deno.env.get("STRAPI_PUBLIC_URL"); -const strapiToken = Deno.env.get("STRAPI_TOKEN") ?? ""; - -export const strapiLoader = new StrapiLoader(strapiUrl, strapiToken, strapiPublicUrl); -- cgit v1.3.1