diff options
author | Stonewall Jackson <stonewall@sacredheartsc.com> | 2023-01-22 09:56:17 -0500 |
---|---|---|
committer | Stonewall Jackson <stonewall@sacredheartsc.com> | 2023-01-22 09:56:17 -0500 |
commit | 9f1b37fa346a7dcf77c1b6963a6d2e4b871fe5ed (patch) | |
tree | 14aa09de866a3283e1b07a94ea9cc6d70fc3e49d | |
download | www-9f1b37fa346a7dcf77c1b6963a6d2e4b871fe5ed.tar.gz www-9f1b37fa346a7dcf77c1b6963a6d2e4b871fe5ed.zip |
initial commit
28 files changed, 1373 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..99dddf5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +public +.bloglist.md +*.pyc +*.pyo +*.swp @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 stonewall@sacredheartsc.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8f81f2b --- /dev/null +++ b/Makefile @@ -0,0 +1,108 @@ +### CHANGE ME ###################### +DOMAIN = www.sacredheartsc.com +URL = https://$(DOMAIN) +RSYNC_TARGET = $(DOMAIN):/var/www/$(DOMAIN) +FEED_TITLE = sacredheartsc blog +FEED_DESCRIPTION = Carolina-grown articles about self-hosting, privacy, unix, and more. +STATIC_REGEX = .*\.(html|css|jpg|jpeg|png|ico|xml|txt|asc) +BLOG_LIST_LIMIT = 5 + + +### VARIABLES ###################### +SHELL = /bin/bash -e -o pipefail + +SOURCE_DIR = src +OUTPUT_DIR = public +SCRIPT_DIR = scripts + +BLOG_DIR = blog + +TEMPLATE = templates/default.html +DEFAULTS = defaults.yaml + +BLOG_LIST_SCRIPT = $(SCRIPT_DIR)/bloglist.py +BLOG_LIST_REPLACE = __BLOG_LIST__ +BLOG_LIST_FILE = .bloglist.md + +BLOG_RSS_SCRIPT = $(SCRIPT_DIR)/rss.py +BLOG_RSS_FILE = $(BLOG_DIR)/feed.xml + +SOURCE_DIRS := $(shell find $(SOURCE_DIR) -mindepth 1 -type d) +SOURCE_MARKDOWN := $(shell find $(SOURCE_DIR) -type f -name '*.md' -and ! -name $(BLOG_LIST_FILE)) +SOURCE_STATIC := $(shell find $(SOURCE_DIR) -type f -regextype posix-extended -iregex '$(STATIC_REGEX)') +BLOG_POSTS := $(shell find $(SOURCE_DIR)/$(BLOG_DIR) -type f -name '*.md' -and ! -name $(BLOG_LIST_FILE) -and ! -path $(SOURCE_DIR)/$(BLOG_DIR)/index.md) + +OUTPUT_DIRS := $(patsubst $(SOURCE_DIR)/%, $(OUTPUT_DIR)/%, $(SOURCE_DIRS)) +OUTPUT_MARKDOWN := $(patsubst $(SOURCE_DIR)/%, $(OUTPUT_DIR)/%, $(patsubst %.md, %.html, $(SOURCE_MARKDOWN))) +OUTPUT_STATIC := $(patsubst $(SOURCE_DIR)/%, $(OUTPUT_DIR)/%, $(SOURCE_STATIC)) + +COPY = cp --preserve=timestamps + +PANDOC = pandoc \ + --highlight-style=kate \ + --metadata=feed:/$(BLOG_RSS_FILE) \ + --defaults=$(DEFAULTS) + +RSSGEN = $(BLOG_RSS_SCRIPT) \ + $(SOURCE_DIR)/$(BLOG_DIR) \ + --title="$(FEED_TITLE)" \ + --description="$(FEED_DESCRIPTION)" \ + --url="$(URL)" \ + --blog-path="/$(BLOG_DIR)" \ + --feed-path="/$(BLOG_RSS_FILE)" + + +### TARGETS ###################### +public: \ + $(OUTPUT_DIRS) \ + $(OUTPUT_MARKDOWN) \ + $(OUTPUT_STATIC) \ + $(OUTPUT_DIR)/$(BLOG_RSS_FILE) + +$(OUTPUT_DIRS): + mkdir -p $@ + +# Homepage +$(OUTPUT_DIR)/index.html: $(SOURCE_DIR)/index.md $(SOURCE_DIR)/$(BLOG_LIST_FILE) $(TEMPLATE) + sed $$'/$(BLOG_LIST_REPLACE)/{r $(SOURCE_DIR)/$(BLOG_LIST_FILE)\nd}' $< | $(PANDOC) --template=$(TEMPLATE) --output=$@ + +# CV +$(OUTPUT_DIR)/cv/index.html: $(SOURCE_DIR)/cv/index.md templates/cv.html + $(PANDOC) --template=templates/cv.html --output=$@ $< + +$(SOURCE_DIR)/$(BLOG_LIST_FILE): $(BLOG_POSTS) $(BLOG_LIST_SCRIPT) + $(BLOG_LIST_SCRIPT) $(SOURCE_DIR)/$(BLOG_DIR) $(BLOG_LIST_LIMIT) > $@ + +# Blog listing +$(OUTPUT_DIR)/$(BLOG_DIR)/index.html: $(SOURCE_DIR)/$(BLOG_DIR)/index.md $(SOURCE_DIR)/$(BLOG_DIR)/$(BLOG_LIST_FILE) $(TEMPLATE) + sed $$'/$(BLOG_LIST_REPLACE)/{r $(SOURCE_DIR)/$(BLOG_DIR)/$(BLOG_LIST_FILE)\nd}' $< | $(PANDOC) --template=$(TEMPLATE) --output=$@ + +$(SOURCE_DIR)/$(BLOG_DIR)/$(BLOG_LIST_FILE): $(BLOG_POSTS) $(BLOG_LIST_SCRIPT) + $(BLOG_LIST_SCRIPT) $(SOURCE_DIR)/$(BLOG_DIR) > $@ + +# RSS feed +$(OUTPUT_DIR)/$(BLOG_RSS_FILE): $(BLOG_POSTS) $(BLOG_RSS_SCRIPT) + $(RSSGEN) > $@ + +# Blog posts +$(OUTPUT_DIR)/%.html: $(SOURCE_DIR)/%.md $(TEMPLATE) + $(PANDOC) --template=$(TEMPLATE) --output=$@ $< + +# Catch-all: static assets +$(OUTPUT_DIR)/%: $(SOURCE_DIR)/% + $(COPY) $< $@ + +.PHONY: install clean serve rsync +install: + pip install -r requirements.txt + +serve: public + cd $(OUTPUT_DIR) && python3 -m http.server + +clean: + rm -rf $(OUTPUT_DIR) + rm -f $(SOURCE_DIR)/$(BLOG_LIST_FILE) + rm -f $(SOURCE_DIR)/$(BLOG_DIR)/$(BLOG_LIST_FILE) + +rsync: public + rsync -rlphv --delete $(OUTPUT_DIR)/ $(RSYNC_TARGET) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3f34cf6 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +www.sacredheadheartsc.com +========================= + +This repository contains the source for [www.sacredheartsc.com](https://www.sacredheartsc.com), +which consists of markdown documents and a Makefile-driven static site generator. + +# Requirements + +- coreutils +- python3 +- pandoc +- make + +# Instructions + +First, install the `pip` requirements: + + make install + +You'll want to edit the [Makefile](/www/tree/Makefile) to set your site URL, +RSS feed title, etc. + +Then, start writing markdown documents in the `src` directory. You can use +whatever naming convention and directory structure you like. Files ending in +`.md` will be converted to `.html` with the same path. + +The `src/blog` directory is special. Markdown files in this directory are +used to populate the front-page blog listing in [index.md](/www/tree/src/index.md). +Before pandoc converts this file to HTML, the special string `__BLOG_LIST__` +is replaced with the output of [bloglist.py](/www/tree/scripts/bloglist.py). +This Python script produces a date-sorted markdown list of all your blog posts. + +Each markdown file can have YAML frontmatter with the following metadata: + + --- + title: A boring blog post + date: YYYY-MM-DD + subtitle: an optional subtitle + heading: optional, if you want the first <h1> to be different from <title> + description: optional, short description for <head> and the blog listing + draft: if set, hides the post from the blog listing + --- + +You can change the resulting HTML by modifying the [template](/www/tree/templates/default.html). +Changing the format of the blog listing requires modifying the Python script. + +Build the website by using the default target: + + make + +This will create a directory called `public` containing all your markdown files +rendered to HTML. + +You also can run a local webserver, which listens on port 8000, using: + + make serve diff --git a/defaults.yaml b/defaults.yaml new file mode 100644 index 0000000..c628134 --- /dev/null +++ b/defaults.yaml @@ -0,0 +1,9 @@ +from: markdown+fenced_divs+bracketed_spans+smart +to: html5 +standalone: true +html-q-tags: true + +css: + - /css/style.css + +metadata: diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ea85e26 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +python-frontmatter +dateparser diff --git a/scripts/bloglist.py b/scripts/bloglist.py new file mode 100755 index 0000000..9e10354 --- /dev/null +++ b/scripts/bloglist.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 + +import argparse +from common import get_blog_posts + +DATE_FORMAT = '%Y-%m-%d' + +parser = argparse.ArgumentParser('bloglist') +parser.add_argument('BLOG_DIR', type=str, help='Directory containing markdown blog posts') +parser.add_argument('LIMIT', nargs='?', default=None, type=int, help='Maximum number of posts to show') +args = parser.parse_args() + +posts = get_blog_posts(args.BLOG_DIR) + +if args.LIMIT is not None: + posts = posts[0:args.LIMIT] + +if len(posts) == 0: + print('Nothing has been posted yet!') +else: + for post in posts: + post_date = post['date'].strftime(DATE_FORMAT) + + print(f'- [{post["title"]}]({post["href"]}) ({post_date})\n') + if post['description']: + print(f' {post["description"]}\n') diff --git a/scripts/common.py b/scripts/common.py new file mode 100644 index 0000000..9b23816 --- /dev/null +++ b/scripts/common.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +import os +import frontmatter +import dateparser +import datetime +from pathlib import Path + +BLOG_LIST_FILE = '.bloglist.md' + +def get_href(path): + path = Path(path) + path = path.relative_to(*path.parts[:1]) + if path.name == 'index.md': + return '/{}/'.format(path.parent) + else: + return '/{}/{}.html'.format(path.parent, path.stem) + +def read_metadata(file): + post = frontmatter.load(file) + + for field in ['title', 'date']: + if post.get(field) is None: + raise Exception("{} is missing metadata field '{}'".format(file, field)) + + if type(post['date']) not in [datetime.datetime, datetime.date]: + date = dateparser.parse(post['date']) + else: + date = post['date'] + + return { + 'title': post.get('title'), + 'date': date, + 'author': post.get('author'), + 'description': post.get('description'), + 'draft': post.get('draft'), + 'href': get_href(file) + } + +def get_blog_posts(blog_dir): + blog_index = os.path.join(blog_dir, 'index.md') + posts = [] + + for root, dirs, files in os.walk(blog_dir): + for file in files: + path = os.path.join(root, file) + if path.endswith('.md') and path != blog_index and file != BLOG_LIST_FILE: + metadata = read_metadata(path) + if not metadata['draft']: + posts.append(metadata) + + posts.sort(key=lambda p: (p is None, p['date']), reverse=True) + return posts diff --git a/scripts/rss.py b/scripts/rss.py new file mode 100755 index 0000000..cf0cb9a --- /dev/null +++ b/scripts/rss.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 + +import argparse +import email.utils +from datetime import datetime +from common import get_blog_posts + +parser = argparse.ArgumentParser('rss') +parser.add_argument('BLOG_DIR', type=str, help='Directory containing markdown blog posts') +parser.add_argument('--limit', default=15, type=int, help='Maximum number of posts to show') +parser.add_argument('--title', help='Feed title', required=True) +parser.add_argument('--description', help='Feed description', required=True) +parser.add_argument('--url', help='Root URL', required=True) +parser.add_argument('--blog-path', help='Blog path', required=True) +parser.add_argument('--feed-path', help='RSS feed path', required=True) +args = parser.parse_args() + +posts = get_blog_posts(args.BLOG_DIR) +posts = posts[0:args.limit] + +build_date = email.utils.format_datetime(datetime.now().astimezone()) + +print(f'''<?xml version="1.0"?> +<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> +<channel> + <title>{args.title}</title> + <link>{args.url}{args.blog_path}</link> + <language>en-US</language> + <description>{args.description}</description> + <lastBuildDate>{build_date}</lastBuildDate> + <atom:link href="{args.url}{args.feed_path}" rel="self" type="application/rss+xml"/>''') + +for post in posts: + pub_date = email.utils.format_datetime(post['date'].astimezone()) + + print(f''' <item> + <title>{post["title"]}</title> + <link>{args.url}{post["href"]}</link> + <guid>{args.url}{post["href"]}</guid> + <pubDate>{pub_date}</pubDate>''') + + if 'description' in post: + print(f' <description>{post["description"]}</description>') + + print(' </item>') + +print('</channel>') +print('</rss>') diff --git a/src/android-chrome-192x192.png b/src/android-chrome-192x192.png Binary files differnew file mode 100644 index 0000000..f731faa --- /dev/null +++ b/src/android-chrome-192x192.png diff --git a/src/apple-touch-icon.png b/src/apple-touch-icon.png Binary files differnew file mode 100644 index 0000000..0491547 --- /dev/null +++ b/src/apple-touch-icon.png diff --git a/src/blog/desktop-linux-with-nfs-homedirs/index.md b/src/blog/desktop-linux-with-nfs-homedirs/index.md new file mode 100644 index 0000000..e01581a --- /dev/null +++ b/src/blog/desktop-linux-with-nfs-homedirs/index.md @@ -0,0 +1,226 @@ +--- +title: Desktop Linux with NFS Home Directories +date: January 19, 2023 +subtitle: Something no one does anymore, apparently. +description: Issues you'll face with NFS-mounted homedirs, and some workarounds. +--- + +I manage multiple [Rocky Linux](https://rockylinux.org/) workstations that automount +users' home directories via kerberized NFS. Unfortunately, I don't think this is a common +setup anymore--I encountered a few bugs and performance issues that needed non-obvious +workarounds. + +## Problems + +### 1. Things break when you log in from two places at once + +If you can somehow restrict your users to a single GNOME session at any given time, +you'll probably be fine. However, as soon as someone leaves his desktop running and +logs into another workstation, strange things begin to happen. Here are some oddities +I've observed: + + - GNOME settings on one machine are clobbered by the other (this may or may not be desirable). + + - Firefox refuses to run, because the profile directory is already in use. + + - `gnome-keyring` freaks out and creates many login keyrings under `~/.local/share/keyrings`, + losing previously stored secrets in the process! + + - Sound quits working (I suspect this is due to `~/.config/pulse/cookie` being clobbered). + + - Flatpak apps completely blow up (each app stores its state in `~/.var`, and + [this is nonconfigurable](https://github.com/flatpak/flatpak/issues/1651)). Running + multiple instances of `signal-dekstop` instantly corrupts the sqlite database. + + - `goa-daemon` generates thousands of syslog messages per minute (I am unsure if this is + due to `~/.config/goa-1.0/accounts.conf` getting clobbered, or a symptom of + [this bug](https://gitlab.gnome.org/GNOME/gnome-online-accounts/-/issues/32)). + I have no idea what `goa-daemon` does, nor do I want to. I have been victimized by + [the bazaar](http://www.catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/) + enough for one lifetime. + +### 2. It's slow + +I/O-heavy tasks, like compiling and grepping, will be much slower over NFS than the local +disk. Browser profiles stored on NFS (`~/.mozilla`, `~/.cache/chromium`, etc.) provide +a noticeably poor experience. + +File browsing is also painful if you have lots of images or videos. Thumbnails for +files stored on NFS will be cached in `~/.cache/thumbnails`, which is **also** stored +on NFS! + +## Solution: Move stuff to local storage + +The [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) +lets you change the default locations of `~/.cache`, `~/.config`, and the like by setting +some environment variables in the user's session. We can solve most of these problems +by moving the various XDG directories to the local disk. + +### Automatically provision local home directories + +First, let's write a script that automatically provisions a _local_ home directory +whenever someone logs in: + +````bash +#!/bin/bash + +# /usr/local/sbin/create-local-homedir.sh + +# Log all output to syslog. +exec 1> >(logger -s -t $(basename "$0")) 2>&1 + +PAM_UID=$(id -u "${PAM_USER}") + +if (( PAM_UID >= 1000 )); then + install -o "${PAM_USER}" -g "${PAM_USER}" -m 0700 -d "/usr/local/home/${PAM_USER}" +fi +```` + +Of course, it needs to be executable: + +````bash +chmod 755 /usr/local/sbin/create-local-homedir.sh +```` + +Next, we modify the PAM configuration to execute our script whenever anyone logs in +via GDM or SSH: + +````diff +--- /etc/pam.d/gdm-password ++++ /etc/pam.d/gdm-password +@@ -1,5 +1,6 @@ + auth [success=done ignore=ignore default=bad] pam_selinux_permit.so + auth substack password-auth ++auth optional pam_exec.so /usr/local/sbin/create-local-homedir.sh + auth optional pam_gnome_keyring.so + auth include postlogin + +--- /etc/pam.d/sshd ++++ /etc/pam.d/sshd +@@ -15,3 +15,4 @@ + session optional pam_motd.so + session include password-auth + session include postlogin ++session optional pam_exec.so /usr/local/sbin/create-local-homedir.sh +```` + +<details> +<summary>A note on SELinux</summary> + +If you're using SELinux, you'll need a separate copy of the `create-local-homedir` script +for use with GDM, labeled with `xdm_unconfined_exec_t`: + +````bash +ln /usr/local/sbin/create-local-homedir{,-gdm}.sh +semanage fcontext -a -t xdm_unconfined_exec_t /usr/local/sbin/create-local-homedir-gdm.sh +restorecon -v /usr/local/sbin/create-local-homedir-gdm.sh +```` + +Be sure to modify `/etc/pam.d/gdm-password` appropriately. + +</details> + +### Set XDG Environment Variables + +We need to tell the user's applications to use the new local home directory +for storage. We have to do this early in the PAM stack for GDM, because `$XDG_DATA_HOME` +must be set before `gnome-keyring` gets executed. + +Edit your PAM files again, adding one more line: + +````diff +--- /etc/pam.d/gdm-password ++++ /etc/pam.d/gdm-password +@@ -1,6 +1,7 @@ + auth [success=done ignore=ignore default=bad] pam_selinux_permit.so + auth substack password-auth + auth optional pam_exec.so /usr/local/sbin/create-local-homedir.sh ++auth optional pam_env.so conffile=/etc/security/pam_env_xdg.conf + auth optional pam_gnome_keyring.so + auth include postlogin + +--- /etc/pam.d/sshd ++++ /etc/pam.d/sshd +@@ -16,3 +16,4 @@ + session include password-auth + session include postlogin + session optional pam_exec.so /usr/local/sbin/create-local-homedir.sh ++session optional pam_env.so conffile=/etc/security/pam_env_xdg.conf +```` + +Then, create the corresponding `pam_env.conf(5)` file: + +````default +# /etc/security/pam_env_xdg.conf + +XDG_DATA_HOME DEFAULT=/usr/local/home/@{PAM_USER}/.local/share +XDG_STATE_HOME DEFAULT=/usr/local/home/@{PAM_USER}/.local/state +XDG_CACHE_HOME DEFAULT=/usr/local/home/@{PAM_USER}/.cache +XDG_CONFIG_HOME DEFAULT=/usr/local/home/@{PAM_USER}/.config +```` + +### Hacks for Non-XDG-Compliant Apps + +Unfortunately, since a majority of open source developers follow the +[CADT model](https://www.jwz.org/doc/cadt.html), there are many apps that ignore the +XDG specification. Sometimes these apps have their own environment variables +for specifying their storage locations. Otherwise, symlinks can provide us with an escape +hatch. + +Create a script in `/etc/profile.d` for these workarounds. Scripts in this directory +are executed within the context of the user's session, so we can freely write inside +his NFS home directory using his UID (and kerberos ticket, if applicable). + +````bash +# /etc/profile.d/local-homedirs.sh + +if (( UID >= 1000 )); then + # Building code is *much* faster on the local disk. Modify as needed: + export PYTHONUSERBASE="/usr/local/home/${USER}/.local" # python + export npm_config_cache="/usr/local/home/${USER}/.npm" # nodejs + export CARGO_HOME="/usr/local/home/${USER}/.cargo" # rust + export GOPATH="/usr/local/home/${USER}/go" # golang + + # Firefox doesn't provide an environment variable for setting the default profile + # path, so we'll just symlink it to /usr/local/home. + mkdir -p "/usr/local/home/${USER}/.mozilla" + ln -sfn "/usr/local/home/${USER}/.mozilla" "${HOME}/.mozilla" + + # Flatpak hardcodes ~/.var, so symlink it to /opt/flatpak. + ln -sfn "/opt/flatpak/${USER}" "${HOME}/.var" +fi +```` + +If you use any Flatpak apps, each user will need his own local Flatpak directory. +The Flatpak runtime appears to shadow the entire `/usr` using mount namespaces, +so any `/usr/local/home` symlinks will disappear into the abyss. Luckily, `/opt` +appears to be undefiled. Modify your original script like so: + +````diff +--- /usr/local/sbin/create-local-homedir.sh ++++ /usr/local/sbin/create-local-homedir.sh +@@ -6,4 +6,5 @@ + + if (( PAM_UID >= 1000 )); then + install -o "${PAM_USER}" -g "${PAM_USER}" -m 0700 -d "/usr/local/home/${PAM_USER}" ++ install -o "${PAM_USER}" -g "${PAM_USER}" -m 0700 -d "/opt/flatpak/${PAM_USER}" + fi +```` + +## Closing Thoughts + +Most of my users are nontechnical, so I'm pleased that these workarounds do not require +any manual intervention on their part. + +I am sad that `$XDG_CONFIG_HOME` can't be shared between multiple workstations reliably. +When I change my desktop background or add a new password to `gnome-keyring`, it only +affects the local machine. + +Initially, I tried symlinking various subdirectories of `~/.config` to the local disk +individually as I encountered different bugs (e.g. `~/.config/pulse`). Unfortunately this +proved brittle, as I was constantly playing whack-a-mole with apps that abused `$XDG_CONFIG_HOME` +for storing local state. In the end, it was less of a headache to just dump the whole thing +onto the local disk. + +I suppose if you verified an app behaved properly with multiple simultaneous NFS clients, +you could always symlink `/usr/local/home/$USER/.config/$APP` **back** onto NFS! diff --git a/src/blog/index.md b/src/blog/index.md new file mode 100644 index 0000000..1c35f0a --- /dev/null +++ b/src/blog/index.md @@ -0,0 +1,9 @@ +--- +title: sacredheartsc blog +header: Blog +description: Carolina-grown articles about self-hosting, privacy, unix, and more. +--- + +::: bloglist +__BLOG_LIST__ +::: diff --git a/src/blog/makefile-based-blogging/index.md b/src/blog/makefile-based-blogging/index.md new file mode 100644 index 0000000..9244a0d --- /dev/null +++ b/src/blog/makefile-based-blogging/index.md @@ -0,0 +1,249 @@ +--- +title: Makefile-Based Blogging +date: December 12, 2022 +subtitle: Yet another static site generator using `pandoc(1)` and `make(1)`. +description: Building a markdown-based static site generator using pandoc and make. +--- + +A few days ago, I got the gumption to start blogging again. The last time I wrote +with any frequency, I lovingly hand-crafted each HTML file before `rsync`ing it to +my web server. This time, I wanted a more efficient workflow. + +I surveyed the [vast number](https://github.com/myles/awesome-static-generators) +of static site generators available on GitHub, but most of them seemed like +overkill for my humble website. I figured that by the time I wrapped by head +around one of them, I could have just written a Makefile. + +Finally, I came across [pandoc-blog](https://github.com/lukasschwab/pandoc-blog), +which gave me inspiration and showed me the ideal pandoc incantations for +generating HTML from markdown files. And thus, my +[Makefile-based static site generator](https://git.sacredheartsc.com/www/about/) +was born. You're reading the inaugural post! + +## Generating the HTML + +The workhorse of this thing is [pandoc](https://pandoc.org), which is a ubiquitous +open-source document converter. Transforming markdown into HTML is as simple as: + +```bash +pandoc document.md -o document.html +``` + +Simple! But to generate an entire website, we'll need some of pandoc's additional +features: custom templates and document metadata. + +### Custom Templates + +The layout of pandoc's output document is determined by the +[template](https://pandoc.org/MANUAL.html#templates) in use. Pandoc includes +default templates for a variety of document formats, but you can also specify +your own. + +A very simple HTML template might look something like this: + +```html +<html lang="en"> + <head> + <meta name="author" content="$author-meta$"> + <meta name="description" content="$description$"> + </head> + <body> + <h1 class="title">$title$</h1> +$body$ + </body> +</html> +``` + +[My pandoc template](https://git.sacredheartsc.com/www/tree/templates/default.html) +is what generates the navigation bar at the top of this page. + +The variable `$body$` is replaced by the content of your markdown document when +pandoc renders the template. The other variables are replaced by their +corresponding values from the document's metadata. + +### Document Metadata + +Each pandoc source document can have associated metadata values. There are three +ways of specifying metadata: the `--medatata` [flag](https://pandoc.org/MANUAL.html#option--metadata), +a dedicated [metadata file](https://pandoc.org/MANUAL.html#option--metadata-file), or +a [YAML metadata block](https://pandoc.org/MANUAL.html#extension-yaml_metadata_block) +embedded within the document itself. We'll be using the embedded metadata blocks. + +Each markdown document for my website starts with a YAML metadata block. The +metadata for the post you're +[currently reading](https://git.sacredheartsc.com/www/tree/src/blog/makefile-based-blogging/index.md) +looks like this: + + +```yaml +--- +title: Makefile-Based Blogging +date: December 12, 2022 +subtitle: Yet another static site generator using `pandoc(1)` and `make(1)`. +description: Building a markdown-based static site generator using pandoc and make. +--- +``` + +You can put whatever YAML you like in your markdown files, as long as the metadata +starts and ends with three hyphens. + +## Automating pandoc with make + +Using a Makefile, we can automatically invoke pandoc to convert each markdown +file in our blog to HTML. In addition, `make` will keep track of which source +files have changed since the last run and rebuild them accordingly. + +First, lets describe the project layout: + +- **src/**: the source files of our blog, including markdown files and static + assets (CSS, images, etc). The subdirectory structure is entirely up to you. + +- **public/**: the output directory. After running `make`, the contents of this + directory can be `rsync`'d straight to your web server. + +- **scripts/**: helper scripts for generating the blog artifacts. Currently there + are only two: + + - [bloglist.py](https://git.sacredheartsc.com/www/tree/scripts/bloglist.py) + generates a markdown-formatted list of all your blog posts, sorted by the + `date` field in the YAML metadata block. + + - [rss.py](https://git.sacredheartsc.com/www/tree/scripts/rss.py) generates + an RSS feed for your blog. + +- **templates/**: pandoc templates which generate HTML from markdown files + (currently, there is only one). + +The Makefile used to build this website is located [here](https://git.sacredheartsc.com/www/tree/Makefile). +I've reproduced a simplified version below, to make it easier to step through. + +```makefile +###################### +# Variable definitions +###################### + +# These variables are used to generate the RSS feed +URL = https://www.sacredheartsc.com +FEED_TITLE = sacredheartsc blog +FEED_DESCRIPTION = Carolina-grown articles about self-hosting, privacy, unix, and more. + +# The number of blog posts to show on the homepage +BLOG_LIST_LIMIT = 5 + +# File extensions (other than .md) that should be included in public/ directory +STATIC_REGEX = .*\.(html|css|jpg|jpeg|png|xml|txt) + +# Pandoc template used to generate HTML +TEMPLATE = templates/default.html + +# List of subdirectories to create +SOURCE_DIRS := $(shell find src -mindepth 1 -type d) + +# List of source markdown files +SOURCE_MARKDOWN := $(shell find src -type f -name '*.md' -and ! -name .bloglist.md) + +# List of static assets +SOURCE_STATIC := $(shell find src \ + -type f \ + -regextype posix-extended \ + -iregex '$(STATIC_REGEX)') + +# List of all blog posts (excluding the main blog page) +BLOG_POSTS := $(shell find src/blog \ + -type f \ + -name '*.md' \ + -and ! -name .bloglist.md \ + -and ! -path src/blog/index.md) + +# Subdirectories to create under public/ +OUTPUT_DIRS := $(patsubst src/%, public/%, $(SOURCE_DIRS)) + +# .html files under public/, corresponding to each .md file under src/ +OUTPUT_MARKDOWN := $(patsubst src/%, public/%, $(patsubst %.md, %.html, $(SOURCE_MARKDOWN))) + +# Static file targets under public/ +OUTPUT_STATIC := $(patsubst src/%, public/%, $(SOURCE_STATIC)) + +# Script to generate RSS feed +RSSGEN = scripts/rss.py \ + src/blog \ + --title="$(FEED_TITLE)" \ + --description="$(FEED_DESCRIPTION)" \ + --url=$(URL) \ + --blog-path=/blog \ + --feed-path=/blog/rss/feed.xml + + +###################### +# File Targets +###################### + +# Default target: convert .md to .html, copy static assets, and generate RSS +public: \ + $(OUTPUT_DIRS) \ + $(OUTPUT_MARKDOWN) \ + $(OUTPUT_STATIC) \ + public/blog/feed.xml + +# Homepage (/) +public/index.html: src/index.md src/.bloglist.md $(TEMPLATE) + sed $$'/__BLOG_LIST__/{r src/.bloglist.md\nd}' $< \ + | pandoc --template=$(TEMPLATE) --output=$@ + +# Markdown list of 5 most recent blog posts +src/.bloglist.md: $(BLOG_POSTS) scripts/bloglist.py + scripts/bloglist.py src/blog $(BLOG_LIST_LIMIT) > $@ + +# The main blog listing (/blog/) +public/blog/index.html: src/blog/index.md src/blog/.bloglist.md $(TEMPLATE) + sed $$'/__BLOG_LIST__/{r src/blog/.bloglist.md\nd}' $< \ + | pandoc --template=$(TEMPLATE) --output=$@ + +# Markdown list of _all_ blog posts +src/blog/.bloglist.md: $(BLOG_POSTS) scripts/bloglist.py + scripts/bloglist.py src/blog > $@ + +# Convert all other .md files to .html +public/%.html: src/%.md $(TEMPLATE) + pandoc --template=$(TEMPLATE) --output=$@ $< + +# Catch-all: copy static assets in src/ to public/ +public/%: src/% + cp --preserve=timestamps $< $@ + +# RSS feed +public/blog/feed.xml: $(BLOG_POSTS) scripts/rss.py + $(RSSGEN) > $@ + + +###################### +# Phony Targets +###################### + +.PHONY: serve rsync clean + +# Run a local HTTP server in the output directory +serve: public + cd public && python3 -m http.server + +# Deploy the site to your webserver +rsync: public + rsync -rlphv --delete public/ webserver.example.com:/var/www/html + +clean: + rm -rf public + rm -f src/.bloglist.md + rm -f src/blog/.bloglist.md +``` + +## Closing Thoughts + +I admit, there is a small amount of hackery involved. You obviously can't generate +a time-sorted list of blog posts using pure markdown, so I'm generating the +markdown list using a Python script in an intermediate step. I then (ab)use `sed` +to shove that list into the markdown source on the fly. This means that changing +the look of the [blog list](/blog/) requires hacking up the Python code. + +But overall, I've been quite happy with this little project. There's just something +about writing paragraphs in `vi` and typing `:!make` that warms my soul with +memories of simpler times. diff --git a/src/browserconfig.xml b/src/browserconfig.xml new file mode 100644 index 0000000..b3930d0 --- /dev/null +++ b/src/browserconfig.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<browserconfig> + <msapplication> + <tile> + <square150x150logo src="/mstile-150x150.png"/> + <TileColor>#da532c</TileColor> + </tile> + </msapplication> +</browserconfig> diff --git a/src/css/style.css b/src/css/style.css new file mode 100644 index 0000000..27d6a7f --- /dev/null +++ b/src/css/style.css @@ -0,0 +1,89 @@ +body { + color: #333; + margin: 1em auto 2em auto; + max-width: 43em; + padding: 0 1em; + font-family: "PT Sans", "Myriad Pro", "Trebuchet MS", Helvetica, sans-serif; +} + +a { + color: #0074D9 +} + +a:visited { + color: #941352 +} + +header .subtitle { + margin-top: -1em; + margin-bottom: 21.44px; +} + +.bloglist p:nth-of-type(2) { + margin-top: -8px; +} + +header .date { + font-style: italic; + margin-bottom: 21.44px; +} + +footer .date { + float: right; +} + +footer { + font-style: italic; + font-size: 14px; + margin-top: 32px; +} + +.logo { + float: right; +} + +@media only screen and (max-device-width : 667px) { + .logo { + max-width: 66px; + } + + footer .date { + float: none; + } +} + +@media print { + .navbar { + display: none !important; + } +} + +pre { + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; + font-family: monospace; + line-height: 1.2; + padding: 8px; + white-space: pre; + word-wrap: normal; +} + +.sourceCode { + overflow: auto; +} + +details { + font-size: 87.5%; + margin: 0 1em; +} + +.redacted { + background: black; + color: black; + cursor: default; +} + +.right { + float: right; +} diff --git a/src/cv/index.md b/src/cv/index.md new file mode 100644 index 0000000..76f7f92 --- /dev/null +++ b/src/cv/index.md @@ -0,0 +1,102 @@ +--- +title: Curriculum Vitae +description: The CV of stonewall, a Linux engineer in upstate South Carolina. +date: January 20, 2023 +--- + +[stonewall@sacredheartsc.com](mailto:stonewall@sacredheartsc.com) +[sacredheartsc.com](https://www.sacredheartsc.com){.right} + +Site reliability engineer in upstate South Carolina, specializing in the programming, +administration, and troubleshooting of Linux systems. 8+ years of development and +operations experience. Strong skills in Unix internals, shell scripting, system +administration, and debugging, from userspace to the kernel. Extensive experience +in Linux, BSD, and Solaris-based operating systems, as well as networking and +infrastructure management. + + +## Work Experience + +**Linux Engineer** at [REDACTED, Inc.]{.redacted} (Remote) +[2021-present]{.right} + + - Details forthcoming. + +**Linux Engineer** at **[Jane Street](https://www.janestreet.com/)** (New York, NY) +[2018-2021]{.right} + + - Investigated production errors and performance regressions using packet captures, + userspace profiling, and kernel instrumentation. + - Maintained Linux infrastructure and developed management tools for network + storage systems. + - Diagnosed performance bottlenecks in NFS application workflows and client/server + implementations. + - Designed and implemented automated systems for the deployment, configuration + management, and monitoring of Linux hosts. + - Wrote patches for bug fixes and performance improvements for various open source + applications in the GNU/Linux ecosystem. + +**Site Reliability Engineer** at **Thesys CAT** (Charleston, SC) +[2017-2018]{.right} + + - Designed infrastructure, networking, and operations strategies for the + implementation of the SEC’s Consolidated Audit Trail. + - Led infrastructure rollout and configuration management effort using Ansible + for hundreds of bare metal and cloud servers. + - Implemented Kerberos-based authentication system for both applications and user + accounts. + - Built an object storage solution using FreeBSD and ZFS nearing a petabyte of + usable space. + - Created automated build and packaging pipeline for in-house software repositories. + +**DevOps Engineer** at **Thesys Technologies** (Charleston, SC) +[2016-2017]{.right} + + - Implemented market access software in C for Tilera’s TILE64 architecture, supporting + over $1 billion per day of order volume. + - Improved the accuracy of distributed C-based client risk checks while maintaining + ≤2.5μs order latency. + - Responsible for implementation and monitoring of a C++-based software trading + platform as part of trading technologies DevOps team. + - Coordinated network connectivity and colocation with clients and stock exchanges + in North America. + - Assessed and resolved real-time trading and connectivity issues in a high-stress + environment while managing relationships with customers. + +**Software Engineer** at **SPARC** (Charleston, SC) +[2015-2016]{.right} + + - Developed benefit management software for the Department of Veterans Affairs + using Java, Spring Framework, and AngularJS. + - Led a special performance team which identified and mitigated application + bottlenecks. + - Achieved a >10x speedup by refactoring application-layer JPA logic into more + efficient database queries. + - Created and maintained CentOS VM images for Oracle Database. + +**Graduate Research Assistant** at **[Clemson University](https://www.clemson.edu/)** (Clemson, SC) +[2013-2015]{.right} + + - Designed and implemented a middleware system for watershed-scale sensor networks + using Java and NodeJS. + - Maintained a middleware backend which served over 50 live ecological sensor + deployment sites across South Carolina. + - Independently developed a metadata management system for sensor network hardware, + which was presented at IEEE and ACM conferences. + + +## Education + +**M.S. in Computer Science** at **[Clemson University](https://www.clemson.edu/)** (Clemson, SC) +[2013-2015]{.right} + + - Developed new data analytics and management tools for wireless sensor networks + in Clemson’s Dependable Systems Research Group. + - GPA 3.70 + +**B.S. in Computer Science** at **[Clemson University](https://www.clemson.edu/)** (Clemson, SC) +[2009-2013]{.right} + + - Graduated magna cum laude as an undergraduate researcher in the Calhoun + Honors College. + - GPA 3.82 diff --git a/src/favicon-16x16.png b/src/favicon-16x16.png Binary files differnew file mode 100644 index 0000000..e64368b --- /dev/null +++ b/src/favicon-16x16.png diff --git a/src/favicon-32x32.png b/src/favicon-32x32.png Binary files differnew file mode 100644 index 0000000..caa141a --- /dev/null +++ b/src/favicon-32x32.png diff --git a/src/favicon.ico b/src/favicon.ico Binary files differnew file mode 100644 index 0000000..4536c08 --- /dev/null +++ b/src/favicon.ico diff --git a/src/gpg.asc b/src/gpg.asc new file mode 100644 index 0000000..1a3ad50 --- /dev/null +++ b/src/gpg.asc @@ -0,0 +1,41 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=MkZr +-----END PGP PUBLIC KEY BLOCK----- diff --git a/src/index.md b/src/index.md new file mode 100644 index 0000000..3ec5b6b --- /dev/null +++ b/src/index.md @@ -0,0 +1,35 @@ +--- +title: sacredheartsc.com +heading: Cor Jesu Sacratissimum, miserere nobis +subtitle: Most Sacred Heart of Jesus, have mercy on us! +description: The digital home of a Southern sysadmin. +--- + +![](sacredheart.png "Sacred Heart"){.logo} + +Welcome to my personal website. I'm a husband, father of three, and Unix herder. +The HTML you're currently viewing was artisanally crafted in South Carolina. + +Professionally, I'm something between a programmer and a system administrator. +Nothing here represents the views of any employer. It's just a collection of my +personal projects and interests, which include digital privacy, self-hosting, +and old-school sysadminning. + +- Contact: + [Email](mailto:stonewall@sacredheartsc.com){title="stonewall@sacredheartsc.com"} | + [XMPP](xmpp:stonewall@sacredheartsc.com?message){title="stonewall@sacredheartsc.com"} | + [IRC](ircs://irc.libera.chat/stonewall,isnick){title="stonewall on irc.libera.chat"} +- GPG: [0x6DF66CCB](/gpg.asc) +- Code: + [Git](https://git.sacredheartsc.com/) | + [GitHub](https://github.com/sacredheartsc) +- [Blog](/blog/) +- [Curriculum Vitae](/cv/) + +This website is powered by a [Makefile](https://git.sacredheartsc.com/www/about/)! + +## Recent Posts + +::: bloglist +__BLOG_LIST__ +::: diff --git a/src/mstile-150x150.png b/src/mstile-150x150.png Binary files differnew file mode 100644 index 0000000..8d29538 --- /dev/null +++ b/src/mstile-150x150.png diff --git a/src/sacredheart.png b/src/sacredheart.png Binary files differnew file mode 100644 index 0000000..e9eedf0 --- /dev/null +++ b/src/sacredheart.png diff --git a/src/safari-pinned-tab.svg b/src/safari-pinned-tab.svg new file mode 100644 index 0000000..0580306 --- /dev/null +++ b/src/safari-pinned-tab.svg @@ -0,0 +1,89 @@ +<?xml version="1.0" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" + "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd"> +<svg version="1.0" xmlns="http://www.w3.org/2000/svg" + width="202.000000pt" height="202.000000pt" viewBox="0 0 202.000000 202.000000" + preserveAspectRatio="xMidYMid meet"> +<metadata> +Created by potrace 1.14, written by Peter Selinger 2001-2017 +</metadata> +<g transform="translate(0.000000,202.000000) scale(0.100000,-0.100000)" +fill="#000000" stroke="none"> +<path d="M376 1935 c-4 -20 -10 -33 -14 -31 -5 3 -14 1 -22 -5 -13 -7 -12 -9 +3 -9 9 0 17 -6 17 -12 0 -9 4 -8 10 2 5 8 10 11 10 6 0 -5 11 1 25 14 29 28 +30 33 9 54 -23 24 -31 20 -38 -19z"/> +<path d="M454 1895 c-7 -18 2 -45 15 -45 13 0 22 27 15 45 -4 8 -10 15 -15 15 +-5 0 -12 -7 -15 -15z"/> +<path d="M526 1884 c-9 -8 -16 -19 -16 -24 0 -5 -3 -16 -6 -24 -5 -12 -2 -15 +10 -10 9 3 23 1 31 -6 12 -10 15 -10 15 0 0 7 6 10 14 7 8 -3 21 2 30 12 18 +20 20 31 6 31 -5 0 -10 -6 -10 -13 0 -7 -13 -13 -30 -13 -27 -2 -29 0 -23 27 +6 33 1 36 -21 13z"/> +<path d="M1025 1860 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0 +-8 -4 -11 -10z"/> +<path d="M1536 1855 c4 -8 11 -15 16 -15 6 0 5 6 -2 15 -7 8 -14 15 -16 15 -2 +0 -1 -7 2 -15z"/> +<path d="M1010 1830 c0 -5 5 -10 10 -10 6 0 10 5 10 10 0 6 -4 10 -10 10 -5 0 +-10 -4 -10 -10z"/> +<path d="M480 1785 c-10 -12 -10 -15 4 -15 9 0 16 7 16 15 0 8 -2 15 -4 15 -2 +0 -9 -7 -16 -15z"/> +<path d="M637 1783 c-4 -3 -7 -18 -6 -32 1 -26 1 -25 10 3 9 31 7 41 -4 29z"/> +<path d="M300 1771 c0 -5 5 -13 10 -16 6 -3 10 -2 10 4 0 5 -4 13 -10 16 -5 3 +-10 2 -10 -4z"/> +<path d="M1016 1770 c-42 -8 -58 -27 -63 -75 -4 -37 -9 -77 -12 -90 -1 -5 -6 +-3 -11 5 -5 8 -10 10 -10 4 0 -6 -10 -9 -23 -7 -13 3 -40 -2 -60 -11 -30 -12 +-37 -21 -38 -43 -1 -15 0 -32 1 -38 2 -5 4 -18 5 -28 3 -29 21 -39 82 -44 32 +-3 60 -7 62 -9 8 -8 13 -48 14 -129 2 -91 -8 -129 -32 -119 -61 26 -115 15 +-179 -37 l-49 -40 -13 -90 c-12 -82 -11 -93 5 -132 10 -23 27 -51 37 -63 10 +-11 18 -28 18 -37 0 -10 11 -27 25 -39 14 -12 25 -27 25 -33 0 -7 20 -31 45 +-54 25 -23 45 -45 45 -50 0 -4 44 -54 99 -111 l99 -104 -28 -26 c-16 -15 -34 +-45 -41 -69 -11 -38 -15 -42 -43 -41 -53 3 -59 1 -53 -20 7 -26 -1 -25 -24 2 +-19 21 -19 22 1 43 41 44 16 73 -31 36 -62 -48 -92 -30 -44 27 14 17 25 33 25 +36 0 10 -29 7 -49 -6 -15 -9 -25 -9 -43 -1 l-23 12 27 0 c42 1 58 11 58 36 0 +27 -20 37 -61 28 -28 -5 -30 -4 -23 21 5 21 3 26 -13 26 -27 0 -93 -58 -93 +-81 0 -11 4 -18 9 -14 10 5 51 -32 51 -46 0 -3 14 -12 31 -19 22 -9 29 -18 26 +-31 -6 -21 31 -49 62 -49 11 0 22 -9 26 -19 3 -10 20 -24 38 -31 18 -6 45 -18 +60 -27 15 -8 47 -16 70 -17 23 -2 47 -4 52 -6 25 -6 56 1 52 13 -2 6 -10 12 +-18 12 -33 1 -41 16 -35 68 10 69 22 90 45 77 23 -12 31 -4 31 28 0 17 4 23 +15 18 9 -3 37 11 68 34 28 22 55 40 60 40 4 0 7 6 7 13 0 8 9 22 20 32 11 10 +18 20 16 23 -3 2 12 32 33 65 39 65 81 145 98 187 29 72 49 195 33 205 -6 3 +-7 17 -4 30 9 38 -24 116 -68 157 -31 30 -50 38 -96 45 -65 9 -101 6 -119 -9 +-25 -20 -37 -2 -39 63 -7 177 -7 179 30 179 34 1 104 15 113 24 10 10 -8 96 +-24 112 -12 11 -27 14 -52 10 -20 -3 -46 -3 -59 1 -22 5 -23 10 -22 77 2 96 3 +91 -25 104 -28 13 -41 13 -99 2z"/> +<path d="M520 1761 c0 -5 7 -11 15 -15 8 -3 15 -1 15 3 0 5 -7 11 -15 15 -8 3 +-15 1 -15 -3z"/> +<path d="M460 1740 c0 -5 5 -10 11 -10 5 0 7 5 4 10 -3 6 -8 10 -11 10 -2 0 +-4 -4 -4 -10z"/> +<path d="M440 1704 c0 -8 5 -12 10 -9 6 4 8 11 5 16 -9 14 -15 11 -15 -7z"/> +<path d="M688 1693 c7 -3 16 -2 19 1 4 3 -2 6 -13 5 -11 0 -14 -3 -6 -6z"/> +<path d="M635 1420 c-3 -5 -1 -10 4 -10 6 0 11 5 11 10 0 6 -2 10 -4 10 -3 0 +-8 -4 -11 -10z"/> +<path d="M1592 743 c3 -26 1 -28 -29 -25 -27 3 -33 0 -33 -15 0 -25 61 -48 88 +-34 40 21 33 101 -9 101 -16 0 -19 -6 -17 -27z"/> +<path d="M1460 750 c0 -5 9 -10 20 -10 11 0 20 5 20 10 0 6 -9 10 -20 10 -11 +0 -20 -4 -20 -10z"/> +<path d="M592 688 c-17 -17 -15 -31 8 -51 11 -9 20 -22 20 -29 0 -10 -3 -9 -9 +1 -7 11 -13 8 -26 -13 l-18 -27 -26 31 c-24 29 -28 31 -53 20 -37 -17 -43 -48 +-12 -72 13 -11 21 -24 18 -30 -4 -6 1 -8 15 -3 18 5 21 2 21 -20 0 -19 4 -26 +14 -22 8 3 20 0 26 -8 6 -7 17 -12 25 -11 24 3 95 83 95 106 0 11 7 23 16 27 +8 3 13 12 10 19 -3 8 -1 14 4 14 6 0 10 7 10 15 0 9 -9 15 -25 15 -24 0 -51 +31 -38 44 3 3 -9 6 -28 6 -19 0 -40 -5 -47 -12z m29 -147 c-7 -5 -11 -13 -7 +-19 3 -5 -1 -12 -9 -16 -9 -3 -19 3 -25 14 -8 15 -7 18 3 14 8 -3 20 2 27 11 +7 8 15 13 19 10 3 -3 -1 -10 -8 -14z"/> +<path d="M1493 641 c-13 -11 -23 -25 -23 -31 0 -19 40 -70 55 -70 16 0 25 -22 +25 -61 0 -27 -3 -29 -38 -29 -31 0 -45 7 -66 31 -25 27 -32 30 -64 24 -87 -16 +-120 -109 -51 -146 34 -17 47 4 26 40 -12 21 -14 32 -6 36 13 9 104 -83 111 +-112 9 -35 -14 -68 -47 -68 -32 0 -43 -16 -21 -34 18 -15 59 -5 81 19 23 25 +25 113 2 137 -24 27 -12 34 43 27 46 -6 47 -5 61 27 7 19 13 52 12 74 0 24 5 +44 14 50 32 24 -31 106 -80 105 -7 -1 -22 -9 -34 -19z"/> +<path d="M777 543 c-4 -3 -7 -15 -7 -25 0 -13 7 -18 25 -18 26 0 29 7 19 34 +-6 16 -26 21 -37 9z"/> +<path d="M1133 283 c-7 -2 -13 -11 -13 -19 0 -8 -5 -14 -10 -14 -6 0 -9 -12 +-6 -27 2 -16 5 -31 5 -35 1 -4 18 -19 38 -33 43 -29 63 -20 38 18 -16 24 -16 +28 1 56 15 25 15 32 5 45 -13 16 -34 19 -58 9z"/> +<path d="M430 160 c-9 -6 -10 -10 -3 -10 6 0 15 5 18 10 8 12 4 12 -15 0z"/> +<path d="M360 116 c0 -19 4 -23 20 -19 23 6 25 14 8 31 -18 18 -28 14 -28 -12z"/> +<path d="M418 128 c-7 -19 2 -38 18 -38 15 0 17 9 8 34 -7 19 -20 21 -26 4z"/> +<path d="M286 94 c-3 -9 -2 -23 3 -32 10 -18 11 -16 27 36 5 18 -23 15 -30 -4z"/> +</g> +</svg> diff --git a/src/site.webmanifest b/src/site.webmanifest new file mode 100644 index 0000000..c9761d6 --- /dev/null +++ b/src/site.webmanifest @@ -0,0 +1,14 @@ +{ + "name": "SacredHeartSC", + "short_name": "SacredHeartSC", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + } + ], + "theme_color": "#ff0000", + "background_color": "#ff0000", + "display": "standalone" +} diff --git a/templates/cv.html b/templates/cv.html new file mode 100644 index 0000000..d61dd33 --- /dev/null +++ b/templates/cv.html @@ -0,0 +1,93 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> +$for(author-meta)$ + <meta name="author" content="$author-meta$"> +$endfor$ +$if(date-meta)$ + <meta name="dcterms.date" content="$date-meta$"> +$endif$ +$if(description)$ + <meta name="description" content="$description$"> +$endif$ +$if(keywords)$ + <meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$"> +$endif$ + <title>$if(title-prefix)$$title-prefix$ – $endif$$title$</title> + <link rel="alternate" type="application/rss+xml" href="$feed$"> + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="manifest" href="/site.webmanifest"> + <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#eea35f"> + <meta name="msapplication-TileColor" content="#da532c"> + <meta name="theme-color" content="#eea35f"> + <style> + code{white-space: pre-wrap;} + span.smallcaps{font-variant: small-caps;} + span.underline{text-decoration: underline;} + div.column{display: inline-block; vertical-align: top; width: 50%;} +$if(quotes)$ + q { quotes: "“" "”" "‘" "’"; } +$endif$ + </style> +$if(highlighting-css)$ + <style> +$highlighting-css$ + </style> +$endif$ +$for(css)$ + <link rel="stylesheet" href="$css$"> +$endfor$ +$for(header-includes)$ + $header-includes$ +$endfor$ +</head> +<body> +$for(include-before)$ +$include-before$ +$endfor$ + +<nav class="navbar"> + [<a href="/">home</a>] + [<a href="/blog/">blog</a>] + [<a href="https://git.sacredheartsc.com/">git</a>] + [<a href="mailto:stonewall@sacredheartsc.com">email</a>] + [<a href="$feed$">rss</a>] + <span style="float: right">JMJ</span> +<hr> +</nav> + +<header> +$if(heading)$ + <h1 class="title">$heading$</h1> +$else$ + <h1 class="title">$title$</h1> +$endif$ +$if(subtitle)$ + <p class="subtitle">$subtitle$</p> +$endif$ +</header> + +$if(toc)$ +<nav id="$idprefix$toc" role="doc-toc"> +$table-of-contents$ +</nav> +$endif$ + +$body$ + +$for(include-after)$ +$include-after$ +$endfor$ +<footer> + <p> + Please <a href="mailto:stonewall@sacredheartsc.com">contact me</a> if you + require a de-anonymized CV. + <span class="date">Last updated $date$</span> + </p> +</footer> +</body> +</html> diff --git a/templates/default.html b/templates/default.html new file mode 100644 index 0000000..4145c9b --- /dev/null +++ b/templates/default.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> +$for(author-meta)$ + <meta name="author" content="$author-meta$"> +$endfor$ +$if(date-meta)$ + <meta name="dcterms.date" content="$date-meta$"> +$endif$ +$if(description)$ + <meta name="description" content="$description$"> +$endif$ +$if(keywords)$ + <meta name="keywords" content="$for(keywords)$$keywords$$sep$, $endfor$"> +$endif$ + <title>$if(title-prefix)$$title-prefix$ – $endif$$title$</title> + <link rel="alternate" type="application/rss+xml" href="$feed$"> + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="manifest" href="/site.webmanifest"> + <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#eea35f"> + <meta name="msapplication-TileColor" content="#da532c"> + <meta name="theme-color" content="#eea35f"> + <style> + code{white-space: pre-wrap;} + span.smallcaps{font-variant: small-caps;} + span.underline{text-decoration: underline;} + div.column{display: inline-block; vertical-align: top; width: 50%;} +$if(quotes)$ + q { quotes: "“" "”" "‘" "’"; } +$endif$ + </style> +$if(highlighting-css)$ + <style> +$highlighting-css$ + </style> +$endif$ +$for(css)$ + <link rel="stylesheet" href="$css$"> +$endfor$ +$for(header-includes)$ + $header-includes$ +$endfor$ +</head> +<body> +$for(include-before)$ +$include-before$ +$endfor$ + +<nav class="navbar"> + [<a href="/">home</a>] + [<a href="/blog/">blog</a>] + [<a href="https://git.sacredheartsc.com/">git</a>] + [<a href="mailto:stonewall@sacredheartsc.com">email</a>] + [<a href="$feed$">rss</a>] + <span style="float: right">JMJ</span> +<hr> +</nav> + +<header> +$if(heading)$ + <h1 class="title">$heading$</h1> +$else$ + <h1 class="title">$title$</h1> +$endif$ +$if(subtitle)$ + <p class="subtitle">$subtitle$</p> +$endif$ +$if(date)$ +<p class="date">$date$</p> +$endif$ +</header> + +$if(toc)$ +<nav id="$idprefix$toc" role="doc-toc"> +$table-of-contents$ +</nav> +$endif$ + +$body$ + +$for(include-after)$ +$include-after$ +$endfor$ +</body> +</html> |