feat: skip bookmark import when no feed is found at the URL

Runs feed discovery before creating a subscription so article URLs
without a detectable feed are silently skipped instead of being
imported as dead feeds. Uses the discovered feed URL (which may differ
from the bookmarked URL) and passes the feed title from discovery to
avoid waiting for the first poll.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
svemagie
2026-04-04 10:16:03 +02:00
parent f3e8648fed
commit f73dd547e5
+29 -14
View File
@@ -11,6 +11,10 @@
*/ */
import { detectCapabilities } from "./feeds/capabilities.js"; import { detectCapabilities } from "./feeds/capabilities.js";
import {
discoverAndValidateFeeds,
getBestFeed,
} from "./feeds/discovery.js";
import { refreshFeedNow } from "./polling/scheduler.js"; import { refreshFeedNow } from "./polling/scheduler.js";
import { createChannel, getChannels } from "./storage/channels.js"; import { createChannel, getChannels } from "./storage/channels.js";
import { import {
@@ -98,6 +102,17 @@ export async function importBookmarkAsFollow(
return { error: "microsub not initialised" }; return { error: "microsub not initialised" };
} }
// Discover the actual feed URL — skip import if no valid feed exists
const discoveredFeeds = await discoverAndValidateFeeds(url).catch(() => []);
const bestFeed = getBestFeed(discoveredFeeds);
if (!bestFeed) {
console.log(
`[Microsub] bookmark-import: no feed found at ${url}, skipping`,
);
return { skipped: true, reason: "no feed found", url };
}
const feedUrl = bestFeed.url;
// Normalise tags to an array of trimmed, non-empty strings // Normalise tags to an array of trimmed, non-empty strings
const tagList = (Array.isArray(tags) ? tags : [tags]) const tagList = (Array.isArray(tags) ? tags : [tags])
.map((t) => String(t).trim()) .map((t) => String(t).trim())
@@ -121,7 +136,7 @@ export async function importBookmarkAsFollow(
} }
// Check if already followed in any channel // Check if already followed in any channel
const existing = await findFeedAcrossChannels(application, url); const existing = await findFeedAcrossChannels(application, feedUrl);
if (existing) { if (existing) {
const existingFeed = existing.feed; const existingFeed = existing.feed;
@@ -138,14 +153,14 @@ export async function importBookmarkAsFollow(
}); });
} }
console.log( console.log(
`[Microsub] bookmark-import: ${url} already followed in "${existingChannelName}" (correct channel)`, `[Microsub] bookmark-import: ${feedUrl} already followed in "${existingChannelName}" (correct channel)`,
); );
return { alreadyExists: true, url, channel: existingChannelName }; return { alreadyExists: true, url: feedUrl, channel: existingChannelName };
} }
// Wrong channel — move the feed: delete from old channel, create in new one. // Wrong channel — move the feed: delete from old channel, create in new one.
console.log( console.log(
`[Microsub] bookmark-import: moving ${url} from "${existingChannelName}" → "${targetChannel.name}"`, `[Microsub] bookmark-import: moving ${feedUrl} from "${existingChannelName}" → "${targetChannel.name}"`,
); );
await deleteFeedById(application, existingFeed._id); await deleteFeedById(application, existingFeed._id);
// Fall through to create below // Fall through to create below
@@ -156,17 +171,17 @@ export async function importBookmarkAsFollow(
try { try {
feed = await createFeed(application, { feed = await createFeed(application, {
channelId: targetChannel._id, channelId: targetChannel._id,
url, url: feedUrl,
title: undefined, title: bestFeed.title || undefined,
photo: undefined, photo: undefined,
micropubPostUrl: postUrl || undefined, micropubPostUrl: postUrl || undefined,
}); });
} catch (error) { } catch (error) {
if (error.code === "DUPLICATE_FEED") { if (error.code === "DUPLICATE_FEED") {
console.log( console.log(
`[Microsub] bookmark-import: duplicate feed detected for ${url}`, `[Microsub] bookmark-import: duplicate feed detected for ${feedUrl}`,
); );
return { alreadyExists: true, url }; return { alreadyExists: true, url: feedUrl };
} }
throw error; throw error;
} }
@@ -174,20 +189,20 @@ export async function importBookmarkAsFollow(
// Fire-and-forget: fetch and detect capabilities // Fire-and-forget: fetch and detect capabilities
refreshFeedNow(application, feed._id).catch((error) => { refreshFeedNow(application, feed._id).catch((error) => {
console.error( console.error(
`[Microsub] bookmark-import: error fetching ${url}:`, `[Microsub] bookmark-import: error fetching ${feedUrl}:`,
error.message, error.message,
); );
}); });
detectCapabilities(url).catch((error) => { detectCapabilities(feedUrl).catch((error) => {
console.error( console.error(
`[Microsub] bookmark-import: capability detection error for ${url}:`, `[Microsub] bookmark-import: capability detection error for ${feedUrl}:`,
error.message, error.message,
); );
}); });
// Notify blogroll (it gets its entries from microsub, not independently) // Notify blogroll (it gets its entries from microsub, not independently)
notifyBlogroll(application, "follow", { notifyBlogroll(application, "follow", {
url, url: feedUrl,
title: feed.title, title: feed.title,
channelName: targetChannel.name, channelName: targetChannel.name,
feedId: feed._id.toString(), feedId: feed._id.toString(),
@@ -197,9 +212,9 @@ export async function importBookmarkAsFollow(
}); });
console.log( console.log(
`[Microsub] bookmark-import: added ${url} to channel "${targetChannel.name}"`, `[Microsub] bookmark-import: added ${feedUrl} (discovered from ${url}) to channel "${targetChannel.name}"`,
); );
return { added: 1, url, channel: targetChannel.name }; return { added: 1, url: feedUrl, channel: targetChannel.name };
} }
/** /**