summaryrefslogtreecommitdiff
path: root/http
diff options
context:
space:
mode:
Diffstat (limited to 'http')
-rw-r--r--http/.gitignore9
-rw-r--r--http/Makefile150
-rwxr-xr-xhttp/bin/blog110
-rwxr-xr-xhttp/bin/cut_longlog_meta34
-rwxr-xr-xhttp/bin/extract_longlog_meta34
-rwxr-xr-xhttp/bin/gmi2upphtml184
-rwxr-xr-xhttp/bin/lmtbin953744 -> 0 bytes
-rwxr-xr-xhttp/bin/mkws72
l---------http/blogs/blog1
l---------http/blogs/microlog1
-rw-r--r--http/composer.json17
-rw-r--r--http/composer.lock101
-rw-r--r--http/config.mk7
-rw-r--r--http/php/config.php62
-rw-r--r--http/php/db.php205
-rw-r--r--http/php/mail.php35
-rw-r--r--http/pp/.gitignore2
-rw-r--r--http/pp/LICENSE15
-rw-r--r--http/pp/Makefile29
-rw-r--r--http/pp/README.md188
-rw-r--r--http/pp/pp.146
-rw-r--r--http/pp/pp.c369
-rw-r--r--http/public/do/send_email.php124
-rw-r--r--http/public/do/send_otp.php104
-rw-r--r--http/public/do/verify_otp.php63
l---------http/public/images1
-rw-r--r--http/public/scripts/contact/actions.js60
-rw-r--r--http/public/scripts/contact/post-error-message.js34
-rw-r--r--http/public/scripts/contact/resend-otp.js31
-rw-r--r--http/public/scripts/contact/reset-resend-button.js32
-rw-r--r--http/public/scripts/contact/selectors.js25
-rw-r--r--http/public/scripts/contact/submit-contact-form.js53
-rw-r--r--http/public/scripts/contact/submit-otp-form.js77
-rw-r--r--http/public/scripts/otp-form-wc.js68
-rw-r--r--http/share/components/formatted_date.upphtml4
-rw-r--r--http/share/components/head.upphtml37
-rw-r--r--http/share/components/meta.upphtml11
-rw-r--r--http/share/components/navbar.upphtml16
-rw-r--r--http/share/components/otp-dialog.upphtml28
-rw-r--r--http/share/layouts/blog.layout.upphtml66
-rw-r--r--http/share/layouts/default.layout.upphtml20
-rw-r--r--http/share/layouts/microlog.layout.upphtml51
-rw-r--r--http/share/layouts/trivial.layout.upphtml3
-rw-r--r--http/share/man/man1/lmt.155
-rw-r--r--http/share/man/man1/mkws.191
l---------http/share/man/man1/pp.11
-rw-r--r--http/src/blog.upphtml76
-rw-r--r--http/src/blog/rss.uppxml50
-rw-r--r--http/src/blog/subscribe.trivial.upphtml4
-rw-r--r--http/src/contact.upphtml96
-rw-r--r--http/src/css/base.uppcss515
-rw-r--r--http/src/css/feed.uppcss45
-rw-r--r--http/src/css/hcard.uppcss57
-rw-r--r--http/src/css/otp.uppcss29
-rw-r--r--http/src/css/post.uppcss8
-rw-r--r--http/src/css/reset.uppcss93
-rw-r--r--http/src/error.upphtml14
-rw-r--r--http/src/index.upphtml47
-rw-r--r--http/src/links/rss.trivial.upphtml4
-rw-r--r--http/src/microlog.upphtml84
-rw-r--r--http/src/microlog/rss.uppxml50
-rw-r--r--http/src/sitemap.uppxml21
-rw-r--r--http/src/subscribe.upphtml91
63 files changed, 0 insertions, 4010 deletions
diff --git a/http/.gitignore b/http/.gitignore
deleted file mode 100644
index ab75dc7..0000000
--- a/http/.gitignore
+++ /dev/null
@@ -1,9 +0,0 @@
-out
-vendor
-src/microlog/*.microlog.upphtml
-src/microlog/*.microlog.upphtml.args
-src/blog/*.blog.upphtml
-src/blog/*.blog.upphtml.args
-bin/pp
-share/man/man1/pp.1
-config.ini
diff --git a/http/Makefile b/http/Makefile
deleted file mode 100644
index 4c50da8..0000000
--- a/http/Makefile
+++ /dev/null
@@ -1,150 +0,0 @@
-include config.mk
-
-
-#############
-# VARIABLES #
-#############
-
-src_files := $(filter-out %.args,$(wildcard src/*.upp*)) $(filter-out %.args,$(wildcard src/**/*.upp*))
-src_files_ext := $(suffix $(src_files))
-out_deps_ext := $(src_files_ext:.upp%=%)
-
-component_files := $(shell find -L share/components -type f)
-layout_files := $(wildcard share/layouts/*.layout.upp*)
-layouts := $(notdir $(basename $(basename $(layout_files))))
-
-remove_all_layouts = $(call remove_layouts,$(layouts),$(1))
-remove_layouts = $(let first rest,$(1),$(call remove_layout,$(first),$(if $(rest),$(call remove_layouts,$(rest),$(2)),$(2))))
-remove_layout = $(subst .$(1).upp,.,$(2))
-out_deps := $(subst .upp,.,$(call remove_all_layouts,$(addprefix out/,$(src_files:src/%=%))))
-out_deps_without_src := $(filter-out $(out_deps),$(shell find -L out -type f))
-out_deps += $(out_deps_without_src)
-
-blogs := $(subst blogs/,,$(wildcard blogs/*))
-
-
-#############
-# FUNCTIONS #
-#############
-
-blog_post_target = src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml
-blog_post_src = blogs/$(blog)/%.$(BLOG_EXT_$(blog)) $$(wildcard bin/*)
-blog_src = $(patsubst \
- blogs/$(blog)/%.$(BLOG_EXT_$(blog)),\
- src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml,\
- $(shell find -L "blogs/$(blog)" -type f -and -name *.$(BLOG_EXT_$(blog))))
-blog_src_without_post_src = $(filter-out $(blog_src),$(shell find -L "src/$(blog)" -type f -and -name *.$(BLOG_LAYOUT_$(blog)).upphtml))
-default_layout_file = $(if \
- $(shell [ -f "share/layouts/default.layout.$(ext)" ] && echo 1),\
- share/layouts/default.layout.$(ext))
-layout_file = $(if $(layout),share/layouts/$(layout).layout.upp$(ext),$(default_layout_file))
-
-define out_rule =
-out/%.$(ext): src/%$(if $(layout),.$(layout)).upp$(ext) $(layout_file) $(component_files) bin/pp
- ./bin/mkws https://joeac.net "$$$$(realpath $$<)"
-endef
-
-define blog_rss_feed_rule =
-out/$(blog)/rss.xml: src/$(blog)/rss.uppxml $(blog_src) $(wildcard bin/*) bin/pp
- ./bin/mkws https://joeac.net "$$(realpath src/$(blog)/rss.uppxml)"
-endef
-
-define blog_rule =
-.PHONY: $(blog)
-$(blog): $(blog_src_without_post_src) $(blog_src) out/$(blog).html
-endef
-
-define blog_post_rule =
-$(blog_post_target): $(blog_post_src) $(let ext,html,$(layout_file) $(component_files))
- ./bin/blog \
- -i $(blog) \
- $(if $(BLOG_LAYOUT_$(blog)),-l $(BLOG_LAYOUT_$(blog))) \
- $(BLOG_FLAGS_$(blog)) \
- -f $$*.$(BLOG_EXT_$(blog))
-endef
-
-define blog_index_rule =
-out/$(blog).html: src/$(blog).upphtml $(blog_src) $(wildcard bin/*) bin/pp
- ./bin/mkws https://joeac.net "$$(realpath src/$(blog).upphtml)"
-endef
-
-define prune_blog_src_file_rule =
-$(let rel_basename,$(patsubst src/$(blog)/%.$(BLOG_LAYOUT_$(blog)).upphtml,%,$(file)),
-blogs/$(blog)/$(rel_basename).$(BLOG_EXT_$(blog)):
- rm $(file)
-)
-endef
-
-define prune_blog_out_file_rule =
-$(let rel_basename,$(patsubst src/$(blog)/%.$(BLOG_LAYOUT_$(blog)).upphtml,%,$(file)),
-$(file):
- rm out/$(rel_basename).html
-)
-endef
-
-define prune_out_file_rule =
-$(let orig_ext,$(patsubst .%,%,$(suffix $(file))),
-$(let ext,$(patsubst upp%,%,$(orig_ext)),
-$(let rel_basename,$(patsubst out/%.$(orig_ext),%,$(file)),
-$(let src_file,src/$(rel_basename).upp$(ext),
-$(let src_files_with_layouts,$(foreach layout,$(layouts),src/$(rel_basename).$(layout).upp$(ext)),
-$(let public_file,public/$(rel_basename).$(orig_ext),
-$(src_file) $(src_files_with_layouts) $(public_file):
- rm $(file)
-))))))
-endef
-
-
-#########
-# RULES #
-#########
-
-all: $(blogs) public out vendor
- $(MAKE) out # if blog posts have been pruned, outfiles must be re-calculated
-
-share/man/man1/pp.1:
- cp pp/pp.1 share/man/man1/pp.1
-
-.PHONY: out
-out: $(out_deps)
-
-bin/pp:
- $(MAKE) --directory=pp && chmod +x pp/pp && cp pp/pp bin/pp
-
-$(foreach ext,$(sort $(out_deps_ext)),\
- $(eval $(out_rule)))
-$(foreach layout,$(layouts),\
- $(foreach ext,$(sort $(out_deps_ext)),\
- $(eval $(out_rule))))
-
-$(foreach blog,$(blogs),$(eval $(blog_rule)))
-$(foreach blog,$(blogs),$(eval $(blog_index_rule)))
-
-$(foreach blog,$(blogs),\
- $(foreach layout,$(layouts),\
- $(eval $(call blog_post_rule,$(blog),$(layout)))))
-$(foreach blog,$(blogs),\
- $(eval $(call blog_post_rule,$(blog),)))
-
-$(foreach blog,$(blogs),$(eval $(blog_rss_feed_rule)))
-
-.PHONY: public
-public: $(subst public/,out/,$(shell find -L public/ -type f))
-
-out/%: public/%
- mkdir -p $(dir $@) && cp $< $@
-
-$(foreach file,$(out_deps_without_src),$(eval $(prune_out_file_rule)))
-$(foreach blog,$(blogs),$(foreach file,$(blog_src_without_post_src),$(eval $(prune_blog_src_file_rule))))
-
-vendor: composer.json composer.lock
- composer install
-
-.PHONY: clean
-clean:
- rm -rf \
- vendor \
- out \
- $(foreach blog,$(blogs), \
- $(foreach ext,upphtml upphtml.args,\
- src/$(blog)/*$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).$(ext)))
diff --git a/http/bin/blog b/http/bin/blog
deleted file mode 100755
index 46be82f..0000000
--- a/http/bin/blog
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/bin/sh -e
-export PATH="$(dirname $(realpath $0)):$PATH"
-export BLOGS="$(realpath blogs)"
-export SRC="$(realpath src)"
-export LAYOUTS="$(realpath share/layouts)"
-
-usage()
-{
- >&2 cat << USAGE
-usage: blog -i LOG [-p POSTPROCESS_CMD] [-a EXTRACT_ARGS_CMD] [-l LAYOUT] [-t] [-f FILE_PATTERN]
-
-options:
-
- -a EXTRACT_ARGS_CMD
- If provided, a shell command to extract arguments to the layout from the
- source files. The shell will be provided with a variable, POST_TITLE.
-
- -f FILE
- If provided, a pattern for the files to process. Default: *.
-
- -i LOG
- The name of the source directory under \$BLOGS where log posts live.
-
- -l LAYOUT
- If provided, the layout under \$LAYOUTS to use for the generated
- upphtml files. If not provided, uses the default layout.
-
- -p POSTPROCESS_CMD
- If provided, a shell command to post-process the source files into
- upphtml. The shell will be provided with a variable, POST_TITLE.
-
- -t
- If enabled, use filenames as post titles
-USAGE
-}
-
-extract_args_cmd="head -n 0 | echo \"POST_TITLE=\\\"\${POST_TITLE}\\\"\""
-file_pattern="*"
-log_name=""
-layout_name="default"
-postprocess_cmd="echo"
-use_filenames_for_titles=0
-
-while getopts "a:f:i:l:p:t" opt
-do
- case $opt in
- a)
- extract_args_cmd="$OPTARG"
- ;;
- f)
- file_pattern="$OPTARG"
- ;;
- i)
- log_name="$OPTARG"
- ;;
- l)
- layout_name="$OPTARG"
- ;;
- p)
- postprocess_cmd="$OPTARG"
- ;;
- t)
- use_filenames_for_titles=1
- ;;
- esac
-done
-
-if [ -z "${log_name}" ]
-then
- usage
- exit 1
-fi
-
-if ! [ -d "${BLOGS}/${log_name}" ]
-then
- >&2 echo "there is no directory '${input_log}' under ${BLOGS}. options: $(ls common)"
- usage
- exit 1
-fi
-
-layout="${LAYOUTS}/${layout_name}.layout.upphtml"
-if [ "${layout_name}" = "default" ]
-then
- layout_ext=""
-else
- layout_ext=".${layout_name}"
-fi
-if ! [ -f "${layout}" ]
-then
- echo >&2 "Warning: no layout found at $(basename "${LAYOUTS}")/${layout_name}.layout.upphtml"
-fi
-
-mkdir -p "${SRC}/${log_name}"
-
-for entry in $(find -L "${BLOGS}/${log_name}" -type f -name "${file_pattern}")
-do
- basename="$(basename "$entry")"
- name="${basename%\.*}"
- dest="${SRC}/${log_name}/${name}${layout_ext}.upphtml"
- if [ $use_filenames_for_titles -eq 1 ]
- then
- export POST_TITLE="$name"
- cat "$entry" | eval "${postprocess_cmd}" > "$dest"
- else
- export POST_TITLE="$(grep -e "^# " -m 1 "$entry" | cut -c3-)"
- cat "$entry" | grep -e "^# " -v | eval "$postprocess_cmd" > "$dest"
- fi
- cat "$entry" | eval "$extract_args_cmd" > "${dest}.args"
- echo "Processed ${entry#$(dirname ${BLOGS})/} to ${dest#$(dirname ${SRC})/}"
-done
diff --git a/http/bin/cut_longlog_meta b/http/bin/cut_longlog_meta
deleted file mode 100755
index 914f455..0000000
--- a/http/bin/cut_longlog_meta
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/sh -e
-
-meta=""
-read_published_on=0
-while read line
-do
- meta="${meta}
-${line}"
- if [ "$(echo "$line" | cut -c1-14)" = "Published on: " ]
- then
- read_published_on=1
- break
- fi
-done
-
-if [ "$read_published_on" -eq 0 ]
-then
- echo "$meta"
- exit 0
-fi
-
-read line
-if [ "$(echo "$line" | cut -c1-14)" = "Updated: " ]
-then
- meta="${meta}
-${line}"
-else
- echo "$line"
-fi
-
-while read line
-do
- echo "$line"
-done
diff --git a/http/bin/extract_longlog_meta b/http/bin/extract_longlog_meta
deleted file mode 100755
index 054c994..0000000
--- a/http/bin/extract_longlog_meta
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/sh -e
-
-POST_TITLE="${POST_TITLE:-}"
-DATE_PUBLISHED=""
-DATE_UPDATED=""
-
-while read line
-do
- if [ "$(echo "$line" | cut -c1-2)" = "# " ]
- then
- POST_TITLE="$(echo "$line" | cut -c3-)"
- elif [ "$(echo "$line" | cut -c1-14)" = "Published on: " ]
- then
- DATE_PUBLISHED="$(date --date="$(echo "$line" | cut -c15-)" +%Y-%m-%d)"
- elif [ "$(echo "$line" | cut -c1-12)" = "Updated on: " ]
- then
- DATE_UPDATED="$(date --date="$(echo "$line" | cut -c13-)" +%Y-%m-%d)"
- fi
-done
-
-if [ -n "$POST_TITLE" ]
-then
- echo "POST_TITLE=\"${POST_TITLE}\""
-fi
-
-if [ -n "$DATE_PUBLISHED" ]
-then
- echo "DATE_PUBLISHED=\"${DATE_PUBLISHED}\""
-fi
-
-if [ -n "$DATE_UPDATED" ]
-then
- echo "DATE_UPDATED=\"${DATE_UPDATED}\""
-fi
diff --git a/http/bin/gmi2upphtml b/http/bin/gmi2upphtml
deleted file mode 100755
index 1631037..0000000
--- a/http/bin/gmi2upphtml
+++ /dev/null
@@ -1,184 +0,0 @@
-#!/bin/sh -e
-
-post_title="${POST_TITLE:-}"
-
-is_in_list=0
-is_in_link_block=0
-is_in_preformatted_block=0
-is_in_blockquote=0
-
-if_in_list_then_close()
-{
- if [ $is_in_list -ne 0 ]
- then
- echo "</ul>"
- is_in_list=0
- fi
-}
-
-if_in_link_block_then_close()
-{
- if [ $is_in_link_block -ne 0 ]
- then
- echo "</p>"
- is_in_link_block=0
- fi
-}
-
-if_in_preformatted_block_then_close()
-{
- if [ $is_in_preformatted_block -ne 0 ]
- then
- echo "</pre>"
- is_in_preformatted_block=0
- fi
-}
-
-if_in_blockquote_then_close()
-{
- if [ $is_in_blockquote -ne 0 ]
- then
- echo "</blockquote>"
- is_in_blockquote=0
- fi
-}
-
-close_all_open_blocks()
-{
- if_in_list_then_close
- if_in_link_block_then_close
- if_in_preformatted_block_then_close
- if_in_blockquote_then_close
-}
-
-escape_upphtml()
-{
- echo "$1" \
- | sed "s/&/\\&amp;/g" \
- | sed "s/</\\&lt;/g" \
- | sed "s/>/\\&gt;/g" \
- | sed "s/\"/\\&quot;/g" \
- | sed "s/'/\\&#x27;/g" \
- | sed "s/\`/\\&#x60;/g" \
- | sed "s/\\\$/\\\\\$/g"
-}
-
-close_all_open_blocks_except()
-{
- except="$1"
- if [ "$except" != "list" ]
- then
- if_in_list_then_close
- fi
- if [ "$except" != "link" ]
- then
- if_in_link_block_then_close
- fi
- if [ "$except" != "pre" ]
- then
- if_in_preformatted_block_then_close
- fi
- if [ "$except" != "blockquote" ]
- then
- if_in_blockquote_then_close
- fi
-}
-
-if [ -z "$post_title" ]
-then
- read first_line
- if [ "$(echo "$first_line" | cut -c1-2)" != "# " ]
- then
- echo >&2 "Error: no [post_title] argument provided and no top-level heading at top of the document."
- exit 1
- fi
- post_title="${first_line### }"
-fi
-
-echo "#!
-export TITLE=\"${post_title} | joeac's blog\"
-export DESCRIPTION=\"${post_title} | joeac's blog\"
-pp \"\${SHARE}\"/components/meta.upphtml
-#!"
-
-while read line
-do
- if [ "$(echo $line | sed "s/[[:space:]]//g")" = "" ]
- then
- continue
- fi
-
- if [ "$(echo $line | cut -c1)" = "#" ]
- then
- title="$(escape_upphtml "${line#*# }")"
- heading="${line%%$title}"
- heading="${heading%% }"
- heading_level="${#heading}"
- if [ "$(echo "$line" | cut -c$(( $heading_level + 1 )) )" = " " ]
- then
- close_all_open_blocks
- echo "<h${heading_level}>${title}</h${heading_level}>"
- continue
- fi
- fi
-
- if [ "$(echo "$line" | cut -c1-2)" = "=>" ]
- then
- close_all_open_blocks_except link
- href="$(escape_upphtml "$(echo "$line" | sed "s/^=>[[:space:]]\+//" | sed "s/[[:space:]]\+.*//")")"
- title="$(escape_upphtml "${line#=> *${href}}")"
- title="${title:-${href}}"
- if [ $is_in_link_block -eq 0 ]
- then
- echo "<p>"
- else
- echo "<br/>"
- fi
- is_in_link_block=1
- echo "=> <a href='${href}'>${title}</a>"
- continue
- fi
-
- if [ "$( echo "$line" | cut -c1-2)" = "* " ]
- then
- close_all_open_blocks_except list
- if [ $is_in_list -eq 0 ]
- then
- echo "<ul>"
- fi
- is_in_list=1
- echo "<li>$(escape_upphtml "$(echo "$line" | cut -c3-)")</li>"
- continue
- fi
-
- if [ "$(echo "$line" | cut -c1)" = ">" ]
- then
- close_all_open_blocks_except blockquote
- if [ $is_in_blockquote -eq 0 ]
- then
- echo "<blockquote>"
- fi
- is_in_blockquote=1
- echo "<p>$(escape_upphtml "$(echo "$line" | cut -c2-)")</p>"
- continue
- fi
-
- if [ "$(echo "$line" | cut -c1-3)" = "\`\`\`" ]
- then
- close_all_open_blocks_except pre
- if [ $is_in_preformatted_block -eq 0 ]
- then
- echo "<pre>"
- else
- echo -e "\n"
- fi
- is_in_preformatted_block=1
- echo "$(escape_upphtml "$(echo "$line" | cut -c4-)")"
- continue
- fi
-
- close_all_open_blocks
- echo "<p>$(escape_upphtml "${line}")</p>"
-done
-
-close_all_open_blocks
diff --git a/http/bin/lmt b/http/bin/lmt
deleted file mode 100755
index e0e19ea..0000000
--- a/http/bin/lmt
+++ /dev/null
Binary files differ
diff --git a/http/bin/mkws b/http/bin/mkws
deleted file mode 100755
index 228109c..0000000
--- a/http/bin/mkws
+++ /dev/null
@@ -1,72 +0,0 @@
-#!/bin/sh -e
-export PATH="$(dirname $(realpath $0)):$PATH"
-
-usage() {
- >&2 printf "usage: %s url [pattern]\\n" "$(basename "$0")"
- exit 1
-}
-
-test $# -lt 1 && usage
-
-export SRC="$(realpath src)"
-export PUBLIC="$(realpath public)"
-export SHARE="$(realpath share)"
-export LAYOUTS="$(realpath share/layouts)"
-export DEFAULT_LAYOUT="${LAYOUTS}/default.layout.upphtml"
-export OUT="$(realpath out)"
-
-if ! test -f "${SRC}"/index.upphtml
-then
- >&2 printf "no index.upphtml file found\\n"
- exit 1
-fi
-
-PATH_PATTERN="${2:-*}"
-for t in $(find -L "${SRC}/" -type f -and -wholename "${PATH_PATTERN}" -and -name "*\.upp*" -and -not -name "*\.upp*\.*")
-do
- basename="$(basename "$t")"
- ext="${basename##*upp}"
-
- basename_without_ext="${basename%\.upp${ext}}"
- penultimate_segment="${basename_without_ext##*\.}"
- if [ "$basename_without_ext" != "$penultimate_segment" ] && [ -f "${LAYOUTS}/${penultimate_segment}.layout.upphtml" ]
- then
- layout="${LAYOUTS}/${penultimate_segment}.layout.upphtml"
- name="${basename%\.${penultimate_segment}\.upp${ext}}"
- else
- layout="${DEFAULT_LAYOUT}"
- name="${basename%\.upp${ext}}"
- fi
-
- in_dir="$(dirname "$t")/"
- rel_dir="${in_dir#${SRC}/}"
- mkdir -p "${OUT}/${rel_dir}/"
-
- args="$( [ -f "${t}.args" ] && cat "${t}.args" || echo "" )"
- if [ "$(head -n 1 "${layout}")" = "#!" ]
- then
- >"${layout}.tmp" echo "
-#!
-${args}
-$(tail -n +2 "${layout}")
-"
- else
- >"${layout}.tmp" echo "
-#!
-${args}
-#!
-$(cat "${layout}")
-"
- fi
- if [ "$ext" = "html" ]
- then
- pp "${layout}.tmp" "${t}" "${1}" > \
- "${OUT}/${rel_dir}/${name}.${ext}"
- else
- pp "${t}" "${1}" > \
- "${OUT}/${rel_dir}/${name}.${ext}"
- fi
- rm "${layout}.tmp"
-
- echo "Written ${OUT}/${rel_dir}${name}.${ext}"
-done
diff --git a/http/blogs/blog b/http/blogs/blog
deleted file mode 120000
index 908e98e..0000000
--- a/http/blogs/blog
+++ /dev/null
@@ -1 +0,0 @@
-../../common/longlog/ \ No newline at end of file
diff --git a/http/blogs/microlog b/http/blogs/microlog
deleted file mode 120000
index 4f95064..0000000
--- a/http/blogs/microlog
+++ /dev/null
@@ -1 +0,0 @@
-../../common/microlog/ \ No newline at end of file
diff --git a/http/composer.json b/http/composer.json
deleted file mode 100644
index 98e8628..0000000
--- a/http/composer.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "name": "joeacnet/http",
- "require": {
- "phpmailer/phpmailer": "^7.1"
- },
- "autoload": {
- "psr-4": {
- "JoeacNet\\Http\\": "php/"
- }
- },
- "authors": [
- {
- "name": "Joe Carstairs",
- "email": "me@joeac.net"
- }
- ]
-}
diff --git a/http/composer.lock b/http/composer.lock
deleted file mode 100644
index 4d72ec7..0000000
--- a/http/composer.lock
+++ /dev/null
@@ -1,101 +0,0 @@
-{
- "_readme": [
- "This file locks the dependencies of your project to a known state",
- "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
- "This file is @generated automatically"
- ],
- "content-hash": "5145609ea1f80d6d838d0f9649acd36a",
- "packages": [
- {
- "name": "phpmailer/phpmailer",
- "version": "v7.1.1",
- "source": {
- "type": "git",
- "url": "https://github.com/PHPMailer/PHPMailer.git",
- "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
- "reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
- "shasum": ""
- },
- "require": {
- "ext-ctype": "*",
- "ext-filter": "*",
- "ext-hash": "*",
- "php": ">=5.5.0"
- },
- "require-dev": {
- "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
- "doctrine/annotations": "^1.2.6 || ^1.13.3",
- "php-parallel-lint/php-console-highlighter": "^1.0.0",
- "php-parallel-lint/php-parallel-lint": "^1.3.2",
- "phpcompatibility/php-compatibility": "^10.0.0@dev",
- "squizlabs/php_codesniffer": "^3.13.5",
- "yoast/phpunit-polyfills": "^1.0.4"
- },
- "suggest": {
- "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
- "directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
- "ext-imap": "Needed to support advanced email address parsing according to RFC822",
- "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
- "ext-openssl": "Needed for secure SMTP sending and DKIM signing",
- "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
- "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
- "league/oauth2-google": "Needed for Google XOAUTH2 authentication",
- "psr/log": "For optional PSR-3 debug logging",
- "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
- "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "PHPMailer\\PHPMailer\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "LGPL-2.1-only"
- ],
- "authors": [
- {
- "name": "Marcus Bointon",
- "email": "phpmailer@synchromedia.co.uk"
- },
- {
- "name": "Jim Jagielski",
- "email": "jimjag@gmail.com"
- },
- {
- "name": "Andy Prevost",
- "email": "codeworxtech@users.sourceforge.net"
- },
- {
- "name": "Brent R. Matzelle"
- }
- ],
- "description": "PHPMailer is a full-featured email creation and transfer class for PHP",
- "support": {
- "issues": "https://github.com/PHPMailer/PHPMailer/issues",
- "source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
- },
- "funding": [
- {
- "url": "https://github.com/Synchro",
- "type": "github"
- }
- ],
- "time": "2026-05-18T08:06:14+00:00"
- }
- ],
- "packages-dev": [],
- "aliases": [],
- "minimum-stability": "stable",
- "stability-flags": {},
- "prefer-stable": false,
- "prefer-lowest": false,
- "platform": {},
- "platform-dev": {},
- "plugin-api-version": "2.9.0"
-}
diff --git a/http/config.mk b/http/config.mk
deleted file mode 100644
index 3b42bd7..0000000
--- a/http/config.mk
+++ /dev/null
@@ -1,7 +0,0 @@
-BLOG_LAYOUT_blog := blog
-BLOG_EXT_blog := gmi
-BLOG_FLAGS_blog = -p 'cut_longlog_meta | gmi2upphtml' -a extract_longlog_meta
-
-BLOG_LAYOUT_microlog := microlog
-BLOG_EXT_microlog := gmi
-BLOG_FLAGS_microlog := -t -p gmi2upphtml
diff --git a/http/php/config.php b/http/php/config.php
deleted file mode 100644
index cfe91d3..0000000
--- a/http/php/config.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php namespace JoeacNet\Http\Config;
-
-define("CONFIG", parse_ini_file(dirname(__FILE__, 2) . "/config.ini"));
-
-function maxDailyEmails(): int|null
-{
- $maxDailyEmails = CONFIG["MAX_DAILY_EMAILS"];
- if (!is_numeric($maxDailyEmails)) {
- return null;
- }
- return $maxDailyEmails;
-}
-
-function contactMailbox(): string|null
-{
- return (string) CONFIG["CONTACT_MAILBOX"];
-}
-
-function contactMailboxName(): string|null
-{
- return (string) CONFIG["CONTACT_MAILBOX_NAME"];
-}
-
-function localSmtpFrom(): string|null
-{
- return (string) CONFIG["LOCAL_SMTP_FROM"];
-}
-
-function localSmtpFromName(): string|null
-{
- return (string) CONFIG["LOCAL_SMTP_FROM_NAME"];
-}
-
-function localSmtpHost(): string|null
-{
- return (string) CONFIG["LOCAL_SMTP_HOST"];
-}
-
-function localSmtpPort(): string|null
-{
- return (string) CONFIG["LOCAL_SMTP_PORT"];
-}
-
-function localSmtpUser(): string|null
-{
- return (string) CONFIG["LOCAL_SMTP_USER"];
-}
-
-function localSmtpPass(): string|null
-{
- return (string) CONFIG["LOCAL_SMTP_PASSWORD"];
-}
-
-function dbScheme(): string|null
-{
- return (string) CONFIG["DB_SCHEME"];
-}
-
-function dbPath(): string|null
-{
- return (string) CONFIG["DB_PATH"];
-}
diff --git a/http/php/db.php b/http/php/db.php
deleted file mode 100644
index a60d6c4..0000000
--- a/http/php/db.php
+++ /dev/null
@@ -1,205 +0,0 @@
-<?php namespace JoeacNet\Http\Db;
-
-use JoeacNet\Http\Config;
-
-use PDO;
-
-require_once __DIR__ . "/config.php";
-
-function connectDb(): PDO
-{
- $pdo = new PDO(Config\dbScheme() . ":" . Config\dbPath());
- $pdo = createTables($pdo);
- return $pdo;
-}
-
-function createTables(PDO $pdo): PDO
-{
- $pdo->exec(
- <<<SQL
- CREATE TABLE IF NOT EXISTS otp (
- userId TEXT NOT NULL,
- value TEXT NOT NULL,
- createdAtUnixSecs INTEGER NOT NULL,
- validUntilUnixSecs INTEGER NOT NULL
- );
- SQL
- ,
- );
-
- $pdo->exec(
- <<<SQL
- CREATE TABLE IF NOT EXISTS send_email_token (
- userId TEXT NOT NULL,
- value TEXT NOT NULL,
- createdAtUnixSecs INTEGER NOT NULL,
- validUntilUnixSecs INTEGER NOT NULL
- );
- SQL
- ,
- );
-
- $pdo->exec(
- <<<SQL
- CREATE TABLE IF NOT EXISTS sent_emails (
- messageId TEXT NOT NULL,
- sentAtUnixSecs INTEGER NOT NULL
- );
- SQL
- ,
- );
-
- return $pdo;
-}
-
-function countEmailsSentLast24Hours(PDO $db): int
-{
- $twentyFourHoursAgo = time() - 24 * 60 * 60;
- return (int) $db
- ->query(
- <<<SQL
- SELECT COUNT(*) as count
- FROM sent_emails
- WHERE sentAtUnixSecs >= $twentyFourHoursAgo;
- SQL
- ,
- )
- ->fetchColumn(0);
-}
-
-function insertSendEmailToken(PDO $db, string $userId, string $token)
-{
- $createdAtUnixSecs = time();
- $validUntilUnixSecs = $createdAtUnixSecs + 60;
- $db
- ->prepare(
- <<<SQL
- INSERT
- INTO send_email_token
- (userId, value, createdAtUnixSecs, validUntilUnixSecs)
- VALUES
- (:userId, :token, :createdAtUnixSecs, :validUntilUnixSecs);
- SQL
- ,
- )
- ->execute([
- "userId" => $userId,
- "token" => $token,
- "createdAtUnixSecs" => $createdAtUnixSecs,
- "validUntilUnixSecs" => $validUntilUnixSecs,
- ]);
-}
-
-function isTokenValid(PDO $db, string $userId, string $token): int
-{
- $now = time();
- $stmt = $db->prepare(
- <<<SQL
- SELECT COUNT(*) AS count
- FROM send_email_token
- WHERE userId = :userId
- AND value = :token
- AND validUntilUnixSecs >= :validUntilUnixSecs;
- SQL
- ,
- );
- $stmt->execute([
- "userId" => $userId,
- "token" => $token,
- "validUntilUnixSecs" => $now,
- ]);
- $validTokenCount = (int) $stmt->fetchColumn(0);
- return $validTokenCount > 0;
-}
-
-function deleteAllTokensForUser(PDO $db, string $userId)
-{
- $db
- ->prepare(
- <<<SQL
- DELETE
- FROM send_email_token
- WHERE userId = :userId;
- SQL
- ,
- )
- ->execute(["userId" => $userId]);
-}
-
-function deleteAllOtpsForUser(PDO $db, string $userId)
-{
- $db
- ->prepare(
- <<<SQL
- DELETE
- FROM otp
- WHERE userId = :userId;
- SQL
- ,
- )
- ->execute(["userId" => $userId]);
-}
-
-function recordSentEmail(PDO $db, string $messageId)
-{
- $now = time();
- $stmt = $db->prepare(
- <<<SQL
- INSERT
- INTO sent_emails
- (messageId, sentAtUnixSecs)
- VALUES
- (:messageId, :now);
- SQL
- ,
- );
- $stmt->execute(["messageId" => $messageId, "now" => $now]);
-}
-
-function insertOtp(PDO $db, string $userId, string $otp)
-{
- $createdAtUnixSecs = time();
- $validUntilUnixSecs = $createdAtUnixSecs + 60 * 5;
- $db
- ->prepare(
- <<<SQL
- INSERT
- INTO otp
- (userId, value, createdAtUnixSecs, validUntilUnixSecs)
- VALUES
- (:userId, :otp, :createdAtUnixSecs, :validUntilUnixSecs)
- SQL
- ,
- )
- ->execute([
- "userId" => $userId,
- "otp" => $otp,
- "createdAtUnixSecs" => $createdAtUnixSecs,
- "validUntilUnixSecs" => $validUntilUnixSecs,
- ]);
-}
-
-function isOtpCorrect(
- PDO $db,
- string $userId,
- string $guess,
- int $leniencySecs,
-): bool {
- $stmt = $db->prepare(
- <<<SQL
- SELECT
- COUNT(*) as count
- FROM otp
- WHERE userId = :userId
- AND value = :guess
- AND validUntilUnixSecs >= :validUntilUnixSecs
- SQL
- ,
- );
- $stmt->execute([
- "userId" => $userId,
- "guess" => $guess,
- "validUntilUnixSecs" => time() - $leniencySecs,
- ]);
- return $stmt->fetchColumn(0);
-}
diff --git a/http/php/mail.php b/http/php/mail.php
deleted file mode 100644
index d9c4125..0000000
--- a/http/php/mail.php
+++ /dev/null
@@ -1,35 +0,0 @@
-<?php namespace JoeacNet\Http\Mail;
-
-use PHPMailer\PHPMailer\PHPMailer;
-
-use JoeacNet\Http\Config;
-
-require_once __DIR__ . "/../vendor/autoload.php";
-
-function sendEmail(
- string $toEmail,
- string $toName,
- string $subject,
- string $body,
- string $replyToEmail = null,
- string $replyToName = null,
-): string {
- $mail = new PHPMailer();
- $mail->isSMTP();
- $mail->Host = Config\localSmtpHost();
- $mail->Port = Config\localSmtpPort();
- $mail->SMTPAuth = true;
- $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
- $mail->Username = Config\localSmtpUser();
- $mail->Password = Config\localSmtpPass();
- $mail->setFrom(Config\localSmtpFrom(), Config\localSmtpFromName());
- if (!is_null($replyToEmail)) {
- $mail->addReplyTo($replyToEmail, $replyToName);
- }
- $mail->addAddress($toEmail, $toName);
- $mail->isHTML(false);
- $mail->Subject = "joeac.net: $subject";
- $mail->Body = "$body";
- $mail->send();
- return $mail->getLastMessageID();
-}
diff --git a/http/pp/.gitignore b/http/pp/.gitignore
deleted file mode 100644
index c0d72fa..0000000
--- a/http/pp/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-pp
-*.o
diff --git a/http/pp/LICENSE b/http/pp/LICENSE
deleted file mode 100644
index 648cf93..0000000
--- a/http/pp/LICENSE
+++ /dev/null
@@ -1,15 +0,0 @@
-ISC License
-
-Copyright (c) 2021, Adrian Emil Grigore <adrian.emil.grigore@gmail.com>
-
-Permission to use, copy, modify, and/or distribute this software for any
-purpose with or without fee is hereby granted, provided that the above
-copyright notice and this permission notice appear in all copies.
-
-THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
-WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
-ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
-WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
-ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
-OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
diff --git a/http/pp/Makefile b/http/pp/Makefile
deleted file mode 100644
index afc122f..0000000
--- a/http/pp/Makefile
+++ /dev/null
@@ -1,29 +0,0 @@
-CC = cc
-LD = ${CC}
-
-PREFIX = /usr/local
-MANPREFIX = ${PREFIX}/share/man
-
-CFLAGS = -Wall -Wextra -pedantic
-
-pp: pp.o
-pp.o: pp.c
-
-.c.o:
- $(CC) $(CPPFLAGS) $< $(CFLAGS) -c -o $@
-.o:
- $(LD) $< $(LDFLAGS) -o $@
-
-clean:
- rm -rf pp *.o
-
-install: pp pp.1
- mkdir -p $(DESTDIR)$(PREFIX)/bin
- mkdir -p $(DESTDIR)$(MANPREFIX)/man1
- cp pp $(DESTDIR)$(PREFIX)/bin/pp
- cp pp.1 $(DESTDIR)$(MANPREFIX)/man1/
- chmod 755 $(DESTDIR)$(PREFIX)/bin/pp
-
-uninstall:
- rm $(DESTDIR)$(PREFIX)/bin/pp
- rm $(DESTDIR)$(MANPREFIX)/man1/pp.1
diff --git a/http/pp/README.md b/http/pp/README.md
deleted file mode 100644
index f69faf1..0000000
--- a/http/pp/README.md
+++ /dev/null
@@ -1,188 +0,0 @@
-# `pp(1)`
-
-a preprocessor
-
-`pp(1)` allows embedding
-[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
-code in files of any type by nesting it inside the `#!\n` token, where
-`\n` is a new line. That means that if you'd like a simple loop or an
-`if` inside an HTML file for instance,
-you could use `pp(1)`.
-
-## Basic usage
-
-For the following code:
-
- <!doctype html>
- <title>pp(1) example</title>
- <ul>
- #!
- i=1
- while test $i -le 10
- do
- if test $((i % 2)) -eq 0
- then
- #!
- <li class=even>$i</li>
- #!
- else
- #!
- <li class=odd>$i</li>
- #!
- fi
- i=$((i + 1))
- done
- #!
- </ul>
-
-`pp(1)` outputs:
-
- <!doctype html>
- <title>pp(1) example</title>
- <ul>
- <li class=odd>1</li>
- <li class=even>2</li>
- <li class=odd>3</li>
- <li class=even>4</li>
- <li class=odd>5</li>
- <li class=even>6</li>
- <li class=odd>7</li>
- <li class=even>8</li>
- <li class=odd>9</li>
- <li class=even>10</li>
- </ul>
-
-## Arguments
-
-You can also pass arguments to it by appending them to the command call:
-
- <!doctype html>
- <title>$1</title>
- <p>
- #!
- echo $2
- #!
- </p>
-
-Calling the above code with
-`pp tmp.upphtml 'pp(1) example' 'hello, world'`
-will result in:
-
- <!doctype html>
- <title>pp(1) example</title>
- <p>
- hello world
- </p>
-
-## Pipes
-
-`pp(1)`'s `stdin` is sent to the child
-[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
-
- <!doctype html>
- <title>pp(1) example</title>
- <p>
- #!
- cat
- #!
- </p>
-
-`echo 'hello, world' | pp tmp.upphtml` will output:
-
- <!doctype html>
- <title>pp(1) example</title>
- <p>
- hello, world
- </p>
-
-## Debugging
-
-`pp(1)` also takes an optional `-d` flag. If passed, `pp(1)` will dump
-the generated
-[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
-code instead of executing it:
-
- <!doctype html>
- <title>pp(1) example</title>
- <ul>
- #!
- i=1
- while test $i -le 10
- do
- if test $((i % 2)) -eq 0
- then
- #!
- <li class=even>$i</li>
- #!
- else
- #!
- <li class=odd>$i</li>
- #!
- fi
- i=$((i + 1))
- done
- #!
- </ul>
-
-`pp -d tmp.upphtml` will output:
-
- echo "<!doctype html>
- <title>pp(1) example</title>
- <ul>"
- i=1
- while test $i -le 10
- do
- if test $((i % 2)) -eq 0
- then
- echo " <li class=even>$i</li>"
- else
- echo " <li class=odd>$i</li>"
- fi
- i=$((i + 1))
- done
- echo "</ul>"
-
-## Example
-
- <!doctype html>
- <html lang=en>
- <title>pp(1)</title>
- <meta charset=UTF-8>
- <meta name=viewport content='width=device-width'>
- <link rel=stylesheet href=../theme.css>
- <body>
- <a href=../#myprojects>Home</a>
- #!
- addcls() {
- sed "s/<code>/<code class=language-bash>/g"
- }
- smu README | addcls
- #!
- <h2>Files</h2>
- <ul>
- #!
- for f in *
- do
- case $f in
- *.upp*|*.html|*.tgz)
- continue
- ;;
- esac
- #!
- <li><a href=$f>$f</a></li>
- #!
- done
- #!
- </ul>
- <h2>Download</h2>
- <a href=pp.tgz>pp.tgz</a>
- <script src=../prism.js data-manual></script>
- <script src=../enhance.js></script>
- </body>
- </html>
-
-## See also
-
-* <https://github.com/radare/spp>
-* <https://werc.cat-v.org/docs/rc-template-lang>
-* <https://code.9front.org/hg/werc/file/92f7463dac1a/bin/template.awk>
diff --git a/http/pp/pp.1 b/http/pp/pp.1
deleted file mode 100644
index a552c97..0000000
--- a/http/pp/pp.1
+++ /dev/null
@@ -1,46 +0,0 @@
-.Dd Apr 25, 2019
-.Dt pp 1
-.Os
-.Sh NAME
-.Nm pp
-.Nd preprocessor
-.Sh SYNOPSIS
-.Nm pp
-.Op Fl d
-.Ar file
-.Op args ...
-.Sh DESCRIPTION
-The
-.Nm
-utility processes
-.Ar file
-to produce output that is used as input to another
-program.
-.Pp
-It executes code enclosed in
-.Dq #!\en
-delimiters using
-.Xr sh 1 .
-.Op args ...
-are passed to the embbeded script and are available
-via the usual
-.Em $1, $2, ...
-positional parameters.
-.Pp
-The options are as follows:
-.Bl -tag -width Ds
-.It Fl d
-.Nm
-will dump the generated
-.Xr sh 1
-code instead of executing it. This is useful for debugging.
-.El
-.Sh EXIT STATUS
-.Ex -std
-.Sh EXAMPLES
-Preprocess file with arguments:
-.Pp
-.Dl $ pp file.upphtml a b
-.Pp
-.Sh SEE ALSO
-.Xr sh 1
diff --git a/http/pp/pp.c b/http/pp/pp.c
deleted file mode 100644
index be96596..0000000
--- a/http/pp/pp.c
+++ /dev/null
@@ -1,369 +0,0 @@
-#include <errno.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <sys/wait.h>
-#include <unistd.h>
-
-#if !__APPLE__
-/*
- * Appends src to string dst of size dsize (unlike strncat, dsize is the
- * full size of dst, not space left). At most dsize-1 characters
- * will be copied. Always NUL terminates (unless dsize <= strlen(dst)).
- * Returns strlen(src) + MIN(dsize, strlen(initial dst)).
- * If retval >= dsize, truncation occurred.
- */
-size_t
-strlcat(char *dst, const char *src, size_t dsize)
-{
- const char *odst = dst;
- const char *osrc = src;
- size_t n = dsize;
- size_t dlen;
-
- /* Find the end of dst and adjust bytes left but don't go past end. */
- while (n-- != 0 && *dst != '\0')
- dst++;
- dlen = dst - odst;
- n = dsize - dlen;
-
- if (n-- == 0)
- return(dlen + strlen(src));
- while (*src != '\0') {
- if (n != 0) {
- *dst++ = *src;
- n--;
- }
- src++;
- }
- *dst = '\0';
-
- return(dlen + (src - osrc)); /* count does not include NUL */
-}
-#endif /* !__APPLE__ */
-
-#define MAXSIZE 64000
-#define MAXINDENTSIZE 80
-#define MAXARGSIZE 20
-
-char *argv0;
-
-int
-efgetc(FILE *fp)
-{
- int c;
-
- c = fgetc(fp);
-
- if (c == EOF &&
- ferror(fp) != 0) {
- perror(argv0);
- exit(1);
- }
-
- return c;
-}
-
-int
-efseek(FILE *fp, long offset, int whence)
-{
- int s;
-
- s = fseek(fp, offset, whence);
-
- if (s == -1) {
- perror(argv0);
- exit(1);
- }
-
- return s;
-}
-
-int
-eungetc(int c, FILE *fp)
-{
- int uc;
-
- uc = ungetc(c, fp);
-
- if (uc == EOF) {
- perror(argv0);
- exit(1);
- }
-
- return uc;
-}
-
-size_t
-estrlcat(char *dst, const char *src, size_t dstsize)
-{
- size_t n;
-
- n = strlcat(dst, src, dstsize);
-
- if (n >= dstsize) {
- fprintf(stderr, "%s: Buffer overflow\n", argv0);
- exit(1);
- }
-
- return n;
-}
-
-int
-eclose(int d)
-{
- int s;
-
- s = close(d);
-
- if (s == -1) {
- perror(argv0);
- exit(1);
- }
-
- return s;
-}
-
-char *
-strappend(char *s, char c)
-{
- size_t len;
-
- len = strlen(s);
-
- s[len] = c;
- s[len + 1] = '\0';
-
- return s ;
-}
-
-int
-shebang(FILE *fp)
-{
- int c;
-
- c = efgetc(fp);
-
- if (c != '#') {
- eungetc(c, fp);
- return 0;
- }
-
- c = efgetc(fp);
-
- if (c != '!') {
- eungetc(c, fp);
- eungetc('#', fp);
- return 0;
- }
-
- c = efgetc(fp);
-
- if (c == '\n') {
- eungetc(c, fp);
- eungetc('!', fp);
- eungetc('#', fp);
- return 0;
- }
-
- for (;;) {
- c = efgetc(fp);
- if (c == '\n')
- break;
- }
-
- return 1;
-}
-
-int
-token(FILE *fp, int consume)
-{
- int c;
-
- c = efgetc(fp);
-
- if (c != '#') {
- eungetc(c, fp);
- return 0;
- }
-
- c = efgetc(fp);
- if (c != '!') {
- eungetc(c, fp);
- eungetc('#', fp);
- return 0;
- };
-
- c = efgetc(fp);
-
- if (c != '\n') {
- eungetc(c, fp);
- eungetc('!', fp);
- eungetc('#', fp);
- return 0;
- }
-
- if (consume == 0) {
- eungetc(c, fp);
- eungetc('!', fp);
- eungetc('#', fp);
- }
-
- return 1;
-}
-
-size_t
-output(char *s)
-{
- return estrlcat(s, "echo \"", MAXSIZE);
-}
-
-size_t
-outputend(char *s)
-{
- return estrlcat(s, "\"\n", MAXSIZE);
-}
-
-int main(int argc, char **argv)
-{
- char s[MAXSIZE] = "", indent[MAXINDENTSIZE] = "",
- *eargv[MAXARGSIZE] = {"sh", "-c", s, "sh", NULL};
- FILE *fp;
- int size, c, i, fd[2], o = 0, status = 0, d = 0;
-
-
- argv0 = argv[0];
- if (argc == 1) {
- fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
- return 1;
-
- }
-
- if (strcmp(argv[1], "-d") == 0) {
- if (argc == 2) {
- fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
- return 1;
-
- }
- d = 1;
- fp = fopen(argv[2], "r");
- } else {
- fp = fopen(argv[1], "r");
- }
-
-
- if (!fp) {
- fprintf(stderr, "%s: %s: %s\n",
- argv[0], d ? argv[2] : argv[1], strerror(errno));
- return 1;
- }
-
- efseek (fp, 0, SEEK_END);
- size = ftell(fp);
-
- if(size == 0) return 0;
-
- efseek(fp, -1, SEEK_END);
- c = efgetc(fp);
-
- if (c != '\n') {
- fprintf(stderr, "%s: %s: No newline at end of file\n",
- argv[0], d ? argv[2] : argv[1]);
- exit(1);
- }
-
- efseek(fp, 0, SEEK_SET);
-
- shebang(fp);
-
- if (!token(fp, 1)) {
- output(s);
- o = 1;
- }
-
- for(;;) {
- if (token(fp, 1)) {
- if (!o) {
- c = efgetc(fp);
- if (c == EOF)
- goto done;
- eungetc(c, fp);
- output(s);
- } else
- outputend(s);
- o = !o;
- }
-
- c = efgetc(fp);
- if (c == EOF)
- break;
-
- if (c == '\n') {
- indent[0] = 0;
- c = efgetc(fp);
- if (c == EOF)
- break;
-
- for (;;) {
- if (c != ' ' && c != '\t')
- break;
- strappend(indent, c);
- c = efgetc(fp);
- if (c == EOF)
- break;
- }
-
- eungetc(c, fp);
-
- if (token(fp, 0) && o)
- continue;
-
- strappend(s, '\n');
- estrlcat(s, indent, MAXSIZE);
- continue;
- }
-
- strappend(s, c);
- }
- if (o) {
- outputend(s);
- o = 0;
- }
- done:
-
- if(d) {
- puts(s);
- return 0;
- }
-
- if (pipe(fd) == -1) {
- perror(argv0);
- return 1;
- }
-
- switch (fork()) {
- case -1:
- perror(argv0);
- return 1;
- case 0:
- eclose(fd[1]);
- eclose(fd[0]);
- for (i = 2; i < argc; i++)
- eargv[i+2] = argv[i];
-
- if (execv("/bin/sh", eargv) == -1) {
- perror(argv0);
- return 1;
- }
- break;
- default:
- eclose(fd[1]);
- eclose(fd[0]);
- if (wait(&status) == -1) {
- perror(argv0);
- return 1;
- }
- }
-
- return status;
-}
diff --git a/http/public/do/send_email.php b/http/public/do/send_email.php
deleted file mode 100644
index 51d8b19..0000000
--- a/http/public/do/send_email.php
+++ /dev/null
@@ -1,124 +0,0 @@
-<?php
-
-use JoeacNet\Http\Config;
-use JoeacNet\Http\Db;
-use JoeacNet\Http\Mail;
-
-require_once __DIR__ . "/../../vendor/autoload.php";
-require_once __DIR__ . "/../../php/config.php";
-require_once __DIR__ . "/../../php/db.php";
-require_once __DIR__ . "/../../php/mail.php";
-
-$db = Db\connectDb();
-
-$payload = json_decode(file_get_contents("php://input"), true);
-$name = getNameFromPayloadAndValidate($payload);
-$email = getEmailFromPayloadAndValidate($payload);
-$message = getMessageFromPayloadAndValidate($payload);
-$token = getTokenFromPayloadAndValidate($payload);
-$userId = getUserIdFromPayloadAndValidate($payload);
-
-limitMaxDailyEmails($db);
-verifyToken(db: $db, userId: $userId, token: $token);
-Db\deleteAllTokensForUser($db, $userId);
-
-$messageId = Mail\sendEmail(
- subject: "joeac.net: $name left a message",
- replyToEmail: $email,
- replyToName: $name,
- toEmail: Config\contactMailbox(),
- toName: Config\contactMailboxName(),
- body: "$name <$email> sent you a message:\n\n\n$message",
-);
-Db\recordSentEmail($db, $messageId);
-syslog(LOG_INFO, "Sent an email to Joe. Message ID: $messageId");
-
-http_response_code(200);
-echo $messageId;
-exit();
-
-/// functions ///
-
-function verifyToken(PDO $db, string $userId, string $token)
-{
- if (!Db\isTokenValid($db, $userId, $token)) {
- http_response_code(400);
- echo "token is not valid";
- exit();
- }
-}
-
-function limitMaxDailyEmails(PDO $db)
-{
- $countEmailsSentLast24Hours = Db\countEmailsSentLast24Hours($db);
- if ($countEmailsSentLast24Hours > Config\maxDailyEmails()) {
- $msg =
- "$countEmailsSentLast24Hours emails have been sent in the last 24 hours, but the max daily load is " .
- Config\maxDailyEmails() .
- ".";
- syslog(LOG_WARNING, $msg);
- http_response_code(500);
- echo $msg;
- exit();
- }
-}
-
-function getNameFromPayloadAndValidate(array $payload): string
-{
- $name = (string) $payload["name"];
- if (is_null($name) || $name == "") {
- http_response_code(400);
- echo "name must not be empty";
- exit();
- }
- return $name;
-}
-
-function getEmailFromPayloadAndValidate(array $payload): string
-{
- $email = (string) $payload["email"];
- if (is_null($email) || $email == "") {
- http_response_code(400);
- echo "email must not be empty";
- exit();
- }
- if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
- http_response_code(400);
- echo "<$email> is not a valid email address.";
- exit();
- }
- return $email;
-}
-
-function getMessageFromPayloadAndValidate(array $payload): string
-{
- $message = (string) $payload["message"];
- if (is_null($message) || $message == "") {
- http_response_code(400);
- echo "message must not be empty";
- exit();
- }
- return $message;
-}
-
-function getTokenFromPayloadAndValidate(array $payload): string
-{
- $token = (string) $payload["token"];
- if (is_null($token) || $token == "") {
- http_response_code(400);
- echo "token must not be empty";
- exit();
- }
- return $token;
-}
-
-function getUserIdFromPayloadAndValidate(array $payload): string
-{
- $userId = (string) $payload["userId"];
- if (is_null($userId) || $userId == "") {
- http_response_code(400);
- echo "userId must not be empty";
- exit();
- }
- return $userId;
-}
diff --git a/http/public/do/send_otp.php b/http/public/do/send_otp.php
deleted file mode 100644
index 5ca4d85..0000000
--- a/http/public/do/send_otp.php
+++ /dev/null
@@ -1,104 +0,0 @@
-<?php
-
-use JoeacNet\Http\Config;
-use JoeacNet\Http\Db;
-use JoeacNet\Http\Mail;
-
-require_once __DIR__ . "/../../php/config.php";
-require_once __DIR__ . "/../../php/db.php";
-require_once __DIR__ . "/../../php/mail.php";
-
-const SEND_OTP_TYPES = ["email"];
-
-$db = Db\connectDb();
-
-$payload = json_decode(file_get_contents("php://input"), true);
-$email = getEmailFromPayloadAndValidate($payload);
-$name = getNameFromPayloadAndValidate($payload);
-$type = getTypeFromPayloadAndValidate($payload);
-
-limitMaxDailyEmails(db: $db, email: $email, name: $name);
-
-$otp = strtoupper(bin2hex(random_bytes(3)));
-$prettyOtp = substr($otp, 0, 3) . "-" . substr($otp, 3, 6);
-Db\insertOtp(db: $db, userId: $email, otp: $otp);
-
-try {
- $messageId = Mail\sendEmail(
- toEmail: $email,
- toName: $name,
- subject: "joeac.net: your OTP is $prettyOtp",
- body: <<<BODY
- Someone tried to use this email address on joeac.net. If this was you,
- your one-time passcode is $prettyOtp. If this wasn't you, you don't need
- to do anything.
- BODY,
- );
-} catch (Exception $e) {
- error_log("Email could not be sent: $e");
- http_response_code(500);
- echo "Email could not be sent: $e";
- exit();
-}
-
-Db\recordSentEmail($db, $messageId);
-syslog(LOG_INFO, "Sent OTP ($prettyOtp) to $email. Message ID: $messageId");
-
-http_response_code(200);
-echo $messageId;
-exit();
-
-/// functions ///
-
-function limitMaxDailyEmails(PDO $db, string $email, string $name)
-{
- $countEmailsSentLast24Hours = Db\countEmailsSentLast24Hours($db);
- if ($countEmailsSentLast24Hours > Config\maxDailyEmails()) {
- $msg =
- "$name <$email> requested an OTP email, but $countEmailsSentLast24Hours emails have already been sent, whereas the max daily load is " .
- Config\maxDailyEmails() .
- ".";
- syslog(LOG_WARNING, $msg);
- http_response_code(500);
- echo $msg;
- exit();
- }
-}
-
-function getNameFromPayloadAndValidate(array $payload): string
-{
- $name = (string) $payload["name"];
- if (is_null($name) || $name == "") {
- http_response_code(400);
- echo "name must not be empty";
- exit();
- }
- return $name;
-}
-
-function getEmailFromPayloadAndValidate(array $payload): string
-{
- $email = (string) $payload["email"];
- if (is_null($email) || $email == "") {
- http_response_code(400);
- echo "email must not be empty";
- exit();
- }
- if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
- http_response_code(400);
- echo "<$email> is not a valid email address.";
- exit();
- }
- return $email;
-}
-
-function getTypeFromPayloadAndValidate(array $payload): string
-{
- $type = (string) $payload["type"];
- if (!in_array($type, SEND_OTP_TYPES)) {
- http_response_code(400);
- echo "type must be one of: " . SEND_OTP_TYPES;
- exit();
- }
- return $type;
-}
diff --git a/http/public/do/verify_otp.php b/http/public/do/verify_otp.php
deleted file mode 100644
index 7f86858..0000000
--- a/http/public/do/verify_otp.php
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-
-use JoeacNet\Http\Db;
-
-require_once __DIR__ . "/../../php/db.php";
-
-$db = Db\connectDb();
-
-$payload = json_decode(file_get_contents("php://input"), true);
-$guess = getGuessFromPayloadAndValidate($payload);
-$leniencySecs = getLeniencySecsFromPayloadAndValidate($payload);
-$userId = getUserIdFromPayloadAndValidate($payload);
-
-if (
- !Db\isOtpCorrect(
- db: $db,
- userId: $userId,
- guess: $guess,
- leniencySecs: $leniencySecs,
- )
-) {
- http_response_code(400);
- echo "OTP is not valid";
- exit();
-}
-Db\deleteAllOtpsForUser($db, $userId);
-$token = bin2hex(random_bytes(256));
-Db\insertSendEmailToken(db: $db, userId: $userId, token: $token);
-http_response_code(200);
-echo $token;
-exit();
-
-/// functions ///
-
-function getGuessFromPayloadAndValidate(array $payload): string
-{
- $guess = (string) $payload["guess"];
- if (strlen($guess) != 6) {
- http_response_code(400);
- echo "guess must be six characters long";
- exit();
- }
- return $guess;
-}
-
-function getLeniencySecsFromPayloadAndValidate(array $payload): int
-{
- if ((bool) $payload["lenient"] ?? false) {
- return 60;
- }
- return 0;
-}
-
-function getUserIdFromPayloadAndValidate(array $payload): string
-{
- $userId = (string) $payload["userId"];
- if (is_null($userId) || $userId == "") {
- http_response_code(400);
- echo "userId must not be empty";
- exit();
- }
- return $userId;
-}
diff --git a/http/public/images b/http/public/images
deleted file mode 120000
index ccc7a5b..0000000
--- a/http/public/images
+++ /dev/null
@@ -1 +0,0 @@
-../../common/images/ \ No newline at end of file
diff --git a/http/public/scripts/contact/actions.js b/http/public/scripts/contact/actions.js
deleted file mode 100644
index 5a7e32a..0000000
--- a/http/public/scripts/contact/actions.js
+++ /dev/null
@@ -1,60 +0,0 @@
-export const actions = {
- /**
- * @param {{type: 'email', name: string, email: string}}
- * @returns {Promise<void>}
- */
- sendOtp: async ({ type, name, email }) => {
- const url = "/do/send_otp.php";
- const req = new Request(url, {
- method: "POST",
- body: JSON.stringify({ type, name, email }),
- });
- const res = await fetch(req);
- if (!res.ok) {
- throw new Error(
- `Request to ${url} failed: ${res.status} ${res.statusText} ${await res.text()}`,
- );
- }
- },
-
- /**
- * @param {{guess: string, lenient?: bool, userId: string}}
- * @returns {Promise<string | false>}
- */
- verifyOtp: async ({ guess, lenient, userId }) => {
- const url = "/do/verify_otp.php";
- const req = new Request(url, {
- method: "POST",
- body: JSON.stringify({ guess, lenient, userId }),
- });
- const res = await fetch(req);
-
- if (res.status === 400) {
- return false;
- } else if (!res.ok) {
- throw new Error(
- `Request to ${url} failed: ${res.status} ${res.statusText} ${await res.text()}`,
- );
- }
-
- return res.text();
- },
-
- /**
- * @param {{email: string, message: string, name: string, userId: string, token: string}}
- * @returns {Promise<void>}
- */
- sendEmail: async ({ email, message, name, userId, token }) => {
- const url = "/do/send_email.php";
- const req = new Request(url, {
- method: "POST",
- body: JSON.stringify({ email, message, name, userId, token }),
- });
- const res = await fetch(req);
- if (!res.ok) {
- throw new Error(
- `Request to ${url} failed: ${res.status} ${res.statusText} ${await res.text()}`,
- );
- }
- },
-};
diff --git a/http/public/scripts/contact/post-error-message.js b/http/public/scripts/contact/post-error-message.js
deleted file mode 100644
index 121cbb6..0000000
--- a/http/public/scripts/contact/post-error-message.js
+++ /dev/null
@@ -1,34 +0,0 @@
-/** @typedef {import("./selectors.js").Selectors} Selectors */
-
-/**
- * @param {{contactForm: Selectors}} contactForm
- * @param {string} errorMsg
- */
-export function postErrorMessageOnContactForm({ contactForm }, errorMsg) {
- const errorElem = contactForm.errorElem();
- if (errorElem) {
- errorElem.textContent = errorMsg;
- errorElem.removeAttribute("hidden");
- } else {
- alert(errorMsg);
- }
-}
-
-/**
- * @param {{otpDialog: Selectors}} otpDialog
- * @param {string} errorMsg
- */
-export function postErrorMessageOnOtpForm({ otpDialog }, errorMsg) {
- const errorElem = otpDialog.errorElem();
- if (errorElem) {
- errorElem.textContent = errorMsg;
- errorElem.removeAttribute("hidden");
- } else {
- alert(errorMsg);
- }
- /** @type NodeListOf<HTMLInputElement> */
- const inputs = otpDialog.allOtpInputs();
- for (const input of inputs) {
- input.value = "";
- }
-}
diff --git a/http/public/scripts/contact/resend-otp.js b/http/public/scripts/contact/resend-otp.js
deleted file mode 100644
index e083d95..0000000
--- a/http/public/scripts/contact/resend-otp.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/** @typedef {import("./selectors.js").Selectors} Selectors */
-
-import { actions } from "./actions.js";
-import { resetResendButton } from "./reset-resend-button.js";
-import { postErrorMessageOnOtpForm } from "./post-error-message.js";
-
-/** @typedef {{ resendButtonInterval: NodeJS.Timeout | undefined }} Result */
-
-const fallbackErrorMsg =
- "No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
-
-/**
- * @param {Selectors} selectors
- * @param {NodeJS.Timeout | undefined} resetButtonInterval
- * @returns {Promise<Result>}
- */
-export async function resendOtp(selectors, resetButtonInterval) {
- const result = resetResendButton(selectors, resetButtonInterval);
- const name = selectors.contactForm.nameElem()?.value;
- const email = selectors.contactForm.emailElem().value;
-
- try {
- await actions.sendOtp({ type: "email", name, email });
- } catch (sendOtpError) {
- const errorMsg = sendOtpError?.toString() ?? fallbackErrorMsg;
- postErrorMessageOnOtpForm(selectors, errorMsg);
- throw sendOtpError;
- }
-
- return result;
-}
diff --git a/http/public/scripts/contact/reset-resend-button.js b/http/public/scripts/contact/reset-resend-button.js
deleted file mode 100644
index 47bc9ac..0000000
--- a/http/public/scripts/contact/reset-resend-button.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/** @typedef {import("./selectors.js").Selectors} Selectors */
-
-/** @typedef {{ resendButtonInterval: NodeJS.Timeout | undefined }} Result */
-
-/**
- * @param {Selectors} selectors
- * @param {NodeJS.Timeout | undefined} resetButtonInterval
- * @returns {Result}
- */
-export function resetResendButton({ otpDialog }, resendButtonInterval) {
- clearInterval(resendButtonInterval);
-
- const resendButton = otpDialog.resendButton();
- if (resendButton) {
- resendButton.setAttribute("data-countdown", "60");
- resendButton.setAttribute("disabled", "");
-
- resendButtonInterval = setInterval(() => {
- const countdown = +(resendButton.getAttribute("data-countdown") ?? 1) - 1;
- resendButton.setAttribute("data-countdown", countdown.toString());
- resendButton.textContent = `Resend (${countdown}s)`;
- }, 1000);
-
- setTimeout(() => {
- clearInterval(resendButtonInterval);
- resendButton.textContent = "Resend";
- resendButton.removeAttribute("disabled");
- }, 1000 * 60);
- }
-
- return { resendButtonInterval };
-}
diff --git a/http/public/scripts/contact/selectors.js b/http/public/scripts/contact/selectors.js
deleted file mode 100644
index 7676414..0000000
--- a/http/public/scripts/contact/selectors.js
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * @typedef {Object} Selectors
- * @property {Object} contactForm
- * @property {() => HTMLInputElement} contactForm.emailElem
- * @property {() => Element | null} contactForm.errorElem
- * @property {() => HTMLInputElement} contactForm.nameElem
- * @property {() => HTMLTextAreaElement} contactForm.messageElem
- * @property {() => HTMLFormElement | null} contactForm.self
- * @property {() => HTMLInputElement | null} contactForm.submitButton
- * @property {Object} otpDialog
- * @property {() => NodeListOf<HTMLInputElement>} otpDialog.allOtpInputs
- * @property {() => Element | null} otpDialog.errorElem
- * @property {() => HTMLInputElement | null} otpDialog.firstOtpInput
- * @property {() => HTMLFormElement | null} otpDialog.otpForm
- * @property {() => Element | null} otpDialog.otpRecipient
- * @property {() => Element | null} otpDialog.otpValidUntil
- * @property {() => HTMLButtonElement | null} otpDialog.resendButton
- * @property {() => HTMLDialogElement} otpDialog.self
- * @property {() => HTMLInputElement | null} otpDialog.submitButton
- * @property {Object} successSection
- * @property {() => Element | null} successSection.email
- * @property {() => Element | null} successSection.message
- * @property {() => Element | null} successSection.name
- * @property {() => Element | null} successSection.self
- */
diff --git a/http/public/scripts/contact/submit-contact-form.js b/http/public/scripts/contact/submit-contact-form.js
deleted file mode 100644
index a90f420..0000000
--- a/http/public/scripts/contact/submit-contact-form.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { actions } from "./actions.js";
-import { postErrorMessageOnContactForm } from "./post-error-message.js";
-import { resetResendButton } from "./reset-resend-button.js";
-
-/** @typedef {import("./selectors.js").Selectors} Selectors */
-
-const fallbackErrorMsg =
- "No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
-
-/**
- * @param {Selectors} selectors
- * @param {NodeJS.Timeout | undefined} resetButtonInterval
- * @returns {Promise<Result>}
- */
-export async function submitContactForm(selectors, resendButtonInterval) {
- const { contactForm, otpDialog } = selectors;
-
- const name = contactForm.nameElem()?.value;
- const email = contactForm.emailElem().value;
-
- contactForm.submitButton()?.setAttribute("disabled", "");
- try {
- await actions.sendOtp({ type: "email", name, email });
- } catch (sendOtpError) {
- const errorMsg = sendOtpError?.toString() ?? fallbackErrorMsg;
- postErrorMessageOnContactForm(selectors, errorMsg);
- throw sendOtpError;
- }
-
- const otpRecipient = otpDialog.otpRecipient();
- email && otpRecipient && (otpRecipient.textContent = `<${email}>`);
- const otpValidUntil = otpDialog.otpValidUntil();
- const validUntil = new Date(Date.now() + 1000 * 60 * 5);
- otpValidUntil &&
- (otpValidUntil.textContent = `until ${validUntil.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}`);
-
- const dialog = otpDialog.self();
- dialog.showModal();
- contactForm.submitButton()?.removeAttribute("disabled");
- dialog.addEventListener("click", function (event) {
- const rect = dialog.getBoundingClientRect();
- const isInDialog =
- rect.top <= event.clientY &&
- event.clientY <= rect.top + rect.height &&
- rect.left <= event.clientX &&
- event.clientX <= rect.left + rect.width;
- if (!isInDialog) {
- dialog.close();
- }
- });
-
- return resetResendButton(selectors, resendButtonInterval);
-}
diff --git a/http/public/scripts/contact/submit-otp-form.js b/http/public/scripts/contact/submit-otp-form.js
deleted file mode 100644
index 8c09314..0000000
--- a/http/public/scripts/contact/submit-otp-form.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { actions } from "./actions.js";
-import { postErrorMessageOnOtpForm } from "./post-error-message.js";
-
-/** @typedef {import("./selectors.js").Selectors} Selectors */
-
-const fallbackErrorMsg =
- "No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
-
-/**
- * @param {Selectors} selectors
- */
-export async function submitOtpForm(selectors) {
- const { contactForm, otpDialog, successSection } = selectors;
- const otpForm = otpDialog.otpForm();
- otpDialog.submitButton()?.setAttribute("disabled", "");
-
- const otpFormData = new FormData(otpForm ?? undefined);
- const guess = [
- otpFormData.get("1"),
- otpFormData.get("2"),
- otpFormData.get("3"),
- otpFormData.get("4"),
- otpFormData.get("5"),
- otpFormData.get("6"),
- ].join("");
-
- const name = contactForm.nameElem()?.value;
- const email = contactForm.emailElem().value;
- const message = contactForm.messageElem()?.value;
-
- let verifyResult;
- try {
- verifyResult = await actions.verifyOtp({ guess, userId: email });
- } catch (verifyError) {
- otpDialog.submitButton()?.removeAttribute("disabled");
- postErrorMessageOnOtpForm(
- selectors,
- verifyError?.toString() ?? fallbackErrorMsg,
- );
- return;
- }
-
- if (verifyResult === false) {
- otpDialog.submitButton()?.removeAttribute("disabled");
- postErrorMessageOnOtpForm(selectors, "Incorrect OTP. Check your email?");
- otpDialog.firstOtpInput()?.focus();
- return;
- }
- const sendmailToken = verifyResult;
-
- try {
- await actions.sendEmail({
- email,
- message: message,
- name: name,
- userId: email,
- token: sendmailToken,
- });
- } catch (sendEmailError) {
- const errorMsg = sendEmailError?.toString() ?? fallbackErrorMsg;
- postErrorMessageOnOtpForm(selectors, errorMsg);
- otpDialog.submitButton()?.removeAttribute("disabled");
- return;
- }
-
- const sentName = successSection.name();
- const sentEmail = successSection.email();
- const sentMessage = successSection.message();
- sentName && (sentName.textContent = name ?? "???");
- sentEmail && (sentEmail.textContent = email ?? "???");
- sentMessage && (sentMessage.textContent = message ?? "???");
-
- contactForm.self()?.remove();
- successSection.self()?.removeAttribute("hidden");
- otpDialog.submitButton()?.removeAttribute("disabled");
- otpDialog.self().close();
-}
diff --git a/http/public/scripts/otp-form-wc.js b/http/public/scripts/otp-form-wc.js
deleted file mode 100644
index 49e1b02..0000000
--- a/http/public/scripts/otp-form-wc.js
+++ /dev/null
@@ -1,68 +0,0 @@
-class OtpForm extends HTMLElement {
- /** @type MutationObserver? */
- observer;
-
- constructor() {
- super();
- }
-
- connectedCallback() {
- this.observer = new MutationObserver(() => {
- this.clearInputs();
- this.configureInputs();
- });
- this.observer.observe(this, { childList: true, subtree: true });
- }
-
- disconnectedCallback() {
- this.observer?.disconnect();
- }
-
- clearInputs() {
- console.log("clearing all inputs");
- /** @type NodeListOf<HTMLInputElement> */
- const inputs = this.querySelectorAll('input:not([type="submit"])');
- for (const input of inputs) {
- input.value = "";
- }
- }
-
- configureInputs() {
- this.observer?.disconnect();
- /** @type NodeListOf<HTMLInputElement> */
- const inputs = this.querySelectorAll('input:not([type="submit"])');
- /** @type NodeListOf<HTMLFormElement> */
- const form = this.querySelector("form");
-
- for (const input of inputs) {
- input.addEventListener("focus", () => {
- input.select();
- });
-
- input.addEventListener("input", () => {
- if (input.value.length > 0) {
- input.value = input.value.slice(0, 1).toLocaleUpperCase();
- /** @type HTMLInputElement */
- const nextInput = input.nextElementSibling;
- if (nextInput) {
- nextInput.focus();
- return;
- }
-
- /** @type HTMLInputElement */
- const submitButton = form.querySelector('input[type="submit"]');
- submitButton.focus();
- let areAllInputsEntered = true;
- inputs.forEach((input) => {
- areAllInputsEntered = areAllInputsEntered && input.value.length > 0;
- });
- if (areAllInputsEntered) {
- form.requestSubmit(submitButton);
- }
- }
- });
- }
- }
-}
-
-customElements.define("otp-form", OtpForm);
diff --git a/http/share/components/formatted_date.upphtml b/http/share/components/formatted_date.upphtml
deleted file mode 100644
index ba86181..0000000
--- a/http/share/components/formatted_date.upphtml
+++ /dev/null
@@ -1,4 +0,0 @@
-#!
-FORMATTED_DATE="$(date --date "${DATE}" +'%A %-d %B %Y')"
-echo "<time datetime='${DATE}' class='${CLASS}' ${ATTRIBUTES}>${FORMATTED_DATE}</time>"
-#!
diff --git a/http/share/components/head.upphtml b/http/share/components/head.upphtml
deleted file mode 100644
index 87728f1..0000000
--- a/http/share/components/head.upphtml
+++ /dev/null
@@ -1,37 +0,0 @@
-<!-- IndieWeb -->
-<link rel="authorization_endpoint" href="https://indieauth.com/auth">
-<link rel="token_endpoint" href="https://tokens.indieauth.com/token">
-<link rel="micropub" href="https://tasty-windows-lick.loca.lt">
-
-<!-- Stylesheets -->
-<link rel="stylesheet" href="/css/reset.css" />
-<link rel="stylesheet" href="/css/base.css" />
-<link rel="stylesheet" href="/css/hcard.css" />
-<link rel="stylesheet" href="/css/feed.css" />
-
-<!-- Global Metadata -->
-<meta charset="utf-8" />
-<meta name="viewport" content="width=device-width,initial-scale=1" />
-<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
-<meta name="generator" content="mkws" />
-
-<!-- Canonical URL -->
-<link rel="canonical" href={canonicalURL} />
-
-<!-- Open Graph / Facebook -->
-<meta property="og:type" content="website" />
-<meta property="og:url" content={Astro.url} />
-<meta property="og:title" content={title} />
-<meta property="og:description" content={description} />
-<meta property="og:image" content={new URL(image, Astro.url)} />
-
-<!-- Twitter -->
-<meta property="twitter:card" content="summary_large_image" />
-<meta property="twitter:url" content={Astro.url} />
-<meta property="twitter:title" content={title} />
-<meta property="twitter:description" content={description} />
-<meta property="twitter:image" content={new URL(image, Astro.url)} />
-
-<!-- Feeds -->
-<link rel="alternate" type="text/xml" title="Blog RSS" href="/blog/rss.xml">
-<link rel="alternate" type="text/xml" title="Links RSS" href="/links/rss.xml">
diff --git a/http/share/components/meta.upphtml b/http/share/components/meta.upphtml
deleted file mode 100644
index fbb0155..0000000
--- a/http/share/components/meta.upphtml
+++ /dev/null
@@ -1,11 +0,0 @@
-#!
-if [ -z "${NO_META}" ]; then
-#!
-
-<title>${TITLE}</title>
-<meta name='title' content='${TITLE}' />
-<meta name='description' content='${DESCRIPTION}' />
-
-#!
-fi
-#!
diff --git a/http/share/components/navbar.upphtml b/http/share/components/navbar.upphtml
deleted file mode 100644
index 777cb40..0000000
--- a/http/share/components/navbar.upphtml
+++ /dev/null
@@ -1,16 +0,0 @@
-<nav>
- <ul>
- <li>
- <a href="/">Home</a>
- </li>
- <li>
- <a href="/blog">Blog</a>
- </li>
- <li>
- <a href="/microlog">Microlog</a>
- </li>
- <li>
- <a href="/contact">Contact</a>
- </li>
- </ul>
-</nav>
diff --git a/http/share/components/otp-dialog.upphtml b/http/share/components/otp-dialog.upphtml
deleted file mode 100644
index a51333f..0000000
--- a/http/share/components/otp-dialog.upphtml
+++ /dev/null
@@ -1,28 +0,0 @@
-<script src="/scripts/otp-form-wc.js"></script>
-<link rel="stylesheet" href="/css/otp.css" />
-
-<dialog class="otp-dialog">
- <otp-form>
- <form class="otp-form" class="otp-form">
- <p>
- I've sent a six-digit code to <span class="otp-recipient">your email address</span>.
- Let me know what it is, so I can confirm this really is your email address. The
- code will be valid <span class="otp-valid-until">for five minutes</span>.
- </p>
-
- <p hidden class="error"/>
-
- <div class="otp-inputs">
- <input maxlength="1" name="1" required autocapitalize="characters" autocomplete="one-time-code" aria-label="First digit of one-time passcode" autofocus>
- <input maxlength="1" name="2" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Second digit of one-time passcode">
- <input maxlength="1" name="3" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Third digit of one-time passcode">
- <input maxlength="1" name="4" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Fourth digit of one-time passcode">
- <input maxlength="1" name="5" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Fifth digit of one-time passcode">
- <input maxlength="1" name="6" required autocapitalize="characters" autocomplete="one-time-code" aria-label="Sixth digit of one-time passcode">
- </div>
-
- <input type="submit" value="Verify">
- <button disabled class="resend-button">Resend (60s)</button>
- </form>
- </otp-form>
-</dialog>
diff --git a/http/share/layouts/blog.layout.upphtml b/http/share/layouts/blog.layout.upphtml
deleted file mode 100644
index ec6130c..0000000
--- a/http/share/layouts/blog.layout.upphtml
+++ /dev/null
@@ -1,66 +0,0 @@
-#!
-PAGE="$1"
-SITE_URL="$2"
-DATE_PUBLISHED="${DATE_PUBLISHED:-$(basename "${PAGE}" | cut -c1-10)}"
-CANONICAL_URL="${SITE_URL}/blog/$(basename "${PAGE}")"
-#!
-
-<!doctype html>
-<html lang="en-GB">
-
-<head>
-#!
-pp "${SHARE}"/components/head.upphtml
-#!
-
-<link rel="stylesheet" href="/css/post.css" />
-</head>
-
-<body>
-
-#!
-pp "${SHARE}"/components/navbar.upphtml
-#!
-
-<article class='h-entry'>
-
-<aside>
-<span>This is a <a href='/blog'>blog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
-#!
-if [ -z "${DATE_UPDATED}" ]; then
-#!
-<span>Published:
-#!
-DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
-#!
-.</span>
-#!
-else
-#!
-<span>Updated:
-#!
-DATE="${DATE_UPDATED}" CLASS="dt-updated" pp "${SHARE}/components/formatted_date.upphtml"
-#!
-.</span>
-<span>Originally published:
-#!
-DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
-#!
-.</span>
-#!
-fi
-#!
-<span hidden><a class='u-url uid' href='${CANONICAL_URL}'>Permalink</a></span>
-</aside>
-
-<header>
-<h1 class='h-name'>${POST_TITLE}</h1>
-</header>
-
-<section class='e-content'>
-#!
-pp "${PAGE}"
-#!
-</section>
-
-</article>
diff --git a/http/share/layouts/default.layout.upphtml b/http/share/layouts/default.layout.upphtml
deleted file mode 100644
index ba94c58..0000000
--- a/http/share/layouts/default.layout.upphtml
+++ /dev/null
@@ -1,20 +0,0 @@
-<!doctype html>
-<html lang="en-GB">
-
-<head>
-#!
-pp "${SHARE}"/components/head.upphtml
-#!
-</head>
-
-<body>
-#!
-pp "${SHARE}"/components/navbar.upphtml
-#!
-<main>
-#!
-pp "$1" "$2"
-#!
-</main>
-</body>
-</html>
diff --git a/http/share/layouts/microlog.layout.upphtml b/http/share/layouts/microlog.layout.upphtml
deleted file mode 100644
index 772db15..0000000
--- a/http/share/layouts/microlog.layout.upphtml
+++ /dev/null
@@ -1,51 +0,0 @@
-#!
-SRC="$1"
-SITE_URL="$2"
-DATE_PUBLISHED="$(basename "${SRC}" | cut -c1-10)"
-CANONICAL_URL="${SITE_URL}/microlog/$(basename "${SRC}")"
-#!
-
-<!doctype html>
-<html lang="en-GB">
-
-<head>
-#!
-pp "${SHARE}"/components/head.upphtml
-#!
-
-<link rel="stylesheet" href="/css/post.css" />
-</head>
-
-<body>
-
-#!
-pp "${SHARE}"/components/navbar.upphtml
-#!
-
-<article class="h-entry">
-
-<aside>
-<span>This is a <a href='/microlog'>microlog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
-<span>Published:
-#!
-DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
-#!
-.</span>
-<span hidden><a class='u-url uid' href='${CANONICAL_URL}'>Permalink</a></span>
-</aside>
-
-<header>
-<h1 class='h-name'>
-#!
-echo "${POST_TITLE}"
-#!
-</h1>
-</header>
-
-<section class="e-content">
-#!
-pp "${SRC}"
-#!
-</section>
-
-</article>
diff --git a/http/share/layouts/trivial.layout.upphtml b/http/share/layouts/trivial.layout.upphtml
deleted file mode 100644
index 897bfe0..0000000
--- a/http/share/layouts/trivial.layout.upphtml
+++ /dev/null
@@ -1,3 +0,0 @@
-#!
-pp $@
-#!
diff --git a/http/share/man/man1/lmt.1 b/http/share/man/man1/lmt.1
deleted file mode 100644
index 755d69b..0000000
--- a/http/share/man/man1/lmt.1
+++ /dev/null
@@ -1,55 +0,0 @@
-.Dd Sep 4, 2020
-.Dt lmt
-.Os
-.Sh NAME
-.Nm lmt
-.Nd last modification time
-.Sh SYNOPSIS
-.Nm lmt
-.Op Fl f Ar format
-.Ar file ...
-.Sh DESCRIPTION
-The
-.Nm
-utility prints the last modification time for the given
-.Ar file
-to standard output.
-The information displayed is obtained by calling
-.Xr lstat 2
-with the given
-argument and evaluating the returned structure.
-.Pp
-The options are as follows:
-.Bl -tag -width Ds
-.It Fl f Ar format
-The format in which to display the time.
-The format string may contain any of the conversion specifications
-described in the
-.Xr strftime 3
-manual page, as well as any arbitrary text.
-A newline
-.Pq Ql \en
-character is always output after the characters specified by
-the format string.
-The format string for the default display is:
-.Bd -literal -offset indent
-%a %b %e %H:%M:%S %Z %Y
-.Ed
-.El
-.Sh EXIT STATUS
-.Ex -std
-.Sh EXAMPLES
-Print last modification time for
-.Pa file1
-to standard output:
-.Pp
-.Dl $ lmt file1
-.Pp
-Print last modification time for
-.Pa file1
-to standard output in ISO 8601 format:
-.Pp
-.Dl $ lmt -f "%Y-%m-%dT%H:%M:%S%z" file1
-.Sh SEE ALSO
-.Xr stat 1
-.Xr strftime 3
diff --git a/http/share/man/man1/mkws.1 b/http/share/man/man1/mkws.1
deleted file mode 100644
index 5ab337d..0000000
--- a/http/share/man/man1/mkws.1
+++ /dev/null
@@ -1,91 +0,0 @@
-.Dd Dec 13, 2019
-.Dt mkws 1
-.Os
-.Sh NAME
-.Nm mkws
-.Nd make web site
-.Sh SYNOPSIS
-.Nm
-.Ar url
-.Op path
-.Sh DESCRIPTION
-The
-.Nm
-utility uses
-.Xr pp 1
-and the theme files in the
-.Ev MKWSTHEMEPATH
-directory
-to preprocess all files in
-.Op path
-in order to produce a static web site in the current directory. If no
-.Op path
-is specified, the current directory is used instead.
-.Pp
-The options are as follows:
-.Bl -tag -width Ds
-.It Ar url
-The url of the web site. It is used for generating
-.Pa sitemap.xml
-.
-.It Op path
-The path where the unpreprocessed files are located. If no
-.Op path
-is specified, the current directory is used instead.
-.El
-.Sh ENVIRONMENT
-.Bl -tag -width MKWSTHEMEDIR
-.It Ev MKWSTHEMEDIR
-The directory containing the theme files. See the
-.Em FILES
-section for the minimum required files for a theme. If no
-.Ev MKWSTHEMEDIR
-variable is set
-.Pa [path]/share
-wil be used.
-.It Ev LANG
-The language for the website. It will be parsed to fill in the
-.Em lang
-attribute of the
-.Em html
-element and then
-.Em charset
-.Em meta
-element.
-.El
-.Sh FILES
-.Bl -tag -width "/tmp/vi.recover
-.It Pa $MKWSTHEMEDIR/l.upphtml
-The layout file of the website. It contains the
-.Em DOCTYPE ,
-.Em html ,
-.Em head ,
-.Em title ,
-.Em meta
-,
-.Em link
-,
-.Em body
-elements.
-.It Pa $MKWSTHEMEDIR/sitemap.xml
-The
-.Pa sitemap.txt
-template.
-.El
-.Sh EXIT STATUS
-.Ex -std
-.Sh EXAMPLES
-Make https://example.com web site using the unpreprocessed files in the
-current directory
-.Pp
-.Dl $ mkws https://example.com
-.Pp
-Make https://example.com web site using the unpreprocessed files in the
-.Ar ~/src/ws
-directory
-.Pp
-.Dl $ mkws https://example.com ~/src/ws
-.Sh SEE ALSO
-.Xr sh 1
-.Xr pp 1
-.Xr locale 1
diff --git a/http/share/man/man1/pp.1 b/http/share/man/man1/pp.1
deleted file mode 120000
index 5f87379..0000000
--- a/http/share/man/man1/pp.1
+++ /dev/null
@@ -1 +0,0 @@
-../../../pp/pp.1 \ No newline at end of file
diff --git a/http/src/blog.upphtml b/http/src/blog.upphtml
deleted file mode 100644
index 8c0e429..0000000
--- a/http/src/blog.upphtml
+++ /dev/null
@@ -1,76 +0,0 @@
-#!
-PAGE="$1"
-SITE_URL="$2"
-export TITLE="Joe Carstairs"
-export DESCRIPTION="Joe Carstairs' personal website"
-
-POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\.blog.upphtml$")"
-set --
-for post in $POSTS
-do
- this_year="$( echo "$post" | cut -c1-4 )"
- has_this_year=0
- for year in $@
- do
- if [ "$this_year" = "$year" ]
- then
- has_this_year=1
- break
- fi
- done
- if [ $has_this_year -eq 0 ]
- then
- set -- $@ "$this_year"
- fi
-done
-YEARS="$@"
-
-pp "${SHARE}"/components/meta.upphtml
-#!
-
-<section class='h-feed'>
-
-<h1>My blog</h1>
-
-<aside>
-<p>This blog is written by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</p>
-<p hidden><a class='u-url' href='${SITE_URL}/blog'>Permalink</a></p>
-</aside>
-
-<section>
-<p><a href='/subscribe'>How to subscribe to my logs</a></p>
-</section>
-
-#!
-for year in $YEARS; do
-#!
-<h2>${year}</h2>
-
-<ul>
-
-#!
-for post in $POSTS; do if [ "$(echo "${post}" | cut -c1-4)" = "$year" ]; then
-title="$(grep "^export TITLE=" "${SRC}/blog/${post}" | cut -c15-)"
-title="$(echo ${title} | cut -c1-$(( ${#title} - 16 )) )"
-date_published="$(echo "${post}" | cut -c1-10)"
-href="${SITE_URL}/blog/$(echo "$post" | cut -c1-10).html"
-#!
-
-<li class='h-entry'>
-<h3><a class='u-url p-name' href='${href}'>${title}</a></h3>
-#!
-DATE="${date_published}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
-#!
-</li>
-
-#!
-fi; done
-#!
-
-</ul>
-
-#!
-done
-#!
-
-</section>
diff --git a/http/src/blog/rss.uppxml b/http/src/blog/rss.uppxml
deleted file mode 100644
index 78800aa..0000000
--- a/http/src/blog/rss.uppxml
+++ /dev/null
@@ -1,50 +0,0 @@
-#!
-SITE_URL="$1"
-POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.blog.upphtml$")"
-NOW_RFC_822="$(TZ=UT date +"%a, %d %b %Y %H:%M:%S UT")"
-#!
-
-<rss version='2.0'>
-
-<channel>
-<title>Joe Carstairs’ blog</title>
-<description>Short posts on random topics I find interesting</description>
-<link>${SITE_URL}/blog</link>
-<image>${SITE_URL}/images/headshot.webp</image>
-<language>en-GB</language>
-<pubDate>${NOW_RFC_822}</pubDate>
-
-#!
-for post in $POSTS; do
-POST_ID="$(echo ${post} | cut -c1-$(( ${#post} - 13 )) )"
-. "${SRC}/blog/${post}.args"
-DATE_PUBLISHED_RFC_822="$(TZ=UT date --date="$( echo "${POST_ID}" | cut -c1-10)" +"%a, %d %b %Y %H:%M:%S UT")"
-#!
-<item>
-
-<title>${POST_TITLE}</title>
-<link>${SITE_URL}/blog/${POST_ID}</link>
-<guid isPermaLink='true'>${SITE_URL}/blog/${POST_ID}</guid>
-<pubDate>${DATE_PUBLISHED_RFC_822}</pubDate>
-<description>
-#!
-NO_META=1 pp "${SRC}/blog/$post" "${SITE_URL}" \
- | sed "s/&/\\&amp;/g" \
- | sed "s/</\\&lt;/g" \
- | sed "s/>/\\&gt;/g" \
- | sed "s/\"/\\&quot;/g" \
- | sed "s/'/\\&#x27;/g" \
- | sed "s/\`/\\&#x60;/g" \
- | sed "s/\\\$/\\\\\$/g"
-#!
-</description>
-<author>Joe Carstairs</author>
-
-</item>
-#!
-done
-#!
-
-</channel>
-
-</rss>
diff --git a/http/src/blog/subscribe.trivial.upphtml b/http/src/blog/subscribe.trivial.upphtml
deleted file mode 100644
index e842234..0000000
--- a/http/src/blog/subscribe.trivial.upphtml
+++ /dev/null
@@ -1,4 +0,0 @@
-#!
-SITE_URL="${1}"
-#!
-<meta http-equiv=refresh content=0;url=${SITE_URL}/subscribe />
diff --git a/http/src/contact.upphtml b/http/src/contact.upphtml
deleted file mode 100644
index 1f425a6..0000000
--- a/http/src/contact.upphtml
+++ /dev/null
@@ -1,96 +0,0 @@
-#!
-export TITLE="Contact"
-export DESCRIPTION="Contact Joe Carstairs"
-pp "${SHARE}"/components/meta.upphtml
-#!
-
-#!
-pp "${SHARE}"/components/otp-dialog.upphtml
-#!
-
-<form class=contact-form>
- <h1>Contact me</h1>
- <p hidden class=error></p>
- <label for=name>Name</label>
- <input id=name name=name type=text required>
- <label for=email>Email</label>
- <input id=email name=email type=email required>
- <label for=message>Message</label>
- <textarea id=message name=message required></textarea>
- <input type=submit value=Send>
-</form>
-
-<section class=success hidden=true>
- <h1>You sent me a message!</h1>
- <p>Thanks for that. I may be in touch.</p>
- <p>In case you forgot, your message was this:</p>
- <hr />
- <dl>
- <dt>Name</dt> <dd class=sentname>???</dd>
- <dt>Email</dt> <dd class=sentemail>???</dd>
- <dt>Message</dt> <dd class=sentmessage>???</dd>
- </dl>
-</section>
-
-<script type=module>
- import { resendOtp } from '/scripts/contact/resend-otp.js';
- import { submitContactForm } from '/scripts/contact/submit-contact-form.js';
- import { submitOtpForm } from '/scripts/contact/submit-otp-form.js';
-
- function locateOrPanic(selector, desc, root) {
- const elem = (root ?? document).querySelector(selector);
- if (!elem) {
- alert('Technical error: could not locate ' + desc + '. Please let Joe know if you have another means of contacting him.');
- throw new Error('Could not locate ' + desc);
- }
- return elem;
- }
-
- const contactForm = locateOrPanic('form.contact-form', 'contact form');
- const otpDialog = locateOrPanic('dialog.otp-dialog', 'OTP dialog');
- const otpForm = locateOrPanic('form.otp-form', 'OTP form');
- const successSection = document.querySelector('section.success');
- let resendButtonInterval = undefined;
-
- const selectors = {
- contactForm: {
- emailElem: () => locateOrPanic('input[name=email]', 'email input', contactForm),
- errorElem: () => contactForm.querySelector('.error'),
- nameElem: () => locateOrPanic('input[name=name]', 'name input', contactForm),
- messageElem: () => locateOrPanic('textarea[name=message]', 'message textarea', contactForm),
- self: () => contactForm,
- submitButton: () => contactForm.querySelector('input[type=submit]'),
- },
- otpDialog: {
- allOtpInputs: () => otpForm.querySelectorAll('input:not([type=submit])'),
- errorElem: () => otpDialog.querySelector('.error'),
- firstOtpInput: () => otpForm.querySelector('input:not([type=submit]):first-child'),
- otpForm: () => otpForm,
- otpRecipient: () => otpDialog.querySelector('.otp-recipient'),
- otpValidUntil: () => otpDialog.querySelector('.otp-valid-until'),
- resendButton: () => otpDialog.querySelector('button.resend-button'),
- self: () => otpDialog,
- submitButton: () => otpForm.querySelector('input[type=submit]'),
- },
- successSection: {
- email: () => successSection?.querySelector('.sentemail') ?? null,
- name: () => successSection?.querySelector('.sentname') ?? null,
- message: () => successSection?.querySelector('.sentmessage') ?? null,
- self: () => successSection,
- }
- };
-
- contactForm.addEventListener('submit', async (event) => {
- event.preventDefault();
- ({ resendButtonInterval } = await submitContactForm(selectors, resendButtonInterval));
- });
-
- selectors.otpDialog.resendButton()?.addEventListener('click', async () => {
- ({ resendButtonInterval } = await resendOtp(selectors, resendButtonInterval));
- });
-
- otpForm.addEventListener('submit', async (event) => {
- event.preventDefault();
- await submitOtpForm(selectors);
- });
-</script>
diff --git a/http/src/css/base.uppcss b/http/src/css/base.uppcss
deleted file mode 100644
index b0e8565..0000000
--- a/http/src/css/base.uppcss
+++ /dev/null
@@ -1,515 +0,0 @@
-/** Variables */
-
-:root {
- --colour-primary-10: #060300;
- --colour-primary-15: #150800;
- --colour-primary-20: #1f1400;
- --colour-primary-30: #3c2b00;
- --colour-primary-40: #5c4300;
- --colour-primary-50: #7f5d00;
- --colour-primary-60: #a37800;
- --colour-primary-70: #c89500;
- --colour-primary-80: #efb300;
- --colour-primary-90: #ffd98c;
- --colour-primary-95: #ffecc8;
-
- --colour-hyperlink-10: #000409;
- --colour-hyperlink-20: #001829;
- --colour-hyperlink-30: #00314d;
- --colour-hyperlink-40: #004d75;
- --colour-hyperlink-50: #006a9f;
- --colour-hyperlink-60: #1388c9;
- --colour-hyperlink-70: #41a8ea;
- --colour-hyperlink-80: #78c7ff;
- --colour-hyperlink-90: #bfe3ff;
- --colour-hyperlink-95: #e0f1ff;
-
- --colour-grey-10: oklch(0.1 0.01 84);
- --colour-grey-20: oklch(0.2 0.01 84);
- --colour-grey-30: oklch(0.3 0.01 84);
- --colour-grey-40: oklch(0.4 0.01 84);
- --colour-grey-50: oklch(0.5 0.01 84);
- --colour-grey-60: oklch(0.6 0.01 84);
- --colour-grey-70: oklch(0.7 0.01 84);
- --colour-grey-80: oklch(0.8 0.01 84);
- --colour-grey-90: oklch(0.9 0.01 84);
- --colour-grey-95: oklch(0.95 0.01 84);
-
- --colour-error-10: oklch(0.1 0.2 26);
- --colour-error-20: oklch(0.2 0.2 26);
- --colour-error-30: oklch(0.3 0.2 26);
- --colour-error-40: oklch(0.4 0.2 26);
- --colour-error-50: oklch(0.5 0.2 26);
- --colour-error-60: oklch(0.6 0.2 26);
- --colour-error-70: oklch(0.7 0.2 26);
- --colour-error-80: oklch(0.8 0.2 26);
- --colour-error-90: oklch(0.9 0.2 26);
- --colour-error-95: oklch(0.95 0.2 26);
-
- --colour-warn-10: oklch(0.1 0.2 46);
- --colour-warn-20: oklch(0.2 0.2 46);
- --colour-warn-30: oklch(0.3 0.2 46);
- --colour-warn-40: oklch(0.4 0.2 46);
- --colour-warn-50: oklch(0.5 0.2 46);
- --colour-warn-60: oklch(0.6 0.2 46);
- --colour-warn-70: oklch(0.7 0.2 46);
- --colour-warn-80: oklch(0.8 0.2 46);
- --colour-warn-90: oklch(0.9 0.2 46);
- --colour-warn-95: oklch(0.95 0.2 46);
-
- --colour-primary-fg: var(--colour-primary-90);
- --colour-primary-fg-accent: var(--colour-primary-80);
- --colour-primary-bg: var(--colour-primary-10);
- --colour-primary-bg-accent: var(--colour-primary-20);
- --colour-code-fg: var(--colour-primary-90);
- --colour-code-bg: var(--colour-primary-15);
- --colour-hyperlink: var(--colour-hyperlink-80);
- --colour-grey-fg: var(--colour-grey-70);
- --colour-grey-bg: var(--colour-grey-30);
- --colour-error-fg: var(--colour-error-90);
- --colour-error-bg: var(--colour-error-40);
- --colour-warn-fg: var(--colour-warn-20);
- --colour-warn-bg: var(--colour-warn-80);
-
- --font-size-sm: 1rem;
- --font-size-base: 1.125rem;
- --font-size-md: 1.5rem;
- --font-size-lg: 2rem;
- --font-size-xl: 3rem;
-
- --spacing-block-xs: 0.5rem;
- --spacing-block-sm: 1.75rem;
- --spacing-block-md: 2.5rem;
- --spacing-block-lg: 3.5rem;
- --spacing-block-xl: 5rem;
- --spacing-inline-xs: 0.25rem;
- --spacing-inline-sm: 0.5rem;
- --spacing-inline-md: 1.5rem;
- --spacing-inline-lg: 3rem;
- --spacing-inline-xl: 6rem;
-}
-
-/** Light theme */
-@media (prefers-color-scheme: light) {
- :root {
- --colour-primary-fg: var(--colour-primary-20);
- --colour-primary-fg-accent: var(--colour-primary-40);
- --colour-primary-bg: var(--colour-primary-95);
- --colour-hyperlink: var(--colour-hyperlink-40);
- --colour-grey-fg: var(--colour-grey-40);
- --colour-grey-bg: var(--colour-grey-80);
- --colour-error-fg: var(--colour-error-20);
- --colour-error-bg: var(--colour-error-80);
- }
-}
-
-/** Base typography */
-
-body {
- font-size: var(--font-size-base);
- color: var(--colour-primary-fg);
- font-weight: light;
- background-color: var(--colour-primary-bg);
- line-height: 1.5;
-
- /* Geometric Humanist stack from https://modernfontstacks.com */
- font-family:
- Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif;
-}
-
-small {
- font-size: var(--font-size-sm);
-}
-
-:is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol, blockquote) {
- margin-block-start: var(--spacing-block-sm);
-}
-
-hr {
- height: 2px;
-}
-
-/** Base layout */
-
-body {
- --body-margin-inline-start: var(--spacing-inline-lg);
- --body-margin-inline-end: var(--body-margin-inline-start);
- --body-margin-block-end: var(--spacing-block-xl);
- margin-inline: var(--body-margin-inline-start) var(--body-margin-inline-end);
- margin-block-end: var(--body-margin-block-end);
-}
-
-:is(h1, h2, h3, h4, h5, h6) {
- text-align: center;
-}
-
-img {
- margin-inline: auto;
- height: auto;
-}
-
-@media (min-width: 60rem) {
- body {
- display: grid;
- grid-template-columns:
- [grid-start media-start]
- var(--grid-margin-inline)
- [media-end content-start]
- minmax(var(--grid-max-content-width), auto)
- [content-end grid-end];
- grid-auto-rows: max-content;
- column-gap: var(--spacing-block-sm);
- max-width: var(--grid-total-width);
-
- --body-margin-inline-end: 6rem;
- --grid-margin-inline: 6rem;
- --grid-total-width: 48rem;
- --grid-max-content-width: calc(
- var(--grid-total-width) - var(--body-margin-inline-start) -
- var(--grid-margin-inline) - var(--spacing-block-sm) -
- var(--grid-margin-inline)
- );
- }
-
- :is(main, article, nav) {
- display: grid;
- grid-column: grid;
- grid-template-columns: subgrid;
-
- > :is(section, header, aside, form) {
- display: grid;
- grid-template-columns: subgrid;
- grid-column: grid;
-
- > :not(.not-grid-content) {
- grid-column: content;
- }
- }
- }
-
- :is(h1, h2, h3, h4, h5, h6) {
- text-align: start;
- }
-}
-
-@media (min-width: 80rem) {
- body {
- grid-template-columns:
- [grid-start media-start]
- var(--grid-margin-inline)
- [media-end content-start]
- minmax(auto, var(--grid-max-content-width))
- [content-end margin-start]
- auto
- [margin-end grid-end];
-
- --grid-total-width: 80rem;
- --grid-max-content-width: 40rem;
- }
-}
-
-/** Headings */
-
-h1 {
- font-size: var(--font-size-xl);
- font-weight: 900;
- margin-block-start: 0;
-}
-
-h2 {
- font-size: var(--font-size-lg);
- font-weight: 900;
- margin-block-start: var(--spacing-block-xl);
-}
-
-h3,
-h4,
-h5,
-h6 {
- font-size: var(--font-size-md);
- font-weight: 600;
- margin-block-start: var(--spacing-block-lg);
-}
-
-/** Hyperlinks */
-
-a:is(:link, :visited) {
- color: var(--colour-hyperlink);
- text-decoration: underline;
- display: inline;
-}
-
-a:hover {
- text-decoration: wavy;
-}
-
-/** Definition lists */
-dl {
- margin-block-start: var(--spacing-block-md);
-}
-
-dl dt {
- font-weight: bold;
-}
-
-dl dd + dt {
- margin-block-start: var(--spacing-block-md);
-}
-
-/** figcaptions */
-
-figcaption {
- font-size: var(--font-size-sm);
- font-style: italic;
- text-align: center;
-}
-
-/** Lists */
-
-:is(ol, ul) {
- margin-inline-start: var(--spacing-inline-lg);
-}
-
-/** Navigation bar */
-
-nav {
- margin-block: var(--spacing-block-sm);
-}
-
-nav ul {
- grid-column: media-start / content-end;
- display: flex;
- justify-content: center;
- gap: var(--spacing-inline-md);
- list-style: none;
- margin: 0;
-}
-
-@media (min-width: 36rem) {
- nav {
- display: grid;
- grid-template-columns: subgrid;
- }
-
- nav ul {
- justify-content: end;
- }
-}
-
-/** Emphasis */
-
-strong {
- font-weight: bold;
- font-style: italic;
-}
-
-/** Blog feed */
-
-/** Block quotes */
-blockquote {
- padding-inline-start: var(--spacing-inline-lg);
- border-inline-start: 2px solid var(--colour-primary-fg);
- font-style: italic;
-}
-
-blockquote :first-child {
- margin-block-start: 0;
-}
-
-blockquote footer {
- font-style: initial;
-}
-
-blockquote :is(b, strong) {
- font-style: italic;
- font-weight: bold;
-}
-
-blockquote :is(i, em) {
- font-style: normal;
-}
-
-/** Small caps */
-.small-caps {
- font-variant: small-caps;
-}
-
-/** Pre-formatted blocks */
-pre {
- border: 2px solid var(--colour-primary-fg);
- border-radius: 2px;
- background-color: var(--colour-code-bg) !important;
- margin-block-start: var(--spacing-block-sm);
- padding-inline: var(--spacing-inline-sm);
- padding-block: var(--spacing-block-xs);
-}
-
-/** Code blocks */
-code {
- border: 2px solid var(--colour-primary-fg);
- border-radius: 2px;
- padding-inline: var(--spacing-inline-xs);
- color: var(--colour-code-fg);
- background-color: var(--colour-code-bg);
- font-size: var(--font-size-sm);
-}
-pre code {
- border: none;
- border-radius: none;
- padding: none;
-}
-
-/* verse */
-.verse {
- span {
- display: block;
- }
-}
-.verse--hanging-indents {
- span + span:not(.not-hanging) {
- margin-inline-start: var(--spacing-inline-md);
- }
-}
-
-/* block-comment */
-block-comment {
- display: grid;
- grid-template-columns: subgrid;
- grid-column: grid;
-
- > * {
- grid-column: content;
- }
-
- > blockquote:last-of-type {
- padding-block: var(--spacing-block-sm);
- padding-inline: var(--spacing-inline-md);
- overflow-y: scroll;
- border: 2px solid var(--colour-primary-fg);
- --colour-scroll-shadow: color-mix(
- in srgb,
- var(--colour-primary-fg),
- transparent 20%
- );
-
- background:
- linear-gradient(var(--colour-primary-bg) 30%, transparent) center top,
- linear-gradient(transparent, var(--colour-primary-bg) 70%) center bottom,
- radial-gradient(
- farthest-side at 50% 0,
- var(--colour-scroll-shadow),
- transparent
- )
- center top,
- radial-gradient(
- farthest-side at 50% 100%,
- var(--colour-scroll-shadow),
- transparent
- )
- center bottom;
- background-repeat: no-repeat;
- background-size:
- 100% 2rem,
- 100% 2rem,
- 100% 1rem,
- 100% 1rem;
- background-attachment: local, local, scroll, scroll;
-
- > :first-child {
- margin-block-start: 0;
- }
- }
-}
-
-@media (min-width: 80rem) {
- block-comment > blockquote:last-of-type {
- grid-column: margin;
- max-height: 67vh;
- }
-}
-
-/* forms */
-form {
- margin-inline: auto;
- max-width: max-content;
-
- :is(button, fieldset, input, label, object, output, select, textarea, img) {
- display: block;
- }
-
- label {
- margin-block-end: var(--spacing-block-xs);
- }
-
- * + :is(label, input[type='submit'], button) {
- margin-block-start: var(--spacing-block-md);
- }
-
- :is(input[type='submit'], input[type='button'], button) {
- padding-inline: var(--spacing-inline-sm);
- max-width: max-content;
- &:not(dialog *) {
- margin-inline-start: auto;
- }
- }
-
- :is(input[type='email'], input[type='text'], select) {
- max-width: 100%;
- width: 16rem;
- }
-
- textarea {
- max-width: 100%;
- width: 40rem;
- }
-}
-
-/* dialogs */
-
-dialog {
- background: var(--colour-primary-bg-accent);
- border: 2px solid var(--colour-primary-fg-accent);
- bottom: auto;
- color: var(--colour-primary-fg);
- left: calc(0.5 * (100vw - min(90vw, 36rem)));
- margin: 0;
- padding-block: var(--spacing-block-sm);
- padding-inline: var(--spacing-inline-sm);
- text-align: center;
- top: auto;
- width: min(90vw, 36rem);
-}
-
-@media (min-width: 80rem) {
- dialog {
- left: calc(
- var(--body-margin-inline-start) + var(--grid-margin-inline) + 2 *
- var(--spacing-inline-md)
- );
- }
-}
-
-/* utilities */
-:is(
- .para-spacing-tight:is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol),
- .para-spacing-tight :is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol)
-) {
- margin-block-start: var(--spacing-block-xs);
-}
-
-.error {
- background: var(--colour-error-bg);
- border: 2px solid var(--colour-error-fg);
- color: var(--colour-error-fg);
- padding-block: var(--spacing--block-sm);
- padding-inline: var(--spacing-inline-sm);
-}
-
-.warn {
- background: var(--colour-warn-bg);
- border: 2px solid var(--colour-warn-fg);
- color: var(--colour-warn-fg);
- padding-block: var(--spacing--block-sm);
- padding-inline: var(--spacing-inline-sm);
- --colour-hyperlink: var(--colour-hyperlink-40);
-}
diff --git a/http/src/css/feed.uppcss b/http/src/css/feed.uppcss
deleted file mode 100644
index 60fcd6d..0000000
--- a/http/src/css/feed.uppcss
+++ /dev/null
@@ -1,45 +0,0 @@
-/* Assumes there is at most one level of subheading for sub-dividing entries */
-.h-feed :is(h2, h3, h4, h5, h6) {
- margin-block-start: var(--spacing-block-md);
-}
-
-.h-feed ul:has(> .h-entry) {
- list-style-type: none;
- margin-inline: 0;
-
- ul {
- list-style-type: disc;
- }
-}
-
-.h-feed .h-entry {
- display: flex;
- flex-direction: column;
-}
-
-.h-feed .h-entry + .h-entry {
- margin-block-start: var(--spacing-block-md);
-}
-
-.h-feed .h-entry > * {
- order: 1;
- margin: 0;
-}
-
-.h-feed .h-entry .dt-published {
- order: 0;
- font-size: var(--font-size-sm);
-}
-
-.h-feed .h-entry .p-name {
- font-size: var(--font-size-md);
- font-weight: 400;
-}
-
-.h-feed .full-feed-link {
- text-align: end;
-}
-
-.h-feed :is(a.full-feed-link, .full-feed-link a)::after {
- content: ' >';
-}
diff --git a/http/src/css/hcard.uppcss b/http/src/css/hcard.uppcss
deleted file mode 100644
index 7908bc8..0000000
--- a/http/src/css/hcard.uppcss
+++ /dev/null
@@ -1,57 +0,0 @@
-.h-card div:has(img) {
- width: 6rem;
- height: 6rem;
- margin-inline: auto;
-}
-
-.h-card img {
- width: 6rem;
- height: 6rem;
- border-radius: 1rem;
- filter: contrast(1.25);
-}
-
-.h-card div:has(img)::after {
- /* Colour overlay */
- background-color: var(--colour-primary-80);
- opacity: 0.3;
-
- /* Same size and shape as the img */
- border-radius: 1rem;
- width: 6rem;
- height: 6rem;
-
- /* Positioned on top of the img */
- display: block;
- position: relative;
- top: -6rem;
-
- /* A content value is needed to get the ::after to render */
- content: '';
-}
-
-@media (min-width: 36rem) {
- .h-card {
- grid-column: media-start / content-end;
- display: grid;
- grid-template-columns: subgrid; /** Subgrid of main column layout */
- grid-template-rows: min-content 1fr;
- grid-template-areas:
- 'empty heading'
- 'photo text';
- column-gap: var(--spacing-block-sm);
- }
-
- .h-card div:has(img) {
- grid-area: photo;
- margin-block-start: var(--spacing-block-sm);
- }
-
- .h-card header {
- grid-area: heading;
- }
-
- .h-card__text {
- grid-area: text;
- }
-}
diff --git a/http/src/css/otp.uppcss b/http/src/css/otp.uppcss
deleted file mode 100644
index 47a74d3..0000000
--- a/http/src/css/otp.uppcss
+++ /dev/null
@@ -1,29 +0,0 @@
-.otp-inputs {
- display: flex;
- flex-direction: row;
- gap: var(--spacing-inline-sm);
- justify-content: center;
-
- input {
- border: none;
- border-block-end: 2px solid var(--colour-grey-fg);
- font-size: var(--font-size-lg);
- text-align: center;
- width: var(--font-size-lg);
- }
-}
-
-.otp-form {
- align-items: center;
- display: flex;
- flex-direction: column;
- gap: var(--spacing-block-sm);
-
- > * {
- margin: 0;
- }
-
- input[type='submit'] {
- max-width: max-content;
- }
-}
diff --git a/http/src/css/post.uppcss b/http/src/css/post.uppcss
deleted file mode 100644
index 436c13e..0000000
--- a/http/src/css/post.uppcss
+++ /dev/null
@@ -1,8 +0,0 @@
-.p-summary {
- font-style: italic;
- font-size: var(--font-size-sm);
-}
-
-h1 {
- margin-block-start: var(--spacing-block-md);
-}
diff --git a/http/src/css/reset.uppcss b/http/src/css/reset.uppcss
deleted file mode 100644
index 03df124..0000000
--- a/http/src/css/reset.uppcss
+++ /dev/null
@@ -1,93 +0,0 @@
-/* Based on Andy Bell’s More Modern CSS Reset: https://piccalil.li/blog/a-more-modern-css-reset/ */
-
-/* Box sizing rules */
-*,
-*::before,
-*::after {
- box-sizing: border-box;
-}
-
-/* Prevent font size inflation */
-html {
- -moz-text-size-adjust: none;
- -webkit-text-size-adjust: none;
- text-size-adjust: none;
-}
-
-/* Remove default margin in favour of better control in authored CSS */
-body,
-h1,
-h2,
-h3,
-h4,
-p,
-figure,
-blockquote,
-dl,
-dd {
- margin-block: 0;
-}
-
-/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
-ul[role='list'],
-ol[role='list'] {
- list-style: none;
-}
-
-/* Set core body defaults */
-body {
- min-height: 100vh;
-}
-
-/* Set shorter line heights on headings and interactive elements */
-h1,
-h2,
-h3,
-h4,
-button,
-input,
-label {
- line-height: 1.1;
-}
-
-/* Balance text wrapping on headings */
-h1,
-h2,
-h3,
-h4 {
- text-wrap: balance;
-}
-
-/* Make images easier to work with */
-img,
-picture {
- max-width: 100%;
- display: block;
-}
-
-/* Inherit fonts for inputs and buttons */
-input,
-button,
-textarea,
-select {
- font: inherit;
-}
-
-/* Make sure textareas without a rows attribute are not tiny */
-textarea:not([rows]) {
- min-height: 10em;
-}
-
-/* Anything that has been anchored to should have extra scroll margin */
-:target {
- scroll-margin-block: 5ex;
-}
-
-* {
- margin: 0;
- padding: 0;
-}
-
-[hidden] {
- display: none;
-}
diff --git a/http/src/error.upphtml b/http/src/error.upphtml
deleted file mode 100644
index 057006f..0000000
--- a/http/src/error.upphtml
+++ /dev/null
@@ -1,14 +0,0 @@
-#!
-export TITLE="Got lost?"
-export DESCRIPTION="Error page for Joe Carstairs' personal website"
-pp "${SHARE}"/components/meta.upphtml
-#!
-
-<section>
- <h1>Got lost?</h1>
-
- <p>
- If you’re on this page, something’s probably gone wrong. Try going to
- my <a href='/'>homepage</a> instead.
- </p>
-</section>
diff --git a/http/src/index.upphtml b/http/src/index.upphtml
deleted file mode 100644
index fec3774..0000000
--- a/http/src/index.upphtml
+++ /dev/null
@@ -1,47 +0,0 @@
-#!
-export TITLE="Joe Carstairs"
-export DESCRIPTION="Joe Carstairs' personal website"
-pp "${SHARE}"/components/meta.upphtml
-#!
-
-<section class="h-card">
- <div>
- <img class="u-photo" src="/images/headshot.webp" height="96" width="96" />
- </div>
-
- <header>
- <h1>Joe Carstairs</h1>
- <div hidden>
- <a class="p-name u-url u-uid" href="https://joeac.net" rel="me">
- Permalink
- </a>
- </div>
- </header>
-
- <div class="h-card__text">
- <p>
- Hi! 👋 My name is <span class="p-given-name">Joe</span>
- <span class="p-family-name">Carstairs</span>. I’m a
- <span class="p-job-title">software developer</span> at
- <a class="p-org" href="https://www.scottlogic.com">Scott Logic</a>,
- a Divinity student at the University of Edinburgh,
- a lapsed fiddle player and a very occasional poet.
- </p>
-
- <p>
- HMU with your thoughts on Elizabethan poetry, connecting to the
- Internet, and the Apocalypse of Saint John.
- </p>
-
- <p>
- <small>
- Or get me on
- <a href="https://www.facebook.com/joe.carstairs.5" rel="me">Facebook</a>,
- <a href="https://mastodon.social/@joe_carstairs" rel="me">Mastodon</a>,
- <a href="https://www.linkedin.com/in/joe-carstairs-0aa936277" rel="me">LinkedIn</a>,
- <a href="https://bsky.app/profile/joeacarstairs.bsky.social" rel="me">BlueSky</a>,
- or <a href="https://github.com/joeacarstairs" rel="me">GitHub</a>.
- </small>
- </p>
- </div>
-</section>
diff --git a/http/src/links/rss.trivial.upphtml b/http/src/links/rss.trivial.upphtml
deleted file mode 100644
index 52becef..0000000
--- a/http/src/links/rss.trivial.upphtml
+++ /dev/null
@@ -1,4 +0,0 @@
-#!
-SITE_URL="${1}"
-#!
-<meta http-equiv=refresh content=0;url=${SITE_URL}/microlog/rss.xml />
diff --git a/http/src/microlog.upphtml b/http/src/microlog.upphtml
deleted file mode 100644
index aa3cfea..0000000
--- a/http/src/microlog.upphtml
+++ /dev/null
@@ -1,84 +0,0 @@
-#!
-PAGE="$1"
-SITE_URL="$2"
-export TITLE="Joe Carstairs"
-export DESCRIPTION="Joe Carstairs' personal website"
-
-POSTS="$(ls --reverse "${SRC}/microlog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.microlog.upphtml$")"
-set --
-for post in $POSTS
-do
- this_year="$( echo "$post" | cut -c1-4 )"
- has_this_year=0
- for year in $@
- do
- if [ "$this_year" = "$year" ]
- then
- has_this_year=1
- break
- fi
- done
- if [ $has_this_year -eq 0 ]
- then
- set -- $@ "$this_year"
- fi
-done
-YEARS="$@"
-
-pp "${SHARE}"/components/meta.upphtml
-#!
-
-<main>
-
-<section class='h-feed'>
-
-<h1>My microlog</h1>
-
-<aside>
-<p>This microlog is written by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</p>
-<p hidden><a class='u-url' href='${SITE_URL}/microlog'>Permalink</a></p>
-</aside>
-
-<section>
-<p><a href='/subscribe'>How to subscribe to my logs</a></p>
-</section>
-
-#!
-for year in $YEARS; do
-#!
-<h2>${year}</h2>
-
-<ul>
-
-#!
-for post in $POSTS; do if [ "$(echo "${post}" | cut -c1-4)" = "$year" ]; then
-title="$(grep "^export TITLE=" "${SRC}/microlog/${post}" | cut -c15-)"
-title="$(echo "${title}" | cut -c1-$(( ${#title} - 16)) )"
-date_published="$(echo "${post}" | cut -c1-10)"
-href="${SITE_URL}/microlog/$(echo ${post} | cut -c1-$(( ${#post} - 17 )) ).html"
-#!
-
-<li class='h-entry'>
-<h3><a class='u-url p-name' href='${href}'>${title}</a></h3>
-<section class='e-content'>
-#!
-pp "${SRC}/microlog/${post}" "${SITE_URL}"
-#!
-</section>
-#!
-DATE="${date_published}" CLASS="dt-published" ATTRIBUTES="hidden" \
- pp "${SHARE}/components/formatted_date.upphtml"
-#!
-</li>
-
-#!
-fi; done
-#!
-
-</ul>
-
-#!
-done
-#!
-
-</section>
diff --git a/http/src/microlog/rss.uppxml b/http/src/microlog/rss.uppxml
deleted file mode 100644
index 20a6ced..0000000
--- a/http/src/microlog/rss.uppxml
+++ /dev/null
@@ -1,50 +0,0 @@
-#!
-SITE_URL="$1"
-POSTS="$(ls --reverse "${SRC}/microlog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.microlog.upphtml$")"
-NOW_RFC_822="$(TZ=UT date +"%a, %d %b %Y %H:%M:%S UT")"
-#!
-
-<rss version='2.0'>
-
-<channel>
-<title>Joe Carstairs’ microlog</title>
-<description>My private X feed I guess?</description>
-<link>${SITE_URL}/microlog</link>
-<image>${SITE_URL}/images/headshot.webp</image>
-<language>en-GB</language>
-<pubDate>${NOW_RFC_822}</pubDate>
-
-#!
-for post in $POSTS; do
-POST_ID="$(echo ${post} | cut -c1-$(( ${#post} - 17 )) )"
-. "${SRC}/microlog/${post}.args"
-DATE_PUBLISHED_RFC_822="$(TZ=UT date --date="$( echo "${POST_ID}" | cut -c1-10)" +"%a, %d %b %Y %H:%M:%S UT")"
-#!
-<item>
-
-<title>${POST_TITLE}</title>
-<link>${SITE_URL}/microlog/${POST_ID}</link>
-<guid isPermaLink='true'>${SITE_URL}/microlog/${POST_ID}</guid>
-<pubDate>${DATE_PUBLISHED_RFC_822}</pubDate>
-<description>
-#!
-NO_META=1 pp "${SRC}/microlog/$post" "${SITE_URL}" \
- | sed "s/&/\\&amp;/g" \
- | sed "s/</\\&lt;/g" \
- | sed "s/>/\\&gt;/g" \
- | sed "s/\"/\\&quot;/g" \
- | sed "s/'/\\&#x27;/g" \
- | sed "s/\`/\\&#x60;/g" \
- | sed "s/\\\$/\\\\\$/g"
-#!
-</description>
-<author>Joe Carstairs</author>
-
-</item>
-#!
-done
-#!
-
-</channel>
-
-</rss>
diff --git a/http/src/sitemap.uppxml b/http/src/sitemap.uppxml
deleted file mode 100644
index bfcf8d8..0000000
--- a/http/src/sitemap.uppxml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version='1.0' encoding='UTF-8'?>
-<urlset xmlns='http://www.sitemaps.org/schemas/sitemap/0.9'>
-#!
-for f in $(find "${OUT}" -name "*.html")
-do
-#!
-<url>
-#!
-loc=$1/$(basename "${f}")
-#!
-<loc>${loc}</loc>
-#!
-lastmod=$(lmt -f '%Y-%m-%dT%H:%M:%SZ' "${f}" | cut -d' ' -f1)
-#!
-<lastmod>${lastmod}</lastmod>
-<priority>1.0</priority>
-</url>
-#!
-done
-#!
-</urlset>
diff --git a/http/src/subscribe.upphtml b/http/src/subscribe.upphtml
deleted file mode 100644
index 606e5c5..0000000
--- a/http/src/subscribe.upphtml
+++ /dev/null
@@ -1,91 +0,0 @@
-#!
-PAGE="$1"
-SITE_URL="$2"
-export TITLE="How to subscribe to joeac's blog"
-export DESCRIPTION="How to subscribe to joeac's blog"
-#!
-
-<section>
-<h1>How to subscribe to my logs</h1>
-
-<p>
-The short version: use my <a href='/blog/rss.xml'>blog RSS feed</a> and
-<a href='/microlog/rss.xml'>microlog RSS feed</a>, or my
-<a href='gemini://joeac.net/logs/loglog.gmi'>gemlog</a>.
-</p>
-
-<p>
-If neither of those things means anything to you, no worries. You could just
-look at my website every once in a while, but that gets old. A better way is to
-use my RSS file. Here's how.
-</p>
-
-<p>
-An RSS file, if you don't know, is a file containing a list of posts in a format
-that's so stupid even computers can understand it. Mine is at
-<a href='https://joeac.net/rss.xml'>https://joeac.net/rss.xml</a>,
-and I've also got one for just my blog posts at
-<a href='https://joeac.net/blog/rss.xml'>https://joeac.net/blog/rss.xml</a>,
-and one just for microlog posts at
-<a href='https://joeac.net/microlog/rss.xml'>https://joeac.net/microlog/rss.xml</a>.
-Since computers can understand it, you can get software to read it automatically
-and tell you when I've made a new blog post. That's much less hassle than having
-to check manually yourself!
-</p>
-
-<p>
-I'm not the only person serving RSS files, of course. You might not have heard
-of them, but they're actually all over the Web. In the early days of the Web,
-everyone was using them, so they've ended up built in to a whole load of
-foundational Web technology, and hence ubiquitous, even if most Web users have
-forgotten they exist. Once you've subscribed to my logs, you can subscribe to
-basically any blog, podcast, or YouTube channel, and a lot of other Websites for
-news, organisations, whatever. Point a well-designed RSS aggregator or RSS
-reader at most Websites, and it'll automatically find an associated RSS feed.
-</p>
-
-<p>
-If you're not persuaded, try reading
-<a href='https://doctorow.medium.com/you-should-be-using-an-rss-reader-76aed31151f9'>
-Cory Doctorow explaining why RSS is ridiculously good
-</a>. He's terrifyingly persuasive. Also, read his novels!
-</p>
-
-<p>
-If you just want to give it a go quickly, probably the quickest way is to
-use someone else's RSS aggregator that they'll let you use as a service.
-<a href='https://feedly.com/news-reader'>Feedly</a> and
-<a href='https://www.inoreader.com'>Inoreader</a> have free tiers.
-</p>
-
-<p>
-If you want to have a little more control, you can self-host an RSS
-reader, like <a href='https://tt-rss.org'>Tiny Tiny RSS</a>,
-<a href='https://miniflux.app'>Miniflux</a>, or
-<a href='https://freshrss.org'>FreshRSS</a>. But why would you do that?
-Self-hosting is complicated! Why not just use a normal, local app? Here are
-some options for that you might want to investigate. All these are active
-free and open-source software projects at the time of writing.
-</p>
-
-<ul>
-<li><a href='https://hyliu.me/fluent-reader'>Fluent Reader</a> (Linux, Windows, Mac, also mobile I think?)</li>
-<li><a href='https://www.rssowl.org'>RSSOwl</a> (Linux, Windows, Mac)</li>
-<li><a href='https://capyreader.com'>Capy Reader</a> (Android)</li>
-<li><a href='https://apps.kde.org/en-gb/akregator'>Akregator</a> (Linux, Windows (unstable))</li>
-<li><a href='https://gfeeds.gabmus.org'>GFeeds</a> (Linux)</li>
-<li><a href='https://lzone.de/liferea'>Liferea</a> (Linux)</li>
-</ul>
-
-<p>
-For the terminally geeky, there is also
-<a href='https://newsboat.org'>Newsboat</a>. I used it for ages, it works
-fabulously, but it's all in the terminal, so you have to be OK with that.
-</p>
-
-<p>
-You can also just enter 'RSS feed reader' into your favourite search
-engine, software distribution, or app store and see what comes up. Try
-adding 'FOSS' into the query to prioritise free, open-source software.
-</p>
-</section>