fix: resolve [Object Object] bug and add sort/source API params

Rename duplicate "sync" locale key to "syncResult" to fix the sources
list page showing [Object Object] instead of the Sync button label.

Add sort=recent and source= query params to the blogs API for the
sidebar widget tabs feature. Tag FeedLand blogs with source: "feedland"
and expose source field for all blogs in API responses.

Bump version to 1.0.22.
This commit is contained in:
Ricardo
2026-02-17 14:20:10 +01:00
parent 129dc78e09
commit f02d46e76e
20 changed files with 40 additions and 28 deletions

View File

@@ -18,16 +18,18 @@ import { handleMicrosubWebhook, isMicrosubAvailable } from "../sync/microsub.js"
async function listBlogs(request, response) { async function listBlogs(request, response) {
const { application } = request.app.locals; const { application } = request.app.locals;
const { category, limit = 100, offset = 0 } = request.query; const { category, source, sort, limit = 100, offset = 0 } = request.query;
try { try {
const blogs = await getBlogs(application, { const blogs = await getBlogs(application, {
category, category,
source,
sort,
limit: Number(limit), limit: Number(limit),
offset: Number(offset), offset: Number(offset),
}); });
const total = await countBlogs(application, { category }); const total = await countBlogs(application, { category, source });
response.json({ response.json({
items: blogs.map(sanitizeBlog), items: blogs.map(sanitizeBlog),
@@ -232,11 +234,11 @@ function sanitizeBlog(blog) {
itemCount: blog.itemCount, itemCount: blog.itemCount,
pinned: blog.pinned, pinned: blog.pinned,
lastFetchAt: blog.lastFetchAt, lastFetchAt: blog.lastFetchAt,
source: blog.source || null,
}; };
// Include Microsub metadata if applicable // Include Microsub metadata if applicable
if (blog.source === "microsub") { if (blog.source === "microsub") {
sanitized.source = "microsub";
sanitized.microsubChannel = blog.microsubChannelName; sanitized.microsubChannel = blog.microsubChannelName;
} }

View File

@@ -77,13 +77,13 @@ async function sync(request, response) {
if (result.skipped) { if (result.skipped) {
request.session.messages = [ request.session.messages = [
{ type: "warning", content: request.__("blogroll.sync.already_running") }, { type: "warning", content: request.__("blogroll.syncResult.already_running") },
]; ];
} else if (result.success) { } else if (result.success) {
request.session.messages = [ request.session.messages = [
{ {
type: "success", type: "success",
content: request.__("blogroll.sync.success", { content: request.__("blogroll.syncResult.success", {
blogs: result.blogs.success, blogs: result.blogs.success,
items: result.items.added, items: result.items.added,
}), }),
@@ -91,13 +91,13 @@ async function sync(request, response) {
]; ];
} else { } else {
request.session.messages = [ request.session.messages = [
{ type: "error", content: request.__("blogroll.sync.error", { error: result.error }) }, { type: "error", content: request.__("blogroll.syncResult.error", { error: result.error }) },
]; ];
} }
} catch (error) { } catch (error) {
console.error("[Blogroll] Manual sync error:", error); console.error("[Blogroll] Manual sync error:", error);
request.session.messages = [ request.session.messages = [
{ type: "error", content: request.__("blogroll.sync.error", { error: error.message }) }, { type: "error", content: request.__("blogroll.syncResult.error", { error: error.message }) },
]; ];
} }
@@ -118,7 +118,7 @@ async function clearResync(request, response) {
request.session.messages = [ request.session.messages = [
{ {
type: "success", type: "success",
content: request.__("blogroll.sync.cleared_success", { content: request.__("blogroll.syncResult.cleared_success", {
blogs: result.blogs.success, blogs: result.blogs.success,
items: result.items.added, items: result.items.added,
}), }),
@@ -126,13 +126,13 @@ async function clearResync(request, response) {
]; ];
} else { } else {
request.session.messages = [ request.session.messages = [
{ type: "error", content: request.__("blogroll.sync.error", { error: result.error }) }, { type: "error", content: request.__("blogroll.syncResult.error", { error: result.error }) },
]; ];
} }
} catch (error) { } catch (error) {
console.error("[Blogroll] Clear resync error:", error); console.error("[Blogroll] Clear resync error:", error);
request.session.messages = [ request.session.messages = [
{ type: "error", content: request.__("blogroll.sync.error", { error: error.message }) }, { type: "error", content: request.__("blogroll.syncResult.error", { error: error.message }) },
]; ];
} }

View File

@@ -29,10 +29,18 @@ export async function getBlogs(application, options = {}) {
if (!includeHidden) query.hidden = { $ne: true }; if (!includeHidden) query.hidden = { $ne: true };
if (category) query.category = category; if (category) query.category = category;
if (sourceId) query.sourceId = new ObjectId(sourceId); if (sourceId) query.sourceId = new ObjectId(sourceId);
if (options.source) query.source = options.source;
// Default sort: pinned first, then alphabetical
// "recent" sort: pinned first, then by last fetch time (newest first)
const sortOrder =
options.sort === "recent"
? { pinned: -1, lastFetchAt: -1, title: 1 }
: { pinned: -1, title: 1 };
return collection return collection
.find(query) .find(query)
.sort({ pinned: -1, title: 1 }) .sort(sortOrder)
.skip(offset) .skip(offset)
.limit(limit) .limit(limit)
.toArray(); .toArray();
@@ -46,11 +54,12 @@ export async function getBlogs(application, options = {}) {
*/ */
export async function countBlogs(application, options = {}) { export async function countBlogs(application, options = {}) {
const collection = getCollection(application); const collection = getCollection(application);
const { category, includeHidden = false } = options; const { category, source, includeHidden = false } = options;
const query = { status: { $ne: "deleted" } }; const query = { status: { $ne: "deleted" } };
if (!includeHidden) query.hidden = { $ne: true }; if (!includeHidden) query.hidden = { $ne: true };
if (category) query.category = category; if (category) query.category = category;
if (source) query.source = source;
return collection.countDocuments(query); return collection.countDocuments(query);
} }

View File

@@ -108,6 +108,7 @@ export async function syncFeedlandSource(application, source) {
const result = await upsertBlog(application, { const result = await upsertBlog(application, {
...blog, ...blog,
category, category,
source: "feedland",
sourceId: source._id, sourceId: source._id,
}); });

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Dadurch werden alle zwischengespeicherten Einträge gelöscht und alles neu abgerufen. Fortfahren?" "clearConfirm": "Dadurch werden alle zwischengespeicherten Einträge gelöscht und alles neu abgerufen. Fortfahren?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "This will delete all cached items and re-fetch everything. Continue?" "clearConfirm": "This will delete all cached items and re-fetch everything. Continue?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Esto eliminará todas las entradas almacenadas en caché y volverá a descargar todo. ¿Continuar?" "clearConfirm": "Esto eliminará todas las entradas almacenadas en caché y volverá a descargar todo. ¿Continuar?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Esto eliminará todas las entradas almacenadas en caché y volverá a obtenerlo todo. ¿Continuar?" "clearConfirm": "Esto eliminará todas las entradas almacenadas en caché y volverá a obtenerlo todo. ¿Continuar?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Cela supprimera toutes les entrées mises en cache et récupérera tout à nouveau. Continuer ?" "clearConfirm": "Cela supprimera toutes les entrées mises en cache et récupérera tout à nouveau. Continuer ?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "इससे सभी कैश किए गए आइटम हटा दिए जाएंगे और सब कुछ फिर से प्राप्त किया जाएगा। जारी रखें?" "clearConfirm": "इससे सभी कैश किए गए आइटम हटा दिए जाएंगे और सब कुछ फिर से प्राप्त किया जाएगा। जारी रखें?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Ini akan menghapus semua item yang di-cache dan mengambil semuanya lagi. Lanjutkan?" "clearConfirm": "Ini akan menghapus semua item yang di-cache dan mengambil semuanya lagi. Lanjutkan?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Questo cancellerà tutti gli elementi memorizzati e recupererà tutto nuovamente. Continuare?" "clearConfirm": "Questo cancellerà tutti gli elementi memorizzati e recupererà tutto nuovamente. Continuare?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Dit verwijdert alle gecachte items en haalt alles opnieuw op. Doorgaan?" "clearConfirm": "Dit verwijdert alle gecachte items en haalt alles opnieuw op. Doorgaan?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Spowoduje to usunięcie wszystkich elementów w pamięci podręcznej i ponowne pobranie wszystkiego. Kontynuować?" "clearConfirm": "Spowoduje to usunięcie wszystkich elementów w pamięci podręcznej i ponowne pobranie wszystkiego. Kontynuować?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Isso excluirá todos os itens em cache e buscará tudo novamente. Continuar?" "clearConfirm": "Isso excluirá todos os itens em cache e buscará tudo novamente. Continuar?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Isto eliminará todos os itens em cache e voltará a obter tudo. Continuar?" "clearConfirm": "Isto eliminará todos os itens em cache e voltará a obter tudo. Continuar?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Ово ће обрисати све кеширане ставке и поново преузети све. Наставити?" "clearConfirm": "Ово ће обрисати све кеширане ставке и поново преузети све. Наставити?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "Detta kommer att ta bort alla cachade poster och hämta allt igen. Fortsätta?" "clearConfirm": "Detta kommer att ta bort alla cachade poster och hämta allt igen. Fortsätta?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -26,7 +26,7 @@
"clearConfirm": "这将删除所有缓存的条目并重新获取所有内容。继续吗?" "clearConfirm": "这将删除所有缓存的条目并重新获取所有内容。继续吗?"
}, },
"sync": { "syncResult": {
"success": "Synced {{blogs}} blogs, added {{items}} items.", "success": "Synced {{blogs}} blogs, added {{items}} items.",
"error": "Sync failed: {{error}}", "error": "Sync failed: {{error}}",
"already_running": "A sync is already in progress.", "already_running": "A sync is already in progress.",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-blogroll", "name": "@rmdes/indiekit-endpoint-blogroll",
"version": "1.0.21", "version": "1.0.22",
"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",