├── .github └── workflows │ ├── docs.yml │ └── pages.yml ├── LICENSE ├── README.md ├── docs └── default.nix ├── flake.lock ├── flake.nix └── lib ├── default.nix ├── fill-templates.nix ├── flake-tools.nix ├── module-system.nix ├── project.nix ├── submodules ├── content │ ├── modules │ │ ├── default.nix │ │ ├── docbook.nix │ │ ├── html.nix │ │ └── markdown.nix │ └── type.nix ├── image │ ├── modules │ │ └── default.nix │ ├── option.nix │ └── type.nix ├── page │ ├── lib.nix │ ├── modules │ │ ├── default.nix │ │ └── sitemap.nix │ └── type.nix ├── post │ ├── modules │ │ └── default.nix │ └── type.nix ├── script │ ├── modules │ │ └── default.nix │ ├── option.nix │ └── type.nix ├── style │ ├── default │ │ ├── default.nix │ │ └── default.scss │ ├── modules │ │ └── default.nix │ ├── option.nix │ └── type.nix ├── template │ ├── modules │ │ └── default.nix │ └── type.nix └── website │ ├── lib.nix │ ├── modules │ ├── default.nix │ ├── files.nix │ ├── fontawesome.nix │ ├── mermaid.nix │ ├── pages.nix │ ├── posts.nix │ └── sitemap.nix │ └── rss_dates.py ├── types.nix └── utils.nix /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | pages: 10 | name: Pages 11 | uses: ./.github/workflows/pages.yml 12 | with: 13 | output_name: docs 14 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Pages 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | output_name: 7 | type: string 8 | required: true 9 | description: | 10 | Flake output to deploy to Pages. This is the string you passed as 11 | `outputName` when calling `coricamu.lib.generateFlakeOutputs`. 12 | 13 | directory: 14 | type: string 15 | default: '.' 16 | required: false 17 | description: | 18 | This can be used to specify a subdirectory if your flake is not at 19 | the repository root. 20 | 21 | jobs: 22 | build: 23 | name: Build 24 | 25 | permissions: 26 | contents: read 27 | 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Install Nix 31 | uses: cachix/install-nix-action@v16 32 | 33 | - name: Checkout repository 34 | uses: actions/checkout@v3 35 | 36 | - name: Build ${{inputs.output_name}} 37 | run: nix -L build ${{inputs.directory}}#${{inputs.output_name}} 38 | 39 | - name: Prepare ${{inputs.output_name}} for upload 40 | run: cp -r --dereference --no-preserve=mode,ownership result/ public/ 41 | 42 | - name: Upload artifact 43 | uses: actions/upload-pages-artifact@v1 44 | with: 45 | path: public/ 46 | 47 | deploy: 48 | name: Deploy 49 | 50 | needs: build 51 | 52 | if: github.event_name == 'push' && github.ref == 'refs/heads/${{ github.event.repository.default_branch }}' 53 | 54 | permissions: 55 | pages: write 56 | id-token: write 57 | 58 | environment: 59 | name: github-pages 60 | url: ${{ steps.deployment.outputs.page_url }} 61 | 62 | concurrency: 63 | group: github-pages 64 | cancel-in-progress: true 65 | 66 | runs-on: ubuntu-latest 67 | steps: 68 | - name: Deploy ${{inputs.output_name}} to GitHub Pages 69 | id: deployment 70 | uses: actions/deploy-pages@v1 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Daniel Thwaites 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Coricamu 2 | 3 | Coricamu allows you to generate a static site using [the Nix package manager](https://nixos.org/). 4 | 5 | A static site is one which does not run any code on the server side. It can still use 6 | JavaScript on the client side to provide some level of interactivity. 7 | 8 | The following documentation assumes that you have some prior experience with the Nix 9 | language and website development. 10 | 11 | ## Usage and features 12 | 13 | Coricamu uses the module system for website configuration. This is the same way that 14 | NixOS machines are configured, but with different options to choose from. You can use 15 | imports, define your own options, and use the `mkIf`, `mkMerge` and `mkForce` functions, 16 | just as you would in NixOS. 17 | 18 | ### Creating a flake 19 | 20 | Coricamu expects you to use [Flakes](https://www.tweag.io/blog/2020-05-25-flakes/) 21 | for dependency management. 22 | 23 | Before writing a module, you need to create `flake.nix` as follows. Alternatively, this 24 | can be combined with an existing `flake.nix` as part of a larger project. 25 | 26 | ```nix 27 | { 28 | inputs.coricamu.url = "github:danth/coricamu"; 29 | 30 | outputs = { coricamu, ... }: 31 | coricamu.lib.generateFlakeOutputs { 32 | outputName = "my-website"; 33 | modules = [ ./website.nix ]; 34 | }; 35 | } 36 | ``` 37 | 38 | - `website.nix` will be your main module. 39 | - `my-website` is the name of the flake output containing the site. 40 | 41 | ### Basic information 42 | 43 | Within the main module, you must define the following options: 44 | 45 | `baseUrl` 46 | : The root URL where your site will be served. 47 | 48 | `siteTitle` 49 | : A human-readable title for the site. If it's not given, this will use your domain name. 50 | 51 | `language` 52 | : Representation of the spoken language used on your website, for example 53 | `EN-US` for English, or `DE` for German. 54 | 55 | Here's an example: 56 | 57 | ```nix 58 | { 59 | baseUrl = "https://coricamu.example.com/"; 60 | siteTitle = "Coricamu Example Site"; 61 | language = "en-gb"; 62 | } 63 | ``` 64 | 65 | ### Files 66 | 67 | You can add any file directly to your site by putting it in the `files` attribute set: 68 | 69 | ```nix 70 | { 71 | files."favicon.ico" = ./favicon.ico; 72 | } 73 | ``` 74 | 75 | This is versatile, but does not have any smart features. For many file types you should use 76 | a more specific option as described below. 77 | 78 | ### Images 79 | 80 | The `images` option will automatically convert many bitmap file types to the modern 81 | `webp` format. This is a recommended action on [PageSpeed Insights](https://pagespeed.web.dev/). 82 | 83 | `svg` images don't need this conversion: they should be added directly to `files`. 84 | 85 | ```nix 86 | { 87 | images = [ 88 | { 89 | path = "clouds.webp"; 90 | file = ./clouds.png; 91 | } 92 | ]; 93 | } 94 | ``` 95 | 96 | - The `path` option defines where the file will appear in your website. Note how it 97 | ends with `webp` but the file ends with `png`. 98 | - The `file` option defines where the file is in your repository. 99 | 100 | `path` and `file` can look totally different: there is no need to lay out your source 101 | files in the same way that they will appear in the finished website. 102 | 103 | Specifying both a path and a file in this way is a common pattern throughout Coricamu. 104 | 105 | ### Pages 106 | 107 | Rather than writing out entire documents by hand and adding them to files, 108 | Coricamu can generate a lot of the boilerplate for you. 109 | 110 | The `pages` option is what you should use for most pages, unless you have a 111 | finished HTML file already. 112 | 113 | ```nix 114 | { 115 | pages = [ 116 | { 117 | path = "index.html"; 118 | title = "Home"; 119 | 120 | # Currently supports HTML, Markdown or DocBook input 121 | body.markdown = '' 122 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 123 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 124 | minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip 125 | ex ea commodo consequat. Duis aute irure dolor in reprehenderit in 126 | voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur 127 | sint occaecat cupidatat non proident, sunt in culpa qui officia 128 | deserunt mollit anim id est laborum. 129 | ''; 130 | } 131 | 132 | { 133 | path = "about.html"; 134 | title = "About Us"; 135 | # File only needs to contain the insides of , not an entire page 136 | body.htmlFile = ./about.html; 137 | } 138 | ]; 139 | } 140 | ``` 141 | 142 | Coricamu supports three formats for the `body`: 143 | 144 | - HTML 145 | - Markdown 146 | - DocBook 147 | 148 | All of which can either be given as a string within your Nix file, or a path to 149 | a separate file. 150 | 151 | You can also give `images` and `files` within an individual page. The path of each 152 | image or file is still relative to the root of the website - so a path of `clouds.png` 153 | will appear at `https://example.com/clouds.png`, even if the page is in a subdirectory 154 | at `https://example.com/subdirectory/page.html`. 155 | 156 | ### Header / Footer 157 | 158 | You can define a header and footer which will be repeated on every page of 159 | your site. 160 | 161 | ```nix 162 | { 163 | header.html = '' 164 |

My Website

165 | ''; 166 | 167 | footer.markdown = '' 168 | Content on this site is available under the *Lorem Ipsum License* unless 169 | otherwise stated. 170 | ''; 171 | } 172 | ``` 173 | 174 | The supported content types for the header and footer are the same as those for the 175 | body of each page. 176 | 177 | ### Posts 178 | 179 | If you are building a blog, consider using `posts` instead of `pages`. You can of course 180 | choose to build your own blog by using the `pages` option instead. 181 | 182 | The `posts` option requires some extra information about each post, in return for which 183 | you get an automatically generated index page and RSS feed, which are linked at the bottom 184 | of each post. The extra information is also embedded into the page, using 185 | [Microdata](https://danth.me/posts/post/microdata.html), so that search engines can 186 | understand it. 187 | 188 | ```nix 189 | { 190 | posts = [ 191 | { 192 | title = "Lorem Ipsum"; 193 | datetime = "2022-01-31 20:10:05Z"; 194 | authors = [ "John Doe" "Jane Doe" ]; 195 | body.markdownFile = ./lorem_ipsum.md; 196 | } 197 | { 198 | title = "Ut Enim Ad Minim"; 199 | datetime = "2022-01-31 20:10:05Z"; 200 | edited = "2022-03-10 07:55:00Z"; 201 | authors = [ "Jane Doe" ]; 202 | body.htmlFile = ./ut_enim_ad_minim.html; 203 | } 204 | ]; 205 | } 206 | ``` 207 | 208 | Posts can be categorised by adding one or more `sections`: 209 | 210 | ```nix 211 | { 212 | title = "Lorem Ipsum Dolor"; 213 | datetime = "2022-02-26 11:29:26Z"; 214 | authors = [ "John Doe" ]; 215 | sections = [ "lorem" "ipsum" "dolor" "sit amet" ]; 216 | body.markdownFile = ./lorem_ipsum_dolor.md; 217 | } 218 | ``` 219 | 220 | #### Generated pages 221 | 222 | The generated index page is at `posts/index.html`, and the RSS feed is at 223 | `posts/rss/index.xml`. 224 | 225 | If you have more than one author, or have used sections, `posts/pills.html` 226 | will also be generated. This allows visitors to filter posts by author or section. 227 | 228 | Coricamu asks search engines not to index `posts/index.html`, `posts/pills.html` 229 | or any other lists. This allows them to spend more time indexing your actual content 230 | instead. Search engines don't need to read these pages to discover your posts because 231 | [`sitemap.xml`](https://developers.google.com/search/docs/crawling-indexing/sitemaps/overview) 232 | is provided. 233 | 234 | #### Templates related to posts 235 | 236 | Coricamu includes two templates related to posts: 237 | 238 | `all-posts` 239 | : A chronological list of all posts, as found on `posts/index.html`. 240 | 241 | `recent-posts` 242 | : A chronological list of the newest `count` posts. 243 | 244 | You will learn more about how to use templates later in this document. 245 | 246 | ### Styles 247 | 248 | Coricamu comes with a basic style sheet which is imported by default. This makes some 249 | of the generated elements look better, without introducing any outstanding design. 250 | 251 | If you define any style sheets of your own... 252 | 253 | ```nix 254 | { 255 | styles = [{ 256 | path = "style.css"; 257 | cssFile = ./style.css; 258 | }]; 259 | } 260 | ``` 261 | 262 | ...then the default one will be removed. If you would like to build on top of Coricamu's 263 | style sheet rather than replacing it, you can insert the default manually like this: 264 | 265 | ```nix 266 | { options, ... }: 267 | 268 | { 269 | styles = options.styles.default ++ [{ 270 | custom = { 271 | path = "style.css"; 272 | cssFile = ./style.css; 273 | }; 274 | }]; 275 | } 276 | ``` 277 | 278 | [Sass / SCSS](https://sass-lang.com/guide) style sheets are also supported: 279 | 280 | ```nix 281 | { 282 | styles = [{ 283 | # This is the path of the output file, so it is still .css 284 | path = "style.css"; 285 | 286 | # This is the input file 287 | scssFile = ./style.scss; 288 | }]; 289 | } 290 | ``` 291 | 292 | Styles can be added within an individual page too. If you use the same `path` for 293 | style sheets on multiple pages, it will cause an error unless those style sheets 294 | are exactly the same. 295 | 296 | ### Templates 297 | 298 | Templates can be used to avoid HTML boilerplate even more, or to standardise the 299 | presentation of a particular design element. 300 | 301 | #### Defining templates 302 | 303 | A template is just a Nix function which takes an arbitrary set of parameters 304 | (in this case `title` and `contents`), and returns some HTML. Templates can rely on 305 | other templates; they can even call themselves, if you are careful to avoid infinite 306 | recursion. 307 | 308 | Custom templates are added to the `templates` attribute set: 309 | 310 | ```nix 311 | { 312 | templates.info.function = 313 | { title, contents }: '' 314 |
315 |

${title}

316 |

${contents}

317 |
318 | ''; 319 | } 320 | ``` 321 | 322 | Most page settings can be specified within templates too. Those settings will be added 323 | to any page where that template is used. This can be used to install extra files to make 324 | the template work, for example a style sheet: 325 | 326 | ```nix 327 | { 328 | templates.info = { 329 | function = 330 | { title, contents }: '' 331 |
332 |

${title}

333 |

${contents}

334 |
335 | ''; 336 | 337 | styles = [{ 338 | path = "info.css"; 339 | css = '' 340 | .info { 341 | border: 3px solid black; 342 | padding: 5px; 343 | } 344 | ''; 345 | }]; 346 | }; 347 | } 348 | ``` 349 | 350 | #### Template tags 351 | 352 | Templates are inserted using "template tag" syntax. This looks similar to a HTML 353 | tag, but its name must match up with one of the templates you have defined for your 354 | website. 355 | 356 | ```html 357 |

358 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod 359 | tempor incididunt ut labore et dolore magna aliqua. 360 |

361 | 362 | Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi 363 | ut aliquip ex ea commodo consequat. 364 | 365 |

366 | Duis aute irure dolor in reprehenderit in voluptate velit esse cillum 367 | dolore eu fugiat nulla pariatur. 368 |

369 | ``` 370 | 371 | Coricamu will parse the tag and call the corresponding template function. 372 | 373 | - Attributes on the tag are converted to corresponding function arguments. 374 | - Text inside the template tag is given as the `contents` argument. 375 | 376 | This is all handled within Nix. 377 | 378 | Note that template tags also work in Markdown, but not DocBook. 379 | 380 | ### Icons 381 | 382 | Icons from [Font Awesome 6](https://fontawesome.com/search?m=free) can be 383 | inserted using the built-in `font-awesome` template: 384 | 385 | ```html 386 | 387 | ``` 388 | 389 | For logos, you need to add `style="brands"`: 390 | 391 | ```html 392 | 393 | ``` 394 | 395 | The `style` attribute can also used to switch between `regular` and `solid` 396 | for the non-branded icons. 397 | 398 | You can add content to the template to have it written alongside the icon: 399 | 400 | ```html 401 | 402 |

These are some notes next to a book icon.

403 |
404 | ``` 405 | 406 | Icons and styles from Font Awesome Pro are not yet supported. 407 | 408 | ### Diagrams 409 | 410 | [Mermaid diagrams](https://mermaid-js.github.io/) can be inserted using the 411 | built-in `mermaid` template. 412 | 413 | ```html 414 | 415 | flowchart TD 416 | insert template ---> get diagram 417 | 418 | ``` 419 | 420 | Using a `mermaid` code block in Markdown may have same visual output, however 421 | this could break in future updates. You should prefer using the template. 422 | 423 | ## Compilation 424 | 425 | There are two commands which will be most important to you during development: 426 | 427 | `nix build .#my-website` 428 | : This will compile your website and place its files into `result`. These can be 429 | inspected, or deployed to a web server by hand. 430 | 431 | `nix run .#my-website-preview` 432 | : This will compile the website as above, but launch a local web server so that you 433 | can test the site in a browser. Further instructions on how to do this will be printed 434 | after running the command. 435 | 436 | ## Deployment 437 | 438 | The commands above are useful while writing a website, but you will want to use a more 439 | automated setup when you publish it. Coricamu works with multiple web servers and hosts, 440 | some of which are documented below. 441 | 442 | ### GitHub Pages 443 | 444 | There is a reusable GitHub Actions workflow for deploying to GitHub Pages. To use it, copy 445 | the following text to `.github/workflows/docs.yml` in your repository: 446 | 447 | ```yaml 448 | name: Deploy 449 | 450 | on: 451 | push: 452 | branches: 453 | - master 454 | 455 | jobs: 456 | pages: 457 | name: Pages 458 | uses: danth/coricamu/.github/workflows/pages.yml@cd253a6940853ffc3da7c14c9311940f1d70e222 459 | with: 460 | output_name: my-website 461 | ``` 462 | 463 | The text after `output_name` must correspond to the `outputName` you provided 464 | to `coricamu.lib.generateFlakeOutputs` in `flake.nix`. In the example earler on 465 | this page, we used `my-website`. 466 | 467 | ## Credits 468 | 469 | Coricamu was heavily inspired by [Styx](https://github.com/styx-static/styx). 470 | Many thanks to the authors of that project! 471 | -------------------------------------------------------------------------------- /docs/default.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, config, ... }: 2 | 3 | with coricamuLib; 4 | 5 | rec { 6 | baseUrl = "https://danth.github.io/coricamu/"; 7 | siteTitle = "Coricamu"; 8 | language = "en-gb"; 9 | 10 | header = makeProjectHeader { 11 | title = siteTitle; 12 | inherit (config) pages; 13 | repository = "https://github.com/danth/coricamu"; 14 | }; 15 | 16 | pages = makeProjectPages ../. ++ [ 17 | { 18 | path = "options.html"; 19 | title = "Options"; 20 | body.docbook = makeOptionsDocBook { 21 | inherit (evalSite { modules = []; }) options; 22 | }; 23 | } 24 | ]; 25 | } 26 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1654845941, 6 | "narHash": "sha256-uXulXu4BQ9ch1ItV0FlL2Ns8X83m6unT5h/0X//VRLQ=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "7b3e907a6fef935794b5049c2c57c519853deb90", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs", 22 | "utils": "utils" 23 | } 24 | }, 25 | "utils": { 26 | "locked": { 27 | "lastModified": 1642700792, 28 | "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", 29 | "owner": "numtide", 30 | "repo": "flake-utils", 31 | "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", 32 | "type": "github" 33 | }, 34 | "original": { 35 | "owner": "numtide", 36 | "repo": "flake-utils", 37 | "type": "github" 38 | } 39 | } 40 | }, 41 | "root": "root", 42 | "version": 7 43 | } 44 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 4 | utils.url = "github:numtide/flake-utils"; 5 | }; 6 | 7 | outputs = 8 | { self, nixpkgs, ... }@inputs: 9 | 10 | let 11 | # Coricamu's flake outputs are coded across multiple Nix files. 12 | # The following two functions help to collect the outputs into 13 | # one value so that they can be returned here. 14 | 15 | mergeOutputs = 16 | outputs: 17 | with nixpkgs.lib; 18 | fold recursiveUpdate {} outputs; 19 | 20 | callOutputs = file: import file inputs; 21 | 22 | # libOutputs contains the value of `«Coricamu's flake».lib`, 23 | # which is used to build the docs website. 24 | 25 | libOutputs = callOutputs ./lib/flake-tools.nix; 26 | 27 | docsOutputs = libOutputs.lib.generateFlakeOutputs { 28 | outputName = "docs"; 29 | modules = [ ./docs/default.nix ]; 30 | }; 31 | 32 | in mergeOutputs [ 33 | libOutputs 34 | docsOutputs 35 | { 36 | hydraJobs = { 37 | inherit (self.packages.x86_64-linux) docs; 38 | }; 39 | } 40 | ]; 41 | } 42 | -------------------------------------------------------------------------------- /lib/default.nix: -------------------------------------------------------------------------------- 1 | args: 2 | (import ./fill-templates.nix args) 3 | // (import ./module-system.nix args) 4 | // (import ./project.nix args) 5 | // (import ./submodules/page/lib.nix args) 6 | // (import ./submodules/website/lib.nix args) 7 | // (import ./types.nix args) 8 | // (import ./utils.nix args) 9 | -------------------------------------------------------------------------------- /lib/fill-templates.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, ... }: 2 | 3 | with pkgsLib; 4 | with coricamuLib; 5 | 6 | let 7 | argumentPattern = ''([^[:space:]/>"'=]+)="(([^"\\]|\\.])*)"''; 8 | 9 | # This function takes a HTML argument list of the string form: 10 | # argument1="value1" argument2="value2" 11 | # And converts it into an attribute set of the form: 12 | # { argument1 = "value1"; argument2 = "value2"; } 13 | matchArguments = input: 14 | let 15 | matches = builtins.split argumentPattern input; 16 | foldMatch = 17 | accumulator: match: 18 | if isString match 19 | then accumulator 20 | else accumulator // { "${elemAt match 0}" = elemAt match 1; }; 21 | in 22 | foldl foldMatch {} matches; 23 | 24 | # Operations on the output value of matchTemplate. 25 | # We can either add a new match object, or update the latest one. 26 | append = collated: value: collated ++ [value]; 27 | updateLast = collated: attrs: init collated ++ [(last collated // attrs)]; 28 | 29 | # If true, we have not reached a closing tag for the current call yet. 30 | lastIsOpen = collated: 31 | (length collated > 0) && 32 | isAttrs (last collated) && 33 | (last collated).open > 0; 34 | 35 | # Start tags and self-closing tags are matched by the same pattern. 36 | # If a "/" is captured by the last group, then we know it's self-closing. 37 | startTagPattern = name: 38 | "<[[:space:]]*templates-${escapeRegex name}(([[:space:]]*${argumentPattern})*)[[:space:]]*(/)?>"; 39 | 40 | isStartOrSelfClosingTag = match: elemAt match 1 != null; 41 | isSelfClosingTag = match: elemAt match 6 == "/"; 42 | 43 | collateStartOrSelfClosingTag = collated: match: 44 | let 45 | # Self-closing tags create a template call but never open it, 46 | # so no content will be picked up and a closing tag is not required. 47 | open = if isSelfClosingTag match then 0 else 1; 48 | in 49 | if lastIsOpen collated 50 | # This tag is nested, so convert it to a string. The string will be passed 51 | # to the already open template as part of its contents, and possibly returned 52 | # to us later, when it will be parsed as a template again. This process allows 53 | # template calls to be nested without causing problems. 54 | then updateLast collated { 55 | # The first capturing group is the entire tag as a string. 56 | contents = (last collated).contents + (elemAt match 0); 57 | # We must count how many times we have seen a nested opening tag 58 | # so that the corresponding number of closing tags can be processed. 59 | open = (last collated).open + open; 60 | } 61 | # There is no template open, so we can start a new one. 62 | else append collated { 63 | inherit open; 64 | contents = ""; 65 | arguments = matchArguments (elemAt match 1); 66 | }; 67 | 68 | endTagPattern = name: 69 | ""; 70 | 71 | collateEndTag = collated: match: 72 | if isAttrs (last collated) 73 | then 74 | if (last collated).open > 1 75 | # This closing tag corresponds to an opening tag which was nested, 76 | # therefore is is converted to a string as per the comment in 77 | # collateStartTag. 78 | then updateLast collated { 79 | contents = (last collated).contents + (elemAt match 0); 80 | # Count how many times we have seen a nested closing tag so that 81 | # we know when the template should be finished. 82 | open = (last collated).open - 1; 83 | } 84 | # This is a normal closing tag. 85 | else updateLast collated { 86 | # We know that open <= 1, so we can skip decrementing and simply 87 | # set it to zero. 88 | open = 0; 89 | } 90 | else 91 | throw "Unexpected closing template tag: ${elemAt match 0}"; 92 | 93 | templatePattern = name: 94 | "(${startTagPattern name}|${endTagPattern name})"; 95 | 96 | collateContent = collated: match: 97 | # This is a string of content, not a relevant template tag. 98 | if !(lastIsOpen collated) 99 | # Between calls, we just insert content to the output list directly. 100 | then append collated match 101 | # Within a template call, we must add to the content of the template. 102 | else updateLast collated { 103 | contents = (last collated).contents + match; 104 | }; 105 | 106 | collateMatches = collated: match: 107 | if isString match then 108 | collateContent collated match 109 | else if isStartOrSelfClosingTag match then 110 | collateStartOrSelfClosingTag collated match 111 | else 112 | collateEndTag collated match; 113 | 114 | # This function uses regular expressions to parse template 115 | # tags into a list of: 116 | # - Strings representing content which is not related to any template 117 | # - Attribute sets representing a template which should be filled 118 | # This is implemented by first creating a list of: 119 | # - Strings representing content which is not related to any template 120 | # - Lists representing start tags 121 | # - Lists representing end tags 122 | matchTemplate = name: input: 123 | let splitted = builtins.split (templatePattern name) input; 124 | in foldl collateMatches [] splitted; 125 | 126 | # matchTemplate returns template matches in the form: 127 | # { arguments = {...}; contents = "..."; } 128 | # But the template functions we are given expect the contents 129 | # to be combined into the arguments to make: 130 | # { argument1 = "..."; argument2 = "..."; contents = "..."; } 131 | # Unless the template doesn't contain anything, in which case 132 | # the contents argument is omitted. 133 | # This function does the required conversion between forms. 134 | matchToArguments = match: 135 | match.arguments // 136 | (optionalAttrs (match.contents != "") { 137 | inherit (match) contents; 138 | }); 139 | 140 | # Used when we have filled one template, and then we fill another template 141 | # into the body returned from the first filling. In this case, the newer body 142 | # should replace the old one, but we have still used all of the templates. 143 | updateFillResult = left: right: { 144 | inherit (right) body; 145 | usedTemplates = left.usedTemplates ++ right.usedTemplates; 146 | }; 147 | 148 | # Used when we have filled a template into part of a body, and then we fill 149 | # it into the next part. In this case the parts are combined to build up a new 150 | # complete body into which the template has been filled. 151 | mergeFillResults = left: right: { 152 | body = left.body + right.body; 153 | usedTemplates = left.usedTemplates ++ right.usedTemplates; 154 | }; 155 | 156 | concatFillResults = foldl mergeFillResults { body = ""; usedTemplates = []; }; 157 | 158 | # This function takes a template match and replaces it with the output 159 | # of the corresponding template function. 160 | expandTemplate = templates: template: match: 161 | updateFillResult 162 | # The return value of fillTemplates will not include the current template, 163 | # because fillTemplates does not know that's where the body came from. 164 | { usedTemplates = [ template ]; } 165 | # We must repeat the template filling in case there are any template tags 166 | # within the output of the template we are about to call. 167 | (fillTemplates { 168 | body = template.function (matchToArguments match); 169 | inherit templates; 170 | }); 171 | 172 | # Here, the effects of matchTemplate and expandTemplate are combined to create a 173 | # function which performs the entire filling for a single template definition. 174 | fillTemplate = templates: name: template: body: 175 | let 176 | matches = matchTemplate name body; 177 | fill = match: 178 | if isString match 179 | then { body = match; usedTemplates = []; } 180 | else expandTemplate templates template match; 181 | in 182 | concatFillResults (map fill matches); 183 | 184 | # Finally: we repeat fillTemplate over all of the defined templates. 185 | # This is done by fully filling the first template as if it was the only one which 186 | # existed, then going back to the start and filling the second template, and so on 187 | # until everything is done. 188 | fillTemplates = { body, templates }: 189 | pipe { 190 | inherit body; 191 | usedTemplates = []; 192 | } 193 | (mapAttrsToList ( 194 | name: template: result: 195 | updateFillResult result 196 | (fillTemplate templates name template result.body) 197 | ) templates); 198 | 199 | in { inherit fillTemplates; } 200 | -------------------------------------------------------------------------------- /lib/flake-tools.nix: -------------------------------------------------------------------------------- 1 | { self, nixpkgs, utils, ... }: 2 | 3 | with utils.lib; 4 | 5 | let 6 | # Generate the outputs for a particular system, in the format 7 | # packages.«outputName» = package 8 | generateSystemOutputs = 9 | { outputName, system, modules, specialArgs ? {} }: 10 | let 11 | siteArgs = { inherit modules specialArgs; }; 12 | 13 | pkgs = import nixpkgs { 14 | inherit system; 15 | overlays = [ 16 | (final: _prev: { 17 | coricamu = self.packages.${final.system}; 18 | }) 19 | ]; 20 | }; 21 | 22 | coricamuLib = import ./default.nix { 23 | inherit coricamuLib pkgs; 24 | pkgsLib = nixpkgs.lib; 25 | }; 26 | in 27 | with coricamuLib; 28 | { 29 | packages."${outputName}" = buildSite siteArgs; 30 | 31 | apps."${outputName}-preview" = mkApp { 32 | name = "${outputName}-preview"; 33 | drv = buildSitePreview siteArgs; 34 | exePath = "/bin/coricamu-preview"; 35 | }; 36 | }; 37 | 38 | # Generate the outputs for all systems, in the format 39 | # packages.«system».«outputName» = package 40 | generateFlakeOutputs = 41 | args: 42 | eachDefaultSystem (system: 43 | generateSystemOutputs (args // { inherit system; }) 44 | ); 45 | 46 | in { 47 | lib = { 48 | inherit generateSystemOutputs generateFlakeOutputs; 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /lib/module-system.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, ... }: 2 | 3 | with coricamuLib; 4 | with pkgsLib; 5 | 6 | let 7 | formatVerbatim = text: 8 | if hasInfix "\n" text 9 | then "${escapeXML text}" 10 | else "${escapeXML text}"; 11 | 12 | formatValue = value: formatVerbatim (showVal value); 13 | 14 | formatParagraph = text: pipe text [ 15 | # We want to replace all occurences of \n\n with , but not 16 | # within code blocks and such. This allows us to skip over any paragraphs 17 | # which are already surrounded by a tag. 18 | (builtins.split "(\n\n<[a-z]+>.*\n\n)") 19 | 20 | (map (item: 21 | # Lists represent text which was already tagged. 22 | # Element 0 is the entire match, unchanged. 23 | if isList item then elemAt item 0 24 | # Strings between matches are where we want to split the paragraphs. 25 | else replaceStrings [ "\n\n" ] [ "" ] item 26 | )) 27 | 28 | # Stick the tagged and untagged sections back together. 29 | (concatStringsSep "\n") 30 | 31 | # It's convention that the entire description is wrapped in a tag. 32 | (para: "${para}") 33 | ]; 34 | 35 | formatAnything = fallback: value: 36 | if value?_type 37 | then 38 | if value._type == "literalDocBook" 39 | then formatParagraph value.text 40 | else 41 | if value._type == "literalExpression" 42 | then formatVerbatim value.text 43 | else 44 | warn "Text type `${value._type}` is not implemented" 45 | (formatVerbatim value.text) 46 | else fallback value; 47 | 48 | formatValue' = formatAnything formatValue; 49 | formatParagraph' = formatAnything formatParagraph; 50 | 51 | in { 52 | makeOptionsDocBook = { 53 | options, 54 | showInvisible ? false, 55 | showInternal ? false, 56 | customFilter ? (option: true) 57 | }: 58 | let 59 | makeOptionDocBook = 60 | option: 61 | let 62 | subOptions = option.type.getSubOptions option.loc; 63 | in '' 64 |
65 | ${escapeXML (showOption option.loc)} 66 | 67 | 68 | 69 | Type 70 | ${option.type.description or "unspecified"} 71 | 72 | ${optionalString (option?defaultText || option?default) '' 73 | 74 | Default 75 | ${formatValue' (option.defaultText or option.default)} 76 | 77 | ''} 78 | ${optionalString (option?example) '' 79 | 80 | Example 81 | ${formatValue' option.example} 82 | 83 | ''} 84 | 85 | 86 | ${ 87 | optionalString (option?description) 88 | (formatParagraph' option.description) 89 | } 90 | 91 | ${ 92 | optionalString (subOptions != {}) 93 | (concatStringsSep "\n" (makeOptionsDocBooks subOptions)) 94 | } 95 |
96 | ''; 97 | 98 | makeOptionsDocBooks = options: pipe options [ 99 | attrValues 100 | (map (option: 101 | if isOption option 102 | then 103 | if (customFilter option) && 104 | ((option.visible or true) || showInvisible) && 105 | (!(option.internal or false) || showInternal) 106 | then [ (makeOptionDocBook option) ] 107 | else [] 108 | else makeOptionsDocBooks option 109 | )) 110 | concatLists 111 | ]; 112 | 113 | in '' 114 |
115 | ${concatStringsSep "\n" (makeOptionsDocBooks options)} 116 |
117 | ''; 118 | } 119 | -------------------------------------------------------------------------------- /lib/project.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, ... }: 2 | 3 | with coricamuLib; 4 | with pkgsLib; 5 | 6 | { 7 | makeProjectPage = name: file: 8 | let 9 | text = builtins.readFile file; 10 | lines = splitString "\n" text; 11 | 12 | titleLine = findFirst (hasPrefix "# ") null lines; 13 | title = 14 | if titleLine != null 15 | then removePrefix "# " titleLine 16 | else name; 17 | 18 | path = 19 | if name == "README" 20 | then "index.html" 21 | else "${makeSlug title}.html"; 22 | 23 | content = pipe lines [ 24 | (filter (line: line != titleLine)) 25 | (concatStringsSep "\n") 26 | ]; 27 | 28 | in { 29 | inherit title path; 30 | body.markdown = content; 31 | }; 32 | 33 | makeProjectPages = projectRoot: pipe projectRoot [ 34 | builtins.readDir 35 | attrNames 36 | (map (builtins.match "([A-Z_]+)\.md")) 37 | (filter (m: m != null)) 38 | (map (m: elemAt m 0)) 39 | (names: genAttrs names (name: "${projectRoot}/${name}.md")) 40 | (mapAttrsToList makeProjectPage) 41 | ]; 42 | 43 | makeProjectHeader = 44 | { title, pages, repository ? null }: 45 | let 46 | makeLink = href: text: "${text}"; 47 | 48 | getTitle = page: 49 | if page.path == "index.html" 50 | then "Home" 51 | else page.title; 52 | 53 | pageLinks = map (page: makeLink page.path (getTitle page)) pages; 54 | 55 | repositoryLinks = optional 56 | (repository != null) 57 | (makeLink repository "Repository"); 58 | 59 | links = concatStringsSep " " 60 | (pageLinks ++ repositoryLinks); 61 | 62 | in { 63 | html = '' 64 |

${title}

65 | 66 | ''; 67 | }; 68 | } 69 | -------------------------------------------------------------------------------- /lib/submodules/content/modules/default.nix: -------------------------------------------------------------------------------- 1 | { allowNull ? false }: 2 | { pkgsLib, config, name, ... }: 3 | 4 | with pkgsLib; 5 | with pkgsLib.types; 6 | 7 | { 8 | imports = [ ./docbook.nix ./html.nix ./markdown.nix ]; 9 | 10 | options = { 11 | outputs = mkOption { 12 | description = '' 13 | List of compiled HTML outputs. 14 | 15 | When this submodule is used correctly, there should be exactly 16 | one value here. 17 | ''; 18 | internal = true; 19 | type = listOf lines; 20 | }; 21 | 22 | output = mkOption { 23 | description = "Compiled HTML."; 24 | internal = true; 25 | readOnly = true; 26 | type = if allowNull then nullOr lines else lines; 27 | }; 28 | }; 29 | 30 | config.output = 31 | if length config.outputs == 0 32 | then 33 | if allowNull 34 | then null 35 | else throw "No content is defined for ${name}" 36 | else 37 | if length config.outputs > 1 38 | then throw "Multiple content types used at once for ${name}" 39 | else elemAt config.outputs 0; 40 | } 41 | -------------------------------------------------------------------------------- /lib/submodules/content/modules/docbook.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, pkgs, config, name, ... }: 2 | 3 | with pkgsLib; 4 | with pkgsLib.types; 5 | with coricamuLib.types; 6 | 7 | let 8 | converted = 9 | let htmlFile = pkgs.runCommand "${name}.html" { 10 | inherit (config) docbook; 11 | passAsFile = [ "docbook" ]; 12 | preferLocalBuild = true; 13 | allowSubstitutes = false; 14 | } '' 15 | ${pkgs.pandoc}/bin/pandoc -f docbook -t html <$docbookPath >$out 16 | ''; 17 | in builtins.readFile htmlFile; 18 | 19 | convertedFile = 20 | let htmlFile = pkgs.runCommand "${name}.html" { 21 | preferLocalBuild = true; 22 | allowSubstitutes = false; 23 | } '' 24 | ${pkgs.pandoc}/bin/pandoc -f docbook -t html <${config.docbookFile} >$out 25 | ''; 26 | in builtins.readFile htmlFile; 27 | 28 | in { 29 | options = { 30 | docbook = mkOption { 31 | description = "DocBook content."; 32 | example = '' 33 | Contact Us 34 | You can reach us by contacting any of the following people: 35 | 36 | Jane Doe 37 | John Doe 38 | 39 | ''; 40 | type = nullOr lines; 41 | default = null; 42 | }; 43 | 44 | docbookFile = mkOption { 45 | description = "A file containing DocBook."; 46 | example = "./example.xml"; 47 | type = nullOr file; 48 | default = null; 49 | }; 50 | }; 51 | 52 | config.outputs = 53 | optional (config.docbook != null) converted 54 | ++ optional (config.docbookFile != null) convertedFile; 55 | } 56 | -------------------------------------------------------------------------------- /lib/submodules/content/modules/html.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, config, ... }: 2 | 3 | with pkgsLib; 4 | with pkgsLib.types; 5 | with coricamuLib.types; 6 | 7 | { 8 | options = { 9 | html = mkOption { 10 | description = '' 11 | HTML content. 12 | 13 | May contain templates-«name» tags which will call 14 | the corresponding template. HTML attributes (if present) will be 15 | passed to the template as an attribute set, along with any HTML 16 | inside the tag as the contents attribute. 17 | ''; 18 | example = '' 19 |

Contact Us

20 |

You can reach us by contacting any of the following people:

21 |
    22 |
  • Jane Doe
  • 23 |
  • John Doe
  • 24 |
25 | ''; 26 | type = nullOr lines; 27 | default = null; 28 | }; 29 | 30 | htmlFile = mkOption { 31 | description = '' 32 | A file containing HTML. 33 | 34 | May contain templates-«name» tags which will call 35 | the corresponding template. HTML attributes (if present) will be 36 | passed to the template as an attribute set, along with any HTML 37 | inside the tag as the contents attribute. 38 | ''; 39 | example = "./example.html"; 40 | type = nullOr file; 41 | default = null; 42 | }; 43 | }; 44 | 45 | config.outputs = 46 | with config; 47 | optional (html != null) html 48 | ++ optional (htmlFile != null) (builtins.readFile htmlFile); 49 | } 50 | -------------------------------------------------------------------------------- /lib/submodules/content/modules/markdown.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, pkgs, config, name, ... }: 2 | 3 | with pkgsLib; 4 | with pkgsLib.types; 5 | with coricamuLib.types; 6 | 7 | let 8 | converted = 9 | let htmlFile = pkgs.runCommand "${name}.html" { 10 | inherit (config) markdown; 11 | passAsFile = [ "markdown" ]; 12 | preferLocalBuild = true; 13 | allowSubstitutes = false; 14 | } '' 15 | ${pkgs.multimarkdown}/bin/multimarkdown \ 16 | --snippet --notransclude \ 17 | --to=html --output=$out $markdownPath 18 | ''; 19 | in builtins.readFile htmlFile; 20 | 21 | convertedFile = 22 | let htmlFile = pkgs.runCommand "${name}.html" { 23 | preferLocalBuild = true; 24 | allowSubstitutes = false; 25 | } '' 26 | ${pkgs.multimarkdown}/bin/multimarkdown \ 27 | --snippet --notransclude \ 28 | --to=html --output=$out ${config.markdownFile} 29 | ''; 30 | in builtins.readFile htmlFile; 31 | 32 | in { 33 | options = { 34 | markdown = mkOption { 35 | description = '' 36 | Markdown content. 37 | 38 | May contain templates.«name» HTML tags in places 39 | where Markdown allows embedded HTML. This will call the corresponding 40 | template. HTML attributes (if present) will be passed to the template 41 | as an attribute set, along with any converted Markdown inside the tag 42 | as the contents attribute. Template tags are not 43 | guaranteed to work in all places when using Markdown - if you need more 44 | flexibility, consider writing in pure HTML instead. 45 | 46 | This uses MultiMarkdown, which is an extension to the common Markdown 47 | syntax. A full cheat sheet can be found on 48 | the MultiMarkdown website. 49 | ''; 50 | example = '' 51 | # Contact Us 52 | 53 | You can reach us by contacting any of the following people: 54 | 55 | - Jane Doe 56 | - John Doe 57 | ''; 58 | type = nullOr lines; 59 | default = null; 60 | }; 61 | 62 | markdownFile = mkOption { 63 | description = '' 64 | A file containing Markdown. 65 | 66 | May contain templates.«name» HTML tags in places 67 | where Markdown allows embedded HTML. This will call the corresponding 68 | template. HTML attributes (if present) will be passed to the template 69 | as an attribute set, along with any converted Markdown inside the tag 70 | as the contents attribute. Template tags are not 71 | guaranteed to work in all places when using Markdown - if you need more 72 | flexibility, consider writing in pure HTML instead. 73 | 74 | This uses MultiMarkdown, which is an extension to the common Markdown 75 | syntax. A full cheat sheet can be found on 76 | the MultiMarkdown website. 77 | ''; 78 | example = "./example.md"; 79 | type = nullOr file; 80 | default = null; 81 | }; 82 | }; 83 | 84 | config.outputs = 85 | optional (config.markdown != null) converted 86 | ++ optional (config.markdownFile != null) convertedFile; 87 | } 88 | -------------------------------------------------------------------------------- /lib/submodules/content/type.nix: -------------------------------------------------------------------------------- 1 | { pkgsLib, ... }@args: 2 | typeArgs: 3 | 4 | with pkgsLib.types; 5 | 6 | submoduleWith { 7 | modules = [ (import ./modules/default.nix typeArgs) ]; 8 | specialArgs = args; 9 | shorthandOnlyDefinesConfig = true; 10 | } 11 | -------------------------------------------------------------------------------- /lib/submodules/image/modules/default.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, pkgs, config, ... }: 2 | 3 | with pkgsLib; 4 | with pkgsLib.types; 5 | with coricamuLib.types; 6 | 7 | { 8 | options = { 9 | path = mkOption { 10 | description = "Path of the optimised image relative to the root URL."; 11 | type = strMatching ".+\\.webp"; 12 | }; 13 | 14 | file = mkOption { 15 | description = "The image."; 16 | type = file; 17 | }; 18 | 19 | outputFile = mkOption { 20 | description = "Optimised version of the image."; 21 | internal = true; 22 | readOnly = true; 23 | type = package; 24 | }; 25 | }; 26 | 27 | config.outputFile = 28 | pkgs.runCommand config.path { } '' 29 | ${pkgs.imagemagick}/bin/convert ${config.file} $out 30 | ''; 31 | } 32 | -------------------------------------------------------------------------------- /lib/submodules/image/option.nix: -------------------------------------------------------------------------------- 1 | { isToplevel }: 2 | { coricamuLib, pkgsLib, config, ... }: 3 | 4 | with pkgsLib; 5 | with pkgsLib.types; 6 | with coricamuLib.types; 7 | 8 | { 9 | options.images = mkOption { 10 | description = "List of images available to all pages."; 11 | type = listOf (image config); 12 | default = []; 13 | }; 14 | 15 | config.files = pipe config.images [ 16 | (map (image: nameValuePair image.path image.outputFile)) 17 | listToAttrs 18 | (mkIf isToplevel) 19 | ]; 20 | } 21 | -------------------------------------------------------------------------------- /lib/submodules/image/type.nix: -------------------------------------------------------------------------------- 1 | { pkgsLib, ... }@args: 2 | 3 | with pkgsLib.types; 4 | 5 | websiteConfig: 6 | 7 | submoduleWith { 8 | modules = [ ./modules/default.nix ]; 9 | specialArgs = args // { inherit websiteConfig; }; 10 | shorthandOnlyDefinesConfig = true; 11 | } 12 | -------------------------------------------------------------------------------- /lib/submodules/page/lib.nix: -------------------------------------------------------------------------------- 1 | { pkgsLib, ... }: 2 | 3 | with pkgsLib; 4 | 5 | { 6 | pageContains = infix: { config, websiteConfig, ... }: 7 | hasInfix infix config.body.output || 8 | hasInfix infix websiteConfig.header.output || 9 | hasInfix infix websiteConfig.footer.output; 10 | } 11 | -------------------------------------------------------------------------------- /lib/submodules/page/modules/default.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, pkgs, config, websiteConfig, ... }: 2 | 3 | with pkgsLib; 4 | with pkgsLib.types; 5 | with coricamuLib; 6 | with coricamuLib.types; 7 | 8 | let 9 | filledTemplates = fillTemplates { 10 | inherit (websiteConfig) templates; 11 | body = '' 12 | ${ 13 | optionalString 14 | (websiteConfig.header.output != null) 15 | "
${websiteConfig.header.output}
" 16 | } 17 |
${config.body.output}
18 | ${ 19 | optionalString 20 | (websiteConfig.footer.output != null) 21 | "
${websiteConfig.footer.output}
" 22 | } 23 | ''; 24 | }; 25 | 26 | inherit (filledTemplates) usedTemplates; 27 | 28 | in { 29 | options = { 30 | path = mkOption { 31 | description = "Path of the page relative to the root URL."; 32 | type = strMatching ".*\\.html"; 33 | }; 34 | 35 | title = mkOption { 36 | description = "Title of the page."; 37 | type = str; 38 | }; 39 | 40 | meta = mkOption { 41 | description = '' 42 | HTML metadata for this page. 43 | 44 | Each key-value pair in this attribute set will be transformed into a 45 | corresponding HTML meta element with 46 | name set to the attribute name and 47 | content set to the attribute value. 48 | 49 | Note: there is also an option to set metadata shared between all pages. 50 | ''; 51 | type = attrsOf str; 52 | default = {}; 53 | }; 54 | 55 | head = mkOption { 56 | description = '' 57 | HTML head of the page. 58 | 59 | Much of the head can be generated automatically based on other 60 | options. You should check if a more specific option is available 61 | before using this! 62 | ''; 63 | example = '' 64 | 141 | '')) 142 | ]} 143 | 144 | ${websiteConfig.head} 145 | ''] ++ catAttrs "head" usedTemplates); 146 | 147 | files = mkMerge ([{ 148 | ${config.path} = pkgs.writeText config.path '' 149 | 150 | 151 | ${config.head} 152 | ${filledTemplates.body} 153 | 154 | ''; 155 | }] ++ catAttrs "files" usedTemplates); 156 | 157 | scripts = mkMerge (map 158 | (map (x: removeAttrs x [ "output" ])) 159 | (catAttrs "scripts" usedTemplates) 160 | ); 161 | styles = mkMerge (map 162 | (map (x: removeAttrs x [ "output" ])) 163 | (catAttrs "styles" usedTemplates) 164 | ); 165 | images = mkMerge (map 166 | (map (x: removeAttrs x [ "output" ])) 167 | (catAttrs "images" usedTemplates) 168 | ); 169 | }; 170 | } 171 | -------------------------------------------------------------------------------- /lib/submodules/page/modules/sitemap.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, config, websiteConfig, ... }: 2 | 3 | with pkgsLib; 4 | with pkgsLib.types; 5 | with coricamuLib; 6 | 7 | { 8 | options.sitemap = { 9 | # https://www.sitemaps.org/protocol.html 10 | 11 | included = mkOption { 12 | description = "Whether to include this page in the sitemap."; 13 | example = false; 14 | type = bool; 15 | defaultText = literalDocBook '' 16 | True unless meta.robots == "noindex". 17 | ''; 18 | default = 19 | if config.meta?robots 20 | then config.meta.robots != "noindex" 21 | else true; 22 | }; 23 | 24 | lastModified = mkOption { 25 | description = "Date this page was last modified."; 26 | example = "2022-01-30"; 27 | type = nullOr (strMatching "[0-9]{4}-[0-9]{2}-[0-9]{2}"); 28 | default = null; 29 | }; 30 | 31 | changeFrequency = mkOption { 32 | description = '' 33 | How often this page is likely to be edited. 34 | 35 | This value may influence how often search engines will crawl your page. 36 | ''; 37 | # "always" is also a vaild value, however it means the page is generated 38 | # dynamically for every request, which is not possible with Coricamu 39 | type = nullOr (enum [ "hourly" "daily" "weekly" "monthly" "yearly" "never" ]); 40 | default = null; 41 | }; 42 | 43 | priority = mkOption { 44 | description = '' 45 | Priority of this page compared to other pages on your site. 46 | 47 | This value may influence the order in which search engines index your 48 | pages (so that higher priority pages are checked sooner / more often). 49 | It is unlikely to affect your position in search results. 50 | 51 | This is a decimal number between 0 and 1, stored as a string. 52 | ''; 53 | example = "1.0"; 54 | type = strMatching "(0\\.[0-9]+|1\\.0+)"; 55 | default = "0.5"; 56 | }; 57 | 58 | xml = mkOption { 59 | description = "Raw XML sitemap entry."; 60 | internal = true; 61 | readOnly = true; 62 | type = lines; 63 | }; 64 | }; 65 | 66 | config.sitemap.xml = optionalString config.sitemap.included '' 67 | 68 | ${websiteConfig.baseUrl}${config.path} 69 | ${ 70 | optionalString (config.sitemap.lastModified != null) 71 | "${config.sitemap.lastModified}" 72 | } 73 | ${ 74 | optionalString (config.sitemap.changeFrequency != null) 75 | "${config.sitemap.changeFrequency}" 76 | } 77 | ${ 78 | # 0.5 is specified as the default priority and can be omitted. 79 | optionalString (config.sitemap.priority != "0.5") 80 | "${config.sitemap.priority}" 81 | } 82 | 83 | ''; 84 | } 85 | -------------------------------------------------------------------------------- /lib/submodules/page/type.nix: -------------------------------------------------------------------------------- 1 | { pkgsLib, ... }@args: 2 | 3 | with pkgsLib.types; 4 | 5 | websiteConfig: 6 | 7 | submoduleWith { 8 | modules = [ 9 | ./modules/default.nix 10 | ./modules/sitemap.nix 11 | (import ../image/option.nix { isToplevel = false; }) 12 | (import ../style/option.nix { isToplevel = false; }) 13 | (import ../script/option.nix { isToplevel = false; }) 14 | ]; 15 | specialArgs = args // { inherit websiteConfig; }; 16 | shorthandOnlyDefinesConfig = true; 17 | } 18 | -------------------------------------------------------------------------------- /lib/submodules/post/modules/default.nix: -------------------------------------------------------------------------------- 1 | { coricamuLib, pkgsLib, config, websiteConfig, ... }: 2 | 3 | with pkgsLib; 4 | with pkgsLib.types; 5 | with coricamuLib; 6 | with coricamuLib.types; 7 | 8 | let datetime = 9 | let pattern = 10 | "[0-9]{4}-[0-9]{2}-[0-9]{2}([T ][0-9]{2}:[0-9]{2}(:[0-9]{2}(\\.[0-9]+)?)?(Z|[+-][0-9]{2}:[0-9]{2}|[+-][0-9]{4})?)?"; 11 | in mkOptionType { 12 | name = "HTML datetime"; 13 | description = "YYYY-MM-DDThh:mm:ssTZD"; 14 | check = x: str.check x && builtins.match pattern x != null; 15 | inherit (str) merge; 16 | }; 17 | 18 | in { 19 | options = { 20 | datetime = mkOption { 21 | description = "Date and time of this post."; 22 | example = "2022-01-31 20:10:05"; 23 | type = datetime; 24 | }; 25 | 26 | edited = mkOption { 27 | description = "Date and time this post was last edited."; 28 | example = "2022-03-10 07:50:40"; 29 | type = nullOr datetime; 30 | default = null; 31 | }; 32 | 33 | title = mkOption { 34 | description = "Title of the post."; 35 | example = "Lorem Ipsum"; 36 | type = str; 37 | }; 38 | 39 | slug = mkOption { 40 | description = "Simplified title suitable for use as a file name."; 41 | example = "lorem_ipsum"; 42 | type = strMatching "[a-z0-9_]+"; 43 | default = makeSlug config.title; 44 | defaultText = literalDocBook "Generated from the post title."; 45 | }; 46 | 47 | authors = mkOption { 48 | description = "Names of the author(s) of this post."; 49 | example = [ "John Doe" "Jane Doe" ]; 50 | type = listOf str; 51 | }; 52 | 53 | sections = mkOption { 54 | description = "Categories used to organise posts and assist browsing."; 55 | example = [ "lorem" "ipsum dolor" ]; 56 | type = listOf str; 57 | default = []; 58 | }; 59 | 60 | body = mkOption { 61 | description = "Main post content."; 62 | example.markdown = '' 63 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do 64 | eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad 65 | minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip 66 | ex ea commodo consequat. Duis aute irure dolor in reprehenderit in 67 | voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur 68 | sint occaecat cupidatat non proident, sunt in culpa qui officia 69 | deserunt mollit anim id est laborum. 70 | ''; 71 | type = content {}; 72 | }; 73 | 74 | indexEntry = mkOption { 75 | description = "Entry in the posts index page for this post."; 76 | internal = true; 77 | type = lines; 78 | }; 79 | 80 | rssEntry = mkOption { 81 | description = "Entry in the RSS feed for this post."; 82 | internal = true; 83 | type = lines; 84 | }; 85 | 86 | page = mkOption { 87 | description = '' 88 | Main page definition for this post. 89 | 90 | Can be used to set any page settings which aren't automatically filled. 91 | ''; 92 | example = { 93 | meta.keywords = "post, key, words"; 94 | }; 95 | # Causes page.file to be defined twice otherwise 96 | type = unspecified; 97 | }; 98 | }; 99 | 100 | config = let 101 | # Extract only the date 102 | datePosted = substring 0 10 config.datetime; 103 | dateEdited = substring 0 10 config.edited; 104 | 105 | authors = sort (a: b: a < b) config.authors; 106 | sections = sort (a: b: a < b) config.sections; 107 | 108 | postInfo = '' 109 | Posted 110 |