Initial commit: Hugo site with Caddy infra and deploy tooling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
20
.gitignore
vendored
Normal file
20
.gitignore
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Hugo build output
|
||||||
|
site/public/
|
||||||
|
site/resources/
|
||||||
|
site/.hugo_build.lock
|
||||||
|
|
||||||
|
# Claude Code
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
.env
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Editor
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
102
CLAUDE.md
Executable file
102
CLAUDE.md
Executable file
@@ -0,0 +1,102 @@
|
|||||||
|
# Monotrope
|
||||||
|
|
||||||
|
Personal blog and server infrastructure for monotrope.au.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
monotrope/
|
||||||
|
site/ # Hugo site (content, templates, config)
|
||||||
|
infra/ # Server setup scripts and config files
|
||||||
|
deploy.sh # Build + rsync to production
|
||||||
|
Makefile # Common tasks
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Static site generator:** Hugo
|
||||||
|
- **Web server:** Caddy (automatic HTTPS via Let's Encrypt)
|
||||||
|
- **Hosting:** DigitalOcean droplet (Sydney region, Ubuntu 24.04 LTS)
|
||||||
|
- **Deployment:** `hugo build` then `rsync` to server
|
||||||
|
- **Future services:** Docker Compose for anything beyond the blog
|
||||||
|
|
||||||
|
## Setup Tasks
|
||||||
|
|
||||||
|
### 1. Initialise the repo
|
||||||
|
|
||||||
|
- Create the directory structure above
|
||||||
|
- `git init` with a sensible `.gitignore` (Hugo output dir `public/`, `.env`, etc.)
|
||||||
|
- Initialise a new Hugo site inside `site/`
|
||||||
|
- Use a minimal theme or no theme — we'll build templates from scratch later
|
||||||
|
- Hugo config should set `baseURL = "https://monotrope.au"`
|
||||||
|
|
||||||
|
### 2. Create a minimal Hugo site
|
||||||
|
|
||||||
|
- A single layout (`layouts/_default/baseof.html`, `single.html`, `list.html`)
|
||||||
|
- Minimal, clean CSS — no framework. Readable prose typography, dark mode support via `prefers-color-scheme`
|
||||||
|
- A sample first post in `site/content/posts/` so we can test the build
|
||||||
|
- RSS feed enabled (Hugo does this by default)
|
||||||
|
- No JavaScript unless strictly necessary
|
||||||
|
|
||||||
|
### 3. Server provisioning script
|
||||||
|
|
||||||
|
Create `infra/setup.sh` — a bash script intended to be run once on a fresh Ubuntu 24.04 droplet via SSH. It should:
|
||||||
|
|
||||||
|
- Update packages
|
||||||
|
- Install Caddy via the official apt repo
|
||||||
|
- Create a `www` user with no login shell to own the site files
|
||||||
|
- Create `/var/www/monotrope` owned by `www`
|
||||||
|
- Install a Caddyfile at `/etc/caddy/Caddyfile` that:
|
||||||
|
- Serves `monotrope.au` from `/var/www/monotrope`
|
||||||
|
- Enables gzip/zstd compression
|
||||||
|
- Sets sensible cache headers for static assets
|
||||||
|
- Handles `www.monotrope.au` redirect to apex
|
||||||
|
- Enable and start the Caddy service
|
||||||
|
- Set up UFW: allow SSH, HTTP, HTTPS, deny everything else
|
||||||
|
- Install Docker and Docker Compose (for future use, not needed yet)
|
||||||
|
- Create a deploy user with SSH key auth and permission to write to `/var/www/monotrope`
|
||||||
|
|
||||||
|
Also create `infra/Caddyfile` as a standalone config file that the setup script copies into place.
|
||||||
|
|
||||||
|
### 4. Deploy script
|
||||||
|
|
||||||
|
Create `deploy.sh` at the repo root:
|
||||||
|
|
||||||
|
- Run `hugo --minify` in `site/`
|
||||||
|
- `rsync -avz --delete site/public/ deploy@<DROPLET_IP>:/var/www/monotrope/`
|
||||||
|
- Print the URL on success
|
||||||
|
|
||||||
|
The droplet IP should come from an environment variable `MONOTROPE_HOST` or a `.env` file (not committed).
|
||||||
|
|
||||||
|
### 5. Makefile
|
||||||
|
|
||||||
|
Targets:
|
||||||
|
- `make build` — build the site locally
|
||||||
|
- `make serve` — `hugo server` for local dev with live reload
|
||||||
|
- `make deploy` — run `deploy.sh`
|
||||||
|
- `make ssh` — SSH into the droplet as the deploy user
|
||||||
|
- `make setup` — run the provisioning script on a fresh droplet
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- All shell scripts should use `set -euo pipefail`
|
||||||
|
- Prefer clarity over cleverness in all scripts
|
||||||
|
- No unnecessary dependencies — this should stay simple
|
||||||
|
- Australian English in all content and comments
|
||||||
|
- Markdown content lives in `site/content/`; one subdirectory per content type (e.g. `posts/`, `pages/`)
|
||||||
|
|
||||||
|
## DNS
|
||||||
|
|
||||||
|
The user will configure DNS separately via their registrar. The server expects:
|
||||||
|
- `monotrope.au` → droplet IP (A record)
|
||||||
|
- `www.monotrope.au` → droplet IP (A record)
|
||||||
|
|
||||||
|
Caddy will handle certificate provisioning automatically once DNS is pointed.
|
||||||
|
|
||||||
|
## What This Is Not
|
||||||
|
|
||||||
|
- No CI/CD pipeline — deploy is manual via `make deploy`
|
||||||
|
- No containerisation of the blog itself — it's static files
|
||||||
|
- No database
|
||||||
|
- No analytics (for now)
|
||||||
|
- No comments system (for now)
|
||||||
26
Makefile
Normal file
26
Makefile
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.PHONY: build serve deploy ssh setup
|
||||||
|
|
||||||
|
# Load .env if it exists
|
||||||
|
-include .env
|
||||||
|
export
|
||||||
|
|
||||||
|
DEPLOY_USER := deploy
|
||||||
|
MONOTROPE_HOST ?=
|
||||||
|
|
||||||
|
build:
|
||||||
|
cd site && hugo --minify
|
||||||
|
|
||||||
|
serve:
|
||||||
|
cd site && hugo server --buildDrafts --disableFastRender
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
@test -n "$(MONOTROPE_HOST)" || (echo "Error: MONOTROPE_HOST is not set"; exit 1)
|
||||||
|
bash deploy.sh
|
||||||
|
|
||||||
|
ssh:
|
||||||
|
@test -n "$(MONOTROPE_HOST)" || (echo "Error: MONOTROPE_HOST is not set"; exit 1)
|
||||||
|
ssh $(DEPLOY_USER)@$(MONOTROPE_HOST)
|
||||||
|
|
||||||
|
setup:
|
||||||
|
@test -n "$(MONOTROPE_HOST)" || (echo "Error: MONOTROPE_HOST is not set"; exit 1)
|
||||||
|
ansible-playbook -i "$(MONOTROPE_HOST)," -u root infra/ansible/playbook.yml
|
||||||
33
deploy.sh
Executable file
33
deploy.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# deploy.sh — Build and deploy monotrope.au to the production droplet
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
# Load .env if present
|
||||||
|
if [[ -f "$SCRIPT_DIR/.env" ]]; then
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$SCRIPT_DIR/.env"
|
||||||
|
fi
|
||||||
|
|
||||||
|
MONOTROPE_HOST="${MONOTROPE_HOST:-}"
|
||||||
|
DEPLOY_USER="deploy"
|
||||||
|
REMOTE_DIR="/var/www/monotrope"
|
||||||
|
|
||||||
|
if [[ -z "$MONOTROPE_HOST" ]]; then
|
||||||
|
echo "Error: MONOTROPE_HOST is not set."
|
||||||
|
echo "Set it in your environment or in a .env file at the repo root."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Building site"
|
||||||
|
cd "$SCRIPT_DIR/site"
|
||||||
|
hugo --minify
|
||||||
|
|
||||||
|
echo "==> Deploying to ${DEPLOY_USER}@${MONOTROPE_HOST}:${REMOTE_DIR}"
|
||||||
|
rsync -avz --delete "$SCRIPT_DIR/site/public/" \
|
||||||
|
"${DEPLOY_USER}@${MONOTROPE_HOST}:${REMOTE_DIR}/"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Done. Live at https://monotrope.au"
|
||||||
24
infra/Caddyfile
Normal file
24
infra/Caddyfile
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
monotrope.au {
|
||||||
|
root * /var/www/monotrope
|
||||||
|
file_server
|
||||||
|
|
||||||
|
# Compression
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# Cache headers for static assets
|
||||||
|
@static {
|
||||||
|
path *.css *.js *.ico *.gif *.jpg *.jpeg *.png *.webp *.svg *.woff *.woff2 *.ttf *.eot
|
||||||
|
}
|
||||||
|
header @static Cache-Control "public, max-age=31536000, immutable"
|
||||||
|
|
||||||
|
# HTML and RSS — revalidate each time
|
||||||
|
@html {
|
||||||
|
path *.html / /posts/ /posts/*
|
||||||
|
}
|
||||||
|
header @html Cache-Control "public, max-age=0, must-revalidate"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect www to apex
|
||||||
|
www.monotrope.au {
|
||||||
|
redir https://monotrope.au{uri} permanent
|
||||||
|
}
|
||||||
188
infra/ansible/playbook.yml
Normal file
188
infra/ansible/playbook.yml
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
---
|
||||||
|
- name: Provision monotrope.au server
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
|
||||||
|
vars:
|
||||||
|
site_dir: /var/www/monotrope
|
||||||
|
deploy_user: deploy
|
||||||
|
deploy_pubkey: "{{ lookup('file', lookup('env', 'HOME') + '/.ssh/id_ed25519.pub') }}"
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
|
||||||
|
# ── System ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
- name: Update apt cache and upgrade packages
|
||||||
|
apt:
|
||||||
|
update_cache: true
|
||||||
|
upgrade: dist
|
||||||
|
cache_valid_time: 3600
|
||||||
|
|
||||||
|
- name: Install common dependencies
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- debian-keyring
|
||||||
|
- debian-archive-keyring
|
||||||
|
- apt-transport-https
|
||||||
|
- curl
|
||||||
|
- ufw
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# ── Caddy ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
- name: Add Caddy GPG key
|
||||||
|
shell: |
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
||||||
|
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||||
|
args:
|
||||||
|
creates: /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||||
|
|
||||||
|
- name: Add Caddy apt repository
|
||||||
|
shell: |
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||||||
|
| tee /etc/apt/sources.list.d/caddy-stable.list
|
||||||
|
args:
|
||||||
|
creates: /etc/apt/sources.list.d/caddy-stable.list
|
||||||
|
|
||||||
|
- name: Install Caddy
|
||||||
|
apt:
|
||||||
|
name: caddy
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Install Caddyfile
|
||||||
|
copy:
|
||||||
|
src: ../Caddyfile
|
||||||
|
dest: /etc/caddy/Caddyfile
|
||||||
|
owner: root
|
||||||
|
group: caddy
|
||||||
|
mode: '0640'
|
||||||
|
notify: Restart Caddy
|
||||||
|
|
||||||
|
- name: Enable and start Caddy
|
||||||
|
systemd:
|
||||||
|
name: caddy
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
# ── Site directory ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
- name: Create www system user
|
||||||
|
user:
|
||||||
|
name: www
|
||||||
|
system: true
|
||||||
|
create_home: false
|
||||||
|
shell: /usr/sbin/nologin
|
||||||
|
state: present
|
||||||
|
|
||||||
|
# ── UFW ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
- name: Set UFW default incoming policy to deny
|
||||||
|
ufw:
|
||||||
|
default: deny
|
||||||
|
direction: incoming
|
||||||
|
|
||||||
|
- name: Set UFW default outgoing policy to allow
|
||||||
|
ufw:
|
||||||
|
default: allow
|
||||||
|
direction: outgoing
|
||||||
|
|
||||||
|
- name: Allow SSH
|
||||||
|
ufw:
|
||||||
|
rule: allow
|
||||||
|
name: OpenSSH
|
||||||
|
|
||||||
|
- name: Allow HTTP
|
||||||
|
ufw:
|
||||||
|
rule: allow
|
||||||
|
port: '80'
|
||||||
|
proto: tcp
|
||||||
|
|
||||||
|
- name: Allow HTTPS
|
||||||
|
ufw:
|
||||||
|
rule: allow
|
||||||
|
port: '443'
|
||||||
|
proto: tcp
|
||||||
|
|
||||||
|
- name: Enable UFW
|
||||||
|
ufw:
|
||||||
|
state: enabled
|
||||||
|
|
||||||
|
# ── Docker ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
- name: Create Docker keyring directory
|
||||||
|
file:
|
||||||
|
path: /etc/apt/keyrings
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
- name: Add Docker GPG key
|
||||||
|
shell: |
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||||
|
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
args:
|
||||||
|
creates: /etc/apt/keyrings/docker.gpg
|
||||||
|
|
||||||
|
- name: Add Docker apt repository
|
||||||
|
shell: |
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||||
|
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||||
|
| tee /etc/apt/sources.list.d/docker.list
|
||||||
|
args:
|
||||||
|
creates: /etc/apt/sources.list.d/docker.list
|
||||||
|
|
||||||
|
- name: Install Docker
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- docker-ce
|
||||||
|
- docker-ce-cli
|
||||||
|
- containerd.io
|
||||||
|
- docker-buildx-plugin
|
||||||
|
- docker-compose-plugin
|
||||||
|
state: present
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Enable Docker
|
||||||
|
systemd:
|
||||||
|
name: docker
|
||||||
|
enabled: true
|
||||||
|
state: started
|
||||||
|
|
||||||
|
# ── Deploy user ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
- name: Create deploy user
|
||||||
|
user:
|
||||||
|
name: "{{ deploy_user }}"
|
||||||
|
create_home: true
|
||||||
|
shell: /bin/bash
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Set up deploy user SSH directory
|
||||||
|
file:
|
||||||
|
path: "/home/{{ deploy_user }}/.ssh"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ deploy_user }}"
|
||||||
|
group: "{{ deploy_user }}"
|
||||||
|
mode: '0700'
|
||||||
|
|
||||||
|
- name: Install deploy user SSH public key
|
||||||
|
ansible.posix.authorized_key:
|
||||||
|
user: "{{ deploy_user }}"
|
||||||
|
key: "{{ deploy_pubkey }}"
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Create site directory
|
||||||
|
file:
|
||||||
|
path: "{{ site_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: "{{ deploy_user }}"
|
||||||
|
group: www
|
||||||
|
mode: '0775'
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
|
||||||
|
- name: Restart Caddy
|
||||||
|
systemd:
|
||||||
|
name: caddy
|
||||||
|
state: restarted
|
||||||
97
infra/setup.sh
Executable file
97
infra/setup.sh
Executable file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# setup.sh — Provision a fresh Ubuntu 24.04 droplet for monotrope.au
|
||||||
|
# Run as root via: ssh root@<DROPLET_IP> 'bash -s' < infra/setup.sh
|
||||||
|
|
||||||
|
DEPLOY_USER="deploy"
|
||||||
|
SITE_DIR="/var/www/monotrope"
|
||||||
|
DEPLOY_PUBKEY="${DEPLOY_PUBKEY:-}" # Set this env var before running, or edit below
|
||||||
|
|
||||||
|
echo "==> Updating packages"
|
||||||
|
apt-get update -y
|
||||||
|
apt-get upgrade -y
|
||||||
|
|
||||||
|
# ── Caddy ─────────────────────────────────────────────────────────────────
|
||||||
|
echo "==> Installing Caddy"
|
||||||
|
apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl
|
||||||
|
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
||||||
|
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||||||
|
|
||||||
|
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||||||
|
| tee /etc/apt/sources.list.d/caddy-stable.list
|
||||||
|
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y caddy
|
||||||
|
|
||||||
|
# ── Site directory ─────────────────────────────────────────────────────────
|
||||||
|
echo "==> Creating www user and site directory"
|
||||||
|
id -u www &>/dev/null || useradd --system --no-create-home --shell /usr/sbin/nologin www
|
||||||
|
mkdir -p "$SITE_DIR"
|
||||||
|
chown www:www "$SITE_DIR"
|
||||||
|
chmod 755 "$SITE_DIR"
|
||||||
|
|
||||||
|
# ── Caddyfile ──────────────────────────────────────────────────────────────
|
||||||
|
echo "==> Installing Caddyfile"
|
||||||
|
cp "$(dirname "$0")/Caddyfile" /etc/caddy/Caddyfile
|
||||||
|
chown root:caddy /etc/caddy/Caddyfile
|
||||||
|
chmod 640 /etc/caddy/Caddyfile
|
||||||
|
|
||||||
|
systemctl enable caddy
|
||||||
|
systemctl restart caddy
|
||||||
|
|
||||||
|
# ── UFW ────────────────────────────────────────────────────────────────────
|
||||||
|
echo "==> Configuring UFW"
|
||||||
|
apt-get install -y ufw
|
||||||
|
ufw default deny incoming
|
||||||
|
ufw default allow outgoing
|
||||||
|
ufw allow ssh
|
||||||
|
ufw allow http
|
||||||
|
ufw allow https
|
||||||
|
ufw --force enable
|
||||||
|
|
||||||
|
# ── Docker ────────────────────────────────────────────────────────────────
|
||||||
|
echo "==> Installing Docker"
|
||||||
|
install -m 0755 -d /etc/apt/keyrings
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
|
||||||
|
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||||
|
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||||
|
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
|
||||||
|
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
|
||||||
|
| tee /etc/apt/sources.list.d/docker.list
|
||||||
|
|
||||||
|
apt-get update -y
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
|
||||||
|
systemctl enable docker
|
||||||
|
|
||||||
|
# ── Deploy user ───────────────────────────────────────────────────────────
|
||||||
|
echo "==> Creating deploy user"
|
||||||
|
id -u "$DEPLOY_USER" &>/dev/null || useradd --create-home --shell /bin/bash "$DEPLOY_USER"
|
||||||
|
|
||||||
|
# Give deploy user write access to the site directory
|
||||||
|
chown -R "$DEPLOY_USER":www "$SITE_DIR"
|
||||||
|
chmod 775 "$SITE_DIR"
|
||||||
|
|
||||||
|
# Set up SSH key auth
|
||||||
|
DEPLOY_HOME="/home/$DEPLOY_USER"
|
||||||
|
mkdir -p "$DEPLOY_HOME/.ssh"
|
||||||
|
chmod 700 "$DEPLOY_HOME/.ssh"
|
||||||
|
touch "$DEPLOY_HOME/.ssh/authorized_keys"
|
||||||
|
chmod 600 "$DEPLOY_HOME/.ssh/authorized_keys"
|
||||||
|
chown -R "$DEPLOY_USER":"$DEPLOY_USER" "$DEPLOY_HOME/.ssh"
|
||||||
|
|
||||||
|
if [[ -n "$DEPLOY_PUBKEY" ]]; then
|
||||||
|
echo "$DEPLOY_PUBKEY" >> "$DEPLOY_HOME/.ssh/authorized_keys"
|
||||||
|
echo "==> Deploy public key installed"
|
||||||
|
else
|
||||||
|
echo "WARNING: DEPLOY_PUBKEY not set. Add your public key to $DEPLOY_HOME/.ssh/authorized_keys manually."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "==> Done. Checklist:"
|
||||||
|
echo " - Point DNS A records for monotrope.au and www.monotrope.au to this server's IP"
|
||||||
|
echo " - If DEPLOY_PUBKEY was not set, add your key to $DEPLOY_HOME/.ssh/authorized_keys"
|
||||||
|
echo " - Run 'make deploy' from your local machine to push the site"
|
||||||
7
site/content/posts/hello-world.md
Normal file
7
site/content/posts/hello-world.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: "Hello, World"
|
||||||
|
date: 2026-04-08
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
|
||||||
|
This is the first post on Monotrope. More to come.
|
||||||
13
site/hugo.toml
Normal file
13
site/hugo.toml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
baseURL = "https://monotrope.au"
|
||||||
|
languageCode = "en-au"
|
||||||
|
title = "Monotrope"
|
||||||
|
|
||||||
|
[markup.goldmark.renderer]
|
||||||
|
unsafe = false
|
||||||
|
|
||||||
|
[outputs]
|
||||||
|
home = ["HTML", "RSS"]
|
||||||
|
section = ["HTML", "RSS"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
description = "A personal blog."
|
||||||
35
site/layouts/_default/baseof.html
Normal file
35
site/layouts/_default/baseof.html
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en-AU">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{{ if not .IsHome }}{{ .Title }} · {{ end }}{{ .Site.Title }}</title>
|
||||||
|
<meta name="description" content="{{ with .Description }}{{ . }}{{ else }}{{ .Site.Params.description }}{{ end }}">
|
||||||
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
|
{{ range .AlternativeOutputFormats -}}
|
||||||
|
{{ printf `
|
||||||
|
<link rel="%s" type="%s" href="%s" title="%s">` .Rel .MediaType.Type .Permalink $.Site.Title | safeHTML }}
|
||||||
|
{{ end -}}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="site-wrap">
|
||||||
|
<header class="site-header">
|
||||||
|
<a class="site-title" href="/">{{ .Site.Title }}</a>
|
||||||
|
<nav class="site-nav">
|
||||||
|
<a href="/posts/">Writing</a>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
{{ block "main" . }}{{ end }}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<p>© {{ now.Year }} Monotrope. Built with <a href="https://gohugo.io">Hugo</a>.</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
11
site/layouts/_default/list.html
Normal file
11
site/layouts/_default/list.html
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
<h2 class="page-heading">{{ .Title }}</h2>
|
||||||
|
<ul class="post-list">
|
||||||
|
{{ range .Pages.ByDate.Reverse }}
|
||||||
|
<li>
|
||||||
|
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2006-01-02" }}</time>
|
||||||
|
<a href="{{ .Permalink }}">{{ .Title }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
9
site/layouts/_default/single.html
Normal file
9
site/layouts/_default/single.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
<article>
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
<p class="post-meta">
|
||||||
|
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2 January 2006" }}</time>
|
||||||
|
</p>
|
||||||
|
{{ .Content }}
|
||||||
|
</article>
|
||||||
|
{{ end }}
|
||||||
10
site/layouts/index.html
Normal file
10
site/layouts/index.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{{ define "main" }}
|
||||||
|
<ul class="post-list">
|
||||||
|
{{ range (where .Site.RegularPages "Type" "posts").ByDate.Reverse }}
|
||||||
|
<li>
|
||||||
|
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "2006-01-02" }}</time>
|
||||||
|
<a href="{{ .Permalink }}">{{ .Title }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
195
site/static/css/main.css
Normal file
195
site/static/css/main.css
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
/* ── Reset & base ─────────────────────────────── */
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Colour tokens ────────────────────────────── */
|
||||||
|
:root {
|
||||||
|
--bg: #ffffff;
|
||||||
|
--fg: #1a1a1a;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--accent: #2563eb;
|
||||||
|
--border: #e5e7eb;
|
||||||
|
--code-bg: #f3f4f6;
|
||||||
|
--max-width: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #111111;
|
||||||
|
--fg: #e5e5e5;
|
||||||
|
--muted: #9ca3af;
|
||||||
|
--accent: #60a5fa;
|
||||||
|
--border: #2d2d2d;
|
||||||
|
--code-bg: #1e1e1e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Typography ───────────────────────────────── */
|
||||||
|
html {
|
||||||
|
font-size: 18px;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
font-family: Georgia, "Times New Roman", serif;
|
||||||
|
line-height: 1.7;
|
||||||
|
padding: 2rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Layout ───────────────────────────────────── */
|
||||||
|
.site-wrap {
|
||||||
|
max-width: var(--max-width);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Header ───────────────────────────────────── */
|
||||||
|
.site-header {
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav a {
|
||||||
|
color: var(--muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
margin-left: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-nav a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Footer ───────────────────────────────────── */
|
||||||
|
.site-footer {
|
||||||
|
margin-top: 4rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Post list ────────────────────────────────── */
|
||||||
|
.post-list {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-list li {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-list time {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-list a {
|
||||||
|
color: var(--fg);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-list a:hover {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Article ──────────────────────────────────── */
|
||||||
|
article h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-meta {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article h2 { font-size: 1.3rem; margin: 2rem 0 0.75rem; }
|
||||||
|
article h3 { font-size: 1.1rem; margin: 1.75rem 0 0.5rem; }
|
||||||
|
|
||||||
|
article p { margin-bottom: 1.25rem; }
|
||||||
|
|
||||||
|
article a {
|
||||||
|
color: var(--accent);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
article ul,
|
||||||
|
article ol {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article li { margin-bottom: 0.25rem; }
|
||||||
|
|
||||||
|
article blockquote {
|
||||||
|
border-left: 3px solid var(--border);
|
||||||
|
padding-left: 1rem;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
article code {
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 0.1em 0.35em;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.875em;
|
||||||
|
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
article pre {
|
||||||
|
background: var(--code-bg);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
article pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
article hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Page heading ─────────────────────────────── */
|
||||||
|
.page-heading {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-family: system-ui, sans-serif;
|
||||||
|
font-weight: normal;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user