diff --git a/http/.gitignore b/http/.gitignore
new file mode 100644
index 0000000..a48cf0d
--- /dev/null
+++ b/http/.gitignore
@@ -0,0 +1 @@
+public
diff --git a/http/bin/lmt b/http/bin/lmt
new file mode 100755
index 0000000..e0e19ea
Binary files /dev/null and b/http/bin/lmt differ
diff --git a/http/bin/mkws b/http/bin/mkws
new file mode 100755
index 0000000..252656c
--- /dev/null
+++ b/http/bin/mkws
@@ -0,0 +1,29 @@
+#!/bin/sh -e
+export PATH="$(dirname $(realpath $0)):$PATH"
+
+usage() {
+ >&2 printf "usage: %s url [path]\\n" "$(basename "$0")"
+ exit 1
+}
+
+test $# -lt 1 && usage
+
+export SRC="$(realpath src)"
+export SHARE="$(realpath share)"
+export OUT="$(realpath public)"
+
+if ! test -f "${SRC}"/index.upphtml
+then
+ >&2 printf "no index.upphtml file found\\n"
+ exit 1
+fi
+
+for t in "${SRC}"/*.upphtml
+do
+ echo "Making $(basename "${t%.upphtml}".html)"
+ pp "${SHARE}"/l.upphtml "${t}" "$1" > \
+ "${OUT}/$(basename "${t%.upphtml}".html)"
+done
+
+echo "Making sitemap.xml"
+pp "${SHARE}"/sitemap.uppxml "$1" > "${OUT}"/sitemap.xml
diff --git a/http/bin/pp b/http/bin/pp
new file mode 100755
index 0000000..0b89023
Binary files /dev/null and b/http/bin/pp differ
diff --git a/http/share/components/head.upphtml b/http/share/components/head.upphtml
new file mode 100644
index 0000000..e0e9391
--- /dev/null
+++ b/http/share/components/head.upphtml
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/http/share/css/base.uppcss b/http/share/css/base.uppcss
new file mode 100644
index 0000000..ea9bace
--- /dev/null
+++ b/http/share/css/base.uppcss
@@ -0,0 +1,516 @@
+/** 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 */
+
+.h-feed ul {
+ list-style: none;
+ margin-inline: 0;
+}
+
+/** Block quotes */
+blockquote {
+ padding-inline-start: var(--spacing-inline-lg);
+ border-inline-start: 2px solid var(--colour-primary-fg);
+ font-style: italic;
+}
+
+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/share/css/feed.uppcss b/http/share/css/feed.uppcss
new file mode 100644
index 0000000..39294be
--- /dev/null
+++ b/http/share/css/feed.uppcss
@@ -0,0 +1,36 @@
+/* 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 .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/share/css/hcard.uppcss b/http/share/css/hcard.uppcss
new file mode 100644
index 0000000..78f82b5
--- /dev/null
+++ b/http/share/css/hcard.uppcss
@@ -0,0 +1,57 @@
+.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/share/css/otp.uppcss b/http/share/css/otp.uppcss
new file mode 100644
index 0000000..4a7db02
--- /dev/null
+++ b/http/share/css/otp.uppcss
@@ -0,0 +1,29 @@
+.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/share/css/post.uppcss b/http/share/css/post.uppcss
new file mode 100644
index 0000000..436c13e
--- /dev/null
+++ b/http/share/css/post.uppcss
@@ -0,0 +1,8 @@
+.p-summary {
+ font-style: italic;
+ font-size: var(--font-size-sm);
+}
+
+h1 {
+ margin-block-start: var(--spacing-block-md);
+}
diff --git a/http/share/css/reset.uppcss b/http/share/css/reset.uppcss
new file mode 100644
index 0000000..00deeb1
--- /dev/null
+++ b/http/share/css/reset.uppcss
@@ -0,0 +1,93 @@
+/* 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/share/l.upphtml b/http/share/l.upphtml
new file mode 100644
index 0000000..30cdba4
--- /dev/null
+++ b/http/share/l.upphtml
@@ -0,0 +1,17 @@
+
+
+
+
+#!
+pp "${SHARE}"/components/head.upphtml
+#!
+
+
+
+
+#!
+pp "$1"
+#!
+
+
+
diff --git a/http/share/man/man1/lmt.1 b/http/share/man/man1/lmt.1
new file mode 100644
index 0000000..755d69b
--- /dev/null
+++ b/http/share/man/man1/lmt.1
@@ -0,0 +1,55 @@
+.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
new file mode 100644
index 0000000..5ab337d
--- /dev/null
+++ b/http/share/man/man1/mkws.1
@@ -0,0 +1,91 @@
+.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
new file mode 100644
index 0000000..a552c97
--- /dev/null
+++ b/http/share/man/man1/pp.1
@@ -0,0 +1,46 @@
+.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/share/sitemap.uppxml b/http/share/sitemap.uppxml
new file mode 100644
index 0000000..ba5ee48
--- /dev/null
+++ b/http/share/sitemap.uppxml
@@ -0,0 +1,21 @@
+
+
+#!
+for f in ${OUT}/*.html
+do
+#!
+
+#!
+loc=$1/$(basename "${f}")
+#!
+${loc}
+#!
+lastmod=$(lmt -f '%Y-%m-%dT%H:%M:%SZ' "${f}" | cut -d' ' -f1)
+#!
+${lastmod}
+1.0
+
+#!
+done
+#!
+
diff --git a/http/src/index.upphtml b/http/src/index.upphtml
new file mode 100644
index 0000000..4b5fa63
--- /dev/null
+++ b/http/src/index.upphtml
@@ -0,0 +1 @@
+hello, world