Adds hugo stack.
This commit is contained in:
@@ -145,8 +145,9 @@ castle apply # served at my-frontend.<domain>
|
|||||||
```
|
```
|
||||||
|
|
||||||
The gateway serves the build **in place** from `<source>/<root>` — no copy, no
|
The gateway serves the build **in place** from `<source>/<root>` — no copy, no
|
||||||
Node process. Stack: **`docs/stacks/react-vite.md`**. Database-backed apps on the
|
Node process. Stack: **`docs/stacks/react-vite.md`**. Content-driven static sites
|
||||||
shared Supabase substrate: **`docs/stacks/supabase.md`**.
|
built by Hugo: **`docs/stacks/hugo.md`**. Database-backed apps on the shared
|
||||||
|
Supabase substrate: **`docs/stacks/supabase.md`**.
|
||||||
|
|
||||||
### Adopt an existing repo (no stack needed)
|
### Adopt an existing repo (no stack needed)
|
||||||
|
|
||||||
@@ -324,6 +325,7 @@ Inspect + drive from the CLI: **`castle mesh status`** / **`castle mesh nodes`**
|
|||||||
| Writing FastAPI services | **`docs/stacks/python-fastapi.md`** |
|
| Writing FastAPI services | **`docs/stacks/python-fastapi.md`** |
|
||||||
| Writing CLI tools | **`docs/stacks/python-cli.md`** |
|
| Writing CLI tools | **`docs/stacks/python-cli.md`** |
|
||||||
| Writing React/Vite frontends | **`docs/stacks/react-vite.md`** |
|
| Writing React/Vite frontends | **`docs/stacks/react-vite.md`** |
|
||||||
|
| Writing Hugo static sites | **`docs/stacks/hugo.md`** |
|
||||||
| Database-backed apps (shared Supabase) | **`docs/stacks/supabase.md`** |
|
| Database-backed apps (shared Supabase) | **`docs/stacks/supabase.md`** |
|
||||||
| **Developing Castle itself** (CLI/core/api/app, key files, endpoints) | **`docs/developing-castle.md`** |
|
| **Developing Castle itself** (CLI/core/api/app, key files, endpoints) | **`docs/developing-castle.md`** |
|
||||||
|
|
||||||
|
|||||||
@@ -29,14 +29,16 @@ STACK_DEFAULTS: dict[str, str] = {
|
|||||||
"python-cli": "tool",
|
"python-cli": "tool",
|
||||||
"react-vite": "static",
|
"react-vite": "static",
|
||||||
"supabase": "static",
|
"supabase": "static",
|
||||||
|
"hugo": "static",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Static build output per stack, for `static` (caddy) deployments. The gateway
|
# Static build output per stack, for `static` (caddy) deployments. The gateway
|
||||||
# serves this dir in place at <name>.<gateway.domain> (no service, no process).
|
# serves this dir in place at <name>.<gateway.domain> (no service, no process).
|
||||||
# A supabase app ships a raw `public/`; react-vite builds to `dist/`.
|
# A supabase app ships a raw `public/`; react-vite builds to `dist/`; hugo to `public/`.
|
||||||
STACK_BUILD_OUTPUTS: dict[str, str] = {
|
STACK_BUILD_OUTPUTS: dict[str, str] = {
|
||||||
"supabase": "public",
|
"supabase": "public",
|
||||||
"react-vite": "dist",
|
"react-vite": "dist",
|
||||||
|
"hugo": "public",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Substrate a stack's apps depend on — seeded as a deployment `requires` at creation
|
# Substrate a stack's apps depend on — seeded as a deployment `requires` at creation
|
||||||
@@ -180,6 +182,10 @@ def run_create(args: argparse.Namespace) -> int:
|
|||||||
print(" # edit migrations/, functions/, public/ — targets the shared substrate")
|
print(" # edit migrations/, functions/, public/ — targets the shared substrate")
|
||||||
print(f" castle program build {name} # apply migrations to the substrate")
|
print(f" castle program build {name} # apply migrations to the substrate")
|
||||||
print(f" castle apply # serve at {name}.<gateway.domain>")
|
print(f" castle apply # serve at {name}.<gateway.domain>")
|
||||||
|
elif stack == "hugo":
|
||||||
|
print(" # edit content/, layouts/ (or add a theme under themes/)")
|
||||||
|
print(f" castle program build {name} # hugo --gc --minify -> public/")
|
||||||
|
print(f" castle apply {name} # serve at {name}.<gateway.domain>")
|
||||||
elif stack:
|
elif stack:
|
||||||
print(" uv sync")
|
print(" uv sync")
|
||||||
if kind == "service":
|
if kind == "service":
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
@@ -20,6 +22,8 @@ def scaffold_project(
|
|||||||
_scaffold_tool(project_dir, name, package_name, description)
|
_scaffold_tool(project_dir, name, package_name, description)
|
||||||
elif stack == "supabase":
|
elif stack == "supabase":
|
||||||
_scaffold_supabase(project_dir, name, description)
|
_scaffold_supabase(project_dir, name, description)
|
||||||
|
elif stack == "hugo":
|
||||||
|
_scaffold_hugo(project_dir, name, description)
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No scaffold template for stack: {stack}")
|
raise ValueError(f"No scaffold template for stack: {stack}")
|
||||||
|
|
||||||
@@ -771,6 +775,200 @@ Auth/WebCrypto apps should get their own HTTPS host route (secure context).
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _hugo_new_site(project_dir: Path) -> bool:
|
||||||
|
"""Create the canonical site skeleton with Hugo's own scaffolder.
|
||||||
|
|
||||||
|
Delegates the standard structure (archetypes/, the content/layouts/static/…
|
||||||
|
dir tree, hugo.toml) to `hugo new site` rather than reinventing it. Returns
|
||||||
|
False if the hugo binary isn't present at create time — the caller then falls
|
||||||
|
back to a minimal hand-written skeleton (the build needs hugo regardless)."""
|
||||||
|
if shutil.which("hugo") is None:
|
||||||
|
return False
|
||||||
|
# `hugo new site` requires an empty/absent target; create runs before git init,
|
||||||
|
# so project_dir doesn't exist yet.
|
||||||
|
result = subprocess.run(
|
||||||
|
["hugo", "new", "site", str(project_dir), "--format", "toml"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def _scaffold_hugo(project_dir: Path, name: str, description: str) -> None:
|
||||||
|
"""Scaffold a Hugo site that builds and serves theme-less out of the box.
|
||||||
|
|
||||||
|
The canonical skeleton comes from Hugo's own `hugo new site`; on top of it we
|
||||||
|
overlay the pieces a bare skeleton lacks — the layouts needed to render a home
|
||||||
|
page without a theme, sample content, and a castle-flavored hugo.toml/README.
|
||||||
|
|
||||||
|
`castle program build` runs `hugo --gc --minify` → `public/`, which a caddy
|
||||||
|
deployment serves in place at <name>.<gateway.domain>. baseURL is `/` so assets
|
||||||
|
resolve at the root of the site's own subdomain. A theme (with its own asset
|
||||||
|
pipeline) can be dropped under themes/ later; such themes declare a two-step
|
||||||
|
`build.commands` in the program spec, which overrides the stack default."""
|
||||||
|
|
||||||
|
def sub(text: str) -> str:
|
||||||
|
return text.replace("__NAME__", name).replace("__DESC__", description)
|
||||||
|
|
||||||
|
# Prefer Hugo's native scaffolder; fall back to the bits it would have made.
|
||||||
|
if not _hugo_new_site(project_dir):
|
||||||
|
_write(
|
||||||
|
project_dir / "archetypes" / "default.md",
|
||||||
|
"""+++
|
||||||
|
date = '{{ .Date }}'
|
||||||
|
draft = true
|
||||||
|
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
|
||||||
|
+++
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- hugo.toml — overwrite Hugo's example.org default; baseURL '/' serves at
|
||||||
|
# the subdomain root ---
|
||||||
|
_write(
|
||||||
|
project_dir / "hugo.toml",
|
||||||
|
sub(
|
||||||
|
"""baseURL = "/"
|
||||||
|
languageCode = "en-us"
|
||||||
|
title = "__NAME__"
|
||||||
|
|
||||||
|
[params]
|
||||||
|
description = "__DESC__"
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- layouts/_default/baseof.html — the shared page skeleton (blocks) ---
|
||||||
|
_write(
|
||||||
|
project_dir / "layouts" / "_default" / "baseof.html",
|
||||||
|
sub(
|
||||||
|
"""<!DOCTYPE html>
|
||||||
|
<html lang="{{ .Site.LanguageCode | default "en" }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ block "title" . }}{{ .Site.Title }}{{ end }}</title>
|
||||||
|
<meta name="description" content="{{ .Site.Params.description }}">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header><a href="{{ "/" | relURL }}">{{ .Site.Title }}</a></header>
|
||||||
|
<main>{{ block "main" . }}{{ end }}</main>
|
||||||
|
<footer>© {{ now.Year }} {{ .Site.Title }}</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- layouts/index.html — the home page ---
|
||||||
|
_write(
|
||||||
|
project_dir / "layouts" / "index.html",
|
||||||
|
"""{{ define "main" }}
|
||||||
|
{{ .Content }}
|
||||||
|
<ul>
|
||||||
|
{{ range (where .Site.RegularPages "Type" "posts") }}
|
||||||
|
<li>
|
||||||
|
<a href="{{ .RelPermalink }}">{{ .Title }}</a>
|
||||||
|
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- layouts/_default/{single,list}.html — content pages ---
|
||||||
|
_write(
|
||||||
|
project_dir / "layouts" / "_default" / "single.html",
|
||||||
|
"""{{ define "title" }}{{ .Title }} · {{ .Site.Title }}{{ end }}
|
||||||
|
{{ define "main" }}
|
||||||
|
<article>
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
|
||||||
|
{{ .Content }}
|
||||||
|
</article>
|
||||||
|
{{ end }}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
_write(
|
||||||
|
project_dir / "layouts" / "_default" / "list.html",
|
||||||
|
"""{{ define "title" }}{{ .Title }} · {{ .Site.Title }}{{ end }}
|
||||||
|
{{ define "main" }}
|
||||||
|
<h1>{{ .Title }}</h1>
|
||||||
|
{{ .Content }}
|
||||||
|
<ul>
|
||||||
|
{{ range .Pages }}
|
||||||
|
<li><a href="{{ .RelPermalink }}">{{ .Title }}</a></li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
{{ end }}
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- content — home + an example post ---
|
||||||
|
_write(
|
||||||
|
project_dir / "content" / "_index.md",
|
||||||
|
sub(
|
||||||
|
"""---
|
||||||
|
title: "__NAME__"
|
||||||
|
---
|
||||||
|
|
||||||
|
Welcome to **__NAME__** — a Hugo site on castle. Edit `content/_index.md` and
|
||||||
|
add posts under `content/posts/`.
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_write(
|
||||||
|
project_dir / "content" / "posts" / "hello.md",
|
||||||
|
"""---
|
||||||
|
title: "Hello, world"
|
||||||
|
date: 2024-01-01
|
||||||
|
---
|
||||||
|
|
||||||
|
Your first post. Edit me in `content/posts/`, or run `hugo new posts/next.md`.
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- .gitignore — build output is regenerated, never committed ---
|
||||||
|
_write(
|
||||||
|
project_dir / ".gitignore",
|
||||||
|
"/public/\n/resources/\n.hugo_build.lock\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- README ---
|
||||||
|
_write(
|
||||||
|
project_dir / "README.md",
|
||||||
|
sub(
|
||||||
|
"""# __NAME__
|
||||||
|
|
||||||
|
__DESC__
|
||||||
|
|
||||||
|
A [Hugo](https://gohugo.io) static site managed by castle.
|
||||||
|
|
||||||
|
## Develop
|
||||||
|
|
||||||
|
hugo server -D # live preview at http://localhost:1313
|
||||||
|
|
||||||
|
## Build & serve
|
||||||
|
|
||||||
|
castle program build __NAME__ # hugo --gc --minify -> public/
|
||||||
|
castle apply __NAME__ # serve at __NAME__.<gateway.domain>
|
||||||
|
|
||||||
|
## Themes
|
||||||
|
|
||||||
|
Drop a theme under `themes/` (often a git submodule) and set `theme` in
|
||||||
|
`hugo.toml`. A theme with an asset pipeline (Tailwind, etc.) needs a pre-build
|
||||||
|
step; declare it in `programs/__NAME__.yaml` so it overrides the stack default:
|
||||||
|
|
||||||
|
build:
|
||||||
|
commands:
|
||||||
|
- [pnpm, build]
|
||||||
|
- [hugo, --gc, --minify]
|
||||||
|
outputs: [public]
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _write(path: Path, content: str) -> None:
|
def _write(path: Path, content: str) -> None:
|
||||||
"""Write content to a file, creating parent directories."""
|
"""Write content to a file, creating parent directories."""
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|||||||
@@ -111,6 +111,14 @@ class StackHandler:
|
|||||||
# stacks whose `teardown` actually destroys something.
|
# stacks whose `teardown` actually destroys something.
|
||||||
owns_data: bool = False
|
owns_data: bool = False
|
||||||
|
|
||||||
|
# The dev verbs this stack advertises — what `available_actions` offers and
|
||||||
|
# `run_action` will dispatch. Defaults to every stack verb (the python/react/
|
||||||
|
# supabase handlers implement them all); a narrow stack like hugo (build-only,
|
||||||
|
# no native lint/test/type-check) overrides this so callers aren't offered verbs
|
||||||
|
# that would only error. `check` composes the sub-verbs, so a handler that drops
|
||||||
|
# lint/type-check/test also drops `check` implicitly.
|
||||||
|
provides: set[str] = _STACK_VERBS
|
||||||
|
|
||||||
async def build(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
async def build(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@@ -374,6 +382,59 @@ class ReactViteHandler(StackHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HugoHandler(StackHandler):
|
||||||
|
"""Handler for the hugo stack — a static site built by the Hugo generator.
|
||||||
|
|
||||||
|
Hugo has one real dev verb: **build** (`hugo --gc --minify` → `public/`), which
|
||||||
|
a `caddy` deployment then serves in place at `<name>.<gateway.domain>`. There is
|
||||||
|
no native lint/test/type-check, so `provides` is narrowed to the verbs that do
|
||||||
|
something. A theme with an asset pipeline (e.g. Blowfish + Tailwind) declares its
|
||||||
|
own two-step `build.commands` (`pnpm build` then `hugo …`), which override this
|
||||||
|
default per the usual declared-command-wins resolution."""
|
||||||
|
|
||||||
|
provides = {"build", "install", "uninstall"}
|
||||||
|
|
||||||
|
async def build(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
|
src = _source_dir(comp, root)
|
||||||
|
rc, output = await _run(["hugo", "--gc", "--minify"], src)
|
||||||
|
return ActionResult(
|
||||||
|
program=name,
|
||||||
|
action="build",
|
||||||
|
status="ok" if rc == 0 else "error",
|
||||||
|
output=output,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def install(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
|
"""Build the site in place. The gateway serves it directly from
|
||||||
|
<source>/<build.outputs[0]> (default `public/`) — no copy step."""
|
||||||
|
result = await self.build(name, comp, root)
|
||||||
|
if result.status != "ok":
|
||||||
|
return ActionResult(
|
||||||
|
program=name,
|
||||||
|
action="install",
|
||||||
|
status="error",
|
||||||
|
output=f"Build failed:\n{result.output}",
|
||||||
|
)
|
||||||
|
outputs = comp.build.outputs if comp.build else []
|
||||||
|
dist = _source_dir(comp, root) / (outputs[0] if outputs else "public")
|
||||||
|
return ActionResult(
|
||||||
|
program=name,
|
||||||
|
action="install",
|
||||||
|
status="ok",
|
||||||
|
output=f"Built; served in place from {dist}",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def uninstall(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult:
|
||||||
|
"""Static sites have no install footprint — served in place. Deactivating one
|
||||||
|
means dropping its gateway route, not deleting build output."""
|
||||||
|
return ActionResult(
|
||||||
|
program=name,
|
||||||
|
action="uninstall",
|
||||||
|
status="ok",
|
||||||
|
output=f"{name}: served in place; nothing to uninstall.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _migration_version(path: Path) -> str:
|
def _migration_version(path: Path) -> str:
|
||||||
"""The version key of a migration file — the leading token before '_'.
|
"""The version key of a migration file — the leading token before '_'.
|
||||||
|
|
||||||
@@ -655,6 +716,7 @@ HANDLERS: dict[str, StackHandler] = {
|
|||||||
"python-fastapi": PythonHandler(),
|
"python-fastapi": PythonHandler(),
|
||||||
"react-vite": ReactViteHandler(),
|
"react-vite": ReactViteHandler(),
|
||||||
"supabase": SupabaseHandler(),
|
"supabase": SupabaseHandler(),
|
||||||
|
"hugo": HugoHandler(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -687,12 +749,12 @@ def _declared_commands(comp: ProgramSpec, verb: str) -> list[list[str]] | None:
|
|||||||
|
|
||||||
|
|
||||||
def _stack_provides(comp: ProgramSpec, verb: str) -> bool:
|
def _stack_provides(comp: ProgramSpec, verb: str) -> bool:
|
||||||
"""Whether the program's stack handler can run this verb."""
|
"""Whether the program's stack handler advertises this verb.
|
||||||
return (
|
|
||||||
bool(comp.source)
|
Consults the handler's ``provides`` set (default: all of ``_STACK_VERBS``) so a
|
||||||
and verb in _STACK_VERBS
|
build-only stack like hugo doesn't offer lint/test/type-check it can't run."""
|
||||||
and get_handler(comp.stack) is not None
|
handler = get_handler(comp.stack)
|
||||||
)
|
return bool(comp.source) and handler is not None and verb in handler.provides
|
||||||
|
|
||||||
|
|
||||||
def is_available(comp: ProgramSpec, verb: str) -> bool:
|
def is_available(comp: ProgramSpec, verb: str) -> bool:
|
||||||
|
|||||||
96
docs/stacks/hugo.md
Normal file
96
docs/stacks/hugo.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# Hugo static sites in Castle
|
||||||
|
|
||||||
|
> **This is a stack — creation-time guidance for writing _new_ sites.**
|
||||||
|
> A stack is a template + conventions, not a runtime requirement. `castle program
|
||||||
|
> create --stack hugo` scaffolds from it (via Hugo's own `hugo new site`) and seeds
|
||||||
|
> the program's default build verb. An existing Hugo site adopted with `castle
|
||||||
|
> program add` doesn't need this stack — it declares its own `commands:` /
|
||||||
|
> `build:`. See @docs/registry.md for `commands:`, `stack:` (optional), and `repo:`.
|
||||||
|
|
||||||
|
How to build, serve, and manage [Hugo](https://gohugo.io) sites as castle programs.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- Generator: Hugo (extended recommended — needed for SCSS/asset processing)
|
||||||
|
- Build: `hugo --gc --minify` → `public/`
|
||||||
|
- Served: `manager: caddy` static deployment, in place at `<name>.<gateway.domain>`
|
||||||
|
- Package manager (only if a theme needs an asset pipeline): pnpm
|
||||||
|
|
||||||
|
Hugo has one meaningful dev verb — **build**. It has no native lint/test/type-check,
|
||||||
|
so the stack advertises only `build` / `install` / `uninstall`; `castle check` and
|
||||||
|
friends aren't offered (a site can still declare its own, e.g. an HTML linter, under
|
||||||
|
`commands:` — a declared verb always wins over the stack).
|
||||||
|
|
||||||
|
## Create a new site
|
||||||
|
|
||||||
|
```bash
|
||||||
|
castle program create my-site --stack hugo --description "My site"
|
||||||
|
cd /data/repos/my-site
|
||||||
|
castle program build my-site # hugo --gc --minify -> public/
|
||||||
|
castle apply my-site # serve at my-site.<gateway.domain>
|
||||||
|
```
|
||||||
|
|
||||||
|
The scaffold delegates the canonical skeleton to `hugo new site` (archetypes/,
|
||||||
|
content/, layouts/, static/, themes/, hugo.toml) and overlays the pieces a bare
|
||||||
|
skeleton lacks: minimal `layouts/` so the site **builds and serves without a
|
||||||
|
theme**, an example `content/posts/hello.md`, a castle-flavored `hugo.toml`
|
||||||
|
(`baseURL = "/"`, so assets resolve at the root of the site's own subdomain), and a
|
||||||
|
`.gitignore` for the regenerated `public/` and `resources/`.
|
||||||
|
|
||||||
|
Develop with the live server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
hugo server -D # http://localhost:1313, rebuilds on save
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a theme
|
||||||
|
|
||||||
|
Drop a theme under `themes/` (usually a git submodule) and set `theme` in
|
||||||
|
`hugo.toml`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule add https://github.com/<owner>/<theme>.git themes/<theme>
|
||||||
|
```
|
||||||
|
|
||||||
|
Themes with an **asset pipeline** (e.g. Blowfish + Tailwind) need a pre-build step
|
||||||
|
before `hugo`. Declare it as a two-step `build` in `programs/<name>.yaml` — a
|
||||||
|
declared `build.commands` overrides the stack's single-step default:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
build:
|
||||||
|
commands:
|
||||||
|
- [pnpm, build] # compile the theme's CSS/JS
|
||||||
|
- [hugo, --gc, --minify] # render the site -> public/
|
||||||
|
outputs: [public]
|
||||||
|
```
|
||||||
|
|
||||||
|
One-time setup those themes expect (run once in the source tree):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git submodule update --init --recursive
|
||||||
|
cd themes/<theme> && pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment shape
|
||||||
|
|
||||||
|
`castle program create --stack hugo` writes:
|
||||||
|
|
||||||
|
- **`programs/<name>.yaml`** — `source`, `stack: hugo`, `build.outputs: [public]`.
|
||||||
|
- **`deployments/statics/<name>.yaml`** — `manager: caddy`, `root: public`,
|
||||||
|
`reach: internal` (flip to `public` to also expose over the tunnel).
|
||||||
|
|
||||||
|
The gateway serves `<source>/public` in place — no copy, no Node/Hugo process at
|
||||||
|
runtime. `castle program build` regenerates `public/`; `castle apply` renders the
|
||||||
|
route and reloads the gateway.
|
||||||
|
|
||||||
|
## Adopting an existing Hugo site
|
||||||
|
|
||||||
|
No stack needed — adopt the repo and declare how it builds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
castle program add /path/to/site --name my-site
|
||||||
|
```
|
||||||
|
|
||||||
|
Then set `build.commands` (as above) and add a `manager: caddy` deployment. This is
|
||||||
|
how a site with a bespoke build (submodule theme + Tailwind) is wired without the
|
||||||
|
scaffold. See @docs/registry.md.
|
||||||
Reference in New Issue
Block a user