Add repository-driven site theme

This commit is contained in:
2026-07-23 20:14:42 -07:00
parent b918b70fe4
commit dcc03c78ec
18 changed files with 845 additions and 16 deletions
+502
View File
@@ -0,0 +1,502 @@
:root {
color-scheme: dark;
--bg: #111314;
--surface: #191c1d;
--surface-raised: #202425;
--text: #f1eee6;
--muted: #aaa99f;
--line: #34393a;
--accent: #d9a75c;
--accent-strong: #f2c57d;
--focus: #8fd3ff;
--max-width: 72rem;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-synthesis: none;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background:
radial-gradient(circle at 10% 0%, rgb(217 167 92 / 10%), transparent 28rem),
var(--bg);
color: var(--text);
font-size: 1rem;
line-height: 1.65;
}
a {
color: var(--accent-strong);
text-underline-offset: 0.2em;
text-decoration-thickness: 0.08em;
}
a:hover {
color: #ffe1ad;
}
:focus-visible {
outline: 0.2rem solid var(--focus);
outline-offset: 0.2rem;
}
.shell {
width: min(calc(100% - 2rem), var(--max-width));
margin-inline: auto;
}
.skip-link {
position: fixed;
z-index: 10;
top: 0.75rem;
left: 0.75rem;
padding: 0.65rem 0.9rem;
transform: translateY(-160%);
border-radius: 0.35rem;
background: var(--text);
color: var(--bg);
}
.skip-link:focus {
transform: translateY(0);
}
.site-header {
border-bottom: 1px solid var(--line);
background: rgb(17 19 20 / 88%);
}
.header-inner {
min-height: 4.75rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
}
.brand {
display: inline-flex;
align-items: center;
gap: 0.7rem;
color: var(--text);
font-weight: 750;
letter-spacing: -0.02em;
text-decoration: none;
}
.brand-mark {
width: 2rem;
height: 2rem;
display: grid;
place-items: center;
border: 1px solid var(--accent);
border-radius: 50%;
color: var(--accent-strong);
font-family: Georgia, "Times New Roman", serif;
font-style: italic;
}
.site-nav {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
}
.site-nav a {
padding: 0.45rem 0.7rem;
border-radius: 0.35rem;
color: var(--muted);
font-size: 0.9rem;
font-weight: 650;
text-decoration: none;
}
.site-nav a:hover,
.site-nav a[aria-current="page"] {
background: var(--surface-raised);
color: var(--text);
}
main.shell {
min-height: 68vh;
padding-block: clamp(3.5rem, 8vw, 7rem);
}
h1,
h2,
h3 {
margin-top: 0;
font-family: Georgia, "Times New Roman", serif;
font-weight: 500;
line-height: 1.08;
letter-spacing: -0.035em;
}
h1 {
max-width: 13ch;
margin-bottom: 1.25rem;
font-size: clamp(3rem, 9vw, 6.5rem);
}
h2 {
font-size: clamp(2rem, 5vw, 3.25rem);
}
h3 {
font-size: 1.5rem;
}
.eyebrow,
.card-kind,
.content-meta,
.footer-meta {
color: var(--accent);
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.hero {
max-width: 62rem;
}
.hero h1 {
max-width: 11ch;
}
.hero-summary,
.lede {
max-width: 48rem;
color: #d2d0c8;
font-size: clamp(1.2rem, 2.5vw, 1.55rem);
line-height: 1.5;
}
.hero-copy {
margin-top: 3rem;
}
.section-block,
.listing {
margin-top: clamp(4rem, 9vw, 7rem);
padding-top: 2rem;
border-top: 1px solid var(--line);
}
.section-heading {
margin-bottom: 2rem;
}
.section-heading h2 {
margin-bottom: 0;
}
.card-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.card {
min-height: 18rem;
display: flex;
flex-direction: column;
padding: clamp(1.35rem, 3vw, 2rem);
border: 1px solid var(--line);
border-radius: 0.75rem;
background: linear-gradient(145deg, var(--surface-raised), var(--surface));
}
.card h2,
.card h3 {
margin-bottom: 0.8rem;
}
.card h2 a,
.card h3 a,
.listing-item h2 a {
color: var(--text);
text-decoration: none;
}
.card p:not(.card-kind) {
color: var(--muted);
}
.card .text-link {
margin-top: auto;
padding-top: 1.5rem;
}
.text-link {
width: fit-content;
font-weight: 750;
text-decoration: none;
}
.page-intro,
.content-header {
margin-bottom: clamp(3rem, 8vw, 5.5rem);
}
.page-intro h1,
.content-header h1 {
font-size: clamp(3rem, 8vw, 5.5rem);
}
.listing {
display: grid;
}
.listing-item {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: end;
gap: 2rem;
padding-block: 2rem;
border-bottom: 1px solid var(--line);
}
.listing-item h2 {
margin-bottom: 0.6rem;
font-size: clamp(1.75rem, 4vw, 2.5rem);
}
.listing-item p {
max-width: 46rem;
margin-bottom: 0;
color: var(--muted);
}
.content-page {
max-width: 56rem;
}
.prose {
max-width: 46rem;
color: #d5d3cb;
font-size: 1.05rem;
}
.prose > * + * {
margin-top: 1.35em;
}
.prose h2,
.prose h3 {
margin-top: 2em;
color: var(--text);
}
.prose h2 {
font-size: 2rem;
}
.prose h3 {
font-size: 1.4rem;
}
.prose code {
padding: 0.12em 0.32em;
border: 1px solid var(--line);
border-radius: 0.25rem;
background: var(--surface);
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
font-size: 0.9em;
}
.prose pre {
overflow-x: auto;
padding: 1.25rem;
border: 1px solid var(--line);
border-radius: 0.5rem;
background: #0b0d0e;
}
.prose pre code {
padding: 0;
border: 0;
background: none;
}
.prose blockquote {
margin-inline: 0;
padding-left: 1.25rem;
border-left: 0.2rem solid var(--accent);
color: var(--muted);
}
.project-facts {
display: flex;
flex-wrap: wrap;
gap: 1rem;
margin-top: 2rem;
}
.project-facts div {
min-width: 9rem;
padding: 0.8rem 1rem;
border: 1px solid var(--line);
border-radius: 0.5rem;
background: var(--surface);
}
.project-facts dt {
color: var(--muted);
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.project-facts dd {
margin: 0.2rem 0 0;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 1.5rem 0 0;
padding: 0;
list-style: none;
}
.tag-list li {
padding: 0.32rem 0.6rem;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--muted);
font-size: 0.82rem;
}
.content-tags {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--line);
}
.button-link {
display: inline-block;
margin-top: 1.5rem;
padding: 0.75rem 1rem;
border: 1px solid var(--accent);
border-radius: 0.4rem;
text-decoration: none;
}
.error-page {
max-width: 48rem;
}
.error-page p:not(.eyebrow) {
color: var(--muted);
font-size: 1.2rem;
}
.empty-state {
color: var(--muted);
}
.site-footer {
padding-block: 2.5rem;
border-top: 1px solid var(--line);
color: var(--muted);
}
.footer-inner {
display: flex;
justify-content: space-between;
gap: 2rem;
}
.footer-inner p {
margin: 0;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media (max-width: 44rem) {
.header-inner,
.footer-inner {
align-items: flex-start;
flex-direction: column;
}
.header-inner {
padding-block: 1rem;
}
.card-grid {
grid-template-columns: 1fr;
}
.listing-item {
grid-template-columns: 1fr;
align-items: start;
gap: 1rem;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
@media (prefers-color-scheme: light) {
:root {
color-scheme: light;
--bg: #f4f0e7;
--surface: #ebe5da;
--surface-raised: #fffaf0;
--text: #202323;
--muted: #5f625f;
--line: #cbc4b7;
--accent: #87591b;
--accent-strong: #704713;
--focus: #075b9b;
}
body {
background:
radial-gradient(circle at 10% 0%, rgb(135 89 27 / 11%), transparent 28rem),
var(--bg);
}
a:hover {
color: #4e2d04;
}
.site-header {
background: rgb(244 240 231 / 90%);
}
.hero-summary,
.lede,
.prose {
color: #3d413f;
}
.prose pre {
background: #242728;
color: #f7f3eb;
}
}
+6
View File
@@ -0,0 +1,6 @@
<section class="error-page">
<p class="eyebrow">Error 404</p>
<h1>That route does not exist.</h1>
<p>The page may have moved, or the address may be incomplete.</p>
<a class="button-link" href="/">Return home</a>
</section>
+25
View File
@@ -0,0 +1,25 @@
<article class="content-page">
<header class="content-header">
<p class="eyebrow">Article</p>
<h1>{{ page.title }}</h1>
{% if page.summary %}
<p class="lede">{{ page.summary }}</p>
{% endif %}
{% if page.published %}
<p class="content-meta">Published {{ page.published }}</p>
{% endif %}
</header>
<div class="prose">
{{ page.renderedContent | safe_content }}
</div>
{% if page.tags.size > 0 %}
<footer class="content-tags">
<p class="visually-hidden">Topics</p>
<ul class="tag-list">
{% for tag in page.tags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
</footer>
{% endif %}
</article>
+12
View File
@@ -0,0 +1,12 @@
<article class="content-page">
<header class="content-header">
<p class="eyebrow">Page</p>
<h1>{{ page.title }}</h1>
{% if page.summary %}
<p class="lede">{{ page.summary }}</p>
{% endif %}
</header>
<div class="prose">
{{ page.renderedContent | safe_content }}
</div>
</article>
+33
View File
@@ -0,0 +1,33 @@
<article class="content-page project-page">
<header class="content-header">
<p class="eyebrow">Project</p>
<h1>{{ page.title }}</h1>
{% if page.summary %}
<p class="lede">{{ page.summary }}</p>
{% endif %}
<dl class="project-facts">
{% if page.metadata.version %}
<div><dt>Version</dt><dd>{{ page.metadata.version }}</dd></div>
{% endif %}
{% if page.metadata.license %}
<div><dt>License</dt><dd>{{ page.metadata.license }}</dd></div>
{% endif %}
{% if page.metadata.stack.size > 0 %}
<div><dt>Stack</dt><dd>{{ page.metadata.stack | join: ", " }}</dd></div>
{% endif %}
</dl>
</header>
<div class="prose">
{{ page.renderedContent | safe_content }}
</div>
{% if page.tags.size > 0 %}
<footer class="content-tags">
<p class="visually-hidden">Topics</p>
<ul class="tag-list">
{% for tag in page.tags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
</footer>
{% endif %}
</article>
+29
View File
@@ -0,0 +1,29 @@
<section class="hero">
<p class="eyebrow">Configuration-first publishing</p>
<h1>{{ page.title }}</h1>
{% if page.summary %}
<p class="hero-summary">{{ page.summary }}</p>
{% endif %}
<div class="prose hero-copy">
{{ page.renderedContent | safe_content }}
</div>
</section>
{% if collection.items.size > 0 %}
<section class="section-block" aria-labelledby="latest-heading">
<div class="section-heading">
<p class="eyebrow">From the repository</p>
<h2 id="latest-heading">Latest work</h2>
</div>
<div class="card-grid">
{% for item in collection.items %}
<article class="card">
<p class="card-kind">{{ item.contentModel }}</p>
<h3><a href="{{ item.route }}">{{ item.title }}</a></h3>
<p>{{ item.summary }}</p>
<a class="text-link" href="{{ item.route }}">Read more <span aria-hidden="true">→</span></a>
</article>
{% endfor %}
</div>
</section>
{% endif %}
+44
View File
@@ -0,0 +1,44 @@
<!doctype html>
<html lang="{{ site.language }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{ page.summary }}">
<meta name="color-scheme" content="dark light">
{% if page.type == "home" %}
<title>{{ site.title }}</title>
{% else %}
<title>{{ page.title }} · {{ site.title }}</title>
{% endif %}
{% for stylesheet in theme.styles %}
<link rel="stylesheet" href="{{ stylesheet }}">
{% endfor %}
</head>
<body>
<a class="skip-link" href="#main-content">Skip to content</a>
<header class="site-header">
<div class="shell header-inner">
<a class="brand" href="/" aria-label="{{ site.title }} home">
<span class="brand-mark" aria-hidden="true">L</span>
<span>{{ site.title }}</span>
</a>
<nav class="site-nav" aria-label="Primary navigation">
{% for item in navigation %}
<a href="{{ item.route }}"{% if item.route == page.route %} aria-current="page"{% endif %}>{{ item.label }}</a>
{% endfor %}
</nav>
</div>
</header>
<main id="main-content" class="shell">
{{ page.body | safe_page_body }}
</main>
<footer class="site-footer">
<div class="shell footer-inner">
<p><strong>{{ site.title }}</strong> turns Git-owned configuration and content into a dependable website.</p>
<p class="footer-meta">Built from immutable sources.</p>
</div>
</footer>
</body>
</html>
+22
View File
@@ -0,0 +1,22 @@
<section class="page-intro">
<p class="eyebrow">Writing</p>
<h1>{{ page.title }}</h1>
{% if page.summary %}
<p class="lede">{{ page.summary }}</p>
{% endif %}
</section>
<section class="listing" aria-label="{{ page.title }}">
{% for item in collection.items %}
<article class="listing-item">
<div>
<p class="card-kind">{{ item.contentModel }}</p>
<h2><a href="{{ item.route }}">{{ item.title }}</a></h2>
<p>{{ item.summary }}</p>
</div>
<a class="text-link" href="{{ item.route }}" aria-label="Read {{ item.title }}">Read <span aria-hidden="true">→</span></a>
</article>
{% else %}
<p class="empty-state">No articles have been published yet.</p>
{% endfor %}
</section>
+27
View File
@@ -0,0 +1,27 @@
<section class="page-intro">
<p class="eyebrow">Projects</p>
<h1>{{ page.title }}</h1>
{% if page.summary %}
<p class="lede">{{ page.summary }}</p>
{% endif %}
</section>
<section class="card-grid" aria-label="{{ page.title }}">
{% for item in collection.items %}
<article class="card project-card">
<p class="card-kind">{{ item.contentModel }}</p>
<h2><a href="{{ item.route }}">{{ item.title }}</a></h2>
<p>{{ item.summary }}</p>
{% if item.tags.size > 0 %}
<ul class="tag-list" aria-label="Technologies and topics">
{% for tag in item.tags %}
<li>{{ tag }}</li>
{% endfor %}
</ul>
{% endif %}
<a class="text-link" href="{{ item.route }}">View project <span aria-hidden="true">→</span></a>
</article>
{% else %}
<p class="empty-state">No projects have been published yet.</p>
{% endfor %}
</section>
+28
View File
@@ -0,0 +1,28 @@
protocol: labyricorn-theme/v1
id: labyricorn-default
version: 1.0.0
engine: liquid
templates:
layout: templates/layout.liquid
home: templates/home.liquid
notFound: templates/404.liquid
sections:
articles: templates/sections/articles.liquid
projects: templates/sections/projects.liquid
content:
page: templates/content/page.liquid
article: templates/content/article.liquid
project: templates/content/project.liquid
assets:
styles:
- assets/styles/theme.css
scripts: []
files: []
fonts: []
security:
allowScripts: false
allowExternalAssets: false
allowedExternalOrigins: []
+9 -2
View File
@@ -7,8 +7,10 @@ publishing path before more content or integrations are added.
## Source of truth
This repository is authoritative for the site content it contains. Labyricorn
reads `.labyricorn/site.yml`, discovers the configured content, builds the
website, and publishes the completed output to the local canonical site.
reads the repository participation policy in `.labyricorn/site.yml`, resolves
the canonical site definition in `site.yml`, validates the `.theme/` package,
discovers the configured content, builds the website, and publishes the
completed output to the local canonical site.
Generated website files do not belong in this repository.
@@ -16,6 +18,10 @@ Generated website files do not belong in this repository.
```text
.labyricorn/site.yml Repository participation and publishing policy
site.yml Canonical site definition and theme selection
navigation.yml Public navigation and section-template selection
projects.yml Allowlisted project-page declarations
.theme/ Strict Liquid templates and declared static assets
pages/ Standalone website pages
articles/ Article source documents
projects/ Project source documents
@@ -30,6 +36,7 @@ The first deployment includes:
- one homepage;
- one introductory article;
- one project page;
- a repository-owned, script-free Liquid theme;
- local publication through Labyricorn.
It does not yet include:
+1 -2
View File
@@ -2,6 +2,7 @@
model: article
title: Welcome to Labyricorn
slug: welcome
route: /articles/welcome/
date: 2026-07-23
status: published
summary: Why the first deployment is intentionally small and locally hosted.
@@ -11,8 +12,6 @@ tags:
- git
---
# Welcome to Labyricorn
Labyricorn is a configuration-first, Git-driven site builder. Repositories
provide content and configuration; the builder turns those sources into a
coherent website; the generated local site is the canonical runtime copy.
+13 -8
View File
@@ -17,15 +17,19 @@ Cloudflare Pages, Docker, or a public Control Plane.
2. Add that Gitea repository as the remote for this local repository.
3. Push the default branch to Gitea.
4. Connect the repository in the Labyricorn Control Plane.
5. Synchronize it and confirm `.labyricorn/site.yml` is valid.
6. Confirm that the `page`, `article`, and `project` models are installed and
5. Synchronize it and confirm `.labyricorn/site.yml` and the root `site.yml`
are valid.
6. Validate `.theme/theme.yml`, every referenced Liquid template, and the
declared stylesheet before creating a release.
7. Confirm that the `page`, `article`, and `project` models are installed and
enabled in Labyricorn.
7. Confirm discovery finds the three Markdown documents in this repository.
8. Preview the generated routes and resolve any validation errors.
9. Build the site.
10. Publish the completed build to the local canonical site.
11. Verify the homepage, article, and project page from a browser.
12. Record the deployed Git revision and active local release.
8. Confirm discovery finds the three Markdown documents in this repository.
9. Preview the generated routes and resolve any validation errors.
10. Build the site.
11. Publish the completed build to the local canonical site.
12. Verify the homepage, article, project index, and project page from a
browser.
13. Record the deployed Git revision and active local release.
## Required proof
@@ -33,6 +37,7 @@ The initial deployment is successful only when all of the following are true:
- the repository revision shown by Labyricorn matches Gitea;
- configuration validation uses the committed `site.yml`;
- theme validation uses the committed `.theme/theme.yml` and declared files;
- all three source documents are discovered without filename heuristics;
- generated routes come from configured model behavior;
- the public pages are served from the activated local release;
+22
View File
@@ -0,0 +1,22 @@
navigation:
- id: home
label: Home
route: /
iconName: Home
contentModel: page
- id: articles
label: Articles
route: /articles/
iconName: FileText
contentModel: article
presentation:
sectionTemplate: articles
- id: projects
label: Projects
route: /projects/
iconName: Boxes
contentModel: project
presentation:
sectionTemplate: projects
-2
View File
@@ -7,8 +7,6 @@ status: published
summary: A configuration-first, Git-driven home for projects, articles, and documentation.
---
# Build once. Understand every part.
Labyricorn brings project information, articles, and documentation together
without replacing Git as the source of truth.
+19
View File
@@ -0,0 +1,19 @@
projects:
- id: labyricorn
source: site-definition
route: /projects/labyricorn/
title: Labyricorn
summary: A configuration-first, Git-driven static-site builder and control plane.
license: Proprietary
tags:
- labyricorn
- git
- static-site
stack:
- TypeScript
- React
- Liquid
- Nginx
presentation:
detailTemplate: project
publishedFiles: []
+1 -2
View File
@@ -2,6 +2,7 @@
model: project
title: Labyricorn
slug: labyricorn
route: /projects/labyricorn/
status: published
summary: A configuration-first, Git-driven site builder and its administrative control plane.
repository_url: https://git.labyricorn.com
@@ -11,8 +12,6 @@ tags:
- static-site
---
# Labyricorn
Labyricorn builds and hosts locally generated sites from Git repositories.
Content models, routes, navigation, styles, themes, and integrations derive
their meaning from configuration rather than hardcoded content-type names.
+52
View File
@@ -0,0 +1,52 @@
protocol: labyricorn-site/v1
site:
id: labyricorn
title: Labyricorn
baseUrl: https://labyricorn.com
language: en-US
navigationFile: ./navigation.yml
projectsFile: ./projects.yml
theme:
source: site-definition
path: .theme
markdown:
dialect: labyricorn-commonmark-v1
rawHtmlPolicy: disabled
rawHtmlEnabled: false
extensions:
tables: true
taskLists: true
footnotes: true
definitionLists: true
headingAnchors: true
fencedCode: true
syntaxHighlighting: true
callouts: true
youtube: false
wikipediaLinks: true
hosting:
engine: nginx
production:
enabled: true
hostname: labyricorn.com
listen: 80
staging:
enabled: true
hostname: preview.labyricorn.com
listen: 8080
releases:
retainCount: 10
buildPolicy:
staging:
enabled: true
requireApproval: true
localActivation:
automatic: false
push:
automatic: false