@rmdes/indiekit-endpoint-blogroll 1.0.17 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # Blogroll Endpoint for Indiekit
2
+
3
+ An Indiekit plugin that provides a comprehensive blogroll management system with feed aggregation, admin UI, and public API.
4
+
5
+ ## Features
6
+
7
+ - **Multiple Source Types:** Import blogs from OPML files/URLs, Microsub subscriptions, or add manually
8
+ - **Background Feed Fetching:** Automatically syncs blogs and caches recent items
9
+ - **Microsub Integration:** Mirror your Microsub subscriptions as a blogroll (zero duplication)
10
+ - **Admin UI:** Manage sources, blogs, and view recent activity
11
+ - **Public JSON API:** Read-only endpoints for frontend integration
12
+ - **OPML Export:** Export your blogroll as OPML (all or by category)
13
+ - **Feed Discovery:** Auto-discover feeds from website URLs
14
+ - **Item Retention:** Automatic cleanup of old items (encourages fresh content discovery)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @rmdes/indiekit-endpoint-blogroll
20
+ ```
21
+
22
+ ## Configuration
23
+
24
+ Add to your `indiekit.config.js`:
25
+
26
+ ```javascript
27
+ import BlogrollEndpoint from "@rmdes/indiekit-endpoint-blogroll";
28
+
29
+ export default {
30
+ plugins: [
31
+ new BlogrollEndpoint({
32
+ mountPath: "/blogrollapi", // Admin UI and API base path
33
+ syncInterval: 3600000, // 1 hour (in milliseconds)
34
+ maxItemsPerBlog: 50, // Items to fetch per blog
35
+ maxItemAge: 7, // Days - older items auto-deleted
36
+ fetchTimeout: 15000 // 15 seconds per feed fetch
37
+ })
38
+ ]
39
+ };
40
+ ```
41
+
42
+ ## Requirements
43
+
44
+ - **Indiekit:** `>=1.0.0-beta.25`
45
+ - **MongoDB:** Required for data storage
46
+ - **Optional:** `@rmdes/indiekit-endpoint-microsub` for Microsub integration
47
+
48
+ ## Usage
49
+
50
+ ### Admin UI
51
+
52
+ Navigate to `/blogrollapi` in your Indiekit instance to access:
53
+
54
+ - **Dashboard:** View sync status, blog counts, recent activity
55
+ - **Sources:** Manage OPML and Microsub sources
56
+ - **Blogs:** Add/edit/delete individual blogs, refresh feeds
57
+ - **Manual Sync:** Trigger immediate sync or clear and resync
58
+
59
+ ### Source Types
60
+
61
+ 1. **OPML URL:** Point to a public OPML file (e.g., your feed reader's export)
62
+ 2. **OPML File:** Paste OPML XML directly into the form
63
+ 3. **Microsub:** Import subscriptions from your Microsub channels
64
+ 4. **Manual:** Add individual blog feeds one at a time
65
+
66
+ ### Public API
67
+
68
+ All API endpoints return JSON (except OPML export which returns XML).
69
+
70
+ **List Blogs**
71
+ ```
72
+ GET /blogrollapi/api/blogs?category=Tech&limit=100&offset=0
73
+ ```
74
+
75
+ **Get Blog with Recent Items**
76
+ ```
77
+ GET /blogrollapi/api/blogs/:id
78
+ ```
79
+
80
+ **List Items Across All Blogs**
81
+ ```
82
+ GET /blogrollapi/api/items?blog=<id>&category=Tech&limit=50&offset=0
83
+ ```
84
+
85
+ **List Categories**
86
+ ```
87
+ GET /blogrollapi/api/categories
88
+ ```
89
+
90
+ **Sync Status**
91
+ ```
92
+ GET /blogrollapi/api/status
93
+ ```
94
+
95
+ **Export OPML**
96
+ ```
97
+ GET /blogrollapi/api/opml (all blogs)
98
+ GET /blogrollapi/api/opml/:category (specific category)
99
+ ```
100
+
101
+ ### Example Response
102
+
103
+ **GET /blogrollapi/api/blogs**
104
+ ```json
105
+ {
106
+ "items": [
107
+ {
108
+ "id": "507f1f77bcf86cd799439011",
109
+ "title": "Example Blog",
110
+ "description": "A great blog about tech",
111
+ "feedUrl": "https://example.com/feed",
112
+ "siteUrl": "https://example.com",
113
+ "feedType": "rss",
114
+ "category": "Tech",
115
+ "tags": ["programming", "web"],
116
+ "photo": "https://example.com/icon.png",
117
+ "status": "active",
118
+ "itemCount": 25,
119
+ "pinned": false,
120
+ "lastFetchAt": "2026-02-13T10:30:00.000Z"
121
+ }
122
+ ],
123
+ "total": 42,
124
+ "hasMore": true
125
+ }
126
+ ```
127
+
128
+ **GET /blogrollapi/api/items**
129
+ ```json
130
+ {
131
+ "items": [
132
+ {
133
+ "id": "507f1f77bcf86cd799439011",
134
+ "url": "https://example.com/post/hello",
135
+ "title": "Hello World",
136
+ "summary": "My first blog post...",
137
+ "published": "2026-02-13T10:00:00.000Z",
138
+ "isFuture": false,
139
+ "author": { "name": "Jane Doe" },
140
+ "photo": ["https://example.com/image.jpg"],
141
+ "categories": ["announcement"],
142
+ "blog": {
143
+ "id": "507f1f77bcf86cd799439011",
144
+ "title": "Example Blog",
145
+ "siteUrl": "https://example.com",
146
+ "category": "Tech",
147
+ "photo": "https://example.com/icon.png"
148
+ }
149
+ }
150
+ ],
151
+ "hasMore": false
152
+ }
153
+ ```
154
+
155
+ ## Microsub Integration
156
+
157
+ If you have `@rmdes/indiekit-endpoint-microsub` installed, the blogroll can mirror your subscriptions:
158
+
159
+ 1. Create a Microsub source in the admin UI
160
+ 2. Select specific channels or sync all channels
161
+ 3. Add a category prefix (optional) to distinguish Microsub blogs
162
+ 4. Blogs and items are referenced, not duplicated
163
+
164
+ **Benefits:**
165
+ - Zero data duplication - items are served directly from Microsub
166
+ - Automatic orphan cleanup when feeds are unsubscribed
167
+ - Webhook support for real-time updates
168
+
169
+ ## Background Sync
170
+
171
+ The plugin automatically syncs in the background:
172
+
173
+ 1. **Initial Sync:** Runs 15 seconds after server startup
174
+ 2. **Periodic Sync:** Runs every `syncInterval` milliseconds (default 1 hour)
175
+ 3. **What it Does:**
176
+ - Syncs enabled sources (OPML/Microsub)
177
+ - Fetches new items from active blogs
178
+ - Deletes items older than `maxItemAge` days
179
+ - Updates sync statistics
180
+
181
+ **Manual Sync:**
182
+ - Trigger from the dashboard
183
+ - Use `POST /blogrollapi/sync` (protected endpoint)
184
+ - Use `POST /blogrollapi/clear-resync` to clear and resync all
185
+
186
+ ## Feed Discovery
187
+
188
+ The plugin includes auto-discovery for finding feeds from website URLs:
189
+
190
+ ```javascript
191
+ // In the admin UI, when adding a blog, paste a website URL
192
+ // The plugin will:
193
+ // 1. Check <link rel="alternate"> tags in HTML
194
+ // 2. Try common feed paths (/feed, /rss, /atom.xml, etc.)
195
+ // 3. Suggest discovered feeds
196
+ ```
197
+
198
+ ## Item Retention
199
+
200
+ By default, items older than 7 days are automatically deleted during sync. This encourages discovery of fresh content rather than archiving everything.
201
+
202
+ **To Change Retention:**
203
+ ```javascript
204
+ new BlogrollEndpoint({
205
+ maxItemAge: 30 // Keep items for 30 days instead
206
+ })
207
+ ```
208
+
209
+ ## Blog Status
210
+
211
+ - **active:** Blog is working, fetching items normally
212
+ - **error:** Last fetch failed (see `lastError` for details)
213
+ - **deleted:** Soft-deleted, won't be recreated by sync
214
+
215
+ ## Navigation
216
+
217
+ The plugin adds itself to Indiekit's navigation:
218
+
219
+ - **Menu Item:** "Blogroll" (requires database)
220
+ - **Shortcut:** Bookmark icon in admin dashboard
221
+
222
+ ## Security
223
+
224
+ - **Protected Routes:** Admin UI and management endpoints require authentication
225
+ - **Public Routes:** Read-only API endpoints are publicly accessible
226
+ - **XSS Prevention:** Feed content is sanitized with `sanitize-html`
227
+ - **Feed Discovery:** Protected to prevent abuse (requires authentication)
228
+
229
+ ## Supported Feed Formats
230
+
231
+ - RSS 2.0
232
+ - Atom 1.0
233
+ - JSON Feed 1.0
234
+
235
+ ## Contributing
236
+
237
+ Report issues at: https://github.com/rmdes/indiekit-endpoint-blogroll/issues
238
+
239
+ ## License
240
+
241
+ MIT
@@ -0,0 +1,133 @@
1
+ {
2
+ "blogroll": {
3
+ "title": "Blogroll",
4
+ "description": "Verwalten Sie Ihre Blogroll-Quellen und Blogs",
5
+ "enabled": "Aktiviert",
6
+ "disabled": "Deaktiviert",
7
+ "edit": "Bearbeiten",
8
+ "sync": "Synchronisieren",
9
+ "refresh": "Aktualisieren",
10
+ "cancel": "Abbrechen",
11
+ "never": "Nie",
12
+
13
+ "stats": {
14
+ "title": "Übersicht",
15
+ "sources": "Quellen",
16
+ "blogs": "Blogs",
17
+ "items": "Einträge",
18
+ "errors": "Fehler",
19
+ "lastSync": "Letzte Synchronisation"
20
+ },
21
+
22
+ "actions": {
23
+ "title": "Aktionen",
24
+ "syncNow": "Jetzt synchronisieren",
25
+ "clearResync": "Löschen & Neu synchronisieren",
26
+ "clearConfirm": "Dadurch werden alle zwischengespeicherten Einträge gelöscht und alles neu abgerufen. Fortfahren?"
27
+ },
28
+
29
+ "errors": {
30
+ "title": "Blogs mit Fehlern",
31
+ "seeAll": "Alle %{count} Blogs mit Fehlern anzeigen"
32
+ },
33
+
34
+ "sources": {
35
+ "title": "OPML-Synchronisation",
36
+ "manage": "OPML-Synchronisation",
37
+ "add": "OPML-Quelle hinzufügen",
38
+ "new": "Neue OPML-Quelle",
39
+ "edit": "OPML-Quelle bearbeiten",
40
+ "create": "Erstellen",
41
+ "save": "Speichern",
42
+ "empty": "Keine OPML-Quellen konfiguriert. Verwenden Sie diese Funktion, um Blogs aus FreshRSS oder anderen Feed-Readern zu importieren.",
43
+ "recent": "OPML-Quellen",
44
+ "interval": "Alle %{minutes} Min.",
45
+ "lastSync": "Zuletzt synchronisiert",
46
+ "deleteConfirm": "Diese OPML-Quelle löschen? Importierte Blogs bleiben erhalten.",
47
+ "created": "OPML-Quelle erfolgreich erstellt.",
48
+ "created_synced": "OPML-Quelle erfolgreich erstellt und synchronisiert.",
49
+ "created_sync_failed": "OPML-Quelle erstellt, aber Synchronisation fehlgeschlagen: %{error}",
50
+ "updated": "OPML-Quelle erfolgreich aktualisiert.",
51
+ "deleted": "OPML-Quelle erfolgreich gelöscht.",
52
+ "synced": "Erfolgreich synchronisiert. Hinzugefügt: %{added}, Aktualisiert: %{updated}",
53
+ "form": {
54
+ "name": "Name",
55
+ "type": "Import-Typ",
56
+ "typeHint": "URL synchronisiert regelmäßig, Datei ist ein einmaliger Import",
57
+ "url": "OPML-URL",
58
+ "urlHint": "URL zu Ihrer OPML-Datei (z.B. FreshRSS-Export-URL)",
59
+ "opmlContent": "OPML-Inhalt",
60
+ "opmlContentHint": "Fügen Sie hier den vollständigen OPML-XML-Inhalt ein",
61
+ "syncInterval": "Synchronisationsintervall",
62
+ "enabled": "Automatische Synchronisation aktivieren"
63
+ }
64
+ },
65
+
66
+ "blogs": {
67
+ "title": "Blogs",
68
+ "manage": "Blogs verwalten",
69
+ "add": "Blog hinzufügen",
70
+ "new": "Neuer Blog",
71
+ "edit": "Blog bearbeiten",
72
+ "create": "Blog hinzufügen",
73
+ "save": "Blog speichern",
74
+ "empty": "Noch keine Blogs. Fügen Sie einen hinzu oder importieren Sie aus einer OPML-Quelle.",
75
+ "recent": "Aktuelle Blogs",
76
+ "pinned": "Angeheftet",
77
+ "hidden": "Versteckt",
78
+ "noItems": "Noch keine Einträge abgerufen.",
79
+ "recentItems": "Aktuelle Einträge",
80
+ "allCategories": "Alle Kategorien",
81
+ "allStatuses": "Alle Status",
82
+ "statusActive": "Aktiv",
83
+ "statusError": "Fehler",
84
+ "statusPending": "Ausstehend",
85
+ "clearFilters": "Filter löschen",
86
+ "deleteConfirm": "Diesen Blog und alle zwischengespeicherten Einträge löschen?",
87
+ "created": "Blog erfolgreich hinzugefügt.",
88
+ "created_synced": "Blog hinzugefügt und synchronisiert. %{items} Einträge abgerufen.",
89
+ "created_sync_failed": "Blog hinzugefügt, aber erster Abruf fehlgeschlagen: %{error}",
90
+ "updated": "Blog erfolgreich aktualisiert.",
91
+ "deleted": "Blog erfolgreich gelöscht.",
92
+ "refreshed": "Blog aktualisiert. %{items} neue Einträge hinzugefügt.",
93
+ "form": {
94
+ "discoverUrl": "Website-URL",
95
+ "discover": "Feed entdecken",
96
+ "discoverHint": "Geben Sie eine Website-URL ein, um automatisch RSS/Atom-Feeds zu finden",
97
+ "discoverNoUrl": "Bitte geben Sie eine Website-URL ein",
98
+ "discovering": "Suche läuft...",
99
+ "discoveringHint": "Suche nach RSS/Atom-Feeds...",
100
+ "discoverFailed": "Feeds konnten nicht gefunden werden",
101
+ "discoverNoFeeds": "Keine Feeds auf dieser Website gefunden",
102
+ "discoverFoundOne": "Feed gefunden:",
103
+ "discoverFoundMultiple": "Mehrere Feeds gefunden. Klicken Sie auf einen, um ihn auszuwählen:",
104
+ "discoverSelected": "Ausgewählter Feed:",
105
+ "feedUrl": "Feed-URL",
106
+ "feedUrlHint": "RSS-, Atom- oder JSON-Feed-URL",
107
+ "title": "Titel",
108
+ "titlePlaceholder": "Automatisch aus Feed erkannt",
109
+ "titleHint": "Leer lassen, um den Feed-Titel zu verwenden",
110
+ "siteUrl": "Website-URL",
111
+ "siteUrlHint": "Link zur Startseite des Blogs (optional)",
112
+ "category": "Kategorie",
113
+ "categoryHint": "Gruppieren Sie Blogs nach Kategorie zum Filtern und OPML-Export",
114
+ "tags": "Schlagwörter",
115
+ "tagsHint": "Kommagetrennte Schlagwörter für zusätzliche Organisation",
116
+ "notes": "Notizen",
117
+ "notesPlaceholder": "Warum Sie diesem Blog folgen...",
118
+ "notesHint": "Persönliche Notizen (nicht öffentlich sichtbar)",
119
+ "pinned": "Diesen Blog anheften (oben in Listen anzeigen)",
120
+ "hidden": "Vor öffentlicher API verbergen (nur für Sie sichtbar)"
121
+ }
122
+ },
123
+
124
+ "api": {
125
+ "title": "API-Endpunkte",
126
+ "blogs": "Alle Blogs mit Metadaten auflisten",
127
+ "items": "Aktuelle Einträge von allen Blogs auflisten",
128
+ "categories": "Alle Kategorien auflisten",
129
+ "opml": "Als OPML exportieren",
130
+ "status": "Synchronisationsstatus und Statistiken"
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,133 @@
1
+ {
2
+ "blogroll": {
3
+ "title": "Blogroll",
4
+ "description": "Administra tus fuentes de blogroll y blogs",
5
+ "enabled": "Habilitado",
6
+ "disabled": "Deshabilitado",
7
+ "edit": "Editar",
8
+ "sync": "Sincronizar",
9
+ "refresh": "Actualizar",
10
+ "cancel": "Cancelar",
11
+ "never": "Nunca",
12
+
13
+ "stats": {
14
+ "title": "Resumen",
15
+ "sources": "Fuentes",
16
+ "blogs": "Blogs",
17
+ "items": "Entradas",
18
+ "errors": "Errores",
19
+ "lastSync": "Última sincronización"
20
+ },
21
+
22
+ "actions": {
23
+ "title": "Acciones",
24
+ "syncNow": "Sincronizar todo ahora",
25
+ "clearResync": "Limpiar y resincronizar",
26
+ "clearConfirm": "Esto eliminará todas las entradas almacenadas en caché y volverá a descargar todo. ¿Continuar?"
27
+ },
28
+
29
+ "errors": {
30
+ "title": "Blogs con errores",
31
+ "seeAll": "Ver los %{count} blogs con errores"
32
+ },
33
+
34
+ "sources": {
35
+ "title": "Sincronización OPML",
36
+ "manage": "Sincronización OPML",
37
+ "add": "Agregar fuente OPML",
38
+ "new": "Nueva fuente OPML",
39
+ "edit": "Editar fuente OPML",
40
+ "create": "Crear",
41
+ "save": "Guardar",
42
+ "empty": "No hay fuentes OPML configuradas. Usa esto para importar blogs de forma masiva desde FreshRSS u otros lectores de feeds.",
43
+ "recent": "Fuentes OPML",
44
+ "interval": "Cada %{minutes} min",
45
+ "lastSync": "Última sincronización",
46
+ "deleteConfirm": "¿Eliminar esta fuente OPML? Los blogs importados se conservarán.",
47
+ "created": "Fuente OPML creada exitosamente.",
48
+ "created_synced": "Fuente OPML creada y sincronizada exitosamente.",
49
+ "created_sync_failed": "Fuente OPML creada, pero la sincronización falló: %{error}",
50
+ "updated": "Fuente OPML actualizada exitosamente.",
51
+ "deleted": "Fuente OPML eliminada exitosamente.",
52
+ "synced": "Sincronización exitosa. Agregados: %{added}, Actualizados: %{updated}",
53
+ "form": {
54
+ "name": "Nombre",
55
+ "type": "Tipo de importación",
56
+ "typeHint": "La URL sincroniza periódicamente, el archivo es una importación única",
57
+ "url": "URL OPML",
58
+ "urlHint": "URL de tu archivo OPML (ej., URL de exportación de FreshRSS)",
59
+ "opmlContent": "Contenido OPML",
60
+ "opmlContentHint": "Pega aquí el contenido XML OPML completo",
61
+ "syncInterval": "Intervalo de sincronización",
62
+ "enabled": "Habilitar sincronización automática"
63
+ }
64
+ },
65
+
66
+ "blogs": {
67
+ "title": "Blogs",
68
+ "manage": "Administrar blogs",
69
+ "add": "Agregar blog",
70
+ "new": "Nuevo blog",
71
+ "edit": "Editar blog",
72
+ "create": "Agregar blog",
73
+ "save": "Guardar blog",
74
+ "empty": "Todavía no hay blogs. Agrega uno o importa desde una fuente OPML.",
75
+ "recent": "Blogs recientes",
76
+ "pinned": "Fijado",
77
+ "hidden": "Oculto",
78
+ "noItems": "Aún no se descargaron entradas.",
79
+ "recentItems": "Entradas recientes",
80
+ "allCategories": "Todas las categorías",
81
+ "allStatuses": "Todos los estados",
82
+ "statusActive": "Activo",
83
+ "statusError": "Error",
84
+ "statusPending": "Pendiente",
85
+ "clearFilters": "Limpiar filtros",
86
+ "deleteConfirm": "¿Eliminar este blog y todas sus entradas almacenadas?",
87
+ "created": "Blog agregado exitosamente.",
88
+ "created_synced": "Blog agregado y sincronizado. Se descargaron %{items} entradas.",
89
+ "created_sync_failed": "Blog agregado, pero la descarga inicial falló: %{error}",
90
+ "updated": "Blog actualizado exitosamente.",
91
+ "deleted": "Blog eliminado exitosamente.",
92
+ "refreshed": "Blog actualizado. Se agregaron %{items} entradas nuevas.",
93
+ "form": {
94
+ "discoverUrl": "URL del sitio web",
95
+ "discover": "Descubrir feed",
96
+ "discoverHint": "Ingresa una URL de sitio web para descubrir automáticamente su feed RSS/Atom",
97
+ "discoverNoUrl": "Por favor ingresa una URL de sitio web",
98
+ "discovering": "Descubriendo...",
99
+ "discoveringHint": "Buscando feeds RSS/Atom...",
100
+ "discoverFailed": "No se pudieron descubrir feeds",
101
+ "discoverNoFeeds": "No se encontraron feeds en este sitio web",
102
+ "discoverFoundOne": "Feed encontrado:",
103
+ "discoverFoundMultiple": "Se encontraron varios feeds. Hace clic en uno para seleccionarlo:",
104
+ "discoverSelected": "Feed seleccionado:",
105
+ "feedUrl": "URL del feed",
106
+ "feedUrlHint": "URL de RSS, Atom o JSON Feed",
107
+ "title": "Título",
108
+ "titlePlaceholder": "Detectado automáticamente del feed",
109
+ "titleHint": "Dejar en blanco para usar el título del feed",
110
+ "siteUrl": "URL del sitio",
111
+ "siteUrlHint": "Enlace a la página principal del blog (opcional)",
112
+ "category": "Categoría",
113
+ "categoryHint": "Agrupa blogs por categoría para filtrar y exportar a OPML",
114
+ "tags": "Etiquetas",
115
+ "tagsHint": "Etiquetas separadas por comas para organización adicional",
116
+ "notes": "Notas",
117
+ "notesPlaceholder": "Por qué sigues este blog...",
118
+ "notesHint": "Notas personales (no se muestran públicamente)",
119
+ "pinned": "Fijar este blog (mostrar al inicio de las listas)",
120
+ "hidden": "Ocultar de la API pública (visible solo para vos)"
121
+ }
122
+ },
123
+
124
+ "api": {
125
+ "title": "Endpoints de la API",
126
+ "blogs": "Listar todos los blogs con metadatos",
127
+ "items": "Listar entradas recientes de todos los blogs",
128
+ "categories": "Listar todas las categorías",
129
+ "opml": "Exportar como OPML",
130
+ "status": "Estado de sincronización y estadísticas"
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,133 @@
1
+ {
2
+ "blogroll": {
3
+ "title": "Blogroll",
4
+ "description": "Gestiona tus fuentes de blogroll y blogs",
5
+ "enabled": "Activado",
6
+ "disabled": "Desactivado",
7
+ "edit": "Editar",
8
+ "sync": "Sincronizar",
9
+ "refresh": "Actualizar",
10
+ "cancel": "Cancelar",
11
+ "never": "Nunca",
12
+
13
+ "stats": {
14
+ "title": "Resumen",
15
+ "sources": "Fuentes",
16
+ "blogs": "Blogs",
17
+ "items": "Entradas",
18
+ "errors": "Errores",
19
+ "lastSync": "Última sincronización"
20
+ },
21
+
22
+ "actions": {
23
+ "title": "Acciones",
24
+ "syncNow": "Sincronizar todo ahora",
25
+ "clearResync": "Borrar y resincronizar",
26
+ "clearConfirm": "Esto eliminará todas las entradas almacenadas en caché y volverá a obtenerlo todo. ¿Continuar?"
27
+ },
28
+
29
+ "errors": {
30
+ "title": "Blogs con errores",
31
+ "seeAll": "Ver todos los %{count} blogs con errores"
32
+ },
33
+
34
+ "sources": {
35
+ "title": "Sincronización OPML",
36
+ "manage": "Sincronización OPML",
37
+ "add": "Añadir fuente OPML",
38
+ "new": "Nueva fuente OPML",
39
+ "edit": "Editar fuente OPML",
40
+ "create": "Crear",
41
+ "save": "Guardar",
42
+ "empty": "No hay fuentes OPML configuradas. Utiliza esto para importar blogs en bloque desde FreshRSS u otros lectores de feeds.",
43
+ "recent": "Fuentes OPML",
44
+ "interval": "Cada %{minutes} min",
45
+ "lastSync": "Última sincronización",
46
+ "deleteConfirm": "¿Eliminar esta fuente OPML? Los blogs importados se conservarán.",
47
+ "created": "Fuente OPML creada correctamente.",
48
+ "created_synced": "Fuente OPML creada y sincronizada correctamente.",
49
+ "created_sync_failed": "Fuente OPML creada, pero la sincronización falló: %{error}",
50
+ "updated": "Fuente OPML actualizada correctamente.",
51
+ "deleted": "Fuente OPML eliminada correctamente.",
52
+ "synced": "Sincronización exitosa. Añadidos: %{added}, Actualizados: %{updated}",
53
+ "form": {
54
+ "name": "Nombre",
55
+ "type": "Tipo de importación",
56
+ "typeHint": "La URL sincroniza periódicamente, el archivo es una importación única",
57
+ "url": "URL OPML",
58
+ "urlHint": "URL de tu archivo OPML (p. ej., URL de exportación de FreshRSS)",
59
+ "opmlContent": "Contenido OPML",
60
+ "opmlContentHint": "Pega aquí el contenido XML OPML completo",
61
+ "syncInterval": "Intervalo de sincronización",
62
+ "enabled": "Activar sincronización automática"
63
+ }
64
+ },
65
+
66
+ "blogs": {
67
+ "title": "Blogs",
68
+ "manage": "Gestionar blogs",
69
+ "add": "Añadir blog",
70
+ "new": "Nuevo blog",
71
+ "edit": "Editar blog",
72
+ "create": "Añadir blog",
73
+ "save": "Guardar blog",
74
+ "empty": "Todavía no hay blogs. Añade uno o impórtalo desde una fuente OPML.",
75
+ "recent": "Blogs recientes",
76
+ "pinned": "Fijado",
77
+ "hidden": "Oculto",
78
+ "noItems": "Aún no se han obtenido entradas.",
79
+ "recentItems": "Entradas recientes",
80
+ "allCategories": "Todas las categorías",
81
+ "allStatuses": "Todos los estados",
82
+ "statusActive": "Activo",
83
+ "statusError": "Error",
84
+ "statusPending": "Pendiente",
85
+ "clearFilters": "Limpiar filtros",
86
+ "deleteConfirm": "¿Eliminar este blog y todas sus entradas almacenadas?",
87
+ "created": "Blog añadido correctamente.",
88
+ "created_synced": "Blog añadido y sincronizado. Se obtuvieron %{items} entradas.",
89
+ "created_sync_failed": "Blog añadido, pero la obtención inicial falló: %{error}",
90
+ "updated": "Blog actualizado correctamente.",
91
+ "deleted": "Blog eliminado correctamente.",
92
+ "refreshed": "Blog actualizado. Se añadieron %{items} entradas nuevas.",
93
+ "form": {
94
+ "discoverUrl": "URL del sitio web",
95
+ "discover": "Descubrir feed",
96
+ "discoverHint": "Introduce una URL de sitio web para descubrir automáticamente su feed RSS/Atom",
97
+ "discoverNoUrl": "Por favor, introduce una URL de sitio web",
98
+ "discovering": "Descubriendo...",
99
+ "discoveringHint": "Buscando feeds RSS/Atom...",
100
+ "discoverFailed": "No se pudieron descubrir feeds",
101
+ "discoverNoFeeds": "No se encontraron feeds en este sitio web",
102
+ "discoverFoundOne": "Feed encontrado:",
103
+ "discoverFoundMultiple": "Se encontraron varios feeds. Haz clic en uno para seleccionarlo:",
104
+ "discoverSelected": "Feed seleccionado:",
105
+ "feedUrl": "URL del feed",
106
+ "feedUrlHint": "URL de RSS, Atom o JSON Feed",
107
+ "title": "Título",
108
+ "titlePlaceholder": "Detectado automáticamente del feed",
109
+ "titleHint": "Dejar en blanco para usar el título del feed",
110
+ "siteUrl": "URL del sitio",
111
+ "siteUrlHint": "Enlace a la página principal del blog (opcional)",
112
+ "category": "Categoría",
113
+ "categoryHint": "Agrupa blogs por categoría para filtrar y exportar a OPML",
114
+ "tags": "Etiquetas",
115
+ "tagsHint": "Etiquetas separadas por comas para organización adicional",
116
+ "notes": "Notas",
117
+ "notesPlaceholder": "Por qué sigues este blog...",
118
+ "notesHint": "Notas personales (no se muestran públicamente)",
119
+ "pinned": "Fijar este blog (mostrar en la parte superior de las listas)",
120
+ "hidden": "Ocultar de la API pública (visible solo para ti)"
121
+ }
122
+ },
123
+
124
+ "api": {
125
+ "title": "Puntos de acceso de la API",
126
+ "blogs": "Listar todos los blogs con metadatos",
127
+ "items": "Listar entradas recientes de todos los blogs",
128
+ "categories": "Listar todas las categorías",
129
+ "opml": "Exportar como OPML",
130
+ "status": "Estado de sincronización y estadísticas"
131
+ }
132
+ }
133
+ }