asw-v01: archive deferred content (packs, site, lab, legacy examples)

- 2.1: packs/ -> archive/packs/
- 2.2: site/ -> archive/site/
- 2.3: src/lab/ -> archive/lab/
- 2.4: examples/ -> archive/examples-legacy/ (SSI-based)
This commit is contained in:
exe.dev user 2026-06-07 10:39:21 +02:00
parent 416fe2f180
commit e47a9f4401
173 changed files with 11 additions and 5 deletions

View file

@ -0,0 +1,30 @@
# ASW Apache Autoindex — config snippet
#
# Enables ASW-styled directory listings.
# Apache's HeaderName/ReadmeName inject custom HTML before/after the file table.
#
# Paste this into your VirtualHost block or a <Directory> block.
# Adjust paths to match your setup.
# ── Autoindex settings ───────────────────────────────────────────────────────
Options +Indexes
IndexOptions FancyIndexing HTMLTable SuppressRules SuppressColumnSorting
IndexOptions +NameWidth=* +DescriptionWidth=* +Charset=UTF-8
# Custom header/footer HTML injected around the file table
HeaderName /autoindex/autoindex-header.html
ReadmeName /autoindex/autoindex-footer.html
# Suppress the default Apache header (H1 "Index of /path")
# — our header file provides the title
IndexOptions +SuppressHTMLPreamble
# Serve the custom header/footer files
Alias /autoindex/ /home/exedev/projects/agentic-semantic-web/packs/apache/
<Directory /home/exedev/projects/agentic-semantic-web/packs/apache/>
Options None
AllowOverride None
Require all granted
</Directory>

View file

@ -0,0 +1,27 @@
# ASW Apache Error Pages — config snippet
#
# Paste this into your VirtualHost block or httpd.conf.
# Adjust the DocumentRoot alias path to where your errors/ directory lives.
#
# Assumes asw.css is served at /asw/asw.css
# (serve the agentic-semantic-web repo root as /asw/ — see README.md)
# ── Error pages ──────────────────────────────────────────────────────────────
ErrorDocument 400 /errors/400.html
ErrorDocument 401 /errors/401.html
ErrorDocument 403 /errors/403.html
ErrorDocument 404 /errors/404.html
ErrorDocument 500 /errors/500.html
ErrorDocument 502 /errors/502.html
ErrorDocument 503 /errors/503.html
# Serve the ASW errors/ directory.
# Adjust the path to where your agentic-semantic-web checkout lives.
Alias /errors/ /home/exedev/projects/agentic-semantic-web/errors/
<Directory /home/exedev/projects/agentic-semantic-web/errors/>
Options None
AllowOverride None
Require all granted
</Directory>

View file

@ -0,0 +1,3 @@
</main>
</body>
</html>

View file

@ -0,0 +1,49 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/asw/asw.css">
<title>Directory Listing</title>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; padding: 2rem; max-width: 80ch; margin: 0 auto; width: 100%; }
/* Apache autoindex outputs a <table> — style it to match ASW */
table {
width: 100%;
border-collapse: collapse;
font-family: var(--asw-font-mono);
font-size: var(--asw-text-sm);
}
table th {
text-align: left;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--asw-border);
color: var(--asw-text-muted);
font-weight: 500;
font-size: var(--asw-text-xs);
text-transform: uppercase;
letter-spacing: 0.06em;
}
table td {
padding: 0.4rem 0.75rem;
border-bottom: 1px solid var(--asw-border-subtle);
color: var(--asw-text-secondary);
}
table tr:hover td { background: var(--asw-bg-hover); }
table td:first-child a {
color: var(--asw-accent);
text-decoration: none;
}
table td:first-child a:hover { text-decoration: underline; }
/* parent directory link */
table td img { display: none; } /* hide Apache's default icons */
</style>
</head>
<body>
<nav>
<ul><li><a href="/"><strong>Home</strong></a></li></ul>
</nav>
<main>
<h1>Directory listing</h1>

View file

@ -0,0 +1,160 @@
{{/*
ASW Caddy Browse Template
Go HTML template for Caddy's file_server browse directive.
Usage in Caddyfile:
file_server browse {
browse {
template_file /path/to/agentic-semantic-web/packs/caddy/browse.html
}
}
Template variables:
.Name — directory name (last segment of path)
.Path — current URL path, e.g. "/files/"
.Files — []FileInfo entries, each with:
.Name, .URL, .Size, .ModTime, .IsDir, .IsSymlink
*/}}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/asw/asw.css">
<title>{{.Name}} — Directory listing</title>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; padding: 2rem; max-width: 90ch; margin: 0 auto; width: 100%; }
.listing {
width: 100%;
border-collapse: collapse;
font-family: var(--asw-font-mono);
font-size: 0.875rem;
}
.listing th {
text-align: left;
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--asw-border);
color: var(--asw-text-muted);
font-weight: 500;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.listing td {
padding: 0.4rem 0.75rem;
border-bottom: 1px solid color-mix(in srgb, var(--asw-border) 40%, transparent);
color: var(--asw-text-secondary);
vertical-align: middle;
}
.listing tr:hover td { background: color-mix(in srgb, var(--asw-bg-elevated) 60%, transparent); }
.listing .col-name a {
color: var(--asw-accent);
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5em;
}
.listing .col-name a:hover { text-decoration: underline; }
.listing .col-size { text-align: right; color: var(--asw-text-muted); }
.listing .col-modified { white-space: nowrap; color: var(--asw-text-muted); }
.icon { font-size: 0.85em; opacity: 0.7; }
.dir-icon::before { content: "📁"; }
.file-icon::before { content: "📄"; }
.parent-icon::before { content: "⬆️"; font-size: 0.8em; }
.path-breadcrumb {
font-family: var(--asw-font-mono);
font-size: 0.875rem;
color: var(--asw-text-muted);
margin-bottom: 1rem;
}
.path-breadcrumb a { color: var(--asw-accent); text-decoration: none; }
.path-breadcrumb a:hover { text-decoration: underline; }
.path-breadcrumb span { color: var(--asw-text-muted); }
</style>
</head>
<body>
<nav>
<ul><li><a href="/"><strong>Home</strong></a></li></ul>
<ul><li><span data-text="dim">browse</span></li></ul>
</nav>
<main>
<h1>{{.Name}}</h1>
<p class="path-breadcrumb">
{{/* Breadcrumb — split path and link each segment */}}
<a href="/">/</a>
{{- $parts := splitList "/" (trimSuffix "/" .Path) -}}
{{- $acc := "" -}}
{{- range $i, $part := $parts -}}
{{- if $part -}}
{{- $acc = printf "%s/%s" $acc $part -}}
<span>/</span><a href="{{$acc}}/">{{$part}}</a>
{{- end -}}
{{- end -}}
</p>
<table class="listing">
<thead>
<tr>
<th class="col-name">Name</th>
<th class="col-size">Size</th>
<th class="col-modified">Modified</th>
</tr>
</thead>
<tbody>
{{/* Parent directory link */}}
{{if ne .Path "/"}}
<tr>
<td class="col-name">
<a href="../">
<span class="icon parent-icon"></span>
..
</a>
</td>
<td class="col-size"></td>
<td class="col-modified"></td>
</tr>
{{end}}
{{/* Directory entries first */}}
{{range .Files}}{{if .IsDir}}
<tr>
<td class="col-name">
<a href="{{.URL}}">
<span class="icon dir-icon"></span>
{{.Name}}/
</a>
</td>
<td class="col-size"></td>
<td class="col-modified">{{.ModTime.Format "2006-01-02 15:04"}}</td>
</tr>
{{end}}{{end}}
{{/* File entries */}}
{{range .Files}}{{if not .IsDir}}
<tr>
<td class="col-name">
<a href="{{.URL}}">
<span class="icon file-icon"></span>
{{.Name}}
</a>
</td>
<td class="col-size">{{humanizeBytes .Size}}</td>
<td class="col-modified">{{.ModTime.Format "2006-01-02 15:04"}}</td>
</tr>
{{end}}{{end}}
</tbody>
</table>
</main>
<footer>
<p>Styled with <a href="/asw/">Agentic Semantic Web</a></p>
</footer>
</body>
</html>

View file

@ -0,0 +1,51 @@
# ASW Caddy Directory Listing — Caddyfile snippet
#
# Caddy's `file_server browse` outputs its own HTML, but accepts a custom
# template file via `browse { template_file }`. The template is a Go HTML
# template with access to directory metadata and file entries.
#
# See: browse.html in this directory for the ASW-styled template.
# ── Option A: Custom template (recommended, Caddy v2.6+) ─────────────────────
#
# Full control over the listing HTML. Browse template receives:
# .Name — directory name (last path segment)
# .Path — current URL path (e.g. "/files/")
# .Files — []FileInfo, each with:
# .Name, .Size, .URL, .ModTime, .IsDir, .IsSymlink
handle /files/* {
root * /var/www/files
file_server browse {
index off
browse {
template_file /home/exedev/projects/agentic-semantic-web/packs/caddy/browse.html
}
}
}
# ── Option B: Default Caddy browse (quick, no custom template) ────────────────
#
# Uses Caddy's built-in file browser. Functional but unstyled.
# Upgrade to Option A when you want ASW aesthetics.
#
# handle /files/* {
# root * /var/www/files
# file_server browse
# }
# ── Example: expose the ASW repo itself for browsing ─────────────────────────
#
# example.com {
# handle /asw/browse/* {
# uri strip_prefix /asw/browse
# root * /home/exedev/projects/agentic-semantic-web
# file_server browse {
# browse {
# template_file /home/exedev/projects/agentic-semantic-web/packs/caddy/browse.html
# }
# }
# }
# }

View file

@ -0,0 +1,66 @@
# ASW Caddy Error Pages — Caddyfile snippet
#
# Paste this inside your site block (not at global scope).
# Adjust the root path to where your agentic-semantic-web repo lives.
#
# Assumes asw.css is served at /asw/asw.css
# (serve the agentic-semantic-web repo root as /asw/ — see README.md)
# ── Error pages ──────────────────────────────────────────────────────────────
#
# Caddy's handle_errors block intercepts any response with a matching
# status code before it reaches the client.
#
# Rewrite the path to the ASW static error page, then serve it from
# the agentic-semantic-web repo root.
handle_errors {
rewrite * /errors/{http.error.status_code}.html
file_server {
root /home/exedev/projects/agentic-semantic-web
}
}
# ── Serving asw.css ───────────────────────────────────────────────────────
#
# The error pages load asw.css from /asw/asw.css.
# Add this route to your site block so the CSS is reachable.
# (Skip if you already serve /asw/ from this repo elsewhere.)
handle /asw/* {
uri strip_prefix /asw
file_server {
root /home/exedev/projects/agentic-semantic-web
}
}
# ── Example site block ────────────────────────────────────────────────────────
#
# example.com {
# root * /var/www/html
# file_server
#
# handle /asw/* {
# uri strip_prefix /asw
# file_server {
# root /home/exedev/projects/agentic-semantic-web
# }
# }
#
# handle_errors {
# rewrite * /errors/{http.error.status_code}.html
# file_server {
# root /home/exedev/projects/agentic-semantic-web
# }
# }
# }
#
# Supported error codes (from errors/ directory):
# 400 — Bad Request
# 401 — Unauthorized
# 403 — Forbidden
# 404 — Not Found
# 500 — Internal Server Error
# 502 — Bad Gateway
# 503 — Service Unavailable

View file

@ -0,0 +1,82 @@
# ASW Flask/FastAPI Error Pack
Drop-in styled error responses for Flask and FastAPI applications.
## Usage
```python
# Copy asw_errors.py to your project directory, then:
from flask import Flask
from asw_errors import register_asw_errors
app = Flask(__name__)
register_asw_errors(app)
```
```python
from fastapi import FastAPI
from asw_errors import register_asw_errors
app = FastAPI()
register_asw_errors(app)
```
## What you get
ASW-styled HTML error pages for all HTTP error codes:
| Code | Status |
|------|--------|
| 400 | Bad Request |
| 401 | Unauthorized |
| 403 | Forbidden |
| 404 | Not Found |
| 405 | Method Not Allowed |
| 408 | Request Timeout |
| 409 | Conflict |
| 410 | Gone |
| 415 | Unsupported Media Type |
| 422 | Unprocessable Entity |
| 429 | Too Many Requests |
| 500 | Internal Server Error |
| 502 | Bad Gateway |
| 503 | Service Unavailable |
| 504 | Gateway Timeout |
For FastAPI, `RequestValidationError` (422) is also caught separately.
## Linking asw.css
By default, styles are inlined — no external dependencies needed. If your app already serves `asw.css`, link it instead:
```python
register_asw_errors(app, css_url="/static/asw.css")
```
The pages will then use your full theme including custom fonts.
## Custom app name
The nav bar shows your app's name. Override it:
```python
register_asw_errors(app, app_name="My Service")
```
Flask auto-detects from `app.name`; FastAPI from `app.title`. Both fall back to sensible defaults.
## Standalone HTML generation
The `error_html()` function is also exported if you need to generate pages directly:
```python
from asw_errors import error_html
html = error_html(404, path="/missing/route")
html = error_html(500, css_url="/static/asw.css", app_name="My API")
```
## No dependencies at module level
Flask and FastAPI are imported lazily inside `register_asw_errors()` — so this file can live in a project that uses either framework without both installed.

View file

@ -0,0 +1,293 @@
#!/usr/bin/env python3
"""
ASW Flask/FastAPI Error Pack styled error responses for Python web frameworks.
Drop this into your project directory and call register_asw_errors(app).
Usage (Flask):
from flask import Flask
from asw_errors import register_asw_errors
app = Flask(__name__)
register_asw_errors(app)
Usage (FastAPI):
from fastapi import FastAPI
from asw_errors import register_asw_errors
app = FastAPI()
register_asw_errors(app)
Optional: link to a hosted asw.css instead of inline styles
register_asw_errors(app, css_url="/static/asw.css")
register_asw_errors(app, css_url="https://cdn.example.com/asw.css")
"""
import html as _html
# ── Inline styles ─────────────────────────────────────────────────────────────
# Minimal ASW tokens — no external dependency. Matches asw.css dark theme.
_ASW_INLINE = """
:root {
--asw-bg: #0d1117;
--asw-bg-elevated: #161b22;
--asw-bg-overlay: #1c2128;
--asw-text: #e6edf3;
--asw-text-secondary: #8b949e;
--asw-text-muted: #484f58;
--asw-accent: #3fb950;
--asw-border: #30363d;
--asw-font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--asw-font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace;
}
*, *::before, *::after { box-sizing: border-box; }
html { font-size: 16px; }
body {
background: var(--asw-bg);
color: var(--asw-text);
font-family: var(--asw-font-body);
font-size: 1rem;
line-height: 1.6;
margin: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--asw-border);
background: var(--asw-bg-elevated);
}
nav a { color: var(--asw-text); text-decoration: none; font-weight: 600; }
nav a:hover { color: var(--asw-accent); }
.badge {
font-family: var(--asw-font-mono);
font-size: 0.75rem;
color: var(--asw-text-muted);
background: var(--asw-bg-overlay);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
border: 1px solid var(--asw-border);
}
main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.error-card {
max-width: 42ch;
text-align: center;
}
.error-code {
font-family: var(--asw-font-mono);
font-size: 6rem;
font-weight: 700;
line-height: 1;
color: var(--asw-text-muted);
letter-spacing: -0.05em;
margin: 0 0 0.5rem;
}
h1 {
margin: 0 0 1rem;
font-size: 1.5rem;
color: var(--asw-text);
}
p { margin: 0 0 0.75rem; color: var(--asw-text-secondary); }
a.button {
display: inline-block;
margin-top: 1.5rem;
padding: 0.5rem 1.25rem;
background: transparent;
color: var(--asw-accent);
border: 1px solid var(--asw-accent);
border-radius: 0.375rem;
text-decoration: none;
font-size: 0.875rem;
transition: background 0.15s;
}
a.button:hover { background: rgba(63, 185, 80, 0.1); }
code {
font-family: var(--asw-font-mono);
font-size: 0.85em;
background: var(--asw-bg-overlay);
padding: 0.1em 0.35em;
border-radius: 0.2rem;
color: var(--asw-text-secondary);
}
footer {
text-align: center;
padding: 1rem;
font-size: 0.75rem;
color: var(--asw-text-muted);
border-top: 1px solid var(--asw-border);
}
footer a { color: var(--asw-text-muted); }
"""
_ERROR_MESSAGES = {
400: ("Bad Request", "The server couldn't understand the request."),
401: ("Unauthorized", "Authentication required. Please sign in."),
403: ("Forbidden", "You don't have permission to access this resource."),
404: ("Not Found", "The page or resource you requested doesn't exist."),
405: ("Method Not Allowed", "That HTTP method isn't supported for this endpoint."),
408: ("Request Timeout", "The request took too long. Please try again."),
409: ("Conflict", "The request conflicts with the current state of the server."),
410: ("Gone", "This resource has been permanently removed."),
415: ("Unsupported Media Type", "The Content-Type you sent isn't accepted."),
422: ("Unprocessable Entity", "The request was well-formed but contains invalid data."),
429: ("Too Many Requests", "You've sent too many requests. Please slow down."),
500: ("Internal Server Error", "Something went wrong on the server."),
501: ("Not Implemented", "This feature hasn't been implemented yet."),
502: ("Bad Gateway", "The server received an invalid response from upstream."),
503: ("Service Unavailable", "The server is temporarily unavailable. Try again soon."),
504: ("Gateway Timeout", "The upstream server didn't respond in time."),
}
# Codes that receive an extra hint about the path in the response
_SHOW_PATH_CODES = {404, 403, 405}
def _make_style_tag(css_url=None):
if css_url:
return f'<link rel="stylesheet" href="{_html.escape(css_url)}">'
return f"<style>{_ASW_INLINE}</style>"
def error_html(code: int, path: str = "", css_url=None, app_name: str = "API") -> str:
"""
Generate an ASW-styled error page as an HTML string.
Args:
code: HTTP status code
path: Request path (shown for 403/404/405)
css_url: Optional URL to link instead of inlining styles
app_name: Shown in the nav bar (default: "API")
"""
title, message = _ERROR_MESSAGES.get(code, ("Error", "An error occurred."))
style_block = _make_style_tag(css_url)
path_hint = ""
if path and code in _SHOW_PATH_CODES:
escaped = _html.escape(path)
path_hint = f'<p>Path: <code>{escaped}</code></p>'
return f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{code} {title}</title>
{style_block}
</head>
<body>
<nav>
<a href="/">{_html.escape(app_name)}</a>
<span class="badge">{code}</span>
</nav>
<main>
<div class="error-card">
<p class="error-code">{code}</p>
<h1>{title}</h1>
<p>{message}</p>
{path_hint}
<a class="button" href="/"> Back</a>
</div>
</main>
<footer>Powered by <a href="https://github.com/trentuna/agentic-semantic-web">agentic-semantic-web</a></footer>
</body>
</html>"""
# ── Flask registration ────────────────────────────────────────────────────────
def _register_flask(app, css_url, app_name):
"""Register error handlers on a Flask application."""
from flask import request as flask_request
from flask import Response as FlaskResponse
codes = list(_ERROR_MESSAGES.keys())
def make_handler(code):
def handler(e):
path = getattr(flask_request, "path", "")
body = error_html(code, path=path, css_url=css_url, app_name=app_name)
return FlaskResponse(body, status=code, mimetype="text/html")
handler.__name__ = f"asw_error_{code}"
return handler
for code in codes:
app.register_error_handler(code, make_handler(code))
# ── FastAPI / Starlette registration ──────────────────────────────────────────
def _register_fastapi(app, css_url, app_name):
"""Register exception handlers on a FastAPI application."""
from starlette.requests import Request
from starlette.responses import HTMLResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
code = exc.status_code
path = request.url.path
body = error_html(code, path=path, css_url=css_url, app_name=app_name)
return HTMLResponse(content=body, status_code=code)
# Catch all Starlette HTTP exceptions (covers 4xx and 5xx)
app.add_exception_handler(StarletteHTTPException, http_exception_handler)
# Also catch FastAPI's RequestValidationError (unprocessable entity — 422)
try:
from fastapi.exceptions import RequestValidationError
async def validation_exception_handler(request: Request, exc: RequestValidationError):
path = request.url.path
body = error_html(422, path=path, css_url=css_url, app_name=app_name)
return HTMLResponse(content=body, status_code=422)
app.add_exception_handler(RequestValidationError, validation_exception_handler)
except ImportError:
pass # Pure Starlette without FastAPI — skip
# ── Public API ────────────────────────────────────────────────────────────────
def register_asw_errors(app, css_url=None, app_name=None):
"""
Register ASW-styled HTTP error handlers on a Flask or FastAPI application.
Args:
app: Flask or FastAPI application instance
css_url: Optional URL to link asw.css instead of inlining styles.
Use this when your app already serves asw.css:
register_asw_errors(app, css_url="/static/asw.css")
app_name: Label shown in the nav bar. Defaults to the app's name.
Raises:
TypeError: If the app type is not recognized as Flask or FastAPI.
"""
app_type = type(app).__name__
# Resolve app_name from app if not provided
if app_name is None:
if app_type == "Flask":
app_name = app.name or "Flask App"
elif app_type == "FastAPI":
app_name = app.title or "FastAPI"
else:
app_name = "API"
if app_type == "Flask":
_register_flask(app, css_url, app_name)
elif app_type == "FastAPI":
_register_fastapi(app, css_url, app_name)
else:
raise TypeError(
f"Unsupported app type: {app_type!r}. "
"Expected 'Flask' or 'FastAPI'."
)

View file

@ -0,0 +1,129 @@
# ASW-Hugo Pack
A Hugo theme that outputs pure [Agentic Semantic Web (ASW)](https://trentuna.com/asw/) semantic HTML with `data-*` attributes. Drop it into any Hugo project → content renders through ASW.
## What this is
This pack is an **ASW adapter for Hugo** — not a standalone theme. It makes Hugo output the semantic HTML + data-attribute vocabulary that ASW CSS understands.
```
Markdown vault (Git)
→ Hugo + ASW-Hugo pack (layouts emit ASW HTML)
→ static HTML with ASW CSS + data-* attributes
→ deploy anywhere
```
Long-term: trentuna will build a native ASW Site Builder. This pack is the bridge to alpha.
## Install
1. Copy or symlink `packs/hugo/` from the ASW repo into your project's `themes/` directory:
```bash
# From the agentic-semantic-web repo root:
cp -r packs/hugo/ /path/to/your-site/themes/asw-hugo/
# or symlink:
ln -s /path/to/agentic-semantic-web/packs/hugo/ /path/to/your-site/themes/asw-hugo
```
2. Add to your `hugo.toml`:
```toml
theme = "asw-hugo"
[taxonomies]
tag = "tags"
```
3. Copy ASW CSS into the theme's static directory:
```bash
cp /path/to/agentic-semantic-web/asw.css themes/asw-hugo/static/css/asw.css
```
> **Note:** `asw.css` is the `asw.css` file from the ASW repo root. B.A.'s deploy script handles this copy automatically for trentuna-web deployments.
4. **Self-host fonts** (recommended for production): The default `theme.css` imports Inter and
JetBrains Mono from Google Fonts. For self-hosted deployment, download the font files and
replace the `@import` in `static/css/theme.css` with local `@font-face` declarations.
See [google-webfonts-helper](https://gwfh.mranftl.com/) for font download.
## Usage
Write standard Markdown. Hugo renders it through ASW layouts automatically.
### Shortcodes
**Callout block:**
```
{{< callout note >}}
This is a note callout. Types: note, warning, tip, info
{{< /callout >}}
```
Renders as: `<aside data-callout="note">...</aside>`
**Wikilink:**
```
{{< wikilink "Page Name" "/path/to/page/" >}}
```
Renders as: `<a href="/path/to/page/" data-wikilink>Page Name</a>`
### ASW data-* vocabulary used
See `docs/agent-directive.md` in the ASW repo for the full vocabulary.
| Attribute | Where used | Meaning |
|-----------|-----------|---------|
| `data-callout="note\|warning\|tip\|info"` | `<aside>` | Callout block type |
| `data-wikilink` | `<a>` | Internal vault-style link |
| `data-role="tag-cloud"` | `<nav>` | Tag navigation |
| `data-layout="grid"` | `<section>` | Grid layout for list pages |
| `data-tag` | `<a>` | Tag label on links |
## Decap CMS
The `admin/` directory contains a minimal Decap CMS configuration for browser-based Markdown editing without SSH.
To enable:
1. Update `admin/config.yml`: set `repo` to your Forgejo repo path
2. Set `base_url` to your Forgejo instance URL
3. Deploy the `admin/` directory alongside your Hugo output
4. Access at `https://yoursite.com/admin/`
## Pack structure
```
packs/hugo/
├── README.md ← This file
├── layouts/
│ ├── _default/
│ │ ├── baseof.html ← Base template (html, head, body structure)
│ │ ├── single.html ← Single pages (articles)
│ │ └── list.html ← List + taxonomy pages (grid)
│ ├── partials/
│ │ ├── head.html ← <head>: meta, CSS links
│ │ └── tag-nav.html ← Tag cloud nav (use in list templates or sidebar)
│ └── shortcodes/
│ ├── callout.html ← {{< callout type >}} shortcode
│ └── wikilink.html ← {{< wikilink text href >}} shortcode
├── static/
│ └── css/
│ └── theme.css ← Trentuna design tokens (Inter, JetBrains Mono, dark palette)
├── archetypes/
│ └── default.md ← Default frontmatter template
└── admin/
└── config.yml ← Minimal Decap CMS config
```
## Alpha scope (April 2026)
- [x] Pack scaffolded: all layouts + partials + shortcodes
- [x] Tag navigation partial (`tag-nav.html`)
- [x] Wikilink support (`wikilink` shortcode + `data-wikilink` on list page links)
- [x] Callout blocks (`callout` shortcode → `<aside data-callout>`)
- [x] Trentuna theme tokens (`theme.css`: Inter, JetBrains Mono, dark palette)
- [x] Decap CMS config minimal but functional
- [ ] Deployed to trentuna-web infra — B.A. task: wire `asw.css` copy + nginx config
## Relationship to existing packs
This pack joins the ASW server/framework integration packs:
- `packs/nginx/` — nginx serving ASW content
- `packs/pandoc/` — pandoc converting Markdown → ASW HTML
- `packs/hugo/`**this pack** — Hugo site generation with ASW output

View file

@ -0,0 +1,82 @@
# Decap CMS configuration for ASW-Hugo pack
# Browser-based Markdown editing — no backend server required.
#
# Setup:
# 1. Update `repo` below to your Forgejo repo path (owner/repo)
# 2. Update `base_url` to your Forgejo instance URL
# 3. Register an OAuth app in Forgejo: Settings → Applications → OAuth2 Apps
# - Redirect URL: https://yoursite.com/admin/
# 4. Deploy admin/ alongside your Hugo output
# 5. Access at https://yoursite.com/admin/
backend:
name: gitea # Forgejo is Gitea-compatible
repo: trentuna/vault # ← CHANGE: your repo (owner/repo)
branch: main
base_url: https://trentuna.com # ← CHANGE: your Forgejo instance URL
auth_endpoint: /api/v1/oauth2/
media_folder: static/images
public_folder: /images
# Slug format for new posts
slug:
encoding: ascii
clean_accents: true
collections:
- name: posts
label: Posts
label_singular: Post
folder: content/posts
create: true
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
preview_path: "posts/{{slug}}/"
fields:
- label: Title
name: title
widget: string
- label: Date
name: date
widget: datetime
default: ""
date_format: "YYYY-MM-DD"
time_format: "HH:mm:ss"
format: "YYYY-MM-DDTHH:mm:ssZ"
- label: Description
name: description
widget: string
required: false
hint: Short summary shown in list views
- label: Tags
name: tags
widget: list
required: false
hint: Comma-separated tags
- label: Draft
name: draft
widget: boolean
default: true
- label: Body
name: body
widget: markdown
- name: pages
label: Pages
label_singular: Page
folder: content
create: true
filter:
field: _type
value: page
fields:
- label: Title
name: title
widget: string
- label: Description
name: description
widget: string
required: false
- label: Body
name: body
widget: markdown

View file

@ -0,0 +1,17 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
description: ""
tags: []
---
<!-- Write content here. Use shortcodes for ASW-specific elements:
{{< callout note >}}
This is a note.
{{< /callout >}}
{{< wikilink "Related Page" "/related-page/" >}}
-->

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="{{ .Site.Language.LanguageCode | default "en" }}" data-theme="dark">
<head>
{{- partial "head.html" . -}}
</head>
<body>
<nav>
<ul><li><a href="{{ .Site.BaseURL }}"><strong>{{ .Site.Title }}</strong></a></li></ul>
<ul data-nav-links>
{{- range .Site.Menus.main }}
<li><a href="{{ .URL }}"{{ if $.IsMenuCurrent "main" . }} aria-current="page"{{ end }}>{{ .Name }}</a></li>
{{- end }}
</ul>
<button data-theme-toggle aria-label="Toggle theme"></button>
</nav>
<main>
{{- block "main" . }}{{- end }}
</main>
<footer>
<small>
<a href="{{ "/" | relURL }}">{{ .Site.Title }}</a>
· {{ now.Year }}
{{- with .Site.Params.description }} · {{ . }}{{- end }}
</small>
</footer>
</body>
</html>

View file

@ -0,0 +1,39 @@
{{ define "main" }}
<section>
<header>
<h1>{{ .Title }}</h1>
{{- with .Description }}<p>{{ . }}</p>{{- end }}
</header>
{{- if .IsHome }}
{{- partial "tag-nav.html" . }}
{{- end }}
<section data-layout="grid">
{{- range .Pages }}
<article>
<header>
<h3>
<a href="{{ .RelPermalink }}" data-wikilink>{{ .Title }}</a>
</h3>
{{- if not .Date.IsZero }}
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "Jan 2, 2006" }}</time>
{{- end }}
</header>
{{- with .Summary }}
<p>{{ . }}</p>
{{- end }}
{{- with .GetTerms "tags" }}
<footer>
{{- range . }}
<a href="{{ .Permalink }}" data-tag="{{ .Name }}">{{ .Name }}</a>
{{- end }}
</footer>
{{- end }}
</article>
{{- end }}
</section>
</section>
{{ end }}

View file

@ -0,0 +1,36 @@
{{ define "main" }}
<article>
<header>
<h1>{{ .Title }}</h1>
{{- if not .Date.IsZero }}
<time datetime="{{ .Date.Format "2006-01-02" }}">{{ .Date.Format "January 2, 2006" }}</time>
{{- end }}
{{- with .Description }}
<p>{{ . }}</p>
{{- end }}
{{- with .GetTerms "tags" }}
<nav data-role="tag-cloud" aria-label="Tags">
{{- range . }}
<a href="{{ .Permalink }}" data-tag="{{ .Name }}">{{ .Name }}</a>
{{- end }}
</nav>
{{- end }}
</header>
{{ .Content }}
{{- if .PrevInSection }}
<footer>
{{- with .PrevInSection }}
<a href="{{ .RelPermalink }}" rel="prev" data-wikilink>← {{ .Title }}</a>
{{- end }}
{{- with .NextInSection }}
<a href="{{ .RelPermalink }}" rel="next" data-wikilink>{{ .Title }} →</a>
{{- end }}
</footer>
{{- end }}
</article>
{{ end }}

View file

@ -0,0 +1,20 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
{{- if .IsHome -}}
{{ .Site.Title }}
{{- else -}}
{{ .Title }} · {{ .Site.Title }}
{{- end -}}
</title>
{{- with .Description }}<meta name="description" content="{{ . }}">{{- end }}
{{- if not .Description }}{{- with .Site.Params.description }}<meta name="description" content="{{ . }}">{{- end }}{{- end }}
<!-- ASW framework CSS — absURL ensures correct path when served at a subpath -->
<link rel="stylesheet" href="{{ "css/asw.css" | absURL }}">
<!-- Trentuna theme tokens: Inter, JetBrains Mono, dark palette -->
<link rel="stylesheet" href="{{ "css/theme.css" | absURL }}">
{{- range .AlternativeOutputFormats -}}
<link rel="{{ .Rel }}" type="{{ .MediaType.Type }}" href="{{ .Permalink | safeURL }}">
{{- end }}

View file

@ -0,0 +1,21 @@
{{- /*
tag-nav.html — renders all site tags as a navigable tag cloud.
Usage in a layout:
{{ partial "tag-nav.html" . }}
Outputs: <nav data-role="tag-cloud"> with links to each tag page.
The (N) count shows how many pages have each tag.
*/ -}}
{{- $tags := .Site.Taxonomies.tags -}}
{{- if $tags }}
<nav data-role="tag-cloud" aria-label="Browse by tag">
{{- range $name, $pages := $tags }}
{{- $tagPage := site.GetPage (printf "/tags/%s" ($name | urlize)) }}
<a href="{{ if $tagPage }}{{ $tagPage.Permalink }}{{ else }}{{ print site.BaseURL "tags/" ($name | urlize) "/" }}{{ end }}" data-tag="{{ $name }}">
{{ $name -}}
<small>({{ len $pages }})</small>
</a>
{{- end }}
</nav>
{{- end }}

View file

@ -0,0 +1,16 @@
{{- /*
callout shortcode — wraps content in an ASW callout block.
Usage:
{{< callout note >}}
Content here. Markdown is rendered.
{{< /callout >}}
First positional param: callout type.
Valid types: note, warning, tip, info (maps to ASW data-callout attribute).
Default: note
*/ -}}
{{- $type := .Get 0 | default "note" -}}
<aside data-callout="{{ $type }}">
{{ .Inner | markdownify }}
</aside>

View file

@ -0,0 +1,20 @@
{{- /*
wikilink shortcode — renders an internal link with ASW data-wikilink attribute.
Usage:
{{< wikilink "Display Text" "/path/to/page/" >}}
Param 0 (required): display text
Param 1 (optional): href path. Defaults to /display-text-slugified/
Examples:
{{< wikilink "My Note" >}}
<a href="/my-note/" data-wikilink>My Note</a>
{{< wikilink "My Note" "/vault/my-note/" >}}
<a href="/vault/my-note/" data-wikilink>My Note</a>
*/ -}}
{{- $text := .Get 0 -}}
{{- $slug := $text | lower | replace " " "-" -}}
{{- $href := .Get 1 | default (printf "/%s/" $slug) -}}
<a href="{{ $href | relURL }}" data-wikilink>{{ $text }}</a>

View file

@ -0,0 +1,82 @@
# Decap CMS configuration for ASW-Hugo pack
# Browser-based Markdown editing — no backend server required.
#
# Setup:
# 1. Update `repo` below to your Forgejo repo path (owner/repo)
# 2. Update `base_url` to your Forgejo instance URL
# 3. Register an OAuth app in Forgejo: Settings → Applications → OAuth2 Apps
# - Redirect URL: https://yoursite.com/admin/
# 4. Deploy admin/ alongside your Hugo output
# 5. Access at https://yoursite.com/admin/
backend:
name: gitea # Forgejo is Gitea-compatible
repo: trentuna/vault # ← CHANGE: your repo (owner/repo)
branch: main
base_url: https://trentuna.com # ← CHANGE: your Forgejo instance URL
auth_endpoint: /api/v1/oauth2/
media_folder: static/images
public_folder: /images
# Slug format for new posts
slug:
encoding: ascii
clean_accents: true
collections:
- name: posts
label: Posts
label_singular: Post
folder: content/posts
create: true
slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
preview_path: "posts/{{slug}}/"
fields:
- label: Title
name: title
widget: string
- label: Date
name: date
widget: datetime
default: ""
date_format: "YYYY-MM-DD"
time_format: "HH:mm:ss"
format: "YYYY-MM-DDTHH:mm:ssZ"
- label: Description
name: description
widget: string
required: false
hint: Short summary shown in list views
- label: Tags
name: tags
widget: list
required: false
hint: Comma-separated tags
- label: Draft
name: draft
widget: boolean
default: true
- label: Body
name: body
widget: markdown
- name: pages
label: Pages
label_singular: Page
folder: content
create: true
filter:
field: _type
value: page
fields:
- label: Title
name: title
widget: string
- label: Description
name: description
widget: string
required: false
- label: Body
name: body
widget: markdown

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,130 @@
/*
* Trentuna theme tokens for ASW-Hugo pack
* Overrides ASW custom properties with trentuna-specific values.
*
* Zero CDN dependencies system font stacks only, matching ASW's own ethos.
* ASW framework (web-fonts.css) uses system fonts by design: zero external requests,
* zero layout shift, fonts resolve to native equivalents on every device.
*
* Inter and JetBrains Mono resolve to system-ui / monospace on most platforms.
* To use web fonts: add @font-face declarations to a local fonts.css and
* @import it before this file.
*/
:root {
/* --- Typography (system font stacks — no external loading) --- */
--font-sans: Inter, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace;
/* --- Dark palette --- */
--color-bg: #0f0f0f;
--color-surface: #1a1a1a;
--color-surface-2:#222222;
--color-border: #2e2e2e;
--color-text: #e8e8e8;
--color-muted: #888888;
--color-accent: #e8c87a; /* warm gold */
--color-accent-2: #7aadcf; /* cool blue */
--color-danger: #cf7a7a;
--color-success: #7acf9a;
/* --- Spacing scale (matches ASW defaults) --- */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2.5rem;
--space-2xl: 4rem;
}
/* Base */
html {
background-color: var(--color-bg);
color: var(--color-text);
font-family: var(--font-sans);
}
code, pre, kbd, samp, tt {
font-family: var(--font-mono);
}
/* Links */
a {
color: var(--color-accent);
}
a:hover {
color: var(--color-accent-2);
}
a[data-wikilink] {
text-decoration: underline dotted;
}
/* Navigation */
header nav {
display: flex;
gap: var(--space-md);
align-items: center;
padding: var(--space-md) var(--space-lg);
border-bottom: 1px solid var(--color-border);
}
header nav a {
text-decoration: none;
font-weight: 500;
}
header nav strong {
color: var(--color-text);
margin-right: var(--space-md);
}
/* Tag cloud */
nav[data-role="tag-cloud"] {
display: flex;
flex-wrap: wrap;
gap: var(--space-xs) var(--space-sm);
}
nav[data-role="tag-cloud"] a {
font-size: 0.8rem;
text-decoration: none;
padding: 0.15em 0.5em;
border: 1px solid var(--color-border);
border-radius: 2px;
color: var(--color-muted);
}
nav[data-role="tag-cloud"] a:hover {
color: var(--color-accent);
border-color: var(--color-accent);
}
a[data-tag] {
font-size: 0.8rem;
color: var(--color-muted);
text-decoration: none;
}
a[data-tag]:hover {
color: var(--color-accent);
}
/* Grid layout */
section[data-layout="grid"] {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
gap: var(--space-lg);
}
/* Article cards in grid */
section[data-layout="grid"] article {
border: 1px solid var(--color-border);
padding: var(--space-md);
border-radius: 3px;
}
section[data-layout="grid"] article:hover {
border-color: var(--color-border);
background: var(--color-surface);
}
/* Footer */
footer {
padding: var(--space-lg);
color: var(--color-muted);
border-top: 1px solid var(--color-border);
font-size: 0.85rem;
}

View file

@ -0,0 +1,30 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/asw/asw.css">
<title>Directory Listing</title>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; }
pre { /* nginx autoindex output */
background: var(--asw-bg-elevated);
border: 1px solid var(--asw-border);
border-radius: var(--asw-radius-md);
padding: 1.5rem;
font-family: var(--asw-font-mono);
font-size: 0.875rem;
line-height: 1.8;
overflow-x: auto;
}
pre a { color: var(--asw-accent); text-decoration: none; }
pre a:hover { text-decoration: underline; }
main { flex: 1; padding: 2rem; max-width: 80ch; margin: 0 auto; width: 100%; }
</style>
</head>
<body>
<nav>
<ul><li><a href="/"><strong>Home</strong></a></li></ul>
</nav>
<main>
<h1>Directory listing</h1>

View file

@ -0,0 +1,73 @@
# ASW — Agentic Semantic Web site
# Self-contained server block. Proxied from trentuna at /asw/.
# Future: point asw.trentuna.com directly at port 8044.
server {
listen 8044;
listen [::]:8044;
server_name _;
root /home/exedev/projects/agentic-semantic-web;
index index.html;
ssi on;
error_page 403 /errors/403.html;
error_page 404 /errors/404.html;
error_page 500 /errors/500.html;
error_page 502 /errors/502.html;
error_page 503 /errors/503.html;
location /errors/ {
alias /home/exedev/projects/agentic-semantic-web/errors/;
internal;
}
# Block source/build files — not for public consumption
location ~ ^/(src|content|templates|node_modules|packs|dist)(/?$|/) {
return 404;
}
location ~ \.(sh|lua|lock)$ {
return 404;
}
# docs/ — directory listing with ASW styling injected into autoindex responses.
# Target patterns unique to nginx autoindex output:
# - '<head><title>Index of' — only in autoindex, never in hand-crafted HTML
# - '<body bgcolor="white">' — only in autoindex
# Hand-crafted HTML pages in /docs/ are NOT affected.
location /docs/ {
autoindex on;
try_files $uri $uri/ =404;
sub_filter '<head><title>Index of' '<head>
<link rel="stylesheet" href="/asw.css">
<meta name="color-scheme" content="dark">
<style>
body{padding:2rem;max-inline-size:64ch}
h1{font-size:1rem;font-weight:500;color:var(--text-2);margin-block-end:1.5rem}
a{color:var(--blue-3);text-decoration:none}a:hover{text-decoration:underline}
pre{font-family:var(--font-mono,"JetBrains Mono",monospace);line-height:1.8}
hr{border:none;border-block-start:1px solid var(--stone-7)}
</style>
<title>Index of';
sub_filter '<body bgcolor="white">' '<body>';
sub_filter_once on;
}
# lab/ — now has index.html; subdirs with autoindex still styled via sub_filter
location /lab/ {
try_files $uri $uri/ =404;
}
# Hugo demo — ASW-Hugo pack showcase (serves lab/hugo-demo/public/)
location /hugo-demo/ {
alias /home/exedev/projects/agentic-semantic-web/lab/hugo-demo/public/;
index index.html;
try_files $uri $uri/ =404;
}
location / {
try_files $uri $uri/ =404;
}
}

View file

@ -0,0 +1,59 @@
# ASW Nginx Directory Listing — config snippet
#
# nginx's autoindex generates its own HTML that can't be easily replaced.
# The cleanest approach: serve autoindex JSON and render it client-side.
#
# This gives full control over the HTML output while keeping nginx doing
# the work of listing directories.
# ── Option A: JSON autoindex + client-side renderer (recommended) ─────────────
#
# nginx serves directory listings as JSON; a small JS fragment renders them
# with ASW styles. The renderer is served from /asw/autoindex.js.
location /files/ {
# The directory to browse
alias /var/www/files/;
autoindex on;
autoindex_format json; # nginx ≥ 1.7.9
# When the path ends in / (directory request), serve our renderer
# instead of the raw JSON. The renderer fetches the JSON itself.
index ___nonexistent___; # disable default index file lookup
# Intercept directory responses and redirect to renderer
# (requires auth_request or a small proxy; see Option B for simpler approach)
}
# Serve the ASW autoindex renderer
location = /asw/autoindex.js {
alias /home/exedev/projects/agentic-semantic-web/packs/nginx/autoindex.js;
}
# ── Option B: add_before_body / add_after_body (simplest) ────────────────────
#
# nginx injects HTML before and after its generated listing.
# This wraps the ugly default output in ASW chrome — not perfect but zero JS.
#
# Requires nginx built with --with-http_addition_module (default on most distros).
location /browse/ {
alias /var/www/browse/;
autoindex on;
# Inject ASW nav before the listing, footer after
add_before_body /asw/autoindex-header.html;
add_after_body /asw/autoindex-footer.html;
}
location = /asw/autoindex-header.html {
alias /home/exedev/projects/agentic-semantic-web/packs/nginx/autoindex-header.html;
internal;
}
location = /asw/autoindex-footer.html {
alias /home/exedev/projects/agentic-semantic-web/packs/nginx/autoindex-footer.html;
internal;
}

View file

@ -0,0 +1,25 @@
# ASW Nginx Error Pages — config snippet
#
# Paste this into your nginx server block.
# Adjust the alias path to where your agentic-semantic-web repo lives.
#
# Assumes asw.css is served at /asw/asw.css
# (see nginx-asw-css.conf for the CSS serving block)
# ── Error pages ──────────────────────────────────────────────────────────────
error_page 400 /errors/400.html;
error_page 401 /errors/401.html;
error_page 403 /errors/403.html;
error_page 404 /errors/404.html;
error_page 500 /errors/500.html;
error_page 502 /errors/502.html;
error_page 503 /errors/503.html;
# Serve error pages from the ASW repo.
# `internal` means these URLs are only accessible via internal redirects (error_page),
# not directly from the browser — prevents users from 200ing your error pages.
location /errors/ {
alias /home/exedev/projects/agentic-semantic-web/errors/;
internal;
}

View file

@ -0,0 +1,105 @@
---
title: "ASW Pandoc Pack"
description: "Convert markdown to ASW-styled HTML with one command"
layout: prose
---
# ASW Pandoc Pack
Converts GFM markdown to fully ASW-styled HTML. Write markdown natively; the pack renders it with correct `data-attribute` semantics.
## Usage
```bash
pandoc input.md \
--from gfm \
--lua-filter packs/pandoc/asw.lua \
--template packs/pandoc/asw.html5 \
-o output.html
```
## What it does
### Semantic mapping (asw.lua)
| Markdown | Output HTML |
|---|---|
| `- [ ] task` | `<li data-task="todo">task</li>` |
| `- [x] done` | `<li data-task="done">done</li>` |
| `> [!NOTE]` | `<div data-callout="note">` |
| `> [!WARNING]` | `<div data-callout="warning">` |
| `> [!TIP]` | `<div data-callout="tip">` |
| `> [!ERROR]` | `<div data-callout="error">` |
| `[[target]]` | `<a data-wikilink href="#target">target</a>` |
| `[[target\|label]]` | `<a data-wikilink href="#target">label</a>` |
Callout aliases: `[!CAUTION]` → warning, `[!IMPORTANT]`/`[!INFO]` → note, `[!DANGER]` → error.
Wikilinks: punctuation attached to `]]` is preserved outside the link element. Unresolved link detection is not automatic — the href slug is always generated; resolution is a build-time concern.
### Frontmatter → data-attributes (asw.html5)
YAML frontmatter keys map to page-level ASW properties:
```yaml
---
title: "Session Report — 2026-04-02"
description: "Summary of what happened"
layout: prose # → data-layout on <main> (default: prose)
status: done # → data-status on <main> and in header badge
date: 2026-04-02 # → <time datetime="...">
section: "Sessions" # → <p data-text="eyebrow"> above <h1>
tags:
- session
- autonomous
# Session metadata (if agent-authored):
agent: vigilio
model: claude-sonnet-4-6
mode: autonomous # → <span data-mode="autonomous">
session: 10
---
```
Keys not set are simply omitted — no empty elements in the output.
## Layout values
The `layout` key maps directly to `data-layout` on `<main>`:
| Value | Use case |
|---|---|
| `prose` (default) | Articles, notes, documentation |
| `docs` | Two-column docs layout |
| `fluid` | Full-width, no max-width |
Any `data-layout` value supported by `asw.css` works here.
## Status values
| Value | Meaning |
|---|---|
| `done` | Complete |
| `awake` | Active agent/session |
| `sleeping` | Inactive |
| `blocked` | Waiting on dependency |
## CSS dependency
The pack generates HTML that requires `asw.css` for styling. The template links `/asw.css` by default. Override with `--variable css=/path/to/asw.css`.
## Relationship to the build pipeline
`build.sh` uses this pack (via the `doc.html` template) for `content/``docs/` compilation. The standalone pack is also useful for:
- Rendering vault notes as public pages
- One-off markdown → ASW HTML conversion
- Integration into other build systems (trentuna-web, agent pipelines)
## Files
```
packs/pandoc/
├── asw.html5 pandoc HTML5 template — page structure, frontmatter vars
├── asw.lua Lua filter — task lists, callouts, wikilinks
└── README.md this file
```

View file

@ -0,0 +1,67 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>$if(title)$$title$$else$Untitled$endif$</title>
$if(description)$ <meta name="description" content="$description$">
$endif$
$-- CSS: link to asw.css (adjust path for your deployment) $
<link rel="stylesheet" href="/asw.css">
$for(css)$ <link rel="stylesheet" href="$css$">
$endfor$
$if(header-includes)$$header-includes$$endif$
</head>
<body>
$-- Page header: title + optional eyebrow / metadata $
$if(title)$
<header>
<hgroup>
$if(section)$ <p data-text="eyebrow">$section$</p>
$endif$ <h1>$title$</h1>
$if(description)$ <p data-text="lead">$description$</p>
$endif$ </hgroup>
$-- Session metadata block (agent-authored content) $
$if(agent)$
<div data-session>
<span data-session-meta>
$if(date)$ <time datetime="$date$">$date$</time>$endif$
$if(agent)$ <span>$agent$</span>$endif$
$if(model)$ <span>$model$</span>$endif$
$if(mode)$ <span data-mode="$mode$">$mode$</span>$endif$
$if(session)$ <span>session $session$</span>$endif$
</span>
$if(status)$ <span data-status="$status$">$status$</span>$endif$
</div>
$endif$
$-- Tags $
$if(tags)$
<nav aria-label="tags">
$for(tags)$ <a data-tag href="/tags/$tags$">$tags$</a>
$endfor$ </nav>
$endif$
</header>
$endif$
$-- Main content — layout from frontmatter or default to prose $
<main$if(layout)$ data-layout="$layout$"$else$ data-layout="prose"$endif$$if(status)$ data-status="$status$"$endif$>
$body$
</main>
$-- Footer $
$if(date)$
<footer>
<small>
$if(date)$ <time datetime="$date$">$date$</time>$endif$
$if(author)$ · $for(author)$$author$$sep$, $endfor$$endif$
</small>
</footer>
$endif$
$if(include-after)$$include-after$$endif$
</body>
</html>

View file

@ -0,0 +1,272 @@
-- asw.lua — Pandoc Lua filter for Agentic Semantic Web (ASW)
-- Converts GFM markdown constructs to ASW data-attribute HTML
--
-- Usage:
-- pandoc input.md --from gfm --lua-filter asw.lua --template asw.html5 -o output.html
--
-- Transforms:
-- - [ ] item → <li data-task="todo">
-- - [x] item → <li data-task="done">
-- > [!NOTE] → <div data-callout="note">
-- > [!WARNING] → <div data-callout="warning">
-- > [!TIP] → <div data-callout="tip">
-- > [!ERROR] → <div data-callout="error">
-- [[target]] → <a data-wikilink href="#target">target</a>
-- [[target|label]] → <a data-wikilink href="#target">label</a>
-- ── Callouts ─────────────────────────────────────────────────────────────────
-- GitHub callout syntax: > [!TYPE]\n> content → <div data-callout="type">
local CALLOUT_MAP = {
NOTE = "note",
INFO = "note",
IMPORTANT = "note",
WARNING = "warning",
CAUTION = "warning",
TIP = "tip",
ERROR = "error",
DANGER = "error",
}
local CALLOUT_LABEL = {
note = "Note",
warning = "Warning",
tip = "Tip",
error = "Error",
}
function BlockQuote(el)
local first = el.content[1]
if not first or (first.t ~= "Para" and first.t ~= "Plain") then return el end
-- The first inline should be a Str like "[!NOTE]"
local first_inline = first.content and first.content[1]
if not first_inline or first_inline.t ~= "Str" then return el end
local marker = first_inline.text:match("^%[!(%u+)%]$")
if not marker then return el end
local callout_type = CALLOUT_MAP[marker]
if not callout_type then return el end
local label = CALLOUT_LABEL[callout_type] or marker:sub(1,1) .. marker:sub(2):lower()
local blocks = {
pandoc.RawBlock("html", '<div data-callout="' .. callout_type .. '">'),
pandoc.RawBlock("html", '<p data-callout-title>' .. label .. '</p>'),
}
-- First para: strip the [!MARKER] token and optional following SoftBreak/Space
local inlines = first.content
local start_i = 2
if inlines[start_i] and
(inlines[start_i].t == "SoftBreak" or inlines[start_i].t == "Space") then
start_i = start_i + 1
end
local rest_inlines = {}
for i = start_i, #inlines do
table.insert(rest_inlines, inlines[i])
end
if #rest_inlines > 0 then
table.insert(blocks, pandoc.Para(rest_inlines))
end
-- Remaining blocks in the blockquote
for i = 2, #el.content do
table.insert(blocks, el.content[i])
end
table.insert(blocks, pandoc.RawBlock("html", '</div>'))
return blocks
end
-- ── Task Lists ───────────────────────────────────────────────────────────────
-- GFM: - [ ] → <li data-task="todo">, - [x] → <li data-task="done">
-- Pandoc (GFM mode) emits checkboxes as RawInline html: <input type="checkbox" ...>
-- Pandoc 3.x encodes GFM task list checkboxes as Unicode Str:
-- ☒ (U+2612) = checked/done, ☐ (U+2610) = unchecked/todo
local CHECKBOX_DONE = "\xe2\x98\x92" -- ☒ UTF-8
local CHECKBOX_TODO = "\xe2\x98\x90" -- ☐ UTF-8
local function checkbox_state(inline)
if inline and inline.t == "Str" then
if inline.text == CHECKBOX_DONE then return "done" end
if inline.text == CHECKBOX_TODO then return "todo" end
end
return nil
end
function BulletList(el)
-- First pass: check if any item is a task
local has_tasks = false
for _, item in ipairs(el.content) do
local first = item[1]
if first and (first.t == "Plain" or first.t == "Para") and first.content[1] then
if checkbox_state(first.content[1]) then
has_tasks = true
break
end
end
end
if not has_tasks then return el end
-- Rebuild as explicit HTML with data-task attributes
local parts = { pandoc.RawBlock("html", "<ul>") }
for _, item in ipairs(el.content) do
local first = item[1]
local state = nil
if first and (first.t == "Plain" or first.t == "Para") and first.content[1] then
state = checkbox_state(first.content[1])
end
if state then
table.insert(parts, pandoc.RawBlock("html", '<li data-task="' .. state .. '">'))
-- Strip checkbox inline (index 1) and optional space (index 2)
local trimmed = {}
for i, inline in ipairs(first.content) do
if i == 1 then
-- skip checkbox
elseif i == 2 and inline.t == "Space" then
-- skip leading space after checkbox
else
table.insert(trimmed, inline)
end
end
if #trimmed > 0 then
table.insert(parts, pandoc.Plain(trimmed))
end
-- Remaining blocks in this item
for i = 2, #item do
table.insert(parts, item[i])
end
table.insert(parts, pandoc.RawBlock("html", "</li>"))
else
-- Regular list item — emit verbatim
table.insert(parts, pandoc.RawBlock("html", "<li>"))
for _, block in ipairs(item) do
table.insert(parts, block)
end
table.insert(parts, pandoc.RawBlock("html", "</li>"))
end
end
table.insert(parts, pandoc.RawBlock("html", "</ul>"))
return parts
end
-- ── Wikilinks ─────────────────────────────────────────────────────────────────
-- [[target|label]] or [[target]] → <a data-wikilink href="#slug">label</a>
--
-- Pandoc 3.x tokenizes at whitespace, so [[target|label with spaces]] arrives as
-- multiple Str/Space tokens. process_wikilinks coalesces adjacent Str+Space runs
-- into a combined string before scanning for [[...]] patterns, then reconstructs
-- the inline list. Non-Str/Space inlines (Bold, Emph, Code, etc.) pass through.
local function slugify(text)
return text:lower():gsub("[%s_]+", "-"):gsub("[^%w%-]", "")
end
-- Scan combined for [[...]] patterns and emit inlines.
-- Returns a list of inlines with wikilinks replaced, plus a changed flag.
local function scan_for_wikilinks(combined)
local parts = {}
local pos = 1
local changed = false
while pos <= #combined do
local s, e = combined:find("%[%[", pos)
if not s then
if pos <= #combined then
table.insert(parts, pandoc.Str(combined:sub(pos)))
end
break
end
if s > pos then
table.insert(parts, pandoc.Str(combined:sub(pos, s - 1)))
end
local close_s, close_e = combined:find("%]%]", e + 1)
if not close_s then
table.insert(parts, pandoc.Str("[["))
pos = e + 1
else
local inner = combined:sub(e + 1, close_s - 1)
local target, label = inner:match("^([^|]+)|(.+)$")
if not target then
target = inner
label = inner
end
target = target:match("^%s*(.-)%s*$")
label = label:match("^%s*(.-)%s*$")
table.insert(parts, pandoc.RawInline("html",
'<a data-wikilink href="#' .. slugify(target) .. '">' .. label .. '</a>'))
pos = close_e + 1
changed = true
end
end
return parts, changed
end
local function process_wikilinks(inlines)
-- Pass 1: coalesce runs of Str+Space into combined strings, scan for wikilinks.
-- Non-text inlines (Emph, Strong, Code, RawInline, etc.) flush any pending run first.
local result = {}
local changed = false
local i = 1
while i <= #inlines do
local el = inlines[i]
if el.t == "Str" or el.t == "Space" then
-- Collect a contiguous Str+Space run
local run = {}
local combined = ""
while i <= #inlines and (inlines[i].t == "Str" or inlines[i].t == "Space") do
if inlines[i].t == "Str" then
combined = combined .. inlines[i].text
else
combined = combined .. " "
end
table.insert(run, inlines[i])
i = i + 1
end
if combined:find("%[%[") then
local parts, any_changed = scan_for_wikilinks(combined)
if any_changed then
for _, part in ipairs(parts) do table.insert(result, part) end
changed = true
else
-- No wikilinks resolved — emit original tokens unchanged
for _, tok in ipairs(run) do table.insert(result, tok) end
end
else
-- No [[ at all — emit original tokens unchanged
for _, tok in ipairs(run) do table.insert(result, tok) end
end
else
table.insert(result, el)
i = i + 1
end
end
return changed and result or nil
end
function Para(el)
local r = process_wikilinks(el.content)
if r then return pandoc.Para(r) end
end
function Plain(el)
local r = process_wikilinks(el.content)
if r then return pandoc.Plain(r) end
end
function Header(el)
local r = process_wikilinks(el.content)
if r then return pandoc.Header(el.level, r, el.attr) end
end

View file

@ -0,0 +1,50 @@
<!doctype html>
<html lang="en">
<head>
<!--#include virtual="/_include/head.html" -->
<title>$title$ — ASW Docs</title>
$if(description)$ <meta name="description" content="$description$">
$endif$</head>
<body>
<!--#include virtual="/_include/nav.html" -->
<div data-layout="docs">
<!--#include virtual="/_include/sidebar.html" -->
<article>
<h1>$title$</h1>
$if(description)$ <p data-text="lead">$description$</p>
$endif$
$body$
$if(prev-url)$
<nav data-role="prev-next">
$if(prev-url)$ <a href="$prev-url$" rel="prev">
<small>← Previous</small>
<span>$prev-title$</span>
</a>
$endif$
$if(next-url)$ <a href="$next-url$" rel="next">
<small>Next →</small>
<span>$next-title$</span>
</a>
$endif$
</nav>
$endif$
</article>
<aside data-toc>
<nav aria-label="On this page" data-nav="toc">
<small>On this page</small>
<ul></ul>
</nav>
</aside>
</div>
<!--#include virtual="/_include/footer.html" -->
<script src="/docs/toc-spy.js"></script>
</body>
</html>

View file

@ -0,0 +1,22 @@
<!doctype html>
<html lang="en">
<head>
<!--#include virtual="/_include/head.html" -->
<title>$title$ — ASW Examples</title>
$if(description)$ <meta name="description" content="$description$">
$endif$</head>
<body>
<!--#include virtual="/_include/nav.html" -->
<main>
<header>
<h1>$title$</h1>
$if(description)$ <p data-text="lead">$description$</p>
$endif$ </header>
$body$
</main>
<!--#include virtual="/_include/footer.html" -->
</body>
</html>

View file

@ -0,0 +1,20 @@
<!doctype html>
<html lang="en">
<head>
<!--#include virtual="/_include/head.html" -->
<title>$title$ — ASW</title>
$if(description)$ <meta name="description" content="$description$">
$endif$</head>
<body>
<!--#include virtual="/_include/nav.html" -->
<main data-layout="prose">
<h1>$title$</h1>
$if(description)$ <p data-text="lead">$description$</p>
$endif$
$body$
</main>
<!--#include virtual="/_include/footer.html" -->
</body>
</html>

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<!--#include virtual="/_include/head.html" -->
<title>$title$ — ASW</title>
$if(description)$ <meta name="description" content="$description$">
$endif$</head>
<body>
<!--#include virtual="/_include/nav.html" -->
$body$
<!--#include virtual="/_include/footer.html" -->
</body>
</html>

View file

@ -0,0 +1,35 @@
# ASW Python Dev Server Pack
Drop-in replacement for `python -m http.server` with ASW-styled error pages and directory listings.
## Usage
```bash
# Copy to your project
cp asw_server.py /path/to/project/
# Run it
python asw_server.py # port 8000
python asw_server.py 3000 # custom port
python asw_server.py 3000 /dir # custom port + custom directory
```
## What you get
- **ASW-styled error pages** for all HTTP error codes (403, 404, 500, etc.)
- **ASW-styled directory listings** replacing the browser-default autoindex
- **Auto port fallback** — if 8000 is busy, tries 8001, 8002… up to 10 attempts
- **Inline styles** — no external CSS dependency, works even when serving offline
## How it works
`ASWHandler` subclasses `SimpleHTTPRequestHandler` and overrides:
- `send_error()` — returns ASW-styled HTML instead of the default ugly page
- `list_directory()` — returns a clean table layout instead of raw `<pre>`
Styles are inlined directly in the response — no link to `asw.css` required.
The colour scheme matches the full ASW dark theme.
## No dependencies
Just Python 3 standard library. Nothing to install.

View file

@ -0,0 +1,348 @@
#!/usr/bin/env python3
"""
ASW Dev Server http.server with ASW-styled error pages.
Drop this in any directory and run it instead of `python -m http.server`.
All error responses (403, 404, 500, etc.) are styled with ASW CSS.
Usage:
python asw_server.py # serve on port 8000
python asw_server.py 3000 # custom port
python asw_server.py 3000 /dir # custom port + directory
The server inlines ASW styles so no external dependencies are needed.
"""
import http.server
import sys
import os
import socket
import urllib.parse
import html
# ── Minimal ASW inline styles ─────────────────────────────────────────────────
# Reduced version for standalone error pages — no external deps.
ASW_INLINE = """
:root {
--asw-bg: #0d1117;
--asw-bg-elevated: #161b22;
--asw-bg-overlay: #1c2128;
--asw-text: #e6edf3;
--asw-text-secondary: #8b949e;
--asw-text-muted: #484f58;
--asw-accent: #3fb950;
--asw-border: #30363d;
--asw-font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
--asw-font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', ui-monospace, monospace;
}
*, *::before, *::after { box-sizing: border-box; }
html { font-size: 16px; }
body {
background: var(--asw-bg);
color: var(--asw-text);
font-family: var(--asw-font-body);
font-size: 1rem;
line-height: 1.6;
margin: 0;
display: flex;
flex-direction: column;
min-height: 100vh;
}
nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--asw-border);
background: var(--asw-bg-elevated);
}
nav a { color: var(--asw-text); text-decoration: none; font-weight: 600; }
nav a:hover { color: var(--asw-accent); }
.badge {
font-family: var(--asw-font-mono);
font-size: 0.75rem;
color: var(--asw-text-muted);
background: var(--asw-bg-overlay);
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
border: 1px solid var(--asw-border);
}
main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
}
.error-card {
max-width: 42ch;
text-align: center;
}
.error-code {
font-family: var(--asw-font-mono);
font-size: 6rem;
font-weight: 700;
line-height: 1;
color: var(--asw-text-muted);
letter-spacing: -0.05em;
margin: 0 0 0.5rem;
}
h1 {
margin: 0 0 1rem;
font-size: 1.5rem;
color: var(--asw-text);
}
p { margin: 0 0 0.75rem; color: var(--asw-text-secondary); }
a.button {
display: inline-block;
margin-top: 1.5rem;
padding: 0.5rem 1.25rem;
background: transparent;
color: var(--asw-accent);
border: 1px solid var(--asw-accent);
border-radius: 0.375rem;
text-decoration: none;
font-size: 0.875rem;
transition: background 0.15s;
}
a.button:hover { background: rgba(63, 185, 80, 0.1); }
code {
font-family: var(--asw-font-mono);
font-size: 0.85em;
background: var(--asw-bg-overlay);
padding: 0.1em 0.35em;
border-radius: 0.2rem;
color: var(--asw-text-secondary);
}
footer {
text-align: center;
padding: 1rem;
font-size: 0.75rem;
color: var(--asw-text-muted);
border-top: 1px solid var(--asw-border);
}
footer a { color: var(--asw-text-muted); }
/* Directory listing */
.dir-listing { max-width: 72ch; width: 100%; }
.dir-listing h1 { font-size: 1.25rem; margin-bottom: 1.5rem; text-align: left; }
.dir-listing h1 code { font-size: 1rem; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 0.5rem 0.75rem; text-align: left; border-bottom: 1px solid var(--asw-border); }
th { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--asw-text-muted); }
td a { color: var(--asw-accent); text-decoration: none; }
td a:hover { text-decoration: underline; }
.size { color: var(--asw-text-muted); font-family: var(--asw-font-mono); font-size: 0.8rem; }
.parent { color: var(--asw-text-muted); }
"""
ERROR_MESSAGES = {
400: ("Bad Request", "The server couldn't understand the request."),
401: ("Unauthorized", "Authentication is required to access this resource."),
403: ("Forbidden", "You don't have permission to access this path."),
404: ("Not Found", "The file or directory you requested doesn't exist."),
405: ("Method Not Allowed", "That HTTP method isn't supported here."),
408: ("Request Timeout", "The request took too long to process."),
500: ("Server Error", "Something went wrong on the server."),
501: ("Not Implemented", "The server doesn't support that feature."),
503: ("Service Unavailable", "The server is temporarily unable to handle requests."),
}
def error_page(code: int, path: str = "") -> bytes:
title, message = ERROR_MESSAGES.get(code, ("Error", "An error occurred."))
path_hint = f"<p>Path: <code>{html.escape(path)}</code></p>" if path else ""
body = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{code} {title}</title>
<style>{ASW_INLINE}</style>
</head>
<body>
<nav>
<a href="/">&#x2302; Dev Server</a>
<span class="badge">http.server</span>
</nav>
<main>
<div class="error-card">
<p class="error-code">{code}</p>
<h1>{title}</h1>
<p>{message}</p>
{path_hint}
<a class="button" href="/"> Back to root</a>
</div>
</main>
<footer>ASW Dev Server &middot; <a href="https://github.com/trentuna/agentic-semantic-web">agentic-semantic-web</a></footer>
</body>
</html>"""
return body.encode("utf-8")
def dir_listing_page(path: str, display_path: str, entries: list) -> bytes:
rows = []
if display_path != "/":
parent = "/" + "/".join(display_path.strip("/").split("/")[:-1])
rows.append(f'<tr><td class="parent"><a href="{parent or "/"}">../</a></td><td class="size">—</td></tr>')
for name, is_dir, size in sorted(entries, key=lambda e: (not e[1], e[0].lower())):
link_name = html.escape(name) + ("/" if is_dir else "")
href = urllib.parse.quote(name) + ("/" if is_dir else "")
size_str = "" if is_dir else _human_size(size)
rows.append(f'<tr><td><a href="{href}">{link_name}</a></td><td class="size">{size_str}</td></tr>')
rows_html = "\n ".join(rows)
body = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Index of {html.escape(display_path)}</title>
<style>{ASW_INLINE}</style>
</head>
<body>
<nav>
<a href="/">&#x2302; Dev Server</a>
<span class="badge">http.server</span>
</nav>
<main>
<div class="dir-listing">
<h1>Index of <code>{html.escape(display_path)}</code></h1>
<table>
<thead><tr><th>Name</th><th>Size</th></tr></thead>
<tbody>
{rows_html}
</tbody>
</table>
</div>
</main>
<footer>ASW Dev Server &middot; <a href="https://github.com/trentuna/agentic-semantic-web">agentic-semantic-web</a></footer>
</body>
</html>"""
return body.encode("utf-8")
def _human_size(n: int) -> str:
for unit in ("B", "KB", "MB", "GB"):
if n < 1024:
return f"{n:.0f} {unit}" if unit == "B" else f"{n:.1f} {unit}"
n /= 1024
return f"{n:.1f} TB"
class ASWHandler(http.server.SimpleHTTPRequestHandler):
"""SimpleHTTPRequestHandler with ASW-styled error pages and directory listings."""
server_version = "ASWServer/1.0"
def send_error(self, code, message=None, explain=None):
"""Override to send ASW-styled error pages."""
try:
short, long = self.responses[code]
except KeyError:
short, long = "???", "???"
if message is None:
message = short
if explain is None:
explain = long
# Log to terminal
self.log_error("code %d, message %s", code, message)
# Build ASW error page
path = getattr(self, "path", "")
content = error_page(code, path)
self.send_response(code, message)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.send_header("Connection", "close")
self.end_headers()
if self.command != "HEAD" and code >= 200 and code not in (204, 304):
self.wfile.write(content)
def list_directory(self, path):
"""Override to send ASW-styled directory listing."""
try:
names = os.listdir(path)
except OSError:
self.send_error(403, "No permission to list directory")
return None
display_path = urllib.parse.unquote(self.path)
entries = []
for name in names:
full = os.path.join(path, name)
is_dir = os.path.isdir(full)
size = 0
if not is_dir:
try:
size = os.path.getsize(full)
except OSError:
pass
entries.append((name, is_dir, size))
content = dir_listing_page(path, display_path, entries)
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.end_headers()
self.wfile.write(content)
return None
def log_message(self, fmt, *args):
"""Slightly cleaner terminal output."""
print(f" {self.address_string()}{fmt % args}")
def main():
port = 8000
directory = "."
if len(sys.argv) > 1:
try:
port = int(sys.argv[1])
except ValueError:
print(f"Usage: {sys.argv[0]} [port] [directory]", file=sys.stderr)
sys.exit(1)
if len(sys.argv) > 2:
directory = sys.argv[2]
directory = os.path.abspath(directory)
handler = lambda *args, **kwargs: ASWHandler(*args, directory=directory, **kwargs)
# Find a free port if needed (dev convenience)
for attempt in range(10):
try:
server = http.server.HTTPServer(("", port + attempt), handler)
actual_port = port + attempt
break
except OSError:
if attempt == 9:
print(f"Could not bind to ports {port}{port+9}", file=sys.stderr)
sys.exit(1)
hostname = socket.gethostname()
print(f"\n ASW Dev Server")
print(f" ──────────────────────────────────")
print(f" Serving: {directory}")
print(f" Local: http://localhost:{actual_port}")
print(f" Network: http://{hostname}:{actual_port}")
print(f" ──────────────────────────────────")
print(f" Press Ctrl+C to stop\n")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n Stopped.")
server.server_close()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/asw.css">
<title>403 — Forbidden</title>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; display: flex; align-items: center; justify-content: center; }
.error-card { max-width: 40ch; text-align: center; }
.error-code {
font-family: var(--asw-font-mono);
font-size: 6rem;
font-weight: 700;
line-height: 1;
color: var(--asw-text-muted);
letter-spacing: -0.05em;
margin-bottom: 0;
}
.error-card h1 { margin-top: 0.5rem; }
.error-card p { color: var(--asw-text-secondary); }
.error-card a { display: inline-block; margin-top: 1.5rem; }
</style>
</head>
<body>
<nav>
<ul><li><a href="/"><strong>Home</strong></a></li></ul>
<ul><li><span data-text="dim">403</span></li></ul>
</nav>
<main>
<div class="error-card">
<p class="error-code">403</p>
<h1>Forbidden</h1>
<p>You don't have permission to access this page.</p>
<a href="/">← Return home</a>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/asw.css">
<title>404 — Not Found</title>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; display: flex; align-items: center; justify-content: center; }
.error-card { max-width: 40ch; text-align: center; }
.error-code {
font-family: var(--asw-font-mono);
font-size: 6rem;
font-weight: 700;
line-height: 1;
color: var(--asw-text-muted);
letter-spacing: -0.05em;
margin-bottom: 0;
}
.error-card h1 { margin-top: 0.5rem; }
.error-card p { color: var(--asw-text-secondary); }
.error-card a { display: inline-block; margin-top: 1.5rem; }
</style>
</head>
<body>
<nav>
<ul><li><a href="/"><strong>Home</strong></a></li></ul>
<ul><li><span data-text="dim">404</span></li></ul>
</nav>
<main>
<div class="error-card">
<p class="error-code">404</p>
<h1>Not Found</h1>
<p>This page doesn't exist or has moved. If you followed a link here, it may be outdated.</p>
<a href="/">← Return home</a>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/asw.css">
<title>500 — Server Error</title>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; display: flex; align-items: center; justify-content: center; }
.error-card { max-width: 40ch; text-align: center; }
.error-code {
font-family: var(--asw-font-mono);
font-size: 6rem;
font-weight: 700;
line-height: 1;
color: var(--asw-text-muted);
letter-spacing: -0.05em;
margin-bottom: 0;
}
.error-card h1 { margin-top: 0.5rem; }
.error-card p { color: var(--asw-text-secondary); }
.error-card a { display: inline-block; margin-top: 1.5rem; }
</style>
</head>
<body>
<nav>
<ul><li><a href="/"><strong>Home</strong></a></li></ul>
<ul><li><span data-text="dim">500</span></li></ul>
</nav>
<main>
<div class="error-card">
<p class="error-code">500</p>
<h1>Server Error</h1>
<p>Something went wrong on the server. It's not you — try again in a moment.</p>
<a href="/">← Return home</a>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/asw.css">
<title>502 — Bad Gateway</title>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; display: flex; align-items: center; justify-content: center; }
.error-card { max-width: 40ch; text-align: center; }
.error-code {
font-family: var(--asw-font-mono);
font-size: 6rem;
font-weight: 700;
line-height: 1;
color: var(--asw-text-muted);
letter-spacing: -0.05em;
margin-bottom: 0;
}
.error-card h1 { margin-top: 0.5rem; }
.error-card p { color: var(--asw-text-secondary); }
.error-card a { display: inline-block; margin-top: 1.5rem; }
</style>
</head>
<body>
<nav>
<ul><li><a href="/"><strong>Home</strong></a></li></ul>
<ul><li><span data-text="dim">502</span></li></ul>
</nav>
<main>
<div class="error-card">
<p class="error-code">502</p>
<h1>Bad Gateway</h1>
<p>The upstream service isn't responding. This usually resolves itself — try again shortly.</p>
<a href="/">← Return home</a>
</div>
</main>
</body>
</html>

View file

@ -0,0 +1,43 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/asw.css">
<title>503 — Service Unavailable</title>
<style>
body { display: flex; flex-direction: column; min-height: 100vh; }
main { flex: 1; display: flex; align-items: center; justify-content: center; }
.error-card { max-width: 40ch; text-align: center; }
.error-code {
font-family: var(--asw-font-mono);
font-size: 6rem;
font-weight: 700;
line-height: 1;
color: var(--asw-text-muted);
letter-spacing: -0.05em;
margin-bottom: 0;
}
.error-card h1 { margin-top: 0.5rem; }
.error-card p { color: var(--asw-text-secondary); }
.error-card a { display: inline-block; margin-top: 1.5rem; }
</style>
</head>
<body>
<nav>
<ul><li><a href="/"><strong>Home</strong></a></li></ul>
<ul><li><span data-text="dim">503</span></li></ul>
</nav>
<main>
<div class="error-card">
<p class="error-code">503</p>
<h1>Service Unavailable</h1>
<p>The service is temporarily offline for maintenance. It should be back shortly.</p>
<a href="/">← Return home</a>
</div>
</main>
</body>
</html>