Skip to content

Commit 08c8827

Browse files
NetdocsCopilot
andcommitted
docs(rss): document feed output paths + auto social icon
- RssPlugin: opt-in social_icon injects an RSS entry into extra.social, linking the RSS feed (or Atom via social_feed: atom). Existing social entries are preserved. - rss.md: clarify feeds are written to the site root at stable URLs, with a table of default files + how to configure/serve them; document the new social_icon/social_feed options. - Add 3 unit tests covering the social injection (211 tests pass). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 7b2d00e commit 08c8827

3 files changed

Lines changed: 103 additions & 7 deletions

File tree

docs-site/docs/plugins/rss.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ feed. Requires the [blog](blog.md) plugin to have collected posts.
2121
| `feed_description` | string | `site_description` | Override the feed/channel description. |
2222
| `image` | string || Channel image (logo) URL; relative URLs are resolved against `site_url`. |
2323
| `ttl` | int || RSS `<ttl>` in minutes (advisory cache time for readers). |
24+
| `social_icon` | bool | `false` | Add the feed as an RSS icon in the header/footer social row (see [Output](#output)). |
25+
| `social_feed` | string | `rss` | Which feed the social icon links when `social_icon: true``rss` or `atom`. |
2426

2527
```json
2628
{ "name": "rss", "options": { "length": 20, "atom": true, "full_content": false } }
@@ -57,8 +59,28 @@ image: hero.png
5759

5860
## Output
5961

60-
`site/feed_rss_created.xml` (and `site/feed_atom_created.xml` when `atom: true`) — link them
61-
from your `extra.social` block or `<head>`:
62+
Feeds are written to the **root of your built site** (next to `index.html`), so they are served at
63+
a stable, predictable URL:
64+
65+
| Feed | Default file | Served at | Configure with |
66+
|---|---|---|---|
67+
| RSS 2.0 | `feed_rss_created.xml` | `<site_url>/feed_rss_created.xml` | `rss_file` |
68+
| Atom 1.0 *(when `atom: true`)* | `feed_atom_created.xml` | `<site_url>/feed_atom_created.xml` | `atom_file` |
69+
70+
The RSS feed advertises itself with an `atom:link rel="self"` element so readers can discover the
71+
canonical feed URL.
72+
73+
### Show the feed as a social icon
74+
75+
Set `social_icon: true` and the plugin adds an RSS entry to your `extra.social` row automatically —
76+
no manual link needed. It links `feed_rss_created.xml` by default, or the Atom feed with
77+
`social_feed: atom`:
78+
79+
```json
80+
{ "name": "rss", "options": { "atom": true, "social_icon": true } }
81+
```
82+
83+
To place or style the link yourself instead, add it to `extra.social` by hand:
6284

6385
```json
6486
"extra": {
@@ -68,9 +90,6 @@ from your `extra.social` block or `<head>`:
6890
}
6991
```
7092

71-
The RSS feed also advertises itself with an `atom:link rel="self"` element so readers can
72-
discover the canonical feed URL.
73-
7493
## Attribution
7594

7695
Behavior is modeled on [mkdocs-rss-plugin](https://github.com/Guts/mkdocs-rss-plugin) by @Guts (MIT). See [Attributions](../about/attributions.md).

src/Netdocs.Plugins/RssPlugin.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,35 @@ public void Configure(IPluginContext ctx)
4444
_feedDescription = o.Get("feed_description").AsString();
4545
_channelImage = o.Get("image").AsString();
4646
_ttl = o.Get("ttl").AsInt(0);
47+
48+
// `social_icon: true` surfaces the feed as an RSS icon in the header/footer social row.
49+
// `social_feed: atom` links the Atom feed instead of the default RSS feed.
50+
if (o.Get("social_icon").AsBool(false))
51+
{
52+
var useAtom = string.Equals(o.Get("social_feed").AsString(), "atom", StringComparison.OrdinalIgnoreCase);
53+
var feedFile = useAtom ? _atomFile : _rssFile;
54+
var siteUrl = (ctx.Config.SiteUrl ?? "").TrimEnd('/');
55+
var link = siteUrl.Length > 0 ? $"{siteUrl}/{feedFile}" : "/" + feedFile;
56+
AddSocialEntry(ctx.Config, "fontawesome/solid/rss", link);
57+
}
58+
}
59+
60+
/// <summary>Appends a <c>{ icon, link }</c> entry to <c>extra.social</c> so the theme renders it
61+
/// alongside the other social links. Config's <c>extra</c> map is treated as immutable, so a
62+
/// shallow copy is written back with the augmented social list.</summary>
63+
private static void AddSocialEntry(SiteConfig config, string icon, string link)
64+
{
65+
var extra = new Dictionary<string, object?>(config.Extra, StringComparer.OrdinalIgnoreCase);
66+
var social = extra.TryGetValue("social", out var existing)
67+
? new List<object?>(existing.AsList())
68+
: [];
69+
social.Add(new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase)
70+
{
71+
["icon"] = icon,
72+
["link"] = link,
73+
});
74+
extra["social"] = social;
75+
config.Extra = extra;
4776
}
4877

4978
public async Task OnBuildCompleteAsync(SiteContext site, CancellationToken ct)

tests/Netdocs.Core.Tests/RssPluginTests.cs

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,57 @@ public async Task Length_LimitsItems()
147147
Assert.Equal(2, System.Text.RegularExpressions.Regex.Matches(xml, "<item>").Count);
148148
}
149149

150-
private sealed class FakeContext(IReadOnlyDictionary<string, object?> options) : IPluginContext
150+
[Fact]
151+
public void SocialIcon_AddsRssFeedEntryToExtraSocial()
152+
{
153+
var config = new SiteConfig { SiteUrl = "https://example.com/" };
154+
var plugin = new RssPlugin();
155+
plugin.Configure(new FakeContext(new Dictionary<string, object?> { ["social_icon"] = true }, config));
156+
157+
Assert.True(config.Extra.TryGetValue("social", out var socialObj));
158+
var social = Assert.IsAssignableFrom<System.Collections.IEnumerable>(socialObj).Cast<object?>().ToList();
159+
var entry = Assert.IsAssignableFrom<IDictionary<string, object?>>(social.Single());
160+
Assert.Equal("fontawesome/solid/rss", entry["icon"]);
161+
Assert.Equal("https://example.com/feed_rss_created.xml", entry["link"]);
162+
}
163+
164+
[Fact]
165+
public void SocialFeedAtom_LinksAtomFeed()
166+
{
167+
var config = new SiteConfig { SiteUrl = "https://example.com" };
168+
var plugin = new RssPlugin();
169+
plugin.Configure(new FakeContext(
170+
new Dictionary<string, object?> { ["social_icon"] = true, ["social_feed"] = "atom" }, config));
171+
172+
var social = ((System.Collections.IEnumerable)config.Extra["social"]!).Cast<object?>().ToList();
173+
var entry = (IDictionary<string, object?>)social.Single()!;
174+
Assert.Equal("https://example.com/feed_atom_created.xml", entry["link"]);
175+
}
176+
177+
[Fact]
178+
public void SocialIcon_PreservesExistingSocialEntries()
179+
{
180+
var config = new SiteConfig
181+
{
182+
SiteUrl = "https://example.com",
183+
Extra = new Dictionary<string, object?>
184+
{
185+
["social"] = new List<object?>
186+
{
187+
new Dictionary<string, object?> { ["icon"] = "fontawesome/brands/github", ["link"] = "https://github.com/x" },
188+
},
189+
},
190+
};
191+
var plugin = new RssPlugin();
192+
plugin.Configure(new FakeContext(new Dictionary<string, object?> { ["social_icon"] = true }, config));
193+
194+
var social = ((System.Collections.IEnumerable)config.Extra["social"]!).Cast<object?>().ToList();
195+
Assert.Equal(2, social.Count);
196+
}
197+
198+
private sealed class FakeContext(IReadOnlyDictionary<string, object?> options, SiteConfig? config = null) : IPluginContext
151199
{
152-
public SiteConfig Config { get; } = new();
200+
public SiteConfig Config { get; } = config ?? new();
153201
public BuildOptions Options { get; } = new();
154202
public Microsoft.Extensions.Logging.ILogger Logger { get; } = NullLogger.Instance;
155203
public Microsoft.Extensions.DependencyInjection.IServiceCollection Services { get; }

0 commit comments

Comments
 (0)