UNPKG

@paroicms/server

Version:
228 lines 9.64 kB
import { getNodeTypeByName } from "@paroicms/internal-anywhere-lib"; import { parseSqliteDateTime, toAbsoluteUrl } from "@paroicms/internal-server-lib"; import { encodeLNodeId, ensureString, isMediaHandleValue, isReadLabelingValue, parseLNodeId, } from "@paroicms/public-anywhere-lib"; import { escapeHtml } from "@paroicms/public-server-lib"; import { type } from "arktype"; import { Feed } from "feed"; import { getFieldTypesByNames, getSiteFieldValue, loadFieldValues, } from "../../admin-backend/fields/load-fields.queries.js"; import { getHandleOfSiteField } from "../../common/media-handles.helpers.js"; import { mainDbSchemaName } from "../../connector/db-init/db-constants.js"; import { cmsVersion, simpleI18n } from "../../context.js"; import { getFieldLabel } from "../../helpers/label-translator.helper.js"; import { executeRenderingHook } from "../../plugin-services/rendering-hook.js"; import { loadExcerpt } from "../../rendering-payload/excerpt.queries.js"; import { loadDefaultImage } from "../../rendering-payload/load-default-image.js"; import { makeImageAvailableByHandle, makeImageAvailableById, } from "../../rendering-payload/make-image-available.js"; import { getHomeUrl, getUrlOfDocument } from "../page-route/make-url.js"; export async function generateRssFeed(renderingContext, { language, feedUrl }) { const feedEntries = await buildFeedEntries(renderingContext, { language }); const feedMetadata = await buildChannelMetadata(renderingContext, { language, }); const feed = new Feed({ ...feedMetadata, feedLinks: { rss: feedUrl }, updated: new Date(), generator: `ParoiCMS ${cmsVersion}`, description: simpleI18n.translate({ key: "feed.description", language, args: [feedMetadata.title], }), }); for (const item of feedEntries) { feed.addItem(item); } return feed.rss2(); } async function buildChannelMetadata(renderingContext, options) { const { siteContext } = renderingContext; const { language } = options; const siteTitle = await getSiteTitle(siteContext, language); const currentYear = new Date().getFullYear(); const image = await makeImageAvailableByHandle(renderingContext, { handle: getHandleOfSiteField(siteContext, { fieldName: "ogImage" }) }, { resizeRule: "x88x", pixelRatio: 1, mediaType: "image/jpeg", absoluteUrl: true }); return { id: siteContext.siteUrl, link: toAbsoluteUrl(siteContext, getHomeUrl(siteContext, options.language)), language, image: image?.url, title: siteTitle, copyright: simpleI18n.translate({ key: "feed.copyright", language, args: [currentYear, siteTitle], }), }; } export async function getSiteTitle(siteContext, language) { const siteTitle = ensureString(await getSiteFieldValue(siteContext, { fieldName: "title", language, publishedOnly: true })) ?? simpleI18n.translate({ key: "feed.missingTitle", language }); return siteTitle; } const FeedItemValuesRowAT = type({ nodeId: "number", typeName: "string", language: "string", relativeId: "string", slug: "string|null", title: "string|null", publishDate: "Date|string|number", "+": "reject", }).pipe((r) => ({ nodeId: String(r.nodeId), typeName: r.typeName, language: r.language, relativeId: r.relativeId, slug: r.slug ?? undefined, title: r.title ?? undefined, publishDate: parseSqliteDateTime(r.publishDate), })); async function buildFeedEntries(renderingContext, options) { const { siteContext } = renderingContext; const { language } = options; const query = createFeedItemsQueryBuilder(siteContext, language); const rows = await query; renderingContext.tracker.trackAccess(mainDbSchemaName, "PaDocument", "read"); const feedItems = rows.map((row) => FeedItemValuesRowAT.assert(row)); const feedEntries = []; for (const item of feedItems) { const nodeType = getNodeTypeByName(siteContext.siteSchema, item.typeName); if (nodeType.kind !== "document") { siteContext.logger.warn(`[feed] Document type not found for typeName '${item.typeName}' (skip)`); continue; } const content = await loadFeedItemContent(renderingContext, item, language); const excerpt = await loadExcerpt(siteContext, renderingContext.tracker, { lNodeId: item, fieldTypes: nodeType.fields ?? [], maxLength: 300, }); const link = toAbsoluteUrl(siteContext, await getUrlOfDocument(siteContext, renderingContext.tracker, parseLNodeId(`${item.nodeId}:${item.language}`))); const defaultImage = await loadDefaultImage(renderingContext, { nodeId: item.nodeId, language, fieldTypes: nodeType.fields, }); const image = defaultImage === undefined ? undefined : await makeImageAvailableById(renderingContext, { mediaId: defaultImage.id }, { resizeRule: "x720x", pixelRatio: 1, mediaType: "image/jpeg" }); feedEntries.push({ guid: `${siteContext.fqdn}:${encodeLNodeId(item)}`, link, title: item.title ?? simpleI18n.translate({ key: "feed.missingTitle", language }), description: excerpt, content, date: item.publishDate, image: image ? toAbsoluteUrl(siteContext, image.url) : undefined, }); } return feedEntries; } async function loadFeedItemContent(renderingContext, item, language) { const { siteContext } = renderingContext; const fieldTypes = getFieldTypesByNames(siteContext, { typeName: item.typeName, }).filter((fieldType) => fieldType.dataType === "json" && fieldType.renderAs !== "html"); const contentFields = await loadFieldValues(siteContext, { nodeId: item.nodeId, language, fieldTypes, publishedOnly: true, }); const preprocessed = await executeRenderingHook(renderingContext, "fieldPreprocessor", { fieldTypes, values: contentFields, options: { absoluteUrls: true, }, }); const chunks = []; for (const fieldType of fieldTypes) { const rawVal = preprocessed[fieldType.name]; if (rawVal === undefined) continue; if (fieldType.storedAs === "labeling") { if (!isReadLabelingValue(rawVal) || rawVal.t.length === 0) continue; const val = rawVal.t.map((term) => term.title).join(", "); const label = getFieldLabel(siteContext.siteSchema, { typeName: item.typeName, language, fieldType, }); chunks.push(`<p><b>${simpleI18n.translate({ key: "common.colon", language, args: [label], })}</b> ${escapeHtml(val)}</p>`); } else if (fieldType.renderAs === "html") { chunks.push(ensureString(rawVal)); } else if (fieldType.dataType === "media") { if (!isMediaHandleValue(rawVal)) continue; const media = await makeImageAvailableByHandle(renderingContext, { handle: rawVal.h }, { resizeRule: "x350x", pixelRatio: 1, absoluteUrl: true }); if (!media) continue; chunks.push(`<img src="${escapeHtml(media.url)}" width="${media.width}" height="${media.height}" alt="" />`); } else { const val = typeof rawVal === "string" ? rawVal : typeof rawVal === "number" ? String(rawVal) : typeof rawVal === "boolean" ? rawVal ? "✓" : "✗" : undefined; if (!val) continue; const label = getFieldLabel(siteContext.siteSchema, { typeName: item.typeName, language, fieldType, }); chunks.push(`<p><b>${simpleI18n.translate({ key: "common.colon", language, args: [label], })}</b> ${escapeHtml(val)}</p>`); } } return chunks.join("\n"); } function createFeedItemsQueryBuilder(siteContext, language) { const { cn } = siteContext; const typeNames = getFeedTypeNames(siteContext.siteSchema); return cn("PaNode as n") .select([ "n.id as nodeId", "n.typeName as typeName", "n.relativeId as relativeId", "l.language as language", "d.slug as slug", "d.title as title", "n.publishDate as publishDate", ]) .innerJoin("PaLNode as l", { "l.nodeId": "n.id", "l.language": cn.raw("?", [language]), }) .innerJoin("PaDocument as d", { "d.nodeId": "l.nodeId", "d.language": cn.raw("?", [language]), }) .where("l.ready", 1) .whereRaw("n.publishDate <= current_timestamp") .whereRaw("n.publishDate >= datetime(current_timestamp, '-6 month')") .whereIn("n.typeName", typeNames) .orderBy("n.publishDate", "DESC") .limit(50); } function getFeedTypeNames(siteSchema) { return Object.values(siteSchema.nodeTypes) .filter((nodeType) => nodeType.kind === "document" && nodeType.documentKind === "regular") .map((nodeType) => nodeType.typeName); } //# sourceMappingURL=feed-generator.js.map