feat: category tabs, future post badge, HTML entity decoding

- Decode HTML entities (& ' etc) in feed titles and summaries
- Add isFuture flag to API items for future-dated posts
- Bump version to 1.0.12

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ricardo
2026-02-10 21:22:16 +01:00
parent 3bf0e765c7
commit 87851bade2
3 changed files with 32 additions and 9 deletions
+2
View File
@@ -249,12 +249,14 @@ function sanitizeBlog(blog) {
* @returns {object} Sanitized item * @returns {object} Sanitized item
*/ */
function sanitizeItem(item) { function sanitizeItem(item) {
const published = item.published ? new Date(item.published) : null;
return { return {
id: item._id.toString(), id: item._id.toString(),
url: item.url, url: item.url,
title: item.title, title: item.title,
summary: item.summary, summary: item.summary,
published: item.published, published: item.published,
isFuture: published ? published > new Date() : false,
author: item.author, author: item.author,
photo: item.photo, photo: item.photo,
categories: item.categories, categories: item.categories,
+27 -6
View File
@@ -134,14 +134,14 @@ function parseJsonFeed(content, feedUrl, maxItems) {
const items = (feed.items || []).slice(0, maxItems).map((item) => ({ const items = (feed.items || []).slice(0, maxItems).map((item) => ({
uid: generateUid(feedUrl, item.id || item.url), uid: generateUid(feedUrl, item.id || item.url),
url: item.url || item.external_url, url: item.url || item.external_url,
title: item.title || "Untitled", title: decodeEntities(item.title) || "Untitled",
content: { content: {
html: item.content_html html: item.content_html
? sanitizeHtml(item.content_html, SANITIZE_OPTIONS) ? sanitizeHtml(item.content_html, SANITIZE_OPTIONS)
: undefined, : undefined,
text: item.content_text, text: item.content_text,
}, },
summary: item.summary || truncateText(item.content_text, 300), summary: decodeEntities(item.summary) || truncateText(item.content_text, 300),
published: item.date_published ? new Date(item.date_published) : new Date(), published: item.date_published ? new Date(item.date_published) : new Date(),
updated: item.date_modified ? new Date(item.date_modified) : undefined, updated: item.date_modified ? new Date(item.date_modified) : undefined,
author: item.author || (item.authors?.[0]), author: item.author || (item.authors?.[0]),
@@ -171,7 +171,7 @@ function normalizeItem(item, feedUrl) {
return { return {
uid: generateUid(feedUrl, item.guid || item.link), uid: generateUid(feedUrl, item.guid || item.link),
url: item.link || item.origlink, url: item.link || item.origlink,
title: item.title || "Untitled", title: decodeEntities(item.title) || "Untitled",
content: { content: {
html: description ? sanitizeHtml(description, SANITIZE_OPTIONS) : undefined, html: description ? sanitizeHtml(description, SANITIZE_OPTIONS) : undefined,
text: stripHtml(description), text: stripHtml(description),
@@ -200,16 +200,37 @@ function generateUid(feedUrl, itemId) {
} }
/** /**
* Strip HTML tags from string * Strip HTML tags and decode HTML entities from string
* @param {string} html - HTML string * @param {string} html - HTML string
* @returns {string} Plain text * @returns {string} Plain text
*/ */
function stripHtml(html) { function stripHtml(html) {
if (!html) return ""; if (!html) return "";
return html return decodeEntities(
html
.replace(/<[^>]*>/g, " ") .replace(/<[^>]*>/g, " ")
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim(); .trim()
);
}
/**
* Decode HTML entities to their character equivalents
* @param {string} str - String with HTML entities
* @returns {string} Decoded string
*/
function decodeEntities(str) {
if (!str) return "";
return str
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&apos;/g, "'")
.replace(/&#39;/g, "'")
.replace(/&#x27;/g, "'")
.replace(/&#(\d+);/g, (_, code) => String.fromCharCode(Number(code)))
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)));
} }
/** /**
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-blogroll", "name": "@rmdes/indiekit-endpoint-blogroll",
"version": "1.0.11", "version": "1.0.12",
"description": "Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.", "description": "Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.",
"keywords": [ "keywords": [
"indiekit", "indiekit",