Commit

Author:

Hash:

Timestamp:

+603 -0 +/-9 browse

Kevin Schoon [me@kevinschoon.com]

b380da40d8ec4f069cd7d1d8b13a46d9451ec1b0

Fri, 28 Nov 2025 23:20:54 +0000 (1 day ago)

automated www update
1diff --git a/gen/blog/announcing-ayllu-0.4/index.html b/gen/blog/announcing-ayllu-0.4/index.html
2new file mode 100644
3index 0000000..8f52c9d
4--- /dev/null
5+++ b/gen/blog/announcing-ayllu-0.4/index.html
6 @@ -0,0 +1,29 @@
7+ <!doctype html><html lang=en><title>kevinschoon-dot-com</title><meta charset=UTF-8><meta content="width=device-width,initial-scale=1" name=viewport><link href=/main.css rel=stylesheet><link href=https://kevinschoon.com/index.xml rel=alternate title=RSS type=application/rss+xml><body><section><div class=wrapper><header class=page-header><a href=/>Home</a></header><main class=page-body><h1>Ayllu 0.4 Has Been Released!</h1><p><code>05/13/2025</code><p><br><p>Ayllu release <a href=https://ayllu-forge.org/ayllu/ayllu/refs/tag/0.4.0>0.4.0</a> has been tagged after a year of inconsistent development. This release has seen a major <em>reduction</em> in the number of features available in Ayllu and a simplification of it's operation. Most of the features that have been removed will eventually make their way back into the software as time permits and can be done in a way that is simple to maintain. Although the core functionality of Ayllu has been minimized, two new major (unfinished) libraries were developed adjacently: <a href=https://ayllu-forge.org/ayllu/maitred>maitred</a>, an embedable SMTP server, and <a href=https://ayllu-forge.org/ayllu/papyri>papyri</a>, an OCI compliant container registry. See more details below.<h2>Notable Reductions</h2><h3>The Ayllu Binary is Now Stateless</h3><p>The <code>ayllu-jobs</code> binary has been temporarily removed after several iterations of implementing sqlite based state. The binary is responsible for doing CLOC anaylsis with <a href=https://github.com/XAMPPRocky/tokei>Tokei</a>, calculating authorship statistics, and anything else that needs to be run at a periodic interval. The jobs themselves have worked well but I've not been satisfied with any approach I've used to communicate stateful data into the UI in a way that is extensible. Ultimately my goal is to maintain an ultra fast static web interface that can be easily extended with new kinds of repository information. A future update will re-enable the state as opt-in likely via an RPC interface or shared database crate.<h3>Git Blame is Removed</h3><p>Although the server has the ability to generate everything that is needed to display <code>git blame</code> data I haven't been able to design UX that looks good enough to present it so I've opted to remove the code for now. If anyone has suggestions for how to do it in pure html/css please reach out.<h3>Simplified Themeing</h3><p>The themeing engine was reverted back from Tera to Askama see commit: <a href=https://ayllu-forge.org/ayllu/ayllu/commit/5c5e94b2eff4d4181bd59ed359dabb509b0a34fc>5c5e94</a>.<h2>New Features</h2><h3>Web Finger Protocol Support</h3><p>A concept of static "membership" has been added to the server which is browsable over the <a href=https://en.wikipedia.org/wiki/WebFinger>webfinger</a> protocol. You can use the <code>quipu</code> binary to query resources on an any Ayllu server:<pre><code class=language-sh>quipu finger "acct:demo@example.org"
8+ </code></pre><pre><code class=language-json>{
9+ "subject": "demo@example.org",
10+ "links": [
11+ {
12+ "rel": "http://webfinger.net/rel/profile-page",
13+ "type": "text/html",
14+ "href": "https://example.org/demo"
15+ },
16+ {
17+ "rel": "http://webfinger.net/rel/profile-page",
18+ "type": "text/html",
19+ "href": "https://example.org/@demo"
20+ },
21+ {
22+ "rel": "http://webfinger.net/rel/avatar",
23+ "href": "https://example.org/avatar.png"
24+ },
25+ {
26+ "rel": "http://ayllu-forge.org/rel/description",
27+ "properties": {
28+ "text": "Programmer interested free software"
29+ }
30+ }
31+ ]
32+ }
33+ </code></pre><pre><code class=language-sh># Or via curl
34+ curl 'http://localhost:10000/.well-known/webfinger?resource=acct:demo@example.org' |jq .
35+ </code></pre><p>Currently the server only can share static account information from the global configuration file but future updates will expose repository and other information. Eventually there are plans to implement global searchable index of Ayllu instances which will crawl this protocol data.<h3>Mirror Aware Repositories</h3><p>Ayllu will detect repositories that contain origins that have the <code>mirror</code> flag enabled and display them as "non-canonical" sources in the UI.<h3>Multiuser Container Image</h3><p>The <a href=https://ayllu-forge.org/ayllu/ayllu/tree/main/containers/multiuser>multiuser</a> container image has been further developed and offers a simple way for managing trusted multi-user forge environments.<h3>New Crates</h3><p>Two new crates <a href=https://ayllu-forge.org/ayllu/ayllu/tree/main/ayllu-shell>ayllu-shell</a> and <a href=https://ayllu-forge.org/ayllu/ayllu/tree/main/ayllu-keys>ayllu-keys</a> were added which when completed will allow a simple static configuration of user authorization into an Ayllu managed server.<h2>Supplemental Libraries</h2><h3>Maitred SMTP Server</h3><p>A new crate offering an embeddable SMTP server has been developed called <a href=https://ayllu-forge.org/ayllu/maitred>maitred</a> for use in Ayllu so that it can easily receive patches over SMTP. This project is made possible by the low-level protocol implementation of the <a href=https://github.com/stalwartlabs/mail-server>stalwart</a> project. Most of this crate is functional however ARC and DMARC verification need to be finished before we can safely handle messages. Once completed this will be implemented as part of the <code>ayllu-mail</code> binary.<h3>Papari OCI Container Registry</h3><p>The <a href=https://ayllu-forge.org/ayllu/papyri>Papyri</a> OCI container registry has started development which will provide the ability to read and write container images directly into an Ayllu instance. This crate currently only supports direct file system storage but a SQLite blob backend or possibly S3 compatible store may be added. This crate is shipped with an export of Axum routes and once integrated will allow pushing and pulling OCI manifests directly from the web interface.<h1></h1><p>That's all for now, thanks for reading!</main><footer class=page-footer></footer></div></section>
36\ No newline at end of file
37 diff --git a/gen/blog/building-this-website/index.html b/gen/blog/building-this-website/index.html
38new file mode 100644
39index 0000000..81308bc
40--- /dev/null
41+++ b/gen/blog/building-this-website/index.html
42 @@ -0,0 +1,97 @@
43+ <!doctype html><html lang=en><title>kevinschoon-dot-com</title><meta charset=UTF-8><meta content="width=device-width,initial-scale=1" name=viewport><link href=/main.css rel=stylesheet><link href=https://kevinschoon.com/index.xml rel=alternate title=RSS type=application/rss+xml><body><section><div class=wrapper><header class=page-header><a href=/>Home</a></header><main class=page-body><h1>Building This Website</h1><p>There is no deficiency of static website generators these days and so naturally I had to write my own.<h2>Overview</h2><p>The main functionality of the tool will be encapsulated in a Python script called <code>render.py</code>. Even though <a href=https://www.devever.net/~hl/stringtemplates#narrow>string based templates are the wrong solution</a> we're going to use a combination of <a href=https://jinja.palletsprojects.com/>Jinja</a> and markdown to generate HTML output. Each page will be generated by a single call to the script which will allow the whole thing to be ran in parallel across multiple CPUs by wiring it together using <a href=https://ninja-build.org/>ninja</a>.<p>Here you can see an example invocation of the <code>render.py</code> script with some annotated flags.<pre><code>./render.py \
44+ # the HTML template file
45+ --template=index.jinja \
46+ # this contains a map of each webpage which is available in the renderer
47+ # as well as page level configuration options.
48+ --sitemap sitemap.yaml \
49+ # markdown file, this one contains the text I'm currently typing!
50+ --content=blog/building-this-website/README.md \
51+ # output of the rendered HTML page
52+ gen/blog/building-this-website/index.html
53+ </code></pre><h3>Hierarchical Sitemap</h3><p>I want to generate a "tree" sitemap similar to a file browser like NERDTree. We'll pass in a YAML document to the <code>render.py</code> script and then transform it into an XML tree.<pre><code>def _load_sitemap(path, current_path):
54+ by_name = dict()
55+ flattened = []
56+ with open(path, "r") as fp:
57+ links = yaml.safe_load(fp.read())
58+
59+ def _link(links, parent=None):
60+ for link in links:
61+ flattened.append(link)
62+ link["parent"] = parent
63+ if "others" in link:
64+ _link(link["others"], parent=link)
65+
66+ def _make_url(link):
67+ path = [link["name"]]
68+ parent = link["parent"]
69+ while parent is not None:
70+ path.append(parent["name"])
71+ parent = parent["parent"]
72+ path.reverse()
73+ return "/".join(path)
74+
75+ _link(links, parent=None)
76+
77+ for link in flattened:
78+ target = "/" + _make_url(link)
79+ if target == current_path:
80+ link["active"] = True
81+ else:
82+ link["active"] = False
83+ link["url"] = target
84+ by_name[target] = link
85+
86+ return dict(by_name=by_name, links=links)
87+
88+
89+ def _make_tree(links):
90+ def _populate(root, links):
91+ for link in links:
92+ elm = ET.Element("li")
93+ if "directory" in link and link["directory"]:
94+ elm.text = link["name"]
95+ else:
96+ lref = ET.Element("a", href=link["url"])
97+ lref.text = link["name"]
98+ if link["active"]:
99+ lref.text = lref.text + " &lt;"
100+ elm.append(lref)
101+ if "others" in link:
102+ others = link["others"]
103+ ul = ET.Element("ul")
104+ elm.append(ul)
105+ _populate(root=ul, links=others)
106+ root.append(elm)
107+
108+ params = {"class": "tree"}
109+
110+ root = ET.Element("ul", **params)
111+ _populate(root=root, links=links)
112+ return ET.tostring(root).decode()
113+ </code></pre><h3>Dithering Engine</h3><p>Images can be reduced in size with <a href=https://en.wikipedia.org/wiki/Dither>dithering algorithms</a> and they also look very cool. The idea for this was inspired by <a href=https://solar.lowtechmagazine.com/>LOW←TECH MAGAZINE</a>.<h3>Spell & Grammar Checking</h3><p>I can implement some basic spell checking against the website with <a href=http://aspell.net/>aspell</a>.<pre><code class=language-sh>find content -name '*.md' -exec aspell --mode=markdown check {} \;
114+ </code></pre><h3>Filesystem "Watch" Support</h3><p>We'll use the native <a href=https://en.wikipedia.org/wiki/Inotify>inotify</a> via the <a href=https://man7.org/linux/man-pages/man1/inotifywatch.1.html>inotifywatch</a> tool which is available on all Linux distributions.<pre><code class=language-sh>#!/bin/sh
115+
116+ # the actual command we want to run each time any file changes in the project
117+ BUILD_CMD="$@"
118+
119+ # some pretty colorized output
120+ _do_build() {
121+ $BUILD_CMD && {
122+ echo -e "\033[32;1;4mSuccess\033[0m"
123+ } || {
124+ echo -e "\033[31;1;4mFailure\033[0m"
125+ }
126+ }
127+
128+ # build the project the first time you start up the watch
129+ _do_build
130+
131+ # watch all the files in the project except for the output directory
132+ find . -type d -not -path './gen*' -printf '%p ' | xargs inotifywait \
133+ -m -e close_write \
134+ --format %e/%f |
135+ while IFS=/ read -r events file; do
136+ echo "file $file modified, rebuilding"
137+ _do_build
138+ done
139+ </code></pre><p>Now we can test the script by launching it <code>scripts/watch.sh build.py</code>.<p>You can checkout the source for the whole site <a href=https://ayllu-forge.org/web/kevinschoon-dot-com>here</a>.</main><footer class=page-footer></footer></div></section>
140\ No newline at end of file
141 diff --git a/gen/contact.png b/gen/contact.png
142new file mode 100644
143index 0000000..e74e5b7
144 Binary files /dev/null and b/gen/contact.png differ
145 diff --git a/gen/favicon.ico b/gen/favicon.ico
146new file mode 100644
147index 0000000..59a4e28
148 Binary files /dev/null and b/gen/favicon.ico differ
149 diff --git a/gen/index.html b/gen/index.html
150new file mode 100644
151index 0000000..7341b51
152--- /dev/null
153+++ b/gen/index.html
154 @@ -0,0 +1 @@
155+ <!doctype html><html lang=en><title>kevinschoon-dot-com</title><meta charset=UTF-8><meta content="width=device-width,initial-scale=1" name=viewport><link href=/main.css rel=stylesheet><link href=https://kevinschoon.com/index.xml rel=alternate title=RSS type=application/rss+xml><body><section><div class=wrapper><header class=page-header></header><main class=page-body><p><a href=/><img alt="dithered photo of me" class=me src=/ks_stylized.png></a><h1>Hi, thanks for stopping by!</h1><p>I'm a programmer interested in free software.<h3>Sitemap</h3><p><ul class=tree><li><a href=/blog>blog</a><ul><li><a href=/blog/announcing-ayllu-0.4>announcing-ayllu-0.4 [2025-05-13]</a><li><a href=/blog/building-this-website>building-this-website [2023-04-14]</a></ul></ul><h3>Email</h3><p>You can contact me at <a href=mailto:me@kevinschoon.com>me@kevinschoon.com</a>.<h3>Software</h3><p>Check out some of my past and present projects <a href=https://ayllu-forge.org/browse>here</a>.<h3>Contact (QR)</h3><p>Contact info (same as above, but a QR code):<p><img alt="qr code" class=contact src=/contact.png><h3>Subscribe</h3><p><a href=/index.xml>Atom/RSS</a></main><footer class=page-footer></footer></div></section>
156\ No newline at end of file
157 diff --git a/gen/index.xml b/gen/index.xml
158new file mode 100644
159index 0000000..0c70421
160--- /dev/null
161+++ b/gen/index.xml
162 @@ -0,0 +1,235 @@
163+ <?xml version='1.0' encoding='utf-8'?><?xml-stylesheet href='/style.xsl' type='text/xsl'?>
164+ <feed xmlns="http://www.w3.org/2005/Atom"><id>https://kevinschoon.com/</id><title>Blog of Kevin Schoon</title><link rel="self" href="https://kevinschoon.com" /><updated>2025-05-13T01:01:01+00:00</updated><author><name>Kevin Schoon</name></author><entry><link href="https://kevinschoon.com/blog/announcing-ayllu-0.4" rel="self" /><id>https://kevinschoon.com/blog/announcing-ayllu-0.4</id><title>Ayllu version 0.4 has been released.</title><updated>2025-05-13T01:01:01+00:00</updated><content type="xhtml" xml:lang="en"><div xmlns="http://www.w3.org/1999/xhtml"><h1>Ayllu 0.4 Has Been Released!</h1>
165+ <p><code>05/13/2025</code></p>
166+ <p><br /></p>
167+ <p>Ayllu release <a href="https://ayllu-forge.org/ayllu/ayllu/refs/tag/0.4.0">0.4.0</a> has
168+ been tagged after a year of inconsistent development. This release has seen a major
169+ <em>reduction</em> in the number of features available in Ayllu and a simplification
170+ of it's operation. Most of the features that have been removed will eventually
171+ make their way back into the software as time permits and can be done in a
172+ way that is simple to maintain. Although the core functionality of Ayllu has
173+ been minimized, two new major (unfinished) libraries were developed adjacently:
174+ <a href="https://ayllu-forge.org/ayllu/maitred">maitred</a>, an embedable SMTP server,
175+ and <a href="https://ayllu-forge.org/ayllu/papyri">papyri</a>, an OCI compliant container
176+ registry. See more details below.</p>
177+ <h2>Notable Reductions</h2>
178+ <h3>The Ayllu Binary is Now Stateless</h3>
179+ <p>The <code>ayllu-jobs</code> binary has been temporarily removed after several iterations
180+ of implementing sqlite based state. The binary is responsible for
181+ doing CLOC anaylsis with <a href="https://github.com/XAMPPRocky/tokei">Tokei</a>,
182+ calculating authorship statistics, and anything else that needs to be run at a
183+ periodic interval. The jobs themselves have worked well but I've not been
184+ satisfied with any approach I've used to communicate stateful data into the UI
185+ in a way that is extensible. Ultimately my goal is to maintain an ultra fast
186+ static web interface that can be easily extended with new kinds of repository
187+ information. A future update will re-enable the state as opt-in likely via
188+ an RPC interface or shared database crate.</p>
189+ <h3>Git Blame is Removed</h3>
190+ <p>Although the server has the ability to generate everything that is needed
191+ to display <code>git blame</code> data I haven't been able to design UX that looks good
192+ enough to present it so I've opted to remove the code for now. If anyone has
193+ suggestions for how to do it in pure html/css please reach out.</p>
194+ <h3>Simplified Themeing</h3>
195+ <p>The themeing engine was reverted back from Tera to Askama see commit:
196+ <a href="https://ayllu-forge.org/ayllu/ayllu/commit/5c5e94b2eff4d4181bd59ed359dabb509b0a34fc">5c5e94</a>.</p>
197+ <h2>New Features</h2>
198+ <h3>Web Finger Protocol Support</h3>
199+ <p>A concept of static "membership" has been added to the server which is browsable
200+ over the <a href="https://en.wikipedia.org/wiki/WebFinger">webfinger</a> protocol. You
201+ can use the <code>quipu</code> binary to query resources on an any Ayllu server:</p>
202+ <pre><code class="language-sh">quipu finger "acct:demo@example.org"
203+ </code></pre>
204+ <pre><code class="language-json">{
205+ "subject": "demo@example.org",
206+ "links": [
207+ {
208+ "rel": "http://webfinger.net/rel/profile-page",
209+ "type": "text/html",
210+ "href": "https://example.org/demo"
211+ },
212+ {
213+ "rel": "http://webfinger.net/rel/profile-page",
214+ "type": "text/html",
215+ "href": "https://example.org/@demo"
216+ },
217+ {
218+ "rel": "http://webfinger.net/rel/avatar",
219+ "href": "https://example.org/avatar.png"
220+ },
221+ {
222+ "rel": "http://ayllu-forge.org/rel/description",
223+ "properties": {
224+ "text": "Programmer interested free software"
225+ }
226+ }
227+ ]
228+ }
229+ </code></pre>
230+ <pre><code class="language-sh"># Or via curl
231+ curl 'http://localhost:10000/.well-known/webfinger?resource=acct:demo@example.org' |jq .
232+ </code></pre>
233+ <p>Currently the server only can share static account information from the global
234+ configuration file but future updates will expose repository and other
235+ information. Eventually there are plans to implement global searchable index of
236+ Ayllu instances which will crawl this protocol data.</p>
237+ <h3>Mirror Aware Repositories</h3>
238+ <p>Ayllu will detect repositories that contain origins that have the <code>mirror</code> flag
239+ enabled and display them as "non-canonical" sources in the UI.</p>
240+ <h3>Multiuser Container Image</h3>
241+ <p>The <a href="https://ayllu-forge.org/ayllu/ayllu/tree/main/containers/multiuser">multiuser</a>
242+ container image has been further developed and offers a simple way for managing
243+ trusted multi-user forge environments.</p>
244+ <h3>New Crates</h3>
245+ <p>Two new crates <a href="https://ayllu-forge.org/ayllu/ayllu/tree/main/ayllu-shell">ayllu-shell</a>
246+ and <a href="https://ayllu-forge.org/ayllu/ayllu/tree/main/ayllu-keys">ayllu-keys</a> were
247+ added which when completed will allow a simple static configuration of user
248+ authorization into an Ayllu managed server.</p>
249+ <h2>Supplemental Libraries</h2>
250+ <h3>Maitred SMTP Server</h3>
251+ <p>A new crate offering an embeddable SMTP server has been developed called
252+ <a href="https://ayllu-forge.org/ayllu/maitred">maitred</a> for use in Ayllu so that it
253+ can easily receive patches over SMTP. This project is made possible by the
254+ low-level protocol implementation of the
255+ <a href="https://github.com/stalwartlabs/mail-server">stalwart</a> project. Most of this
256+ crate is functional however ARC and DMARC verification need to be finished
257+ before we can safely handle messages. Once completed this will be implemented
258+ as part of the <code>ayllu-mail</code> binary.</p>
259+ <h3>Papari OCI Container Registry</h3>
260+ <p>The <a href="https://ayllu-forge.org/ayllu/papyri">Papyri</a> OCI container registry has
261+ started development which will provide the ability to read and write container
262+ images directly into an Ayllu instance. This crate currently only supports
263+ direct file system storage but a SQLite blob backend or possibly S3 compatible
264+ store may be added. This crate is shipped with an export of Axum routes and
265+ once integrated will allow pushing and pulling OCI manifests directly from
266+ the web interface.</p>
267+ <h1 />
268+ <p>That's all for now, thanks for reading!</p></div></content></entry><entry><link href="https://kevinschoon.com/blog/building-this-website" rel="self" /><id>https://kevinschoon.com/blog/building-this-website</id><title>There is no deficiency of static website generators these days and so I had to write my own.</title><updated>2023-04-14T01:01:01+00:00</updated><content type="xhtml" xml:lang="en"><div xmlns="http://www.w3.org/1999/xhtml"><h1>Building This Website</h1>
269+ <p>There is no deficiency of static website generators these days and so naturally
270+ I had to write my own.</p>
271+ <h2>Overview</h2>
272+ <p>The main functionality of the tool will be encapsulated in a Python script
273+ called <code>render.py</code>. Even though
274+ <a href="https://www.devever.net/~hl/stringtemplates#narrow">string based templates are the wrong solution</a>
275+ we're going to use a combination of
276+ <a href="https://jinja.palletsprojects.com/">Jinja</a> and markdown to generate HTML
277+ output. Each page will be generated by a single call to the script which will
278+ allow the whole thing to be ran in parallel across multiple CPUs by wiring
279+ it together using <a href="https://ninja-build.org/">ninja</a>.</p>
280+ <p>Here you can see an example invocation of the <code>render.py</code> script with some
281+ annotated flags.</p>
282+ <pre><code>./render.py \
283+ # the HTML template file
284+ --template=index.jinja \
285+ # this contains a map of each webpage which is available in the renderer
286+ # as well as page level configuration options.
287+ --sitemap sitemap.yaml \
288+ # markdown file, this one contains the text I'm currently typing!
289+ --content=blog/building-this-website/README.md \
290+ # output of the rendered HTML page
291+ gen/blog/building-this-website/index.html
292+ </code></pre>
293+ <h3>Hierarchical Sitemap</h3>
294+ <p>I want to generate a "tree" sitemap similar to a file browser like NERDTree.
295+ We'll pass in a YAML document to the <code>render.py</code> script and then transform
296+ it into an XML tree.</p>
297+ <pre><code>def _load_sitemap(path, current_path):
298+ by_name = dict()
299+ flattened = []
300+ with open(path, "r") as fp:
301+ links = yaml.safe_load(fp.read())
302+
303+ def _link(links, parent=None):
304+ for link in links:
305+ flattened.append(link)
306+ link["parent"] = parent
307+ if "others" in link:
308+ _link(link["others"], parent=link)
309+
310+ def _make_url(link):
311+ path = [link["name"]]
312+ parent = link["parent"]
313+ while parent is not None:
314+ path.append(parent["name"])
315+ parent = parent["parent"]
316+ path.reverse()
317+ return "/".join(path)
318+
319+ _link(links, parent=None)
320+
321+ for link in flattened:
322+ target = "/" + _make_url(link)
323+ if target == current_path:
324+ link["active"] = True
325+ else:
326+ link["active"] = False
327+ link["url"] = target
328+ by_name[target] = link
329+
330+ return dict(by_name=by_name, links=links)
331+
332+
333+ def _make_tree(links):
334+ def _populate(root, links):
335+ for link in links:
336+ elm = ET.Element("li")
337+ if "directory" in link and link["directory"]:
338+ elm.text = link["name"]
339+ else:
340+ lref = ET.Element("a", href=link["url"])
341+ lref.text = link["name"]
342+ if link["active"]:
343+ lref.text = lref.text + " &lt;"
344+ elm.append(lref)
345+ if "others" in link:
346+ others = link["others"]
347+ ul = ET.Element("ul")
348+ elm.append(ul)
349+ _populate(root=ul, links=others)
350+ root.append(elm)
351+
352+ params = {"class": "tree"}
353+
354+ root = ET.Element("ul", **params)
355+ _populate(root=root, links=links)
356+ return ET.tostring(root).decode()
357+ </code></pre>
358+ <h3>Dithering Engine</h3>
359+ <p>Images can be reduced in size with <a href="https://en.wikipedia.org/wiki/Dither">dithering algorithms</a>
360+ and they also look very cool. The idea for this was inspired by <a href="https://solar.lowtechmagazine.com/">LOW←TECH MAGAZINE</a>.</p>
361+ <h3>Spell &amp; Grammar Checking</h3>
362+ <p>I can implement some basic spell checking against the website with <a href="http://aspell.net/">aspell</a>.</p>
363+ <pre><code class="language-sh">find content -name '*.md' -exec aspell --mode=markdown check {} \;
364+ </code></pre>
365+ <h3>Filesystem "Watch" Support</h3>
366+ <p>We'll use the native <a href="https://en.wikipedia.org/wiki/Inotify">inotify</a> via the
367+ <a href="https://man7.org/linux/man-pages/man1/inotifywatch.1.html">inotifywatch</a> tool
368+ which is available on all Linux distributions.</p>
369+ <pre><code class="language-sh">#!/bin/sh
370+
371+ # the actual command we want to run each time any file changes in the project
372+ BUILD_CMD="$@"
373+
374+ # some pretty colorized output
375+ _do_build() {
376+ $BUILD_CMD &amp;&amp; {
377+ echo -e "\033[32;1;4mSuccess\033[0m"
378+ } || {
379+ echo -e "\033[31;1;4mFailure\033[0m"
380+ }
381+ }
382+
383+ # build the project the first time you start up the watch
384+ _do_build
385+
386+ # watch all the files in the project except for the output directory
387+ find . -type d -not -path './gen*' -printf '%p ' | xargs inotifywait \
388+ -m -e close_write \
389+ --format %e/%f |
390+ while IFS=/ read -r events file; do
391+ echo "file $file modified, rebuilding"
392+ _do_build
393+ done
394+ </code></pre>
395+ <p>Now we can test the script by launching it <code>scripts/watch.sh build.py</code>.</p>
396+ <p>You can checkout the source for the whole site
397+ <a href="https://ayllu-forge.org/web/kevinschoon-dot-com">here</a>.</p></div></content></entry></feed>
398\ No newline at end of file
399 diff --git a/gen/ks_stylized.png b/gen/ks_stylized.png
400new file mode 100644
401index 0000000..0123974
402 Binary files /dev/null and b/gen/ks_stylized.png differ
403 diff --git a/gen/main.css b/gen/main.css
404new file mode 100644
405index 0000000..852ef3e
406--- /dev/null
407+++ b/gen/main.css
408 @@ -0,0 +1,193 @@
409+ :root {
410+ --accent: #b1675d;
411+ --light-gray: #eee;
412+ --dark-gray: #c2c2c2;
413+ --white: #f8f9fa;
414+ --black: #2d2d2d;
415+ --border-width: 5px;
416+ }
417+
418+ table {
419+ text-align: justify;
420+ width: 100%;
421+ border-collapse: collapse;
422+ margin-bottom: 2rem;
423+ }
424+
425+ td, th {
426+ padding: 0.5em;
427+ border-bottom: 1px solid #4a4a4a;
428+ }
429+
430+ html, body {
431+ box-sizing: border-box;
432+ font-size: 16px;
433+ /*font-family: monospace;*/
434+ height: 100%;
435+ margin: 0;
436+ padding: 0;
437+ max-width: 800px;
438+ }
439+
440+ section {
441+ height: 100%;
442+ }
443+
444+ .wrapper {
445+ min-height: 100%;
446+ display: grid;
447+ grid-template-rows: auto 1fr auto;
448+ }
449+
450+ .page-body {
451+ padding: 20px;
452+ max-inline-size: 70ch;
453+ }
454+
455+ footer.page-footer {
456+ }
457+
458+ *,
459+ *:before,
460+ *:after {
461+ box-sizing: inherit;
462+ }
463+
464+ ol,
465+ ul {
466+ list-style: none;
467+ }
468+
469+ p {
470+ padding: 1px;
471+ margin: 10px;
472+ }
473+
474+ span {
475+ background: white;
476+ border: 1px solid black;
477+ }
478+
479+ pre {
480+ background: #e2e2e2;
481+ font-family: monospace;
482+ overflow: scroll;
483+ }
484+
485+ img.me {
486+ border-radius: 2em;
487+ max-width: 350px;
488+ }
489+
490+ main.rss-page {
491+ margin: 20px;
492+ }
493+
494+ @media (prefers-color-scheme: dark) {
495+ :root {
496+ background-color: #2c2c2c;
497+ color: white;
498+ }
499+
500+ pre {
501+ color: white;
502+ background: #3f3f3f;
503+ }
504+
505+ a:link {
506+ color: white;
507+ text-decoration: underline;
508+ }
509+
510+ a:visited {
511+ color: white;
512+ text-decoration: none;
513+ }
514+
515+ .notice {
516+ border-color: white;
517+ }
518+ }
519+
520+ @media (prefers-color-scheme: light) {
521+ :root {
522+ background-color: #f5f5f5;
523+ color: black;
524+ }
525+
526+ pre {
527+ color: black;
528+ background: #e2e2e2;
529+ }
530+
531+ a:link {
532+ color: black;
533+ text-decoration: underline;
534+ }
535+
536+ a:visited {
537+ color: black;
538+ text-decoration: none;
539+ }
540+
541+ .notice {
542+ border-color: black;
543+ }
544+ }
545+
546+
547+
548+ /*
549+ Tree structure using CSS:
550+ http://stackoverflow.com/questions/14922247/how-to-get-a-tree-in-html-using-pure-css
551+ */
552+
553+ .tree,
554+ .tree ul {
555+ font-size: 18px;
556+ font: normal normal 14px/20px Helvetica, Arial, sans-serif;
557+ list-style-type: none;
558+ margin-left: 0 0 0 10px;
559+ padding: 0;
560+ position: relative;
561+ overflow: hidden;
562+ }
563+
564+ .tree li {
565+ margin: 0;
566+ padding: 0 12px;
567+ position: relative;
568+ }
569+
570+ .tree li::before,
571+ .tree li::after {
572+ content: "";
573+ position: absolute;
574+ left: 0;
575+ }
576+
577+ /* horizontal line on inner list items */
578+ .tree li::before {
579+ border-top: 2px solid #999;
580+ top: 10px;
581+ width: 10px;
582+ height: 0;
583+ }
584+
585+ /* vertical line on list items */
586+ .tree li:after {
587+ border-left: 2px solid #999;
588+ height: 100%;
589+ width: 0px;
590+ top: -10px;
591+ }
592+
593+ /* lower line on list items from the first level because they don't have parents */
594+ .tree > li::after {
595+ top: 10px;
596+ }
597+
598+ /* hide line from the last of the first level list items */
599+ .tree > li:last-child::after {
600+ display: none;
601+ }
602 diff --git a/gen/style.xsl b/gen/style.xsl
603new file mode 100644
604index 0000000..3dcd0ff
605--- /dev/null
606+++ b/gen/style.xsl
607 @@ -0,0 +1,48 @@
608+ <?xml version="1.0" encoding="utf-8"?>
609+ <xsl:stylesheet version="3.0"
610+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
611+ xmlns:atom="http://www.w3.org/2005/Atom">
612+ <xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
613+ <xsl:template match="/">
614+ <html lang="en">
615+ <head>
616+ <title>
617+ <xsl:value-of select="atom:feed/atom:title"/>
618+ </title>
619+ <link rel="stylesheet" href="/main.css" />
620+ </head>
621+ <body>
622+ <main class="rss-page">
623+ <article>
624+ <header>
625+ <h1>
626+ <a href="/"><img class="me" src="/ks_stylized.png"/></a>
627+ </h1>
628+ <h2><xsl:value-of select="atom:feed/atom:title"/></h2>
629+ </header>
630+ <p><strong>This is an <a href="https://en.wikipedia.org/wiki/Atom_(web_standard)">Atom feed</a>.</strong></p>
631+ <p>Copy the link from the address bar into your feed reader to receive regular updates.</p>
632+ </article>
633+ <xsl:for-each select="atom:feed/atom:entry">
634+ <article class="card">
635+ <header>
636+ <h3>
637+ <a>
638+ <xsl:attribute name="href">
639+ <xsl:value-of select="atom:link/@href"/>
640+ </xsl:attribute>
641+ <xsl:value-of select="atom:title"/>
642+ </a>
643+ </h3>
644+ </header>
645+ <footer class="rss">
646+ <xsl:value-of select="author" /><br/>
647+ <b><xsl:value-of select="pubDate"/></b>
648+ </footer>
649+ </article>
650+ </xsl:for-each>
651+ </main>
652+ </body>
653+ </html>
654+ </xsl:template>
655+ </xsl:stylesheet>