Commit
Author: Kevin Schoon [me@kevinschoon.com]
Hash: 984ee7afcdb506f8055ab043755d8d5e8bd213db
Timestamp: Sat, 24 Aug 2024 14:46:10 +0000 (3 months ago)

+172 -130 +/-8 browse
add rss support
1diff --git a/build.py b/build.py
2index a15b4c8..e424c55 100755
3--- a/build.py
4+++ b/build.py
5 @@ -13,26 +13,39 @@ def _nth(n):
6
7
8 def _render_rule(w: ninja_syntax.Writer, name, link):
9- w.rule(f"render_{name}", " ".join([
10- "./render.py",
11- "-stylesheet",
12- "gen/main.css",
13- "-template",
14- _nth(1),
15- "-sitemap",
16- "sitemap.yaml",
17- "-link", link,
18- "-content", _nth(2) + "| htmlmin > $out",
19- ]))
20+ w.rule(
21+ f"render_{name}",
22+ " ".join(
23+ [
24+ "./render.py",
25+ "-stylesheet",
26+ "gen/main.css",
27+ "-template",
28+ _nth(1),
29+ "-sitemap",
30+ "sitemap.yaml",
31+ "-link",
32+ link,
33+ "-content",
34+ _nth(2) + "| htmlmin > $out",
35+ ]
36+ ),
37+ )
38 return f"render_{name}"
39
40
41+ def _render_rss_rule(w: ninja_syntax.Writer):
42+ w.rule(f"render_rss", " ".join(["./render.py", "-rss", "> $out"]))
43+ return f"render_rss"
44+
45+
46 def _compile_sm(w: ninja_syntax.Writer, entries: list, path: pathlib.Path):
47- entries = list(filter(
48- lambda entry: "enabled" not in entry or entry["enabled"], entries))
49+ entries = list(
50+ filter(lambda entry: "enabled" not in entry or entry["enabled"], entries)
51+ )
52 for entry in entries:
53 next_path = path.joinpath(entry["name"])
54- if "others" in entry and entry["others"]: # its a dir
55+ if "others" in entry and entry["others"]: # its a dir
56 w.build(str(next_path), "mkdir")
57 _compile_sm(w, entry["others"], next_path)
58 else:
59 @@ -47,8 +60,11 @@ def _compile_sm(w: ninja_syntax.Writer, entries: list, path: pathlib.Path):
60 target = target.replace("README.md", "index.html", 1)
61 target = "gen/" + target
62 cmd = _render_rule(w, source.replace("/", "_", -1), link)
63- w.build(target, cmd, inputs=[
64- "index.jinja", source, "sitemap.yaml", "render.py"])
65+ w.build(
66+ target,
67+ cmd,
68+ inputs=["index.html.jinja", source, "sitemap.yaml", "render.py"],
69+ )
70
71
72 if __name__ == "__main__":
73 @@ -62,7 +78,7 @@ if __name__ == "__main__":
74 w.rule("compile_sass", command="sassc $sass_flags $in $out")
75 w.rule("make_qr_code", command="cat $in | qrencode -o $out")
76 w.rule("copy", command="cp $in $out")
77- w.build("index.jinja", "phony")
78+ w.build("index.html.jinja", "phony")
79 w.build("render.py", "phony")
80 w.build("sitemap.yaml", "phony")
81 w.build("gen", "mkdir")
82 @@ -71,8 +87,18 @@ if __name__ == "__main__":
83 w.build("gen/favicon.ico", "copy", inputs=["assets/favicon.ico"])
84 w.build("gen/main.css", "compile_sass", inputs=["main.scss"])
85 cmd = _render_rule(w, "index", "/")
86- w.build("gen/index.html", cmd, inputs=[
87- "index.jinja", "content/index.md", "sitemap.yaml", "render.py"])
88+ w.build(
89+ "gen/index.html",
90+ cmd,
91+ inputs=[
92+ "index.html.jinja",
93+ "content/index.md",
94+ "sitemap.yaml",
95+ "render.py",
96+ ],
97+ )
98+ cmd = _render_rss_rule(w)
99+ w.build("gen/index.xml", cmd)
100 if sitemap["enabled"]:
101 _compile_sm(w, sitemap["entries"], pathlib.Path("./content"))
102 w.close()
103 diff --git a/content/blog/building-this-website/README.md b/content/blog/building-this-website/README.md
104index 55af1a0..25cf874 100644
105--- a/content/blog/building-this-website/README.md
106+++ b/content/blog/building-this-website/README.md
107 @@ -30,27 +30,6 @@ annotated flags.
108 gen/blog/building-this-website/index.html
109 ```
110
111- ### Builtin Preview Server
112-
113- In "production" we'll serve all of the static content of an Nginx server but
114- locally we want a way to browse the current state. This will be really simple,
115- just add a Make target that starts up Python's builtin
116- [http.server](https://docs.python.org/3/library/http.server.html#module-http.server).
117-
118- ```
119- BIND_ARGS := 127.0.0.1 9000
120-
121- serve:
122- python -m http.server --bind ${BIND_ARGS} --directory gen
123- ```
124-
125- ### Templating
126-
127- Each page and it's content will be written as a markdown file that can include
128- Jinja syntax.
129-
130- Python + Markdown + Jinja
131-
132 ### Hierarchical Sitemap
133
134 I want to generate a "tree" sitemap similar to a file browser like NERDTree.
135 @@ -128,12 +107,9 @@ and they also look very cool. The idea for this was inspired by <a href="https:/
136 ### Spell & Grammar Checking
137
138 I can implement some basic spell checking against the website with [aspell](http://aspell.net/).
139- If I was really ambitious (I'm not) it could be integrated into some kind of
140- automated testing. For now a simple Make target should suffice.
141
142- ```
143- spell:
144- find content -name '*.md' -exec aspell --mode=markdown check {} \;
145+ ```sh
146+ find content -name '*.md' -exec aspell --mode=markdown check {} \;
147 ```
148
149 ### Filesystem "Watch" Support
150 diff --git a/content/index.md b/content/index.md
151index 7e6a726..543ca64 100644
152--- a/content/index.md
153+++ b/content/index.md
154 @@ -13,3 +13,6 @@ Check out some of my past and present projects [here](https://ayllu-forge.org/br
155 Contact info (same as above, but a QR code):
156
157 <img alt="qr code" class="contact" src="/contact.png"/>
158+
159+ ### Subscribe
160+ [RSS](/index.xml)
161 diff --git a/index.html.jinja b/index.html.jinja
162new file mode 100644
163index 0000000..3ae4697
164--- /dev/null
165+++ b/index.html.jinja
166 @@ -0,0 +1,34 @@
167+ <!DOCTYPE html>
168+ <html lang="en">
169+ <head>
170+ <title>kevinschoon-dot-com</title>
171+ <meta charset="UTF-8">
172+ <meta name="viewport" content="width=device-width, initial-scale=1">
173+ <link rel="stylesheet" href="/main.css"/>
174+ </head>
175+ <body>
176+ <section>
177+ <div class="wrapper">
178+ <header class="page-header">
179+ {% if not link == "/" %}<a href="/">Home</a>{% endif %}
180+ </header>
181+ <main class="page-body">
182+ {{ content }}
183+ </main>
184+ <footer class="page-footer"></footer>
185+ </div>
186+ </section>
187+ </body>
188+ {% if devmode %}
189+ <script>
190+ const socket = new WebSocket("ws://localhost:8080/ws")
191+ socket.addEventListener("open", (event) => {
192+ console.log("connected to server", event);
193+ });
194+ socket.addEventListener("message", (event) => {
195+ console.log("reloading page", event);
196+ window.location.reload();
197+ })
198+ </script>
199+ {% endif %}
200+ </html>
201 diff --git a/index.jinja b/index.jinja
202deleted file mode 100644
203index 3ae4697..0000000
204--- a/index.jinja
205+++ /dev/null
206 @@ -1,34 +0,0 @@
207- <!DOCTYPE html>
208- <html lang="en">
209- <head>
210- <title>kevinschoon-dot-com</title>
211- <meta charset="UTF-8">
212- <meta name="viewport" content="width=device-width, initial-scale=1">
213- <link rel="stylesheet" href="/main.css"/>
214- </head>
215- <body>
216- <section>
217- <div class="wrapper">
218- <header class="page-header">
219- {% if not link == "/" %}<a href="/">Home</a>{% endif %}
220- </header>
221- <main class="page-body">
222- {{ content }}
223- </main>
224- <footer class="page-footer"></footer>
225- </div>
226- </section>
227- </body>
228- {% if devmode %}
229- <script>
230- const socket = new WebSocket("ws://localhost:8080/ws")
231- socket.addEventListener("open", (event) => {
232- console.log("connected to server", event);
233- });
234- socket.addEventListener("message", (event) => {
235- console.log("reloading page", event);
236- window.location.reload();
237- })
238- </script>
239- {% endif %}
240- </html>
241 diff --git a/index.xml.jinja b/index.xml.jinja
242new file mode 100644
243index 0000000..b92f652
244--- /dev/null
245+++ b/index.xml.jinja
246 @@ -0,0 +1 @@
247+ {{content}}
248 diff --git a/render.py b/render.py
249index e746f49..216798e 100755
250--- a/render.py
251+++ b/render.py
252 @@ -3,11 +3,13 @@ import argparse
253 import os
254 import sys
255 import xml.etree.ElementTree as ET
256-
257+ from io import StringIO
258 from datetime import datetime
259
260 import markdown
261 import yaml
262+
263+ from bs4 import BeautifulSoup
264 from jinja2 import Environment, FunctionLoader
265
266
267 @@ -22,8 +24,11 @@ def _load_sitemap(path, current_path):
268
269 def _filter_entries(entries):
270 entries = list(
271- filter(lambda entry: "enabled"
272- not in entry or entry["enabled"] is True, entries))
273+ filter(
274+ lambda entry: "enabled" not in entry or entry["enabled"] is True,
275+ entries,
276+ )
277+ )
278 for entry in entries:
279 if "others" in entry:
280 entry["others"] = _filter_entries(entry["others"])
281 @@ -90,65 +95,95 @@ def _make_tree(links):
282 return ET.tostring(root).decode()
283
284
285+ def load_content(path):
286+ with open(path) as fp:
287+ text = fp.read()
288+ content = markdown.markdown(
289+ text,
290+ extensions=["fenced_code", "tables"],
291+ )
292+ return content
293+
294+
295 def generate(template, content_path, stylesheet, link, sitemap, devmode=False):
296- sm, tree = None, None
297- if sitemap:
298- sm = _load_sitemap(sitemap, current_path=link)
299- if sm:
300- tree = _make_tree(sm["links"])
301+ tree = _make_tree(sitemap["links"])
302 with open(template, "r") as fp:
303 template = fp.read()
304 env = Environment(loader=FunctionLoader(lambda name: template))
305 base = env.get_template(template)
306 with open(stylesheet) as fp:
307 style = fp.read()
308- with open(content_path) as fp:
309- text = fp.read()
310- content = markdown.markdown(
311- text,
312- extensions=["fenced_code", "tables"],
313- )
314+ content = load_content(content_path)
315 env_content = Environment(loader=FunctionLoader(lambda name: content))
316- out = base.render({
317- "content": env_content.get_template("").render({
318- "sitemap": sm,
319+ out = base.render(
320+ {
321+ "content": env_content.get_template("").render(
322+ {
323+ "sitemap": sitemap,
324+ "tree": tree,
325+ }
326+ ),
327+ "is_index": False,
328+ "link": link,
329+ "style": style,
330+ "sitemap": sitemap,
331 "tree": tree,
332- }),
333- "is_index": False,
334- "link": link,
335- "style": style,
336- "sitemap": sm,
337- "tree": tree,
338- "devmode": devmode,
339- "current_time": datetime.now(),
340- })
341- print(out)
342+ "devmode": devmode,
343+ "current_time": datetime.now(),
344+ }
345+ )
346+ sys.stdout.write(out)
347+
348+
349+ def generate_rss(sitemap):
350+ sitemap = _load_sitemap(sitemap, "/")
351+
352+ rss = ET.Element("rss", attrib={"version": "2.0", "encoding": "UTF-8"})
353+ channel = ET.SubElement(rss, "channel")
354+
355+ title = ET.SubElement(channel, "title")
356+ title.text = "Blog of Kevin Schoon"
357+ link = ET.SubElement(channel, "link")
358+ link.attrib["href"] = "https://kevinschoon.com"
359+ description = ET.SubElement(channel, "description")
360+ description.text = "Blog of Kevin Schoon"
361+
362+ for entry in sitemap["by_name"]["/blog"]["others"]:
363+ item = ET.SubElement(channel, "item")
364+ title_item = ET.SubElement(item, "title")
365+ link_item = ET.SubElement(item, "link")
366+ description_item = ET.SubElement(item, "description")
367+ title_item.text = entry["name"]
368+ link_item.attrib["href"] = "https://kevinschoon.com/blog/" + entry["name"]
369+ description_item.text = entry["tagline"]
370+
371+ xml_str = ET.tostring(rss, xml_declaration=True, encoding="utf-8", method="xml")
372+ sys.stdout.buffer.write(xml_str)
373
374
375 if __name__ == "__main__":
376 parser = argparse.ArgumentParser(description="static renderer")
377- parser.add_argument("-content",
378- required=True,
379- help="markdown file with content")
380- parser.add_argument("-template",
381- default="index.jinja",
382- help="jinja template file")
383- parser.add_argument("-stylesheet",
384- default="./assets/main.css",
385- help="style sheet")
386- parser.add_argument("-sitemap",
387- help="sitemap")
388- parser.add_argument("-link",
389- default="/",
390- help="current url")
391- parser.add_argument("-devmode",
392- action="store_true",
393- help="developer mode (websocket page refresh)")
394+ parser.add_argument("-rss", help="if rendering the rss feed", action="store_true")
395+ parser.add_argument("-content", help="markdown file with content")
396+ parser.add_argument(
397+ "-template", default="index.html.jinja", help="jinja template file"
398+ )
399+ parser.add_argument("-stylesheet", default="./assets/main.css", help="style sheet")
400+ parser.add_argument("-sitemap", default="sitemap.yaml", help="sitemap")
401+ parser.add_argument("-link", default="/", help="current url")
402+ parser.add_argument(
403+ "-devmode", action="store_true", help="developer mode (websocket page refresh)"
404+ )
405 args = parser.parse_args()
406- generate(
407- template=args.template,
408- content_path=args.content,
409- stylesheet=args.stylesheet,
410- link=args.link,
411- sitemap=args.sitemap,
412- devmode=args.devmode)
413+ sitemap = _load_sitemap(args.sitemap, args.link)
414+ if args.rss:
415+ generate_rss(args.sitemap)
416+ else:
417+ generate(
418+ template=args.template,
419+ content_path=args.content,
420+ stylesheet=args.stylesheet,
421+ link=args.link,
422+ sitemap=sitemap,
423+ devmode=args.devmode,
424+ )
425 diff --git a/sitemap.yaml b/sitemap.yaml
426index e772827..07d9bdc 100644
427--- a/sitemap.yaml
428+++ b/sitemap.yaml
429 @@ -4,6 +4,7 @@ entries:
430 enabled: true
431 others:
432 - name: building-this-website
433+ tagline: There is no deficiency of static website generators these days and so I had to write my own.
434 date: 2023-04-04
435 - name: projects
436 enabled: true