jamstack 2.0.0rc1__py3-none-any.whl
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.
- jamstack/__about__.py +1 -0
- jamstack/__init__.py +3 -0
- jamstack/__main__.py +4 -0
- jamstack/builder.py +70 -0
- jamstack/cli.py +113 -0
- jamstack/config.py +60 -0
- jamstack/content.py +131 -0
- jamstack/feed.py +75 -0
- jamstack/render.py +12 -0
- jamstack/server.py +92 -0
- jamstack/theme.py +84 -0
- jamstack/themes/blog/scaffold/content/posts/hello-world.md +9 -0
- jamstack/themes/blog/static/style.css +186 -0
- jamstack/themes/blog/templates/404.html +15 -0
- jamstack/themes/blog/templates/base.html +22 -0
- jamstack/themes/blog/templates/index.html +21 -0
- jamstack/themes/blog/templates/page.html +3 -0
- jamstack/themes/blog/templates/post.html +16 -0
- jamstack/themes/blog/theme.toml +2 -0
- jamstack/themes/minimal/static/style.css +137 -0
- jamstack/themes/minimal/templates/404.html +11 -0
- jamstack/themes/minimal/templates/base.html +21 -0
- jamstack/themes/minimal/templates/index.html +17 -0
- jamstack/themes/minimal/templates/page.html +11 -0
- jamstack/themes/minimal/theme.toml +2 -0
- jamstack-2.0.0rc1.dist-info/METADATA +226 -0
- jamstack-2.0.0rc1.dist-info/RECORD +30 -0
- jamstack-2.0.0rc1.dist-info/WHEEL +4 -0
- jamstack-2.0.0rc1.dist-info/entry_points.txt +2 -0
- jamstack-2.0.0rc1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
--text: #1f1c19;
|
|
4
|
+
--muted: #756e66;
|
|
5
|
+
--background: #fbf9f6;
|
|
6
|
+
--accent: #0f5e59;
|
|
7
|
+
--rule: #e3ddd4;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@media (prefers-color-scheme: dark) {
|
|
11
|
+
:root {
|
|
12
|
+
--text: #ece7e0;
|
|
13
|
+
--muted: #a49c92;
|
|
14
|
+
--background: #16130f;
|
|
15
|
+
--accent: #7cc4bd;
|
|
16
|
+
--rule: #322c25;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
* {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
background: var(--background);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
font: 1.125rem/1.75 "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
|
|
29
|
+
text-rendering: optimizeLegibility;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.masthead,
|
|
33
|
+
.content,
|
|
34
|
+
.colophon {
|
|
35
|
+
max-width: 40rem;
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
padding: 0 1.25rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.masthead {
|
|
41
|
+
padding-top: 4rem;
|
|
42
|
+
padding-bottom: 2.5rem;
|
|
43
|
+
text-align: center;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.logo {
|
|
47
|
+
color: var(--text);
|
|
48
|
+
font-size: 1.75rem;
|
|
49
|
+
font-weight: 600;
|
|
50
|
+
letter-spacing: -0.02em;
|
|
51
|
+
text-decoration: none;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.tagline {
|
|
55
|
+
margin: 0.5rem 0 0;
|
|
56
|
+
color: var(--muted);
|
|
57
|
+
font-size: 0.95rem;
|
|
58
|
+
font-style: italic;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.content {
|
|
62
|
+
padding-bottom: 4rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
a {
|
|
66
|
+
color: var(--accent);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
h1,
|
|
70
|
+
h2,
|
|
71
|
+
h3 {
|
|
72
|
+
line-height: 1.2;
|
|
73
|
+
letter-spacing: -0.02em;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
h1 {
|
|
77
|
+
margin: 0 0 0.5rem;
|
|
78
|
+
font-size: 2.25rem;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
time {
|
|
82
|
+
color: var(--muted);
|
|
83
|
+
font-size: 0.8rem;
|
|
84
|
+
letter-spacing: 0.06em;
|
|
85
|
+
text-transform: uppercase;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.post-list {
|
|
89
|
+
margin: 0;
|
|
90
|
+
padding: 0;
|
|
91
|
+
list-style: none;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.post-list li {
|
|
95
|
+
padding: 1.75rem 0;
|
|
96
|
+
border-top: 1px solid var(--rule);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.post-list li:first-child {
|
|
100
|
+
border-top: none;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.post-title {
|
|
104
|
+
display: block;
|
|
105
|
+
color: var(--text);
|
|
106
|
+
font-size: 1.4rem;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
text-decoration: none;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.post-title:hover {
|
|
112
|
+
color: var(--accent);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.summary {
|
|
116
|
+
margin: 0.4rem 0 0;
|
|
117
|
+
color: var(--muted);
|
|
118
|
+
font-size: 0.95rem;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.post-header {
|
|
122
|
+
padding-bottom: 1.5rem;
|
|
123
|
+
border-bottom: 1px solid var(--rule);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.prose {
|
|
127
|
+
padding-top: 1.5rem;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.prose p {
|
|
131
|
+
margin: 0 0 1.25em;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.prose blockquote {
|
|
135
|
+
margin: 1.5rem 0;
|
|
136
|
+
padding-left: 1.25rem;
|
|
137
|
+
border-left: 2px solid var(--rule);
|
|
138
|
+
color: var(--muted);
|
|
139
|
+
font-style: italic;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pre,
|
|
143
|
+
code {
|
|
144
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
145
|
+
font-size: 0.85em;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
pre {
|
|
149
|
+
padding: 1rem;
|
|
150
|
+
overflow-x: auto;
|
|
151
|
+
background: color-mix(in srgb, var(--text) 6%, transparent);
|
|
152
|
+
border-radius: 6px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
code {
|
|
156
|
+
padding: 0.1em 0.3em;
|
|
157
|
+
background: color-mix(in srgb, var(--text) 7%, transparent);
|
|
158
|
+
border-radius: 3px;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pre code {
|
|
162
|
+
padding: 0;
|
|
163
|
+
background: none;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
img {
|
|
167
|
+
max-width: 100%;
|
|
168
|
+
height: auto;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.back {
|
|
172
|
+
margin-top: 3rem;
|
|
173
|
+
font-size: 0.9rem;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.empty {
|
|
177
|
+
color: var(--muted);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.colophon {
|
|
181
|
+
padding-top: 1.25rem;
|
|
182
|
+
padding-bottom: 3rem;
|
|
183
|
+
border-top: 1px solid var(--rule);
|
|
184
|
+
color: var(--muted);
|
|
185
|
+
font-size: 0.85rem;
|
|
186
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Page not found · {{ site.title }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<article class="post">
|
|
7
|
+
<header class="post-header">
|
|
8
|
+
<h1>Page not found</h1>
|
|
9
|
+
</header>
|
|
10
|
+
<div class="prose">
|
|
11
|
+
<p>There is nothing at this address.</p>
|
|
12
|
+
<p><a href="/">Back to the home page</a></p>
|
|
13
|
+
</div>
|
|
14
|
+
</article>
|
|
15
|
+
{% endblock %}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="{{ site.language }}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{% block title %}{{ site.title }}{% endblock %}</title>
|
|
7
|
+
{% if site.description %}<meta name="description" content="{{ site.description }}">{% endif %}
|
|
8
|
+
<link rel="stylesheet" href="/style.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header class="masthead">
|
|
12
|
+
<a class="logo" href="/">{{ site.title }}</a>
|
|
13
|
+
{% if site.description %}<p class="tagline">{{ site.description }}</p>{% endif %}
|
|
14
|
+
</header>
|
|
15
|
+
<main class="content">
|
|
16
|
+
{% block content %}{% endblock %}
|
|
17
|
+
</main>
|
|
18
|
+
<footer class="colophon">
|
|
19
|
+
<p>{{ site.title }}</p>
|
|
20
|
+
</footer>
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
{% if page.content %}
|
|
5
|
+
<div class="intro">{{ page.content }}</div>
|
|
6
|
+
{% endif %}
|
|
7
|
+
{% set posts = pages | rejectattr("url", "equalto", "/") | selectattr("date") | list %}
|
|
8
|
+
{% if posts %}
|
|
9
|
+
<ol class="post-list">
|
|
10
|
+
{% for item in posts %}
|
|
11
|
+
<li>
|
|
12
|
+
<a class="post-title" href="{{ item.url }}">{{ item.title }}</a>
|
|
13
|
+
<time datetime="{{ item.date.isoformat() }}">{{ item.date.strftime("%d-%m-%Y") }}</time>
|
|
14
|
+
{% if item.meta.get("summary") %}<p class="summary">{{ item.meta.get("summary") }}</p>{% endif %}
|
|
15
|
+
</li>
|
|
16
|
+
{% endfor %}
|
|
17
|
+
</ol>
|
|
18
|
+
{% else %}
|
|
19
|
+
<p class="empty">No posts yet. Create one in <code>content/posts/</code>.</p>
|
|
20
|
+
{% endif %}
|
|
21
|
+
{% endblock %}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ page.title }} · {{ site.title }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<article class="post">
|
|
7
|
+
<header class="post-header">
|
|
8
|
+
<h1>{{ page.title }}</h1>
|
|
9
|
+
{% if page.date %}<time datetime="{{ page.date.isoformat() }}">{{ page.date.strftime("%d-%m-%Y") }}</time>{% endif %}
|
|
10
|
+
</header>
|
|
11
|
+
<div class="prose">{{ page.content }}</div>
|
|
12
|
+
{% block post_footer %}
|
|
13
|
+
<p class="back"><a href="/">← All posts</a></p>
|
|
14
|
+
{% endblock %}
|
|
15
|
+
</article>
|
|
16
|
+
{% endblock %}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
color-scheme: light dark;
|
|
3
|
+
--text: #1a1a1a;
|
|
4
|
+
--muted: #6b6b6b;
|
|
5
|
+
--background: #fdfdfc;
|
|
6
|
+
--accent: #2563eb;
|
|
7
|
+
--rule: #e5e5e2;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
@media (prefers-color-scheme: dark) {
|
|
11
|
+
:root {
|
|
12
|
+
--text: #e8e8e6;
|
|
13
|
+
--muted: #a0a0a0;
|
|
14
|
+
--background: #131313;
|
|
15
|
+
--accent: #7aa2f7;
|
|
16
|
+
--rule: #2a2a2a;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
* {
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
body {
|
|
25
|
+
margin: 0;
|
|
26
|
+
background: var(--background);
|
|
27
|
+
color: var(--text);
|
|
28
|
+
font: 1.0625rem/1.7 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.site-header,
|
|
32
|
+
.site-main,
|
|
33
|
+
.site-footer {
|
|
34
|
+
max-width: 42rem;
|
|
35
|
+
margin: 0 auto;
|
|
36
|
+
padding: 0 1.25rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.site-header {
|
|
40
|
+
padding-top: 2.5rem;
|
|
41
|
+
padding-bottom: 1rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.site-title {
|
|
45
|
+
color: var(--text);
|
|
46
|
+
font-size: 1.125rem;
|
|
47
|
+
font-weight: 700;
|
|
48
|
+
letter-spacing: -0.01em;
|
|
49
|
+
text-decoration: none;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.site-main {
|
|
53
|
+
padding-bottom: 4rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
h1,
|
|
57
|
+
h2,
|
|
58
|
+
h3 {
|
|
59
|
+
line-height: 1.25;
|
|
60
|
+
letter-spacing: -0.02em;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
h1 {
|
|
64
|
+
font-size: 2rem;
|
|
65
|
+
margin: 1.5rem 0 0.75rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
a {
|
|
69
|
+
color: var(--accent);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
time {
|
|
73
|
+
color: var(--muted);
|
|
74
|
+
font-size: 0.875rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.page-list {
|
|
78
|
+
list-style: none;
|
|
79
|
+
margin: 2rem 0 0;
|
|
80
|
+
padding: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.page-list li {
|
|
84
|
+
display: flex;
|
|
85
|
+
justify-content: space-between;
|
|
86
|
+
gap: 1rem;
|
|
87
|
+
padding: 0.6rem 0;
|
|
88
|
+
border-top: 1px solid var(--rule);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.page-list a {
|
|
92
|
+
text-decoration: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
pre,
|
|
96
|
+
code {
|
|
97
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
98
|
+
font-size: 0.9em;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pre {
|
|
102
|
+
padding: 1rem;
|
|
103
|
+
overflow-x: auto;
|
|
104
|
+
background: color-mix(in srgb, var(--text) 6%, transparent);
|
|
105
|
+
border-radius: 8px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
code {
|
|
109
|
+
padding: 0.1em 0.3em;
|
|
110
|
+
background: color-mix(in srgb, var(--text) 7%, transparent);
|
|
111
|
+
border-radius: 4px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pre code {
|
|
115
|
+
padding: 0;
|
|
116
|
+
background: none;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
blockquote {
|
|
120
|
+
margin: 1.5rem 0;
|
|
121
|
+
padding-left: 1rem;
|
|
122
|
+
border-left: 3px solid var(--rule);
|
|
123
|
+
color: var(--muted);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
img {
|
|
127
|
+
max-width: 100%;
|
|
128
|
+
height: auto;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.site-footer {
|
|
132
|
+
padding-top: 1rem;
|
|
133
|
+
padding-bottom: 2.5rem;
|
|
134
|
+
border-top: 1px solid var(--rule);
|
|
135
|
+
color: var(--muted);
|
|
136
|
+
font-size: 0.875rem;
|
|
137
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}Page not found · {{ site.title }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<article>
|
|
7
|
+
<h1>Page not found</h1>
|
|
8
|
+
<p>There is nothing at this address.</p>
|
|
9
|
+
<p><a href="/">Back to the home page</a></p>
|
|
10
|
+
</article>
|
|
11
|
+
{% endblock %}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="{{ site.language }}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{% block title %}{{ site.title }}{% endblock %}</title>
|
|
7
|
+
{% if site.description %}<meta name="description" content="{{ site.description }}">{% endif %}
|
|
8
|
+
<link rel="stylesheet" href="/style.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header class="site-header">
|
|
12
|
+
<a class="site-title" href="/">{{ site.title }}</a>
|
|
13
|
+
</header>
|
|
14
|
+
<main class="site-main">
|
|
15
|
+
{% block content %}{% endblock %}
|
|
16
|
+
</main>
|
|
17
|
+
<footer class="site-footer">
|
|
18
|
+
<p>{{ site.title }}{% if site.description %} — {{ site.description }}{% endif %}</p>
|
|
19
|
+
</footer>
|
|
20
|
+
</body>
|
|
21
|
+
</html>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<h1>{{ page.title }}</h1>
|
|
5
|
+
{{ page.content }}
|
|
6
|
+
{% set listed = pages | rejectattr("url", "equalto", "/") | list %}
|
|
7
|
+
{% if listed %}
|
|
8
|
+
<ul class="page-list">
|
|
9
|
+
{% for item in listed %}
|
|
10
|
+
<li>
|
|
11
|
+
<a href="{{ item.url }}">{{ item.title }}</a>
|
|
12
|
+
{% if item.date %}<time datetime="{{ item.date.isoformat() }}">{{ item.date.strftime("%d-%m-%Y") }}</time>{% endif %}
|
|
13
|
+
</li>
|
|
14
|
+
{% endfor %}
|
|
15
|
+
</ul>
|
|
16
|
+
{% endif %}
|
|
17
|
+
{% endblock %}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}{{ page.title }} · {{ site.title }}{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<article>
|
|
7
|
+
<h1>{{ page.title }}</h1>
|
|
8
|
+
{% if page.date %}<time datetime="{{ page.date.isoformat() }}">{{ page.date.strftime("%d-%m-%Y") }}</time>{% endif %}
|
|
9
|
+
{{ page.content }}
|
|
10
|
+
</article>
|
|
11
|
+
{% endblock %}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jamstack
|
|
3
|
+
Version: 2.0.0rc1
|
|
4
|
+
Summary: Static site generator built on Python and Jinja
|
|
5
|
+
Project-URL: Homepage, https://github.com/jamstackpy/jamstack
|
|
6
|
+
Project-URL: Source, https://github.com/jamstackpy/jamstack
|
|
7
|
+
Project-URL: Issues, https://github.com/jamstackpy/jamstack/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/jamstackpy/jamstack/blob/stable/CHANGELOG.md
|
|
9
|
+
Author-email: Abdur-Rahmaan Janhangeer <arj.python@gmail.com>, Pedro Torcatt <pedrotorcattsoto@gmail.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: jinja,python,ssg,static site generator
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: click>=8.1
|
|
25
|
+
Requires-Dist: jinja2>=3.1
|
|
26
|
+
Requires-Dist: markdown-it-py>=3.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: commitizen>=4.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pre-commit>=3.7; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: ruff>=0.17; extra == 'dev'
|
|
33
|
+
Requires-Dist: zensical>=0.1; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
<img alt="Jamstack logo" src="https://i.imgur.com/sXUAdYJ.png" height="217" />
|
|
38
|
+
|
|
39
|
+
<p>A small static site generator built on Python and Jinja.</p>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
[](https://github.com/jamstackpy/jamstack/actions/workflows/ci.yml)
|
|
43
|
+

|
|
44
|
+
|
|
45
|
+
> **2.0 is under development.** This branch contains the rewrite and is not
|
|
46
|
+
> published on PyPI yet. The `stable` branch holds the current 1.x release.
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
The full documentation lives in [`docs/`](docs/) and is built with Zensical.
|
|
51
|
+
Preview it locally with `zensical serve`; it is published at
|
|
52
|
+
<https://jamstackpy.github.io/>.
|
|
53
|
+
|
|
54
|
+
Jamstack turns a folder of Markdown and Jinja templates into a static site.
|
|
55
|
+
There is no database, no runtime, and no network access during a build: you
|
|
56
|
+
write content, run one command, and deploy the generated `dist/` folder
|
|
57
|
+
anywhere.
|
|
58
|
+
|
|
59
|
+
## Features
|
|
60
|
+
|
|
61
|
+
- Markdown pages with TOML front matter, or raw Jinja templates when you need
|
|
62
|
+
full control.
|
|
63
|
+
- Themes as plain folders: bundled ones, your own, or a per-project override.
|
|
64
|
+
- A development server that rebuilds on change and keeps the last good build
|
|
65
|
+
when something fails.
|
|
66
|
+
- `sitemap.xml`, an Atom feed, and a themed `404.html` generated for you.
|
|
67
|
+
- Deterministic output: the same sources always produce the same bytes.
|
|
68
|
+
- Three runtime dependencies: `click`, `jinja2`, `markdown-it-py`.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
Until 2.0 is published:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
python -m pip install "jamstack @ git+https://github.com/jamstackpy/jamstack@2.0"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
After the release:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python -m pip install jamstack
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Python 3.11 or newer is required.
|
|
85
|
+
|
|
86
|
+
## Quick start
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
jamstack init my-site --theme blog
|
|
90
|
+
cd my-site
|
|
91
|
+
jamstack build
|
|
92
|
+
jamstack serve
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`jamstack init` scaffolds `site.toml`, a starter page, and the files the chosen
|
|
96
|
+
theme needs. `jamstack build` writes the site to `dist/`. `jamstack serve`
|
|
97
|
+
serves it at `http://127.0.0.1:8000` and rebuilds when a source changes.
|
|
98
|
+
|
|
99
|
+
## Project layout
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
my-site/
|
|
103
|
+
site.toml # site configuration
|
|
104
|
+
content/ # pages (.md or .html)
|
|
105
|
+
static/ # files copied as-is to the output root
|
|
106
|
+
templates/ # optional overrides of the theme's templates
|
|
107
|
+
themes/ # optional local themes
|
|
108
|
+
dist/ # build output (do not edit)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Writing content
|
|
112
|
+
|
|
113
|
+
A page is a file under `content/`. Its path becomes its URL:
|
|
114
|
+
|
|
115
|
+
| Source | URL | Output |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `content/index.md` | `/` | `dist/index.html` |
|
|
118
|
+
| `content/about.md` | `/about/` | `dist/about/index.html` |
|
|
119
|
+
| `content/posts/hello.md` | `/posts/hello/` | `dist/posts/hello/index.html` |
|
|
120
|
+
|
|
121
|
+
Markdown files may start with TOML front matter delimited by `+++`:
|
|
122
|
+
|
|
123
|
+
```markdown
|
|
124
|
+
+++
|
|
125
|
+
title = "Hello, world"
|
|
126
|
+
date = 2026-11-06
|
|
127
|
+
summary = "The first post."
|
|
128
|
+
+++
|
|
129
|
+
|
|
130
|
+
This is **Markdown**.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Recognised keys:
|
|
134
|
+
|
|
135
|
+
| Key | Type | Meaning |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `title` | string | Page title. Defaults to the file name. |
|
|
138
|
+
| `date` | ISO date | Publication date. Feeds and listings order by it. |
|
|
139
|
+
| `template` | string | Template to render the page with. |
|
|
140
|
+
| `draft` | boolean | Excluded from builds unless `--drafts` is passed. |
|
|
141
|
+
| `summary` | string | Short description used by feeds and the blog theme. |
|
|
142
|
+
|
|
143
|
+
Any other key is available to templates as `page.meta`. Files ending in
|
|
144
|
+
`.html` are rendered as Jinja templates instead of Markdown, which makes it
|
|
145
|
+
easy to build landing pages with custom markup.
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
```toml
|
|
150
|
+
[site]
|
|
151
|
+
title = "My Site" # required
|
|
152
|
+
description = ""
|
|
153
|
+
author = ""
|
|
154
|
+
base_url = "https://example.com" # required for sitemap.xml and feed.xml
|
|
155
|
+
language = "en"
|
|
156
|
+
|
|
157
|
+
[build]
|
|
158
|
+
output = "dist"
|
|
159
|
+
|
|
160
|
+
[theme]
|
|
161
|
+
name = "blog"
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`base_url` must be the public, absolute URL of the site. When it is set,
|
|
165
|
+
Jamstack writes `sitemap.xml` with every page and `feed.xml` with every dated
|
|
166
|
+
page. When it is empty, both files are skipped rather than emitting relative
|
|
167
|
+
URLs.
|
|
168
|
+
|
|
169
|
+
## Themes
|
|
170
|
+
|
|
171
|
+
List the available themes with:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
jamstack themes
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Two themes ship with the package: `minimal` for small sites and `blog` for
|
|
178
|
+
dated posts. A project can override a bundled theme by creating
|
|
179
|
+
`themes/<name>/` inside the project; local themes take precedence.
|
|
180
|
+
|
|
181
|
+
Themes are plain folders, so a custom theme is just a directory with a
|
|
182
|
+
`theme.toml`, `templates/`, and optionally `static/` and `scaffold/`. See
|
|
183
|
+
[docs/themes.md](docs/themes.md) for the full specification.
|
|
184
|
+
|
|
185
|
+
## Command reference
|
|
186
|
+
|
|
187
|
+
| Command | Description |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `jamstack init [PATH] [--theme NAME] [--force]` | Create a new site. |
|
|
190
|
+
| `jamstack build [--clean] [--drafts]` | Build the site into `dist/`. |
|
|
191
|
+
| `jamstack serve [--port PORT] [--drafts]` | Build, serve, and rebuild on change. |
|
|
192
|
+
| `jamstack themes` | List available themes. |
|
|
193
|
+
| `jamstack --version` | Print the installed version. |
|
|
194
|
+
|
|
195
|
+
`python -m jamstack` behaves like the `jamstack` command.
|
|
196
|
+
|
|
197
|
+
## Migrating from 1.x
|
|
198
|
+
|
|
199
|
+
The 1.x engine (`settings.py`, `posts.json`, `static.py`, html5up themes) is
|
|
200
|
+
replaced entirely in 2.0. See [docs/migration.md](docs/migration.md) for a
|
|
201
|
+
step-by-step conversion guide, or keep using 1.x with:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
python -m pip install "jamstack<2"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Development
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
git clone git@github.com:jamstackpy/jamstack.git
|
|
211
|
+
cd jamstack
|
|
212
|
+
git switch 2.0
|
|
213
|
+
python -m venv .venv
|
|
214
|
+
. .venv/bin/activate
|
|
215
|
+
pip install -e ".[dev]"
|
|
216
|
+
pre-commit install --hook-type pre-commit --hook-type commit-msg
|
|
217
|
+
pytest
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Commits follow [Conventional Commits](https://www.conventionalcommits.org/);
|
|
221
|
+
see [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT. The 1.x html5up templates were distributed under CC BY 3.0 and are no
|
|
226
|
+
longer part of this branch.
|