mirror of
https://github.com/svemagie/indiekit-endpoint-microsub.git
synced 2026-04-02 15:35:00 +02:00
- Add validator.js: validateFeedUrl with comments feed detection - Add discovery.js: discoverAndValidateFeeds with type labels - Add opml.js: OPML 2.0 export of all subscriptions - Update reader.js: searchFeeds uses validation, subscribe validates - Update feeds.js: updateFeedStatus for health tracking - Update search.njk: Show feed types, validation status, error messages - Add CSS for badges, notices, and invalid feed styling - Register OPML export route at /reader/opml Phase 1 of blogroll implementation plan. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
96 lines
2.6 KiB
JavaScript
96 lines
2.6 KiB
JavaScript
/**
|
|
* Enhanced feed discovery with type labels and validation
|
|
* @module feeds/discovery
|
|
*/
|
|
|
|
import { discoverFeedsFromUrl } from "./fetcher.js";
|
|
import { validateFeedUrl } from "./validator.js";
|
|
|
|
/**
|
|
* Feed type display labels
|
|
*/
|
|
const FEED_TYPE_LABELS = {
|
|
rss: "RSS Feed",
|
|
atom: "Atom Feed",
|
|
jsonfeed: "JSON Feed",
|
|
hfeed: "h-feed (Microformats)",
|
|
activitypub: "ActivityPub",
|
|
unknown: "Unknown",
|
|
};
|
|
|
|
/**
|
|
* Discover and validate all feeds from a URL
|
|
* @param {string} url - Page or feed URL
|
|
* @returns {Promise<Array>} Array of discovered feeds with validation status
|
|
*/
|
|
export async function discoverAndValidateFeeds(url) {
|
|
// First discover feeds from the URL
|
|
const feeds = await discoverFeedsFromUrl(url);
|
|
|
|
// If no feeds found, return empty with error info
|
|
if (feeds.length === 0) {
|
|
return [
|
|
{
|
|
url,
|
|
type: "unknown",
|
|
typeLabel: "No feed found",
|
|
valid: false,
|
|
error: "No feeds were discovered at this URL",
|
|
isCommentsFeed: false,
|
|
},
|
|
];
|
|
}
|
|
|
|
// Validate each discovered feed in parallel
|
|
const validatedFeeds = await Promise.all(
|
|
feeds.map(async (feed) => {
|
|
const validation = await validateFeedUrl(feed.url);
|
|
|
|
return {
|
|
url: feed.url,
|
|
type: validation.feedType || feed.type,
|
|
typeLabel:
|
|
FEED_TYPE_LABELS[validation.feedType] ||
|
|
FEED_TYPE_LABELS[feed.type] ||
|
|
"Feed",
|
|
valid: validation.valid,
|
|
error: validation.error,
|
|
isCommentsFeed: validation.isCommentsFeed || false,
|
|
title: validation.title || feed.title,
|
|
rel: feed.rel,
|
|
};
|
|
}),
|
|
);
|
|
|
|
// Sort: valid feeds first, non-comments before comments, then alphabetically
|
|
return validatedFeeds.sort((a, b) => {
|
|
// Valid feeds first
|
|
if (a.valid !== b.valid) return a.valid ? -1 : 1;
|
|
// Non-comments before comments
|
|
if (a.isCommentsFeed !== b.isCommentsFeed) return a.isCommentsFeed ? 1 : -1;
|
|
// Then by URL
|
|
return a.url.localeCompare(b.url);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Filter to only main content feeds (exclude comments)
|
|
* @param {Array} feeds - Array of feed objects
|
|
* @returns {Array} Filtered array of main content feeds
|
|
*/
|
|
export function filterMainFeeds(feeds) {
|
|
return feeds.filter((feed) => feed.valid && !feed.isCommentsFeed);
|
|
}
|
|
|
|
/**
|
|
* Get the best feed from a list (first valid, non-comments feed)
|
|
* @param {Array} feeds - Array of feed objects
|
|
* @returns {object|undefined} Best feed or undefined
|
|
*/
|
|
export function getBestFeed(feeds) {
|
|
const mainFeeds = filterMainFeeds(feeds);
|
|
return mainFeeds.length > 0 ? mainFeeds[0] : undefined;
|
|
}
|
|
|
|
export { FEED_TYPE_LABELS };
|