Page meta from frontmatter

This commit is contained in:
2025-11-04 14:15:00 -08:00
parent 4504a88f4b
commit dc4a1def5f
13 changed files with 518 additions and 163 deletions

22
Cargo.lock generated
View File

@@ -1985,6 +1985,19 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_yaml"
version = "0.9.34+deprecated"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
dependencies = [
"indexmap",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]]
name = "service-webpage"
version = "0.0.1"
@@ -1993,6 +2006,7 @@ dependencies = [
"axum",
"chrono",
"emojis",
"lazy_static",
"libservice",
"lru",
"macro-assets",
@@ -2000,6 +2014,8 @@ dependencies = [
"markdown-it",
"maud",
"parking_lot",
"serde",
"serde_yaml",
"strum",
"tracing",
]
@@ -2545,6 +2561,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.9.0"

View File

@@ -112,6 +112,7 @@ anstyle = { version = "1.0.13" }
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
toml = "0.9.8"
serde_yaml = "0.9"
base64 = "0.22.1"
#
@@ -126,7 +127,7 @@ num = "0.4.3"
chrono = "0.4.42"
lru = "0.16.2"
parking_lot = "0.12.5"
lazy_static = "1.5.0"
#
# Macro utilities

View File

@@ -22,3 +22,6 @@ strum = { workspace = true }
chrono = { workspace = true }
parking_lot = { workspace = true }
lru = { workspace = true }
lazy_static = { workspace = true }
serde_yaml = { workspace = true }
serde = { workspace = true }

View File

@@ -1,33 +1,51 @@
use lazy_static::lazy_static;
use markdown_it::parser::block::{BlockRule, BlockState};
use markdown_it::parser::core::Root;
use markdown_it::parser::inline::{InlineRule, InlineState};
use markdown_it::{Node, NodeValue, Renderer};
use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
use maud::{Markup, PreEscaped, Render};
use std::str::FromStr;
use crate::components::fa::FAIcon;
use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail};
lazy_static! {
static ref MdParser: MarkdownIt = {
let mut md = markdown_it::MarkdownIt::new();
markdown_it::plugins::cmark::add(&mut md);
markdown_it::plugins::html::add(&mut md);
md.inline.add_rule::<InlineEmote>();
md.inline.add_rule::<InlineEmote>();
md.inline.add_rule::<InlineMdx>();
md.block.add_rule::<FrontMatter>().before_all();
md
};
}
pub struct Markdown<'a>(pub &'a str);
impl Render for Markdown<'_> {
fn render(&self) -> Markup {
// TODO: init once
let md = &mut markdown_it::MarkdownIt::new();
markdown_it::plugins::cmark::add(md);
markdown_it::plugins::html::add(md);
md.inline.add_rule::<InlineEmote>();
md.inline.add_rule::<InlineMdx>();
let md = md.parse(self.0);
let md = Self::parse(self.0);
let html = md.render();
return PreEscaped(html);
}
}
impl Markdown<'_> {
pub fn parse(md_str: &str) -> Node {
MdParser.parse(md_str)
}
}
//
// MARK: extensions
//
//
// MARK: emote
//
#[derive(Debug)]
pub struct InlineEmote(String);
@@ -66,6 +84,10 @@ impl InlineRule for InlineEmote {
}
}
//
// MARK: mdx
//
#[derive(Debug)]
pub struct InlineMdx(String);
@@ -245,3 +267,58 @@ fn mdx_include(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool {
return true;
}
//
// MARK: frontmatter
//
#[derive(Debug)]
/// AST node for front-matter
pub struct FrontMatter {
pub content: String,
}
impl NodeValue for FrontMatter {
fn render(&self, _node: &Node, _fmt: &mut dyn Renderer) {}
}
impl BlockRule for FrontMatter {
fn run(state: &mut BlockState<'_, '_>) -> Option<(Node, usize)> {
// check the parent is the document Root
if !state.node.is::<Root>() {
return None;
}
// check we are on the first line of the document
if state.line != 0 {
return None;
}
// check line starts with opening dashes
let opening = state
.get_line(state.line)
.chars()
.take_while(|c| *c == '-')
.collect::<String>();
if !opening.starts_with("---") {
return None;
}
// Search for the end of the block
let mut next_line = state.line;
loop {
next_line += 1;
if next_line >= state.line_max {
return None;
}
let line = state.get_line(next_line);
if line.starts_with(&opening) {
break;
}
}
let (content, _) = state.get_lines(state.line + 1, next_line, 0, true);
Some((Node::new(FrontMatter { content }), next_line + 1))
}
}

View File

@@ -12,19 +12,24 @@ use axum::{
use chrono::{DateTime, Utc};
use libservice::ServiceConnectInfo;
use lru::LruCache;
use maud::{Markup, Render, html};
use maud::{Markup, PreEscaped, Render, html};
use parking_lot::Mutex;
use serde::Deserialize;
use std::{collections::HashMap, num::NonZero, sync::Arc, time::Duration};
use tracing::{debug, trace};
use crate::components::{md::Markdown, misc::Backlinks};
use crate::components::{
md::{FrontMatter, Markdown},
misc::Backlinks,
};
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize)]
pub struct PageMetadata {
pub title: String,
pub author: Option<String>,
pub description: Option<String>,
pub image: Option<String>,
pub slug: Option<String>,
}
impl Default for PageMetadata {
@@ -34,6 +39,7 @@ impl Default for PageMetadata {
author: None,
description: None,
image: None,
slug: None,
}
}
}
@@ -103,16 +109,37 @@ impl Page {
(self.generate_html)(self)
}
pub fn from_markdown(meta: PageMetadata, md: impl Into<String>) -> Self {
pub fn from_markdown(md: impl Into<String>, default_image: Option<String>) -> Self {
let md: String = md.into();
let md = Markdown::parse(&md);
let mut meta = md
.children
.get(0)
.map(|x| x.cast::<FrontMatter>())
.flatten()
.map(|x| serde_yaml::from_str::<PageMetadata>(&x.content))
.unwrap_or(Ok(Default::default()))
.unwrap_or(PageMetadata {
title: "Invalid frontmatter!".into(),
..Default::default()
});
if meta.image.is_none() {
meta.image = default_image
}
let html = PreEscaped(md.render());
// TODO: define metadata and backlinks in markdown
Page {
meta,
generate_html: Box::new(move |page| {
html! {
(Backlinks(&[("/", "home")], &page.meta.title))
(Markdown(&md))
@if let Some(slug) = &page.meta.slug {
(Backlinks(&[("/", "home")], slug))
}
(html)
}
}),

View File

@@ -1,34 +1,10 @@
use assetserver::Asset;
use maud::html;
---
title: "What's a \"betalupi?\""
author: "Mark"
slug: whats-a-betalupi
---
use crate::{
components::{md::Markdown, misc::Backlinks},
page::{Page, PageMetadata},
routes::assets::{Image_Betalupi, Image_Icon},
};
pub fn page() -> Page {
Page {
meta: PageMetadata {
title: "What's a \"betalupi?\"".into(),
author: Some("Mark".into()),
description: None,
image: Some(Image_Icon::URL.into()),
},
generate_html: Box::new(|_page| {
html! {
(Backlinks(&[("/", "home")], "whats-a-betalupi"))
(Markdown(MD))
img alt="betalupi map" class="image" src=(Image_Betalupi::URL) {}
}
}),
..Default::default()
}
}
const MD: &str = r#"[es]: https://github.com/endless-sky/endless-sky
[es]: https://github.com/endless-sky/endless-sky
[*Stellaris*]: https://www.paradoxinteractive.com/games/stellaris/about
[Arabic]: https://en.wikipedia.org/wiki/List_of_Arabic_star_names
[wiki-betalupi]: https://en.wikipedia.org/wiki/Beta_Lupi
@@ -54,4 +30,5 @@ A snippet of the [_Endless Sky_][es] map is below.
- Isn't owned by a scalper that's selling it for $300"
<br/>
"#;
<img alt="betalupi map" class="image" src="/assets/img/betalupi.png"></img>

View File

@@ -1,33 +1,10 @@
use assetserver::Asset;
use maud::html;
---
title: Mark's Handouts
author: Mark
slug: handouts
---
use crate::{
components::{md::Markdown, misc::Backlinks},
page::{Page, PageMetadata},
routes::assets::Image_Icon,
};
pub fn page() -> Page {
Page {
meta: PageMetadata {
title: "Mark's Handouts".into(),
author: Some("Mark".into()),
description: None,
image: Some(Image_Icon::URL.into()),
},
generate_html: Box::new(|_page| {
html! {
(Backlinks(&[("/", "home")], "handouts"))
(Markdown(MD_A))
}
}),
..Default::default()
}
}
const MD_A: &str = r#"# Mark's Handouts
# Mark's Handouts
[ORMC]: https://circles.math.ucla.edu/circles
@@ -155,6 +132,8 @@ giving them something to do until we can start the lesson.
</script>
<br></br>
## Advanced
@@ -250,5 +229,4 @@ they're ~14-18 years old.
});
</script>
"#;
<br></br>

View File

@@ -12,13 +12,14 @@ use crate::{
routes::assets::{Image_Cover, Image_Icon},
};
pub fn page() -> Page {
pub fn index() -> Page {
Page {
meta: PageMetadata {
title: "Betalupi: About".into(),
author: Some("Mark".into()),
description: Some("Description".into()),
image: Some(Image_Icon::URL.into()),
slug: None,
},
generate_html: Box::new(move |_page| {

View File

@@ -1,8 +1,15 @@
---
title: Links
author: Mark
slug: links
---
# Bookmarks
This is a heavily opinionated bookmarks toolbar.
<hr style="margin-top: 8rem; margin-bottom: 8rem"></hr>
<hr style="margin-top: 4rem; margin-bottom: 4rem"></hr>
## Podcasts
@@ -13,12 +20,18 @@ This is a heavily opinionated bookmarks toolbar.
- :star: [On the Metal](https://onthemetal.transistor.fm/): Quality stories from quality engineers.
- [Security Cryptography Whatever](https://securitycryptographywhatever.com/): Modern cryptography, for those who understand the underlying theory.
<br></br>
## Essays
- [Real Programmers don't use Pascal](https://www.ee.torontomu.ca/~elf/hack/realmen.html)
- [A Mathematician's Lament](/files/lockhart.pdf)
- :star: [The Jargon File](http://www.catb.org/jargon/)
<br></br>
## Textbooks
- :star: [OpenLogic](https://openlogicproject.org/): The gold standard
@@ -28,6 +41,9 @@ This is a heavily opinionated bookmarks toolbar.
- [An Introduction to Mathematical Cryptography](https://link.springer.com/book/10.1007/978-0-387-77993-5)
- [Stories about Maxima and Minima](https://archive.org/details/storiesaboutmaxi0000tikh)
<br></br>
## Miscellanea
- [Hackaday](https://hackaday.com/)
@@ -41,7 +57,7 @@ This is a heavily opinionated bookmarks toolbar.
- :star: [Spintronics](https://store.upperstory.com/collections/spintronics/products/spintronics-act-one): Mechanical circuits. Very clever toy.
- :star: [Turing Tumble](https://store.upperstory.com/collections/turing-tumble-game/products/turing-tumble): Modern Dr. Nim
<hr style="margin-top: 8rem; margin-bottom: 8rem"></hr>
<hr style="margin-top: 4rem; margin-bottom: 4rem"></hr>
## Tools
@@ -65,6 +81,9 @@ This is a heavily opinionated bookmarks toolbar.
- [delta](https://github.com/dandavison/delta): pretty pager for diffs
- [dust](https://github.com/dandavison/delta): `du`, but better
<br></br>
## Math Resources
- [Quantum Quest](https://www.quantum-quest.org/)
@@ -74,6 +93,9 @@ This is a heavily opinionated bookmarks toolbar.
- [Euclidea](https://www.euclidea.xyz/)
- [Problems.ru](https://problems.ru/)
<br></br>
## OS Dev Resources
- [OS Dev Wiki](https://wiki.osdev.org/Expanded_Main_Page)
@@ -84,6 +106,9 @@ This is a heavily opinionated bookmarks toolbar.
- [FDC Programming](http://www.brokenthorn.com/Resources/OSDev20.html)
- [CS77 at Bristol College](http://www.c-jump.com/CIS77/CIS77syllabus.htm)
<br></br>
## Misc Resources
- [Learn OpenGL](https://learnopengl.com/)
@@ -99,6 +124,10 @@ This is a heavily opinionated bookmarks toolbar.
- [The Architecture of Open Source Applications](http://aosabook.org/en/index.html)
- [wtfjs](https://github.com/denysdovhan/wtfjs): js [wat](https://www.destroyallsoftware.com/talks/wat)s
<br></br>
## Reference
- [DevHints](https://devhints.io/)
@@ -108,12 +137,15 @@ This is a heavily opinionated bookmarks toolbar.
- [The Pinouts Book](https://pinouts.org/)
- [Laws of UX](https://lawsofux.com/)
<br></br>
## Rust
- [Understanding Memory Ordering in Rust](https://emschwartz.me/understanding-memory-ordering-in-rust/)
- [Unfair Rust Quiz](https://this.quiz.is.fckn.gay/): wtfjs, but in Rust.
<hr style="margin-top: 8rem; margin-bottom: 8rem"></hr>
<hr style="margin-top: 4rem; margin-bottom: 4rem"></hr>
## Misc

View File

@@ -1,35 +0,0 @@
use assetserver::Asset;
use maud::html;
use crate::{
components::{md::Markdown, misc::Backlinks},
page::{Page, PageMetadata},
routes::assets::Image_Icon,
};
pub fn page() -> Page {
Page {
meta: PageMetadata {
title: "Links".into(),
author: Some("Mark".into()),
description: None,
image: Some(Image_Icon::URL.into()),
},
generate_html: Box::new(|_page| {
html! {
(Backlinks(&[("/", "home")], "links"))
(Markdown(include_str!("links.md")))
}
}),
..Default::default()
}
}
/*
Dead links:
https://www.commitstrip.com/en/
http://www.3dprintmath.com/
*/

View File

@@ -1,4 +1,30 @@
pub mod betalupi;
pub mod handouts;
pub mod index;
pub mod links;
mod index;
use assetserver::Asset;
pub use index::index;
use crate::{page::Page, routes::assets::Image_Icon};
pub fn links() -> Page {
/*
Dead links:
https://www.commitstrip.com/en/
http://www.3dprintmath.com/
*/
Page::from_markdown(include_str!("links.md"), Some(Image_Icon::URL.to_string()))
}
pub fn betalupi() -> Page {
Page::from_markdown(
include_str!("betalupi.md"),
Some(Image_Icon::URL.to_string()),
)
}
pub fn handouts() -> Page {
Page::from_markdown(
include_str!("handouts.md"),
Some(Image_Icon::URL.to_string()),
)
}

View File

@@ -0,0 +1,229 @@
+++
title = "HtWaH: Typesetting"
template = "page.html"
[extra]
show_title = false
back_links = [
{target = "/htwah", text = "htwah"}
]
+++
## Table of Contents
This page is part of my [how to write a handout](/htwah) series.
- [Part 1: Typesetting](.) {{ color(c="green", t="**<-- you are here**") }}
- Part 2: Notation
- Part 3: Designing lessons
- Part 4: Designing puzzles
- Part 5: Leading a class
<hr style="margin-top: 5rem; margin-bottom: 5rem"/>
## The Medium
### Always teach in print
A physical handout is three-dimensional. Every point inside it has a horizontal, vertical, and azimuthal position. This makes it easy to navigate, and thus easier to understand.
Navigation is inseparable from memory---this fact has been known for millennia.[^1]
Lessons presented on a screen do not take advantage of this; lessons presented on paper do.
### Never print on both sides
Double-sided handouts break this spacial intuition. The act of turning a page upside-down severs its connection to physical space. Do not entangle your students in the pages of a double-sided worksheet---print only on one side.
{{color(c="grey", t="This is not true of books, which are presented in a fundamentally different way.")}}
Double-sided handouts also clutter the page with the opposite side's nodes. Even when using a pencil, work on the front of a page makes its way to the back.
<br/>
> Humans live and learn in the real world. \
> Paper is a renewable resource. \
> Edu-tech is bullshit.
[^1]: See: method of loci
<hr style="margin-top: 5rem; margin-bottom: 5rem"/>
## Spacing
Writing is visual. It catches the eye before it has a chance to catch the brain.[^2] A good lesson takes advantage of this: it uses space to frame its content.
Take care to organize the text you produce. Provide a clear visual boundary between separate ideas---unbroken waterfalls mathematical text are difficult to parse---and be sure to place visual boundaries only where there are logical ones.
Use empty space generously, but never use it wastefully. Too much space is just as distracting as too little.
### Leave space for work
If you force your students to cram their work into the margin,
their understanding will have a similar quality. \
Give them space to work and space to think.
{{color(c="grey", t="This rule only applies to handouts. Lecture notes and textbooks do not need to worry about this, since students should work through them on a separate sheet of paper.")}}
Every problem should be followed by a `\vfill`. \
There are exceptions, but they are rare.
If your handouts include solutions
{{color(c="grey", t="(they should),")}}
observe the following rule: each problem's typeset
solution should fit in the empty space after the problem.
This isn't perfectly accurate estimate of space
(your students will likely need more), but it is a very good minimum.
{{color(c="grey", t="If your students will be drawing pictures, make each gap twice as bit as it needs to be.")}}
In a handout that is typeset well, the layout of each page should not
change when solutions are shown. This helps you give students enough
room to solve each problem, and keeps page numbers consistent between
instructors' and students' handouts:
{{ twopdf(left="/htwah/sols-a.pdf", right="/htwah/sols-b.pdf") }}
<br>
### Break with intention
A page should _never_ break in the middle of a definition, problem, or theorem.
A page should _never_ break between the first mention of a concept and the problems used to introduce it.
New sections should _always_ start on a new page.
{{color(c="grey", t="Unlike the previous comments on spacing, the above rules apply to all formats. Whether you are writing a textbook, a handout, or lecture notes, do not allow your page breaks to break as student's train of thought.")}}
These rules are easy to follow if you leave space for work: \
Place your `\pagebreak`s well and let `\vfill` handle the rest.
### Separation
Problems, definitions, and theorems should come in clear blocks.
It should be easy to tell which element of a page each line of text
relates to[^3].
The [Open Logic Project](https://openlogicproject.org/) provides a
striking example of this. Definitions are boxed and titled---it is abundandly
clear that these are the definitions we will be using in the future.
It is also a good idea to mark the first use of a term. \
In the example below, this is done with italicized text.
{{ onepdf(src="/htwah/definitions.pdf") }}
Of course, not all texts need such aggressive grouping. I tend to avoid clear "boxing" in [my lessons](/handouts), opting for subtle grouping by whitespace instead. This is a matter of taste.
{{color(c="grey", t="Regardless of implementation, visual boundaries should match conceptual boundaries. It should be easy to see where each logical element ends.")}}
Finally, remember that too much separation is just as distracting as too little.
Balance is key.
### Proximity
Different elements should be far apart, and related elements should be close together[^4]. This rule is often violated by equations and diagrams.
Consider the two pages below:
{{ twopdf(left="/htwah/spacing-a.pdf", right="/htwah/spacing-b.pdf") }}
The circuit diagram on the left is clearly a part of the setup at the top of the page. It is not connected to Problem 14.
Compare this to the equations under Examples 1.25 and 1.26 on the right. These elements do not have a clear owner, since they are just as far from the text above them as the text below.
> The visual layout of a lesson must match its logical layout. \
> Similar items are close, different items are far. \
> Key information should be clearly visible and easy to find.
[^2]: See "Paragraphs" in William Zinsser's _On Writing Well_.
[^3]: See: [the chunking principle](https://lawsofux.com/chunking/)
[^4]: See: [the law of proximity](https://lawsofux.com/law-of-proximity/)
<hr style="margin-top: 5rem; margin-bottom: 5rem"/>
## Numbering
### Pages
Pages should be numbered unless the document only has one page. There are no exceptions to this rule.
Numbering should be simple: use a single counter that is set to 1 on the first page of the document. Every page should have a number, even if that number isn't visible. This includes frontmatter and the table of contents.
Different numbering systems (`i, ii, iii...` for a preface; `A31, A32, A33...` for appendices) have little value and only make navigation more difficult.
{{color(c="grey", t="Roman numbering of frontmatter in large textbooks is acceptable, but anything more complicated is unnecessary.")}}
### Versions
The document itself should also be numbered. In most cases, a `\today` on the front page of the lesson is all you need.
This helps synchronize the handout you _think_ the class has with the handout that the class _really_ has.
Future instructors {{color(c="grey", t="(and future you)")}} will be thankful.
### Items
Propositions, definitions, and examples should all be numbered with the same counter. Problems should also use this counter, unless they are only listed at the end of each section {{color(c="grey", t="(as they often are in textbooks).")}}
Do not use different counters for different objects. Theorem 1 should not follow Definition 2. This is the default behavior of LaTeX, and it is a serious mistake.
Such a numbering system makes it difficult for readers to orient themselves. Say a student is solving Problem 6, which references Theorem 2. Where should she look? Is Theorem 2 on the previous page, or is it near the beginning of the lesson? It could even be on the _next_ page!
The only choice she has is to look through the lesson page-by-page until she finds Theorem 2. Finding Theorem 3 will not help---it would tell her where Theorem 2 _isn't_, but it won't tell her where it _is_.
With a single counter, this is not an issue. Readers are aware of their absolute position at every point in the lesson, and can easily find what they need.
[Open Logic](https://openlogicproject.org/) again provides an example of quality typesetting. Notice how the numbering of Propositions, Definitions, and Examples on the page below is consecutive:
{{ onepdf(src="/htwah/numbering.pdf") }}
In long textbooks, prefixing numbers with the chapter index (e.g, the `2` in `2.32` above) is wise. In any other case, a single counter should be enough.
<br>
> Good numbering is simple and consistent. \
> Don't tell me to "look for the fourth theorem". \
> Tell me _where it is_.
<hr style="margin-top: 5rem; margin-bottom: 5rem"/>
## Margins
### Always have a title
Any document you produce should be identifiable by its first page.
Always have a title that tells the reader what they're looking at,
who made it, when, and why.
If you're writing a document that is available in multiple variants (for example, a handout with solutions or an exam with multiple versions), the variant should be easily visible in the title. Note the box under the title in the handout on the left.
{{ twopdf(left="/htwah/sols-a.pdf", right="/htwah/sols-b.pdf") }}
This lets us detect errors quickly: we only need to look at the first page of a lesson to know if we printed the wrong variant, handed the students solutions, or graded an exam using the wrong answer key.
### Never have a running header
We already have a title, so a running header adds no value. \
Let your students fill the margin.
### Avoid footnotes
Do not put important information in footnotes. \
Find a way to state it directly.
Footnotes are for trivia, historical notes, or extra reading. \
They are for information your lesson could live without.
This reasoning can be applied to parentheticals, hints, and other auxiliary text:
as a general rule, it should be avoided.
Important information deserves a place in the main flow of the document,
and you should structure your prose so that it fits. Don't stick it on
as an afterthought.
<br>
> If you don't need it, take it out. \
> If you do, put it in. \
> Always introduce yourself.
> Never do so twice.

View File

@@ -1,9 +1,14 @@
use assetserver::Asset;
use axum::Router;
use maud::{DOCTYPE, PreEscaped, html};
use maud::{DOCTYPE, Markup, PreEscaped, html};
use tracing::info;
use crate::{components::misc::FarLink, page::PageServer, pages, routes::assets::Styles_Main};
use crate::{
components::misc::FarLink,
page::{Page, PageServer},
pages,
routes::assets::Styles_Main,
};
pub mod assets;
@@ -11,52 +16,64 @@ pub(super) fn router() -> Router<()> {
let (asset_prefix, asset_router) = assets::asset_router();
info!("Serving assets at {asset_prefix}");
let server = PageServer::new(Box::new(|page| {
html! {
(DOCTYPE)
html {
head {
meta charset="UTF" {}
meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {}
meta content="text/html; charset=UTF-8" http-equiv="content-type" {}
meta property="og:type" content="website" {}
let server = build_server().into_router();
link rel="stylesheet" href=(Styles_Main::URL) {}
Router::new().merge(server).nest(asset_prefix, asset_router)
}
(&page.meta)
title { (PreEscaped(page.meta.title.clone())) }
}
fn build_server() -> PageServer {
PageServer::new(Box::new(page_wrapper))
.add_page("/", pages::index())
.add_page("/links", pages::links())
.add_page("/whats-a-betalupi", pages::betalupi())
.add_page("/handouts", pages::handouts())
}
body {
div class="wrapper" {
main { ( page.generate_html() ) }
fn page_wrapper(page: &Page) -> Markup {
html! {
(DOCTYPE)
html {
head {
meta charset="UTF" {}
meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" {}
meta content="text/html; charset=UTF-8" http-equiv="content-type" {}
meta property="og:type" content="website" {}
footer {
hr class = "footline" {}
div class = "footContainer" {
p {
"This site was built by hand using "
(FarLink("https://rust-lang.org", "Rust"))
", "
(FarLink("https://maud.lambda.xyz", "Maud"))
", "
(FarLink("https://github.com/connorskees/grass", "Grass"))
", and "
(FarLink("https://docs.rs/axum/latest/axum", "Axum"))
"."
}
link rel="stylesheet" href=(Styles_Main::URL) {}
(&page.meta)
title { (PreEscaped(page.meta.title.clone())) }
}
body {
div class="wrapper" {
main { ( page.generate_html() ) }
footer {
hr class = "footline" {}
div class = "footContainer" {
p {
"This site was built by hand using "
(FarLink("https://rust-lang.org", "Rust"))
", "
(FarLink("https://maud.lambda.xyz", "Maud"))
", "
(FarLink("https://github.com/connorskees/grass", "Grass"))
", and "
(FarLink("https://docs.rs/axum/latest/axum", "Axum"))
"."
}
}
}
}
}
}
}))
.add_page("/", pages::index::page())
.add_page("/links", pages::links::page())
.add_page("/whats-a-betalupi", pages::betalupi::page())
.add_page("/handouts", pages::handouts::page())
.into_router();
Router::new().merge(server).nest(asset_prefix, asset_router)
}
}
#[test]
fn server_builds_without_panic() {
// Catches some runtime errors thrown by axum,
// e.g bad route nesting or routes not starting with "/"
let _server = build_server().into_router();
}