@paroicms/server
Version:
The ParoiCMS server
228 lines • 9.64 kB
JavaScript
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