diff --git a/CLAUDE.md b/CLAUDE.md index e4bf928..9a13344 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,8 +5,9 @@ `@rmdes/indiekit-endpoint-blogroll` is an Indiekit plugin that provides a comprehensive blogroll management system. It aggregates blog feeds from multiple sources (OPML files/URLs, Microsub subscriptions), fetches and caches recent items, and exposes both an admin UI and public JSON API. **Key Capabilities:** -- Aggregates blogs from OPML (URL or file), JSON feeds, or manual entry +- Aggregates blogs from OPML (URL or file), JSON feeds, FeedLand, or manual entry - Integrates with Microsub plugin to mirror subscriptions +- FeedLand integration (feedland.com or self-hosted) with category discovery - Background feed fetching with configurable intervals - Admin UI for managing sources, blogs, and viewing recent items - Public read-only JSON API for frontend integration @@ -21,13 +22,13 @@ ### Data Flow ``` -Sources (OPML/Microsub) → Blogs → Items +Sources (OPML/Microsub/FeedLand) → Blogs → Items ↓ ↓ ↓ blogrollSources blogrollBlogs blogrollItems microsub_items (reference) ``` -1. **Sources** define where blogs come from (OPML URL, OPML file, Microsub channels) +1. **Sources** define where blogs come from (OPML URL, OPML file, Microsub channels, FeedLand) 2. **Blogs** are individual feed subscriptions with metadata 3. **Items** are recent posts/articles from blogs (cached for 7 days by default) @@ -42,13 +43,17 @@ Sources (OPML/Microsub) → Blogs → Items ```javascript { _id: ObjectId, - type: "opml_url" | "opml_file" | "manual" | "json_feed" | "microsub", + type: "opml_url" | "opml_file" | "manual" | "json_feed" | "microsub" | "feedland", name: String, // Display name url: String | null, // For opml_url, json_feed opmlContent: String | null, // For opml_file // Microsub-specific channelFilter: String | null, // Specific channel UID or null for all categoryPrefix: String, // Prefix for blog categories + // FeedLand-specific + feedlandInstance: String | null, // e.g., "https://feedland.com" + feedlandUsername: String | null, // FeedLand screen name + feedlandCategory: String | null, // Category filter (or null for all) enabled: Boolean, syncInterval: Number, // Minutes between syncs lastSyncAt: String | null, // ISO 8601 @@ -141,6 +146,7 @@ Sources (OPML/Microsub) → Blogs → Items - **lib/sync/scheduler.js** - Background sync, interval management - **lib/sync/opml.js** - OPML parsing, fetch from URL, export - **lib/sync/microsub.js** - Microsub channel/feed sync, webhook handler +- **lib/sync/feedland.js** - FeedLand sync, category discovery, OPML URL builder - **lib/sync/feed.js** - RSS/Atom/JSON Feed parsing, item fetching ### Utilities diff --git a/index.js b/index.js index 7d2a136..6b4ec5d 100644 --- a/index.js +++ b/index.js @@ -93,6 +93,9 @@ export default class BlogrollEndpoint { protectedRouter.post("/api/microsub-webhook", apiController.microsubWebhook); protectedRouter.get("/api/microsub-status", apiController.microsubStatus); + // FeedLand integration (protected - category discovery) + protectedRouter.get("/api/feedland-categories", sourcesController.feedlandCategories); + return protectedRouter; } diff --git a/lib/controllers/sources.js b/lib/controllers/sources.js index 8558e5f..069fb9e 100644 --- a/lib/controllers/sources.js +++ b/lib/controllers/sources.js @@ -16,6 +16,10 @@ import { getMicrosubChannels, isMicrosubAvailable, } from "../sync/microsub.js"; +import { + syncFeedlandSource, + fetchFeedlandCategories, +} from "../sync/feedland.js"; /** * List sources @@ -95,6 +99,9 @@ async function create(request, response) { enabled, channelFilter, categoryPrefix, + feedlandInstance, + feedlandUsername, + feedlandCategory, } = request.body; try { @@ -120,6 +127,13 @@ async function create(request, response) { return response.redirect(`${request.baseUrl}/sources/new`); } + if (type === "feedland" && (!feedlandInstance || !feedlandUsername)) { + request.session.messages = [ + { type: "error", content: request.__("blogroll.sources.form.feedlandRequired") }, + ]; + return response.redirect(`${request.baseUrl}/sources/new`); + } + const sourceData = { name, type, @@ -135,12 +149,21 @@ async function create(request, response) { sourceData.categoryPrefix = categoryPrefix || ""; } + // Add feedland-specific fields + if (type === "feedland") { + sourceData.feedlandInstance = feedlandInstance.replace(/\/+$/, ""); + sourceData.feedlandUsername = feedlandUsername; + sourceData.feedlandCategory = feedlandCategory || null; + } + const source = await createSource(application, sourceData); // Trigger initial sync based on source type try { if (type === "microsub") { await syncMicrosubSource(application, source); + } else if (type === "feedland") { + await syncFeedlandSource(application, source); } else { await syncOpmlSource(application, source); } @@ -223,6 +246,9 @@ async function update(request, response) { enabled, channelFilter, categoryPrefix, + feedlandInstance, + feedlandUsername, + feedlandCategory, } = request.body; try { @@ -247,6 +273,15 @@ async function update(request, response) { updateData.categoryPrefix = categoryPrefix || ""; } + // Add feedland-specific fields + if (type === "feedland") { + updateData.feedlandInstance = feedlandInstance + ? feedlandInstance.replace(/\/+$/, "") + : null; + updateData.feedlandUsername = feedlandUsername || null; + updateData.feedlandCategory = feedlandCategory || null; + } + await updateSource(application, id, updateData); request.session.messages = [ @@ -313,6 +348,8 @@ async function sync(request, response) { let result; if (source.type === "microsub") { result = await syncMicrosubSource(application, source); + } else if (source.type === "feedland") { + result = await syncFeedlandSource(application, source); } else { result = await syncOpmlSource(application, source); } @@ -358,6 +395,26 @@ function consumeFlashMessage(request) { return result; } +/** + * Fetch FeedLand categories (AJAX endpoint) + * GET /api/feedland-categories?instance=...&username=... + */ +async function feedlandCategories(request, response) { + const { instance, username } = request.query; + + if (!instance || !username) { + return response.status(400).json({ error: "instance and username are required" }); + } + + try { + const data = await fetchFeedlandCategories(instance, username); + response.json(data); + } catch (error) { + console.error("[Blogroll] FeedLand categories fetch error:", error.message); + response.status(502).json({ error: error.message }); + } +} + export const sourcesController = { list, newForm, @@ -366,4 +423,5 @@ export const sourcesController = { update, remove, sync, + feedlandCategories, }; diff --git a/lib/storage/sources.js b/lib/storage/sources.js index 465fb3c..d6617d8 100644 --- a/lib/storage/sources.js +++ b/lib/storage/sources.js @@ -48,13 +48,17 @@ export async function createSource(application, data) { const now = new Date().toISOString(); const source = { - type: data.type, // "opml_url" | "opml_file" | "manual" | "json_feed" | "microsub" + type: data.type, // "opml_url" | "opml_file" | "manual" | "json_feed" | "microsub" | "feedland" name: data.name, url: data.url || null, opmlContent: data.opmlContent || null, // Microsub-specific fields channelFilter: data.channelFilter || null, categoryPrefix: data.categoryPrefix || "", + // FeedLand-specific fields + feedlandInstance: data.feedlandInstance || null, + feedlandUsername: data.feedlandUsername || null, + feedlandCategory: data.feedlandCategory || null, enabled: data.enabled !== false, syncInterval: data.syncInterval || 60, // minutes lastSyncAt: null, @@ -160,7 +164,7 @@ export async function getSourcesDueForSync(application) { return collection .find({ enabled: true, - type: { $in: ["opml_url", "json_feed", "microsub"] }, + type: { $in: ["opml_url", "json_feed", "microsub", "feedland"] }, $or: [ { lastSyncAt: null }, { diff --git a/lib/sync/feedland.js b/lib/sync/feedland.js new file mode 100644 index 0000000..a69fb2d --- /dev/null +++ b/lib/sync/feedland.js @@ -0,0 +1,139 @@ +/** + * FeedLand synchronization + * @module sync/feedland + */ + +import { fetchAndParseOpml } from "./opml.js"; +import { upsertBlog } from "../storage/blogs.js"; +import { updateSourceSyncStatus } from "../storage/sources.js"; + +/** + * Fetch user categories from a FeedLand instance + * @param {string} instanceUrl - FeedLand instance URL (e.g., https://feedland.com) + * @param {string} username - FeedLand username + * @param {number} timeout - Fetch timeout in ms + * @returns {Promise} Category data { categories: string[], homePageCategories: string[] } + */ +export async function fetchFeedlandCategories(instanceUrl, username, timeout = 10000) { + const baseUrl = instanceUrl.replace(/\/+$/, ""); + const url = `${baseUrl}/getusercategories?screenname=${encodeURIComponent(username)}`; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + + try { + const response = await fetch(url, { + signal: controller.signal, + headers: { + "User-Agent": "Indiekit-Blogroll/1.0", + Accept: "application/json", + }, + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + // FeedLand returns comma-separated strings + const categories = data.categories + ? data.categories.split(",").map((c) => c.trim()).filter(Boolean) + : []; + const homePageCategories = data.homePageCategories + ? data.homePageCategories.split(",").map((c) => c.trim()).filter(Boolean) + : []; + + return { categories, homePageCategories, screenname: data.screenname }; + } catch (error) { + clearTimeout(timeoutId); + if (error.name === "AbortError") { + throw new Error("Request timed out"); + } + throw error; + } +} + +/** + * Build the OPML URL for a FeedLand source + * @param {object} source - Source document with feedland fields + * @returns {string} OPML URL + */ +export function buildFeedlandOpmlUrl(source) { + const baseUrl = source.feedlandInstance.replace(/\/+$/, ""); + let url = `${baseUrl}/opml?screenname=${encodeURIComponent(source.feedlandUsername)}`; + + if (source.feedlandCategory) { + url += `&catname=${encodeURIComponent(source.feedlandCategory)}`; + } + + return url; +} + +/** + * Build the FeedLand river URL for linking back + * @param {object} source - Source document with feedland fields + * @returns {string} River URL + */ +export function buildFeedlandRiverUrl(source) { + const baseUrl = source.feedlandInstance.replace(/\/+$/, ""); + return `${baseUrl}/?river=true&screenname=${encodeURIComponent(source.feedlandUsername)}`; +} + +/** + * Sync blogs from a FeedLand source + * @param {object} application - Application instance + * @param {object} source - Source document + * @returns {Promise} Sync result + */ +export async function syncFeedlandSource(application, source) { + try { + const opmlUrl = buildFeedlandOpmlUrl(source); + const blogs = await fetchAndParseOpml(opmlUrl); + + let added = 0; + let updated = 0; + + for (const blog of blogs) { + // FeedLand OPML includes a category attribute with comma-separated categories. + // Use the first category, or fall back to the source's feedlandCategory filter, + // or use the FeedLand username as a category grouping. + const category = blog.category + || source.feedlandCategory + || source.feedlandUsername + || ""; + + const result = await upsertBlog(application, { + ...blog, + category, + sourceId: source._id, + }); + + if (result.upserted) added++; + else if (result.modified) updated++; + } + + // Update source sync status + await updateSourceSyncStatus(application, source._id, { success: true }); + + console.log( + `[Blogroll] Synced FeedLand source "${source.name}" (${source.feedlandUsername}@${source.feedlandInstance}): ${added} added, ${updated} updated, ${blogs.length} total` + ); + + return { success: true, added, updated, total: blogs.length }; + } catch (error) { + // Update source with error status + await updateSourceSyncStatus(application, source._id, { + success: false, + error: error.message, + }); + + console.error( + `[Blogroll] FeedLand sync failed for "${source.name}":`, + error.message + ); + return { success: false, error: error.message }; + } +} diff --git a/lib/sync/scheduler.js b/lib/sync/scheduler.js index d499f72..9cb496d 100644 --- a/lib/sync/scheduler.js +++ b/lib/sync/scheduler.js @@ -8,6 +8,7 @@ import { getBlogs, countBlogs } from "../storage/blogs.js"; import { countItems, deleteOldItems } from "../storage/items.js"; import { syncOpmlSource } from "./opml.js"; import { syncMicrosubSource } from "./microsub.js"; +import { syncFeedlandSource } from "./feedland.js"; import { syncBlogItems } from "./feed.js"; let syncInterval = null; @@ -42,7 +43,7 @@ export async function runFullSync(application, options = {}) { // Sync all enabled sources (OPML, JSON, Microsub) const sources = await getSources(application); const enabledSources = sources.filter( - (s) => s.enabled && ["opml_url", "opml_file", "json_feed", "microsub"].includes(s.type) + (s) => s.enabled && ["opml_url", "opml_file", "json_feed", "microsub", "feedland"].includes(s.type) ); let sourcesSuccess = 0; @@ -53,6 +54,8 @@ export async function runFullSync(application, options = {}) { let result; if (source.type === "microsub") { result = await syncMicrosubSource(application, source); + } else if (source.type === "feedland") { + result = await syncFeedlandSource(application, source); } else { result = await syncOpmlSource(application, source); } diff --git a/locales/de.json b/locales/de.json index 8846c27..63c8d09 100644 --- a/locales/de.json +++ b/locales/de.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/en.json b/locales/en.json index 2cb17f8..9553fd5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/es-419.json b/locales/es-419.json index 5a0b19c..ba6a8bc 100644 --- a/locales/es-419.json +++ b/locales/es-419.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/es.json b/locales/es.json index 10bb500..d4b841c 100644 --- a/locales/es.json +++ b/locales/es.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/fr.json b/locales/fr.json index a4988c0..e30782d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/hi.json b/locales/hi.json index 4c311fd..c3ce4ba 100644 --- a/locales/hi.json +++ b/locales/hi.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/id.json b/locales/id.json index a8d6cc1..26fe9f5 100644 --- a/locales/id.json +++ b/locales/id.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/it.json b/locales/it.json index 2134002..dcdaa0b 100644 --- a/locales/it.json +++ b/locales/it.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/nl.json b/locales/nl.json index 1d1215b..89522cf 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/pl.json b/locales/pl.json index a87b5bb..c2aae5c 100644 --- a/locales/pl.json +++ b/locales/pl.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/pt-BR.json b/locales/pt-BR.json index bfc3a50..3ff4e91 100644 --- a/locales/pt-BR.json +++ b/locales/pt-BR.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/pt.json b/locales/pt.json index 171c4dd..e795794 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/sr.json b/locales/sr.json index 44a8b1c..ba725b5 100644 --- a/locales/sr.json +++ b/locales/sr.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/sv.json b/locales/sv.json index e0253df..2a101aa 100644 --- a/locales/sv.json +++ b/locales/sv.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/locales/zh-Hans-CN.json b/locales/zh-Hans-CN.json index e202dbe..823ab53 100644 --- a/locales/zh-Hans-CN.json +++ b/locales/zh-Hans-CN.json @@ -70,7 +70,16 @@ "microsubChannel": "Microsub Channel", "microsubChannelHint": "Sync feeds from a specific channel, or all channels", "categoryPrefix": "Category Prefix", - "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')" + "categoryPrefixHint": "Optional prefix for blog categories (e.g., 'Following: ')", + "feedlandInstance": "FeedLand Instance URL", + "feedlandInstanceHint": "FeedLand instance URL (feedland.com or self-hosted)", + "feedlandUsername": "FeedLand Username", + "feedlandUsernameHint": "Your FeedLand screen name", + "feedlandCategory": "FeedLand Category", + "feedlandCategoryAll": "All subscriptions", + "feedlandCategoryHint": "Optional: sync only feeds from a specific category", + "feedlandLoadCategories": "Load", + "feedlandRequired": "FeedLand instance URL and username are required" } }, diff --git a/package.json b/package.json index 652bafc..a1df4b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-blogroll", - "version": "1.0.20", + "version": "1.0.21", "description": "Blogroll endpoint for Indiekit. Aggregates blog feeds from OPML, JSON feeds, or manual entry.", "keywords": [ "indiekit", diff --git a/views/blogroll-source-edit.njk b/views/blogroll-source-edit.njk index 5fcdeb2..64759e3 100644 --- a/views/blogroll-source-edit.njk +++ b/views/blogroll-source-edit.njk @@ -15,6 +15,7 @@ {% if microsubAvailable %} {% endif %} + {{ __("blogroll.sources.form.typeHint") }} @@ -48,6 +49,32 @@ {{ __("blogroll.sources.form.categoryPrefixHint") | default("Optional prefix for blog categories (e.g., 'Following: ')") }} + + + + + +