Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/auxesia/src/content.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pub struct Capture {
#[derive(Clone, Debug)]
pub struct Page {
pub title: String,
pub navbar: Option<String>,
pub description: Option<String>,
pub index: bool,
pub source: Arc<[u8]>,
Expand All @@ -69,6 +70,8 @@ pub struct Site {

#[derive(Deserialize)]
struct Frontmatter {
#[serde(default, deserialize_with = "crate::config::optional_string")]
navbar: Option<String>,
#[serde(default, deserialize_with = "crate::config::optional_string")]
title: Option<String>,
#[serde(default, deserialize_with = "crate::config::optional_string")]
Expand Down Expand Up @@ -473,6 +476,7 @@ fn parse_page(
}
Ok(Page {
title,
navbar: meta.navbar.filter(|v| !v.trim().is_empty()),
description,
index: meta.index,
source: file.bytes.clone(),
Expand Down
20 changes: 20 additions & 0 deletions crates/auxesia/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,26 @@ fn shell(site: &Site, path: &str, title: &str, description: Option<&str>, body:
let mut navigation: Vec<_> = entries(site, dir)
.into_iter()
.filter(|(link, _)| Some(link) != directory)
.map(|(link, _)| {
let label = if link.ends_with('/') {
directory_page(site, &link)
.filter(|(_, page)| page.index)
.and_then(|(_, page)| page.navbar.clone())
.unwrap_or_else(|| {
link.trim_end_matches('/')
.rsplit('/')
.next()
.unwrap()
.to_owned()
})
} else {
let page = &site.pages[&link];
page.navbar
.clone()
.unwrap_or_else(|| page.source_path.rsplit('/').next().unwrap().to_owned())
};
(link, label)
})
.collect();
navigation.sort_by_cached_key(|(link, title)| {
(
Expand Down
62 changes: 51 additions & 11 deletions crates/auxesia/tests/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ async fn footer_supports_markdown_defaults_and_removal() {
}

#[tokio::test]
async fn header_uses_dot_for_index_and_sorts_links_by_title() {
async fn header_uses_dot_for_index_and_sorts_links_by_filename() {
let root = TempDir::new().unwrap();
for prefix in ["", "guide/"] {
put(
Expand Down Expand Up @@ -170,10 +170,10 @@ async fn header_uses_dot_for_index_and_sorts_links_by_title() {
};
assert!(nav.contains(&format!("href=\"{dir}\">.</a>")));
assert!(!nav.contains("index.md"));
assert!(nav.find(">apple</a>").unwrap() < nav.find(">Zebra</a>").unwrap());
assert!(nav.find(">.</a>").unwrap() < nav.find(">apple</a>").unwrap());
assert!(nav.find(">a.md</a>").unwrap() < nav.find(">z.md</a>").unwrap());
assert!(nav.find(">.</a>").unwrap() < nav.find(">a.md</a>").unwrap());
if dir == "/guide/" {
assert!(nav.find(">..</a>").unwrap() < nav.find(">apple</a>").unwrap());
assert!(nav.find(">..</a>").unwrap() < nav.find(">a.md</a>").unwrap());
}
}
assert_eq!(
Expand All @@ -185,7 +185,7 @@ async fn header_uses_dot_for_index_and_sorts_links_by_title() {
}

#[tokio::test]
async fn header_groups_folders_and_uses_directory_titles_or_readable_names() {
async fn header_groups_folders_and_uses_navbar_overrides_or_folder_names() {
let root = TempDir::new().unwrap();
for (path, title) in [
("z-folder/index.md", "Alpha & reference"),
Expand All @@ -205,7 +205,7 @@ async fn header_groups_folders_and_uses_directory_titles_or_readable_names() {
put(
&root,
path,
&format!("---\ntitle: {title}\n{hidden}---\nBody"),
&format!("---\ntitle: {title}\nnavbar: {title}\n{hidden}---\nBody"),
);
}
let app = App::open(root.path(), false).unwrap();
Expand All @@ -219,16 +219,16 @@ async fn header_groups_folders_and_uses_directory_titles_or_readable_names() {
.unwrap();
let labels = [
"Alpha &amp; reference",
"Getting started",
"Private notes",
"getting-started",
"private-notes",
"Zulu guides",
"apple page",
"Zebra page",
];
let positions: Vec<_> = labels
.iter()
.map(|label| {
nav.find(label)
nav.find(&format!("{label}</a>"))
.unwrap_or_else(|| panic!("missing {label}: {nav}"))
})
.collect();
Expand Down Expand Up @@ -610,7 +610,7 @@ async fn sidebar_hides_the_page_that_supplies_the_directory() {
let html = String::from_utf8(request(&app, path, None, "GET").await.2).unwrap();
let nav = nav(&html);
assert!(nav.contains("href=\"/guide/\">.</a>"), "{path}: {nav}");
assert!(nav.contains(">Child</a>"), "{path}: {nav}");
assert!(nav.contains(">child.md</a>"), "{path}: {nav}");
assert!(!nav.contains("README.md"), "{path}: {nav}");
assert!(!nav.contains("Readme title"), "{path}: {nav}");
}
Expand All @@ -624,7 +624,7 @@ async fn sidebar_hides_the_page_that_supplies_the_directory() {
let nav = nav(&html);
assert!(!nav.contains("index.md"), "{nav}");
assert!(
nav.contains("href=\"/guide/README.md\">Readme title</a>"),
nav.contains("href=\"/guide/README.md\">README.md</a>"),
"{nav}"
);
}
Expand Down Expand Up @@ -916,6 +916,46 @@ async fn favicon_outputs_are_images_and_refresh_atomically() {
assert!(!root.path().join(".auxesia").exists());
}

#[tokio::test]
async fn navbar_overrides_are_escaped_and_do_not_replace_metadata() {
let root = TempDir::new().unwrap();
for (path, metadata) in [
("plain.md", "title: Full page title"),
("custom.md", "title: Another title\nnavbar: 'Short & sweet'"),
("blank.md", "title: Blank override\nnavbar: ' '"),
("skills/task/SKILL.md", "name: Task title"),
("skills/other.md", "name: Other title\nnavbar: Custom skill"),
] {
put(&root, path, &format!("---\n{metadata}\n---\nBody"));
}
let app = App::open(root.path(), false).unwrap();
for (path, labels) in [
(
"/custom.md",
vec!["plain.md", "Short &amp; sweet", "blank.md"],
),
("/skills/", vec!["SKILL.md", "Custom skill"]),
] {
let html = String::from_utf8(request(&app, path, None, "GET").await.2).unwrap();
let nav = html
.split("<nav ")
.nth(1)
.unwrap()
.split("</nav>")
.next()
.unwrap();
for label in labels {
assert!(nav.contains(&format!(">{label}</a>")), "{nav}");
}
assert!(!nav.contains("Full page title"));
if path == "/custom.md" {
assert!(html.contains("content=\"Another title\""));
}
}
let llms = String::from_utf8(request(&app, "/llms.txt", None, "GET").await.2).unwrap();
assert!(llms.contains("[Another title](/custom.md)"));
}

#[tokio::test]
async fn svg_favicons_render_with_transparency_and_refresh_atomically() {
let root = TempDir::new().unwrap();
Expand Down
18 changes: 14 additions & 4 deletions docs/authoring.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: Write pages
title: Write pages
description: Write Markdown pages with frontmatter, links, directory pages, and local assets.
---
Expand All @@ -13,14 +14,15 @@ Create a file such as `guide.md` in the documentation folder:
```markdown
---
title: Project guide
navbar: Guide
description: Learn how to use the project.
---
# Project guide

This page explains the project.
```

The file is available at `/guide.md`. The `title` supplies navigation labels and HTML metadata. The Markdown body supplies the visible page content.
The file is available at `/guide.md`. The `title` supplies HTML metadata and discovery entries. The Markdown body supplies the visible page content.

The `description` supplies HTML metadata and the summary in `/llms.txt`. A missing or blank description causes a warning. It does not prevent the server from publishing the page.

Expand Down Expand Up @@ -66,11 +68,19 @@ For example, `reference/index.md` supplies the page at `/reference/`. The file a

Without either file, Auxesia generates a directory page from the indexed pages. The root directory uses the same rules.

Folder links use the title from `index.md`, or from `README.md` when no index exists.
Without a discoverable directory page, Auxesia converts the folder name into a readable label, such as “Getting started.”
Navbar links use exact filenames, including the `.md` suffix.
The optional `navbar` field replaces this label.
For example, `navbar: Guide` shows “Guide” instead of “guide.md”.
Missing, null, empty, or whitespace-only values use the filename.

Folder links use the folder name.
A discoverable `index.md` can override this label with `navbar`.
If no index exists, Auxesia uses the `navbar` value from a discoverable `README.md`.

Skill links use the source filename, such as `SKILL.md`, unless the skill frontmatter supplies `navbar`.

The header keeps `.` and `..` first, then lists folders before pages.
Each group sorts alphabetically by display title, ignoring case. Folder links include an outline icon.
Each group sorts alphabetically by display label, ignoring case. Folder links include an outline icon.
The current directory's `index.md` appears only as `.` in the header.

## Control discovery
Expand Down
1 change: 1 addition & 0 deletions docs/deployment.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: Deploy with Docker
title: Deploy with Docker
description: Build the Docker image, serve a documentation folder, and publish images to GHCR.
---
Expand Down
1 change: 1 addition & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: Get started
title: Get started
description: Run the Auxesia documentation, install the server, and serve your own pages.
---
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: Home
title: Auxesia
description: Serve Markdown documentation and downloadable skills from a local folder.
---
Expand Down
8 changes: 5 additions & 3 deletions docs/reference/configuration.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: Configuration
title: Configuration reference
description: Configure site metadata, custom CSS, ignored paths, and page frontmatter.
---
Expand Down Expand Up @@ -147,14 +148,15 @@ index: true

| Field | Type | Default | Behavior |
| --- | --- | --- | --- |
| `title` | String | Required for documentation | Title in the HTML page, navigation, and discovery entries. |
| `name` | String | Required for skill entrypoints | Skill title in the HTML page, navigation, and discovery entries. |
| `title` | String | Required for documentation | Title in the HTML page and discovery entries. |
| `name` | String | Required for skill entrypoints | Skill title in the HTML page and discovery entries. |
| `navbar` | Optional string | Filename or folder name | Navbar label override. Missing, null, empty, or whitespace-only values use the default. |
| `description` | Optional string | None | HTML description metadata and text in `/llms.txt`. |
| `index` | Boolean | `true` | Include the page in directory navigation and `/llms.txt`. |

Missing, null, empty, or whitespace-only descriptions produce warnings.
They do not prevent publication.
The `title`, `name`, and `description` fields accept strings or null, but required titles and names cannot be null.
The `title`, `name`, `navbar`, and `description` fields accept strings or null, but required titles and names cannot be null.
Unknown frontmatter fields are accepted.

The `index: false` value hides a page from discovery.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/http.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: HTTP
title: HTTP reference
description: Understand page representations, directory routes, redirects, static files, and discovery responses.
---
Expand Down
1 change: 1 addition & 0 deletions docs/reference/mcp.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: MCP
title: MCP reference
description: Connect an MCP client to /mcp to list skills and read their files with tools.
---
Expand Down
1 change: 1 addition & 0 deletions docs/reference/operations.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: Operations
title: Operations reference
description: Understand startup errors, request refreshes, snapshot publication, and deployment limits.
---
Expand Down
1 change: 1 addition & 0 deletions docs/reference/skills.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: Skills
title: Skills
description: Publish agent instructions as Markdown pages and downloadable skill archives.
---
Expand Down
1 change: 1 addition & 0 deletions docs/skills/serve-auxesia/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
---
navbar: Serve Auxesia
name: serve-auxesia
description: Run Auxesia from its Docker image and inspect documentation, Markdown, discovery, and skill archive responses.
---
Expand Down