diff --git a/.gitignore b/.gitignore
index ea8c4bf..d787b70 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/target
+/result
diff --git a/Cargo.lock b/Cargo.lock
index d0133a5..06b5d60 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -86,9 +86,9 @@ dependencies = [
[[package]]
name = "bitflags"
-version = "2.12.1"
+version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
+checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]]
name = "block-buffer"
@@ -469,9 +469,9 @@ dependencies = [
[[package]]
name = "ignore"
-version = "0.4.25"
+version = "0.4.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
+checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d"
dependencies = [
"crossbeam-deque",
"globset",
diff --git a/Cargo.toml b/Cargo.toml
index 5c79e7d..833205b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -54,6 +54,7 @@ cast_sign_loss = "warn"
checked_conversions = "warn"
clear_with_drain = "warn"
cloned_instead_of_copied = "warn"
+cmp_owned = "warn"
coerce_container_to_any = "warn"
collapsible_else_if = "allow"
collapsible_if = "allow"
@@ -70,6 +71,7 @@ doc_link_code = "warn"
doc_link_with_quotes = "warn"
doc_markdown = "warn"
doc_paragraphs_missing_punctuation = "warn"
+double_must_use = "warn"
duration_suboptimal_units = "warn"
empty_drop = "warn"
empty_enum_variants_with_brackets = "warn"
@@ -80,6 +82,7 @@ error_impl_error = "warn"
exit = "warn"
expect_used = "warn"
expl_impl_clone_on_copy = "warn"
+explicit_counter_loop = "warn"
explicit_deref_methods = "warn"
explicit_into_iter_loop = "warn"
explicit_iter_loop = "warn"
@@ -107,6 +110,7 @@ index_refutable_slice = "warn"
indexing_slicing = "warn"
inefficient_to_string = "warn"
infinite_loop = "warn"
+int_plus_one = "warn"
integer_division = "warn"
integer_division_remainder_used = "warn"
into_iter_without_iter = "warn"
@@ -114,6 +118,7 @@ invalid_upcast_comparisons = "warn"
ip_constant = "warn"
iter_filter_is_ok = "warn"
iter_filter_is_some = "warn"
+iter_kv_map = "warn"
iter_not_returning_iterator = "warn"
iter_on_empty_collections = "warn"
iter_on_single_items = "warn"
@@ -129,14 +134,19 @@ literal_string_with_formatting_args = "warn"
lossy_float_literal = "warn"
macro_use_imports = "warn"
manual_assert = "warn"
+manual_checked_ops = "warn"
manual_ilog2 = "warn"
manual_instant_elapsed = "warn"
+manual_is_multiple_of = "warn"
manual_is_power_of_two = "warn"
manual_is_variant_and = "warn"
manual_let_else = "warn"
manual_midpoint = "warn"
manual_non_exhaustive = "allow"
+manual_option_zip = "warn"
+manual_pop_if = "warn"
manual_string_new = "warn"
+manual_take = "warn"
many_single_char_names = "warn"
map_err_ignore = "warn"
map_with_unused_argument_over_ranges = "warn"
@@ -179,15 +189,19 @@ option_option = "warn"
panic_in_result_fn = "warn"
path_buf_push_overwrite = "warn"
pathbuf_init_then_push = "warn"
+possible_missing_else = "warn"
pub_underscore_fields = "warn"
pub_without_shorthand = "warn"
+question_mark = "warn"
range_minus_one = "warn"
range_plus_one = "warn"
rc_buffer = "warn"
rc_mutex = "warn"
read_zero_byte_vec = "warn"
redundant_clone = "warn"
+redundant_closure = "warn"
redundant_closure_for_method_calls = "warn"
+redundant_iter_cloned = "warn"
redundant_pub_crate = "warn"
redundant_test_prefix = "warn"
redundant_type_annotations = "warn"
@@ -195,6 +209,7 @@ ref_binding_to_reference = "warn"
ref_option = "warn"
ref_option_ref = "warn"
renamed_function_params = "warn"
+replace_box = "warn"
rest_pat_in_fully_bound_structs = "warn"
return_and_then = "warn"
return_self_not_must_use = "warn"
@@ -235,12 +250,17 @@ unchecked_time_subtraction = "warn"
unicode_not_nfc = "warn"
uninlined_format_args = "warn"
unnecessary_box_returns = "warn"
+unnecessary_cast = "warn"
unnecessary_debug_formatting = "warn"
+unnecessary_fold = "warn"
unnecessary_join = "warn"
unnecessary_literal_bound = "warn"
+unnecessary_option_map_or_else = "warn"
+unnecessary_result_map_or_else = "warn"
unnecessary_self_imports = "warn"
unnecessary_semicolon = "warn"
unnecessary_struct_initialization = "warn"
+unnecessary_trailing_comma = "warn"
unnecessary_wraps = "warn"
unneeded_field_pattern = "warn"
unnested_or_patterns = "warn"
@@ -257,6 +277,8 @@ unwrap_in_result = "warn"
unwrap_used = "warn"
used_underscore_binding = "warn"
used_underscore_items = "warn"
+useless_concat = "warn"
+useless_conversion = "warn"
useless_let_if_seq = "warn"
verbose_file_reads = "warn"
volatile_composites = "warn"
diff --git a/README.md b/README.md
index 960f996..2416d37 100644
--- a/README.md
+++ b/README.md
@@ -10,10 +10,10 @@ You can learn more and see what en looks like by visiting the [homepage](https:/
## Install and run
-See the [Documentation](https://en.jutty.dev/node/Documentation) page for instructions and how to install and start using en.
+See the [Get Started](https://en.jutty.dev/node/GetStarted) page for instructions and how to install and start using en.
## Roadmap
-For a high-level view of what's in the future for en, see the [What's ahead section](https://en.jutty.dev/node/Introduction/#What's) of the docs Introduction.
+For a high-level view of what's in the future for en, see the [What's ahead section](https://en.jutty.dev/node/Introduction#What's) of the docs Introduction.
For a more detailed outline of what's planned, along with what's already been completed, see the [roadmap](https://en.jutty.dev/node/Roadmap).
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..8f095cd
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,26 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1780453794,
+ "narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338",
+ "type": "github"
+ },
+ "original": {
+ "id": "nixpkgs",
+ "ref": "nixos-26.05",
+ "type": "indirect"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..bfac211
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,80 @@
+{
+ description = "A non-linear writing instrument.";
+
+ inputs.nixpkgs.url = "nixpkgs/nixos-26.05";
+
+ outputs = { nixpkgs, self }: let
+ name = "en";
+ version = "0.4.0";
+
+ supportedSystems = [
+ "x86_64-linux"
+ "aarch64-linux"
+ ];
+
+ forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
+
+ nixpkgsFor = forAllSystems (system: import nixpkgs {
+ inherit system;
+ });
+
+ in {
+ packages = forAllSystems (system: let
+ pkgs = nixpkgsFor.${system};
+ in {
+ default = pkgs.rustPlatform.buildRustPackage {
+ inherit name version;
+ src = ./.;
+
+ cargoHash =
+ "sha256-"
+ + "em229cShq/IShRnxlp5mgcIu7pIOf0LflV8Pw0lLUEY=";
+ };
+ });
+
+ apps = forAllSystems (system: {
+ default = {
+ type = "app";
+ program =
+ "${self.packages.${system}.default}/bin/en";
+ };
+ });
+
+ devShells = forAllSystems (system:
+ let pkgs = nixpkgsFor.${system}; in {
+ default = pkgs.mkShell {
+ buildInputs = with pkgs; [
+ rustup
+ just
+ watchexec
+ cargo-deny
+ cargo-llvm-cov
+ cargo-mutants
+ go-tools
+ typos
+ taplo
+ ];
+ };
+ }
+ );
+
+
+ nixosModules.bot = { config, lib, system, ... }: {
+ options.within.services.en.enable =
+ lib.mkEnableOption "enable en server";
+
+ config = lib.mkIf config.within.services.en.enable {
+ systemd.services.en = {
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ Restart = "always";
+ ExecStart =
+ "${self.packages."${system}".default}/bin/en";
+ };
+ };
+ };
+ };
+
+
+ };
+}
diff --git a/static/graph-schema.json b/static/graph-schema.json
index bbf6cc3..95c177a 100644
--- a/static/graph-schema.json
+++ b/static/graph-schema.json
@@ -6,7 +6,33 @@
"type": "object",
"$defs": {
"node": {
-
+ "type": "object",
+ "title": "Node",
+ "description": "A node object.",
+ "example": "[nodes.Earth]\ntext = \"Earth is a planet of the solar system.\"",
+ "default": null,
+ "properties": {
+ "text": {
+ "type": "string",
+ "description": "The text content of this node."
+ },
+ "title": {
+ "type": "string",
+ "description": "The node display title, useful to make it distinct from the ID.",
+ "default": "The node's ID."
+ },
+ "listed": {
+ "type": "boolean",
+ "description": "Whether this node is shown in listing pages.",
+ "default": true
+ },
+ "redirect": {
+ "type": "string",
+ "description": "A node ID to where any requests for this node will be redirected.",
+ "default": null
+ }
+ },
+ "additionalProperties": false
}
},
"properties": {
@@ -23,33 +49,7 @@
"default": null,
"type": "object",
"additionalProperties": {
- "type": "object",
- "title": "Node",
- "description": "A node object.",
- "example": "[nodes.Earth]\ntext = \"Earth is a planet of the solar system.\"",
- "default": null,
- "properties": {
- "text": {
- "type": "string",
- "description": "The text content of this node."
- },
- "title": {
- "type": "string",
- "description": "The node display title, useful to make it distinct from the ID.",
- "default": "The node's ID."
- },
- "listed": {
- "type": "boolean",
- "description": "Whether this node is shown in listing pages.",
- "default": true
- },
- "redirect": {
- "type": "string",
- "description": "A node ID to where any requests for this node will be redirected.",
- "default": null
- }
- },
- "additionalProperties": false
+ "$ref": "#/$defs/node"
},
"propertyNames": {
"pattern": "^[^-][^!@#$%^&*;:/~| \\]\\[()\\\\]*$"
diff --git a/static/graph.toml b/static/graph.toml
index bc51702..df35c11 100644
--- a/static/graph.toml
+++ b/static/graph.toml
@@ -842,6 +842,64 @@ en must differentiate node anchors from outgoing URLs:
It does this by looking at the destination and checking if it contains a `/` or `:`. That's one more reason to avoid these characters in your node IDs.
"""
+[nodes.Customization]
+text = """
+You can customize several aspects of en by overriding its default templates, styles and other assets.
+
+## The `public` directory
+
+The `static/public` directory is searched by en in the current working directory and can also be |passed as a command line option|CLI. All files placed inside this directory will be served by the en server as static files. You can create directories and organize files as you see fit, and then reference them through custom templates and includes.
+
+If you place a file in a default path, it will override the default. Namely:
+
+- `static/public/assets/favicon.svg`: Website icon as seen on tabs and other UI elements
+- `static/public/assets/style.css`: Main CSS stylesheet
+- `static/public/assets/fonts/fonts.css`: Fonts CSS index mapping the default families `sans`, `serifed`, `mono`, `prose` and `title`. The `prose` family is used for paragraphs such as in the node text content, while the `sans` family is used for UI elements such as the navigation bar and footer.
+
+The en server supports a variety of file types including plain text; data formats such as CSV, TOML and JSON; various font and image formats; and document formats such as PDF and EPUB. If you want to serve a file with a mimetype that is not included among the |builtin ones|https://codeberg.org/jutty/en/src/branch/main/src/router/handlers/mime.rs|, you can use the `mimes` |configuration option|Configuration#mimes|. If you don't, the file will be served with mimetype `application/octet-stream`, which may or may not work depending on what you are actually serving and how it's being consumed.
+
+## Styles
+
+You can override the default CSS and fonts using custom CSS files.
+
+To completely override the default style, you can place your replacement at the default path as explained in the previous section.
+
+If you just want to add small customizations, this can be better accomplished by adding a CSS file as part of a custom headers include, as explained in the next section.
+
+## Templates
+
+en uses the Tera|https://keats.github.io/tera templating engine, which provides several features for creating your own templates.
+
+When starting up, en will look for a `templates` directory in the current working directory. For each template, it looks up the corresponding filename inside this directory. If it can't find one, it will fallback to a default template.
+
+For a list of templates along with their names, see the |templates directory|https://codeberg.org/jutty/en/src/branch/main/templates| in the source code repository.
+
+See the |Tera documentation|https://keats.github.io/tera/docs for a more extensive description of the available features for writing templates.
+
+### Includes
+
+Overriding an entire template can be very verbose, considering the default templates attempt to handle various edge cases.
+
+To make it easier to override the most common use cases, the following templates are consumed as "includes" in specific parts of the main templates if placed inside the `templates` directory with the suffix `.include.html`:
+
+- `header`: Included at the end of the default templates' base header
+- `post-body`: Included after the body in the default base template
+- `favicon`: Replaces the default favicon code
+- `styles`: Replaces the default CSS links
+- `footer`: Replaces the footer
+- `navigation`: Replaces the navigation bar
+- `index-header`: Replaces the block showing the title and subtitle of the website in the index page
+- `index-list`: Replaces the block showing the list of nodes in the index page
+
+For example, to override the block in the header of all pages that globally sets the CSS stylesheets, you can drop a file at `templates/styles.include.html` containing something like:
+
+`
+
+
+`
+
+"""
+
[nodes.Graph]
text = """
A graph is a data structure composed of connected (and disconnected) nodes.