Compare commits
64 Commits
7d993bc96d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| beb9a26512 | |||
| 00f793c53b | |||
| 9f85d2f1b3 | |||
| d8d2e94008 | |||
| d9824e60a5 | |||
| 3966da7bc5 | |||
| 48ca4c2a03 | |||
| bde6f2a253 | |||
| 8e00726b04 | |||
| e0170e82aa | |||
| 64f2092161 | |||
| ba4b4ea980 | |||
| 6592d49165 | |||
| 2fdf12259c | |||
| e568105b99 | |||
| a1304c5afd | |||
| c9f7ad699c | |||
| 20effac610 | |||
| 9c50e74904 | |||
| b0be9fae2f | |||
| 8f95c6ff74 | |||
| 9d8d2a266a | |||
| 6c268a5548 | |||
| 50f4d52317 | |||
| 46c9b77316 | |||
| 40d6c7f248 | |||
| a81d1de1e5 | |||
| 46387b41ce | |||
| 476fe39f50 | |||
| a85b7b36c6 | |||
| 1d83c50e27 | |||
| fcb297637d | |||
| 781af6414e | |||
| 35f391d933 | |||
| 22ce9d06e2 | |||
| 9d610b6b3d | |||
| 0da1f8710e | |||
| e119eb62e8 | |||
| 9dab11d615 | |||
| fe69e75001 | |||
| 508db104f2 | |||
| e0fad33706 | |||
| 4ac7f6af8e | |||
| ff8201c4f3 | |||
| b0161b6ddc | |||
| 9458062b77 | |||
| 1b1d0eddf8 | |||
| 7300b15421 | |||
| fa8948da13 | |||
| ed6a6428a3 | |||
| e5cc05519a | |||
| a49b62433e | |||
| cc9a580fe6 | |||
| a3d0ee0438 | |||
| af27e03ba5 | |||
| aad9e56e4c | |||
| 261f7d4f5a | |||
| b5e58b279a | |||
| b83c5d1cf8 | |||
| 65955287d5 | |||
| a416336039 | |||
| c510415b64 | |||
| c9161a750c | |||
| 938f5e6d89 |
@@ -1,4 +1,7 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_size = 2
|
||||
|
||||
[*.{md,markdown,mdx}]
|
||||
max_line_length = 80
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,3 +25,4 @@ pnpm-debug.log*
|
||||
**/*.tfstate
|
||||
**/*.tfstate.backup
|
||||
|
||||
*.sqlite
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
// 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
|
||||
{
|
||||
"soft_wrap": "editor_width"
|
||||
"soft_wrap": "editor_width",
|
||||
"format_on_save": "on"
|
||||
}
|
||||
|
||||
47
README.md
47
README.md
@@ -4,24 +4,41 @@ Joe Carstairs' personal website
|
||||
|
||||
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
|
||||
free for static websites, and is quite flexible to add in extra apps as Droplets
|
||||
or Functions at a later time if I so please.
|
||||
To run with Podman, first set up your environment variables. Copy `example.env`
|
||||
to `.env` and edit the values accordingly.
|
||||
|
||||
DigitalOcean App Platform re-deploys the website every time there's an update to
|
||||
the `main` branch in this repo.
|
||||
Then, create the `remote_smtp_password` secret, storing the password for the
|
||||
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
|
||||
this is in the `infrastructure/` directory.
|
||||
```bash
|
||||
sudo podman secret create remote_smtp_password /path/to/remote/smtp/password
|
||||
```
|
||||
|
||||
The domain, however, is registered on AWS. The nameservers registered in AWS
|
||||
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
|
||||
once.
|
||||
Now build and start the containers:
|
||||
|
||||
```bash
|
||||
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
30
compose.yml
Normal 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
20
example.env
Executable 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
|
||||
26
infrastructure/.terraform.lock.hcl
generated
26
infrastructure/.terraform.lock.hcl
generated
@@ -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",
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
locals {
|
||||
domain = "joeac.net"
|
||||
region = "lon"
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
digitalocean = {
|
||||
source = "digitalocean/digitalocean"
|
||||
version = "~> 2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
provider "digitalocean" {
|
||||
token = var.do_token
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
variable "do_token" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "A DigitalOcean access token. Can also be provided as an environment variable"
|
||||
}
|
||||
21
package.json
21
package.json
@@ -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
12
smtp/.msmtprc
Normal 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
23
smtp/Dockerfile
Normal 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
7
website/.dockerignore
Normal file
@@ -0,0 +1,7 @@
|
||||
.astro/
|
||||
dist/
|
||||
node_modules/
|
||||
.dockerignore
|
||||
.env
|
||||
*.sqlite
|
||||
Dockerfile
|
||||
22
website/Dockerfile
Normal file
22
website/Dockerfile
Normal 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"]
|
||||
@@ -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
|
||||
export default defineConfig({
|
||||
site: 'https://joeac.net',
|
||||
integrations: [sitemap()],
|
||||
adapter: node({
|
||||
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
34
website/db/config.ts
Normal 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
3
website/db/seed.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { db } from "astro:db";
|
||||
|
||||
export default async function seed() {}
|
||||
6916
package-lock.json → website/package-lock.json
generated
6916
package-lock.json → website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
website/package.json
Normal file
29
website/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -24,12 +24,39 @@
|
||||
--colour-hyperlink-90: #bfe3ff;
|
||||
--colour-hyperlink-95: #e0f1ff;
|
||||
|
||||
--colour-grey-10: oklch(0.1 0.01 84);
|
||||
--colour-grey-20: oklch(0.2 0.01 84);
|
||||
--colour-grey-30: oklch(0.3 0.01 84);
|
||||
--colour-grey-40: oklch(0.4 0.01 84);
|
||||
--colour-grey-50: oklch(0.5 0.01 84);
|
||||
--colour-grey-60: oklch(0.6 0.01 84);
|
||||
--colour-grey-70: oklch(0.7 0.01 84);
|
||||
--colour-grey-80: oklch(0.8 0.01 84);
|
||||
--colour-grey-90: oklch(0.9 0.01 84);
|
||||
--colour-grey-95: oklch(0.95 0.01 84);
|
||||
|
||||
--colour-error-10: oklch(0.1 0.2 26);
|
||||
--colour-error-20: oklch(0.2 0.2 26);
|
||||
--colour-error-30: oklch(0.3 0.2 26);
|
||||
--colour-error-40: oklch(0.4 0.2 26);
|
||||
--colour-error-50: oklch(0.5 0.2 26);
|
||||
--colour-error-60: oklch(0.6 0.2 26);
|
||||
--colour-error-70: oklch(0.7 0.2 26);
|
||||
--colour-error-80: oklch(0.8 0.2 26);
|
||||
--colour-error-90: oklch(0.9 0.2 26);
|
||||
--colour-error-95: oklch(0.95 0.2 26);
|
||||
|
||||
--colour-primary-fg: var(--colour-primary-90);
|
||||
--colour-primary-fg-accent: var(--colour-primary-80);
|
||||
--colour-primary-bg: var(--colour-primary-10);
|
||||
--colour-primary-bg-accent: var(--colour-primary-20);
|
||||
--colour-code-fg: var(--colour-primary-90);
|
||||
--colour-code-bg: var(--colour-primary-15);
|
||||
--colour-hyperlink: var(--colour-hyperlink-80);
|
||||
--colour-grey-fg: var(--colour-grey-70);
|
||||
--colour-grey-bg: var(--colour-grey-30);
|
||||
--colour-error-fg: var(--colour-error-90);
|
||||
--colour-error-bg: var(--colour-error-40);
|
||||
|
||||
--font-size-sm: 1rem;
|
||||
--font-size-base: 1.125rem;
|
||||
@@ -56,6 +83,10 @@
|
||||
--colour-primary-fg-accent: var(--colour-primary-40);
|
||||
--colour-primary-bg: var(--colour-primary-95);
|
||||
--colour-hyperlink: var(--colour-hyperlink-40);
|
||||
--colour-grey-fg: var(--colour-grey-40);
|
||||
--colour-grey-bg: var(--colour-grey-80);
|
||||
--colour-error-fg: var(--colour-error-20);
|
||||
--colour-error-bg: var(--colour-error-80);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,17 +100,22 @@ body {
|
||||
line-height: 1.5;
|
||||
|
||||
/* 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 {
|
||||
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);
|
||||
}
|
||||
|
||||
hr {
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
/** Base layout */
|
||||
|
||||
body {
|
||||
@@ -99,38 +135,43 @@ img {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (min-width: 48rem) {
|
||||
@media (min-width: 60rem) {
|
||||
body {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
[media-start]
|
||||
[grid-start media-start]
|
||||
var(--grid-margin-inline)
|
||||
[content-start]
|
||||
[media-end content-start]
|
||||
minmax(var(--grid-max-content-width), auto)
|
||||
[content-end];
|
||||
column-gap: var(--spacing-block-sm);
|
||||
[content-end grid-end];
|
||||
grid-auto-rows: max-content;
|
||||
column-gap: var(--spacing-block-sm);
|
||||
max-width: var(--grid-total-width);
|
||||
|
||||
--body-margin-inline-end: 6rem;
|
||||
--grid-margin-inline: 6rem;
|
||||
--grid-total-width: 48rem;
|
||||
--grid-max-content-width: calc(
|
||||
var(--grid-total-width)
|
||||
- var(--body-margin-inline-start)
|
||||
- var(--grid-margin-inline)
|
||||
- var(--spacing-block-sm)
|
||||
- var(--grid-margin-inline)
|
||||
var(--grid-total-width) - var(--body-margin-inline-start) -
|
||||
var(--grid-margin-inline) - var(--spacing-block-sm) -
|
||||
var(--grid-margin-inline)
|
||||
);
|
||||
}
|
||||
|
||||
:is(main, article, nav) {
|
||||
display: grid;
|
||||
grid-column: media-start / content-end;
|
||||
grid-column: grid;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
:is(section, header, aside) {
|
||||
grid-column: content-start / content-end;
|
||||
> :is(section, header, aside, form) {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-column: grid;
|
||||
|
||||
> :not(.not-grid-content) {
|
||||
grid-column: content;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
@@ -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 */
|
||||
|
||||
h1 {
|
||||
@@ -152,7 +209,10 @@ h2 {
|
||||
margin-block-start: var(--spacing-block-xl);
|
||||
}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-size: var(--font-size-md);
|
||||
font-weight: 600;
|
||||
margin-block-start: var(--spacing-block-lg);
|
||||
@@ -233,8 +293,8 @@ strong {
|
||||
/** Blog feed */
|
||||
|
||||
.h-feed ul {
|
||||
list-style: none;
|
||||
margin-inline: 0;
|
||||
list-style: none;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
/** Block quotes */
|
||||
@@ -244,7 +304,7 @@ blockquote {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote footer {
|
||||
blockquote footer {
|
||||
font-style: initial;
|
||||
}
|
||||
|
||||
@@ -286,3 +346,149 @@ pre code {
|
||||
border-radius: none;
|
||||
padding: none;
|
||||
}
|
||||
|
||||
/* verse */
|
||||
.verse {
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
.verse--hanging-indents {
|
||||
span + span:not(.not-hanging) {
|
||||
margin-inline-start: var(--spacing-inline-md);
|
||||
}
|
||||
}
|
||||
|
||||
/* block-comment */
|
||||
block-comment {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
grid-column: grid;
|
||||
|
||||
> * {
|
||||
grid-column: content;
|
||||
}
|
||||
|
||||
> blockquote:last-of-type {
|
||||
padding-block: var(--spacing-block-sm);
|
||||
padding-inline: var(--spacing-inline-md);
|
||||
overflow-y: scroll;
|
||||
border: 2px solid var(--colour-primary-fg);
|
||||
--colour-scroll-shadow: color-mix(
|
||||
in srgb,
|
||||
var(--colour-primary-fg),
|
||||
transparent 20%
|
||||
);
|
||||
|
||||
background:
|
||||
linear-gradient(var(--colour-primary-bg) 30%, transparent) center top,
|
||||
linear-gradient(transparent, var(--colour-primary-bg) 70%) center bottom,
|
||||
radial-gradient(
|
||||
farthest-side at 50% 0,
|
||||
var(--colour-scroll-shadow),
|
||||
transparent
|
||||
)
|
||||
center top,
|
||||
radial-gradient(
|
||||
farthest-side at 50% 100%,
|
||||
var(--colour-scroll-shadow),
|
||||
transparent
|
||||
)
|
||||
center bottom;
|
||||
background-repeat: no-repeat;
|
||||
background-size:
|
||||
100% 2rem,
|
||||
100% 2rem,
|
||||
100% 1rem,
|
||||
100% 1rem;
|
||||
background-attachment: local, local, scroll, scroll;
|
||||
|
||||
> :first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 80rem) {
|
||||
block-comment > blockquote:last-of-type {
|
||||
grid-column: margin;
|
||||
max-height: 67vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* forms */
|
||||
form {
|
||||
margin-inline: auto;
|
||||
max-width: max-content;
|
||||
|
||||
:is(button, fieldset, input, label, object, output, select, textarea, img) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-block-end: var(--spacing-block-xs);
|
||||
}
|
||||
|
||||
* + :is(label, input[type="submit"], button) {
|
||||
margin-block-start: var(--spacing-block-md);
|
||||
}
|
||||
|
||||
:is(input[type="submit"], input[type="button"], button) {
|
||||
padding-inline: var(--spacing-inline-sm);
|
||||
max-width: max-content;
|
||||
&:not(dialog *) {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
|
||||
:is(input[type="email"], input[type="text"], select) {
|
||||
max-width: 100%;
|
||||
width: 16rem;
|
||||
}
|
||||
|
||||
textarea {
|
||||
max-width: 100%;
|
||||
width: 40rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* dialogs */
|
||||
|
||||
dialog {
|
||||
background: var(--colour-primary-bg-accent);
|
||||
border: 2px solid var(--colour-primary-fg-accent);
|
||||
bottom: auto;
|
||||
color: var(--colour-primary-fg);
|
||||
left: calc(0.5 * (100vw - min(90vw, 36rem)));
|
||||
margin: 0;
|
||||
padding-block: var(--spacing-block-sm);
|
||||
padding-inline: var(--spacing-inline-sm);
|
||||
text-align: center;
|
||||
top: auto;
|
||||
width: min(90vw, 36rem);
|
||||
}
|
||||
|
||||
@media (min-width: 80rem) {
|
||||
dialog {
|
||||
left: calc(
|
||||
var(--body-margin-inline-start) + var(--grid-margin-inline) + 2 *
|
||||
var(--spacing-inline-md)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/* utilities */
|
||||
:is(
|
||||
.para-spacing-tight:is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol),
|
||||
.para-spacing-tight :is(p, h1, h2, h3, h4, h5, h6, hr, img, figure, ul, ol)
|
||||
) {
|
||||
margin-block-start: var(--spacing-block-xs);
|
||||
}
|
||||
|
||||
.error {
|
||||
background: var(--colour-error-bg);
|
||||
border: 2px solid var(--colour-error-fg);
|
||||
color: var(--colour-error-fg);
|
||||
padding-block: var(--spacing--block-sm);
|
||||
padding-inline: var(--spacing-inline-sm);
|
||||
}
|
||||
|
||||
@@ -1,16 +1,35 @@
|
||||
/* Assumes there is at most one level of subheading for sub-dividing entries */
|
||||
.h-feed :is(h2, h3, h4, h5, h6) {
|
||||
margin-block-start: var(--spacing-block-md);
|
||||
margin-block-start: var(--spacing-block-md);
|
||||
}
|
||||
|
||||
.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 {
|
||||
text-align: end;
|
||||
text-align: end;
|
||||
}
|
||||
|
||||
.h-feed :is(a.full-feed-link, .full-feed-link a)::after {
|
||||
content: ' >'
|
||||
content: " >";
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
.h-card div:has(img) {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin-inline: auto;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.h-card img {
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border-radius: 1rem;
|
||||
filter: contrast(1.25);
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
border-radius: 1rem;
|
||||
filter: contrast(1.25);
|
||||
}
|
||||
|
||||
.h-card div:has(img)::after {
|
||||
/* Colour overlay */
|
||||
background-color: var(--colour-primary-80);
|
||||
opacity: 0.3;
|
||||
/* Colour overlay */
|
||||
background-color: var(--colour-primary-80);
|
||||
opacity: 0.3;
|
||||
|
||||
/* Same size and shape as the img */
|
||||
border-radius: 1rem;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
/* Same size and shape as the img */
|
||||
border-radius: 1rem;
|
||||
width: 6rem;
|
||||
height: 6rem;
|
||||
|
||||
/* Positioned on top of the img */
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -6rem;
|
||||
/* Positioned on top of the img */
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -6rem;
|
||||
|
||||
/* A content value is needed to get the ::after to render */
|
||||
content: '';
|
||||
/* A content value is needed to get the ::after to render */
|
||||
content: "";
|
||||
}
|
||||
|
||||
|
||||
@media (min-width: 36rem) {
|
||||
.h-card {
|
||||
grid-column: media-start / content-end;
|
||||
display: grid;
|
||||
grid-template-columns: subgrid; /** Subgrid of main column layout */
|
||||
grid-template-rows: min-content 1fr;
|
||||
grid-template-areas:
|
||||
"empty heading"
|
||||
"photo text";
|
||||
}
|
||||
.h-card {
|
||||
grid-column: media-start / content-end;
|
||||
display: grid;
|
||||
grid-template-columns: subgrid; /** Subgrid of main column layout */
|
||||
grid-template-rows: min-content 1fr;
|
||||
grid-template-areas:
|
||||
"empty heading"
|
||||
"photo text";
|
||||
column-gap: var(--spacing-block-sm);
|
||||
}
|
||||
|
||||
.h-card div:has(img) {
|
||||
grid-area: photo;
|
||||
margin-block-start: var(--spacing-block-sm);
|
||||
}
|
||||
.h-card div:has(img) {
|
||||
grid-area: photo;
|
||||
margin-block-start: var(--spacing-block-sm);
|
||||
}
|
||||
|
||||
.h-card header {
|
||||
grid-area: heading;
|
||||
}
|
||||
.h-card header {
|
||||
grid-area: heading;
|
||||
}
|
||||
|
||||
.h-card__text {
|
||||
grid-area: text;
|
||||
}
|
||||
.h-card__text {
|
||||
grid-area: text;
|
||||
}
|
||||
}
|
||||
|
||||
29
website/public/css/otp.css
Normal file
29
website/public/css/otp.css
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -15,14 +15,22 @@ html {
|
||||
}
|
||||
|
||||
/* Remove default margin in favour of better control in authored CSS */
|
||||
body, h1, h2, h3, h4, p,
|
||||
figure, blockquote, dl, dd {
|
||||
body,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
p,
|
||||
figure,
|
||||
blockquote,
|
||||
dl,
|
||||
dd {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
|
||||
ul[role='list'],
|
||||
ol[role='list'] {
|
||||
ul[role="list"],
|
||||
ol[role="list"] {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
@@ -32,14 +40,21 @@ body {
|
||||
}
|
||||
|
||||
/* Set shorter line heights on headings and interactive elements */
|
||||
h1, h2, h3, h4,
|
||||
button, input, label {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
button,
|
||||
input,
|
||||
label {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
/* Balance text wrapping on headings */
|
||||
h1, h2,
|
||||
h3, h4 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
@@ -51,8 +66,10 @@ picture {
|
||||
}
|
||||
|
||||
/* Inherit fonts for inputs and buttons */
|
||||
input, button,
|
||||
textarea, select {
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
@@ -71,3 +88,6 @@ textarea:not([rows]) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
7
website/src/actions/index.ts
Normal file
7
website/src/actions/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import otp from "./otp/otp";
|
||||
import sendmail from "./sendmail";
|
||||
|
||||
export const server = {
|
||||
otp,
|
||||
sendmail,
|
||||
};
|
||||
7
website/src/actions/otp/otp.ts
Normal file
7
website/src/actions/otp/otp.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import send from "./send-otp";
|
||||
import verify from "./verify-otp";
|
||||
|
||||
export default {
|
||||
send,
|
||||
verify,
|
||||
};
|
||||
45
website/src/actions/otp/send-otp.ts
Normal file
45
website/src/actions/otp/send-otp.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
48
website/src/actions/otp/verify-otp.ts
Normal file
48
website/src/actions/otp/verify-otp.ts
Normal 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;
|
||||
};
|
||||
93
website/src/actions/sendmail.ts
Normal file
93
website/src/actions/sendmail.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
@@ -6,10 +6,11 @@ import FormattedDate from './FormattedDate.astro';
|
||||
export interface Props {
|
||||
headingLevel?: 1 | 2 | 3 | 4 | 5 | 6,
|
||||
hideAuthor?: boolean,
|
||||
hideSubheadings?: boolean,
|
||||
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);
|
||||
|
||||
@@ -32,41 +33,46 @@ function sortByPubDateDescending(post1: CollectionEntry<'blog'>, post2: Collecti
|
||||
return date2 - date1;
|
||||
}
|
||||
|
||||
const headingElem = `h${headingLevel}`;
|
||||
const subHeadingElem = `h${headingLevel + 1}`
|
||||
const HeadingElem = `h${headingLevel} class="p-name"`;
|
||||
const SubHeadingElem = `h${headingLevel + 1}`;
|
||||
const AuthorElem = `p${hideAuthor ? " hidden" : ""}`;
|
||||
|
||||
const canonicalBlogUrl = new URL('blog', Astro.site)
|
||||
---
|
||||
|
||||
<section class="h-feed">
|
||||
<Fragment set:html={`
|
||||
<${headingElem} class="p-name">
|
||||
My blog
|
||||
</${headingElem}>
|
||||
`} />
|
||||
<HeadingElem>
|
||||
My blog
|
||||
</HeadingElem>
|
||||
|
||||
<aside>
|
||||
<p hidden={hideAuthor}>
|
||||
<AuthorElem>
|
||||
This blog is written by <a class="p-author h-card" href="/">Joe Carstairs</a>
|
||||
</p>
|
||||
</AuthorElem>
|
||||
|
||||
<p hidden>
|
||||
<a class="u-url" href={canonicalBlogUrl}>Permalink</a>
|
||||
</p>
|
||||
</aside>
|
||||
|
||||
{ distinctYears.map(year => (
|
||||
<Fragment set:html={`
|
||||
<${subHeadingElem}>
|
||||
${year}
|
||||
</${subHeadingElem}>
|
||||
`} />
|
||||
{ hideSubheadings
|
||||
? <ul>
|
||||
{ posts.sort(sortByPubDateDescending).map(post => (
|
||||
<li class="h-entry">
|
||||
<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>
|
||||
{ posts.filter(matchesYear(year)).sort(sortByPubDateDescending).map(post => (
|
||||
<li class="h-entry">
|
||||
<a class="u-url p-name" href={`/blog/${post.id}`}>{post.data.title}</a>.
|
||||
<Fragment set:html={post.data.description} />
|
||||
Added: <FormattedDate date={post.data.pubDate} />
|
||||
<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>
|
||||
|
||||
@@ -63,9 +63,9 @@ const canonicalLinksUrl = new URL('links', Astro.site)
|
||||
<ul>
|
||||
{ links.filter(matchesYear(year)).sort(sortByDateAddedDescending).map(link => (
|
||||
<li class="h-entry e-content">
|
||||
<a class="u-url p-name" href={link.href} set:html={link.title} />.
|
||||
<Fragment set:html={link.description} />
|
||||
Added: <FormattedDate date={link.isoDateAdded} />
|
||||
<FormattedDate className="dt-published" date={link.isoDateAdded} />
|
||||
<a class="u-url p-name" href={link.href} set:html={link.title} />
|
||||
<p class="p-description" set:html={link.description} />
|
||||
</li>
|
||||
)) }
|
||||
</ul>
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
Hi! 👋 My name is <span class="p-given-name">Joe</span>
|
||||
<span class="p-family-name">Carstairs</span>. I’m a
|
||||
<span class="p-job-title">software developer</span> at
|
||||
<a class="p-org" href="https://www.scottlogic.com">Scott Logic</a>, a
|
||||
graduate of Philosophy and Mathematics at the University of Edinburgh,
|
||||
a committed Christian and a pretty rubbish poet.
|
||||
<a class="p-org" href="https://www.scottlogic.com">Scott Logic</a>,
|
||||
a Divinity student at the University of Edinburgh,
|
||||
a lapsed fiddle player and a very occasional poet.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
@@ -12,5 +12,8 @@
|
||||
<li>
|
||||
<a href="/links">Links</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/contact">Contact</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
31
website/src/components/OtpDialog.astro
Normal file
31
website/src/components/OtpDialog.astro
Normal 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>
|
||||
200
website/src/content/blog/2025/06/23/work.md
Normal file
200
website/src/content/blog/2025/06/23/work.md
Normal 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
|
||||
415
website/src/content/blog/2025/07/03/ps118.mdx
Normal file
415
website/src/content/blog/2025/07/03/ps118.mdx
Normal 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 Lord’s right hand has done mighty things!</span>
|
||||
<span>The Lord’s right hand is lifted high;</span>
|
||||
<span>the Lord’s 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 father’s 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>Pharaoh’s chariots and his army</span>
|
||||
<span>he has hurled into the sea.</span>
|
||||
<span class="not-hanging">The best of Pharaoh’s 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 Zenger’s 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
|
||||
88
website/src/content/blog/2025/09/18/starting_msc.md
Normal file
88
website/src/content/blog/2025/09/18/starting_msc.md
Normal 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.
|
||||
125
website/src/content/blog/2025/09/24/creeds.md
Normal file
125
website/src/content/blog/2025/09/24/creeds.md
Normal 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!
|
||||
415
website/src/content/blog/2025/10/05/creeds.md
Normal file
415
website/src/content/blog/2025/10/05/creeds.md
Normal 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.
|
||||
171
website/src/content/blog/2025/10/09/arius.md
Normal file
171
website/src/content/blog/2025/10/09/arius.md
Normal 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.
|
||||
227
website/src/content/blog/2025/12/11/persecution.md
Normal file
227
website/src/content/blog/2025/12/11/persecution.md
Normal 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.
|
||||
96
website/src/pages/contact.astro
Normal file
96
website/src/pages/contact.astro
Normal 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>
|
||||
@@ -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>
|
||||
and/or good drink<br>
|
||||
and/or good chat<br>
|
||||
and/or good tunes<br>
|
||||
and/or a good game<br>
|
||||
and/or just your good self
|
||||
</dd>
|
||||
</dl>
|
||||
</section>
|
||||
</Page>
|
||||
|
||||
@@ -8,6 +8,6 @@ import Page from '../layouts/Page.astro';
|
||||
|
||||
<Page title={SITE_TITLE} description={SITE_DESCRIPTION}>
|
||||
<Me />
|
||||
<BlogFeed hideAuthor maxEntries={3} />
|
||||
<LinksFeed hideAuthor maxEntries={5} />
|
||||
<BlogFeed hideAuthor hideSubheadings maxEntries={1} />
|
||||
<LinksFeed hideAuthor maxEntries={1} />
|
||||
</Page>
|
||||
|
||||
30
website/src/scripts/contact/post-error-message.ts
Normal file
30
website/src/scripts/contact/post-error-message.ts
Normal 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 = "";
|
||||
}
|
||||
}
|
||||
29
website/src/scripts/contact/resend-otp.ts
Normal file
29
website/src/scripts/contact/resend-otp.ts
Normal 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;
|
||||
};
|
||||
32
website/src/scripts/contact/reset-resend-button.ts
Normal file
32
website/src/scripts/contact/reset-resend-button.ts
Normal 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;
|
||||
};
|
||||
27
website/src/scripts/contact/selectors.ts
Normal file
27
website/src/scripts/contact/selectors.ts
Normal 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;
|
||||
};
|
||||
};
|
||||
53
website/src/scripts/contact/submit-contact-form.ts
Normal file
53
website/src/scripts/contact/submit-contact-form.ts
Normal 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;
|
||||
};
|
||||
72
website/src/scripts/contact/submit-otp-form.ts
Normal file
72
website/src/scripts/contact/submit-otp-form.ts
Normal 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();
|
||||
}
|
||||
68
website/src/scripts/otp-form-wc.ts
Normal file
68
website/src/scripts/otp-form-wc.ts
Normal 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);
|
||||
Reference in New Issue
Block a user