RSS/Atom-feed (oppgave 14.8): feed-discovery + betinget RSS-lenke
Feeden i rss.rs var allerede implementert med full RSS 2.0 og Atom 1.0 støtte, inkl. podcast-enclosures. Det som manglet var integrasjon med publiseringstemplates: - base.html: <link rel="alternate"> for feed auto-discovery (kun når samlingen har rss-trait) - base.html: RSS-lenke i nav vises kun for samlinger med rss-trait - publishing.rs: has_rss propageres fra CollectionRow gjennom alle render-funksjoner til Tera-kontekst - CAS-rendering (render_article_to_cas, render_index_to_cas) sjekker også rss-trait for korrekt template-kontekst Verifisert: kompilerer, alle 36 tester passerer, feed.xml returnerer gyldig RSS/Atom, 404 for ukjente slugs, discovery-link i HTML. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
71f7264100
commit
265adea0b3
3 changed files with 35 additions and 18 deletions
|
|
@ -329,6 +329,7 @@ pub fn render_article(
|
|||
collection_title: &str,
|
||||
base_url: &str,
|
||||
seo: &SeoData,
|
||||
has_rss: bool,
|
||||
) -> Result<String, tera::Error> {
|
||||
let css_vars = build_css_variables(theme, config);
|
||||
let template_name = format!("{theme}/article.html");
|
||||
|
|
@ -341,6 +342,7 @@ pub fn render_article(
|
|||
ctx.insert("base_url", base_url);
|
||||
ctx.insert("logo_hash", &config.logo_hash);
|
||||
ctx.insert("seo", seo);
|
||||
ctx.insert("has_rss", &has_rss);
|
||||
|
||||
tera.render(&template_name, &ctx)
|
||||
}
|
||||
|
|
@ -352,6 +354,7 @@ pub fn render_index(
|
|||
config: &ThemeConfig,
|
||||
index: &IndexData,
|
||||
base_url: &str,
|
||||
has_rss: bool,
|
||||
) -> Result<String, tera::Error> {
|
||||
let css_vars = build_css_variables(theme, config);
|
||||
let template_name = format!("{theme}/index.html");
|
||||
|
|
@ -362,6 +365,7 @@ pub fn render_index(
|
|||
ctx.insert("index", index);
|
||||
ctx.insert("base_url", base_url);
|
||||
ctx.insert("logo_hash", &config.logo_hash);
|
||||
ctx.insert("has_rss", &has_rss);
|
||||
|
||||
tera.render(&template_name, &ctx)
|
||||
}
|
||||
|
|
@ -405,13 +409,16 @@ pub async fn render_article_to_cas(
|
|||
return Err(format!("Samling {collection_id} finnes ikke"));
|
||||
};
|
||||
|
||||
let publishing_config: PublishingConfig = collection_metadata
|
||||
.get("traits")
|
||||
let coll_traits = collection_metadata.get("traits");
|
||||
|
||||
let publishing_config: PublishingConfig = coll_traits
|
||||
.and_then(|t| t.get("publishing"))
|
||||
.cloned()
|
||||
.map(|v| serde_json::from_value(v).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let has_rss = coll_traits.and_then(|t| t.get("rss")).is_some();
|
||||
|
||||
let slug = publishing_config.slug.as_deref().unwrap_or("unknown");
|
||||
let theme = publishing_config.theme.as_deref().unwrap_or("blogg");
|
||||
let config = &publishing_config.theme_config;
|
||||
|
|
@ -496,7 +503,7 @@ pub async fn render_article_to_cas(
|
|||
let seo = build_seo_data(&article_data, &collection_title, &canonical_url);
|
||||
|
||||
let tera = build_tera();
|
||||
let html = render_article(&tera, theme, config, &article_data, &collection_title, &base_url, &seo)
|
||||
let html = render_article(&tera, theme, config, &article_data, &collection_title, &base_url, &seo, has_rss)
|
||||
.map_err(|e| format!("Tera render-feil: {e}"))?;
|
||||
|
||||
// 5. Lagre i CAS
|
||||
|
|
@ -586,13 +593,16 @@ pub async fn render_index_to_cas(
|
|||
return Err(format!("Samling {collection_id} finnes ikke"));
|
||||
};
|
||||
|
||||
let publishing_config: PublishingConfig = collection_metadata
|
||||
.get("traits")
|
||||
let idx_traits = collection_metadata.get("traits");
|
||||
|
||||
let publishing_config: PublishingConfig = idx_traits
|
||||
.and_then(|t| t.get("publishing"))
|
||||
.cloned()
|
||||
.map(|v| serde_json::from_value(v).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let has_rss = idx_traits.and_then(|t| t.get("rss")).is_some();
|
||||
|
||||
let slug = publishing_config.slug.as_deref().unwrap_or("unknown");
|
||||
let theme = publishing_config.theme.as_deref().unwrap_or("blogg");
|
||||
let config = &publishing_config.theme_config;
|
||||
|
|
@ -621,7 +631,7 @@ pub async fn render_index_to_cas(
|
|||
|
||||
// Render med Tera
|
||||
let tera = build_tera();
|
||||
let html = render_index(&tera, theme, config, &index_data, &base_url)
|
||||
let html = render_index(&tera, theme, config, &index_data, &base_url, has_rss)
|
||||
.map_err(|e| format!("Tera render-feil (index): {e}"))?;
|
||||
|
||||
// Lagre i CAS
|
||||
|
|
@ -691,6 +701,7 @@ struct CollectionRow {
|
|||
id: Uuid,
|
||||
title: Option<String>,
|
||||
publishing_config: PublishingConfig,
|
||||
has_rss: bool,
|
||||
}
|
||||
|
||||
/// Finn samling med publishing-trait basert på slug.
|
||||
|
|
@ -715,17 +726,23 @@ async fn find_publishing_collection(
|
|||
return Ok(None);
|
||||
};
|
||||
|
||||
let publishing_config: PublishingConfig = metadata
|
||||
.get("traits")
|
||||
let traits = metadata.get("traits");
|
||||
|
||||
let publishing_config: PublishingConfig = traits
|
||||
.and_then(|t| t.get("publishing"))
|
||||
.cloned()
|
||||
.map(|v| serde_json::from_value(v).unwrap_or_default())
|
||||
.unwrap_or_default();
|
||||
|
||||
let has_rss = traits
|
||||
.and_then(|t| t.get("rss"))
|
||||
.is_some();
|
||||
|
||||
Ok(Some(CollectionRow {
|
||||
id,
|
||||
title,
|
||||
publishing_config,
|
||||
has_rss,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -1093,7 +1110,7 @@ pub async fn serve_index(
|
|||
};
|
||||
|
||||
let tera = build_tera();
|
||||
let html = render_index(&tera, theme, &config, &index_data, &base_url).map_err(|e| {
|
||||
let html = render_index(&tera, theme, &config, &index_data, &base_url, collection.has_rss).map_err(|e| {
|
||||
tracing::error!(slug = %slug, theme = %theme, error = %e, "Tera render-feil (index)");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
|
@ -1178,7 +1195,7 @@ pub async fn serve_article(
|
|||
let seo = build_seo_data(&fetched.article, &collection_title, &canonical_url);
|
||||
|
||||
let tera = build_tera();
|
||||
let html = render_article(&tera, theme, config, &fetched.article, &collection_title, &base_url, &seo)
|
||||
let html = render_article(&tera, theme, config, &fetched.article, &collection_title, &base_url, &seo, collection.has_rss)
|
||||
.map_err(|e| {
|
||||
tracing::error!(slug = %slug, article = %article_id, theme = %theme, error = %e, "Tera render-feil (artikkel)");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
|
|
@ -1229,7 +1246,7 @@ pub async fn preview_theme(
|
|||
let base_url = format!("/pub/{slug}");
|
||||
|
||||
let tera = build_tera();
|
||||
let html = render_index(&tera, &theme, &config, &index_data, &base_url).map_err(|e| {
|
||||
let html = render_index(&tera, &theme, &config, &index_data, &base_url, false).map_err(|e| {
|
||||
tracing::error!(theme = %theme, error = %e, "Tera render-feil (preview)");
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
|
|
@ -1316,7 +1333,7 @@ mod tests {
|
|||
let seo = default_seo();
|
||||
|
||||
for theme in &["avis", "magasin", "blogg", "tidsskrift"] {
|
||||
let html = render_article(&tera, theme, &config, &article, "Testsamling", "/pub/test", &seo)
|
||||
let html = render_article(&tera, theme, &config, &article, "Testsamling", "/pub/test", &seo, false)
|
||||
.unwrap_or_else(|e| panic!("Render feilet for {theme}: {e}"));
|
||||
assert!(html.contains("Testittel"), "Tittel mangler i {theme}");
|
||||
assert!(html.contains("Testinnhold"), "Innhold mangler i {theme}");
|
||||
|
|
@ -1346,7 +1363,7 @@ mod tests {
|
|||
json_ld: r#"{"@type":"Article"}"#.to_string(),
|
||||
};
|
||||
|
||||
let html = render_article(&tera, "blogg", &config, &article, "Testpub", "/pub/test", &seo)
|
||||
let html = render_article(&tera, "blogg", &config, &article, "Testpub", "/pub/test", &seo, false)
|
||||
.expect("Render feilet");
|
||||
|
||||
assert!(html.contains("og:title"), "OG-tittel mangler");
|
||||
|
|
@ -1377,7 +1394,7 @@ mod tests {
|
|||
};
|
||||
|
||||
for theme in &["avis", "magasin", "blogg", "tidsskrift"] {
|
||||
let html = render_index(&tera, theme, &config, &index, "/pub/test")
|
||||
let html = render_index(&tera, theme, &config, &index, "/pub/test", false)
|
||||
.unwrap_or_else(|e| panic!("Render feilet for {theme}: {e}"));
|
||||
assert!(html.contains("Testforside"), "Tittel mangler i {theme}");
|
||||
assert!(html.contains("Strøm-artikkel"), "Strøm-artikkel mangler i {theme}");
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{% block title %}{{ collection_title | default(value="Synops") }}{% endblock %}</title>
|
||||
{% if has_rss %}<link rel="alternate" type="application/rss+xml" title="{{ collection_title | default(value='RSS') }}" href="{{ base_url }}/feed.xml">{% endif %}
|
||||
{% block seo %}{% endblock %}
|
||||
<style>
|
||||
{{ css_variables | safe }}
|
||||
|
|
@ -72,9 +73,9 @@
|
|||
<a href="{{ base_url }}">{{ index.title | default(value=collection_title) | default(value="Synops") }}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<nav>
|
||||
{% if has_rss %}<nav>
|
||||
<a href="{{ base_url }}/feed.xml" title="RSS-feed">RSS</a>
|
||||
</nav>
|
||||
</nav>{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
|
|||
3
tasks.md
3
tasks.md
|
|
@ -149,8 +149,7 @@ Uavhengige faser kan fortsatt plukkes.
|
|||
- [x] 14.5 Slot-håndtering i maskinrommet: `slot` og `slot_order` i `belongs_to`-edge metadata. Ved ny hero → gammel hero flyttes til strøm. Ved featured over `featured_max` → FIFO tilbake til strøm. `pinned`-flagg forhindrer automatisk fjerning.
|
||||
- [x] 14.6 Forside-admin i frontend: visuell editor for hero/featured/strøm. Drag-and-drop mellom plasser. Pin-knapp. Forhåndsvisning. Oppdaterer edge-metadata via maskinrommet.
|
||||
- [x] 14.7 Publiseringsflyt i frontend (personlig): publiseringsknapp på noder i samlinger med `publishing`-trait der `require_approval: false`. Forhåndsvisning, slug-editor, bekreftelse. Avpublisering ved fjerning av edge.
|
||||
- [~] 14.8 RSS/Atom-feed: samling med `rss`-trait genererer feed automatisk ved publisering/avpublisering. `synops.no/pub/{slug}/feed.xml`. Maks `rss_max_items` (default 50).
|
||||
> Påbegynt: 2026-03-18T01:34
|
||||
- [x] 14.8 RSS/Atom-feed: samling med `rss`-trait genererer feed automatisk ved publisering/avpublisering. `synops.no/pub/{slug}/feed.xml`. Maks `rss_max_items` (default 50).
|
||||
- [ ] 14.9 Custom domains: bruker registrerer domene i `publishing`-trait. Maskinrommet validerer DNS, Caddy on-demand TLS med validerings-callback. Re-rendring med riktig canonical URL.
|
||||
- [ ] 14.10 Redaksjonell innsending: `submitted_to`-edge med status-metadata (`pending`, `in_review`, `revision_requested`, `rejected`, `approved`). Maskinrommet validerer at kun roller i `submission_roles` kan opprette `submitted_to`, og kun owner/admin kan endre status eller opprette `belongs_to`. Ref: `docs/concepts/publisering.md` § "Innsending".
|
||||
- [ ] 14.11 Redaktørens arbeidsflate: frontend-visning av noder med `submitted_to`-edge til samling, gruppert på status. Kanban-stil drag-and-drop for statusendring. Siste kolonne ("Planlagt") setter `publish_at` i edge-metadata.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue