epresso 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epresso-0.1.1/.gitignore +50 -0
- epresso-0.1.1/LICENSE +21 -0
- epresso-0.1.1/PKG-INFO +521 -0
- epresso-0.1.1/README.md +486 -0
- epresso-0.1.1/pyproject.toml +69 -0
- epresso-0.1.1/src/epresso/__init__.py +31 -0
- epresso-0.1.1/src/epresso/assets.py +153 -0
- epresso-0.1.1/src/epresso/cli.py +450 -0
- epresso-0.1.1/src/epresso/components.py +451 -0
- epresso-0.1.1/src/epresso/config.py +311 -0
- epresso-0.1.1/src/epresso/content/__init__.py +38 -0
- epresso-0.1.1/src/epresso/content/collections.py +90 -0
- epresso-0.1.1/src/epresso/content/docs.py +214 -0
- epresso-0.1.1/src/epresso/content/loaders.py +145 -0
- epresso-0.1.1/src/epresso/content/store.py +126 -0
- epresso-0.1.1/src/epresso/css.py +93 -0
- epresso-0.1.1/src/epresso/deploy.py +56 -0
- epresso-0.1.1/src/epresso/devtoolbar.py +600 -0
- epresso-0.1.1/src/epresso/docs_source.py +45 -0
- epresso-0.1.1/src/epresso/docsgen.py +195 -0
- epresso-0.1.1/src/epresso/document.py +150 -0
- epresso-0.1.1/src/epresso/enforce.py +87 -0
- epresso-0.1.1/src/epresso/errors.py +61 -0
- epresso-0.1.1/src/epresso/fmt.py +294 -0
- epresso-0.1.1/src/epresso/gitrepo.py +65 -0
- epresso-0.1.1/src/epresso/highlight.py +68 -0
- epresso-0.1.1/src/epresso/images.py +191 -0
- epresso-0.1.1/src/epresso/incremental.py +331 -0
- epresso-0.1.1/src/epresso/jsx.py +162 -0
- epresso-0.1.1/src/epresso/jsxattrs.py +60 -0
- epresso-0.1.1/src/epresso/links.py +69 -0
- epresso-0.1.1/src/epresso/logger.py +97 -0
- epresso-0.1.1/src/epresso/markdown.py +571 -0
- epresso-0.1.1/src/epresso/minify.py +76 -0
- epresso-0.1.1/src/epresso/outputs.py +184 -0
- epresso-0.1.1/src/epresso/pipeline.py +72 -0
- epresso-0.1.1/src/epresso/plugins.py +378 -0
- epresso-0.1.1/src/epresso/private.py +14 -0
- epresso-0.1.1/src/epresso/pygments.py +117 -0
- epresso-0.1.1/src/epresso/render.py +236 -0
- epresso-0.1.1/src/epresso/routing.py +447 -0
- epresso-0.1.1/src/epresso/scoped.py +135 -0
- epresso-0.1.1/src/epresso/search.py +115 -0
- epresso-0.1.1/src/epresso/server.py +363 -0
- epresso-0.1.1/src/epresso/site.py +507 -0
- epresso-0.1.1/src/epresso/static/epresso.svg +1 -0
- epresso-0.1.1/src/epresso/static/favicon.ico +0 -0
- epresso-0.1.1/src/epresso/templates.py +296 -0
- epresso-0.1.1/src/epresso/themes.py +86 -0
epresso-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
dist/
|
|
11
|
+
build/
|
|
12
|
+
|
|
13
|
+
# docs/guides/build is a docs category, not a build artifact
|
|
14
|
+
!docs/guides/build/
|
|
15
|
+
!docs/guides/build/**
|
|
16
|
+
*.egg-info/
|
|
17
|
+
.eggs/
|
|
18
|
+
*.egg
|
|
19
|
+
MANIFEST
|
|
20
|
+
|
|
21
|
+
# Virtual environments
|
|
22
|
+
.venv/
|
|
23
|
+
venv/
|
|
24
|
+
env/
|
|
25
|
+
ENV/
|
|
26
|
+
|
|
27
|
+
# Test / coverage artifacts
|
|
28
|
+
.coverage
|
|
29
|
+
.coverage.*
|
|
30
|
+
htmlcov/
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.cache/
|
|
33
|
+
.coverage.xml
|
|
34
|
+
*.cover
|
|
35
|
+
|
|
36
|
+
# Type checking
|
|
37
|
+
.mypy_cache/
|
|
38
|
+
.pyright/
|
|
39
|
+
.pytype/
|
|
40
|
+
|
|
41
|
+
# Linting
|
|
42
|
+
.ruff_cache/
|
|
43
|
+
|
|
44
|
+
# Build
|
|
45
|
+
*.tar.gz
|
|
46
|
+
*.whl
|
|
47
|
+
|
|
48
|
+
# Tooling / misc
|
|
49
|
+
.DS_Store
|
|
50
|
+
*.log
|
epresso-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nikoshell
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
epresso-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: epresso
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: epresso — a modern, Python-first static site generator. Routes-as-code, content collections, deterministic + incremental builds.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: jinja2>=3.1
|
|
9
|
+
Requires-Dist: markdown-it-py>=3.0
|
|
10
|
+
Requires-Dist: pydantic>=2.7
|
|
11
|
+
Requires-Dist: pygments>=2.17
|
|
12
|
+
Requires-Dist: pyyaml>=6.0
|
|
13
|
+
Requires-Dist: starlette>=0.37
|
|
14
|
+
Requires-Dist: tinycss2>=1.5.1
|
|
15
|
+
Requires-Dist: typer>=0.12
|
|
16
|
+
Requires-Dist: uvicorn>=0.30
|
|
17
|
+
Requires-Dist: watchfiles>=0.21
|
|
18
|
+
Requires-Dist: websockets>=12.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: httpx>=0.27; extra == 'dev'
|
|
21
|
+
Requires-Dist: pillow>=10.0; extra == 'dev'
|
|
22
|
+
Requires-Dist: pyright>=1.1.360; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-snapshot; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
27
|
+
Provides-Extra: fmt
|
|
28
|
+
Requires-Dist: cssbeautifier>=1.14; extra == 'fmt'
|
|
29
|
+
Requires-Dist: djhtml>=1.4; extra == 'fmt'
|
|
30
|
+
Requires-Dist: jsbeautifier>=1.14; extra == 'fmt'
|
|
31
|
+
Requires-Dist: ruff>=0.5; extra == 'fmt'
|
|
32
|
+
Provides-Extra: images
|
|
33
|
+
Requires-Dist: pillow>=10.0; extra == 'images'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# epresso
|
|
37
|
+
|
|
38
|
+
A modern, **Python-first static site generator** — routes-as-code, content collections, deterministic + incremental builds, and a no-JavaScript-by-default philosophy.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install epresso
|
|
42
|
+
epresso new blog myblog && cd myblog
|
|
43
|
+
epresso dev # develop with live reload
|
|
44
|
+
epresso build # deterministic, incremental → dist/
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Why epresso?
|
|
48
|
+
|
|
49
|
+
- **Content and routes are separate.** `content/` is data (collections, validated with Pydantic); `pages/` is where routes live as templates that consume the data — a content-first model in Python.
|
|
50
|
+
- **Incremental from day one.** A content digest + build graph + reverse index means a content edit re-renders only the affected pages.
|
|
51
|
+
- **Deterministic builds.** Same source + config → same output (hashed filenames, stable ordering).
|
|
52
|
+
- **JS is optional.** A pure-Python site needs zero Node. esbuild/Tailwind/PostCSS are external binaries used only when you declare JS/CSS.
|
|
53
|
+
- **No arbitrary Python in templates.** Templates see a curated set of globals — safe and predictable.
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- Content collections (glob + Python/remote loaders) with **Pydantic schemas**, data collections (JSON/YAML/TOML), content references, drafts & scheduled-content exclusion.
|
|
58
|
+
- Routes-as-code: filesystem routing, `{param}` / `{...param}` dynamic routes, `get_static_paths()` sidecars, **`.ep` single-file routes** (Python frontmatter + Jinja), static endpoints (JSON/XML/text), clean URLs, redirects.
|
|
59
|
+
- **Jinja2** templates with inheritance, curated globals (`url`, `asset`, `image`, `picture`, `seo`, `get_collection`, `get_entry`, …).
|
|
60
|
+
- **`.ep` components** with strict Pydantic `Props` validation, **scoped CSS** (`<style>` blocks, `:global()` opt-out), and **client `<script>` blocks** (bundled page-level JS).
|
|
61
|
+
- Dev server (Starlette + watchfiles) with **WebSocket live reload**, sharing the production incremental engine.
|
|
62
|
+
- Assets: content-hashed `asset()`, `public/` passthrough, esbuild JS bundling, **PostCSS/Tailwind CSS** pipeline, **Pillow responsive images** (`image()` → WebP srcset).
|
|
63
|
+
- **First-class client behavior** — a `.ep` component's `<script>` block is bundled (esbuild) page-level JS, injected before `</body>`. No separate islands/ dir.
|
|
64
|
+
- Generated outputs: `sitemap.xml`, `robots.txt`, `404.html`, `search-index.json`, and an RSS/Atom helper.
|
|
65
|
+
- **Plugin API** — a capability registry: named plugins with lifecycle hooks that receive a scoped `Capabilities` handle (never the raw `Site`), so extensions are deterministic and isolated. See the [plugin guide](docs/guides/extending/plugins.md).
|
|
66
|
+
- Theme scaffolding — `epresso new docs|blog`.
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
Requires **Python 3.12+**.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install epresso # or: uv tool install epresso
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Quickstart
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
epresso new blog mysite # scaffold from the blog theme (or: epresso new docs)
|
|
80
|
+
cd mysite
|
|
81
|
+
epresso dev # http://127.0.0.1:4321 with live reload (next free port if busy)
|
|
82
|
+
epresso build # deterministic + incremental build → dist/
|
|
83
|
+
epresso preview # build then serve dist/ (production preview)
|
|
84
|
+
epresso check # validate config + content, list routes
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A blank project:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
epresso init mysite && cd mysite
|
|
91
|
+
epresso build
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Project layout
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
site.toml # configuration (TOML)
|
|
98
|
+
content.config.py # define collections + schemas (Pydantic)
|
|
99
|
+
content/ # data — collections (markdown / JSON / YAML / TOML)
|
|
100
|
+
pages/ # routes (.ep, .md, or .py endpoints)
|
|
101
|
+
layouts/ # layout templates (.ep)
|
|
102
|
+
components/ # reusable components (.ep), grouped into subdirs
|
|
103
|
+
ui/ # presentational building blocks
|
|
104
|
+
layout/ # page/site structure (Header, Nav, Footer, Search)
|
|
105
|
+
sections/ # visual page regions
|
|
106
|
+
behavior/ # client-side enhancement / interaction
|
|
107
|
+
features/ # business-feature components
|
|
108
|
+
styles/ # global stylesheets (CSS)
|
|
109
|
+
assets/ # buildable assets (images, js); top-level files land at root
|
|
110
|
+
public/ # files copied verbatim to the output root
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
> Back-compat: a legacy `templates/` root (with `components/` + `layouts/` subdirs) is
|
|
114
|
+
> still loaded when present.
|
|
115
|
+
|
|
116
|
+
## Configuration (`site.toml`)
|
|
117
|
+
|
|
118
|
+
### Environments
|
|
119
|
+
|
|
120
|
+
epresso supports per-environment configuration with a
|
|
121
|
+
`.env.development` / `.env.preview` / `.env.production` split:
|
|
122
|
+
|
|
123
|
+
* `site.<env>.toml` — deep-merged over `site.toml` (per-env URL, toggles, …)
|
|
124
|
+
* `.env.<env>` — dotenv-style vars (`EP_SESSIONS_API=...`) exposed to content
|
|
125
|
+
loaders/plugins via `os.environ` and to templates via `env_vars`.
|
|
126
|
+
|
|
127
|
+
Select with `epresso build --env <name>` (or `EPRESSO_ENV`); `epresso dev` defaults to
|
|
128
|
+
`development`, `epresso build`/`preview`/`check` to `production`. In templates,
|
|
129
|
+
`{{ env }}` is the active env name and `{{ env_vars.KEY }}` any env var.
|
|
130
|
+
|
|
131
|
+
```toml
|
|
132
|
+
[site]
|
|
133
|
+
name = "My Site"
|
|
134
|
+
url = "https://example.com"
|
|
135
|
+
language = "en"
|
|
136
|
+
|
|
137
|
+
[build]
|
|
138
|
+
output = "dist"
|
|
139
|
+
trailing_slash = "always" # always | never
|
|
140
|
+
|
|
141
|
+
[assets]
|
|
142
|
+
css = ["css/main.css"] # entry points (esbuild / postcss / tailwind)
|
|
143
|
+
js = ["js/app.js"]
|
|
144
|
+
|
|
145
|
+
[seo]
|
|
146
|
+
sitemap = true
|
|
147
|
+
robots = true
|
|
148
|
+
|
|
149
|
+
[search]
|
|
150
|
+
enabled = true
|
|
151
|
+
index = "search-index.json"
|
|
152
|
+
|
|
153
|
+
plugins = ["mypkg:MyPlugin"] # dotted-path plugin specs
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Content collections
|
|
157
|
+
|
|
158
|
+
Define collections in `content.config.py`:
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
from pydantic import BaseModel
|
|
162
|
+
from epresso.content import define_collection, reference
|
|
163
|
+
|
|
164
|
+
class Post(BaseModel):
|
|
165
|
+
title: str
|
|
166
|
+
date: str
|
|
167
|
+
tags: list[str] = []
|
|
168
|
+
author: reference("authors") # content reference
|
|
169
|
+
draft: bool = False
|
|
170
|
+
|
|
171
|
+
posts = define_collection("posts", glob="*.md", base="./content/posts", schema=Post)
|
|
172
|
+
authors = define_collection("authors", loader=fetch_authors) # remote/derived
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Fenced code blocks in Markdown are syntax-highlighted with **Pygments** by
|
|
176
|
+
default (``markdown.highlight = false`` opts out). Highlighted blocks use
|
|
177
|
+
`<code class="language-<lang>">` with token spans; include the generated CSS in
|
|
178
|
+
your layout:
|
|
179
|
+
|
|
180
|
+
```jinja
|
|
181
|
+
<style>{{ pygments_css() }}</style>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
A markdown post with YAML front matter:
|
|
185
|
+
|
|
186
|
+
```markdown
|
|
187
|
+
---
|
|
188
|
+
title: Hello
|
|
189
|
+
date: 2026-01-01
|
|
190
|
+
tags: [python]
|
|
191
|
+
---
|
|
192
|
+
# Hello
|
|
193
|
+
|
|
194
|
+
Your **content** here.
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`get_collection("posts")`, `get_entry("posts", id)`, and `get_entries(...)` resolve data in templates and in `get_static_paths()`. In production builds, `draft: true` and future-dated entries are automatically excluded.
|
|
198
|
+
|
|
199
|
+
## Routing (routes-as-code)
|
|
200
|
+
|
|
201
|
+
`content/` is data; `pages/` produces URLs. Three kinds of route:
|
|
202
|
+
|
|
203
|
+
1. **Direct Markdown page** — `pages/about.md` → `/about/` (layout via front matter).
|
|
204
|
+
2. **`.ep` single-file route** — `pages/blog/[slug].ep` with Python frontmatter + Jinja body (recommended):
|
|
205
|
+
|
|
206
|
+
```epresso
|
|
207
|
+
---
|
|
208
|
+
from epresso.routing import Route
|
|
209
|
+
|
|
210
|
+
def get_static_paths():
|
|
211
|
+
return [Route(path=f"/blog/{p.id}/", params={"slug": p.id}, data=p)
|
|
212
|
+
for p in site.get_collection("posts")]
|
|
213
|
+
---
|
|
214
|
+
{% extends 'base.html' %}
|
|
215
|
+
{% block title %}{{ props.title }}{% endblock %}
|
|
216
|
+
{% block content %}<h1>{{ props.title }}</h1>{{ content|safe }}{% endblock %}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The `--- … ---` block is **Python** (run with `site` injected); its variables are
|
|
220
|
+
exposed to the template. A static `.ep` route needs no
|
|
221
|
+
`get_static_paths()`.
|
|
222
|
+
|
|
223
|
+
3. **Template + sidecar** — `pages/blog/[slug].html` + `pages/blog/[slug].py` (legacy):
|
|
224
|
+
|
|
225
|
+
```python
|
|
226
|
+
from epresso.routing import Route
|
|
227
|
+
|
|
228
|
+
def get_static_paths():
|
|
229
|
+
return [Route(path=f"/blog/{p.id}/", params={"slug": p.id}, data=p)
|
|
230
|
+
for p in site.get_collection("posts")]
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
4. **Static endpoint** — `pages/robots.txt.py` exporting `get()`:
|
|
234
|
+
|
|
235
|
+
```python
|
|
236
|
+
def get():
|
|
237
|
+
return "text/plain", "User-agent: *\nAllow: /\n"
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Pagination is a helper, not magic:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from epresso.routing import paginate
|
|
244
|
+
|
|
245
|
+
def get_static_paths():
|
|
246
|
+
return paginate(site.get_collection("posts"), per_page=10, base_path="/blog/")
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Redirects (Option C)
|
|
250
|
+
|
|
251
|
+
Redirects are configured in `site.toml` and emitted as routes in the build graph
|
|
252
|
+
(so they participate in incremental builds). Targets may be a string (permanent
|
|
253
|
+
301) or a `{destination, status}` dict for 302:
|
|
254
|
+
|
|
255
|
+
```toml
|
|
256
|
+
[[redirects]]
|
|
257
|
+
"/old-home/" = "/"
|
|
258
|
+
|
|
259
|
+
[[redirects]]
|
|
260
|
+
"/legacy/" = { destination = "/new/", status = 302 }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Each emits a browser-safe meta-refresh page under `dist/<from>/index.html`;
|
|
264
|
+
`data-epresso-status` reflects the configured HTTP status for edge/host rewrites.
|
|
265
|
+
Set `[build] redirects = false` to disable. Invalid targets (missing
|
|
266
|
+
`destination`) are rejected at load time.
|
|
267
|
+
|
|
268
|
+
## The `.ep` file format
|
|
269
|
+
|
|
270
|
+
`.ep` files unify routes, layouts, and components into a single file:
|
|
271
|
+
**Python frontmatter** (`--- … ---`) + **Jinja body**. Content stays in
|
|
272
|
+
Markdown; data stays in YAML/JSON/TOML.
|
|
273
|
+
|
|
274
|
+
### Components with typed props
|
|
275
|
+
|
|
276
|
+
`components/Card.ep` validates props against a strict Pydantic model
|
|
277
|
+
before rendering — no unvalidated kwargs reach the template:
|
|
278
|
+
|
|
279
|
+
```epresso
|
|
280
|
+
---
|
|
281
|
+
from pydantic import BaseModel
|
|
282
|
+
|
|
283
|
+
class Props(BaseModel):
|
|
284
|
+
title: str
|
|
285
|
+
level: int = 3
|
|
286
|
+
---
|
|
287
|
+
<div class="card"><h{{ props.level }}>{{ props.title }}</h{{ props.level }}>
|
|
288
|
+
{{ content }}</div>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Used from any template with the `{% component %}` tag:
|
|
292
|
+
|
|
293
|
+
```jinja
|
|
294
|
+
{% component "Card", title="Hi", level=2 %}Body text{% endcomponent %}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### JSX-style component tags
|
|
298
|
+
|
|
299
|
+
Components can also be written with HTML/JSX-like syntax — epresso rewrites these
|
|
300
|
+
to `{% component %}` blocks before parsing, so you don't need the tag at all:
|
|
301
|
+
|
|
302
|
+
```jinja
|
|
303
|
+
<Header />
|
|
304
|
+
|
|
305
|
+
<main>
|
|
306
|
+
<Hero />
|
|
307
|
+
<FeatureSection layout="three-column" count={items|length} />
|
|
308
|
+
<Card title="Hi">Body <strong>text</strong></Card>
|
|
309
|
+
</main>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
* Only tags that resolve to a registered component are converted — `<main>`,
|
|
313
|
+
`<div>`, `<section>` etc. pass through as plain HTML.
|
|
314
|
+
* Attributes are JSX-style: `key="value"` / `key='value'` (string), `key={expr}`
|
|
315
|
+
(expression), or a bare `key` (boolean `true`).
|
|
316
|
+
* Paired components take children: `<Card>…</Card>` renders `…` as the
|
|
317
|
+
component's `content`, and children may themselves contain nested components.
|
|
318
|
+
* The legacy `{% component %}` tag still works and can be mixed freely.
|
|
319
|
+
|
|
320
|
+
### Scoped CSS
|
|
321
|
+
|
|
322
|
+
A `<style>` block in a `.ep` file is extracted, scoped to the component/route's
|
|
323
|
+
output (a `data-epresso-<hash>` attribute), and linked from the head:
|
|
324
|
+
|
|
325
|
+
```epresso
|
|
326
|
+
---
|
|
327
|
+
---
|
|
328
|
+
<style>
|
|
329
|
+
.card { border: 1px solid #ccc; }
|
|
330
|
+
:global(.reset) { margin: 0; } /* opt out of scoping */
|
|
331
|
+
</style>
|
|
332
|
+
<div class="card">{{ content }}</div>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The scoped CSS is written to `dist/_scoped/epresso-<hash>.css` and a `<link>` is
|
|
336
|
+
injected into any page that uses it. Legacy `.html` components and `.html`+`.py`
|
|
337
|
+
routes continue to work unchanged.
|
|
338
|
+
|
|
339
|
+
#### Layout components are automatically unscoped
|
|
340
|
+
|
|
341
|
+
A `.ep` component is **scoped only when it contains a scoped `<style>` block**:
|
|
342
|
+
it is then wrapped in a `data-epresso-*` div and its selectors are rewritten. A
|
|
343
|
+
component with no scoped CSS — or only `<style is:global>` — renders **unscoped**
|
|
344
|
+
(no wrapper). So a layout shell that emits a full `<!doctype html>` document is
|
|
345
|
+
automatically unscoped, and any layout `<style>` is made global with
|
|
346
|
+
`<style is:global>` or by linking a global stylesheet —
|
|
347
|
+
a layout is just a component whose body renders `{{ content }}` (the default
|
|
348
|
+
slot):
|
|
349
|
+
|
|
350
|
+
```epresso
|
|
351
|
+
---
|
|
352
|
+
---
|
|
353
|
+
<!doctype html><html><head><title>{{ title }}</title></head>
|
|
354
|
+
<body>{{ content }}</body></html>
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
```jinja
|
|
358
|
+
<BaseLayout title={title}>
|
|
359
|
+
<Header />
|
|
360
|
+
<main>…</main>
|
|
361
|
+
<Footer />
|
|
362
|
+
</BaseLayout>
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Layout components may live in `layouts/` (resolved as a component in
|
|
366
|
+
addition to `components/`), so `BaseLayout` can stay alongside your
|
|
367
|
+
layouts while being composed as a component.
|
|
368
|
+
|
|
369
|
+
### Slots
|
|
370
|
+
|
|
371
|
+
Components get a **default slot** (`{{ content }}`, the children between the
|
|
372
|
+
open/close tags) plus **named slots** mirroring `<slot name=…>`: a child
|
|
373
|
+
`<Fragment slot="name">…</Fragment>` contributes to `slots["name"]` and is
|
|
374
|
+
removed from the default `content`.
|
|
375
|
+
|
|
376
|
+
```jinja
|
|
377
|
+
<Card title="Hi">
|
|
378
|
+
<Fragment slot="header">Header content</Fragment>
|
|
379
|
+
Default body content
|
|
380
|
+
</Card>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
```epresso
|
|
384
|
+
---
|
|
385
|
+
---
|
|
386
|
+
<div class="card"><h3>{{ props.title }}</h3><header>{{ slot('header') }}</header>{{ content }}</div>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Use `{{ slot('name') or 'fallback' }}` for slot fallback content. Works for both
|
|
390
|
+
`.ep` and `.html` components.
|
|
391
|
+
|
|
392
|
+
### Client scripts
|
|
393
|
+
|
|
394
|
+
A `<script>` block in a `.ep` file is extracted, bundled with esbuild (falling
|
|
395
|
+
back to a raw module when esbuild isn't installed — JS stays optional), and
|
|
396
|
+
loaded on any page that uses the route/component:
|
|
397
|
+
|
|
398
|
+
```epresso
|
|
399
|
+
---
|
|
400
|
+
---
|
|
401
|
+
<style>.count { color: red; }</style>
|
|
402
|
+
<script>
|
|
403
|
+
document.querySelector('.count').addEventListener('click', () => alert('hi'));
|
|
404
|
+
</script>
|
|
405
|
+
<button class="count">{{ content }}</button>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
The bundle is written to `dist/_epresso/scripts/<hash>.js` and a
|
|
409
|
+
`<script type="module" src="/_epresso/scripts/<hash>.js">` tag is injected before
|
|
410
|
+
`</body>` on the pages that use it. Identical script blocks dedupe to one file.
|
|
411
|
+
|
|
412
|
+
So a single `.ep` file bundles: **Python frontmatter
|
|
413
|
+
(template logic) + Jinja body (markup) + `<style>` (scoped CSS) + `<script>`
|
|
414
|
+
(page-level client JS)**.
|
|
415
|
+
|
|
416
|
+
## Templates
|
|
417
|
+
|
|
418
|
+
Jinja2 with curated globals — no arbitrary Python:
|
|
419
|
+
|
|
420
|
+
```jinja
|
|
421
|
+
{% extends "layouts/base.html" %}
|
|
422
|
+
{% block title %}{{ props.title }}{% endblock %}
|
|
423
|
+
{% block content %}
|
|
424
|
+
{{ seo(title=props.title, path=route.path) }}
|
|
425
|
+
<article>
|
|
426
|
+
<h1>{{ props.title }}</h1>
|
|
427
|
+
<img src="{{ image('photos/hero.jpg', widths=[400,800,1200], alt='Hero') }}">
|
|
428
|
+
{{ content | safe }}
|
|
429
|
+
</article>
|
|
430
|
+
{% endblock %}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Globals: `site`, `url()`, `asset()`, `image()`, `seo()`, `get_collection()`, `get_entry()`, `route`, `params`, `props`, `content`.
|
|
434
|
+
|
|
435
|
+
## Plugins
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
from epresso.plugins import Plugin
|
|
439
|
+
|
|
440
|
+
def greeter(*, text="hello"):
|
|
441
|
+
def on_setup(caps):
|
|
442
|
+
caps.add_global("greeting", lambda: text)
|
|
443
|
+
return Plugin(name="greeter", hooks={"on_setup": on_setup})
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Plugins are **deduplicated by name** and run in **`priority` order**; each hook
|
|
447
|
+
receives a narrow `Capabilities` handle rather than the `Site`, and can add
|
|
448
|
+
template globals/filters, register content collections & Markdown extensions,
|
|
449
|
+
and transform rendered HTML. Enable them from a project `plugins.py` (build with
|
|
450
|
+
options via a factory) or `[plugins]` dotted paths in `site.toml`. Lifecycle
|
|
451
|
+
hooks: `before_load`, `on_setup`, `after_load`, `before_build`, `after_build`,
|
|
452
|
+
`on_assets`. See `examples/plugins/` and the [plugin guide](docs/guides/extending/plugins.md).
|
|
453
|
+
|
|
454
|
+
## Themes
|
|
455
|
+
|
|
456
|
+
```bash
|
|
457
|
+
epresso new docs mydocs # docs theme (sidebar nav, ToC-friendly)
|
|
458
|
+
epresso new blog myblog # blog theme (posts, index, RSS feed)
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Themes are git-cloned from their own repos and given to you as a starter project you fully own.
|
|
462
|
+
|
|
463
|
+
## CLI
|
|
464
|
+
|
|
465
|
+
| Command | Description |
|
|
466
|
+
|---|---|
|
|
467
|
+
| `epresso init` | Scaffold a blank project |
|
|
468
|
+
| `epresso new <theme> <dest>` | Scaffold from a theme (docs/blog) |
|
|
469
|
+
| `epresso dev` | Development server with live reload |
|
|
470
|
+
| `epresso build` | Deterministic + incremental production build |
|
|
471
|
+
| `epresso preview` | Build then serve `dist/` (production preview) |
|
|
472
|
+
| `epresso docs` | Build + serve the documentation (port 4321) |
|
|
473
|
+
| `epresso clean` | Remove `dist/` and the build cache |
|
|
474
|
+
| `epresso check` | Validate config + content, list routes |
|
|
475
|
+
| `epresso version` | Print the version |
|
|
476
|
+
|
|
477
|
+
## Development
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
git clone https://github.com/epresso-ssg/epresso
|
|
481
|
+
cd epresso
|
|
482
|
+
uv sync --extra dev
|
|
483
|
+
uv run pytest # 276 tests
|
|
484
|
+
uv run ruff check .
|
|
485
|
+
uv run pyright src/epresso
|
|
486
|
+
uv run pytest --cov=epresso
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### Run the bundled themes
|
|
490
|
+
|
|
491
|
+
The repo ships runnable theme projects under `themes/` (`basic`, `blog`,
|
|
492
|
+
`docs`, `website`). From the repo root, point any epresso command at one with
|
|
493
|
+
`--project .`:
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
uv run --project . epresso dev themes/basic # dev server with live reload
|
|
497
|
+
uv run --project . epresso build themes/basic # build → themes/basic/dist
|
|
498
|
+
uv run --project . epresso check themes/basic # validate config + content
|
|
499
|
+
uv run --project . epresso preview themes/basic # build + serve dist/
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
The `website` example is the most feature-rich — it exercises `.ep` components
|
|
503
|
+
and scoped CSS.
|
|
504
|
+
|
|
505
|
+
## Editor support
|
|
506
|
+
|
|
507
|
+
Neovim / Vim syntax highlighting for `.ep` files (Python frontmatter +
|
|
508
|
+
Jinja2 body) ships in `extras/nvim/`. Add it to your runtimepath once and every
|
|
509
|
+
epresso project is highlighted automatically:
|
|
510
|
+
|
|
511
|
+
```lua
|
|
512
|
+
-- ~/.config/nvim/init.lua
|
|
513
|
+
vim.opt.rtp:append("/path/to/epresso/extras/nvim")
|
|
514
|
+
```
|
|
515
|
+
|
|
516
|
+
See [`extras/nvim/README.md`](extras/nvim/README.md) for details and LazyVim
|
|
517
|
+
instructions.
|
|
518
|
+
|
|
519
|
+
## License
|
|
520
|
+
|
|
521
|
+
MIT
|