feat: Phase 1 - Enhanced feed discovery with validation

- 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>
This commit is contained in:
Ricardo
2026-02-07 01:39:58 +01:00
parent 6caf37a003
commit ab6f81bf72
9 changed files with 609 additions and 10 deletions

View File

@@ -25,16 +25,40 @@
</div>
</form>
{% if validationError %}
<div class="notice notice--error">
<p>{{ validationError }}</p>
</div>
{% endif %}
{% if discoveryError %}
<div class="notice notice--error">
<p>{{ discoveryError }}</p>
</div>
{% endif %}
{% if results and results.length > 0 %}
<div class="search__results">
<h3>{{ __("microsub.search.title") }}</h3>
<div class="search__list">
{% for result in results %}
<div class="search__item">
<div class="search__item{% if not result.valid %} search__item--invalid{% endif %}{% if result.isCommentsFeed %} search__item--comments{% endif %}">
<div class="search__feed">
<span class="search__name">{{ result.title or "Feed" }}</span>
<span class="search__name">
{{ result.title or "Feed" }}
<span class="search__type badge badge--{% if result.valid %}info{% else %}warning{% endif %}">
{{ result.typeLabel }}
</span>
{% if result.isCommentsFeed %}
<span class="search__type badge badge--warning">Comments</span>
{% endif %}
</span>
<span class="search__url">{{ result.url | replace("https://", "") | replace("http://", "") }}</span>
{% if not result.valid %}
<span class="search__error">{{ result.error }}</span>
{% endif %}
</div>
{% if result.valid %}
<form method="post" action="{{ baseUrl }}/subscribe" class="search__subscribe">
<input type="hidden" name="url" value="{{ result.url }}">
<label for="channel-{{ loop.index }}" class="visually-hidden">{{ __("microsub.channels.title") }}</label>
@@ -48,6 +72,9 @@
classes: "button--small"
}) }}
</form>
{% else %}
<span class="search__invalid-badge">Invalid</span>
{% endif %}
</div>
{% endfor %}
</div>