Feed-redirect: 301 for podcast som flyttes til ny host (oppgave 30.8)

Når redirect_feed er satt i podcast-trait, returnerer maskinrommet
HTTP 301 Moved Permanently med Location-header i stedet for å serve
feeden. iTunes new-feed-url-taggen bevares også i RSS-en for klienter
som ikke følger 301.

Admin-UI: erstatter det enkle tekstfeltet med tre tilstander:
- Inaktiv: knapp "Flytt podcast til annen plattform..."
- Bekreftelse: advarsel + URL-felt + rød "Aktiver redirect"-knapp
- Aktiv: gul statusindikator med deaktiver-knapp

Backend: sjekker redirect_feed tidlig i generate_feed() og returnerer
301 før noe annet arbeid gjøres (DB-oppslag for episodes osv).
This commit is contained in:
vegard 2026-03-19 00:31:39 +00:00
parent ba8d361626
commit c5239d2923
4 changed files with 98 additions and 17 deletions

View file

@ -202,8 +202,13 @@ Brukeren eier dataene sine. Flytte bort er enkelt:
} }
``` ```
Når satt: Caddy returnerer 301 for feed-URL. Når satt: maskinrommet returnerer HTTP 301 Moved Permanently for
Apple/Spotify oppdaterer automatisk. `/pub/{slug}/feed.xml` med `Location`-header til ny URL.
Apple/Spotify oppdaterer automatisk. I tillegg inkluderes
`<itunes:new-feed-url>` i RSS-en for klienter som ikke følger 301.
Admin-UI har én-klikks aktivering med advarsel. Redirecten kan
deaktiveres når som helst fra podcast-trait-innstillingene.
Brukeren kan også eksportere all data: Brukeren kan også eksportere all data:
- RSS-feed med alle episoder - RSS-feed med alle episoder

View file

@ -19,6 +19,8 @@
let explicit = $state((config.explicit as boolean) ?? false); let explicit = $state((config.explicit as boolean) ?? false);
let language = $state((config.language as string) ?? 'no'); let language = $state((config.language as string) ?? 'no');
let redirectFeed = $state((config.redirect_feed as string) ?? ''); let redirectFeed = $state((config.redirect_feed as string) ?? '');
let redirectInput = $state('');
let showRedirectConfirm = $state(false);
// iTunes category tree (Apple Podcasts standard) // iTunes category tree (Apple Podcasts standard)
const itunesCategories: Record<string, string[]> = { const itunesCategories: Record<string, string[]> = {
@ -182,19 +184,81 @@
<!-- Redirect Feed --> <!-- Redirect Feed -->
<div> <div>
<label for="podcast-redirect" class="block text-xs font-medium text-gray-600 mb-1"> <label class="block text-xs font-medium text-gray-600 mb-1">
Redirect-feed (valgfritt) Feed-redirect (flytte podcast)
</label> </label>
{#if redirectFeed}
<!-- Active redirect indicator -->
<div class="rounded border border-amber-300 bg-amber-50 p-3 space-y-2">
<div class="flex items-center gap-2">
<span class="inline-block h-2 w-2 rounded-full bg-amber-500"></span>
<span class="text-xs font-semibold text-amber-800">Redirect aktiv</span>
</div>
<p class="text-xs text-amber-700 break-all">
Feed-URL returnerer 301 til:<br/>
<a href={redirectFeed} target="_blank" rel="noopener" class="underline">{redirectFeed}</a>
</p>
<button
type="button"
onclick={() => { redirectFeed = ''; emitConfig(); }}
class="rounded bg-white border border-amber-400 px-3 py-1 text-xs font-medium text-amber-800 hover:bg-amber-100"
>
Deaktiver redirect
</button>
</div>
{:else if showRedirectConfirm}
<!-- Confirmation dialog -->
<div class="rounded border border-red-300 bg-red-50 p-3 space-y-2">
<p class="text-xs font-semibold text-red-800">
Advarsel: Dette aktiverer en permanent redirect (301)
</p>
<p class="text-xs text-red-700">
Alle som henter feeden vil bli sendt til den nye URL-en.
Podcastklienter (Apple, Spotify) oppdaterer automatisk.
Du kan deaktivere redirecten nar som helst.
</p>
<input <input
id="podcast-redirect" id="podcast-redirect"
type="url" type="url"
bind:value={redirectFeed} bind:value={redirectInput}
oninput={emitConfig}
placeholder="https://ny-host.example.com/feed.xml" placeholder="https://ny-host.example.com/feed.xml"
class="w-full rounded border border-gray-300 px-3 py-1.5 text-sm focus:border-indigo-500 focus:outline-none" class="w-full rounded border border-red-300 px-3 py-1.5 text-sm focus:border-red-500 focus:outline-none"
/> />
<div class="flex gap-2">
<button
type="button"
disabled={!redirectInput.trim()}
onclick={() => {
redirectFeed = redirectInput.trim();
showRedirectConfirm = false;
emitConfig();
}}
class="rounded bg-red-600 px-3 py-1 text-xs font-medium text-white hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Aktiver redirect
</button>
<button
type="button"
onclick={() => { showRedirectConfirm = false; redirectInput = ''; }}
class="rounded bg-white border border-gray-300 px-3 py-1 text-xs font-medium text-gray-700 hover:bg-gray-50"
>
Avbryt
</button>
</div>
</div>
{:else}
<!-- Inactive state — one-click to start -->
<button
type="button"
onclick={() => { showRedirectConfirm = true; }}
class="rounded border border-gray-300 px-3 py-1.5 text-xs font-medium text-gray-600 hover:bg-gray-50"
>
Flytt podcast til annen plattform...
</button>
<p class="mt-0.5 text-[11px] text-gray-400"> <p class="mt-0.5 text-[11px] text-gray-400">
Brukes for a flytte podcasten til en annen plattform. Setter itunes:new-feed-url i RSS. Aktiverer 301-redirect pa feed-URL-en slik at podcastklienter automatisk oppdaterer.
</p> </p>
{/if}
</div> </div>
</div> </div>

View file

@ -105,6 +105,19 @@ pub async fn generate_feed(
})? })?
.ok_or(StatusCode::NOT_FOUND)?; .ok_or(StatusCode::NOT_FOUND)?;
// 301 Moved Permanently når redirect_feed er satt (podcast flyttet til ny host)
if let Some(ref redirect_url) = collection.podcast_config.redirect_feed {
if !redirect_url.is_empty() {
tracing::info!(slug = %slug, redirect = %redirect_url, "Feed-redirect aktiv");
return Ok(Response::builder()
.status(StatusCode::MOVED_PERMANENTLY)
.header(header::LOCATION, redirect_url.as_str())
.header(header::CACHE_CONTROL, "public, max-age=3600")
.body("Moved Permanently".into())
.unwrap());
}
}
let max_items = collection.rss_config.max_items.unwrap_or(50); let max_items = collection.rss_config.max_items.unwrap_or(50);
let items = fetch_feed_items(&state.db, collection.id, max_items, collection.is_podcast) let items = fetch_feed_items(&state.db, collection.id, max_items, collection.is_podcast)
.await .await

View file

@ -437,5 +437,4 @@ prøveimport-flyt.
- [x] 30.7 Prøveimport-flyt i frontend: "Importer podcast"-wizard i admin. Steg 1: lim inn RSS-URL, vis forhåndsvisning av episoder. Steg 2: importer (kan ta tid for mange episoder). Steg 3: sjekk resultat, sammenlign feeds. Steg 4: re-importer nye episoder når klar. Steg 5: aktiver 301-redirect på gammel host. - [x] 30.7 Prøveimport-flyt i frontend: "Importer podcast"-wizard i admin. Steg 1: lim inn RSS-URL, vis forhåndsvisning av episoder. Steg 2: importer (kan ta tid for mange episoder). Steg 3: sjekk resultat, sammenlign feeds. Steg 4: re-importer nye episoder når klar. Steg 5: aktiver 301-redirect på gammel host.
### Eksport og redirect ### Eksport og redirect
- [~] 30.8 Feed-redirect: `redirect_feed`-felt i podcast-trait. Når satt: Caddy returnerer 301 for feed-URL. Brukeren kan alltid flytte bort. Admin-UI med én-klikks aktivering og advarsel. - [x] 30.8 Feed-redirect: `redirect_feed`-felt i podcast-trait. Når satt: Caddy returnerer 301 for feed-URL. Brukeren kan alltid flytte bort. Admin-UI med én-klikks aktivering og advarsel.
> Påbegynt: 2026-03-19T00:25