Skip to content

Site templates — repositories & catalogue

A site template is a complete website repository that has not been filled with content yet: theme, build configuration and pipeline are in place, the pages folder holds a placeholder start page. Copying one produces a repository that builds on its first commit.

This document is the contract between the templates, the catalogue that lists them, and any tool offering a choice among them. For the routes into a new repository, see Starting a new website.

The catalogue

A single JSON file lists the templates that are considered supported:

  • Canonical URL: https://cratly.io/templates.json
  • Served with Access-Control-Allow-Origin: *, so a browser application may read it directly.
  • Hand-maintained in this repository (docs/public/templates.json). Adding a template is one entry; nothing generates it.
json
{
	"version": 0,
	"templates": [
		{
			"id": "scavold-plain",
			"name": "Scavold — Plain",
			"description": {
				"en": "Minimal starting point: header, footer, one page …",
				"de": "Minimaler Anfang: Kopf, Fuß, eine Seite …"
			},
			"instance": "https://gitlab.com",
			"project_path": "cratly/templates/scavold-plain",
			"ref": "main",
			"adapter": { "name": "scavold", "range": "^0.2.0-rc.3" },
			"exclude": ["README.md"],
			"tags": [ "monolingual", "starter" ],
			"license": "MIT"
		}
	]
}
FieldRequiredMeaning
versionyesFormat version of the catalogue itself. 0 while the format may still change.
idyesStable, lowercase identifier, <adapter>-<variant>, identical to the project's path segment. Never reused for a different template.
nameyesWhat a human picking a template reads. Not localised — a template's name is its name.
descriptionyesOne sentence on what the template is. Either a string or an object of locale → text, see Languages.
instanceyesOrigin of the GitLab instance hosting the template.
project_pathyesgroup/subgroup/project — the template's project on that instance. Must be publicly readable.
refyesBranch or tag to copy from.
adapteryesWhich theme framework the template builds with, and the version range it declares. Lets a consumer say what it is getting into.
excludenoPaths not to copy — see What stays behind.
tagsnoFree-form markers (monolingual, multilingual, starter, …).
licensenoSPDX identifier of the template's own licence.
previewnoAbsolute URL of a screenshot. Omitted while none exists.

What a template repository must provide

One repository per template, all of them flat siblings in the same group (cratly/templates/) — not folders in a shared repository, and not sorted into a subgroup per adapter. Both alternatives break GitLab's custom project templates feature: it offers the projects of one group, and "projects in nested subgroups are not included in the template list". Separate repositories also let each template carry its own lockfile, pipeline and framework version, which they will need as soon as they age at different speeds.

Since the grouping cannot be expressed in the hierarchy, it is expressed in the name: <adapter>-<variant>, e.g. scavold-plain. In GitLab's template picker the project name is all a human sees — the catalogue's adapter field is not there — so a template whose name hides which framework it builds with is unusable at exactly the moment it is picked. A second adapter would also want a "plain" of its own.

A repository listed in the catalogue promises:

  • a valid .cratly.config.yaml at the root;
  • the folders it declares, media/ materialised with a .gitkeep so the editor's media panel does not report it missing;
  • a committed, current .cratly/sections.json, with CI failing when it drifts from what the build produces;
  • a pipeline that publishes the built site as a job artifact, and a deploy job that is switched off until secrets are provided;
  • a README covering the steps that follow the copy, named in exclude so it does not land in the new website;
  • public read access, so the catalogue entry can actually be resolved.

Languages

description may be a plain string or an object keyed by locale:

json
"description": { "en": "Minimal starting point …", "de": "Minimaler Anfang …" }

A reader picks the exact locale, then its base language (de-ATde), then en, then a plain string — which declares no language and is therefore never treated as English. An entry with no usable description in any language is dropped rather than shown blank.

What stays behind

A template repository documents itself: its README explains how to set the site up, how to create another template, how to register the group with GitLab. That is the right content for someone choosing a template and the wrong content for the website that comes out of it — so exclude names the paths a copy leaves behind.

json
"exclude": [ "README.md", "docs/" ]

Rules are literal paths. The one shorthand is a trailing slash: docs/ covers that folder and everything below it. Deliberately not glob patterns — a template author naming a file should be able to predict the outcome without knowing which glob dialect the copying tool implements.

The exclusion applies while the file list is being read, so what a tool shows before committing is exactly what it commits. GitLab's project-template route knows nothing of this field and copies the repository as it is; excluding files there means deleting them after the fact.

Adding a template

  1. Create the repository next to the existing templates, named <adapter>-<variant>, and make it satisfy the list above.
  2. Add one entry to docs/public/templates.json in the site repository.
  3. If the group is registered for GitLab's custom project templates, the new project is offered there automatically — nothing else to do.

Consuming the catalogue from a browser

The editor offers these templates when setting up a repository without commits. The mechanics are free of the obstacles one would expect — recorded here so nobody re-derives them:

  • The editor's CSP does not block it. connect-src is 'self' https:, deliberately open because the GitLab instance is chosen by whoever uses the editor.
  • The catalogue is a simple cross-origin GET of a public JSON file — no preflight, which is exactly what the static host serves.
  • The template's files are read through the GitLab API, the same path the editor already uses for every file tree and every blob. That API answers with Access-Control-Allow-Origin: * and allows Authorization on the preflight, so a template on gitlab.com can be read even while the target repository lives on a self-hosted instance. No token is needed for a public project.
  • Writing is one commit with an action per file, binaries as base64 — the same mechanism the empty-repository bootstrap already uses.

Two constraints are real, and neither is transport:

  • A template brings a pipeline. Copying one means committing someone else's .gitlab-ci.yml into the target repository. That is why the catalogue is curated and why the editor lists every file before writing it — never a free-text repository URL. The whole template becomes one commit, so a repository is either set up or untouched.
  • The editor must stay framework-agnostic. It may treat a template as data: a name, a description, a list of files to copy. The moment it knows what VitePress is, the boundary that keeps a second adapter possible is gone. See ADR 0004.