Compare commits

...

76 Commits

Author SHA1 Message Date
beb9a26512 updates README with new Podman instructions 2026-01-09 10:01:42 +00:00
00f793c53b Revert "WIP sign on contact form"
This reverts commit 9f85d2f1b3.
2026-01-09 09:27:36 +00:00
9f85d2f1b3 WIP sign on contact form 2026-01-08 22:30:25 +00:00
d8d2e94008 does nothing when compiling image assets 2026-01-08 22:16:58 +00:00
d9824e60a5 Revert "manually installs sharp"
This reverts commit 3966da7bc5.
2026-01-08 22:16:11 +00:00
3966da7bc5 manually installs sharp 2026-01-08 22:05:03 +00:00
48ca4c2a03 fixes SMTP client config in website 2026-01-08 21:13:38 +00:00
bde6f2a253 fixes env and args in website Dockerfile 2026-01-08 21:13:09 +00:00
8e00726b04 remove TODO comment block 2026-01-08 21:12:16 +00:00
e0170e82aa fixes SMTP server 2026-01-08 21:10:08 +00:00
64f2092161 website uses smtp 2025-12-18 21:08:04 +00:00
ba4b4ea980 package-lock.json updated from move 2025-12-18 21:07:49 +00:00
6592d49165 moves tsconfig.json to website/ 2025-12-18 21:06:48 +00:00
2fdf12259c add smtp service 2025-12-18 20:52:19 +00:00
e568105b99 website image uses alpine linux 2025-12-18 17:01:26 +00:00
a1304c5afd website image uses alpine linux 2025-12-18 16:58:36 +00:00
c9f7ad699c infra is not on DigitalOcean 2025-12-18 16:41:57 +00:00
20effac610 add compose.yaml 2025-12-18 16:41:00 +00:00
9c50e74904 Move website root to website/ 2025-12-18 16:29:06 +00:00
b0be9fae2f improve form styles in dialog 2025-12-18 13:13:48 +00:00
8f95c6ff74 WIP: Dockerise with sendmail 2025-12-18 13:13:34 +00:00
9d8d2a266a add !package-* to dockerignore 2025-12-18 13:13:12 +00:00
6c268a5548 ignore SQLite files 2025-12-18 11:28:18 +00:00
50f4d52317 dockerignore .env files 2025-12-18 11:06:13 +00:00
46c9b77316 contact page 2025-12-18 11:04:03 +00:00
40d6c7f248 otp actions 2025-12-18 11:03:32 +00:00
a81d1de1e5 sendmail action 2025-12-18 11:03:12 +00:00
46387b41ce updates styles 2025-12-18 11:00:24 +00:00
476fe39f50 define OtpDialog 2025-12-18 10:59:46 +00:00
a85b7b36c6 adds SentEmails and SendmailToken tables to DB 2025-12-18 10:59:15 +00:00
1d83c50e27 adds MAX_DAILY_EMAILS env var 2025-12-18 10:58:52 +00:00
fcb297637d adds --remote to build command 2025-12-18 10:58:34 +00:00
781af6414e resets hidden things to display:none 2025-12-16 16:57:49 +00:00
35f391d933 forms are sections for layout purposes 2025-12-16 16:57:36 +00:00
22ce9d06e2 hr is 2px high 2025-12-16 16:57:22 +00:00
9d610b6b3d reformat base.css 2025-12-16 16:57:13 +00:00
0da1f8710e adds SENDMAIL_BIN env var 2025-12-16 16:56:36 +00:00
e119eb62e8 install @types/nodemailer 2025-12-16 14:22:37 +00:00
9dab11d615 remove housewarming notice 2025-12-16 13:17:37 +00:00
fe69e75001 install nodemailer 2025-12-16 13:15:46 +00:00
508db104f2 update package-lock.json 2025-12-16 13:14:23 +00:00
e0fad33706 Revert "very"
This reverts commit ff8201c4f3.
2025-12-15 22:55:24 +00:00
4ac7f6af8e Merge branch 'main' of https://git.joeac.net/joeac/joeac.net 2025-12-15 22:29:49 +00:00
ff8201c4f3 very 2025-12-15 22:29:43 +00:00
b0161b6ddc remove rpi.sh 2025-12-15 20:52:16 +00:00
9458062b77 update bio 2025-12-15 20:50:17 +00:00
1b1d0eddf8 rpi.sh 2025-12-13 21:56:00 +00:00
7300b15421 rpi.sh 2025-12-13 20:38:40 +00:00
fa8948da13 run instructions 2025-12-12 14:47:04 +00:00
ed6a6428a3 Dockerise 2025-12-12 14:13:25 +00:00
e5cc05519a node.js adapter 2025-12-12 14:13:16 +00:00
a49b62433e tidy gitignore 2025-12-12 14:13:02 +00:00
cc9a580fe6 style changes 2025-12-11 14:22:12 +00:00
a3d0ee0438 edit persecution 2025-12-11 13:43:17 +00:00
af27e03ba5 persecution 2025-12-11 13:18:33 +00:00
aad9e56e4c arius 2025-10-09 21:15:47 +01:00
261f7d4f5a creeds 2025-10-05 22:04:31 +01:00
b5e58b279a creeds 2025-09-24 21:14:41 +01:00
b83c5d1cf8 starting_msc 2025-09-18 19:46:24 +01:00
65955287d5 node: ^22.14.0 2025-07-17 07:09:46 +01:00
a416336039 block-comment commentary is aligned to top 2025-07-05 20:57:26 +01:00
c510415b64 style improvements, block-comments 2025-07-04 22:02:46 +01:00
c9161a750c ps118 2025-07-03 22:41:12 +01:00
938f5e6d89 work 2025-06-23 08:27:16 +01:00
7d993bc96d does resurrection ground works 2025-05-04 10:08:21 +01:00
c4d8835927 update surprised by hope 2025-05-04 08:50:34 +01:00
dc850cbcbb Formats markdown line length with Prettier 2025-05-04 08:44:25 +01:00
0f88e3e68f Surprised by Hope 2025-05-03 20:47:39 +01:00
a54096a733 temp localtunnel micropub url 2025-02-16 17:02:34 +00:00
b4066f954f drinking 2025-02-16 10:43:50 +00:00
d2248853e7 indieweb links 2025-02-16 08:38:46 +00:00
fd3c8f0383 Free our feeds 2025-01-28 20:36:53 +00:00
da5263d4b2 Merge branch 'main' of https://github.com/joeacarstairs/personal-website 2025-01-28 19:58:48 +00:00
ac98d67a2d should 2025-01-28 19:58:44 +00:00
Joe Carstairs
ed52216fb8 Merge branch 'main' of https://github.com/joeacarstairs/personal-website 2025-01-28 18:06:53 +00:00
Joe Carstairs
17aa658fb8 Removes CV 2025-01-28 18:06:47 +00:00
62 changed files with 7801 additions and 2958 deletions

7
.editorconfig Normal file
View File

@@ -0,0 +1,7 @@
root = true
[*]
indent_size = 2
[*.{md,markdown,mdx}]
max_line_length = 80

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ pnpm-debug.log*
**/*.tfstate **/*.tfstate
**/*.tfstate.backup **/*.tfstate.backup
*.sqlite

1
.prettierrc Normal file
View File

@@ -0,0 +1 @@
proseWrap: always

View File

@@ -3,5 +3,6 @@
// For a full list of overridable settings, and general information on folder-specific settings, // For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files // see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{ {
"soft_wrap": "editor_width" "soft_wrap": "editor_width",
"format_on_save": "on"
} }

View File

@@ -4,24 +4,41 @@ Joe Carstairs' personal website
Structure: Structure:
├website: My public-facing website ```
└infrastructure: The infrastructure of my website as code /
├── website: My public-facing website
└── infrastructure: The infrastructure of my website as code
```
## Infrastructure ## Running with Podman
The infrastructure is on DigitalOcean. These instructions will probably work with Docker, too: just substitute `podman`
for `docker` in all the commands.
The website is hosted using the App Platform service from DigitalOcean. This is To run with Podman, first set up your environment variables. Copy `example.env`
free for static websites, and is quite flexible to add in extra apps as Droplets to `.env` and edit the values accordingly.
or Functions at a later time if I so please.
DigitalOcean App Platform re-deploys the website every time there's an update to Then, create the `remote_smtp_password` secret, storing the password for the
the `main` branch in this repo. remote SMTP server which will send the contact emails on behalf of the website.
All the DigitalOcean infrastructure is managed using Terraform. The code for ```bash
this is in the `infrastructure/` directory. sudo podman secret create remote_smtp_password /path/to/remote/smtp/password
```
The domain, however, is registered on AWS. The nameservers registered in AWS Now build and start the containers:
have to be kept manually up-to-date with the DigitalOcean nameservers. These
shouldn't change, though, so this is unlikely to need intervention more than ```bash
once. sudo podman-compose build && sudo podman-compose up -d
```
## Running on the host machine
To run on the host machine, first, as before, set up your environment variables
by copying `example.env` to `.env` and editing the values as appropriate.
```bash
npm run start
```
Note that emails may not work locally without further setup. These instructions
are of course woefully incomplete.

30
compose.yml Normal file
View File

@@ -0,0 +1,30 @@
services:
website:
build:
context: website
env_file:
- .env
environment:
LOCAL_SMTP_HOST: smtp
LOCAL_SMTP_PORT: 2500
LOCAL_SMTP_PASSWORD: smtp
ports:
- "8000:4321"
smtp:
build:
context: smtp
args:
LOCAL_SMTP_PORT: 2500
env_file:
- .env
environment:
LOCAL_SMTP_PORT: 2500
LOCAL_SMTP_PASSWORD: smtp
REMOTE_SMTP_PASSWORD_FILE: /run/secrets/remote_smtp_password
secrets:
- remote_smtp_password
secrets:
remote_smtp_password:
external: true

20
example.env Executable file
View File

@@ -0,0 +1,20 @@
# The username for authenticating to the local SMTP server
# Recommended to be the same as REMOTE_SMTP_USER, else the remote may reject it
LOCAL_SMTP_USER=
# The envelope-from used by the local SMTP server
# Recommended to be the same as REMOTE_SMTP_USER, else the remote may reject it
LOCAL_SMTP_ENVELOPE_FROM=
# The host of the remote SMTP server, e.g. smtp.gmail.com
REMOTE_SMTP_HOST=
# The port of the remote SMTP server: usually 25, 465, or 587
REMOTE_SMTP_PORT=
# The username for authenticating to the remote SMTP server
# Usually the email address which will be sending the contact emails
REMOTE_SMTP_USER=me@joeac.net
# The email address where contact emails will be sent
CONTACT_MAILBOX=me@joeac.net

View File

@@ -1,26 +0,0 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/digitalocean/digitalocean" {
version = "2.38.0"
constraints = "~> 2.0"
hashes = [
"h1:ElG5GAiHN6paMNUG0JnQ4uCv1Y37ZUbCXQSAQwb/j8U=",
"zh:04d1ca6ac6d7e69635657aeac8aadb75f84018305514381f9d7bed48065df61b",
"zh:081258f8526b3597eeb7154d5b453c2fe36194ca1a95e0de655d7a1080224be0",
"zh:15e77088b8a73012d87ad29ad3cf10392642a6781a35c0923a45386fe61cda61",
"zh:1a0c741f9be0f22c18fec93b4200c1968c1c6e4ac02292d494ace78b16f13fea",
"zh:2a3a778cd982e5e09a2414f0a16c54101c56d7ab6f824ea3e55709a608f7f3ea",
"zh:6832b594a7e408e085bf148c5a1e2eaf94ed8f47796b917acf56078ee9362ca1",
"zh:78b8acad99e7035344677f70c08c3daf457cfa3f8b467e73352d14353889b8cf",
"zh:8a1c446f1b3b5dee097fc000cf4d341b00602ace60246af4a09e5a49666a0638",
"zh:8c23094d06f5b4c780dbdde21c56b9b03b275c1cc8728669ef8f3f45e0d9fb24",
"zh:8c5a9871c2a2e094f58723624bb3fd2dbedafc8d9cab609df79d01011de79b8a",
"zh:a32adff1d0fc405f1596b6dc679b3029c87177e717eb991c418fff13ec559427",
"zh:a3f89176401db06ccc97b89a23bb004c8ab1562ce04178d6a08125bd4b8c073e",
"zh:d739de876cdd6176570c610e0edca0d2133b8e21787f22caa9a72d9806055c07",
"zh:d8a874a0e442c10deae3217bb88375209d45f79d52cb7d2b19756863d6ab6414",
"zh:eac47cd0b28953404622d11d8d4049ce2a4da3bc0a0ecaf386e5f55117386bbd",
"zh:ec3686163d1177d41f13588bb24db9a2b3afd199ac410d8c9899f053f271515c",
]
}

View File

@@ -1,53 +0,0 @@
resource "digitalocean_app" "joeac_net" {
project_id = "c106269c-1115-4682-8757-867368e057a4"
spec {
name = "joeac-net"
region = local.region
domain {
name = local.domain
}
features = [
"buildpack-stack=ubuntu-22"
]
alert {
disabled = false
rule = "DEPLOYMENT_FAILED"
}
alert {
disabled = false
rule = "DOMAIN_FAILED"
}
ingress {
rule {
component {
name = "personal-website"
preserve_path_prefix = false
}
match {
path {
prefix = "/"
}
}
}
}
static_site {
build_command = "npm run build"
environment_slug = "node-js"
name = "personal-website"
output_dir = "website/dist"
source_dir = "/"
github {
branch = "main"
deploy_on_push = true
repo = "joeacarstairs/personal-website"
}
}
}
}

View File

@@ -1,5 +0,0 @@
locals {
domain = "joeac.net"
region = "lon"
}

View File

@@ -1,9 +0,0 @@
terraform {
required_providers {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.0"
}
}
}

View File

@@ -1,4 +0,0 @@
provider "digitalocean" {
token = var.do_token
}

View File

@@ -1,5 +0,0 @@
variable "do_token" {
type = string
sensitive = true
description = "A DigitalOcean access token. Can also be provided as an environment variable"
}

View File

@@ -1,21 +0,0 @@
{
"private": "true",
"scripts": {
"dev": "cd website && astro dev",
"start": "cd website && astro dev",
"build": "cd website && astro build",
"preview": "cd cd website && webiste && astro preview",
"astro": "cd website && astro"
},
"dependencies": {
"@astrojs/rss": "^4.0.10",
"@astrojs/sitemap": "^3.2.1",
"astro": "^5.1.1",
"markdown-it": "^14.1.0",
"typescript": "^5.4.3"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@types/markdown-it": "^14.1.1"
}
}

12
smtp/.msmtprc Normal file
View File

@@ -0,0 +1,12 @@
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/msmtp/msmtp.log
account default
eval echo from "$LOCAL_SMTP_ENVELOPE_FROM"
eval echo host "$REMOTE_SMTP_HOST"
eval echo port "$REMOTE_SMTP_PORT"
eval echo user "$REMOTE_SMTP_USER"
passwordeval cat "$REMOTE_SMTP_PASSWORD_FILE"

23
smtp/Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
FROM alpine:3.22
WORKDIR /
RUN mkdir -p /var/msmtp
RUN apk --update --no-cache add git autoconf automake build-base gettext gettext-dev gnutls-dev libtool make texinfo && \
git clone https://github.com/marlam/msmtp.git --branch msmtp-1.8.32 --single-branch --depth 1
WORKDIR /msmtp
RUN autoreconf -fi && \
./configure && \
make && \
make install
ARG LOCAL_SMTP_PORT
EXPOSE $LOCAL_SMTP_PORT
COPY .msmtprc ./
CMD msmtpd \
--auth=$LOCAL_SMTP_USER,'echo $LOCAL_SMTP_PASSWORD' \
--command='msmtp -C .msmtprc -f %F --' \
--interface=0.0.0.0 \
--log=/var/msmtp/msmtpd.log \
--port=$LOCAL_SMTP_PORT

7
website/.dockerignore Normal file
View File

@@ -0,0 +1,7 @@
.astro/
dist/
node_modules/
.dockerignore
.env
*.sqlite
Dockerfile

22
website/Dockerfile Normal file
View File

@@ -0,0 +1,22 @@
FROM node:lts-alpine3.22 AS runtime
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY astro.config.mjs ./
COPY db ./db/
ARG DB_URL=file:/app/db.sqlite
ENV ASTRO_DB_REMOTE_URL=$DB_URL
RUN mkdir -p "$(dirname "$(echo "$ASTRO_DB_REMOTE_URL" | cut -d':' -f 2)")"
RUN npm run astro db push
COPY . .
RUN npm run build
ENV HOST=0.0.0.0
ENV PORT=4321
ENV MAX_DAILY_EMAILS=100
EXPOSE 4321
CMD ["node", "./dist/server/entry.mjs"]

View File

@@ -1,9 +1,38 @@
import { defineConfig } from 'astro/config'; import { defineConfig, envField, passthroughImageService } from "astro/config";
import db from "@astrojs/db";
import mdx from "@astrojs/mdx";
import node from "@astrojs/node";
import sitemap from '@astrojs/sitemap'; import sitemap from "@astrojs/sitemap";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
site: 'https://joeac.net', adapter: node({
integrations: [sitemap()], mode: "standalone",
}),
env: {
schema: {
MAX_DAILY_EMAILS: envField.number({
context: "server",
access: "secret",
}),
LOCAL_SMTP_ENVELOPE_FROM: envField.string({
context: "server",
access: "secret",
}),
LOCAL_SMTP_HOST: envField.string({ context: "server", access: "secret" }),
LOCAL_SMTP_PORT: envField.number({ context: "server", access: "secret" }),
LOCAL_SMTP_USER: envField.string({ context: "server", access: "secret" }),
LOCAL_SMTP_PASSWORD: envField.string({
context: "server",
access: "secret",
}),
CONTACT_MAILBOX: envField.string({ context: "server", access: "secret" }),
},
},
image: {
service: passthroughImageService(),
},
site: "https://joeac.net",
integrations: [db(), mdx(), sitemap()],
}); });

34
website/db/config.ts Normal file
View File

@@ -0,0 +1,34 @@
import { column, defineDb, defineTable } from "astro:db";
const Otp = defineTable({
columns: {
userId: column.text(),
value: column.text(),
createdAt: column.number(),
validUntil: column.number(),
},
});
const SendmailToken = defineTable({
columns: {
userId: column.text(),
value: column.text(),
createdAt: column.number(),
validUntil: column.number(),
},
});
const SentEmails = defineTable({
columns: {
messageId: column.text(),
sentAt: column.number(),
},
});
export default defineDb({
tables: {
Otp,
SendmailToken,
SentEmails,
},
});

3
website/db/seed.ts Normal file
View File

@@ -0,0 +1,3 @@
import { db } from "astro:db";
export default async function seed() {}

File diff suppressed because it is too large Load Diff

29
website/package.json Normal file
View File

@@ -0,0 +1,29 @@
{
"private": "true",
"scripts": {
"dev": "astro dev",
"start": "astro build --remote && node ./dist/server/entry.mjs",
"build": "astro build --remote",
"preview": "astro preview",
"astro": "astro"
},
"engines": {
"node": "^22.14.0"
},
"dependencies": {
"@astrojs/db": "^0.18.3",
"@astrojs/mdx": "^4.3.0",
"@astrojs/node": "^9.5.1",
"@astrojs/rss": "^4.0.10",
"@astrojs/sitemap": "^3.2.1",
"@types/nodemailer": "^7.0.4",
"astro": "^5.1.1",
"markdown-it": "^14.1.0",
"nodemailer": "^7.0.11",
"typescript": "^5.4.3"
},
"devDependencies": {
"@astrojs/check": "^0.9.4",
"@types/markdown-it": "^14.1.1"
}
}

View File

@@ -24,12 +24,39 @@
--colour-hyperlink-90: #bfe3ff; --colour-hyperlink-90: #bfe3ff;
--colour-hyperlink-95: #e0f1ff; --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-primary-fg: var(--colour-primary-90); --colour-primary-fg: var(--colour-primary-90);
--colour-primary-fg-accent: var(--colour-primary-80); --colour-primary-fg-accent: var(--colour-primary-80);
--colour-primary-bg: var(--colour-primary-10); --colour-primary-bg: var(--colour-primary-10);
--colour-primary-bg-accent: var(--colour-primary-20);
--colour-code-fg: var(--colour-primary-90); --colour-code-fg: var(--colour-primary-90);
--colour-code-bg: var(--colour-primary-15); --colour-code-bg: var(--colour-primary-15);
--colour-hyperlink: var(--colour-hyperlink-80); --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);
--font-size-sm: 1rem; --font-size-sm: 1rem;
--font-size-base: 1.125rem; --font-size-base: 1.125rem;
@@ -56,6 +83,10 @@
--colour-primary-fg-accent: var(--colour-primary-40); --colour-primary-fg-accent: var(--colour-primary-40);
--colour-primary-bg: var(--colour-primary-95); --colour-primary-bg: var(--colour-primary-95);
--colour-hyperlink: var(--colour-hyperlink-40); --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);
} }
} }
@@ -69,17 +100,22 @@ body {
line-height: 1.5; line-height: 1.5;
/* Geometric Humanist stack from https://modernfontstacks.com */ /* Geometric Humanist stack from https://modernfontstacks.com */
font-family: Avenir, Montserrat, Corbel, 'URW Gothic', source-sans-pro, sans-serif; font-family:
Avenir, Montserrat, Corbel, "URW Gothic", source-sans-pro, sans-serif;
} }
small { small {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
} }
:is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol) { :is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol, blockquote) {
margin-block-start: var(--spacing-block-sm); margin-block-start: var(--spacing-block-sm);
} }
hr {
height: 2px;
}
/** Base layout */ /** Base layout */
body { body {
@@ -99,15 +135,16 @@ img {
height: auto; height: auto;
} }
@media (min-width: 48rem) { @media (min-width: 60rem) {
body { body {
display: grid; display: grid;
grid-template-columns: grid-template-columns:
[media-start] [grid-start media-start]
var(--grid-margin-inline) var(--grid-margin-inline)
[content-start] [media-end content-start]
minmax(var(--grid-max-content-width), auto) minmax(var(--grid-max-content-width), auto)
[content-end]; [content-end grid-end];
grid-auto-rows: max-content;
column-gap: var(--spacing-block-sm); column-gap: var(--spacing-block-sm);
max-width: var(--grid-total-width); max-width: var(--grid-total-width);
@@ -115,22 +152,26 @@ img {
--grid-margin-inline: 6rem; --grid-margin-inline: 6rem;
--grid-total-width: 48rem; --grid-total-width: 48rem;
--grid-max-content-width: calc( --grid-max-content-width: calc(
var(--grid-total-width) var(--grid-total-width) - var(--body-margin-inline-start) -
- var(--body-margin-inline-start) var(--grid-margin-inline) - var(--spacing-block-sm) -
- var(--grid-margin-inline) var(--grid-margin-inline)
- var(--spacing-block-sm)
- var(--grid-margin-inline)
); );
} }
:is(main, article, nav) { :is(main, article, nav) {
display: grid; display: grid;
grid-column: media-start / content-end; grid-column: grid;
grid-template-columns: subgrid; grid-template-columns: subgrid;
}
:is(section, header, aside) { > :is(section, header, aside, form) {
grid-column: content-start / content-end; display: grid;
grid-template-columns: subgrid;
grid-column: grid;
> :not(.not-grid-content) {
grid-column: content;
}
}
} }
:is(h1, h2, h3, h4, h5, h6) { :is(h1, h2, h3, h4, h5, h6) {
@@ -138,6 +179,22 @@ img {
} }
} }
@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 */ /** Headings */
h1 { h1 {
@@ -152,7 +209,10 @@ h2 {
margin-block-start: var(--spacing-block-xl); margin-block-start: var(--spacing-block-xl);
} }
h3, h4, h5, h6 { h3,
h4,
h5,
h6 {
font-size: var(--font-size-md); font-size: var(--font-size-md);
font-weight: 600; font-weight: 600;
margin-block-start: var(--spacing-block-lg); margin-block-start: var(--spacing-block-lg);
@@ -244,7 +304,7 @@ blockquote {
font-style: italic; font-style: italic;
} }
blockquote footer { blockquote footer {
font-style: initial; font-style: initial;
} }
@@ -286,3 +346,149 @@ pre code {
border-radius: none; border-radius: none;
padding: 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);
}

View File

@@ -4,7 +4,26 @@
} }
.h-feed .h-entry { .h-feed .h-entry {
margin-block-start: var(--spacing-block-xs); 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);
} }
.h-feed .full-feed-link { .h-feed .full-feed-link {
@@ -12,5 +31,5 @@
} }
.h-feed :is(a.full-feed-link, .full-feed-link a)::after { .h-feed :is(a.full-feed-link, .full-feed-link a)::after {
content: ' >' content: " >";
} }

View File

@@ -27,10 +27,9 @@
top: -6rem; top: -6rem;
/* A content value is needed to get the ::after to render */ /* A content value is needed to get the ::after to render */
content: ''; content: "";
} }
@media (min-width: 36rem) { @media (min-width: 36rem) {
.h-card { .h-card {
grid-column: media-start / content-end; grid-column: media-start / content-end;
@@ -40,6 +39,7 @@
grid-template-areas: grid-template-areas:
"empty heading" "empty heading"
"photo text"; "photo text";
column-gap: var(--spacing-block-sm);
} }
.h-card div:has(img) { .h-card div:has(img) {

View File

@@ -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;
}
}

View File

@@ -15,14 +15,22 @@ html {
} }
/* Remove default margin in favour of better control in authored CSS */ /* Remove default margin in favour of better control in authored CSS */
body, h1, h2, h3, h4, p, body,
figure, blockquote, dl, dd { h1,
h2,
h3,
h4,
p,
figure,
blockquote,
dl,
dd {
margin-block: 0; margin-block: 0;
} }
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ /* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
ul[role='list'], ul[role="list"],
ol[role='list'] { ol[role="list"] {
list-style: none; list-style: none;
} }
@@ -32,14 +40,21 @@ body {
} }
/* Set shorter line heights on headings and interactive elements */ /* Set shorter line heights on headings and interactive elements */
h1, h2, h3, h4, h1,
button, input, label { h2,
h3,
h4,
button,
input,
label {
line-height: 1.1; line-height: 1.1;
} }
/* Balance text wrapping on headings */ /* Balance text wrapping on headings */
h1, h2, h1,
h3, h4 { h2,
h3,
h4 {
text-wrap: balance; text-wrap: balance;
} }
@@ -51,8 +66,10 @@ picture {
} }
/* Inherit fonts for inputs and buttons */ /* Inherit fonts for inputs and buttons */
input, button, input,
textarea, select { button,
textarea,
select {
font: inherit; font: inherit;
} }
@@ -71,3 +88,6 @@ textarea:not([rows]) {
padding: 0; padding: 0;
} }
[hidden] {
display: none;
}

View File

@@ -0,0 +1,7 @@
import otp from "./otp/otp";
import sendmail from "./sendmail";
export const server = {
otp,
sendmail,
};

View File

@@ -0,0 +1,7 @@
import send from "./send-otp";
import verify from "./verify-otp";
export default {
send,
verify,
};

View File

@@ -0,0 +1,45 @@
import crypto from "node:crypto";
import { z } from "astro/zod";
import { defineAction } from "astro:actions";
import { db, Otp } from "astro:db";
import { transporter } from "../sendmail";
export default defineAction({
input: z.object({
email: z.string().email(),
name: z.string().optional(),
type: z.enum(["email"]),
}),
handler: sendOtp,
});
type OtpParams = {
email: string;
name?: string;
type: "email";
};
async function sendOtp({ email, name }: OtpParams) {
const otp = crypto.randomBytes(3).toString("hex").toLocaleUpperCase();
const otpPretty = `${otp.slice(0, 3)}-${otp.slice(3)}`;
const info = await transporter.sendMail({
from: `"Joe Carstairs" <me@joeac.net>`,
to: `${name ? `"${name}" ` : ""}<${email}>`,
subject: `joeac.net: your OTP is ${otpPretty}`,
text: `
Someone tried to use this email address on joeac.net. If this was you,
your one-time passcode is ${otpPretty}. If this wasn't you, you don't need
to do anything.`,
});
console.log(
`Sent OTP (${otpPretty}) to ${email}. Message ID: ${info.messageId}`,
);
await db.insert(Otp).values({
userId: email,
value: otp,
createdAt: Date.now(),
validUntil: Date.now() + 1000 * 60 * 5,
});
}

View File

@@ -0,0 +1,48 @@
import { randomBytes } from "node:crypto";
import { z } from "astro/zod";
import { defineAction } from "astro:actions";
import { and, db, eq, gte, Otp, SendmailToken } from "astro:db";
export default defineAction({
input: z.object({
guess: z.string().length(6),
lenient: z.boolean().default(false),
userId: z.string().nonempty(),
}),
handler: verifyOtp,
});
async function verifyOtp({ guess, lenient, userId }: VerifyOtpParams) {
const leniency = lenient ? 1000 * 60 : 0;
const isOtpCorrect =
(await db.$count(
Otp,
and(
eq(Otp.userId, userId),
eq(Otp.value, guess),
gte(Otp.validUntil, Date.now() - leniency),
),
)) > 0;
if (!isOtpCorrect) {
return false;
}
await db.delete(Otp).where(and(eq(Otp.userId, userId), eq(Otp.value, guess)));
const token = randomBytes(256).toString("hex");
await db.insert(SendmailToken).values({
userId,
value: token,
createdAt: Date.now(),
validUntil: Date.now() + 60_000,
});
return token;
}
type VerifyOtpParams = {
guess: string;
lenient: boolean;
userId: string;
};

View File

@@ -0,0 +1,93 @@
import { z } from "astro/zod";
import { defineAction } from "astro:actions";
import nodemailer from "nodemailer";
import {
MAX_DAILY_EMAILS,
LOCAL_SMTP_HOST,
LOCAL_SMTP_PASSWORD,
LOCAL_SMTP_PORT,
LOCAL_SMTP_USER,
CONTACT_MAILBOX,
LOCAL_SMTP_ENVELOPE_FROM,
} from "astro:env/server";
import { and, db, eq, gte, SendmailToken, SentEmails } from "astro:db";
export default defineAction({
input: z.object({
email: z.string().email(),
message: z.string().nonempty(),
name: z.string().nonempty(),
userId: z.string().nonempty(),
token: z.string().nonempty(),
}),
handler: sendmail,
});
type SendEmailParams = {
email: string;
message: string;
name: string;
token: string;
userId: string;
};
async function sendmail({
name,
email,
message,
token,
userId,
}: SendEmailParams) {
const isTokenCorrect =
(await db.$count(
SendmailToken,
and(
eq(SendmailToken.userId, userId),
eq(SendmailToken.value, token),
gte(SendmailToken.validUntil, Date.now()),
),
)) > 0;
if (!isTokenCorrect) {
return false;
}
await db
.delete(SendmailToken)
.where(
and(eq(SendmailToken.userId, userId), eq(SendmailToken.value, token)),
);
const emailsSentLast24Hours = await db.$count(
SentEmails,
gte(SentEmails.sentAt, Date.now() - 1000 * 60 * 60 * 24),
);
if (emailsSentLast24Hours > MAX_DAILY_EMAILS) {
throw new Error(
`${emailsSentLast24Hours} emails have been sent in the last 24 hours, but the max daily load is ${MAX_DAILY_EMAILS}.`,
);
}
const info = await transporter.sendMail({
from: LOCAL_SMTP_ENVELOPE_FROM,
to: CONTACT_MAILBOX,
subject: `joeac.net: ${name} left a message`,
text: `${name} <${email}> sent you a message:\n\n\n${message}`,
});
await db
.insert(SentEmails)
.values({ messageId: info.messageId, sentAt: Date.now() });
console.log("Sent an email to Joe. Message ID: ", info.messageId);
}
export const transporter = nodemailer.createTransport({
host: LOCAL_SMTP_HOST,
from: LOCAL_SMTP_ENVELOPE_FROM,
port: LOCAL_SMTP_PORT,
secure: false,
authMethod: "PLAIN",
auth: {
type: "login",
user: LOCAL_SMTP_USER,
pass: LOCAL_SMTP_PASSWORD,
},
});

View File

@@ -10,6 +10,11 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
const { title, description, image = '/images/headshot.jpg' } = Astro.props; const { title, description, image = '/images/headshot.jpg' } = Astro.props;
--- ---
<!-- 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 --> <!-- Stylesheets -->
<link rel="stylesheet" href="/css/reset.css" /> <link rel="stylesheet" href="/css/reset.css" />
<link rel="stylesheet" href="/css/base.css" /> <link rel="stylesheet" href="/css/base.css" />

View File

@@ -6,10 +6,11 @@ import FormattedDate from './FormattedDate.astro';
export interface Props { export interface Props {
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6, headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
hideAuthor?: boolean, hideAuthor?: boolean,
hideSubheadings?: boolean,
maxEntries?: number, maxEntries?: number,
}; };
const { headingLevel = 2, hideAuthor = false, maxEntries } = Astro.props; const { headingLevel = 2, hideSubheadings = false, hideAuthor = false, maxEntries } = Astro.props;
const allPosts = (await getCollection('blog')).filter((post) => !post.data.hidden); const allPosts = (await getCollection('blog')).filter((post) => !post.data.hidden);
@@ -32,41 +33,46 @@ function sortByPubDateDescending(post1: CollectionEntry<'blog'>, post2: Collecti
return date2 - date1; return date2 - date1;
} }
const headingElem = `h${headingLevel}`; const HeadingElem = `h${headingLevel} class="p-name"`;
const subHeadingElem = `h${headingLevel + 1}` const SubHeadingElem = `h${headingLevel + 1}`;
const AuthorElem = `p${hideAuthor ? " hidden" : ""}`;
const canonicalBlogUrl = new URL('blog', Astro.site) const canonicalBlogUrl = new URL('blog', Astro.site)
--- ---
<section class="h-feed"> <section class="h-feed">
<Fragment set:html={` <HeadingElem>
<${headingElem} class="p-name">
My blog My blog
</${headingElem}> </HeadingElem>
`} />
<aside> <aside>
<p hidden={hideAuthor}> <AuthorElem>
This blog is written by <a class="p-author h-card" href="/">Joe Carstairs</a> This blog is written by <a class="p-author h-card" href="/">Joe Carstairs</a>
</p> </AuthorElem>
<p hidden> <p hidden>
<a class="u-url" href={canonicalBlogUrl}>Permalink</a> <a class="u-url" href={canonicalBlogUrl}>Permalink</a>
</p> </p>
</aside> </aside>
{ distinctYears.map(year => ( { hideSubheadings
<Fragment set:html={` ? <ul>
<${subHeadingElem}> { posts.sort(sortByPubDateDescending).map(post => (
${year} <li class="h-entry">
</${subHeadingElem}> <a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
`} /> <p class="p-summary" set:html={post.data.description} />
<FormattedDate className="dt-published" date={post.data.pubDate} />
</li>
)) }
</ul>
: distinctYears.map(year => (
<SubHeadingElem>{year}</SubHeadingElem>
<ul> <ul>
{ posts.filter(matchesYear(year)).sort(sortByPubDateDescending).map(post => ( { posts.filter(matchesYear(year)).sort(sortByPubDateDescending).map(post => (
<li class="h-entry"> <li class="h-entry">
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>. <a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>
<Fragment set:html={post.data.description} /> <p class="p-summary" set:html={post.data.description} />
Added: <FormattedDate date={post.data.pubDate} /> <FormattedDate className="dt-published" date={post.data.pubDate} />
</li> </li>
)) } )) }
</ul> </ul>

View File

@@ -63,9 +63,9 @@ const canonicalLinksUrl = new URL('links', Astro.site)
<ul> <ul>
{ links.filter(matchesYear(year)).sort(sortByDateAddedDescending).map(link => ( { links.filter(matchesYear(year)).sort(sortByDateAddedDescending).map(link => (
<li class="h-entry e-content"> <li class="h-entry e-content">
<a class="u-url p-name" href={link.href} set:html={link.title} />. <FormattedDate className="dt-published" date={link.isoDateAdded} />
<Fragment set:html={link.description} /> <a class="u-url p-name" href={link.href} set:html={link.title} />
Added: <FormattedDate date={link.isoDateAdded} /> <p class="p-description" set:html={link.description} />
</li> </li>
)) } )) }
</ul> </ul>

View File

@@ -23,9 +23,9 @@
Hi! 👋 My name is <span class="p-given-name">Joe</span> Hi! 👋 My name is <span class="p-given-name">Joe</span>
<span class="p-family-name">Carstairs</span>. Im a <span class="p-family-name">Carstairs</span>. Im a
<span class="p-job-title">software developer</span> at <span class="p-job-title">software developer</span> at
<a class="p-org" href="https://www.scottlogic.com">Scott Logic</a>, a <a class="p-org" href="https://www.scottlogic.com">Scott Logic</a>,
graduate of Philosophy and Mathematics at the University of Edinburgh, a Divinity student at the University of Edinburgh,
a committed Christian and a pretty rubbish poet. a lapsed fiddle player and a very occasional poet.
</p> </p>
<p> <p>

View File

@@ -13,7 +13,7 @@
<a href="/links">Links</a> <a href="/links">Links</a>
</li> </li>
<li> <li>
<a href="/cv">CV</a> <a href="/contact">Contact</a>
</li> </li>
</ul> </ul>
</nav> </nav>

View File

@@ -0,0 +1,31 @@
---
---
<script src="../scripts/otp-form-wc.ts"></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>

View File

@@ -0,0 +1,194 @@
---
title: Surprised By Hope
description:
I've been working on my resurrection doctrine. Here's where I've got to.
pubDate: 2025-05-02
---
A couple of months ago, I was chatting to my friend Neil on the way home from
church, and in that conversation, I confessed to him that I had no idea what
happens to people after they die.
This might come as a surprise to some people who know me. Lots of people have
solid ideas about what happens to people after they die. For different people,
those ideas are very different. Here in Scotland, many people believe that death
is a final end. Many more believe that death marks a physical, and perhaps also
a spiritual, reunion with the rest of the universe, as the matter of your body
begins to be slowly digested and recycled: hence why ever more people are opting
to be cremated rather than buried in one piece. Other minorities believe in an
immortal soul that goes to some other place - be it heaven, hell, purgatory,
nirvana or reincarnation. I belong to the Christian community, which is supposed
to have clear answers on these questions passed down from ancient times, and
people who know me know that I think hard about doctrines. So it may be a
surprise that amongst all the convictions which people have all around me, and
amongst all my own convictions on other topics, I hadn't the faintest clue what
happens to people after they die.
If you are surprised, let me surprise you some more: I am of no fixed opinion on
a whole range of really important philosophical and theological topics, from the
existence of the soul to the purpose of sex, from the nature of the sacraments
to the metaphysics of the mind. But late last year, I set myself some New Year's
resolutions to address some of these questions. Not, by any means, to decide
once and for all the end of the matter: just to form a well-informed opinion.
Sometimes staying quiet isn't good enough: I'm aiming to rectify my silence on
these topics, because I think these topics are too important to ignore.
And one of the issues I picked out was this very issue: what happens to people
after they die? To that end, Neil recommended me a book by the conservative
Anglican theologian, Tom Wright, called _Surprised By Hope_, published in 2007,
at which point I was just learning to spell.
As a result of this book, I feel I understand what the Christian orthodoxy is,
and feel able to treat that view as my working assumption.
Wright defends traditional Christian orthodoxy. He claims that his view is
orthodox, and I'm roundly convinced that it is. When I wrote down in bullet
points what his view amounted to, I found that I had more or less re-written
half the Nicene Creed.
- Jesus Christ was crucified under Pontius Pilate.
- On the third day, he rose from the dead.
- He ascended into heaven and is seated at the right hand of the Father.
- He will come again in glory to judge the living and the dead.
- His kingdom will have no end.
- We look for the resurrection of the dead, and the life of the world to come.
Amen. That's pretty much it. All that I need to stress, to avoid under-stating
Wright's view, is that he specifically thinks that physical creation, including
our bodies, will be transformed into a new kind of physicality, including new
kinds of physical bodies for you and me, and that the 'world to come' means that
heaven and earth - which he regards as God's physical space and our physical
space - will be united.
He contrasts this orthodox view with several views common today amongst
Christians, some of which have even been muddled up with the traditional
orthodoxy:
- 'Jesus was raised to new life, spiritually, like a ghost.'
- 'Jesus literally ascended into the sky, as if he had an invisible jetpack: and
that's where he is now.'
- 'The Christian hope is that we will go to be united with God in heaven after
we die.'
- 'The Christian hope is that we will be snatched up to heaven at the rapture
and taken to a resurrection life there.'
- 'The Christian hope is that we will experience God's eternal life temporarily
before we die.'
- 'Jesus won't really judge anyone, because he loves everyone, and because he's
meek and lowly, not judgy.'
- 'The world will be redeemed through the work of the Church.'
- 'Only God can ever make a difference to the sinful state of the world, so the
only works we should care about now are "saving souls".'
I am happy to admit that I have often been guilty of most of these heresies. The
only ones I've never been tempted by are the 'rapture' view, and the thing about
the invisible jetpack.
Wright has not definitively put any of these ideas to rest for me. _Surprised By
Hope_ is just not that kind of book. It's not a treatise. It's actually quite
light on substantial argument in favour of Wright's position. Wright's main
achievement for me, isn't to convince me that he's right, but that his position
is a good starting point, a good place from which I should need to be convinced.
He does this chiefly by showing that his view is the consensus view of the New
Testament. (He claims to be showing it is the consensus view of 'the early
Church', but he never presents much evidence outside the New Testament, so I'm
being charitable by restricting his claim to the New Testament authors.) Say
what you like about Scriptural authority; if Mark, Matthew, Luke, John and Paul
all were convinced something was apostolic teaching, you'd better well take it
seriously.
If you want convincing, take a look for yourself. Some of the key New Testament
texts are John 5; Acts 17:30-32, 24:14-16; 1 Cor 15, 16:22; 2 Cor 4-5; Rom 6, 8;
Col 3:1-4; Eph 1:10; 1 Thess 4:14-18 and of course Rev 21-22.
You can also try convincing yourself that this is coherent with the Old
Testament hope, by looking at Isa 11, Dan 7, Ps 2, and having another look at
the assumptions behind Paul's behaviour in Acts 24:14-16.
The only significant problem texts I've found for Wright's view are 2 Cor 4-5
and Rev 21-22. In 2 Cor 4-5, Paul seems to plainly assert that we will have to
leave the body in order to face the judgement seat of Christ, and which makes no
apology for the assertion that, even though Christ has reconciled us to God, we
will still have to face judgement for our deeds - which seems to justify the
infamously un-Biblical doctrine of purgatory. If you assume that Paul's writings
express a completely consistent view, however, you will have routes out; in
particular, you could look at the language of Rom 6 and 8. Large chunks of
Paul's letter to the Romans also suggest, if taken out of context, that we will
have to leave our bodies behind, and that even those reconciled through Christ
will face judgement for their deeds - except that key verses contradict both of
those views. Clearly, that's not what Paul meant in Romans; so, you might argue,
it's not what he meant in 2 Cor either: provided you assume that Paul's writings
present a consistent view. (If Paul changed his mind, no explanation is
necessary why Rom and 2 Cor seem to be inconsistent: they could actually be
inconsistent in that case.)
Meanwhile, in Rev 21-22, John has a vision of a 'new heaven and a new earth, for
the first heaven and the first earth had passed away'. This directly contradicts
Wright's emphatic insistence that God's new creation will be continuous with the
first. For Wright, this isn't an academic detail, it's needed in order to give
us a motive to care for the world we've currently got. Without continuity, he
fears we'd be right to join those who are content with trashing the natural
environment because the whole thing's going to end up in fire and brimstone
anyway. Yet this piece of Revelation seems to permit exactly that.
If you were to defend Wright against Revelation, you might point out that
Revelation is a literal description of a vision John had, and is therefore not
in every detail an accurate picture of the future, but a metaphor, an image of
the future. (Fine, but if the wholesale replacement of heaven and earth is a
metaphor, what is it a metaphor for? If the literal future is continuity, why
not describe a vision of continuity?) And you may also assume that the entire
Bible is consistent on the matter of God's ultimate future, and on that
assumption, bring your analysis of the rest of the New Testament to bear.
Whatever you do with the problem texts, it seems clear to me that the
overwhelming weight of Biblical evidence favours the traditional orthodox
position over any of the alternatives. Given that, I'm happy to take it as a
starting point as I continue to think about what happens to people after they
die.
So, I may go back to Neil now, and say - maybe not quite yet 'I have an
opinion' - but at least 'I know what my working assumptions are.' I know what is
the orthodox Christian view: that is, the consensus view of the relevant
experts. The consensus view of relevant experts is generally a good place to
start.
I still have plenty of concerns, though. Here are my top three quandaries on
this topic now.
Firstly, it would be rather unsettling if the orthodox Christian vision for
God's ultimate future popped entirely into existence after the Ascension. The
apostles say that their teaching was given to them by the Holy Spirit - but are
we going to trust our entire doctrine on the future to what a small number of
men claim was told to them by an invisible being behind closed doors? If the
view of the New Testament authors is trustworthy, then it at the very least
needs to cohere very well with the Old Testament. The New Testament hope should
be woven deep into the Old Testament promises. I find Genesis, Daniel, Isaiah
and the Psalms promising, but I've only gotten skin-deep into comparing these
texts to the New Testament: I'd like to go both deeper into these texts, and
broader across the Old Testament.
Secondly, I want to hear the opposition in their own words. Wright very openly
admits that his view is currently a minority opinion even within Christianity,
despite being Christian orthodoxy. Given that is the case, it's reasonable to
expect the opposition to have some good arguments on their side. Wright has not
presented any strong arguments from opposing views, which makes me suspect not
that there are no good arguments, but that he has omitted to cover them in his
short and accessible book. And if there really are no strong arguments against
the traditional view, then we should expect powerful explanations as to why so
few people accept what apparently they should.
Thirdly, I have residual concerns from the metaphysics of mind. I recall from my
undergraduate days that continuity is a major concern amongst the relevant
experts. I think a minority of them even claim that the person who goes to sleep
and the person who wakes up again are completely distinct people who just so
happen to time-share the same body. If continuity is a major problem, then it is
a major problem for resurrection doctrine, too, which even in the New Testament
is compared to a kind of sleep, admitting that there is some kind of
discontinuity between the old body and the resurrection body. Add to this the
easily observable fact that many Christian bodies have rotted and are no longer
suitable for re-animation: their new bodies will have to be physically
discontinuous as well as mentally discontinuous with their old bodies. If I will
be given a new body, is it metaphysically plausible that the person who inhabits
that body will be the same 'me' that inhabits this body, now?
Much love all. As always, answers on a postcard please.

View File

@@ -0,0 +1,201 @@
---
title: Does resurrection doctrine give us unique reasons to work for justice?
description: >-
Tom Wright claims that the only Christian grounds for striving for justice now
is in resurrection doctrine. I'm not convinced.
pubDate: 2025-05-04
---
I've been reading Tom Wright's _Surprised By Hope_, defending his orthodox view
on resurrection. One of his key claims is that only by accepting the orthodox
position on resurrection can Christians justify striving for justice on earth.
To argue this, he needs to first show that resurrection doctrine does justify
striving for justice on earth, and secondly that the available alternatives fail
to do so. Firstly, the positive argument.
## Does resurrection give us reasons to work for justice?
Wright's argument depends on his view on what God's ultimate future will look
like: the present creation will not be abandoned, destroyed, or replaced, but
physically transformed into the new creation.
He argues that our work now has value, because, at the time when God transforms
the old world into the new, he will incorporate the outcomes of our good works
into the new creation, like an architect incorporating the works of many
stonemasons into a great cathedral.
He argues this on the basis of 1 Cor 15. I found it a struggle to find a good
justification for Wright's view in chapter 15 alone, but I did find some crucial
context in chapter 3, just before he begins the first of his many exhortations
to the Corinthians. It would be best to read the whole chapter, but here is
verses 11-15:
> For no one can lay any foundation other than the one already laid, which is
> Jesus Christ. If anyone builds on this foundation using gold, silver, costly
> stones, wood, hay or straw, their work will be shown for what it is, because
> the Day will bring it to light. It will be revealed with fire, and the fire
> will test the quality of each persons work. If what has been built survives,
> the builder will receive a reward. If it is burned up, the builder will suffer
> loss but yet will be saved—even though only as one escaping through the
> flames.
This at least implies that the outcome of all our work will persist at least
until the time of judgement, when it will undergo testing, and those that pass
the test will enjoy a reward. You could read this as being like a quality check,
with God dishing out benefits to those that pass his assessment. But you could
read it in another way, more favourable to Wright. The works will be proven, and
the ones that withstand the process will themselves generate a benefit. You
could think of it like baking: when you put a cake in the oven, you prove
whether or not you got the recipe right; if not, it goes in the bin, and if you
did get it right, you get to enjoy the cake. So I agree that 1 Cor provides a
reason to think that the outcome of our works will somehow persist until the
time of judgement.
However, Wright doesn't just think that our works will persist until the time of
judgement, he also believes that they will at that time be transformed and then
incorporated into the new creation. 1 Cor doesn't directly justify this view. It
is, at least, coherent. Baking a cake in the oven transforms the dough.
So it seems reasonable to me to use 1 Cor to justify Wright's view that our
works will be transformed into the new Creation. However, that's before
considering any counter-arguments, and I have two which concern me.
One is that, as Ecclesiastes points out, the profits of our work will be laid to
waste by time. We don't know when God is going to bring about the new creation.
It could be tomorrow, and we should be behaving today in light of that
possibility - but of all the available possibilities, most of them are in the
distant future, so we should expect on average that there will be a long
interval between my deeds today and the judgement of them. Since time lays waste
to all our endeavours, we should expect that the profits of our work will have
vanished long before the judgement. Therefore, it would be unreasonable to work
for justice in the hope that our work will persist until the time of judgement
in order to be incorporated into the new creation, as Wright argues we should
do.
This first problem is probably the most important, but it's vulnerable to some
counter-arguments, which rather miss the point. So I'm going to move on to my
second problem, which is more niche, but which I think holds stronger against
counter-arguments.
In 1845, the HMS Erebus and HMS Terror left Britain on an expedition to explore
the Northwest Passage. In July 1845, they were spotted by whalers in Baffin Bay.
None of the sailors were ever seen alive again by Europeans. Both ships became
locked into ice in September 1846, and probably never sailed again. In April
1848, the remaining crew began a desperate 250-mile march to the nearest
European outpost. It is almost certain that all of them died.
Now, suppose that one of those sailors, between 1846 and 1848, did something
good. Maybe a major act of heroism, or maybe a small, kind word. I think it's
reasonable to suppose that someone, at some time, did something good.
All the profits of that good act have now completely perished. They are not
recorded in the small records the sailors left behind. None of the sailors who
benefited lived to pass on the benefits. None survived to pass on the light of
justice to the next generation.
At the time of judgement, then, there will be no remnant of this good act for
God to test, transform and incorporate into his new creation.
Does this mean that the good act was wasted? Was it only worth a shot just in
case, despite all the odds, the sailors made it home again?
I think this is a cruel conclusion: but it seems to be where Wright must go.
If it is implausible that the profits of all our good works will make it through
to the time of judgement, then we have to accept that 1 Cor 3 is true only in
some metaphorical sense, not that our works will literally persist in order to
be tested. And if that is true, then the premise of Wright's argument is false:
our work will not necessarily be incorporated into the new creation, so that
cannot generate reasons for working for justice now.
I am not convinced by Wright's positive argument. I would like to consider
alternative interpretations of 1 Cor. This is what Wright goes on to do.
## Do alternative views give us reason to work for justice?
The first view which Wright considers is the gnostic view that resurrection is
just an afterlife in heaven. He argues that this does not generate reasons for
working for justice now, but his argument is really the converse of the argument
in favour of his own view, which I've already considered above, so I'll move on.
The other alternative Wright considers is what he calls 'evolutionary optimism'.
You might also call this 'progressivism'. By this he means the view that the new
creation will be made the Church gradually building upon its own works,
generation after generation, approaching and eventually achieving God's perfect
standard by its works. This is the Victorian optimism which is still a powerful
force in our politics: that history is building upon itself, and progressing
from barbarism to civilisation, from brutishness to beauty, from tragedy to
justice.
He argues that this view, too, does not give us reasons to work for justice now.
His argument is intriguing: if our work for justice is condemned to only ever be
partially successful, then we have no reason to do it. In fact, contra the
evolutionary optimist, no amount of hard work on our part will ever achieve
perfect justice, and therefore if bringing in the new creation is all about our
works, we have no reason to strive for justice.
I'm intrigued by the premise that if we know that our work will at best be
partially successful, then we have no reason to do it.
This isn't how we ordinarily think: typically, if I think attempting to go to
the gym twice a week is going to be partially successful, I would say that this
generates a reason for me to go to the gym.
But I sense there may be an interesting meta-ethical thesis here: perhaps what
we ordinarily call 'partial success' is in fact a mistake, papering over what is
in fact simply a failure.
There's an obvious error theory: we had to create the concept of the 'partial
success' in order to generate reasons in the world as it appears, the world as
described in Ecclesiastes as 'vanity', where the best-laid plans of mice and men
gang aft agley, and all our works crumble into the dust eventually. In order to
think we had reasons at all, we needed to invent the concept of the 'partial
success'.
But is there a good argument for the view that there is no such thing as a
partial success?
In 1915, the HMS Endurance, under the command of Captain Ernest Shackleton,
became stuck fast in Antarctic pack ice in the Weddell Sea. In the face of
extraordinary challenges, Shackleton vowed to bring all his sailors back to the
UK alive: and in one of the most famous exploits of Antarctic exploration, he
succeeded.
It would have been wrong, had Shackleton vowed only to bring back _most_ of his
sailors. We know that he would have been able to bring _all_ of them back,
because he in fact did so. To strive for less would have been negligent.
This is to illustrate a general principle: we ought to strive for the best that
we are able.
Grant that necessarily, no particular justice is inevitable. It follows that
necessarily, it is possible to prevent all injustice. Therefore, perfect justice
is achievable.
And yet we _know_ that we will not achieve perfect justice. It's way too hard.
It does seem that I've proven a contradiction: both that perfect justice is
possible and that it is impossible. I expect these are two different kinds of
modality. I'm not too bothered to carefully distinguish them, as long we agree
that these two things can both be true in some sense. By analogy, consider that,
if you can run a mile in so many seconds, you can run it in a second less; that,
by sorites, it follows that you can run a mile in a minute; and that you
obviously cannot run a mile in a minute: it is too hard. Perfect justice is
perhaps a little like this: it is achievable in the sense that it is physically
possible for us to achieve it, but unachievable in the sense that it's way too
hard.
So, since perfect justice is in some sense achievable, it follows that we ought
to strive for it. But since we know we will not achieve perfect justice, it
follows that we cannot have a reason to strive for it: we cannot genuinely
strive for what we know we cannot do. Therefore, if you reject that God will
work to transform our world of vanity into something fundamentally different
where perfect justice is not only genuinely achievable but actually realised,
then you will be stuck in this hopeless tension, where you both must bring about
perfect justice, and have no reason to do it, because you have no hope of
success.
Where from here? I would really like to find alternative interpretations of 1 Cor,
and weigh them up against Wright's interpretation. It may be that, whatever the
counter-arguments, Wright's view is the strongest available. It may not.

View File

@@ -0,0 +1,200 @@
---
title: Figuring things out
description:
I thought I needed to 'figure things out'. Here's what I did instead.
pubDate: 2025-06-23
---
'You could always do a Panic Masters.' In my last year of undergraduate studies,
that was often the sort of advice we liked to console one another with. A lucky
few people in my year had a clear sense of vocation, but most of us felt
confused.
Not that we lacked options - graduating with a good degree from a good
university, we were lucky to have a great deal more options than most people our
age. We went all sorts of directions. Some followed the money, going into big
boring management consultancy, big bad tech companies or startups swimming in
venture capital. Others wanted something more noble, and pursued teaching or the
third sector. Others still went travelling the world on a shoestring or worked a
low-skilled job living with their parents, hoping to 'figure things out'.
I thought I needed to figure things out. But I was sure I wasn't going to do
that by pulling pints, going on holiday, or staying in the university (even
though I felt passion for academia). I needed something different, something
that would move my life forward, and ideally, something that would pay the
bills. Then, maybe after a year or two, I would have a better idea of what
longer-term future I saw for myself. This, I thought, is the way to start
figuring things out.
But by January of this year (2025), nothing seemed to have changed. I was still
working in the same job. I hadn't discovered a passion for software engineering.
Nor had I discovered a passion for anything else. I was more skilled, I suppose,
but I didn't have any clearer ideas about how the skills I have should guide me
into any career into particular. I had looked at other jobs, but not made many
serious applications. I had applied to a Masters programme in 2024, got an
offer, turned it down, and applied again in 2025. I was disappointed that I
apparently hadn't made much progress.
So I vowed to do something about it. I promised myself to study the matter. I
wanted to know what route to pursue. And, being Christian, I thought, I had to
figure out how to leverage my theological resources to answer this question. I
believed that God would have a path set out for me, and so I had to find out
what it was. A friend told me I needed discernment. That, I thought, was what I
needed to do - discern the will of God for my career.
I supposed, what God willed me to do in general was quite obvious -- he wants me
to live in line with the gospel. But that doesn't say much about my career
choices. So I expected to find something a little more specific. I didn't expect
to find it in the Bible directly, of course, as there's not much about software
engineering in the Bible. But maybe the Holy Spirit was trying to nudge me in
the right direction, and I just needed to figure out how to hear him.
By the way, if you're not a super-spiritual sort and this is starting to sound a
little kooky, I'm with you -- but I didn't see any other possibility. After all,
what else could 'discernment' mean in practice, if not 'discerning' some still
small voice?
So I studied. I got myself copies of some tracts, including Tim Keller's [_Every
Good Endeavour_][every-good-endeavour] and William Taylor's [_Revolutionary
Work_][revolutionary-work]. These writers showed me how I had for so long been
stuck in a view of work which didn't make sense and wasn't leading me anywhere.
I came away shaken off from how I had been thinking before, and given a new
perspective from which to start re-thinking my attitudes to work. It's been
exhilirating, and I recommend both books to anyone for whom work is a major
concern (but especially to those who, like me, are already infected with
middle-class thinking, or those at risk of catching it).
The will of God for my life really is as simple as I had feared. What God wants
for me is the same as what he wants for everyone: to live in line with the
gospel. God probably doesn't have any special extras for me personally. If the
Holy Spirit does want to speak to me and wants me to hear it, I can trust him to
make that happen, and in the meantime, I can carry on listening to God's words
in the miraculous way he has already provided, not in private whispers but in
the blinding clear public light of the testimony of the Bible and of the Church
to Jesus Christ.
I still have unanswered questions about my future career. But my angst is gone.
My angst is gone because I see now I was asking the wrong questions. I wasn't
really anxious about which career I ought to pursue. I perceived -- rightly --
that I had been called to walk a narrow path in a life full of junctions. But
this led me to think that for me, those junctions are mostly about my career
choices. It followed that the career choices I faced had the power to lead me
astray from God's way if I chose wrong. Without a map charting the way ahead,
without a rule by which to determine which was God's way and which the wrong
way, I feared that my career choices were a dangerous gamble. If I got it wrong,
I wouldn't be a genuine follower of Christ, I wouldn't genuinely be trying to do
what's right, and I wouldn't be fulfilling my God-given destiny.
What I didn't see was that I had re-worded the world's anxieties in God-speak.
It sounded reassuringly pious, but it wasn't right. In fact, it was idolatry.
As I observed at the start of this essay, a large part of my generation of
university graduates, Christian and non-Christian, share this angst. Most
wouldn't word it in Christian-sounding God-speak. They might say they're worried
about fulfilling their potential. But it's the same angst - the fear that if you
don't choose the right career, you won't be living life to the full, or you
won't be making the most of your talents and passions, or you won't be genuinely
doing what's right, but just following the rest of the world into a lukewarm
career-ladder rat-race. I hadn't 'leveraged my theological resources' at all:
I'd only leveraged my theological thesaurus.
I think the scales fell from my eyes when commentators brought me back to the
New Testament's advice on work, which doesn't talk about career choices at all.
Since Jesus calls all his followers to enter by the narrow gate (Matt 7:13-14),
likewise, Paul urged the Ephesians to 'live a life worthy of the calling which
you have received' while arguing that Christ has given different gifts of
service to each of us, his workers (Eph 4:1, 7-13). But almost all of the people
Jesus and Paul were addressing had very little control over what work they were
doing. Indeed, almost all people in the world today have very little control
over what work they do. The paralysis of choice that I face is also a rare
privelege. But that means that, when Jesus calls his followers to enter by the
narrow gate, and Paul urges Christians to use their gifts of service, they can't
possibly be primarily talking about career choices: most of their audience
didn't have careers and they didn't have choices. They just had work, and if
they didn't carry on working, they wouldn't eat (2 Thess 3:10).
The narrow gate is not about choosing the right career in a world of options.
The narrow gate is choosing to trust God in a world of temptation to worship
anything else.
Nor does Paul encourage us to switch jobs until we find our God-provided perfect
match of talents and passions to service. Indeed, some of his most powerful
encouragement and advice to Christian workers is addressed to people who had
almost no control whatsoever over what work they did: slaves (eg Eph 6:5-8). In
two areas where people did have some limited control, namely, circumcision and
marriage, Paul advises the Corinthians that 'each person should remain in the
situation they were in when God called them' (1 Cor 17:24).
So God's will for me in my situation is the same as it is for everyone else: to
come back to our father when he calls. In practice, accepting the good news of
Jesus Christ means continually confessing my sin and repenting of it. And that
means being turned inside out: no longer turned in on myself by sin, but turned
outside onto God my father and onto my neighbour in love.
Nor is there any need for angst, because this is the good news: that we have all
already failed to fulfil our God-given purpose, which is to love God and one
another. If we felt angst, it was justified, and indeed the situation was far
worse than we feared. But despite that, Jesus Christ has made a way for us to be
acceptable, and if we trust in him, we are permanently secure; free from fear,
and free to turn back, however faltingly, to the way we were made to be.
For me, this has changed how I think about my career choices.
I've come to see that my career choices are a rare privelege, and something I
should thank God for. It's also a responsibility to take seriously, as it's an
opportunity to choose between service and self-service.
I shouldn't choose a career just because it's easy, and I should seek out
careers with opportunities to serve, and commit to using the opportunities I
have in whatever work I'm doing to serve. I shouldn't choose a career just
because it fits my university-educated, middle-class prejudices about what work
is dignified and what isn't; what kind of job counts as a 'proper job' and what
is 'dead-end'.
I also shouldn't choose a career just because it's perceived as 'noble'. The
world needs carers, teachers and preachers. It also needs principled, committed,
competent white-collar workers making sure that certain boring, technical,
invisible systems work well. These systems make caring, teaching and preaching
possible. Through my own experience, I've been humbled by brilliant people in
front-line jobs doing amazing work, but I've also seen how important those
tertiary systems are.
I also shouldn't dwell too long on my career choices, paralysed by an irrational
angst that the value of my life hangs on making the right decision. I should
remember that Jesus calls everyone alike, although most people don't have
anywhere near as much power over their own career as I do. And I should remember
that, as a result, God will use pretty much any line of work for his glory if I
commit it to him.
So I shouldn't choose what's easy, nor what's perceived as noble, and nor should
I be paralysed by choice. But what ought I do instead?
Instead, I should commit my work to God right now, starting from this morning. I
don't have to wait until I find a perfect career, because I will never have a
perfect career. God can use the line of work I'm already in for his glory, and
if I don't believe that, I'm not just doubting myself, I'm doubting him. I
should trust his power. And when I do have career choices, I should commit those
to him too, not fretting endlessly as if one career is holy and another damned,
but prioritising service to God and others over myself and trusting God with the
rest.
Comfort, elitism and moralism are all forms of idolatry. I can toil endlessly
pursuing any of them and never be satisfied. But instead, I can rest easy in the
knowledge that my place in God's family is secure, and work hard knowing that
whenever and wherever and however I make sacrifices for the good of others, God
is working through me and by me, even though I fall far short of fulfilling my
potential and my God-given purpose.
I haven't 'figured things out'. As it transpires, there wasn't anything to
'figure out'. I was saddled with angst at a phantom problem, which my knowledge
of the gospel should have told me did not exist. I cannot earn my worth on
earth. But because of Christ, my value is secure. Because of that, I am free to
work without snobbery, without shame and without angst for the sake of love and
in the certain hope that in the end, by God's work, not mine, everything will be
figured out.
[every-good-endeavour]:
https://uk.10ofthose.com/product/9781444702606/every-good-endeavour-paperback
[revolutionary-work]:
https://uk.10ofthose.com/product/9781910587997/revolutionary-work-paperback

View File

@@ -0,0 +1,415 @@
---
title: Why Psalm 118 is the theme tune to Matthew's Gospel
description: >-
Partly inspired by what I misheard at Cornhill Summer School 2025.
pubDate: 2025-06-26
---
[Psalm 118][ps-118] is one of the best-loved hits in the Hebrews' ancient
songbook, the Psalms, and also one of the most re-interpreted.
It has been heavily used in both Jewish and Christian liturgy since ancient
times. It is heavily referenced in the Rabbinical literature. Depending how
generous you are with what counts as an 'allusion', you can count between twenty
and sixty quotes and allusions to Psalm 118 in the New Testament. It has been
frequently set and re-set to music, memorised, sung, interpreted and
re-interpreted.
But why should we care about an old song and its ensemble of interpretations? At
least part of the answer that its long history of usage includes another
Biblical text which urgently appeals to us today: the Gospel of Matthew.
If we can understand why Matthew referred to Psalm 118, not once, not twice, but
five times, all in the space of five chapters, we might understand a little
better the story that Matthew wants to tell us.
To understand why it's so important for Matthew, first, let's get on the same
page on what the psalm actually says.
## A story in four characters
The psalm features four characters: a hero, a congregation, some enemies, and
the Lord.
<block-comment class="not-grid-content">
The hero narrates the psalm's central block, from verse 5 to verse 21. He is a
warrior hero: he 'cuts off' his enemies. He is nearly defeated, but is
eventually victorious, and ascribes his victory to the Lord. He then approaches
the 'gates through which the righteous shall enter', and appeals to go through
so that he can praise the Lord there.
<blockquote class="verse verse--hanging-indents para-spacing-tight">
<p>
<span>When hard pressed, I cried to the Lord;</span>
<span>he brought me into a spacious place.</span>
</p>
<p>
<span>The Lord is with me; I will not be afraid.</span>
<span>What can mere mortals do to me?</span>
</p>
<p>
<span>The Lord is with me; he is my helper.</span>
<span>I look in triumph on my enemies.</span>
</p>
<p>
<span>It is better to take refuge in the Lord</span>
<span>than to trust in humans.</span>
</p>
<p>
<span>It is better to take refuge in the Lord</span>
<span>than to trust in princes.</span>
</p>
<p>
<span>All the nations surrounded me,</span>
<span>but in the name of the Lord I cut them down.</span>
</p>
<p>
<span>They surrounded me on every side,</span>
<span>but in the name of the Lord I cut them down.</span>
</p>
<p>
<span>They swarmed around me like bees,</span>
<span>but they were consumed as quickly as burning thorns;</span>
<span>in the name of the Lord I cut them down.</span>
</p>
<p>
<span>I was pushed back and about to fall,</span>
<span>but the Lord helped me.</span>
</p>
<p>
<span>The Lord is my strength and my defense;</span>
<span>he has become my salvation.</span>
</p>
<p>
<span>Shouts of joy and victory</span>
<span>resound in the tents of the righteous:</span>
</p>
<p>
<span>“The Lords right hand has done mighty things!</span>
<span>The Lords right hand is lifted high;</span>
<span>the Lords right hand has done mighty things!”</span>
</p>
<p>
<span>I will not die but live,</span>
<span>and will proclaim what the Lord has done.</span>
</p>
<p>
<span>The Lord has chastened me severely,</span>
<span>but he has not given me over to death.</span>
</p>
<p>
<span>Open for me the gates of the righteous;</span>
<span>I will enter and give thanks to the Lord.</span>
</p>
<p>
<span>This is the gate of the Lord</span>
<span>through which the righteous may enter.</span>
</p>
<p>
<span>I will give you thanks, for you answered me;</span>
<span>you have become my salvation.</span>
</p>
</blockquote>
</block-comment>
<block-comment class="not-grid-content">
Having heard the hero's account, the final section is dominated by the
congregation. They thank the Lord for his saving work, which they describe thus:
'the stone the builders rejected has become the chief cornerstone.' This implies
that the hero had initially faced rejection, before being vindicated. The people
show their praise by bringing a sacrifice bound with branches up to the altar,
and finally the psalm is book-ended by repetition of the opening motif: 'give
thanks to the Lord, for he is good, for his mercy endures forever!'
<blockquote class="verse verse--hanging-indents para-spacing-tight">
<p>
<span>The stone the builders rejected</span>
<span>has become the cornerstone;</span>
</p>
<p>
<span>the Lord has done this,</span>
<span>and it is marvelous in our eyes.</span>
</p>
<p>
<span>The Lord has done it this very day;</span>
<span>let us rejoice today and be glad.</span>
</p>
<p>
<span>Lord, save us!</span>
<span>Lord, grant us success!</span>
</p>
<p>
<span>Blessed is he who comes in the name of the Lord.</span>
<span>From the house of the Lord we bless you.</span>
</p>
<p>
<span>The Lord is God,</span>
<span>and he has made his light shine on us.</span>
</p>
<p>
<span>With boughs in hand, join in the festal procession</span>
<span>up to the horns of the altar.</span>
</p>
</blockquote>
</block-comment>
There is potentially a fifth character, the 'builders' who rejected the stone.
Interpreters often identify these 'builders' either with the enemies or with the
congregation, though not always. The text doesn't say.
Apart from the Lord, none of these four (or five) characters are named in the
text.
This is where the intrigue lies: who are these characters? Who are the enemies?
Who is the congregation? And who is this embattled hero, this 'stone the
builders rejected' which has become 'the chief cornerstone'?
If I were to enumerate all the solutions that have been proposed to this puzzle,
reading this essay would give you piles. But in order to understand some of the
context in which Matthew was writing, permit me briefly to introduce two of the
most popular Jewish interpretations.
## Moses
The first is Moses. Psalm 118 lays on thick the references to the Song of the
Sea in Ex 15.
<block-comment class="not-grid-content">
The central line, 'the Lord is my strength and song, he has become my
salvation!' is a direct quote from Ex 15:2. Like Ex 15, the psalm uses the
divine name frequently. Not only that, but the psalm, like Ex 15, prefers the
relatively unusual form YH rather than the more common YHWH. The psalm echoes Ex
15 also in its references to the right hand of the Lord doing mighty things, his
chosen hero being hard-pressed by foreign nations and enjoying the Lord's
'salvation', and by the hero's response, 'praising' and 'exalting' the Lord.
<blockquote class="verse verse--hanging-indents para-spacing-tight">
<p>
<span>The Lord is my strength and my defense;</span>
<span>he has become my salvation.</span>
<span class="not-hanging">He is my God, and I will praise him,</span>
<span>my fathers God, and I will exalt him.</span>
</p>
<p>
<span>The Lord is a warrior;</span>
<span>the Lord is his name.</span>
</p>
<p>
<span>Pharaohs chariots and his army</span>
<span>he has hurled into the sea.</span>
<span class="not-hanging">The best of Pharaohs officers</span>
<span>are drowned in the Red Sea.</span>
</p>
<p>
<span>The deep waters have covered them;</span>
<span>they sank to the depths like a stone.</span>
</p>
<p>
<span>Your right hand, Lord,</span>
<span>was majestic in power.</span>
<span class="not-hanging">Your right hand, Lord,</span>
<span>shattered the enemy.</span>
</p>
</blockquote>
</block-comment>
In short, the psalm is absolutely reeking with references to the Song of the
Sea, Moses' classic number-1 hit. No ancient Jew, for whom the psalm was
originally written, could have failed to smell it.
The Midrash Tehillim, a Jewish commentary on the psalter composed in the early
medieval period, even ascribes the psalm to Moses, claiming that he sang it on
the first Pesach (Passover). Certainly, the psalm has featured heavily in Jewish
celebrations of both Pesach and Sukkoth (another exodus-inspired festival) since
ancient times.
However, perhaps surprisingly, Moses is not the most common Jewish reading of
the hero of Psalm 118. That accolade goes to the next great hero of the Hebrew
Scriptures: David.
## David
Although, unlike many other psalms, this one is not explicitly described as
being 'of David', very many Jewish interpreters associate this psalm with that
improbable king. For instance, the Targum -- an Aramaic paraphrase and
commentary on the Hebrew Scriptures -- explicitly reads David, Samuel and Saul
into the psalm. David has also been a favourite reading of some Christian
readers, including John Calvin.
Why is this? One reason might be the psalm's context in the psalter. The psalter
is divided into five books, and contemporary scholars theorise that in the
second Temple period, editors arranged these five books thematically.
Books I and II tell how God had a covenant with David, and Book III laments that
the covenant with David has failed, perhaps because David failed to keep the
commands of the Torah. The task of Books IV and V is to show that God will
restore his Davidic kingdom and fulfil his promises.
Psalm 118 sits in this final block, as the last psalm of Book IV. This suggests
we should expect David, or a type of David, to feature: a returning king, coming
back to fulfil his destiny to rule as an intermediary between God and his
people. (Presumably, this time, he's got to be a true keeper of the Torah in
order for this to work.)
Notice that a Davidic interpretation is inherently implicitly also a Messianic
interpretation. David is dead. God promised that he would establish an
everlasting throne in Jerusalem, where a human mediator would rule on his
behalf, and God and his people could live together in peace forever. David, for
all his merits, has conspicuously failed to deliver on this promise. So, if this
psalm looks back to David, it must also look forward to the one who will fulfil
God's promises to David.
So in this traditional Davidic interpretation, it's understood that God is going
to choose someone who will re-establish that Davidic throne, and this time it's
going to really work. Which means this time, it's going to be really different.
## Jesus
On the face of it, the New Testament authors seem to have nothing to do with the
traditional interpretations. Instead of Moses or David, they exclusively
identify the hero of Psalm 118 with Jesus. What are they up to?
One reason the New Testament authors went ham for Psalm 118 is simply because it
was well-known. I mentioned that it was used heavily at Pesach and Sukkoth. As a
result, lots of Jews were very familiar with its ideas and its language. Many
ordinary people would have memorised it.
But that in itself doesn't explain why the New Testament authors used it. They
didn't refer to Scripture arbitrarily, but they subverted shared interpretations
in order to tell a new story. The cleverest instance of this is in the Gospel of
Matthew.
<block-comment class="not-grid-content">
Matthew first gets his reader tuned in to Psalm 118 as Jesus enters Jerusalem on
the back of a colt. Matthew quotes the crowds quoting Psalm 118, shouting
'Hosanna to the son of David! Blessed is he who comes in the name of the Lord!'
In case we missed the application, he pairs this with his own quotation from the
prophecy of Zechariah: Jesus is the coming king who will fulfil God's promises.
The crowd also wave him in with branches, typical of Sukkoth celebrations and a
reference to Ps 118:27.
<blockquote>
A very large crowd spread their cloaks on the road, while others cut branches from the trees and spread them on the road. The crowds that went ahead of him and those that followed shouted, 'Hosanna to the Son of David!' 'Blessed is he who comes in the name of the Lord!' 'Hosanna in the highest heaven!'
</blockquote>
</block-comment>
So now we know Jesus is the returning king, we're expecting his imminent victory
over his enemies, right? That's what Psalm 118, and its traditional Mosaic and
Davidic interpretations, suggests, and so it's clearly what Matthew wants us to
think. But that's when things take a sudden turn.
<block-comment class="not-grid-content">
Immediately after this, Jesus tells the Parable of the Tenants. He implies that
the well-educated, respectable religious leaders are complicit in murder and
enemies of God. It's a shocking teaching, and it doesn't go down well. Perplexingly,
Jesus quotes Psalm 118 again in the midst of this teaching.
<blockquote>
Jesus said to them, 'Have you never read in the Scriptures:
<p class="verse verse--hanging-indents">
<span>"The stone the builders rejected</span>
<span>has become the cornerstone;</span>
<span class="not-hanging">the Lord has done this,</span>
<span>and it is marvelous in our eyes"?</span>
</p>
'Therefore I tell you that the kingdom of God will be taken away from you
and given to a people who will produce its fruit. Anyone who falls on this
stone will be broken to pieces; anyone on whom it falls will be crushed.'
</blockquote>
</block-comment>
Matthew depicts Jesus continuing to teach in the Temple while sparring with the
religious elites. Jesus caps off what was already a dreadful day by declaring
seven devastating woes on the religious leaders. As he finally exits the Temple,
he leaves another ominous quote from Psalm 118 hanging in the air: 'For I tell
you, you will not see me again until you say, Blessed is he who comes in the
name of the Lord.'
This doesn't make sense at all. According to the Psalm 118 storyline, we were
supposed to be seeing Jesus cutting down his enemies and arriving at the Temple
to celebrate with God's people. But now he's doing the opposite: he's cutting
down God's people and then leaving the Temple mired in controversy.
Jesus then, after taking a private seminar for his disciples, invites them to
what he knew would be his last supper. Matthew shows the reader how Judas had
already betrayed Jesus behind his back. And yet, Matthew doesn't let up. He
points out that they are celebrating their Pesach meal, and at the end, he
points out that they finished with a hymn.
Why these apparently irrelevant details? He's begging you to put two and two
together. His Jewish readers would have immediately clocked that the hymn in
question was Psalm 118, ritually sung at the end of the Pesach meal.
So even at the very moment Jesus' total defeat in shame and misery is sealed,
they're still singing this song about a victorious returning king, coming to
re-establish David's throne forever?
The point that Matthew wants us to clock is the point Jesus made to the
religious leaders in the Parable of the Tenants: 'the stone the builders
rejected has become the chief cornerstone.' He really is the perfect Moses and
the perfect David that God has promised. But before his great victory, he needs
a great rejection. The surprise is that neither Jesus' rejection nor his victory
look anything like what anyone expected.
Rather than being hard-pressed by foreign nations and defeating them in battle,
Jesus is oppressed by his own people, the Jews. (We should understand this in
the context that Matthew's Gospel was written primarily for an audience of Jews,
hence why he expects them to pick up on all the references to Psalm 118.)
But this oppression is only the surface layer: his real fight was his fight with
the spiritual powers of sin and death. By going to the cross, he consented to be
hard-pressed.
And his Resurrection is his victory. Through it, he shows that he has defeated
death. Now he is ascended to the right hand of the Father, where he rules as the
perfect David, as the one who could both act as a human intermediary between God
and humanity, and as one who could truly keep God's law. He is also the perfect
Moses, who, by God's power, led his people out of captivity to sin and death in
order to worship God. The old covenants are broken, but God has remained
faithful and delivered on them anyway, and in doing so has created a new people,
the Church, who will enter the gates of righteousness because Jesus has opened
the way.
For a contemporary Jewish reader of Matthew's Gospel, the references to Psalm
118 would automatically have conjured all the associations with Moses and David,
and as a result, all the Messianic secondary meanings, that he needed to make
his point. He could have expected his original readers to join the dots.
For a contemporary reader, particularly one like me that didn't get an
old-fashioned Biblical education, it might take a bit more work to spot the
links. But isn't it worth it? This psalm helps us to understand the message of
Matthew's Gospel: Jesus fulfils God's promises in a way that nobody expected.
## Conclusion
As I've discovered, Matthew's way is far from the only way of reading Psalm 118.
That's to be expected: as I noted at the start, none of the characters apart
from the Lord are named in the text. It's up to us as readers to impose
allegories onto the text, if that is what we choose to do.
And that is what interpreters from ancient times have strove to do. Indeed,
Matthew didn't ignore or overwrite previous interpretations: he used Psalm 118
precisely because he knew that if he put Jesus into Psalm 118, his readers would
have made the link to Moses and David themselves. In order to get Matthew's
subversive new reading, you've got to be fluent in the rich tradition of old
readings.
Therefore I will keep reading. As I've encountered Psalm 118 recently, I've
re-discovered how understanding one Biblical text can shed dramatic new light on
another. If God is willing, perhaps this will help me to see him once again in
sharp relief.
## Further reading
- [Calvin's commentary on Psalm 118](https://biblehub.com/commentaries/calvin/psalms/118.htm)
- [Cook, EM. 2001. Targum Tehillim: An English Translation. Book V](http://targum.info/pss/ps5.htm)
- [Vaillancourt, IJ. 2019. Psalm 118 and the eschatological son of David. JETS 62(4) pp 721-738](https://etsjets.org/wp-content/uploads/2020/01/files_JETS-PDFs_62_62-4_JETS_62.4_721-738_Vaillancourt.pdf)
- [Gillingham 2020. Das schöne Confitemini: engaging with Erich Zengers reading of Psalm 118 from a Jewish and Christian reception history perspective. In: 'By my God I can leap over a wall': Interreligious Horizons in Psalms and Psalms Studies](https://ora.ox.ac.uk/objects/uuid:1dff6f67-3c9e-41a8-a691-90e1e260fcdd)
- [Botha PJ 2003. Psalm 118 and social values in Ancient Israel. OTE 16(2) pp 195-215](https://www.researchgate.net/publication/237449388_Psalm_118_and_social_values_in_Ancient_Israel)
I was inspired to write this essay by the teaching on Psalm 118 at Cornhill
Summer School 2025.
[ps-118]: https://www.biblegateway.com/passage/?search=Psalm%20118

View File

@@ -0,0 +1,88 @@
---
title: Changing my ambitions
description: >-
When I started my first degree, I had unrealistic and unhelpful ambitions. For
my second degree, I'm setting my sights on different targets.
pubDate: 2025-09-18
---
Is 'virtue' a terribly old-fashioned word? I don't mind either way. If I've run
into you for more than three seconds in the last couple of weeks, you'll know
that I've just started my second degree, and I'm very happy about it. I'm having
a great deal of fun, and expect my studies to continue to be fun. But fun is not
my goal. My goal is virtue.
In particular, the virtues I'm striving after in my degree are a greater ability
to ask questions well, and to answer them well; to write well, and to dispute --
that is, speak with, listen, reason, discuss -- well. Insightfully, sensitively,
humanely, intelligently, informedly, fluently: well.
But what about all the starving children! I hear you cry. In the past, I myself
have got myself stuck fearing that doing another degree would be ignoring some
more immediate duty to do something about all the evil in the world. So, is my
degree selfish? Or how can it not be? How can this be good?
I believe it's precisely by abandoning that restrictive sense of public duty
which has freed me at last to do something good. Let me explain.
During my first degree, I had a great deal of ambition. I was genuinely
convinced that I could find robust answers to big questions if I thought about
them hard enough. I thought I was clever enough to make progress, or at least
contribute. I thought I could, if I wanted to, get into a PhD programme and end
up employed as Dean of Philosophy of Oxford, paid to smoke from a pipe all day
in a tweed jacket with leather patches while quietly resolving all the world's
burning intellectual issues.
What's changed? If I were a pessimist, I might mention my encounter with that
devil, reality. It turns out that I'm not actually the cleverest person in the
room, that the biggest philosophical problems are pretty intractable, and that I
can't get into Oxford -- and even if I could, it wouldn't necessarily be right
to uproot myself from my friends, family and church community to pursue my dream
career.
All this did matter a great deal. It's what slowly convinced me to finally drop
those unrealistic philosophical ambitions. It's why, a year and a half ago, I
turned down the offer of a Master's in Philosophy at a excellent university
(albeit not Oxford).
But that's not the whole story. I'm not sat here with a sob story of broken
dreams. After I turned down that PhD, I didn't feel deflated, I felt liberated.
I haven't just dropped those ambitions, I've found new ones.
My friends, family and church community ought to matter far more to me, I
realised, than my career. So, turning away from academia, I turned towards love.
This is what the gospel does. It's the most good story, beautifully true, which
says to the human heart: since God so loved us, so also we ought to love one
another.
When I'm targeting virtue, I find it helpful to imagine a character who displays
the virtues I'm after. So picture Helpful John. He's an encouragement. Whenever
you talk to Helpful John, you come away feeling emotionally mature and
intellectually confident, because his overwhelming respect wipes away your
anxiety. He listens to you carefully, and insists on understanding you at more
than a superficial level. When it's appropriate to do so, he can ask devious
questions which unlock new possibilities you hadn't considered before. He knows
lots of relevant and often surprising facts. He can compare your perspective
with that of strange and subversive alternative perspectives. He doesn't like to
tell people what to think, but when he speaks or when he writes, you pay
attention, because you know he is capable of profound insight.
Helpful John sounds great. A model to replicate, right? Not in every respect,
necessarily. Helpful John might not be the life and soul of the party. He might
not be the first person you go to for comfort in times of trouble. He might not
be the most reliable person in the world, or the best with children, or the best
with hand tools. Helpful John is a character, but he doesn't have to be good at
everything.
Helpful John is roughly my north star. I don't expect to become Helpful John.
But with the Spirit's help, with me continuing to lean in to the process, I do
intend for Useless Joe to become more like Helpful John in his most enviable
respects.
Is this selfish? Is this a shortage of ambition? Wouldn't you love to have a
Helpful John as a friend? A brother? Across the table at small group at church?
In your workplace?
So forgive me if I'm old-fashioned: I believe virtue is a virtue. A better world
is one full of better people.

View File

@@ -0,0 +1,125 @@
---
title: Why the creeds matter
hidden: true
description: >-
Plenty of Christians don't think the creeds are important for their faith.
Plenty others take the creeds for granted. But Christians ought to appreciate
that the creeds are a sophisticated, profound and essential foundation of the
church.
pubDate: 2025-09-24
---
<blockquote>
But you, man of God, flee from all this, and pursue righteousness, godliness,
faith, love, endurance and gentleness. Fight the good fight of the faith. Take
hold of the eternal life to which you were called when you made your good
confession in the presence of many witnesses.
<cite>1 Tim 6:12</cite>
</blockquote>
Since the earliest days of the church, Christians have confessed their faith.
That is to say, we have declared what we believe to each other and to the world.
For the vast majority of the world's Christians, this frequently takes the form
of one of two fixed texts, respectively, the Apostle's Creed and the Nicene
Creed. The Nicene Creed in particular unites almost all Christians worldwide,
including the Greek Orthodox and Roman Catholic denominations and almost all
Protestant denominations. Despite celebrating its 1700th anniversary this year,
and despite all the ways in which the global church is sadly divided, the Nicene
Creed stands as a symbol of Christian unity and a faithful summary of what
Christians believe.
Yet not all Christians fully appreciate their creeds.
Perhaps you're familiar with the creeds from your church's form of worship, or
maybe you've heard it used at baptisms. You might have even confessed one
yourself at your own baptism. But if you've never given it much thought, you
might have assumed the creeds are simply neutral summaries of Christian belief,
abstracted out of any historical context. You might think it dates to a
primitive time in the Church's history, before the Church went through the
refining fire of advanced theology.
In fact, in the fourth century, when the text of the Nicene Creed and the
ancestor of what became the Apostle's Creed was fixed, the creeds were
formulated in response to some very particular challenges of that time. They do
not represent primitive Christianity, but on the contrary, they exist in the way
they do precisely because of the need for exact, exclusive theology.
In the fourth century, the Church was straining within itself to understand what
the revelation of Jesus Christ revealed about God and his purposes.
For an earlier generation, the main threat had been that Christians might adopt
ideas from the gnostics, a mystical religious community which probably formed
about the time of Christ. In some respects, gnostic ideas cohered nicely with
the revelation of Jesus. But the fusion of gnostic ideas with Christianity also
meant mutilating the New Testament and ditching the Old altogether. It meant
giving up on the idea of a God who cared for his people and was willing to die
to save us. It meant dividing the world into people who were by nature
spiritual, and those destined for death. And it meant giving up on the hope that
the world might be redeemed, settling instead for a future where those lucky
enough to have the magic spark within their souls could escape the world and
leave it for dust.
The first generations of Christian theologians fought to steer the church away
from these harmful ideas, including Irenaeus, Justin Martyr and Origen. In so
doing, they made a huge contribution to the fundamentals of our faith.
We can see the influence of this battle in the creeds. For example, the first
article of the Nicene Creed asserts that God the Father created the heavens and
the earth. This corrected the gnostic notion that a truly good God would never
have anything to do with something so rotten as creation. Instead, the creed
reminds us that God made the world good, that despite its fallen state, it still
bears his likeness, and through his unfolding plan, he intends to make it
perfect.
By the fourth century, the main controversy was over the ideas of an Alexandrian
Christian teacher called Arius. He claimed that Jesus Christ, the Son of God,
was not truly divine, nor an eternal Person of the Triune God, but rather a
created being.
This might sound like a technical issue, but the consequences are massive. If
Jesus is not God, then he has no power to save us. The Christian hope is that
God came down to bring his life to a dead world. But if he isn't truly God, but
a lesser being, not much more than an angel, then he doesn't possess God's life,
so he can't do any of that.
The Nicene Creed was formulated to try and specify exactly what was wrong with
this view. Thus we get the assertion that Jesus Christ, the Son of God, is of
one substance with the Father, light of light, very God of very God, who for our
sake and for our salvation was incarnate by the Holy Ghost of the Virgin Mary,
and was made man.
But maybe none of this is new to you, and perhaps all this chat about heresy is
summing up for you exactly why you aren't into the creeds. If it's just a tool
for manhandling fourth-century heretics, then why should I care about it today?
Well, I could point out how the same heresies have repeatedly re-occurred
throughout church history, including the present -- but instead, I'll highlight
that the creeds are not in fact just a stick for bashing heretics with. Some
words are surgically inserted to force Arius to make a choice, yes. But that's
not the whole story.
Large parts of the Nicene Creed were not up for discussion at the Councils which
formed them. For example, nobody questioned the basic trinitarian form: 'We
believe in God the Father ... and in Jesus Christ, the Son of God ... and in the
Holy Spirit.' So something else has to be playing a huge role here.
Indeed, we have evidence that the trinitarian formula was one way that
Christians had been confessing their faith at their baptism since the early
second century. By the time of the Nicene Creed, it was probably dominant. So
the Nicene Creed isn't just a list of things Arius can't say: the bulk of it
comes from an existing tradition built up within the church from its earliest
days, for Christians to affirm to other Christians the basics of what we
believe.
Furthermore, the creeds are far from unimportant. Even if you're not part of one
of those denominations, representing an overwhelming majority of global
Christians, which use the creeds to aid their worship, the creeds should matter
to you. They are formed in large part from material from the New Testament. They
represent apostolic and catholic teaching. And they remain the best symbol of
what Christians believe both within the church, and to the world outside the
church.
As for me, I'm trying to memorise the Nicene Creed. If you don't know it
already, I'd recommend you do, too!

View File

@@ -0,0 +1,415 @@
---
title: "381: how the church as we know it was made"
description: >-
The church which defines our world now is in a significant way the one which
emerged out of sixty years of controversy from the Council of Constantinople
in 381. I've been charting what happened, why, and the ongoing legacy.
pubDate: 2025-10-05
---
Athanasius defined the fourth century. Not that he was a god, or even a king, or
that he always got his way. But he wrote the history books. His tale of an epic
battle fought tooth-and-nail between Arian heretics and him and his loyal allies
has come to be the standard account of how, over the course of the fourth
century, the Church redefined what orthodoxy means and how it is declared and
identified.
The result was the Nicene Creed. It had been first written for a very particular
polemical purpose in 325, but later found itself the centre of a strange
theological revival, and was finally revised in a council at Constantinople
in 381. In so doing, the bishops assembled a recognisable 'Nicene' tradition
which is still one of the defining features of planet Earth.
For better and for worse, the church as we know it has a capacity both for great
humility, faith and submission to the mystery of God, but it also has a capacity
for great intolerance. This is the church created in 381.
To understand the church as we know it today, then, we need to understand the
complex, confusing journey from 325 to 381.
Athanasius' chronicle of that journey is temptingly simple. The only problem
with it is that it isn't true. Indeed, his 'history' was never meant to function
as an all-encompassing narrative of Church history, to be read for centuries
ever after. His accounts function as polemics, meant to cajole, condemn and
persuade his readers in his own time of his vision for their future.
Nevertheless, whatever Athanasius' real significance in how his times unfolded,
his witness is important. He fully inhabited his times, often in the middle of
the fray. Whether or not we buy Athanasius' portrayal of himself as fighting the
good fight, he was certainly a fighter. By looking through his eyes, then, we
can get a perspective on how the Church as we know it came to be.
So it makes sense to start with him. As a young priest in his native Alexandria,
he became tangled up in a controversy which would come to define his career. A
strong-minded and fearless young priest had begun to preach. His name was Arius.
---
According to the Egyptian tradition, Alexander, the bishop of Alexandria, was
the nineteenth in a direct line of succession from Mark the Evangelist himself.
With a great deal of justice, he would have regarded himself as one of the most
important Christian leaders in the world, and at least the equal of the bishop
of Rome.
Small wonder, then, that the insubordination that plagued his diocese bothered
him. First Erescentius had started a schism, disputing the rule he used for
calculating the date of Easter.
Then there was Meletius. During the persecution under the Roman emperor
Diocletian, Meletius had already rubbed a few people the wrong way: while other
bishops were in hiding or in prison, he took the initiative to resolve problems
and ordain priests without properly consulting the absent bishops'
representatives. Perhaps it was intended kindly: it was seen as meddling. Now
Meletius accused Alexander of being too soft on Christians who had caved into
the threat of torture and made sacrifices to the pagan cults. When he decided
Alexander was never going to match his high rigorist standards, he broke away,
too.
Alexander must have longed for the relatively good order of the Greek and Roman
churches, where bickering subordinates were generally willing to let their
bishop have the last say. The throne of St Mark was in trouble. If Christ's body
wasn't to get chopped up any more than it already was, he needed to establish
his personal authority.
This was the context in which Arius, a young firebrand priest, steps onto stage
right. He surely knew his own bishop's teaching: God is one substance and one
essence, unchangeable, indivisible. Christ his Son is in every way God: God from
God, light from light, true God from true God, eternally begotten of the Father
before all ages. How else could Christ, by adopting human flesh, mediate the
transcendent God to fallen humanity?
But Arius didn't like this one bit. If God is unchangeable, how could he adopt
flesh? That suggests he was not flesh, and then became flesh. And in any case,
if the martyrs were right to give up their lives to know God, he must have the
perfect, uncompromising transcendence which the martyrs so admired. But how can
God adopt flesh, never mind suffer and die on a cross, without compromising that
transcendence? Something had to give. For Arius, the solution was to modify the
relationship between the Father and the Son.
Arius accepted that Christ had to be in some sense divine, in order to mediate
God to humanity. But he denied that he was quite as much God as God is. He has
something like his Father's essence, not in a co-equal way, but rather in a
derivative way. This makes sense of Father-Son language, which suggests the
Father came first, and the Son came next, a derivative of the Father. So the Son
is God from God, but not true God from true God. The Son was begotten in time,
and is not eternal: only God the Father himself is eternal.
At another time in another place, Arius might have passed for a creative,
independent thinker without much notice. But Arius was directly contradicting
Alexander just as the latter was desperate to assert his authority. It got ugly.
Alexander called a council of local bishops in about 320. The council condemned
Arius and removed him from his post as priest. In response, Arius went on the
campaign trail, visiting bishops in Palestine and Asia Minor who he thought
would be sympathetic to his theology. Shortly afterwards, he returned to
Alexandria, triumphantly brandishing vindications from two councils, one in
Jerusalem and one in Bithynia. He wasn't going to make it easy for Alexander.
Luckily for Alexander, the Emperor Constantine had just united the eastern and
western halves of the Empire. He had famously converted to Christianity after
seeing the sign of the cross at the Battle of Milvian Bridge in 312, and saw the
bishops as means towards his mission of uniting the Empire under one government
and one God. Constantine had been made aware of the dispute between Arius and
Alexander, and he didn't want schisms in the church any more than Alexander did.
He called a council in his own palace in Nicaea, paying the travel expenses and
hotel bills of all the bishops in attendance. For those bishops, many carrying
the scars of torture they had endured under Diocletian, it must have been a
bewildering experience. Alexander was in attendance. His secretary was
Athanasius.
In 325, the council condemned Arius. To avoid anyone else following in his path,
they produced a statement of faith, designed to exclude Arius' teaching, no
matter who taught it. This statement of faith is now known as the Nicene Creed.
The council also fixed the date of Easter to boot. Alexander must have been
relieved.
You might have thought that would have been the end for Arius. In fact,
Constantine engineered his re-admittance into the church as soon as 328. Arius
died in peace in 336. Constantine's mission wasn't to purge the church, but to
unite the church. As long as all sides worshipped God and could live in peace,
he wanted as many people as possible included. His mission was unity, not
uniformity.
Bishops like Eusebius of Caesarea in Syria got this. He had been provisionally
excommunicated on suspicion of Arianism in 325, but was reconciled at Nicaea
given the chance to explain himself and sign up to the Nicene Creed. No sooner
had he done this, however, than he had started explaining to the faithful back
home how they could carry on believing that the Son was not really eternal, even
as the Creed was designed to exclude exactly such a claim. While Eusebius might
seem duplicitous, at the time, this was exactly the kind of tolerant pragmatism
that Constantine asked of the bishops: as long as they didn't cause more
out-and-out conflict.
Alexander didn't have long to enjoy the peace of Nicaea. He died just a few
years afterward in 328. The throne of St Mark passed to Athanasius.
---
The peace didn't last long. Just as Athanasius was donning his mitre, Eusebius
was plotting against Eustathius the bishop of Antioch, and engineered his
deposition. In his defence, Eusebius accused Eustathius of the long-condemned
heresy, Sabellianism. Then in 335, he followed up by deposing Marcellus, the
bishop of Ancyra, at a council in Tyre.
To defend his action, he wrote _Against Marcellus_, in which he accused
Marcellus of being a Sabellian, too. Sabellius' heresy was (to borrow a modern
term) modalism, the view that 'Father', 'Son' and 'Spirit' are mere titles,
aspects, 'modes' of God, not in any real way distinct. He also accused Marcellus
of adoptionism, another agreed heresy. Marcellus taught that the Son only became
an aspect of the divine nature at the Incarnation, and that in the last day,
Christ would hand over his kingdom to his Father.
This action would cast a long shadow over the next half-century. Time and again,
bishops allied to Eusebius' way of thinking, or 'Eusebians', would re-affirm
their opposition to that 'heretic' Marcellus and his 'Sabellianism'. This is a
crucial dynamic for understanding where theological factions drew up their
battle lines, and for what compromises were needed in order to get to 381.
Even the bishop of Alexandria wasn't immune from Eusebius' purge. Athanasius had
vigorously defended his ally, Marcellus, at the council of Tyre in 335. Eusebius
set about plotting his downfall. He dug up dirt. He accused Athanasius of using
threats and bribes to get himself elected, and sending goons to beat up his
political opponents. Once he'd found evidence of Athanasius meddling with the
crucial Egyptian grain export that kept Rome fed, he had the emperor on side.
Constantine convened a meeting in 336 and exiled him to the German frontier.
---
Or at least, that's how Athanasius tells it. Athanasius loves a plot: at the
time, alleging a conspiracy was a classic rhetorical technique for painting your
enemies as heretics.
Eusebius was no stranger to rhetoric himself, and it's to his 337 best-seller,
the _Life of Constantine_, that we owe our standard account of Constantine's
reign. He regarded Empire and Church as allies in a joint mission, to unite the
world under one government and one faith. To him, someone like Athanasius,
constitutionally incapable of tolerating anyone who disagreed with him and
willing to use gangster tactics to get his way, was a threat to this divine
mission.
It's worth remembering that after Constantine died, Athanasius would be
re-exiled by four more Roman emperors. In his lifetime, only Julian failed to
exile Athanasius, and him only perhaps because he didn't have time in his
whirlwind twenty-month reign. We also can't be sure how much influence Eusebius
actually had in the expulsion of Athanasius and his allies: it coheres well
enough with the emperor's anti-sectarian agenda that it might have happened with
or without Eusebius' involvement.
Perhaps Athanasius was a brute. Still, the Roman Catholic Church manages to
venerate both Eusebius and Athanasius as saints. This may seem like a
contradiction. But perhaps an ability to tolerate contradiction is precisely the
legacy of 381.
But we're not there yet. By 335, Eusebius had engineered the exile of
Eustathius, Marcellus, and Athanasius. After Constantine died, he had to do it
all over again, but by 339, he had persuaded his successor, Constantius, to
re-assert his father's exiles of the three men. With the Empire once again
split, Athanasius and Marcellus headed to Rome to re-group and re-think.
---
From Rome, Athanasius and Marcellus were safe for now from Eusebius' clutches,
but also relatively impotent. In this period of exile in the 340s, in an effort
to claw back his reputation, Athanasius developed the polemic which still
defines the standard history of the fourth century. He invented a cunning label
for Eusebius and his cronies: he called them 'Arians'.
Eusebius rejected the label as ridiculous. Arius had been reconciled, and more
to the point, had died in 336. For that matter, why would a bishop follow the
teaching of a mere priest? Not only that, but the label ignored significant
differences between Arius' and Eusebius' teaching. His verdict was clear: the
label 'Arian' is a baseless slur, with no other purpose than to tar his
reputation as a heretic.
He was right, of course. But like it or not, Athanasius' theory of an Arian
conspiracy began to win adherents, not least Julian, the bishop of Rome. Julian
called a council to exonerate Athanasius and Marcellus. When the Greeks refused
to turn up, he called a local council anyway and vindicated the two men. In the
face of Greek obstinacy, Julian wrote east, pleading the bishops to take the
'Arian' threat seriously.
In response, the easterners held a council in Antioch in 341, agreeing four
creeds which powerfully condemned Marcellus' teaching, including the influential
Dedication Creed. This includes assertions that Father, Son and Spirit are
'three in subsistence, one in agreement', that the Son was generated before time
began, against Marcellus' teaching that the Father, Son and Spirit are aspects
of God without division in subsistence, and that there only came to be a divine
Son at his incarnation. They explicitly condemned Arius, Sabellius and
Marcellus.
So the divisions grew deeper. Without an emperor to compel the bishops to come
together, there may not have been much chance of a rapprochement. But even if
there were to be such an emperor, who's to say that their settlement would have
satisfied the bishops?
---
Meanwhile, in the 340s and through the 350s, two further theological movements
gathered steam: the homoians and the heterousians.
The homoians, perhaps tired of the squabbles between the Athanasian and Eusebian
factions, determined to sidestep their petty debates altogether.
A key term of the theological disagreement was 'essence' or 'ousia'. Athanasius,
in his lifelong battle to make sure Arius stayed dead, insisted that Father, Son
and Spirit shared the same ousia. In contrast, Eusebius, with his anti-Sabellian
polemic, needed to assert the real distinction between Father, Son and Spirit,
and so asserted that each had a separate ousia. So the difference can be summed
up as a counting problem. How many divine ousias are there? One or three?
The homoians claimed that both sides were mistaken, simply because they used the
word 'ousia'. There is no mention of ousia in Scripture, so, they claimed, we
have no basis for asserting it of God one way or the other. All we can truly say
is that Father, Son and Spirit are distinct but somehow alike. Whereof we cannot
speak, there must we remain silent.
This might have worked as a way forward, except that the heterousians provoked
such a strong reaction that 'ousia'-talk was needed to refute them. Aetius, and
his followed Eunomius, argued that since God is simple, and all generate things
are divided, it follows that God is ingenerate. But the Son is generate:
therefore Father and Son must be altogether unalike. They expressed this by
saying that Father and Son are unlike in ousia. This teaching was swiftly
branded 'neo-Arian', provoking a strong reaction. To counter the heterousian
teaching, their opponents were forced to fight on their terms, and that meant
using 'ousia'-talk.
Thus enters Basil of Caesarea. He argued that if we abandon 'ousia'-talk, we
will have no way of saying that the Father and Son have anything in common at
all, which makes a nonsense of the idea that the Son brings humanity knowledge
of his Father. Without like essence, they might as well be two completely
different Gods. Therefore we have to say at least that they have like essence --
'homoiousia'. But without direct access to perfect knowledge of the invisible
God, we're not in a position to judge that they have exactly the same essence,
so he stopped short of agreeing with the 'homoousia' of the Nicene Creed which
Athanasius so treasured.
Seeing the opportunity to make common cause against the homoians, Athanasius
started to soften. He wrote an extremely charitable commentary on Basil's
theology which emphasised their similarities and papered over their differences.
Athanasius recognised that both he and Basil wanted to assert the unity of God
while still preserving distinctions between Father, Son and Spirit. The two
began to campaign against the homoian movement.
But Basil got there too late. In 359, the emperor Constantine II called a
council in Constantinople, and in 360 it issued a homoian creed with full
imperial backing. Any campaign against the homoians would have to take place sub
rosa.
---
In Athanasius' and Basil's long, slow campaign against homoianism, their weapon
of choice was surprising: they dusted off the Nicene Creed of 325. Athanasius
argued, against the homoians, that 'ousia'-talk, although not directly
Scriptural, was essential in order to draw out the consequences of Scripture
while ruling out Arian mis-interpretations.
Thus Nicaea, conceived as a one-off meant to clean up the Arian controversy,
found a new life as the anti-homoian movement -- or perhaps you could call it
the Nicene revival? -- rallied around it.
As the movement progressed, the formerly disagreeing bishops found ways to come
together. An essential move was that made in Athanasius' _Antiochene Tome_
of 362. In it, he relented on his long opposition to there being three
'hypostases' or 'substances' in the Godhead.
'Hypostasis' had for a long time been used interchangeably with 'ousia'.
However, Athanasius claimed that perhaps God could have three hypostases, but
only one ousia, at the same time. In so doing, he wedged apart a sharp technical
distinction between 'hypostasis' and 'ousia' which previously wouldn't have made
sense. Logical or not, it enabled the Nicene revival to have its cake and eat
it. God is both one in ousia, protecting against Arianism, and three in
hypostasis, protecting against Sabellianism.
So the Nicene revival gained a new superpower: the power to use formerly
synonymous terms to assert contradictions without blushing. This power to accept
apparent contradiction as part of the unknowable mystery of God is perhaps the
most important legacy of the period. Arguably, the church has been at its best
when it has put aside the need to know everything, and embraced this spirit of
tolerance, humility and faith.
---
For much of the 360s and 370s, the homoian emperor Valens had ruled over the
eastern part of the Empire, while his big brother, Valentinian, ruled the west.
In the late 370s, Valentinian and then Valens died within quick succession of
each other. Valentinian's twenty-year-old son, Gratian, was left to clear up the
mess. In 379, Gratian delegated rule of the east to Theodosius, who was to
implement a decisively different religious policy than his predecessor, Valens.
In 380, Theodosius issued an edict, saying that only those who agreed to the
homoousios clause of the Nicene Creed could be considered 'catholic' Christians.
The message was clear: the homoians were out, and the Nicenes were in.
In 381, he called a council to Constantinople, and it (probably) issued the
revision of the 325 creed which is still used in various versions in all the
world's largest Christian denominations. There would be no more revisions, and
it would become, then as now, compulsory reading for all those preparing to don
vestments.
One question is, why did the 381 creed differ in the ways it did from 325? Many
of the differences, including the much-enlarged section on the Son, seem to have
little controversial content: nobody was disputing that Jesus was born of the
Virgin Mary, for example, though she makes her first appearance in the Creed in
the 381 version. Some historians think this suggests that the 381 was based on a
similar, but distinct creed from 325. This seems unlikely to me, given that
about half the creed is in verbatim agreement with 325.
However, a couple of edits stand out. There are some clear signs of
anti-Marcellianism: 'his \[the Son's] kingdom shall have no end', the Son is
begotten of the Father 'before all ages'. Perhaps a clear emphasis on the
eternal relationship between the Son and the Father was part of the diplomacy
needed to get the Eusebian faction on-side.
The new details on the Holy Spirit are interesting too. They suggest a delicate
compromise. Some bishops were reluctant to suppose that the Father and the
Spirit have the same essence. On the other hand, others reckoned that they must
share the same essence, given that they are equally deserving of worship. Thus
the creed does not have a 'homoousios' clause for the Spirit, but does assert
that the Spirit 'together with the Father and with the Son is worshipped and
glorified'. With a spoonful of humility, both sides can be satisfied with that.
The revised Nicene Creed was the focus point, the distillation of a growing
theological movement, formed by the various anti-homoian bishops finding a way
to keep true to their own convictions while respecting each other's red lines.
As a result of the context of 325, Athanasius' relentless anti-Arian polemic
which kept that movement alive, and the 'neo-Arian' heterousian movement, the
new Nicene tradition insisted on the full co-equal divinity of Father, Son and
Holy Spirit. This doctrine ensures Nicenes can affirm that Christ mediates true
knowledge of the transcendent Godhead to humanity: the one who was born of Mary,
suffered and died on the cross, was raised from the dead and ascended into
heaven was true God from true God, of the same essence as his Father.
To satisfy the Eusebian strain, which defined itself by opposition to Marcellus,
the Nicene tradition included a commitment to a robust distinction between
Father, Son and Spirit, and to the eternity of the Son: begotten of the Father
before all ages, his kingdom shall have no end. As a result, Nicenes inherited a
way of thinking about God's action in the world, as instrinsically co-operative
without being divided.
The biggest change between 325 and 381 was not the text, but what the text is
used for. In 325, the Creed functioned to condemn Arius in order to heal the
divisions his teachings had caused. In its second life, the Creed found an
altogether new purpose: to serve as a common statement of orthodox faith. It
started life as a way to define who was out. It ended up defining who was in.
Where was Athanasius? Consider that when Athanasius was appointed bishop in 328,
he was relatively young for a bishop at thirty-five. That means that in 381, he
would have been the ripe old age of eighty-eight. In fact, he didn't make it
that far: he died in peace in the countryside outside his native Alexandria
in 373. If he had seen the outcome of 381, he might have regarded his life
project complete. Perhaps he knew that with the new generation of bishops, the
tide was turning for good, and died in peace. Perhaps not. Either way, his
compromises, and his beloved homoousios, have left a permanent mark on the
church.
This is the legacy of 381. It is two-faced: any common statement of faith can be
used to exclude. Indeed, in the late fourth century, both non-Nicene Christians
and pagans found themselves the victims of increasing state-backed sectarian
violence.
However, 381 also bears witness to the power of humility and faith. Once we stop
grasping at perfect knowledge we cannot attain, we can begin to appreciate the
mystery of God. This is one legacy I hope we can carry forward into our century.

View File

@@ -0,0 +1,171 @@
---
title: Arianism
description: >-
I'm summarising what I've learned recently about Arianism: the heresy par
excellence, named for the early-fourth-century Alexandrian priest, Arius. I'll
conclude with some reflections on why we still need to reject Arian
temptations and affirm Nicene orthodoxy today.
pubDate: 2025-10-09
---
Arianism neither started nor ended with Arius. When he preached in the 320s, he,
like so many of his contemporary Alexandrians, only followed Origen in
subordinating the Son to the Father. In Alexandria, there was a strong emphasis
on the absolute transcendence and perfection of God, and therefore the
difference between that and the Jesus who was born, was tempted, suffered and
died. Arius was unremarkable in that respect. He was only remarkable in drawing
the logical conclusion: since God is indivisible, ingenerate, immutable, eternal
and impassible, but the Son of God was begotten, born of a woman, was tempted,
suffered and died, it follows that the Son of God is not fully God. The image of
the Father, sure, but not sharing in his Godhead: that wouldn't do justice to
the Father's Godhead.
The movement later characterised as 'Arianism' did not share all his teaching.
In particular, the idea that the Son was begotten in time -- that 'there was
when he was not' -- was a slur, and respectable Arians accepted that the Son is
eternal. Some historians deny that there was any coherent movement worth calling
'Arianism', however, I think the creeds and councils of the fourth century show
that there was a theological movement, self-consciously and unashamedly
associated with Arius, which privileged God's transcendence over the Godhead of
the Son. So, although the name 'Arianism' is certainly intended to be
derogatory, and there was surely no conspiracy to follow Arius as such, I think
the term 'Arianism' is meaningful and has a referent.
Whether or not individual bishops completely agreed with Arius theology, very
many were sympathetic to his pro-transcendence leaning. So, when he applied his
considerable arts of persuasion to influential likeminded bishops like Eusebius
of Nicomedia, he succeeded in establishing a considerable alliance behind him.
What had already been a theological strain was forced by the heat of controversy
to coalesce into a faction.
Arius and his supporters were set back temporarily by the Council of Nicaea in
325, when the emperor Constantine personally backed the homoousion under the
impression this would resolve the dispute. But Nicaea lost influence very
quickly, and the Eusebian faction gained ground. By 328, Arius himself had been
reconciled into the church by Constantine. And through the 330s, Eusebius of
Caesarea, another Arian sympathiser, repeatedly engineered the exile of key
supporters of Nicaea, including Eustathius, Marcellus and Athanasius.
In this period, it may be fair to characterise Athanasius as standing more or
less alone in fighting against Arianism. This changed somewhat in the 340s, when
he gathered the support of the bishop of Rome and many other western bishops in
his cause. He remained anathema in the East.
He still had a great deal of sympathy back in Egypt, however. The imperial
administration had installed Gregory of Cappadocia, a stalwart Arian, in place
of Athanasius as bishop of Alexandria from 339 until 346. Yet when Athanasius
returned to reclaim his see in 346, he was greeted with, according to Gregory of
Nazanius, 'universal cheers ..., nightlong festivities, the whole city gleaming
with light, and both public and private feasting'. And when Arius was exiled
again, and another Arian, George of Cappadocia, again installed in his place in
356, the results were riots. When George attempted to carry out one of his key
roles as bishop -- distributing money to widows -- many widows had to be beaten
to accept money from his hands. After five years, George was lynched in 361. The
people of Alexandria were roundly behind their local hero, and did not take
kindly to Rome imposing their agenda on them by force.
While Egypt held strong for Athanasius, the Arian party became ever more
triumphant in the rest of the Empire. In 358 and 359, a series of fraught
councils in East and West produced conflicting resolutions, and the emperor
Constantius resolved in 360 to get the situation under control. He called a
council to Constantinople and ensured an even result. These were the homoean
creeds, asserting that while the Son is like the Father, we cannot and must not
say anything about their ousia.
These creeds have subsequently been called the 'Arian' creeds. The reason for
describing the homoean party as 'Arian' is, first, that they explicitly rejected
Nicaea and the homoousion, and second, they failed to affirm the full Godhead of
the Son, saying only that the Son is 'like the Father' and that he is 'God from
God', pointedly omitting the Nicaean elaboration, 'true God from true God'.
The councils of 358-360 were chaotic. Councils overrode councils. The emperor
rejected creeds and forced his own ones through. Swathes of bishops were
banished or deposed. Amidst all the chaos, Athanasius found himself making
unexpected friends: the Cappadocian Fathers, including Basil 'the Great', while
preferring 'homoiousion' or 'like essence' to his Nicene 'homoousion' or 'same
essence', ended up on his side against the triumphant homoeans. The violence of
those councils bred hostility, and with a common grudge, a coalition had
suddenly formed against the homoeans and around Nicaea.
Perhaps Constantius might have been able to force his way, but he died in
only 361. His replacement was Julian. While Julian's reign was short, he brought
about a sharp change in direction.
Julian, 'the Apostate', had converted from Christianity to paganism, and during
his reign, the alliance between Church and Empire was briefly severed. In a
deliberate attempt to sow chaos, he refused to mediate on behalf of the Church
and allowed all banished bishops to return from exile. At one time, there were
five competing bishops all in Antioch.
One result of this severance was that bishops were free to form their own
alliances and make their own case. As a result, the Nicene alliance emerged from
Julian's brief reign decisively stronger.
The Nicene alliance would have to wait until 379 for a sympathetic emperor. But
once Theodosius acceded, the Nicene victory was absolute and irreversible. He
decreed that all clergy had to agree to the Nicene Creed, and called a council
to amend and affirm the Creed.
This did not mean that Arianism died out. Eusebius of Nicomedia had sponsored a
mission to the Goths, and, being outside of the emperor's grasp, they held
strong to their Arian convictions for centuries after. Indeed, when the Goths
later took possession of large parts of the Western Empire, Arians may well have
significantly outnumbered Nicenes in the West, long after the matter was settled
within the Empire. And certain theologies today, which seek to reduce Jesus to a
mere emanation from or pointer towards a transcendent God, or a religious genius
or a spiritual guru, rather than the real presence of God, are Arian in so far
as they seek to protect the transcendence of God at the cost of his choice to
dwell with us in Jesus Christ.
So it's worth considering what's lost in the difference between the Nicene faith
which is now indisputably Christian orthodoxy, and Arianism in all its forms. If
a preacher today elides away Jesus' full Godhead, what does it matter?
Three problems arise in consequence. One is that, if Jesus is not true God, then
his miracles are meaningless. This is particularly problematic for those who
want to read the Gospels as mere myth without affirming the truth of any of its
particular historical content. If Jesus is not true God, then the miracles lose
their mythic function. If Jesus is not true God, then a story about him healing
someone far away has nothing to do with me. But if Jesus is true God, if he is
Emmanuel, then his healings have the power to function as a mythic sign,
pointing to something about God's plans to redeem the world. For his miracles to
work as myth, whether or not they are true history, Jesus must be true God.
Secondly, if Jesus is not true God, then what kind of salvation can he offer? If
a mere man can save us from our sin, what does that mean about sin? If we can be
saved by a religious genius, a guru, someone specially in-touch with the
spiritual reality, then salvation is no more than fixing up the material world.
How's that going, two thousand years on? Has the Church made any progress in
translating Jesus' teaching into universal peace? No doubt the Church has done
some good -- but it doesn't seem credible that the Church is about to fix the
world's problems by following Jesus' self-help agenda. In contrast, if Jesus is
God, we can affirm the biblical notion that sin is a crime against God, which
separates us from him. Since it is a crime against God, only God can bring about
reconciliation. And since God has proven that he is bringing about that
reconciliation in Jesus Christ, we can hold strong to our trust in his promise
not simply to fix the world according to its own rules -- for that would be
impossible -- but to change the rules in a second creation.
Finally, if Jesus is not true God, then we have no way of knowing anything at
all about his relationship to God. If he is not true God, then all we can see in
Jesus is a man. We can guess at some super-spiritual connection if we like,
seeing his words, deeds and miracles as evidence of some semi-divine status. But
that would be pure guesswork, in other words, wishful thinking, in other words,
fantasy. Athanasius called it 'mania': pulling wild theological claims out of
your own head with no substantial basis in reality. But if Jesus is true God,
and God gives humans the gift of the Holy Spirit to recognise that Godhead, then
we can know that Jesus is true God by seeing him for what he is. This is not
fantasising without a grounding in reality, this is the most basic form of
knowing: seeing and believing. If Jesus is not God, then Christian faith is
fantasy, but if Jesus is true God, then Christian faith can stand firm.
With Arius, we have a religion that reduces the Gospels to fairy stories with no
relevance for you or me, a religion that reduces the Church to a struggling
self-help movement, and a religion that rests on fantasy. But if we stick to
Nicene orthodoxy, instead, we have a religion that reads the Gospels as true
myth, real history profused with life-changing theology; a religion that can
have hope for the world as still needing God's work of reconciliation to be
perfected, yet containing within it anticipations of that future, including in
the Church; and a religion that rests on true faith, certain knowledge derived
not from wishful thinking but encountering the very essence of God in Jesus
Christ.

View File

@@ -0,0 +1,227 @@
---
title: Why did the church become persecuting in the fourth century?
description: >-
In one generation, Christians in the Roman Empire went from officially
persecuted to becoming imperially-backed persecutors themselves. It's
important to understand why, to prevent the church from persecuting today.
pubDate: 2025-12-11
---
In the year 325, Constantine stood before an assembly of Christian bishops. He
had just the year before killed his last remaining rival in battle, leaving him
as the sole Augustus of the Roman Empire, from Brittania to Arabia. The bishops
must have assembled before him in reverent awe.
Many of them sported scars from torture they had endured in the reign of
Diocletian, Constantine's predecessor. Diocletian had sponsored an enormous and
brutal persecution of Christians. But that generation of bishops was witnessing
an epochal shift of power. Over his reign, Constantine would divert large chunks
of the wealth and influence of the Roman state into the safe-keeping of the
bishops. Under Constantine's leadership, the bishops would be transformed from
enemies of the state to the state's agents.
Official Roman persecution of Christians was decisively coming to an end. But
the tragedy of the fourth century is that rather than ushering in a new age of
religious tolerance, the bishops only continued the Roman habit of religious
persecution, directing the force of the Empire first against internal rivals,
'heretics', and then against pagans and Jews.
Why did Constantine bestow so much power on the bishops? Part of the answer may
be the creaking disfunction of the Roman state. The imperial systems for
protecting the poor were falling apart. The justice system was notoriously
corrupt, and was known to effectively be a means for the rich to get their way
by paying for the best lawyers and greasing the palms of the judges. The
poor-relief system, based on the magnanimity of local patrons, was stuttering as
an increasing proportion of the aristocracy's surplus wealth went to fund the
tottering military system, frequently consuming huge resources in ill-fated
expeditions against the Sassanids or fighting coups and civil wars between rival
emperors.
The bishops were already in control of an impressive poor-relief system within
Christian communities, and, unlike the Roman system, which rewarded rich
philanthropists with honours, the Christian system encouraged patrons to give
anonymously via their bishop, meaning the bishops were in control of how large
amounts of Christian money was spent. When Constantine ascended, they were ready
to go with their own bureaucratic systems independent of the imperial civil
service.
Constantine may have regarded the bishops, fresh out of persecution, as less
corrupt than imperial pen-pushers. However, in the long run, the effect of his
transfer of power was to transform the episcopate into an alternative civil
service, perhaps no less corrupt than the first. But how did this power turn
into persecution?
As the bishops became ever more powerful, Constantine and his successors became
increasingly dependent on their power. Bishops had huge moral influence over
their congregations, and their word had the power to stop -- or start -- riots.
Emperors also needed them to keep distributing poor relief, an important
foundation for the emperor's moral authority. When the hugely unpopular George
of Cappadocia was installed in Alexandria in 357, the local widows refused to
receive alms from him: as a result, they were physically beaten by George's
imperial goons.
Since the emperors needed the bishops' support, they became increasingly willing
to acquiesce to their demands. And one of the bishops' demands was that the
emperor use his authority to help them crush heresy.
The bishops of the fourth century inherited a dichotomy between orthodoxy and
heresy which had developed in the early church. Orthodoxy meant true belief,
defined and enforced by the bishop. Whoever promoted false beliefs, and together
with it insurrection against the bishop's authority, was defined as a heretic.
Orthodoxy was conceived of as the unchanging teaching of the apostles, who were
in turn taught directly by the Holy Spirit. Orthodoxy might have to be re-stated
as sneaky heretics sought to twist its language, but orthodoxy was never
supposed to be innovative: only heresy was innovative. Further, heresy was
always thought of as a combination teaching falsehoods, behaving immorally, and
refusing to take part in mainstream Christian community. It all came as a
package. Truth means right behaviour means loyalty.
It's difficult to explain exactly why this system emerged. It's true that faith
lies at the root of Christian religion, and that Christ taught that he is truth.
The Epistles are clear that false teachings can be dangerous, and Christians
have a duty to resist them. But that doesn't in itself explain why the bishop
gets to decide which teachings are true or false, nor why the myth of an
unchanging apostolic orthodox teaching should have prevailed over the idea that
Christian teaching can grow over time as it encounters new problems and
contexts.
This system may have been motivated by the need for a distinguishing feature to
ground Christian family identity in the absence of an identity based on
nationality, social class, sex, or religion. It may also have been some kind of
reaction or defence mechanism against persecution. In a world that was often
hunting for an excuse to persecute Christians, it was a matter of life and death
that Christian communities were tight-knit, loyal to one another, and visibly
living according to the highest moral standards.
Whatever the case may be, the result by the Constantinian turning point was that
bishops had significant influence over their local Christian communities, and an
ideological commitment to maintaining their communities' loyalty to the bishop
and his teachings.
And the bishops' desire to crush heretics only increased as the fourth century
wore on. With the wealth and power of the civil service increasingly transferred
to the episcopate, the aristocracy which had dominated the civil service
inevitably moved in to capture the episcopate. Those aristocrats guarded their
power jealously, and elections became increasingly marred by accusations of
corruption. When Athanasius was elected in 328, he was accused of being
underage, of bribing electors and of beating up his Meletian opponents once he
got in office. No doubt, the aristocratic bishops were more than happy to use
the church's concept of orthodoxy to keep out challengers, as Athanasius did
when he used the label 'Arian' to describe just about anyone who wanted him out
of power, no matter how distant their ideas were from those of Arius. As bishops
found the need to fight ever stiffer competition for their jobs, accusations of
heresy multiplied.
As a result of their dependence on episcopal power, Constantine and his
successors supported the bishops in their attempts to crush heresy. The bishops
appealed to the emperor to adjudicate on disputes, and the emperor responded by
calling councils such as Nicaea (325), Antioch (341), Constantinople (360) and
Constantinople again (381). Under the emperor's authority, bishops were exiled
from their sees, and some theological views were condemned as heresy while
others affirmed as orthodoxy, to justify the empowerment of some and the
dethronement of others. The particular orthodoxies implied by succeeding
emperors was not consistent, leading to some emperors and councils being known
to history as 'Nicene' and others as 'Arian'.
Apart from simply doing a favour for the bishops, the emperors had their own
reasons for wanting to defend the bishops from challengers. The bishops now had
the keys to the welfare system and the justice system. The emperor therefore
could not tolerate rival bishops fighting for authority. That would only
undermine those systems, which underpinned imperial power and moral authority.
The emperors may also have been motivated by the need to uphold true religion
and keep peace in the Empire. It was a universal consensus that, if the Empire
was to flourish, it would only be with God's blessing, and that would only
happen in turn if the people were united in acceptable worship. Before the Edict
of Milan in 313, which finally ended official persecution of Christians in the
Roman Empire, there had been a long debate about whether Christian worship
counted. It was controversial because Christian worship didn't look much like
worship at all to pagan eyes, in particular because Christians didn't make
sacrifices. When Constantine settled the issue in favour of Christians, it must
have signalled a step change, where acceptable worship became less about proper
rites and more about proper belief. This trend may have led emperors to regard
heresy as a threat to the Empire's security. Further, where there were schisms,
there was no peace, and the Emperor's mission, to unite the world under one
government in perpetual peace, was incomplete.
These forces amplified one another in a terrible feedback loop. As bishops
increasingly were empowered to define and enforce orthodoxy, they increasingly
monopolised local church leadership, which made them more desirable as imperial
bureaucrats, which meant they got more power, which meant they were more able
still to define and enforce orthodoxy. It was a spiral which led to the
definition of orthodoxy being continually sharpened (even as the myth persisted,
ever less plausibly, that they were defending pristine, unaltered apostolic
teachings). Eventually, it pushed bishops to support persecution not only of
Christians who disagreed with them, but also pagans and Jews.
Orthodoxy may also have become more important in the fourth century because of
the large number of new converts. With so much influx, insiders may have felt
that their core belief-identity was being threatened, and so will have enforced
orthodoxy more strictly, while outsiders may have felt the need to prove their
authenticity by strongly committing to orthodoxy. Committing violence against
heretics, pagans, and Jews may also have functioned as a way to prove that
you're an authentic Christian. This drive towards violence was pushed especially
strongly from the monastic sector, which exploded in scale in the fourth
century.
When orthodoxy gets sharp enough, it eventually gets sharp enough to cut the
church in half. To put it another way, bishops competed to get imperial backing
for their thinking, and therefore their right to power. Since this imperial
backing must have some consistency to remain legitimate, this means orthodoxy
gets standardised across the Empire, and that means that local differences of
opinion become international schisms. Although the Arian controversy never
resulted in a schism within the Empire, there were numerous schisms in the
fourth and fifth centuries, culminating in the epic Nestorian schism, which
split the imperial church three ways along Chalcedonian, Antiochene and
Alexandrian fault lines.
My main reaction to this period of church history is dismay. It seems to me that
the church was captured by the Empire and the aristocracy. The church became in
large part a way for powerful people to grab, hold onto and accumulate power.
When that happens today, the Gospel is suppressed, and the church loses moral
authority.
To avoid this happening again, we ought to protect the right of Christians and
others to believe and gather free from persecution. True belief is important,
but that doesn't mean we should attempt to compel agreement. Christian leaders
cannot enforce their teachings if dissatisfied Christians can just go to the
church next door.
Opening communion also disempowers those forces which seek to enforce orthodoxy.
If the bishop can't bar you from taking communion, they can't force you to
accept what they teach or to support their political programme.
Finally, established churches are vulnerable to the perverse incentive
structures of the state, and must be disestablished. The Church of England
should not have seats in the Lords and should not crown British monarchs.
I believe in one holy, catholic and apostolic church -- but I do not believe in
one opinion or one authority. My realistic ideal of church unity now involves a
plurality of disestablished denominations which robustly disagree with one
another on important points of belief, but which admit one another to communion
and are willing to work together for the sake of the Gospel.
I have to caveat my pessimism about the fourth century. As much as I regret the
imperialisation of the church, I remain attached to the particular orthodoxies
which it produced at Nicaea, Constantinople and Chalcedon. I've been convinced
that they are important ground truths for theology, and have stood the test of
time because they are intellectually robust. Other creeds and councils
(including creeds from fourth-century councils) have been forgotten, but these
stand tall. I suppose that Nicaea, Constantinople and Chalcedon give good
guardrails for theology, and, whatever the political forces which gave rise to
them, have been subsequently vindicated by their theological fruits and by the
enduring testimony of the church.
In summary, the church became increasingly persecuting in the fourth century as
a result of the entangled interests of, on the one hand, an increasingly landed,
aristocratic episcopate which needed to protect its influence amidst stiff
competition, and, on the other hand, of embattled emperors who regarded the
bishops as a better way of exerting the Empire's power and achieving the
Empire's mission amidst the failure of the old imperial systems: provided they
could be kept happy and kept in unchallenged power. This persecuting force
produced the church's foundational ecumenical creeds, but was just as effective
at producing disharmony as enforcing harmony, and ultimately led to the massive
and ongoing Nestorian schism. This is a sober lesson for today's church, and
should move us to protect freedom of belief and gathering for all, to
disestablish the church and to open the communion.

View File

@@ -0,0 +1,77 @@
---
title: A paradox about 'should'
description: >-
I seem to have accidentally proven that drinking wine both is and is not a
thing you should do. Let's hope that wine doesn't disappear in a puff of
logical smoke.
pubDate: 2025-01-28
---
We're pretty familiar with the idea that there can be reasons for doing
something, and reasons against. Drinking wine is bad for your liver, but good
for your social life.
But look what happens if we express this in this way:
1. Drinking wine is bad for your liver.
2. You shouldn't do things which are bad for your liver.
3. All things you shouldn't do aren't things you should do.
4. Therefore, drinking wine isn't a thing you should do.
In contrast to this:
5. Drinking wine is a good social activity.
6. You should do things which are good social activities.
7. Therefore, drinking wine is a thing you should do.
Now both 1-4 and 5-7 seem like logically valid arguments with true premises,
but 4 and 7 are contradictory!
I don't think there's any use in complaining about premise 3. All that gives us
is the possibility that wine is both a thing you should do, and a thing you
shouldn't do. But that's an absurdity. Something can't be both obligatory and
forbidden at the same time. It's scarcely any better than a contradiction: it
is inconsistent with any useful concept of obligation.
Remember that we would quite like to know, at the end of all our argument,
whether we should drink wine or whether we shouldn't. 'Both' is not an adequate
answer, because it's not a useful guide for action: we can't both drink wine and
not drink wine. So if 'should' is to function as we need it to, 3 must be true.
I think a more profitable way forward is this. Let's re-write premise 2:
2. All things which are bad for your liver are things such that the fact that
that thing is bad for your liver is a reason not to do it.
We can similarly re-write premise 6:
6. All things which are good social activites are things such that the fact that
they are good social activities is a reason to do them.
Then premise 3, if it's to play the same logical role in the argument, would
have to read:
3. All things such that the fact that that thing is bad for your liver is a
reason not to do it is not a thing such that the fact that that thing is a
good social activity is a reason to do it.
Our re-written 2 and 6 seem to adequately capture the sense of the original,
but 3 is now obviously false. With our re-written sentences, we can avoid
generating a contradiction without doing any fatal damage to our concept of
'should'.
OK. Grant for the sake of argument that that was a good move. What have we
achieved? Have we actually solved the problem?
We started out with a pair of arguments which generate a contradiction. Our
move generated a new pair of arguments which don't generate a contradiction. So
what? Isn't the first contradiction still there?
Perhaps you could say that our re-written version of 2 (and so on) are more
accurate elaborations of the originals. Fine. But what about those originals,
then? You've still either got to say that they're true, false, or gibberish.
Producing your second argument hasn't convinced me to move my opinion about the
originals. The original premises seem just as true, and the original arguments
just as valid, as when we began.
What gives? Answers on a postcard as usual please.

View File

@@ -140,6 +140,12 @@ const LINKS: Link[] = [
description: 'There was no long-term price inflation from 1200 (when these data begin) until 1550. WHAT?!', description: 'There was no long-term price inflation from 1200 (when these data begin) until 1550. WHAT?!',
isoDateAdded: '2025-01-23', isoDateAdded: '2025-01-23',
}, },
{
href: 'https://freeourfeeds.com',
title: 'Free social media from billionaire control',
description: 'I just donated $40. These guys are promising to do whatever it takes to make sure the AT Protocol is genuinely owned by everyone.',
isoDateAdded: '2025-01-28',
},
]; ];
export default LINKS; export default LINKS;

View File

@@ -0,0 +1,96 @@
---
import OtpDialog from "../components/OtpDialog.astro";
import Page from "../layouts/Page.astro";
---
<Page title="Contact" description="Contact Joe Carstairs">
<OtpDialog />
<form class="contact-form">
<h1>Contact me</h1>
<p hidden class="error"/>
<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>
import { resendOtp } from '../scripts/contact/resend-otp';
import { submitContactForm } from '../scripts/contact/submit-contact-form';
import { submitOtpForm } from '../scripts/contact/submit-otp-form';
import type { Selectors } from '../scripts/contact/selectors';
function locateOrPanic<T extends Element>(selector: string, desc: string, root?: Element): T {
const elem = (root ?? document).querySelector<T>(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<HTMLFormElement>('form.contact-form', 'contact form');
const otpDialog = locateOrPanic<HTMLDialogElement>('dialog.otp-dialog', 'OTP dialog');
const otpForm = locateOrPanic<HTMLFormElement>('form.otp-form', 'OTP form');
const successSection = document.querySelector('section.success');
let resendButtonInterval: undefined | NodeJS.Timeout = undefined;
const selectors: Selectors = {
contactForm: {
emailElem: () => locateOrPanic<HTMLInputElement>('input[name="email"]', 'email input', contactForm),
errorElem: () => contactForm.querySelector('.error'),
nameElem: () => locateOrPanic<HTMLInputElement>('input[name="name"]', 'name input', contactForm),
messageElem: () => locateOrPanic<HTMLTextAreaElement>('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<HTMLButtonElement>('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>
</Page>

View File

@@ -1,162 +0,0 @@
---
import { SITE_DESCRIPTION } from '../consts';
import Page from '../layouts/Page.astro';
---
<Page title={'Joe Carstairs CV'} description={SITE_DESCRIPTION}>
<link rel="stylesheet" href="/css/cv.css">
<div>
<img src="/images/headshot.webp" height="96" width="96" />
</div>
<header>
<h1>Joe Carstairs CV</h1>
<p><a href="mailto:me@joeac.net">me@joeac.net</a></p>
<p><a href="https://joeac.net">joeac.net</a></p>
</header>
<section>
<p>
Early-career software developer with broad interests, technical precision
and deep thinking. Not actively looking for a job right now, but happy to
hear from you about future opportunities.
</p>
</section>
<section>
<h2>Soft skills</h2>
<ul>
<li>Thinks critically with precision</li>
<li>Communicates clearly, especially in prepared presentations</li>
<li>Better at engineering things carefully and reflectively than hacking something quick and dirty (though can do that too!)</li>
</ul>
</section>
<section class="technical-skills">
<h2>Technical skills</h2>
<section>
<h3>Front end</h3>
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JavaScript/TS</li>
<li>Astro</li>
<li>React</li>
</ul>
</section>
<section>
<h3>Back end</h3>
<ul>
<li>NodeJS</li>
<li>Java/Spring</li>
<li>Python</li>
<li>C</li>
</ul>
</section>
<section>
<h3>Infrastructure</h3>
<ul>
<li>Terraform</li>
<li>Docker</li>
<li>AWS</li>
<li>DigitalOcean</li>
</ul>
</section>
<section>
<h3>Miscellaneous</h3>
<ul>
<li>Git</li>
<li>Bash</li>
<li>Langchain</li>
</ul>
</section>
</section>
<section class="my-engineering-priorities">
<h2>My engineering priorities</h2>
<ul>
<li>I put the process before the product</li>
<li>I put user needs before technical solutions</li>
<li>I put resilience before features</li>
</ul>
</section>
<section class="what-makes-me-tick">
<h2>What makes me tick</h2>
<ul>
<li>I like straightening out chaos</li>
<li>I like communities which share wisdom</li>
<li>I value finding things out over making the perfect product</li>
</ul>
</section>
<section class="experience">
<h2>Experience</h2>
<ol>
<li>
Aug 2024 - present, Scott Logic. Building a fullstack application for
a large financial institution. Java Spring Boot microservices on the
backend, Angular on the frontend, with Kafka, MongoDB and Elasticsearch
as supporting technologies.
</li>
<li>
2024, Scott Logic. Researched the potential of applying LLMs to
automated accessibility testing of websites, using Langchain Python
library, elementary prompt engineering techniques, and various scripting
languages to knit all our data together.
</li>
<li>
2023, Scott Logic. Investigated the accessibility of the
<a href="https://blog.scottlogic.com">Scott Logic blog</a> both manually
and using automated tools, including pa11y and Lighthouse. Made several
recommendations for improvements.
</li>
<li>
2023, Scott Logic. Built a social app with a React/Redux frontend and a
Spring Boot backend, deployed via Azure Pipelines onto an AWS EC2
instance, as part of the Scott Logic Graduate Programme.
</li>
<li>
2021, FreeAgent. Analysed how front-end developers were consuming the
FreeAgent design system for the Design System Team as a summer
internship.
</li>
</ol>
</section>
<section class="education">
<h2>Education</h2>
<p>Philosophy and Mathematics, MA Hons, Edin, 2022.</p>
<p>Full transcript available upon request.</p>
</section>
<section class="passions">
<h2>Passions</h2>
<ul>
<li>
I would love to see ordinary people empowered to engage with Web
technology as sovereign citizens, owning their own data, with the
ability to choose freely between competing digital services. I haven't
yet been shaken out of my naïve hope for the IndieWeb, though I
recognise it has a long way to go before it is ready for mass adoption.
</li>
<li>
I love Jesus Christ as the Redeemer of the world. Lots of people believe
that there is some higher power behind the things we can see, and Jesus
has revealed who God is by being made flesh, dying on the cross and by
being raised to a new Resurrection life, in which we are all invited to
participate. And I think that's pretty gnarly.
</li>
<li>
I love hillwalking. A day in the hills is the best kind of rest I know.
</li>
</ul>
</section>
</Page>

View File

@@ -1,36 +0,0 @@
---
import Page from '../layouts/Page.astro';
---
<Page title="Joe's housewarming" description="Details for Joe's housewarming, July 2024">
<section>
<h1>Joe's housewarming</h1>
<p>
I, Joe Carstairs, hereby promise to keep this webpage updated with
accurate information.
</p>
<dl>
<dt>when</dt>
<dd>2pm-6pm Sunday 21 July 2024</dd>
<dt>where</dt>
<dd>57 Manor Place, EH3 7EG</dd>
<dt>what to wear</dt>
<dd>whatever you like (as long as it's decent)</dd>
<dt>what to bring</dt>
<dd>
good food<br>
&nbsp;&nbsp; and/or good drink<br>
&nbsp;&nbsp; and/or good chat<br>
&nbsp;&nbsp; and/or good tunes<br>
&nbsp;&nbsp; and/or a good game<br>
&nbsp;&nbsp; and/or just your good self
</dd>
</dl>
</section>
</Page>

View File

@@ -8,6 +8,6 @@ import Page from '../layouts/Page.astro';
<Page title={SITE_TITLE} description={SITE_DESCRIPTION}> <Page title={SITE_TITLE} description={SITE_DESCRIPTION}>
<Me /> <Me />
<BlogFeed hideAuthor maxEntries={3} /> <BlogFeed hideAuthor hideSubheadings maxEntries={1} />
<LinksFeed hideAuthor maxEntries={5} /> <LinksFeed hideAuthor maxEntries={1} />
</Page> </Page>

View File

@@ -0,0 +1,30 @@
import type { Selectors } from "./selectors";
export function postErrorMessageOnContactForm(
{ contactForm }: Selectors,
errorMsg: string,
) {
const errorElem = contactForm.errorElem();
if (errorElem) {
errorElem.textContent = errorMsg;
errorElem.removeAttribute("hidden");
} else {
alert(errorMsg);
}
}
export function postErrorMessageOnOtpForm(
{ otpDialog }: Selectors,
errorMsg: string,
) {
const errorElem = otpDialog.errorElem();
if (errorElem) {
errorElem.textContent = errorMsg;
errorElem.removeAttribute("hidden");
} else {
alert(errorMsg);
}
for (const input of otpDialog.allOtpInputs()) {
(input as HTMLInputElement).value = "";
}
}

View File

@@ -0,0 +1,29 @@
import { actions } from "astro:actions";
import { resetResendButton } from "./reset-resend-button";
import { postErrorMessageOnOtpForm } from "./post-error-message";
import type { Selectors } from "./selectors";
const fallbackErrorMsg =
"No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
export async function resendOtp(
selectors: Selectors,
resetButtonInterval: NodeJS.Timeout | undefined,
): Promise<Result> {
const result = resetResendButton(selectors, resetButtonInterval);
const name = selectors.contactForm.nameElem()?.value;
const email = selectors.contactForm.emailElem().value;
const sendOtpResult = await actions.otp.send({ type: "email", name, email });
if (sendOtpResult.error) {
const errorMsg = sendOtpResult.error?.toString() ?? fallbackErrorMsg;
postErrorMessageOnOtpForm(selectors, errorMsg);
throw sendOtpResult.error;
}
return result;
}
type Result = {
resendButtonInterval: NodeJS.Timeout | undefined;
};

View File

@@ -0,0 +1,32 @@
import type { Selectors } from "./selectors";
export function resetResendButton(
{ otpDialog }: Selectors,
resendButtonInterval: NodeJS.Timeout | undefined,
): Result {
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 };
}
type Result = {
resendButtonInterval: NodeJS.Timeout | undefined;
};

View File

@@ -0,0 +1,27 @@
export type Selectors = {
contactForm: {
emailElem: () => HTMLInputElement;
errorElem: () => Element | null;
nameElem: () => HTMLInputElement;
messageElem: () => HTMLTextAreaElement;
self: () => HTMLFormElement | null;
submitButton: () => HTMLInputElement | null;
};
otpDialog: {
allOtpInputs: () => NodeListOf<HTMLInputElement>;
errorElem: () => Element | null;
firstOtpInput: () => HTMLInputElement | null;
otpForm: () => HTMLFormElement | null;
otpRecipient: () => Element | null;
otpValidUntil: () => Element | null;
resendButton: () => HTMLButtonElement | null;
self: () => HTMLDialogElement;
submitButton: () => HTMLInputElement | null;
};
successSection: {
email: () => Element | null;
message: () => Element | null;
name: () => Element | null;
self: () => Element | null;
};
};

View File

@@ -0,0 +1,53 @@
import { actions } from "astro:actions";
import { postErrorMessageOnContactForm } from "./post-error-message";
import { resetResendButton } from "./reset-resend-button";
import type { Selectors } from "./selectors";
const fallbackErrorMsg =
"No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
export async function submitContactForm(
selectors: Selectors,
resendButtonInterval: NodeJS.Timeout | undefined,
): Promise<Result> {
const { contactForm, otpDialog } = selectors;
const name = contactForm.nameElem()?.value;
const email = contactForm.emailElem().value;
contactForm.submitButton()?.setAttribute("disabled", "");
const sendOtpResult = await actions.otp.send({ type: "email", name, email });
if (sendOtpResult.error) {
const errorMsg = sendOtpResult.error?.toString() ?? fallbackErrorMsg;
postErrorMessageOnContactForm(selectors, errorMsg);
throw sendOtpResult.error;
}
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);
}
type Result = {
resendButtonInterval: NodeJS.Timeout | undefined;
};

View File

@@ -0,0 +1,72 @@
import { actions } from "astro:actions";
import type { Selectors } from "./selectors";
import {
postErrorMessageOnContactForm,
postErrorMessageOnOtpForm,
} from "./post-error-message";
const fallbackErrorMsg =
"No can do. I'm afraid joeac.net is a bit broken right now - sorry about that.";
export async function submitOtpForm(selectors: 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;
const verifyResult = await actions.otp.verify({ guess, userId: email });
if (verifyResult.error) {
otpDialog.submitButton()?.removeAttribute("disabled");
postErrorMessageOnOtpForm(
selectors,
verifyResult.error?.toString() ?? fallbackErrorMsg,
);
return;
}
if (!verifyResult.data) {
otpDialog.submitButton()?.removeAttribute("disabled");
postErrorMessageOnOtpForm(selectors, "Incorrect OTP. Check your email?");
otpDialog.firstOtpInput()?.focus();
return;
}
const sendmailToken = verifyResult.data;
const sendmailResult = await actions.sendmail({
email,
message: message,
name: name,
userId: email,
token: sendmailToken,
});
if (sendmailResult.error) {
const errorMsg = sendmailResult.error?.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();
}

View File

@@ -0,0 +1,68 @@
class OtpForm extends HTMLElement {
observer?: MutationObserver;
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");
const inputs = this.querySelectorAll(
'input:not([type="submit"])',
) as NodeListOf<HTMLInputElement>;
for (const input of inputs) {
input.value = "";
}
}
configureInputs() {
this.observer?.disconnect();
const inputs = this.querySelectorAll(
'input:not([type="submit"])',
) as NodeListOf<HTMLInputElement>;
const form = this.querySelector("form") as HTMLFormElement;
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();
const nextInput = input.nextElementSibling as HTMLInputElement;
if (nextInput) {
nextInput.focus();
return;
}
const submitButton = form.querySelector(
'input[type="submit"]',
) as HTMLInputElement;
submitButton.focus();
let areAllInputsEntered = true;
inputs.forEach((input) => {
areAllInputsEntered = areAllInputsEntered && input.value.length > 0;
});
if (areAllInputsEntered) {
form.requestSubmit(submitButton);
}
}
});
}
}
}
customElements.define("otp-form", OtpForm);