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
1 change: 1 addition & 0 deletions crates/forge-docs-check/src/checks/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ pub mod cli_commands;
pub mod counts;
pub mod crate_pages;
pub mod forge_docs;
pub mod slug_prefix;

use std::path::Path;

Expand Down
103 changes: 103 additions & 0 deletions crates/forge-docs-check/src/checks/slug_prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//! Rule `slug-prefix`: every docs page is mounted under `/docs/`.
//!
//! The site serves a marketing landing page at `/` (`site/src/pages/index.astro`)
//! and the Starlight docs under `/docs/`. Starlight derives each page's URL from
//! its `slug:` frontmatter, so a page whose slug lacks the `docs/` prefix would
//! render at the site root (e.g. `/crates/forge/`) — and every authored
//! `](/docs/crates/forge)` link to it would 404.
//!
//! This rule asserts every content page's slug is under `docs/`, so a generator
//! (e.g. [`crate::cratepage`]) or a hand edit that emits a root-level slug fails
//! CI instead of silently breaking site navigation.

use crate::checks::read_optional;
use crate::discovery::Workspace;
use crate::Finding;
use std::path::{Path, PathBuf};

pub fn check(ws: &Workspace) -> Vec<Finding> {
let mut findings = Vec::new();
for page in markdown_pages(&ws.docs_dir()) {
let src = match read_optional(&page) {
Some(s) => s,
None => continue,
};
let slug = match frontmatter_slug(&src) {
Some(s) => s,
None => continue, // no explicit slug -> path-derived; not this rule's concern
};
if slug != "docs" && !slug.starts_with("docs/") {
let rel = page
.strip_prefix(&ws.root)
.unwrap_or(&page)
.components()
.map(|c| c.as_os_str().to_string_lossy())
.collect::<Vec<_>>()
.join("/");
findings.push(Finding::new(
"slug-prefix",
format!(
"{rel}: slug `{slug}` is not under `docs/` — docs are served at /docs/, so this page (and links to it) would 404; use `slug: docs/{slug}`"
),
));
}
}
findings
}

/// Extract the value of the frontmatter `slug:` key (the first `slug:` line
/// inside the leading `---` block).
fn frontmatter_slug(src: &str) -> Option<String> {
let body = src.strip_prefix("---")?;
let end = body.find("\n---")?;
for line in body[..end].lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("slug:") {
return Some(rest.trim().trim_matches(['"', '\'']).to_string());
}
}
None
}
Comment on lines +50 to +60

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current frontmatter parser has two robustness issues:

  1. UTF-8 BOM Bypass: If a markdown file starts with a UTF-8 Byte Order Mark (\u{FEFF}), strip_prefix("---") will fail, causing the file to be silently skipped by this check.
  2. Nested Key False Positives: Trimming leading whitespace (line.trim()) before checking for the slug: prefix means nested keys (e.g., inside a description block or a nested YAML object) will be incorrectly parsed as the page's slug.

We can fix both issues by stripping the BOM if present and matching slug: only at the start of the line (without trimming leading whitespace).

Suggested change
fn frontmatter_slug(src: &str) -> Option<String> {
let body = src.strip_prefix("---")?;
let end = body.find("\n---")?;
for line in body[..end].lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("slug:") {
return Some(rest.trim().trim_matches(['"', '\'']).to_string());
}
}
None
}
fn frontmatter_slug(src: &str) -> Option<String> {
let src = src.strip_prefix("\u{feff}").unwrap_or(src);
let body = src.strip_prefix("---")?;
let end = body.find("\n---")?;
for line in body[..end].lines() {
if let Some(rest) = line.strip_prefix("slug:") {
return Some(rest.trim().trim_matches(['"', '\'']).to_string());
}
}
None
}


/// Recursively collect `.md`/`.mdx` files under `dir`, sorted for determinism.
fn markdown_pages(dir: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(d) = stack.pop() {
let entries = match std::fs::read_dir(&d) {
Ok(e) => e,
Err(_) => continue,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if matches!(
path.extension().and_then(|e| e.to_str()),
Some("md") | Some("mdx")
) {
out.push(path);
}
}
}
out.sort();
out
}

#[cfg(test)]
mod tests {
use super::frontmatter_slug;

#[test]
fn extracts_quoted_and_bare_slugs() {
assert_eq!(
frontmatter_slug("---\ntitle: x\nslug: docs/crates/forge\n---\nbody"),
Some("docs/crates/forge".to_string())
);
assert_eq!(
frontmatter_slug("---\nslug: \"crates/forge\"\n---\n"),
Some("crates/forge".to_string())
);
assert_eq!(frontmatter_slug("# no frontmatter\n"), None);
}
}
2 changes: 1 addition & 1 deletion crates/forge-docs-check/src/cratepage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ fn crate_source(krate: &CrateInfo) -> Option<String> {
pub fn render_crate_page(krate: &CrateInfo, description: &str, body: &str) -> String {
let stem = krate.crate_page_stem();
format!(
"---\ntitle: \"{}\"\ndescription: \"{}\"\nslug: crates/{}\n---\n\n{}\n",
"---\ntitle: \"{}\"\ndescription: \"{}\"\nslug: docs/crates/{}\n---\n\n{}\n",
krate.package_name,
description.replace('"', "\\\""),
stem,
Expand Down
1 change: 1 addition & 0 deletions crates/forge-docs-check/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ pub fn run_all_checks(ws: &Workspace) -> Report {
report.extend(checks::counts::check(ws));
report.extend(checks::cli_commands::check(ws));
report.extend(checks::forge_docs::check(ws));
report.extend(checks::slug_prefix::check(ws));
report.extend(apiblock::check(ws));
report.extend(exampleblock::check(ws));
report.extend(extindex::check(ws));
Expand Down
28 changes: 28 additions & 0 deletions crates/forge-docs-check/tests/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,34 @@ fn cli_doc_flags_stale_region_and_passes_when_current() {
);
}

#[test]
fn slug_prefix_flags_root_slug_and_passes_under_docs() {
// Docs are served under /docs/, so a page slug must start with `docs/`.
let fx = Fixture::new();
fx.write(
"site/src/content/docs/crates/forge.md",
"---\ntitle: forge\nslug: crates/forge\n---\n# forge\n",
);
let ws = fx.discover();
assert!(
any_contains(&checks::slug_prefix::check(&ws), "crates/forge"),
"a root-level slug must be flagged: {:?}",
messages(&checks::slug_prefix::check(&ws))
);

// Same page mounted under docs/ passes.
fx.write(
"site/src/content/docs/crates/forge.md",
"---\ntitle: forge\nslug: docs/crates/forge\n---\n# forge\n",
);
let ws = fx.discover();
assert!(
checks::slug_prefix::check(&ws).is_empty(),
"a docs/-prefixed slug must pass: {:?}",
messages(&checks::slug_prefix::check(&ws))
);
}

/// Sanity: discovery distinguishes extension crates and maps page stems.
#[test]
fn discovery_classifies_crates() {
Expand Down
8 changes: 4 additions & 4 deletions site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ export default defineConfig({
{
label: 'Getting Started',
items: [
'getting-started',
'architecture',
'internals',
'roadmap',
'docs/getting-started',
'docs/architecture',
'docs/internals',
'docs/roadmap',
],
},
{
Expand Down
10 changes: 5 additions & 5 deletions site/src/components/CodeExample.astro
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,23 @@ const indexHtmlCode = `<span class="tag">&lt;script&gt;</span>
<div class="host-modules">
<h3>Host Modules</h3>
<div class="modules-grid">
<a href="/api/runtime-ui/" class="module-badge">
<a href="/docs/api/runtime-ui/" class="module-badge">
<code>runtime:ui</code>
<span>Windows, Dialogs, Menus</span>
</a>
<a href="/api/runtime-fs/" class="module-badge">
<a href="/docs/api/runtime-fs/" class="module-badge">
<code>runtime:fs</code>
<span>File System Access</span>
</a>
<a href="/api/runtime-net/" class="module-badge">
<a href="/docs/api/runtime-net/" class="module-badge">
<code>runtime:net</code>
<span>HTTP Requests</span>
</a>
<a href="/api/runtime-sys/" class="module-badge">
<a href="/docs/api/runtime-sys/" class="module-badge">
<code>runtime:sys</code>
<span>System Info, Clipboard</span>
</a>
<a href="/api/runtime-process/" class="module-badge">
<a href="/docs/api/runtime-process/" class="module-badge">
<code>runtime:process</code>
<span>Spawn Processes</span>
</a>
Expand Down
6 changes: 3 additions & 3 deletions site/src/components/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
<span>Forge</span>
</a>
<div class="nav-links">
<a href="/getting-started/">Docs</a>
<a href="/api/runtime-ui/">API</a>
<a href="/docs/getting-started/">Docs</a>
<a href="/docs/api/runtime-ui/">API</a>
<a href="https://github.com/LayerDynamics/forge" target="_blank" rel="noopener">GitHub</a>
</div>
</div>
Expand Down Expand Up @@ -43,7 +43,7 @@
</div>

<div class="cta-buttons">
<a href="/getting-started/" class="btn btn-primary">Get Started</a>
<a href="/docs/getting-started/" class="btn btn-primary">Get Started</a>
<a href="https://github.com/LayerDynamics/forge" class="btn btn-secondary" target="_blank" rel="noopener">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
Expand Down
2 changes: 1 addition & 1 deletion site/src/components/QuickStart.astro
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const steps = [
</div>

<div class="cta-section">
<a href="/getting-started/" class="btn btn-primary">
<a href="/docs/getting-started/" class="btn btn-primary">
Read the Full Guide
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M5 12h14M12 5l7 7-7 7"/>
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/manifest.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Manifest Reference
description: The manifest.app.toml file defines your Forge application's metadata, defaults, bundle settings, and permissions.
slug: api/manifest
slug: docs/api/manifest
---

The manifest file defines your Forge application's metadata, default window configuration, bundling options, and permissions (capabilities).
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-app.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:app"
description: Application lifecycle management, metadata, and system integration for Forge applications.
slug: api/runtime-app
slug: docs/api/runtime-app
---

The `runtime:app` module provides application lifecycle management, metadata access, and system integration capabilities for Forge applications.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-crypto.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:crypto"
description: Cryptographic operations including random generation, hashing, HMAC, and symmetric encryption.
slug: api/runtime-crypto
slug: docs/api/runtime-crypto
---

The `runtime:crypto` module provides cryptographic operations for Forge applications, powered by the [ring](https://github.com/briansmith/ring) cryptography library.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-database.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:database"
description: "Full-featured SQLite database access for Forge applications"
slug: api/runtime-database
slug: docs/api/runtime-database
---

Full-featured SQLite database access for Forge applications with query execution, transactions, prepared statements, result streaming, and schema migrations.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-devtools.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:devtools"
description: "Browser DevTools control for debugging Forge WebView windows"
slug: api/runtime-devtools
slug: docs/api/runtime-devtools
---

Browser DevTools control extension for Forge applications. Open, close, and check the state of DevTools panels for WebView windows, enabling programmatic control over the debugging experience.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-display.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:display"
description: "Display and monitor information for Forge applications"
slug: api/runtime-display
slug: docs/api/runtime-display
---

Display and monitor information for Forge applications. Provides monitor enumeration, cursor position tracking, and display change event subscriptions.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-dock.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:dock"
description: "macOS dock customization for Forge applications - badges, bounce, icons, and menus"
slug: api/runtime-dock
slug: docs/api/runtime-dock
---

macOS dock icon customization for Forge applications. Control dock icon badges, bounce animations, visibility, custom icons, and right-click menus.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-fs.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:fs"
description: File system operations with capability-based access control.
slug: api/runtime-fs
slug: docs/api/runtime-fs
---

The `runtime:fs` module provides file system operations with capability-based access control.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-image_tools.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:image_tools"
description: "Image manipulation and format conversion for Forge applications"
slug: api/runtime-image_tools
slug: docs/api/runtime-image_tools
---

Image manipulation and format conversion utilities for Forge applications. Process PNG, SVG, and WebP images with operations for loading, saving, converting between formats, and applying transformations.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-ipc.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:ipc"
description: Inter-process communication between Deno and window renderers.
slug: api/runtime-ipc
slug: docs/api/runtime-ipc
---

The `runtime:ipc` module provides inter-process communication (IPC) between your Deno application and window renderers (WebViews).
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-lock.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:lock"
description: "Named resource locking for coordinating concurrent access in Forge applications"
slug: api/runtime-lock
slug: docs/api/runtime-lock
---

Named resource locking for Forge applications. Coordinate access to shared resources across async operations using token-based lock acquisition.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-log.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:log"
description: "Structured logging for Forge applications with host and browser output"
slug: api/runtime-log
slug: docs/api/runtime-log
---

Structured logging for Forge applications with dual output capabilities. Log to the host terminal via the Rust tracing system and/or forward logs to browser DevTools via IPC.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-net.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:net"
description: HTTP networking, WebSocket connections, and streaming capabilities with capability-based access control.
slug: api/runtime-net
slug: docs/api/runtime-net
---

The `runtime:net` module provides HTTP networking, WebSocket connections, and streaming capabilities with capability-based access control.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-os_compat.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:os_compat"
description: "Operating system compatibility information for cross-platform Forge applications"
slug: api/runtime-os_compat
slug: docs/api/runtime-os_compat
---

Operating system compatibility utilities for Forge applications. Query runtime environment information including OS type, architecture, and platform-specific path separators.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-path.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:path"
description: "Cross-platform path manipulation utilities for Forge applications"
slug: api/runtime-path
slug: docs/api/runtime-path
---

Cross-platform path manipulation utilities for Forge applications. All operations handle forward slashes on Unix and backslashes on Windows automatically.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-process.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:process"
description: Child process spawning and management capabilities.
slug: api/runtime-process
slug: docs/api/runtime-process
---

The `runtime:process` module provides child process spawning and management capabilities.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-protocol.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:protocol"
description: "Custom URL protocol handler for deep linking in Forge applications"
slug: api/runtime-protocol
slug: docs/api/runtime-protocol
---

Custom URL protocol handler for deep linking in Forge applications. Register custom URL schemes (like `myapp://`) to enable launching your app from browsers, emails, and other applications.
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/api/runtime-shell.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: "runtime:shell"
description: Shell integration, command execution, and desktop environment interaction for Forge applications.
slug: api/runtime-shell
slug: docs/api/runtime-shell
---

The `runtime:shell` module provides comprehensive shell integration and command execution capabilities, allowing Forge applications to interact with the operating system shell and desktop environment.
Expand Down
Loading
Loading