Hugo site with Woodpecker CI -> Surfer deploy pipeline
Builds the site with hugomods/hugo:dart-sass-node-git and syncs public/ to the Surfer app on the same Cloudron server (surfer put --all --delete). README covers full setup: Gitea OAuth app, Woodpecker env.sh forge config, agent options, and repo secrets.
This commit is contained in:
commit
346c859eb5
8 files changed
+480
No files matched your search
+10
@@ -0,0 +1,10 @@
|
||||
# Hugo build output
|
||||
/public/
|
||||
/resources/_gen/
|
||||
/.hugo_build.lock
|
||||
|
||||
# OS and editor cruft
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -0,0 +1,54 @@
|
||||
# Build the Hugo site and push the result to Surfer (both apps on the same Cloudron).
|
||||
#
|
||||
# Docs:
|
||||
# Workflow syntax: https://woodpecker-ci.org/docs/usage/workflow-syntax
|
||||
# Surfer CLI: https://docs.cloudron.io/packages/surfer/#cli-tool
|
||||
#
|
||||
# Repo secrets expected in Woodpecker (Settings -> Secrets):
|
||||
# surfer_server - Surfer app domain, e.g. surfer.cloudron.example (no scheme)
|
||||
# surfer_token - access token created in the Surfer admin UI (/_admin)
|
||||
|
||||
when:
|
||||
# Deploy on pushes to main only. Other branches never build or deploy.
|
||||
- event: push
|
||||
branch: main
|
||||
# Also allow "Run pipeline" in the Woodpecker UI (redeploying without a push).
|
||||
# If you use this, make sure the two secrets below are also allowed for the
|
||||
# "manual" event, not just push.
|
||||
- event: manual
|
||||
|
||||
steps:
|
||||
# 1) Build the site into ./public
|
||||
#
|
||||
# hugomods/hugo variants: https://docker.hugomods.com/
|
||||
# `dart-sass-node-git` is the maintained "everything" image: extended Hugo +
|
||||
# Node/yarn + Git + Dart Sass. Pin a version with, for example,
|
||||
# hugomods/hugo:dart-sass-node-git-0.165.0 (and drop `pull: true`).
|
||||
- name: build
|
||||
image: hugomods/hugo:dart-sass-node-git
|
||||
pull: true
|
||||
commands:
|
||||
- hugo --minify --gc
|
||||
|
||||
# 2) Upload ./public to Surfer
|
||||
- name: deploy
|
||||
image: node:24-alpine
|
||||
environment:
|
||||
# Where on the Surfer server the site lives.
|
||||
# "/" - the site is served at the Surfer app's domain root
|
||||
# "/mysite/" - served in a subdirectory (keep baseURL in hugo.toml in sync!)
|
||||
SURFER_DEST: /
|
||||
SURFER_SERVER:
|
||||
from_secret: surfer_server
|
||||
SURFER_TOKEN:
|
||||
from_secret: surfer_token
|
||||
commands:
|
||||
- npm install --global cloudron-surfer
|
||||
# public/* (not public) so the CONTENTS of public/ end up in $SURFER_DEST -
|
||||
# `surfer put public /` would upload a literal "public" folder instead.
|
||||
# --all also includes hidden files nested inside those dirs, and --delete removes remote files that are no longer in public/
|
||||
# (a real sync, so renamed assets don't pile up on the server).
|
||||
- surfer put --all --delete --token "$SURFER_TOKEN" --server "$SURFER_SERVER" public/* "$SURFER_DEST"
|
||||
# Optional: top-level dot directories (e.g. .well-known/) are skipped by the
|
||||
# shell glob above, so re-sync them explicitly without --delete:
|
||||
# - if [ -d public/.well-known ]; then surfer put --token "$SURFER_TOKEN" --server "$SURFER_SERVER" public/.well-known "$SURFER_DEST"; fi
|
||||
@@ -0,0 +1,292 @@
|
||||
# auto-deploy — a Gitea → Woodpecker CI → Surfer pipeline on one Cloudron
|
||||
|
||||
A minimal but complete example of a fully self-hosted static-site publishing
|
||||
pipeline, with every piece running on a single
|
||||
[Cloudron](https://cloudron.io) server:
|
||||
|
||||
```text
|
||||
git push webhook "surfer put"
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌───────────┐ clone ┌─────────────────────┐ upload ┌──────────┐
|
||||
│ Gitea ├──────────►│ Woodpecker ├───────────►│ Surfer │
|
||||
│ git host │◄──OAuth───┤ (server + agent) │ public/* │ static │
|
||||
└───────────┘ └─────────────────────┘ │ files │
|
||||
source build & CI └────┬─────┘
|
||||
▼
|
||||
https://surfer.cloudron.example
|
||||
```
|
||||
|
||||
You edit content, push to `main` on Gitea, and the site reappears on Surfer a
|
||||
minute later — no manual uploads, no credentials in the repo. This repo is a
|
||||
working Hugo site; the pipeline in `.woodpecker.yml` is all the plumbing.
|
||||
|
||||
## What's in this repo
|
||||
|
||||
| File | Purpose |
|
||||
| ---- | ------- |
|
||||
| `.woodpecker.yml` | The CI pipeline: build Hugo, upload the output with `surfer put` |
|
||||
| `hugo.toml` | Site config — **`baseURL` must match the Surfer deployment domain** |
|
||||
| `content/_index.md`, `content/about.md` | Minimal content so the example deploys out of the box |
|
||||
| `layouts/home.html`, `layouts/single.html` | Tiny standalone templates (no theme dependency) |
|
||||
| `.gitignore` | Keeps build output (`public/`) out of git |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
A Cloudron server with three apps installed from the App Store:
|
||||
|
||||
| Stage | App | Example location | Cloudron docs |
|
||||
| ----- | --- | ---------------- | ------------- |
|
||||
| Source | Gitea | `https://gitea.cloudron.example` | [docs](https://docs.cloudron.io/packages/gitea/) |
|
||||
| CI | Woodpecker | `https://woodpecker.cloudron.example` | [docs](https://docs.cloudron.io/packages/woodpecker/) |
|
||||
| Serving | Surfer | `https://surfer.cloudron.example` | [docs](https://docs.cloudron.io/packages/surfer/) |
|
||||
|
||||
Substitute your real domains everywhere below.
|
||||
|
||||
## 1. Create a Surfer access token
|
||||
|
||||
1. Open the Surfer admin UI: `https://surfer.cloudron.example/_admin/`.
|
||||
2. Go to **Settings** and create an **Access Token** (it looks like
|
||||
`api-7e6d90ff-...`). You'll add it to Woodpecker in the next sections.
|
||||
3. Decide where the site lives on the Surfer app:
|
||||
- **At the root** — `https://surfer.cloudron.example/` serves the site.
|
||||
Keep `SURFER_DEST: /` in `.woodpecker.yml` and `baseURL = "https://surfer.cloudron.example/"` in `hugo.toml`.
|
||||
- **In a subdirectory** — e.g. `.../mysite/`. Set `SURFER_DEST: /mysite/` **and**
|
||||
`baseURL = "https://surfer.cloudron.example/mysite/"`. If these don't match,
|
||||
CSS/JS/links will 404.
|
||||
|
||||
The simplest setup is one Surfer app per site, each deployed at `/`. Surfer is
|
||||
cheap, so prefer that unless you specifically need several sites in one place.
|
||||
|
||||
## 2. Point Woodpecker at Gitea
|
||||
|
||||
### 2a. Register an OAuth application in Gitea
|
||||
|
||||
In Gitea go to **Settings → Applications → Manage OAuth2 Applications**
|
||||
(`https://gitea.cloudron.example/user/settings/applications`) and create a new
|
||||
application:
|
||||
|
||||
- **Name:** `Woodpecker CI`
|
||||
- **Redirect URI:** `https://woodpecker.cloudron.example/authorize`
|
||||
|
||||
The redirect URI must match your Woodpecker domain *exactly* (scheme, host,
|
||||
`/authorize` path). Copy the generated **Client ID** and **Client Secret**.
|
||||
|
||||
### 2b. Configure the Woodpecker app
|
||||
|
||||
Open the Woodpecker app's **File manager** in the Cloudron dashboard and edit
|
||||
`/app/data/env.sh`, adding a Gitea block:
|
||||
|
||||
```bash
|
||||
export WOODPECKER_GITEA=true
|
||||
export WOODPECKER_GITEA_URL=https://gitea.cloudron.example
|
||||
export WOODPECKER_GITEA_CLIENT=<client id from 2a>
|
||||
export WOODPECKER_GITEA_SECRET=<client secret from 2a>
|
||||
```
|
||||
|
||||
Restart the Woodpecker app. Notes from the [Cloudron
|
||||
docs](https://docs.cloudron.io/packages/woodpecker/):
|
||||
|
||||
- Only one auth provider may be active at a time — remove or set any other
|
||||
`WOODPECKER_<forge>` variables to `false`.
|
||||
- To keep strangers out, add `export WOODPECKER_OPEN=false` and add explicit
|
||||
users via `WOODPECKER_ADMIN=<your-gitea-username,...>` if desired.
|
||||
|
||||
### 2c. Log in
|
||||
|
||||
Open `https://woodpecker.cloudron.example`, choose the Gitea login, and
|
||||
authorize. The first user to sign in becomes a Woodpecker admin.
|
||||
|
||||
## 3. Start a Woodpecker agent
|
||||
|
||||
The Woodpecker app only provides the *server*; pipelines are run by *agents*
|
||||
that connect to it. An agent needs Docker and **must run outside the Cloudron
|
||||
apps themselves**:
|
||||
|
||||
> From the Cloudron docs: *Do not install the agent on the Cloudron server
|
||||
> itself. This is dangerous because the agent has full access to docker and it
|
||||
> can (accidentally) delete or corrupt your apps.*
|
||||
|
||||
Two ways to get a running agent, given the first agent's shared secret is in
|
||||
the Woodpecker app at `/app/data/env.sh` (open the File manager, note the
|
||||
`WOODPECKER_AGENT_SECRET` value):
|
||||
|
||||
**Option A — recommended: separate Docker host / VM**
|
||||
|
||||
```bash
|
||||
docker run --name=woodpecker-agent --restart=always --detach \
|
||||
-e WOODPECKER_SERVER="woodpecker.cloudron.example:9000" \
|
||||
-e WOODPECKER_MAX_WORKFLOWS=4 \
|
||||
-e WOODPECKER_GRPC_SECURE=true \
|
||||
-e WOODPECKER_LOG_LEVEL=info \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-e WOODPECKER_BACKEND=docker \
|
||||
-e WOODPECKER_AGENT_SECRET="value from /app/data/env.sh" \
|
||||
woodpeckerci/woodpecker-agent:latest
|
||||
```
|
||||
|
||||
Any cheap VM works, since the agent only needs outbound access to the server.
|
||||
|
||||
**Option B — same Cloudron server** (accepting the risk flagged above): run the
|
||||
exact same command on the Cloudron host itself, via `ssh`. The agent talks to
|
||||
the Woodpecker server over its public gRPC endpoint (`:9000`, TLS) and spawns
|
||||
pipeline step containers (Hugo build, Surfer upload). For a one-person server
|
||||
this is a common trade-off; keep the exposure as small as it can be:
|
||||
|
||||
- `WOODPECKER_OPEN=false` so only your Gitea users can ever log in.
|
||||
- Enable only repositories you control, and never turn on *trusted* mode for
|
||||
them: without trusted mode, pipeline steps run as ordinary throwaway
|
||||
containers — no docker socket inside them, no host mounts, no privileged
|
||||
access. The socket lives in the agent process only.
|
||||
- Keep secrets unavailable to pull requests (Woodpecker's default) and never
|
||||
store anything more powerful than the Surfer token in repo secrets.
|
||||
- Point Cloudron backups off-server (e.g. S3): the scenario this warning
|
||||
guards against is damage to the whole platform, which local backups on the
|
||||
same disk may not survive.
|
||||
- Rotate `WOODPECKER_AGENT_SECRET` if in doubt (it also lives in
|
||||
`/app/data/env.sh`).
|
||||
|
||||
Verify the agent registered: `docker logs -f woodpecker-agent` should show it
|
||||
polling without errors, and the Woodpecker UI's *Agents* page lists it.
|
||||
|
||||
## 4. Create the repo in Gitea and push
|
||||
|
||||
Create an empty repository in the Gitea web UI (e.g.
|
||||
`<you>/auto-deploy`), then push this directory:
|
||||
|
||||
```bash
|
||||
git init
|
||||
git add .
|
||||
git commit -m "Hugo site with Woodpecker → Surfer deploy pipeline"
|
||||
git branch -M main
|
||||
git remote add origin https://gitea.cloudron.example/<you>/auto-deploy.git
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
## 5. Activate the repo in Woodpecker and add secrets
|
||||
|
||||
1. In Woodpecker, open **Repositories → New repository** and enable your repo
|
||||
(you need admin rights on it in Gitea). Woodpecker installs a webhook on it.
|
||||
2. In the repo's **Settings → Secrets**, add:
|
||||
|
||||
| Name | Value |
|
||||
| ---- | ----- |
|
||||
| `surfer_server` | `surfer.cloudron.example` — domain only, no `https://` |
|
||||
| `surfer_token` | the access token from step 1 (`api-...`) |
|
||||
|
||||
When adding the secrets, make sure they are permitted for the events your
|
||||
pipeline uses — at minimum `push`; add `manual` as well if you plan UI-triggered
|
||||
redeploys. Woodpecker's default secrecy rules already exclude pull requests.
|
||||
|
||||
## 6. Deploy
|
||||
|
||||
Every push to `main` now triggers a build. If nothing runs (e.g. you want to
|
||||
re-deploy without a commit), use **Pipelines → Run pipeline** (the `manual`
|
||||
event). After the pipeline goes green, visit
|
||||
`https://surfer.cloudron.example/` — the example site is there, complete with
|
||||
a footer noting how it got built.
|
||||
|
||||
## How the pipeline works
|
||||
|
||||
`.woodpecker.yml` has two steps, triggered only by pushes to `main` and manual
|
||||
runs:
|
||||
|
||||
1. **build** — `hugomods/hugo:dart-sass-node-git` runs `hugo --minify --gc`,
|
||||
producing `public/`. That image variant contains extended Hugo plus Node,
|
||||
Git and Dart Sass; other variants are listed at
|
||||
[docker.hugomods.com](https://docker.hugomods.com/). To pin an exact Hugo
|
||||
version, set `image: hugomods/hugo:dart-sass-node-git-0.165.0` and drop the
|
||||
`pull: true` line (which only exists to keep a floating tag fresh).
|
||||
2. **deploy** — a small `node:24-alpine` step installs the Surfer CLI and runs:
|
||||
|
||||
```bash
|
||||
surfer put --all --delete --token ... --server ... public/* "$SURFER_DEST"
|
||||
```
|
||||
|
||||
Exactly why each part matters:
|
||||
|
||||
| piece | effect |
|
||||
| ----- | ------ |
|
||||
| `public/*` (not `public`) | uploads the *contents* of `public/` into the destination; `surfer put public /` would create a literal `public/` folder |
|
||||
| `--all` | include dotfiles in subdirectories |
|
||||
| `--delete` | remove remote files that are no longer in `public/`, so renamed assets don't pile up; changed files upload incrementally (mtime/size), unchanged ones are skipped |
|
||||
| destination | must be an absolute path like `/` or `/mysite/`, kept in sync with `baseURL` in `hugo.toml` |
|
||||
|
||||
Note that this makes Surfer's destination a **managed mirror of `public/`**:
|
||||
anything you upload manually to that folder will be deleted on the next
|
||||
deploy. The `--server` value may be the bare domain — the CLI adds
|
||||
`https://` automatically.
|
||||
|
||||
## Top-level dotfiles (`.well-known/`, `.htaccess`, …)
|
||||
|
||||
The shell glob `public/*` never expands to top-level dotfiles, and `--delete`
|
||||
would then treat them as stale. If you need one (e.g. `.well-known/` for
|
||||
webmaster verification files), uncomment the optional line at the end of the
|
||||
`deploy` step — it re-uploads the dotdir right after the main sync, guarding on
|
||||
its existence so deploys without it keep working.
|
||||
|
||||
## Adapting it
|
||||
|
||||
- **Another generator?** Replace the build step and the output dir; the deploy
|
||||
step stays identical:
|
||||
|
||||
```yaml
|
||||
# Astro # Eleventy # Zola
|
||||
commands: commands: image: ghcr.io/getzola/zola:v0.20.2
|
||||
- npm ci - npm ci commands:
|
||||
- npm run build - npx @11ty/eleventy - zola build
|
||||
# deploy step source: # deploy step source: # deploy step source:
|
||||
# dist/* # _site/* # public/*
|
||||
```
|
||||
|
||||
- **Multiple sites on one Surfer** in subfolders: give each repo its own
|
||||
`SURFER_DEST` (e.g. `/blog/`, `/photos/`) and match `baseURL` in each. Keep
|
||||
in mind the `--delete` sync only touches that destination folder.
|
||||
- **Private repos work fine**: Woodpecker clones via Gitea OAuth; nothing here
|
||||
assumes your Gitea repo is public.
|
||||
- **Non-deploy jobs** (lint, tests) just become a normal step before `deploy`
|
||||
— steps run in declaration order and a failure stops the pipeline.
|
||||
|
||||
## Security notes
|
||||
|
||||
- Never commit the Surfer token — it lives only in Woodpecker secrets. If it
|
||||
ever leaks, revoke it in the Surfer admin UI and update the secret, then
|
||||
re-run the pipeline manually.
|
||||
- The `deploy` step only exposes the two secrets to `node:24-alpine`; other
|
||||
steps never see them.
|
||||
- For extra paranoia you can restrict both secrets to specific images in the
|
||||
secret's settings, and consider Surfer's own access control (password-restricted
|
||||
sites) for staging sites.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
| ------- | ------------ | --- |
|
||||
| Pipeline never appears after push | webhook missing or blocked | re-check repo activation; check Gitea → repo → Webhooks for delivery errors; on Cloudron, Gitea config lives in `/app/data/app.ini` |
|
||||
| Pipeline stuck in *pending* | no agent connected | `docker logs woodpecker-agent`; verify `WOODPECKER_SERVER=<woodpecker-domain>:9000`, `WOODPECKER_GRPC_SECURE=true`, and the agent secret from the Woodpecker app's `/app/data/env.sh` |
|
||||
| Login fails / repo list empty | OAuth setup | redirect URI must be exactly `https://<woodpecker-domain>/authorize`; only one `WOODPECKER_<forge>` provider active |
|
||||
| Deploy step says "Run surfer config first" | secret missing / wrong name | secrets must be named `surfer_server` / `surfer_token` (see `.woodpecker.yml`) and permitted for this event |
|
||||
| `Invalid token` at deploy | token rotated or revoked | create a fresh token in Surfer's admin UI, update the `surfer_token` secret, run pipeline manually |
|
||||
| Site deploys but CSS/links 404 | `baseURL` and `SURFER_DEST` don't match | make them identical (including path) |
|
||||
| Site under a literal `public/` path | forgot the glob | use `public/*`, not `public` |
|
||||
| Old files still served after renaming assets | sync flags missing | keep `--delete --all` in the `surfer put` command |
|
||||
| `surfer: command not found` | npm install failed in the deploy step | check the deploy step logs (registry access) |
|
||||
|
||||
## Local development
|
||||
|
||||
```bash
|
||||
hugo server -D # preview at http://localhost:1313
|
||||
hugo --minify --gc # same build as CI, output in public/
|
||||
```
|
||||
|
||||
You can also dry-run the workflow with [woodpecker-cli](https://woodpecker-ci.org/docs/usage/cli) (`woodpecker-cli exec .woodpecker.yml`).
|
||||
|
||||
## References
|
||||
|
||||
- Cloudron Surfer docs & CLI: https://docs.cloudron.io/packages/surfer/
|
||||
- Cloudron Woodpecker docs (forge setup, agent): https://docs.cloudron.io/packages/woodpecker/
|
||||
- Woodpecker workflow syntax: https://woodpecker-ci.org/docs/usage/workflow-syntax
|
||||
- Woodpecker secrets: https://woodpecker-ci.org/docs/usage/secrets
|
||||
- Woodpecker × Gitea: https://woodpecker-ci.org/docs/administration/configuration/forges/gitea
|
||||
- Hugo Docker images: https://docker.hugomods.com/
|
||||
- Hugo layout rules: https://gohugo.io/templates/lookup-order/
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Home
|
||||
---
|
||||
|
||||
# It is alive!
|
||||
|
||||
This static site is deployed automatically:
|
||||
|
||||
1. You push a commit to `main` on **Gitea** (same Cloudron server).
|
||||
2. **Woodpecker CI** picks up the push, builds the site with Hugo.
|
||||
3. The build output is uploaded to **Surfer**, which serves it.
|
||||
|
||||
That's it — no credentials in the repo, no manual uploads. Push something to
|
||||
`main` right now and watch the pipeline in Woodpecker.
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
title: About
|
||||
---
|
||||
|
||||
This example shows a complete, self-hosted publishing pipeline on a single
|
||||
Cloudron server:
|
||||
|
||||
| Stage | App | Example location |
|
||||
| ----- | --- | ---------------- |
|
||||
| Source | Gitea | `https://gitea.cloudron.example` |
|
||||
| Build | Woodpecker CI | `https://woodpecker.cloudron.example` |
|
||||
| Serving | Surfer | `https://surfer.cloudron.example` |
|
||||
|
||||
The pipeline is defined by a single checked-in file, [`.woodpecker.yml`](https://woodpecker-ci.org/docs/usage/workflow-syntax), and the only
|
||||
secrets it needs are a Surfer domain and an access token. See the repository
|
||||
README for full setup instructions.
|
||||
|
||||
Delete this page or rewrite everything — the templates in `layouts/` are
|
||||
deliberately small so you can replace them with a real design.
|
||||
@@ -0,0 +1,15 @@
|
||||
# The final URL of the site, as served by Surfer. It must exactly match the
|
||||
# deployment destination on the Surfer app, or CSS/JS/images will 404.
|
||||
#
|
||||
# Site at the Surfer app's root domain (SURFER_DEST = "/" in .woodpecker.yml):
|
||||
baseURL = "https://surfer.cloudron.example/"
|
||||
#
|
||||
# Site in a subdirectory (SURFER_DEST = "/mysite/"):
|
||||
# baseURL = "https://surfer.cloudron.example/mysite/"
|
||||
|
||||
locale = "en-us"
|
||||
title = "My Cloudron Site"
|
||||
|
||||
# This minimal example uses no taxonomies; disabling them avoids empty
|
||||
# /tags/ and /categories/ pages.
|
||||
disableKinds = ["taxonomy", "term"]
|
||||
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html lang="{{ site.Language.Locale }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ .Title }} | {{ site.Title }}</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 42rem; margin: 0 auto; padding: 0 1rem 3rem;
|
||||
line-height: 1.6; font-size: 1.05rem;
|
||||
}
|
||||
header, footer { padding: 1.25rem 0; }
|
||||
header a { font-weight: 700; text-decoration: none; }
|
||||
header nav { float: right; font-weight: 400; }
|
||||
footer { border-top: 1px solid gray; font-size: .85rem; opacity: .75; }
|
||||
img { max-width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="{{ site.Home.RelPermalink }}">{{ site.Title }}</a>
|
||||
<nav>
|
||||
{{ with site.GetPage "/about" }}<a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{{ .Content }}
|
||||
</main>
|
||||
<footer>
|
||||
<small>
|
||||
Served by Surfer · built by Woodpecker CI · source on Gitea
|
||||
— one Cloudron server.
|
||||
</small>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html lang="{{ site.Language.Locale }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ .Title }} | {{ site.Title }}</title>
|
||||
<style>
|
||||
:root { color-scheme: light dark; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, sans-serif;
|
||||
max-width: 42rem; margin: 0 auto; padding: 0 1rem 3rem;
|
||||
line-height: 1.6; font-size: 1.05rem;
|
||||
}
|
||||
header, footer { padding: 1.25rem 0; }
|
||||
header a { font-weight: 700; text-decoration: none; }
|
||||
header nav { float: right; font-weight: 400; }
|
||||
footer { border-top: 1px solid gray; font-size: .85rem; opacity: .75; }
|
||||
img { max-width: 100%; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<a href="{{ site.Home.RelPermalink }}">{{ site.Title }}</a>
|
||||
<nav>
|
||||
{{ with site.GetPage "/about" }}<a href="{{ .RelPermalink }}">{{ .Title }}</a>{{ end }}
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{{ .Content }}
|
||||
</main>
|
||||
<footer>
|
||||
<small>
|
||||
Served by Surfer · built by Woodpecker CI · source on Gitea
|
||||
— one Cloudron server.
|
||||
</small>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user