├── .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 | "[[:space:]]*templates-${escapeRegex name}[[:space:]]*>";
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]+>.*[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 | ""
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 |
65 | '';
66 | type = lines;
67 | default = "";
68 | };
69 |
70 | body = mkOption {
71 | description = "Main content of the page.";
72 | example.markdown = ''
73 | # Contact Us
74 |
75 | You can reach us by contacting any of the following people:
76 |
77 | - Jane Doe
78 | - John Doe
79 | '';
80 | type = content {};
81 | };
82 |
83 | files = mkOption {
84 | description = "Attribute set containing files by path.";
85 | type = attrsOf file;
86 | default = {};
87 | };
88 | };
89 |
90 | config = {
91 | meta = mkMerge ([{
92 | viewport = mkDefault "width=device-width, initial-scale=1";
93 | generator = mkDefault "Coricamu"; # We do a little advertising
94 | }] ++ catAttrs "meta" usedTemplates);
95 |
96 | head = mkMerge ([''
97 | ${config.title}
98 |
99 |
116 |
117 |
118 | ${
119 | mapAttrsToString
120 | (name: content: "")
121 | (websiteConfig.meta // config.meta)
122 | }
123 |
124 |
125 |
126 | ${pipe (config.styles ++ websiteConfig.styles) [
127 | (catAttrs "path")
128 | lists.unique
129 | (concatMapStringsSep "\n" (path: ''
130 |
131 | ''))
132 | ]}
133 |
134 | ${pipe (config.scripts ++ websiteConfig.scripts) [
135 | lists.unique
136 | (concatMapStringsSep "\n" (script: ''
137 |
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 |
113 |
114 | ${optionalString (config.edited != null) ''
115 | and edited
116 |
119 | ''}
120 |
121 | by
122 |