Commit

Author:

Hash:

Timestamp:

+177 -36 +/-7 browse

Kevin Schoon [me@kevinschoon.com]

fcc1ff23f92e5308ae954f5a7044f3ab8a7393aa

Fri, 28 Nov 2025 19:50:08 +0000 (1 day ago)

migrate feed from rss -> atom
1diff --git a/assets/ayllu-0.5-demo.mp4 b/assets/ayllu-0.5-demo.mp4
2new file mode 100644
3index 0000000..55b43cb
4 Binary files /dev/null and b/assets/ayllu-0.5-demo.mp4 differ
5 diff --git a/assets/style.xsl b/assets/style.xsl
6index 10b5101..2b45a6a 100644
7--- a/assets/style.xsl
8+++ b/assets/style.xsl
9 @@ -1,15 +1,13 @@
10 <?xml version="1.0" encoding="utf-8"?>
11 <xsl:stylesheet version="3.0"
12 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
13- xmlns:atom="http://www.w3.org/2005/Atom"
14- xmlns:dc="http://purl.org/dc/elements/1.1/"
15- xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
16+ xmlns:atom="http://www.w3.org/2005/Atom">
17 <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
18 <xsl:template match="/">
19 <html lang="en">
20 <head>
21 <title>
22- <xsl:value-of select="/rss/channel/title"/>
23+ <xsl:value-of select="atom:feed/atom:title"/>
24 </title>
25 <link rel="stylesheet" href="/main.css" />
26 </head>
27 @@ -18,29 +16,28 @@
28 <article>
29 <header>
30 <h1>
31- <img class="me" src="/ks_stylized.png"/>
32- <a>
33- <xsl:attribute name="href"> <xsl:value-of select="/rss/channel/link"/> </xsl:attribute>
34- </a>
35+ <a href="/"><img class="me" src="/ks_stylized.png"/></a>
36 </h1>
37- <h2><xsl:value-of select="/rss/channel/description"/></h2>
38+ <h2><xsl:value-of select="atom:feed/atom:title"/></h2>
39 </header>
40- <p><strong>This is an <a href="https://en.wikipedia.org/wiki/RSS">RSS feed</a>.</strong></p>
41+ <p><strong>This is an <a href="https://en.wikipedia.org/wiki/Atom_(web_standard)">Atom feed</a>.</strong></p>
42 <p>Copy the link from the address bar into your feed reader to receive regular updates.</p>
43 </article>
44- <xsl:for-each select="/rss/channel/item">
45+ <xsl:for-each select="atom:feed/atom:entry">
46 <article class="card">
47 <header>
48 <h3>
49 <a>
50 <xsl:attribute name="href">
51- <xsl:value-of select="link"/>
52+ <xsl:value-of select="atom:link/@href"/>
53 </xsl:attribute>
54- <xsl:value-of select="title"/>
55+ <xsl:value-of select="atom:title"/>
56 </a>
57 </h3>
58 </header>
59- <xsl:value-of select="description" disable-output-escaping="yes"/>
60+ <div>
61+ <xsl:value-of select="atom:content" disable-output-escaping="yes"/>
62+ </div>
63 <footer class="rss">
64 <xsl:value-of select="author" /><br/>
65 <b><xsl:value-of select="pubDate"/></b>
66 diff --git a/content/blog/announcing-ayllu-0.5/README.md b/content/blog/announcing-ayllu-0.5/README.md
67new file mode 100644
68index 0000000..7192b45
69--- /dev/null
70+++ b/content/blog/announcing-ayllu-0.5/README.md
71 @@ -0,0 +1,96 @@
72+ # Ayllu 0.5 Has Been Released!
73+
74+ `11/21/2025`
75+
76+ <br />
77+
78+ Hello again and welcome to another sporadic set of release notes for
79+ Ayllu. This began as a fun experiment for me to hack together a
80+ development environment which was setup the way I wanted it and also
81+ as an excuse to learn Rust. It continues to be a fun side project and
82+ staging ground for basically programming things I find interesting.
83+ There are no shortages of code forges
84+
85+
86+ ## Identities in Ayllu
87+
88+ Most of the popular forges work like traditional web apps: you sign-up
89+ with an e-mail address and then take on the persona of a developer with
90+ ability to push and pull in a tightly controlled environment. The ability
91+ to push code via SSH is usually available via a highly restricted shell
92+ environment where you can authenticate via public keys and then typically
93+ run only the commands needed to push or pull from a repository. Ayllu
94+ instead provides a management TUI interface over SSH as well as support
95+ for delegation to normal system users.
96+
97+
98+ ```toml
99+ [[identities]]
100+ username = "hello"
101+ email = "world@example.org"
102+ avatar = {}
103+ tagline = "...."
104+ authorized_keys = [...]
105+ gpg_keys = [...]
106+ [[identities.profiles]]
107+ url = ""
108+ ```
109+
110+ <video controls> <source src="/ayllu-0.5-demo.mp4" type="video/mp4" /> </video>
111+
112+ #### New Binaries
113+
114+ ##### ayllu-keys
115+
116+ The `ayllu-keys` command is a shim for authorizing user access via OpenSSH's
117+ `AuthorizedKeysCommand`.
118+
119+ ##### ayllu-shell
120+
121+ The `ayllu-shell` command implements a restricted shell environment for
122+ management of Ayllu related tasks. It's also responsble for delegating
123+ calls to `git-receive-pack` and `git-upload-pack`.
124+
125+ Here is a quick video of what logging into an account with Ayllu looks
126+ like today.
127+
128+
129+ ## ForgeFeed Specification
130+
131+ I've struggled with what a programmatic API should look like for Ayllu because
132+ after having written 10,000,000 REST/Protobuf/... web APIs they're really all
133+ miserable reinventions of the same thing. What I really want is a generalized
134+ interface for querying forge resources across ANY forge, not just Ayllu. The
135+ [ForgeFeed](https://forge-feed.org) specification attempts to define a
136+ common interface.
137+
138+ ## A Primitive Build System
139+
140+ A primitive but functional build system has been added (actually re-enabled)
141+ to Ayllu and integrated into it's UI. The `ayllu-build` binary when invoked
142+ will now run an un-sandboxed build of the HEAD at whichever branch is
143+ selected. This can be invoked manually
144+
145+ ## Towards 1.0
146+
147+ There is no timeline for when Ayllu will reach 1.0 or exactly what
148+ it needs to encompass but roughly here are a few items.
149+
150+ * Tooling for Package Managers
151+ * Functional build system with single host isolation
152+ * Multiuser Environments
153+ * A way to accept changes
154+ * An internal issue tracking system
155+
156+ # User Identities in Ayllu
157+
158+ Code forges typically follow a traditional "web app" model of user accounts
159+ by allowing people to sign up with their e-mail addresses and log into a web
160+ interface.
161+
162+ Ayllu uses PAM managed system accounts which are optionally granted a shell
163+ in the spirit of Unix Timesharing systems.
164+
165+ ## Managing User Accounts in Ayllu
166+
167+ ## Security Implications
168 diff --git a/content/index.md b/content/index.md
169index 543ca64..94b6ed0 100644
170--- a/content/index.md
171+++ b/content/index.md
172 @@ -15,4 +15,4 @@ Contact info (same as above, but a QR code):
173 <img alt="qr code" class="contact" src="/contact.png"/>
174
175 ### Subscribe
176- [RSS](/index.xml)
177+ [Atom/RSS](/index.xml)
178 diff --git a/index.xml.jinja b/index.xml.jinja
179deleted file mode 100644
180index b92f652..0000000
181--- a/index.xml.jinja
182+++ /dev/null
183 @@ -1 +0,0 @@
184- {{content}}
185 diff --git a/render.py b/render.py
186index eefbd0d..9bbfb78 100755
187--- a/render.py
188+++ b/render.py
189 @@ -1,6 +1,7 @@
190 #!/usr/bin/env python
191 import argparse
192 import os
193+ import io
194 import sys
195 import xml.etree.ElementTree as ET
196 from io import StringIO
197 @@ -15,6 +16,36 @@ from jinja2 import Environment, FunctionLoader
198 DEVMODE_WEBSOCKET_ADDR = "ws://localhost:8888/ws"
199
200
201+ def _set_dates(entries):
202+ for entry in entries:
203+ if "date" in entry:
204+ entry["date"] = datetime.fromisoformat(entry["date"])
205+ if "others" in entry:
206+ return _set_dates(entry["others"])
207+
208+
209+ def _get_dates(entries):
210+ dates = []
211+ for entry in entries:
212+ if "date" in entry:
213+ dates.append(entry["date"])
214+ if "others" in entry:
215+ for other in _get_dates(entry["others"]):
216+ dates.append(other)
217+ return dates
218+
219+
220+ def _get_latest(entries):
221+ latest = None
222+ for other in _get_dates(entries):
223+ if latest is None:
224+ latest = other
225+ else:
226+ if other > latest:
227+ latest = other
228+ return latest
229+
230+
231 def _load_sitemap(path, current_path):
232 by_name = dict()
233 flattened = []
234 @@ -69,7 +100,10 @@ def _load_sitemap(path, current_path):
235 entry["url"] = target
236 by_name[target] = entry
237
238- return dict(by_name=by_name, variables=variables, links=entries)
239+ _set_dates(entries)
240+ latest = _get_latest(entries)
241+
242+ return dict(by_name=by_name, variables=variables, links=entries, latest=_get_latest(entries))
243
244
245 def _make_tree(links):
246 @@ -146,29 +180,44 @@ def generate_rss(sitemap):
247 header = ET.Element(
248 "xml-stylesheet", attrib={"href": "/feed.xsl", "type": "text/xsl"}
249 )
250- rss = ET.Element("rss", attrib={"version": "2.0", "encoding": "utf-8"})
251- channel = ET.SubElement(rss, "channel")
252
253- title = ET.SubElement(channel, "title")
254+ feed = ET.Element("feed", attrib={"xmlns": "http://www.w3.org/2005/Atom"})
255+
256+ feed_id = ET.SubElement(feed, "id")
257+ feed_id.text = "https://kevinschoon.com/"
258+ title = ET.SubElement(feed, "title")
259 title.text = "Blog of Kevin Schoon"
260- link = ET.SubElement(channel, "link")
261+ link = ET.SubElement(feed, "link")
262+ link.attrib["rel"] = "self"
263 link.attrib["href"] = "https://kevinschoon.com"
264- description = ET.SubElement(channel, "description")
265- description.text = "Blog of Kevin Schoon"
266+ updated = ET.SubElement(feed, "updated")
267+ updated.text = sitemap["latest"].isoformat()
268+ author = ET.SubElement(feed, "author")
269+ name = ET.SubElement(author, "name")
270+ name.text = "Kevin Schoon"
271
272 for entry in sitemap["by_name"]["/blog"]["others"]:
273 absolute_url = "https://kevinschoon.com/blog/" + entry["name"]
274- item = ET.SubElement(channel, "item")
275- title_item = ET.SubElement(item, "title")
276- link_item = ET.SubElement(item, "link")
277- guid_item = ET.SubElement(item, "guid")
278- guid_item.text = absolute_url
279- description_item = ET.SubElement(item, "description")
280- title_item.text = entry["name"]
281- link_item.attrib["href"] = absolute_url
282- description_item.text = entry["tagline"]
283-
284- xml_str = ET.tostring(rss, xml_declaration=True, encoding="unicode", method="xml")
285+ atom_entry = ET.SubElement(feed, "entry")
286+ link = ET.SubElement(atom_entry, "link")
287+ link.attrib["href"] = absolute_url
288+ link.attrib["rel"] = "self"
289+ _id = ET.SubElement(atom_entry, "id")
290+ _id.text = absolute_url
291+ title = ET.SubElement(atom_entry, "title")
292+ title.text = entry["tagline"]
293+ content_path = "content/blog/" + entry["name"] + "/README.md"
294+ content = load_content(content_path)
295+ content_element = ET.SubElement(atom_entry, "content")
296+ content_element.attrib["type"] = "xhtml"
297+ content_element.attrib["xml:lang"] = "en"
298+ content_tree = ET.parse(io.StringIO(f"<div xmlns=\"http://www.w3.org/1999/xhtml\">{content}</div>")).getroot()
299+ content_element.append(content_tree)
300+ updated = ET.SubElement(atom_entry, "updated")
301+ updated.text = entry["date"].isoformat()
302+
303+
304+ xml_str = ET.tostring(feed, xml_declaration=True, encoding="unicode", method="xml")
305 xml_str = xml_str.replace(
306 "<?xml version='1.0' encoding='utf-8'?>",
307 "<?xml version='1.0' encoding='utf-8'?><?xml-stylesheet href='/style.xsl' type='text/xsl'?>",
308 diff --git a/sitemap.yaml b/sitemap.yaml
309index 969527d..11a4b57 100644
310--- a/sitemap.yaml
311+++ b/sitemap.yaml
312 @@ -7,7 +7,7 @@ entries:
313 others:
314 - name: announcing-ayllu-0.4
315 tagline: Ayllu version 0.4 has been released.
316- date: 2025-05-13
317+ date: "2025-05-13T01:01:01Z"
318 - name: building-this-website
319 tagline: There is no deficiency of static website generators these days and so I had to write my own.
320- date: 2023-04-04
321+ date: "2023-04-14T01:01:01Z"