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) {
const { application } = request.app.locals;
const { category, limit = 100, offset = 0 } = request.query;
const { category, source, sort, limit = 100, offset = 0 } = request.query;
try {
const blogs = await getBlogs(application, {
category,
source,
sort,
limit: Number(limit),
offset: Number(offset),
});
const total = await countBlogs(application, { category });
const total = await countBlogs(application, { category, source });
response.json({
items: blogs.map(sanitizeBlog),
@@ -232,11 +234,11 @@ function sanitizeBlog(blog) {
itemCount: blog.itemCount,
pinned: blog.pinned,
lastFetchAt: blog.lastFetchAt,
source: blog.source || null,
};
// Include Microsub metadata if applicable
if (blog.source === "microsub") {
sanitized.source = "microsub";
sanitized.microsubChannel = blog.microsubChannelName;
}

View File

@@ -77,13 +77,13 @@ async function sync(request, response) {
if (result.skipped) {
request.session.messages = [
{ type: "warning", content: request.__("blogroll.sync.already_running") },
{ type: "warning", content: request.__("blogroll.syncResult.already_running") },
];
} else if (result.success) {
request.session.messages = [
{
type: "success",
content: request.__("blogroll.sync.success", {
content: request.__("blogroll.syncResult.success", {
blogs: result.blogs.success,
items: result.items.added,
}),
@@ -91,13 +91,13 @@ async function sync(request, response) {
];
} else {
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) {
console.error("[Blogroll] Manual sync error:", error);
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 = [
{
type: "success",
content: request.__("blogroll.sync.cleared_success", {
content: request.__("blogroll.syncResult.cleared_success", {
blogs: result.blogs.success,
items: result.items.added,
}),
@@ -126,13 +126,13 @@ async function clearResync(request, response) {
];
} else {
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) {
console.error("[Blogroll] Clear resync error:", error);
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 (category) query.category = category;
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
.find(query)
.sort({ pinned: -1, title: 1 })
.sort(sortOrder)
.skip(offset)
.limit(limit)
.toArray();
@@ -46,11 +54,12 @@ export async function getBlogs(application, options = {}) {
*/
export async function countBlogs(application, options = {}) {
const collection = getCollection(application);
const { category, includeHidden = false } = options;
const { category, source, includeHidden = false } = options;
const query = { status: { $ne: "deleted" } };
if (!includeHidden) query.hidden = { $ne: true };
if (category) query.category = category;
if (source) query.source = source;
return collection.countDocuments(query);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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