Merge branch 'main' of dokku:firehose.apps.sustainabledelivery.com

This commit is contained in:
Firehose Bot 2026-07-13 15:02:22 +01:00
commit 4ead5702fb
10 changed files with 130 additions and 11 deletions

View File

@ -420,6 +420,30 @@ defmodule FirehoseWeb.CoreComponents do
""" """
end end
@doc """
Renders an RSS feed icon (inline SVG).
## Examples
<.rss_icon class="size-5" />
"""
attr :class, :string, default: "size-4"
attr :rest, :global
def rss_icon(assigns) do
~H"""
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class={[@class]}
{@rest}
>
<path d="M6.18 15.64a2.18 2.18 0 0 1 2.18 2.18C8.36 19 7.38 20 6.18 20C5 20 4 19 4 17.82a2.18 2.18 0 0 1 2.18-2.18M4 4.44A15.56 15.56 0 0 1 19.56 20h-2.83A12.73 12.73 0 0 0 4 7.27V4.44m0 5.66a9.9 9.9 0 0 1 9.9 9.9h-2.83A7.07 7.07 0 0 0 4 12.93v-2.83Z" />
</svg>
"""
end
## JS Commands ## JS Commands
def show(js \\ %JS{}, selector) do def show(js \\ %JS{}, selector) do

View File

@ -39,4 +39,30 @@
</div> </div>
</main> </main>
<footer class="border-t border-base-200 px-4 py-8 sm:px-6 lg:px-8">
<div class="mx-auto max-w-2xl space-y-3">
<h3 class="text-sm font-semibold tracking-wide text-base-content/60 uppercase">Subscribe</h3>
<div class="flex flex-wrap gap-4">
<a
href="/api/blog/engineering/feed.xml"
class="inline-flex items-center gap-2 text-sm text-base-content/70 hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-4 text-orange-500" />
Engineering Blog
</a>
<a
href="/api/blog/releases/feed.xml"
class="inline-flex items-center gap-2 text-sm text-base-content/70 hover:text-primary transition-colors"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-4 text-orange-500" />
Release Notes
</a>
</div>
</div>
</footer>
<.flash_group flash={@flash} /> <.flash_group flash={@flash} />

View File

@ -41,6 +41,9 @@
<% end %> <% end %>
<meta name="twitter:card" content={meta.twitter_card} /> <meta name="twitter:card" content={meta.twitter_card} />
<% end %> <% end %>
<%= for feed <- get_content_for(assigns, :feed_links) do %>
<link rel="alternate" type="application/rss+xml" title={feed.title} href={feed.rss_url} />
<% end %>
</head> </head>
<body> <body>
{@inner_content} {@inner_content}

View File

@ -9,9 +9,11 @@ defmodule FirehoseWeb.BlogController do
result = blog.paginate(page) result = blog.paginate(page)
meta = Blogex.SEO.meta_tags_for_blog(blog, FirehoseWeb.Endpoint.url()) meta = Blogex.SEO.meta_tags_for_blog(blog, FirehoseWeb.Endpoint.url())
rss_url = rss_feed_url(blog)
conn conn
|> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta) |> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta)
|> FirehoseWeb.Layouts.put_content_for(:feed_links, %{rss_url: rss_url, title: blog.title()})
|> render(:index, |> render(:index,
page_title: blog.title(), page_title: blog.title(),
blog_title: blog.title(), blog_title: blog.title(),
@ -19,7 +21,8 @@ defmodule FirehoseWeb.BlogController do
posts: result.entries, posts: result.entries,
base_path: blog.base_path(), base_path: blog.base_path(),
page: result.page, page: result.page,
total_pages: result.total_pages total_pages: result.total_pages,
rss_feed_url: rss_url
) )
end end
@ -27,31 +30,40 @@ defmodule FirehoseWeb.BlogController do
blog = conn.assigns.blog blog = conn.assigns.blog
post = blog.get_post!(slug) post = blog.get_post!(slug)
visibility = Blogex.Post.visibility(post) visibility = Blogex.Post.visibility(post)
rss_url = rss_feed_url(blog)
meta = Blogex.SEO.meta_tags(post, FirehoseWeb.Endpoint.url(), blog) meta = Blogex.SEO.meta_tags(post, FirehoseWeb.Endpoint.url(), blog)
conn conn
|> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta) |> FirehoseWeb.Layouts.put_content_for(:meta_tags, meta)
|> FirehoseWeb.Layouts.put_content_for(:feed_links, %{rss_url: rss_url, title: blog.title()})
|> render(:show, |> render(:show,
page_title: post.title, page_title: post.title,
post: post, post: post,
meta: meta, meta: meta,
base_path: blog.base_path(), base_path: blog.base_path(),
visibility: visibility, visibility: visibility,
authenticated: !!(conn.assigns[:current_scope] && conn.assigns.current_scope.user) authenticated: !!(conn.assigns[:current_scope] && conn.assigns.current_scope.user),
rss_feed_url: rss_url
) )
end end
def tag(conn, %{"tag" => tag}) do def tag(conn, %{"tag" => tag}) do
blog = conn.assigns.blog blog = conn.assigns.blog
posts = blog.posts_by_tag(tag) posts = blog.posts_by_tag(tag)
rss_url = rss_feed_url(blog)
render(conn, :tag, rss_link = %{rss_url: rss_url, title: blog.title()}
conn
|> FirehoseWeb.Layouts.put_content_for(:feed_links, rss_link)
|> render(:tag,
page_title: "#{blog.title()}#{tag}", page_title: "#{blog.title()}#{tag}",
blog_title: blog.title(), blog_title: blog.title(),
tag: tag, tag: tag,
posts: posts, posts: posts,
base_path: blog.base_path() base_path: blog.base_path(),
rss_feed_url: rss_url
) )
end end
@ -77,4 +89,9 @@ defmodule FirehoseWeb.BlogController do
_ -> 1 _ -> 1
end end
end end
defp rss_feed_url(blog) do
base_id = blog.base_path() |> String.trim_leading("/blog/")
FirehoseWeb.Endpoint.url() <> "/api/blog/#{base_id}/feed.xml"
end
end end

View File

@ -1,6 +1,17 @@
<div class="space-y-8"> <div class="space-y-8">
<header> <header>
<h1 class="text-3xl font-bold font-display">{@blog_title}</h1> <div class="flex items-center gap-3">
<h1 class="text-3xl font-bold font-display">{@blog_title}</h1>
<a
href={@rss_feed_url}
class="link link-hover text-orange-500 hover:text-orange-600 transition-colors"
title="Subscribe to {@blog_title} RSS feed"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-5" />
</a>
</div>
<p :if={@blog_description} class="mt-2 text-base-content/70">{@blog_description}</p> <p :if={@blog_description} class="mt-2 text-base-content/70">{@blog_description}</p>
</header> </header>

View File

@ -1,5 +1,16 @@
<div class="space-y-8"> <div class="space-y-8">
<a href={@base_path} class="text-sm text-primary hover:underline">&larr; Back to posts</a> <div class="flex items-center justify-between">
<a href={@base_path} class="text-sm text-primary hover:underline">&larr; Back to posts</a>
<a
href={@rss_feed_url}
class="link link-hover text-orange-500 hover:text-orange-600 transition-colors"
title="Subscribe to RSS feed"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-5" />
</a>
</div>
<%= if @authenticated and @visibility == :draft do %> <%= if @authenticated and @visibility == :draft do %>
<div <div

View File

@ -1,6 +1,17 @@
<div class="space-y-8"> <div class="space-y-8">
<header> <header>
<h1 class="text-3xl font-bold font-display">{@blog_title}</h1> <div class="flex items-center gap-3">
<h1 class="text-3xl font-bold font-display">{@blog_title}</h1>
<a
href={@rss_feed_url}
class="link link-hover text-orange-500 hover:text-orange-600 transition-colors"
title="Subscribe to {@blog_title} RSS feed"
target="_blank"
rel="noopener noreferrer"
>
<.rss_icon class="size-5" />
</a>
</div>
<p class="mt-2 text-base-content/70">Posts tagged "{@tag}"</p> <p class="mt-2 text-base-content/70">Posts tagged "{@tag}"</p>
</header> </header>

View File

@ -4,7 +4,7 @@ defmodule Firehose.MixProject do
def project do def project do
[ [
app: :firehose, app: :firehose,
version: "0.1.0", version: "0.2.0",
elixir: "~> 1.15", elixir: "~> 1.15",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
start_permanent: Mix.env() == :prod, start_permanent: Mix.env() == :prod,

View File

@ -0,0 +1,16 @@
%{
title: "v0.2.0 — RSS Subscribe Links",
author: "Willem van den Ende",
tags: ~w(release),
description: "RSS feed subscribe links with classic waveguide icons across blog pages, post pages, and site footer."
}
---
RSS feeds have always been available at `/api/blog/engineering/feed.xml` and `/api/blog/releases/feed.xml`, but they were never advertised. This release adds subscribe links throughout the site.
## What's new
- RSS `<link>` tags in `<head>` on all blog pages for browser/feed reader auto-discovery
- Orange RSS icon next to blog title on index pages
- RSS icon on each post page
- Subscribe section in the footer with links to both feeds

View File

@ -90,9 +90,9 @@ defmodule FirehoseWeb.MicroprintsLiveTest do
assert html =~ ~s(phx-value-path="#{file_a}") assert html =~ ~s(phx-value-path="#{file_a}")
assert html =~ "Collapse" assert html =~ "Collapse"
# Highlight a line in file B via direct event (avoids brittle DOM selector) # Highlight a line in file B by dispatching the highlight_line event
view # (microprint renders multiple rect segments per line, element() can't pick one)
|> render_click("highlight_line", %{"line" => "1", "path" => file_b}) render_click(view, "highlight_line", %{"line" => "1", "path" => file_b})
# file A should be collapsed when highlighting a different file # file A should be collapsed when highlighting a different file
html = render(view) html = render(view)