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