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

View File

@@ -249,12 +249,14 @@ function sanitizeBlog(blog) {
* @returns {object} Sanitized item
*/
function sanitizeItem(item) {
const published = item.published ? new Date(item.published) : null;
return {
id: item._id.toString(),
url: item.url,
title: item.title,
summary: item.summary,
published: item.published,
isFuture: published ? published > new Date() : false,
author: item.author,
photo: item.photo,
categories: item.categories,

View File

@@ -134,14 +134,14 @@ function parseJsonFeed(content, feedUrl, maxItems) {
const items = (feed.items || []).slice(0, maxItems).map((item) => ({
uid: generateUid(feedUrl, item.id || item.url),
url: item.url || item.external_url,
title: item.title || "Untitled",
title: decodeEntities(item.title) || "Untitled",
content: {
html: item.content_html
? sanitizeHtml(item.content_html, SANITIZE_OPTIONS)
: undefined,
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(),
updated: item.date_modified ? new Date(item.date_modified) : undefined,
author: item.author || (item.authors?.[0]),
@@ -171,7 +171,7 @@ function normalizeItem(item, feedUrl) {
return {
uid: generateUid(feedUrl, item.guid || item.link),
url: item.link || item.origlink,
title: item.title || "Untitled",
title: decodeEntities(item.title) || "Untitled",
content: {
html: description ? sanitizeHtml(description, SANITIZE_OPTIONS) : undefined,
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
* @returns {string} Plain text
*/
function stripHtml(html) {
if (!html) return "";
return html
return decodeEntities(
html
.replace(/<[^>]*>/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)));
}
/**

View File

@@ -1,6 +1,6 @@
{
"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.",
"keywords": [
"indiekit",