Rust library to generate static websites
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: a whole lot of progress on docs

+373 -83
+6
website/content/docs/entrypoint.md
··· 1 + --- 2 + title: "Entrypoint" 3 + description: "Welcome to the Maudit documentation!" 4 + --- 5 + 6 + Something
+6 -20
website/content/docs/index.md
··· 1 1 --- 2 2 title: "Prologue" 3 - description: "You're gonna learn" 4 3 --- 5 4 6 - ## Section 1: Formatting Text 7 - 8 - ### Bold and Italic 9 - 10 - - **Bold Text** makes things stand out. 11 - - _Italic Text_ gives emphasis to words. 12 - - **_Bold and Italic_** combined for maximum emphasis. 13 - 14 - ### Mixed Formatting 15 - 16 - - **This is bold and _italic_** to combine styles. 17 - - _This is italic and **bold**_ as well. 5 + Welcome to the Maudit documentation! Maudit (pronounced /mo.di/, meaning _cursed_ in French) is a static site generator. 18 6 19 - ## Section 2: Links 7 + [Static site generators](https://en.wikipedia.org/wiki/Static_site_generator) are tools that take a collection of files and convert them into a website, once in a build step. This is in contrast to dynamic websites, which are generated on-the-fly by a server. Other similar tools include [Jekyll](https://jekyllrb.com), [Hugo](https://gohugo.io), [Astro](https://astro.build), [Eleventy](https://www.11ty.dev), [Zola](https://www.getzola.org) and [many more](https://jamstack.org/generators/). 20 8 21 - You can include links like this: 9 + Maudit aims to be a simple and easy-to-use static site generator, [nothing more](/docs/philosophy/). We hope that you'll find it useful for your projects, and we're excited to see what you'll create with it! 22 10 23 - - [Check out the Markdown Guide](https://www.markdownguide.org/) 11 + ### Audience 24 12 25 - ## Section 3: Combining All Features 26 - 27 - Here’s a sentence that combines everything: 13 + This documentation assumes basic knowledge of web development (e.g. HTML and CSS) and Rust. If you are new to web development, we recommend checking out the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web) and for Rust, the [Rust Book](https://doc.rust-lang.org/book/). 28 14 29 - - **_Check out the [Markdown Guide](https://www.markdownguide.org/) for more tips_**. 15 + Having said that, we do try to make the documentation approchable for beginners, and concepts will generally be explained as they are introduced.
+40
website/content/docs/installation.md
··· 4 4 section: "getting-started" 5 5 --- 6 6 7 + ### Prerequisites 8 + 9 + - [Rust (1.83 or later)](https://www.rust-lang.org) 10 + - A code editor (e.g. Visual Studio Code, RustRover, Helix, etc.) 11 + - A terminal emulator (e.g. Windows Terminal, Terminal.app, Ghostty, etc.) 12 + 13 + We recommend using [rustup](https://rustup.rs/) to install Rust. Maudit is intended to be used with the latest stable version of Rust and does not require nightly features. 14 + 15 + Once Rust is installed, run the following command to ensure the latest stable version of Rust is being used: 16 + 17 + ```bash 18 + rustup default stable 19 + ``` 20 + 21 + ### Installing Maudit 22 + 23 + Maudit provides a CLI tool for interacting with websites created using the library and generating new ones. To install the CLI tool, run the following command: 24 + 25 + ```bash 26 + cargo install maudit-cli 27 + ``` 28 + 29 + This will install the `maudit` binary in your Cargo bin directory. You can now run `maudit --help` to see the available commands and options. 30 + 31 + If you do not wish to use the CLI, or are integrating Maudit into an existing project, follow the instructions in the [manual installation guide](/docs/manual-install). 32 + 33 + ### Creating a new project 34 + 35 + To create a new Maudit project, run the following command: 36 + 37 + ```bash 38 + maudit init 39 + ``` 40 + 41 + Maudit will then ask you a series of questions to configure your project. Once complete, you can navigate to the project directory and start the development server using `maudit dev`: 42 + 43 + ```bash 44 + cd my-website 45 + maudit dev 46 + ```
+20
website/content/docs/manual-install.md
··· 1 + --- 2 + title: "Manual installation" 3 + description: "While it is recommended to use the CLI tool to create and manage Maudit projects, it is also possible to manually install Maudit like any other Rust library." 4 + --- 5 + 6 + Create a new Rust project using Cargo, specifying the `--bin` flag to create a binary project: 7 + 8 + ```bash 9 + cargo new my-website --bin 10 + cd my-website 11 + ``` 12 + 13 + Next, add Maudit as a dependency in your `Cargo.toml` file, or run `cargo add maudit` to do so automatically: 14 + 15 + ```toml 16 + [dependencies] 17 + maudit = "0.1" 18 + ``` 19 + 20 + Voilà! You can now use Maudit in your project. Check out the rest of the [documentation](/docs) for more information on how to use Maudit, or if you prefer jumping straght into the code, take a look at the [examples](https://github.com/Princesseuh/maudit/tree/main/examples).
+10 -2
website/content/docs/philosophy.md
··· 1 1 --- 2 2 title: "Philosophy" 3 - description: "What is Maudit?" 3 + description: "Maudit follows a few core principles that guide its development and design" 4 4 section: "getting-started" 5 5 --- 6 6 7 - Maudit is a tool for creating and managing documentation websites. It is designed to be simple, fast, and easy to use. Maudit is built with [Hugo](https://gohugo.io/), a static site generator written in Go. 7 + ### Maudit is about making static websites 8 + 9 + Many of the so-called modern web frameworks have gained new output modes opposite to their original purpose, for instance Next.js, a SSR-first framework, has `output: "export"` to generate a static website and Astro, SSG-first, has `output: "server"` to do the reverse. 10 + 11 + While there is nothing intrinsically wrong with wanting to grow the use cases your software can serve, supporting different output modes comes with a inherent cost, both technological and human. 12 + 13 + Supporting certain features in your less used output mode might add technical constraints affecting your main use case, and your attention is forever split between the two universes you intend to support. 14 + 15 + **Maudit is about making static websites**. It has no higher goals than that. It won't try to become a server-side rendering framework, a hybrid framework, or anything else.
+4
website/content/docs/quick-start.md
··· 1 + --- 2 + title: "Quick Start" 3 + section: "getting-started" 4 + ---
+179
website/content/docs/routing.md
··· 1 + --- 2 + title: "Routing" 3 + description: "How to create pages and routes in Maudit" 4 + section: "core-concepts" 5 + --- 6 + 7 + ### Static Routes 8 + 9 + Maudit uses a simple and intuitive API to define routes and pages. To create a new page, define a struct that implements the `Page` trait, adding the `#[route]` attribute to the struct definition with the path of the route as an argument. 10 + 11 + ```rust 12 + use maudit::page::prelude::*; 13 + 14 + #[route("/hello-world")] 15 + pub struct HelloWorld; 16 + 17 + impl Page for HelloWorld { 18 + fn render(&self, ctx: &PageContext) -> RenderResult { 19 + RenderResult::Text("Hello, world!".to_string()) 20 + } 21 + } 22 + ``` 23 + 24 + The `Page` trait requires the implementation of a `render` method that returns a `RenderResult`. This method is called when the page is built and should return the content that will be displayed. In most cases, you'll be using a templating library to create HTML content. 25 + 26 + Finally, make sure to [register the page](#registering-routes) in the `coronate` function for it to be built. 27 + 28 + ### Dynamic Routes 29 + 30 + Maudit supports creating dynamic routes with parameters. Allowing one to create many pages that share the same structure and logic, but with different content. 31 + 32 + For example, one could create a route that matches `/posts/[slug]` and renders a page with the content of the post with the given slug. 33 + 34 + To create a dynamic route, export a struct using the `route!` macro and add parameters to the route path using the `[]` syntax. For example, to create a route that matches `/posts/[slug]`, you would write: 35 + 36 + ```rust 37 + use maudit::route::prelude::*; 38 + 39 + #[route("/posts/[slug]")] 40 + pub struct Post; 41 + 42 + impl Page for Post { 43 + fn render(&self, ctx: &PageContext) -> String { 44 + format!("Hello, {}!", ctx.params.get("slug").unwrap()) 45 + } 46 + } 47 + ``` 48 + 49 + In addition to the `Page` trait, dynamic routes must implement the `DynamicRoute` trait for their struct. This trait requires a `routes` function that returns a list of all the possible values for each parameter in the route's path. 50 + 51 + ```rust 52 + use maudit::{page::prelude::*, FxHashMap}; 53 + 54 + #[route("/posts/[slug]")] 55 + pub struct Post; 56 + 57 + impl DynamicRoute for Post { 58 + fn routes(&self, ctx: &DynamicRouteContext) -> Vec<RouteParams> { 59 + let mut routes = FxHashMap::default(); 60 + routes.insert("slug".to_string(), "hello-world".to_string()); 61 + 62 + vec![RouteParams(routes)] 63 + } 64 + } 65 + 66 + impl Page for Post { 67 + fn render(&self, ctx: &PageContext) -> RenderResult { 68 + RenderResult::Text(format!("Hello, {}!", ctx.params.get("slug").unwrap())) 69 + } 70 + } 71 + ``` 72 + 73 + The `RouteParams` type is a [newtype](https://doc.rust-lang.org/rust-by-example/generics/new_types.html) around a `FxHashMap<String, String>`, representing the raw parameters as if they were directly extracted from an URL. 74 + 75 + Interacting with HashMaps in Rust can be a bit cumbersome, so Maudit provides the ability to use a custom struct to define your params and easily convert them into `RouteParams` after. 76 + 77 + ```rust 78 + #[derive(Params)] 79 + pub struct Params { 80 + pub slug: String, 81 + } 82 + 83 + impl DynamicRoute for Post { 84 + fn routes(&self, ctx: &DynamicRouteContext) -> Vec<RouteParams> { 85 + let routes = vec![ArticleParams { 86 + slug: "hello-world".to_string(), 87 + }]; 88 + 89 + RouteParams::from_vec(routes) 90 + } 91 + } 92 + ``` 93 + 94 + This struct can also be used when defining the `Page` implementation, making it possible to access the parameters in a type-safe way. For more information on how to use the `Params` derive, see the [TODO](TODO) section. 95 + 96 + ```rust 97 + #[derive(Params)] 98 + pub struct Params { 99 + pub slug: String, 100 + } 101 + 102 + impl Page for Post { 103 + fn render(&self, ctx: &PageContext) -> RenderResult { 104 + let params = ctx.params::<Params>(); 105 + 106 + RenderResult::Text(format!("Hello, {}!", params.slug)) 107 + } 108 + } 109 + ``` 110 + 111 + Like static routes, dynamic routes must be [registered](#registering-routes) in the `coronate` function in order for them to be built. 112 + 113 + ### Endpoints 114 + 115 + Maudit supports returning other types of content besides HTML, such as JSON or plain text. To do this, simply add a file extension to the route path and return the content in the `render` method. 116 + 117 + ```rust 118 + use maudit::page::prelude::*; 119 + 120 + #[route("/api.json")] 121 + pub struct HelloWorldJson; 122 + 123 + impl Page for HelloWorldJson { 124 + fn render(&self, ctx: &PageContext) -> RenderResult { 125 + RenderResult::Text(r#"{"message": "Hello, world!"}"#.to_string()) 126 + } 127 + } 128 + ``` 129 + 130 + Dynamic routes can also return different types of content. For example, to return a JSON response with the post's content, you could write: 131 + 132 + ```rust 133 + use maudit::page::prelude::*; 134 + 135 + #[route("/api/[slug].json")] 136 + pub struct PostJson; 137 + 138 + #[derive(Params)] 139 + pub struct Params { 140 + pub slug: String, 141 + } 142 + 143 + impl DynamicRoute for PostJson { 144 + fn routes(&self, ctx: &DynamicRouteContext) -> Vec<RouteParams> { 145 + let routes = vec![Params { slug: "hello-world".to_string() }]; 146 + 147 + RouteParams::from_vec(routes) 148 + } 149 + } 150 + 151 + impl Page for PostJson { 152 + fn render(&self, ctx: &PageContext) -> RenderResult { 153 + let params = ctx.params::<Params>(); 154 + 155 + RenderResult::Text(format!(r#"{{"message": "Hello, {}!"}}"#, params.slug)) 156 + } 157 + } 158 + ``` 159 + 160 + Endpoints must also be [registered](#registering-routes) in the `coronate` function in order for them to be built. 161 + 162 + ### Registering Routes 163 + 164 + All kinds of routes must be passed to the `coronate` function in [the entrypoint](/docs/entrypoint) in order to be built. 165 + 166 + The first argument to the `coronate` function is a `Vec` of all the routes that should be built. This list can be created using the `routes!` macro to make it more concise. 167 + 168 + ```rust 169 + use pages::Index; 170 + use maudit::{coronate, routes, BuildOptions, BuildOutput}; 171 + 172 + fn main() -> Result<BuildOutput, Box<dyn std::error::Error>> { 173 + coronate( 174 + routes![Index], 175 + vec![].into(), 176 + BuildOptions::default() 177 + ) 178 + } 179 + ```
+3 -1
website/src/content.rs
··· 7 7 #[serde(rename_all = "kebab-case")] 8 8 pub enum DocsSection { 9 9 GettingStarted, 10 + CoreConcepts, 10 11 } 11 12 12 13 impl Render for DocsSection { 13 14 fn render(&self) -> PreEscaped<String> { 14 15 match self { 15 16 DocsSection::GettingStarted => PreEscaped("Getting Started".to_string()), 17 + DocsSection::CoreConcepts => PreEscaped("Core Concepts".to_string()), 16 18 } 17 19 } 18 20 } ··· 20 22 #[derive(Deserialize)] 21 23 pub struct DocsContent { 22 24 pub title: String, 23 - pub description: String, 25 + pub description: Option<String>, 24 26 pub section: Option<DocsSection>, 25 27 } 26 28
+29 -15
website/src/layout.rs
··· 1 - use maud::{html, Markup}; 2 - mod docs_sidebar; 1 + use maud::{html, Markup, DOCTYPE}; 2 + mod docs_sidebars; 3 3 mod header; 4 4 5 - use docs_sidebar::sidebar; 5 + use docs_sidebars::{left_sidebar, right_sidebar}; 6 6 7 7 pub use header::header; 8 8 use maudit::generator; ··· 13 13 14 14 layout( 15 15 html! { 16 - div.container.mx-auto.grid-cols-docs.grid.p-6.py-8 { 17 - aside { 18 - (sidebar(ctx)) 16 + div.container.mx-auto.grid-cols-docs.grid."min-h-[calc(100%-64px)]" { 17 + aside.bg-gradient-to-l."from-[#e9e9e7]"."py-8"."h-full" { 18 + (left_sidebar(ctx)) 19 19 } 20 - main.w-full.max-w-larger-prose.mx-auto { 20 + main.w-full.max-w-larger-prose.mx-auto.py-8 { 21 21 (main) 22 + } 23 + aside."py-8" { 24 + (right_sidebar(ctx)) 22 25 } 23 26 } 24 27 }, 28 + true, 25 29 ctx, 26 30 ) 27 31 } 28 32 29 - pub fn layout(main: Markup, ctx: &mut RouteContext) -> RenderResult { 33 + pub fn layout(main: Markup, bottom_border: bool, ctx: &mut RouteContext) -> RenderResult { 30 34 ctx.assets.include_style("assets/prin.css", true); 31 35 32 36 html! { 33 - head { 34 - title { "Maudit" } 35 - (generator()) 36 - } 37 - body { 38 - (header(ctx)) 39 - (main) 37 + (DOCTYPE) 38 + html lang="en" { 39 + head { 40 + meta charset="utf-8"; 41 + title { "Maudit" } 42 + (generator()) 43 + link rel="icon" href="/favicon.svg"; 44 + } 45 + body { 46 + (header(ctx, bottom_border)) 47 + (main) 48 + footer.bg-our-black.text-white { 49 + div.container.mx-auto.py-8 { 50 + p.text-center { "© 2021 Maudit" } 51 + } 52 + } 53 + } 40 54 } 41 55 } 42 56 .into()
-40
website/src/layout/docs_sidebar.rs
··· 1 - use maud::{html, Markup}; 2 - use maudit::page::RouteContext; 3 - 4 - use crate::content::DocsContent; 5 - 6 - pub fn sidebar(ctx: &mut RouteContext) -> Markup { 7 - let content = ctx.content.get_source::<DocsContent>("docs"); 8 - 9 - // Map entries into HTML 10 - let mut sections = std::collections::HashMap::new(); 11 - 12 - for entry in content.entries.iter() { 13 - if let Some(section) = &entry.data.section { 14 - sections.entry(section).or_insert_with(Vec::new).push(entry); 15 - } 16 - } 17 - 18 - let entries = sections.iter().map(|(section, entries)| { 19 - html! { 20 - li { 21 - h2.text-lg.font-bold { (section) } 22 - ul.pl-1 { 23 - @for entry in entries { 24 - li { 25 - a href=(format!("/docs/{}", entry.id)) { (entry.data.title) } 26 - } 27 - } 28 - } 29 - } 30 - } 31 - }); 32 - 33 - html! { 34 - ul { 35 - @for entry in entries { 36 - (entry) 37 - } 38 - } 39 - } 40 - }
+64
website/src/layout/docs_sidebars.rs
··· 1 + use maud::{html, Markup}; 2 + use maudit::page::RouteContext; 3 + 4 + use crate::content::{DocsContent, DocsSection}; 5 + 6 + pub fn left_sidebar(ctx: &mut RouteContext) -> Markup { 7 + let content = ctx.content.get_source::<DocsContent>("docs"); 8 + 9 + let mut sections = std::collections::HashMap::new(); 10 + 11 + for entry in content.entries.iter() { 12 + if let Some(section) = &entry.data.section { 13 + sections.entry(section).or_insert_with(Vec::new).push(entry); 14 + } 15 + } 16 + 17 + let mut sections: Vec<_> = sections.into_iter().collect(); 18 + 19 + // TODO: Implement sorting on the enum ord itself? 20 + sections.sort_by_key(|(section, _)| { 21 + // Define sort order 22 + match section { 23 + DocsSection::GettingStarted => 0, 24 + DocsSection::CoreConcepts => 1, 25 + } 26 + }); 27 + 28 + let entries = sections.iter().map(|(section, entries)| { 29 + html! { 30 + li.mb-4 { 31 + h2.text-lg.font-bold { (section) } 32 + ul.pl-1 { 33 + @for entry in entries { 34 + li { 35 + a href=(format!("/docs/{}", entry.id)) { (entry.data.title) } 36 + } 37 + } 38 + } 39 + } 40 + } 41 + }); 42 + 43 + html! { 44 + ul { 45 + @for entry in entries { 46 + (entry) 47 + } 48 + } 49 + } 50 + } 51 + 52 + pub fn right_sidebar(ctx: &mut RouteContext) -> Markup { 53 + html!( 54 + h2.text-lg.font-bold { "On This Page" } 55 + nav.sticky.top-8 { 56 + // TODO: Implement this properly 57 + ul { 58 + li { 59 + a href="#hello-world" { "Hello World" } 60 + } 61 + } 62 + } 63 + ) 64 + }
+4 -2
website/src/layout/header.rs
··· 3 3 use maud::PreEscaped; 4 4 use maudit::page::RouteContext; 5 5 6 - pub fn header(_: &mut RouteContext) -> Markup { 6 + pub fn header(_: &mut RouteContext, bottom_border: bool) -> Markup { 7 + let border = if bottom_border { "border-b" } else { "" }; 8 + 7 9 html! { 8 - header.px-8.py-4.bg-faded-black.text-our-white { 10 + header.px-8.py-4.text-faded-black.bg-our-white."border-[#e9e9e7]".(border) { 9 11 div.container.flex.items-center.gap-x-8.mx-auto { 10 12 a.flex.gap-x-2.items-center."hover:text-brighter-brand" href="/" { 11 13 (PreEscaped(include_str!("../../assets/logo.svg")))
+4 -2
website/src/pages/docs.rs
··· 19 19 20 20 fn render_entry(entry: &ContentEntry<DocsContent>) -> Markup { 21 21 html! { 22 - section.mb-4 { 22 + section.mb-4.border-b."border-[#e9e9e7]".pb-2 { 23 23 @if let Some(section) = &entry.data.section { 24 24 p.text-sm.font-bold.mb-2 { (section) } 25 25 } 26 26 h2.text-5xl.font-bold.mb-2 { (entry.data.title) } 27 - h3.text-lg { (entry.data.description) } 27 + @if let Some(description) = &entry.data.description { 28 + h3.text-lg { (description) } 29 + } 28 30 } 29 31 section.prose."lg:prose-lg".max-w-none { 30 32 (PreEscaped((entry.render)()))
+2 -1
website/src/pages/index.rs
··· 29 29 } 30 30 } 31 31 } 32 - div.h-12.bg-gradient-to-b."from-[#e6e6e3]"{} 32 + div.h-12.bg-gradient-to-b."from-[#e9e9e7]"{} 33 33 34 34 div."px-12"."lg:container".mx-auto { 35 35 section { ··· 39 39 } 40 40 41 41 }, 42 + false, 42 43 ctx, 43 44 ) 44 45 }
+1
website/static/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 64 64"><path fill="currentColor" d="M52.76 53.1c-8.5-1-8.5-1-13.84-.84h-5.34l-10.5 1.17a236.63 236.63 0 0 0-15.34 2L5.9 37.42 4.23 19.58l-.5-.17c-1-.33-1.66-1.67-1.5-3.17 0-.83.17-1 1-1.5.84-.67.84-.67 1.84-.5.83 0 1.17.17 1.5.5.5.5.5.5.67 2 0 1.34 0 1.67-.34 2l-.33.34 2.5 3.16c3.67 4.5 9.67 11.18 9.84 11.01l.67-2.5a320.85 320.85 0 0 1 2.16-10.67l-.66-1.34c-.5-.5-1.17-1.83-1.5-2.67l-.5-1.66 1-1.17c1-1 1-1.17 2.16-1.33 1.5-.5 2.17-.17 3 .83.84 1.33.84 1.83.17 3.83l-.66 2 1.16 3.5c1.17 3.18 1.17 3.18 4.5 8.68 3.34 5.34 4.17 6.5 4.34 6l.5-2.33 2.34-10 1.66-8.01c0-.34-.33-.5-.83-1l-.83-1.17.16-1c.17-1 .34-1.17 1.17-1.83.67-.34 1-.5 1.67-.34 1 0 1.33.17 2 1.17.67 1.17.83 2.5.5 3.17l-1 .83-.83.5c0 .84 8 19.51 8.17 19.51.66 0 .83-.83 3-10.5l2.17-9.84-1-.67-.84-.5-.16-1.67-.17-1.66 1-1.34 1-1.33 1.33-.17c1.34 0 1.34 0 2.17.5.84.5 1 .67 1.17 1.83.33 1.34.33 2.5 0 3l-1.34 1-1 .67 1.34 5.67 1.33 5.67 1.34 13.51c1.16 13.34 1.16 13.5.83 13.5z" style="stroke-width:.16676"/></svg>
+1
website/tailwind.config.js
··· 28 28 "html, body": { 29 29 backgroundColor: "#F1F1EE", 30 30 color: "#12130F", 31 + height: "100%", 31 32 }, 32 33 33 34 body: {