├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Caddyfile ├── LICENSE ├── README.md ├── demo ├── another.md ├── index-html │ └── index.html ├── index-md │ └── index.md ├── index-txt │ └── index.txt ├── index.md ├── media │ └── photo.jpg └── templates ├── justfile ├── screenshot.png ├── templates ├── axist.css ├── body-footer.html ├── body-header.html ├── error.html ├── head.html ├── index.html └── site.css └── test.ts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v4 9 | - name: Download and install Caddy 10 | run: | 11 | wget --output-document caddy.deb --quiet https://github.com/caddyserver/caddy/releases/download/v2.7.6/caddy_2.7.6_linux_amd64.deb 12 | sudo dpkg -i caddy.deb 13 | - name: Download and install Deno 14 | run: | 15 | wget --output-document deno.zip --quiet https://github.com/denoland/deno/releases/download/v1.40.3/deno-x86_64-unknown-linux-gnu.zip 16 | unzip deno.zip deno 17 | sudo install deno /usr/local/bin/ 18 | - name: Set up just 19 | uses: extractions/setup-just@v2 20 | - name: Run tests 21 | run: just test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Caddyfile.test 2 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | # https://github.com/dbohdan/caddy-markdown-site 2 | # Copyright (c) 2021, 2025 D. Bohdan. 3 | # License: MIT. 4 | 5 | http://localhost:8080 { 6 | root * demo 7 | 8 | encode gzip 9 | 10 | file_server 11 | templates 12 | 13 | @templates { 14 | path /templates/* 15 | not path /templates/*.css /templates/*.js 16 | } 17 | handle @templates { 18 | error 403 19 | } 20 | 21 | @markdown { 22 | path_regexp \.md$ 23 | } 24 | handle @markdown { 25 | rewrite * /templates/index.html 26 | } 27 | 28 | @markdown_exists { 29 | file {path}.md 30 | } 31 | handle @markdown_exists { 32 | map {path} {caddy_markdown_site.append_to_path} { 33 | default extension 34 | } 35 | rewrite * /templates/index.html 36 | } 37 | 38 | handle_errors { 39 | file_server 40 | templates 41 | 42 | @markdown_index_exists_404 { 43 | file {path}/index.md 44 | expression `{http.error.status_code} == 404` 45 | } 46 | handle @markdown_index_exists_404 { 47 | map {path} {caddy_markdown_site.append_to_path} { 48 | default index 49 | } 50 | file_server { 51 | status 200 52 | } 53 | rewrite * /templates/index.html 54 | } 55 | 56 | handle { 57 | rewrite * /templates/error.html 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021, 2024-2025 D. Bohdan and contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # caddy-markdown-site 2 | 3 | This project is a proof of concept showing how you can serve Markdown files 4 | as reasonably good-looking minimal web pages 5 | with just the [Caddy](https://caddyserver.com/) web server. 6 | It is not a static site generator; there is no build step. 7 | The project consists of a Caddy configuration file (Caddyfile), HTML [templates](https://caddyserver.com/docs/caddyfile/directives/templates), and CSS files. 8 | You will need some knowledge of Caddy to use and customize the project. 9 | Expect a lot of work, and possibly insurmountable barriers, if you decide to adapt it for anything but a simple website. 10 | 11 | 12 | ## Screenshot 13 | 14 | ![A screenshot the index page of the demo website.](screenshot.png) 15 | 16 | 17 | ## Requirements 18 | 19 | - Caddy 2.4 or later. 20 | - Optional: 21 | - [Deno](https://deno.land/) 1.31 or later to run the [tests](test.ts) 22 | (`just test`). 23 | - [entr](https://github.com/eradman/entr) for development 24 | (`just dev`). 25 | - [just](https://github.com/casey/just) to run the tasks. 26 | 27 | 28 | ## Features 29 | 30 | - If your page file is `demo/foo.md` and your domain example.com, 31 | you will be able to access the file as example.com/foo with no extension and example.com/foo.md. 32 | - `index.md` serves as the directory index (but `index.html` takes priority). 33 | - You can customize the look of your site without touching the main template. 34 | Edit `templates/{head,header,footer}.html` to do it. 35 | `head.html` links to the style sheets. 36 | - An error page is shown on error. 37 | 38 | 39 | ## Front matter 40 | 41 | Markdown page files can include Caddy template 42 | [front matter](https://caddyserver.com/docs/modules/http.handlers.templates#splitfrontmatter) 43 | in JSON, TOML, or YAML. 44 | The front matter sets variables that configure how a page is presented. 45 | 46 | The following variables are recognized: 47 | 48 | - `lang`: the [`lang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) HTML global attribute. 49 | If not specified, defaults to an empty string. 50 | - `text_dir`: the [`dir`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir) HTML global attribute. 51 | Defaults to `auto`. 52 | - `title`: The page title. 53 | Defaults to the request path. 54 | 55 | ### Example 56 | 57 | ```none 58 | --- 59 | lang: en 60 | title: Greeting 61 | --- 62 | Hello, world! 63 | ``` 64 | 65 | 66 | ## License 67 | 68 | MIT. 69 | 70 | [`index.html`](templates/index.html) derives from the 71 | [`index.html` template](https://github.com/caddyserver/website/blob/1ff5103c73c921c8faa82ef3342d904a7f6a8e22/src/docs/index.html) used on the Caddy website. 72 | 73 | [`axist.css`](templates/axist.css) is [axist](https://github.com/ruanmartinelli/axist), 74 | a [classless](https://github.com/dbohdan/classless-css) CSS stylesheet. 75 | 76 | [`photo.jpg`](demo/media/photo.jpg) is by Siarhei Plashchynski 77 | [on Unsplash](https://unsplash.com/photos/6FmtLICCvxI). 78 | 79 | > Unsplash grants you an irrevocable, nonexclusive, worldwide copyright license 80 | > to download, copy, modify, distribute, perform, and use photos from Unsplash for free, 81 | > including for commercial purposes, without permission from or attributing the photographer or Unsplash. 82 | > This license does not include the right to compile photos from Unsplash 83 | > to replicate a similar or competing service. 84 | -------------------------------------------------------------------------------- /demo/another.md: -------------------------------------------------------------------------------- 1 | # Another page 2 | 3 | How exciting. 4 | 5 | ```shell 6 | # This is a code block. 7 | echo 'Hello, world!' 8 | ``` 9 | -------------------------------------------------------------------------------- /demo/index-html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | HTML index 5 | 6 | {{ include "/templates/head.html" }} 7 | 8 | 9 |
10 |

This is an HTML index.

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
idfirst_namelast_nameemail
1KarilFairhallkfairhall0@example.net
2KerryKirbykkirby1@example.net
3DemetraSanhamdsanham2@example.net
4DarbGricksdgricks3@example.net
5PepeDi Carlopdicarlo4@example.net
6MallissaCraikmcraik5@example.net
7ThomasSuthernstsutherns6@example.net
8AllardReinaareina7@example.net
9DotiCatondcaton8@example.net
10MeridelDumphreymdumphrey9@example.net
25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /demo/index-md/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | lang: en 3 | --- 4 | This file is the index of a subdirectory. 5 | -------------------------------------------------------------------------------- /demo/index-txt/index.txt: -------------------------------------------------------------------------------- 1 | This is a text file index. 2 | -------------------------------------------------------------------------------- /demo/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | lang: en 3 | text_dir: ltr 4 | title: Welcome! 5 | --- 6 | # Hello! 7 | 8 | This is the index file. 9 | 10 | Nunc feugiat metus et velit efficitur dapibus. Nulla egestas velit leo, a sagittis eros porta convallis. Suspendisse elementum nisl ac imperdiet posuere. Integer vehicula augue sit amet nisl efficitur, sit amet molestie felis pellentesque. Quisque lobortis semper ipsum, id porta purus vehicula eu. Proin quis ipsum in ex finibus vulputate. 11 | 12 | Pellentesque at ligula sed massa hendrerit pulvinar id sit amet lacus. Nam efficitur, nibh id rutrum pharetra, dolor ex mollis odio, at dictum augue arcu non sapien. Nulla eu justo sed nisl vehicula dignissim. Ut mattis odio eu neque pretium pharetra. Mauris leo felis, sagittis id ullamcorper vel. 13 | 14 | * [Another page](/another) 15 | * Indices: 16 | * [HTML](/index-html) 17 | * [Markdown](/index-md) 18 | * [Text](/index-txt) 19 | 20 | ![Test image](media/photo.jpg) 21 | 22 | Aliquam suscipit est sit amet cursus viverra. Mauris magna mauris, mollis vel efficitur eu, ullamcorper non est. Maecenas eget neque lorem. Sed euismod accumsan sodales. In consequat pellentesque velit ullamcorper ullamcorper. Aliquam blandit enim posuere tortor iaculis interdum. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. 23 | -------------------------------------------------------------------------------- /demo/media/photo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbohdan/caddy-markdown-site/a5e1d8f4dcdb32df3d7471e25467280e6bfb4453/demo/media/photo.jpg -------------------------------------------------------------------------------- /demo/templates: -------------------------------------------------------------------------------- 1 | ../templates -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set windows-shell := ["powershell.exe", "-NoLogo", "-Command"] 2 | 3 | caddy := "caddy" 4 | 5 | default: test 6 | 7 | [unix] 8 | dev: 9 | find . -type f | entr -r {{ quote(caddy) }} run 10 | 11 | test: 12 | deno test \ 13 | --allow-env \ 14 | --allow-net \ 15 | --allow-read=Caddyfile \ 16 | --allow-run \ 17 | --allow-write=Caddyfile.test \ 18 | test.ts \ 19 | ; 20 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbohdan/caddy-markdown-site/a5e1d8f4dcdb32df3d7471e25467280e6bfb4453/screenshot.png -------------------------------------------------------------------------------- /templates/axist.css: -------------------------------------------------------------------------------- 1 | /* 2 | - 3.815rem 3 | - 3.052rem 4 | - 2.441rem 5 | - 1.953rem 6 | - 1.563rem 7 | - 1.25rem 8 | - 1rem 9 | - 0.8rem 10 | - 0.64rem 11 | - 0.512rem 12 | - 0.41rem 13 | - 0.328rem 14 | - 0.262rem 15 | - 0.209rem 16 | */ 17 | 18 | :root { 19 | --primary: #1524d9; 20 | --light-primary: #2332ea; 21 | --secondary: #ff2e88; 22 | --light-secondary: #fc77b1; 23 | --red: red; 24 | --black: #212529; 25 | --white: #fdfdfd; 26 | --dark-gray: #343334; 27 | --gray: #616060; 28 | --light-gray: #ccc; 29 | --lighter-gray: #f6f6f6; 30 | --font-sans-serif: 31 | system-ui, 32 | -apple-system, 33 | segoe ui, 34 | roboto, 35 | ubuntu, 36 | helvetica, 37 | cantarell, 38 | noto sans, 39 | sans-serif; 40 | --font-monospace: 41 | menlo, 42 | monaco, 43 | lucida console, 44 | liberation mono, 45 | dejavu sans mono, 46 | bitstream vera sans mono, 47 | courier new, 48 | monospace, 49 | serif; 50 | --border-radius: 0.2rem; 51 | } 52 | 53 | * { 54 | box-sizing: border-box; 55 | margin: 0; 56 | padding: 0; 57 | text-rendering: geometricPrecision; 58 | -webkit-font-smoothing: antialiased; 59 | -moz-osx-font-smoothing: grayscale; 60 | -webkit-tap-highlight-color: transparent; 61 | font-family: var(--font-sans-serif); 62 | } 63 | 64 | html { 65 | font-size: calc(16px + ((100vw - 600px) / 250)); 66 | padding: 0; 67 | text-decoration-skip-ink: "auto"; 68 | line-height: 1.953rem; 69 | margin: auto; 70 | min-height: 100%; 71 | overflow-x: hidden; 72 | max-width: 1140px; 73 | } 74 | 75 | body { 76 | padding: 0; 77 | margin: calc((100vh / 25) * 1.563) calc((100vw / 25) * 1.563); 78 | background-color: var(--white); 79 | color: var(--black); 80 | caret-color: var(--black); 81 | word-wrap: break-word; 82 | } 83 | 84 | h1, 85 | h2, 86 | h3, 87 | h4, 88 | h5, 89 | h6 { 90 | margin-bottom: 1rem; 91 | margin-top: 1em; 92 | font-weight: bold; 93 | } 94 | 95 | h1 { 96 | font-size: 3.052rem; 97 | letter-spacing: -0.15rem; 98 | line-height: 1; 99 | } 100 | 101 | h2 { 102 | font-size: 2.441rem; 103 | letter-spacing: -0.12rem; 104 | line-height: 1.2; 105 | } 106 | 107 | h3 { 108 | font-size: 1.953rem; 109 | letter-spacing: -0.09rem; 110 | line-height: 1.2; 111 | } 112 | 113 | h4 { 114 | font-size: 1.563rem; 115 | letter-spacing: -0.06rem; 116 | line-height: 1.3; 117 | } 118 | 119 | h5 { 120 | font-size: 1.25rem; 121 | letter-spacing: -0.03rem; 122 | line-height: 1.4; 123 | } 124 | 125 | h6 { 126 | font-size: 1rem; 127 | letter-spacing: 0; 128 | line-height: 1.5; 129 | } 130 | 131 | p { 132 | margin-bottom: 1.563rem; 133 | } 134 | 135 | p > *:last-child { 136 | margin-bottom: 0; 137 | } 138 | 139 | blockquote { 140 | border-left: 1px solid var(--light-gray); 141 | padding: 0 1rem; 142 | margin-bottom: 1.563rem; 143 | } 144 | 145 | a { 146 | color: var(--primary); 147 | text-decoration: none; 148 | } 149 | 150 | @media (hover: hover) { 151 | a:hover { 152 | text-decoration: underline; 153 | } 154 | } 155 | 156 | small { 157 | font-size: 0.888rem; 158 | } 159 | 160 | hr { 161 | border: 0; 162 | height: 2px; 163 | margin: 1rem 0; 164 | background: var(--light-gray); 165 | } 166 | 167 | fieldset { 168 | border: none; 169 | padding: 0; 170 | margin: 0; 171 | } 172 | 173 | label, 174 | legend { 175 | font-weight: bold; 176 | display: inline-block; 177 | } 178 | 179 | input[type="email"], 180 | input[type="text"], 181 | input[type="number"], 182 | input[type="password"], 183 | input[type="date"], 184 | input[type="month"], 185 | input[type="week"], 186 | input[type="datetime"], 187 | input[type="datetime-local"], 188 | input[type="url"], 189 | input[type="search"], 190 | input[type="tel"], 191 | input:not([type]) { 192 | display: block; 193 | padding: 1rem; 194 | font-size: 1rem; 195 | border: 2px solid var(--lighter-gray); 196 | color: var(--black); 197 | appearance: none; 198 | border-radius: var(--border-radius); 199 | background-color: var(--lighter-gray); 200 | -webkit-appearance: none; 201 | -moz-appearance: none; 202 | } 203 | 204 | select { 205 | display: block; 206 | padding: 1rem; 207 | font-size: 1em; 208 | border: 2px solid var(--lighter-gray); 209 | border-radius: var(--border-radius); 210 | color: var(--black); 211 | background-color: var(--lighter-gray); 212 | appearance: none; 213 | -webkit-appearance: none; 214 | -moz-appearance: none; 215 | } 216 | 217 | textarea { 218 | display: block; 219 | font-size: 1rem; 220 | padding: 1rem; 221 | line-height: 1rem; 222 | color: var(--black); 223 | border-radius: var(--border-radius); 224 | border: 2px solid var(--lighter-gray); 225 | background-color: var(--lighter-gray); 226 | box-sizing: border-box; 227 | resize: none; 228 | appearance: none; 229 | -webkit-appearance: none; 230 | -moz-appearance: none; 231 | } 232 | 233 | input:focus, 234 | select:focus, 235 | textarea:focus { 236 | outline: none; 237 | border: 2px solid var(--primary); 238 | } 239 | 240 | input:invalid, 241 | select:invalid, 242 | textarea:invalid { 243 | border: 2px solid var(--red); 244 | outline: none; 245 | } 246 | 247 | input[type="checkbox"]:hover, 248 | input[type="radio"]:hover { 249 | cursor: pointer; 250 | } 251 | 252 | input[type="submit"], 253 | input[type="reset"], 254 | input[type="button"], 255 | button { 256 | padding: 0.5rem 1.25rem; 257 | font-size: 1rem; 258 | border: 0; 259 | border-radius: var(--border-radius); 260 | color: var(--lighter-gray); 261 | height: 2.5rem; 262 | background-color: var(--primary); 263 | -webkit-appearance: none; 264 | -moz-appearance: none; 265 | font-weight: bold; 266 | } 267 | 268 | @media (hover: hover) { 269 | input[type="reset"]:hover, 270 | input[type="submit"]:hover, 271 | input[type="button"]:hover, 272 | button:hover { 273 | cursor: pointer; 274 | background-color: var(--light-primary); 275 | } 276 | } 277 | 278 | button:focus-visible, 279 | input[type="submit"]:focus-visible, 280 | input[type="reset"]:focus-visible, 281 | input[type="button"]:focus-visible { 282 | border-color: var(--light-primary); 283 | outline: none; 284 | } 285 | 286 | input[disabled], 287 | button:disabled { 288 | background-color: var(--gray); 289 | } 290 | 291 | table { 292 | width: 100%; 293 | border-collapse: collapse; 294 | margin: 1.75rem 0; 295 | font-variant-numeric: tabular-nums; 296 | } 297 | 298 | th, 299 | td { 300 | vertical-align: top; 301 | border-bottom: 2px solid var(--light-gray); 302 | line-height: 15px; 303 | padding: 15px; 304 | } 305 | 306 | th { 307 | font-weight: bold; 308 | text-align: left; 309 | color: var(--dark-gray); 310 | } 311 | 312 | code, 313 | pre { 314 | font-family: var(--font-monospace); 315 | color: var(--dark-gray); 316 | background-color: var(--lighter-gray); 317 | font-size: 0.8rem; 318 | vertical-align: middle; 319 | overflow: scroll; 320 | border-radius: var(--border-radius); 321 | } 322 | 323 | code { 324 | white-space: nowrap; 325 | vertical-align: baseline; 326 | padding: 0 0.328rem; 327 | } 328 | 329 | pre { 330 | white-space: pre; 331 | margin: 0.262rem 0; 332 | padding: 0.64rem 1rem; 333 | } 334 | 335 | pre::after { 336 | content: " "; 337 | } 338 | 339 | ul { 340 | margin: 0; 341 | padding: 0 1px; 342 | list-style: disc outside; 343 | font-variant-numeric: tabular-nums; 344 | } 345 | 346 | ol { 347 | list-style: decimal outside; 348 | } 349 | 350 | ol, 351 | ul { 352 | padding-left: 1rem; 353 | margin-bottom: 1rem; 354 | } 355 | 356 | li { 357 | list-style-position: inside; 358 | } 359 | 360 | kbd { 361 | display: inline-block; 362 | padding: 0 0.328rem; 363 | font-family: 364 | "SFMono-Regular", 365 | Consolas, 366 | "Liberation Mono", 367 | Menlo, 368 | Courier, 369 | monospace; 370 | font-size: 0.64rem; 371 | color: var(--dark-gray); 372 | vertical-align: middle; 373 | background-color: #f9f9f9; 374 | border: solid 1px #d8d8d8; 375 | border-bottom: solid 2px #a6a5a6; 376 | border-radius: 5px; 377 | } 378 | 379 | abbr { 380 | text-decoration: none; 381 | border-bottom: 2px dashed #949394; 382 | } 383 | 384 | @media (hover: hover) { 385 | abbr:hover { 386 | cursor: help; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /templates/body-footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/body-header.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{/* https://github.com/dbohdan/caddy-markdown-site */}} 4 | 5 | {{ $code := placeholder "http.error.status_code" }} 6 | {{ $text := placeholder "http.error.status_text" }} 7 | 8 | 9 | 10 | {{ $code }} {{ $text }} 11 | 12 | {{ include "/templates/head.html" }} 13 | 14 | 15 | {{ include "/templates/header.html" }} 16 | 17 |
18 |

Error {{ $code }}

19 | {{ if eq $code "403" }} 20 |

You don't have permission to access this resource.

21 | {{ else if eq $code "404" }} 22 |

The requested URL was not found on this server.

23 | {{ else if eq $code "500" }} 24 |

An internal server error has occurred.

25 | {{ else }} 26 |

{{ $text }}.

27 | {{ end }} 28 |
29 | 30 | {{ include "/templates/footer.html" }} 31 | 32 | 33 | -------------------------------------------------------------------------------- /templates/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{/* https://github.com/dbohdan/caddy-markdown-site */}} 4 | 5 | {{ $path := .OriginalReq.URL.Path }} 6 | {{ $append := placeholder "caddy_markdown_site.append_to_path" }} 7 | {{ if eq $append "extension" }} 8 | {{ $path = printf "%s.md" $path }} 9 | {{ else if eq $append "index" }} 10 | {{ $path = printf "%s/index.md" $path }} 11 | {{ end }} 12 | {{ $markdownFile := (include $path | splitFrontMatter) }} 13 | 14 | {{ $lang := default "" $markdownFile.Meta.lang }} 15 | {{ $textDir := default "auto" $markdownFile.Meta.text_dir }} 16 | {{ $title := default .OriginalReq.URL.Path $markdownFile.Meta.title }} 17 | 18 | 19 | 20 | {{ $title }} 21 | 22 | 23 | 24 | {{ include "/templates/head.html" }} 25 | 26 | 27 | 28 | {{ include "/templates/body-header.html" }} 29 |
30 |
{{ markdown $markdownFile.Body }}
31 |
32 | {{ include "/templates/body-footer.html" }} 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /templates/site.css: -------------------------------------------------------------------------------- 1 | code { 2 | white-space: pre-wrap; 3 | } 4 | 5 | img { 6 | max-width: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env -S deno test --allow-env --allow-net --allow-read=Caddyfile --allow-run --allow-write=Caddyfile.test --check 2 | 3 | import { 4 | assertEquals, 5 | assertStringIncludes, 6 | } from "https://deno.land/std@0.100.0/testing/asserts.ts"; 7 | import { delay } from "https://deno.land/std@0.100.0/async/delay.ts"; 8 | 9 | const randInt = (min: number, max: number) => { 10 | return Math.floor(Math.random() * (max - min) + min); 11 | }; 12 | 13 | const compressWhitespace = (s: string) => { 14 | s.replaceAll(/(\s)\s+/g, "$1"); 15 | }; 16 | 17 | const caddy = Deno.env.get("CADDY") || "caddy"; 18 | const port = 8000 + randInt(0, 101); 19 | const url = `http://localhost:${port}`; 20 | const adminPort = 22000 + randInt(0, 101); 21 | const adminAddr = `localhost:${adminPort}`; 22 | 23 | let config = await Deno.readTextFile("Caddyfile"); 24 | config = `{\n\tadmin ${adminAddr}\n}\n\n` + 25 | config.replace(":8080", `:${port}`); 26 | await Deno.writeTextFile("Caddyfile.test", config); 27 | 28 | const caddyProcess = (new Deno.Command( 29 | caddy, 30 | { 31 | args: ["run", "--config", "Caddyfile.test"], 32 | stderr: "inherit", 33 | stdout: "inherit", 34 | }, 35 | )).spawn(); 36 | 37 | await delay(2000); 38 | 39 | const get = async (path = "") => await (await fetch(`${url}${path}`)).text(); 40 | 41 | Deno.test("index 1", async () => { 42 | const html = await get(); 43 | assertStringIncludes(html, "Welcome"); 44 | }); 45 | 46 | Deno.test("index 2", async () => { 47 | const html = await get("/index-html/"); 48 | assertStringIncludes(html, "axist.css"); 49 | assertStringIncludes(html, "<h1>This is an HTML index.</h1>"); 50 | }); 51 | 52 | Deno.test("index 3", async () => { 53 | const html = await get("/index-txt/"); 54 | assertStringIncludes(html, "text file index"); 55 | }); 56 | 57 | Deno.test("index 4", async () => { 58 | const html = await get("/index-md"); 59 | assertStringIncludes(html, "index of a subdirectory"); 60 | }); 61 | 62 | Deno.test("index 5", async () => { 63 | const req = await fetch(`${url}/index-md`); 64 | await req.text(); 65 | assertEquals(req.status, 200); 66 | }); 67 | 68 | Deno.test("extension", async () => { 69 | const a = await get("/index"); 70 | const b = await get("/index.md"); 71 | assertEquals(compressWhitespace(a), compressWhitespace(b)); 72 | }); 73 | 74 | Deno.test("front matter vars", async () => { 75 | const html = await get(); 76 | assertStringIncludes(html, `dir="ltr" lang="en"`); 77 | }); 78 | 79 | Deno.test("template CSS", async () => { 80 | const css = await get("/templates/axist.css"); 81 | assertStringIncludes(css, "font-size:"); 82 | }); 83 | 84 | Deno.test("server shutdown", async () => { 85 | const req = await fetch(`http://${adminAddr}/stop`, { 86 | method: "POST", 87 | }); 88 | await req.text(); 89 | }); 90 | --------------------------------------------------------------------------------