This commit is contained in:
2025-12-01 05:27:29 +01:00
parent 5dcdf72310
commit 0a731f83e5
20 changed files with 1128 additions and 1406 deletions

View File

@@ -71,9 +71,9 @@ pub struct DevArgs {
pub mut: pub mut:
host string = 'localhost' host string = 'localhost'
port int = 3000 port int = 3000
open bool = true // whether to open the browser automatically open bool = true // whether to open the browser automatically
watch_changes bool // whether to watch for changes in docs and rebuild automatically watch_changes bool = false // whether to watch for changes in docs and rebuild automatically
skip_generate bool // whether to skip generation (useful when docs are pre-generated, e.g., from atlas) skip_generate bool = false // whether to skip generation (useful when docs are pre-generated, e.g., from atlas)
} }
pub fn (mut s DocSite) open(args DevArgs) ! { pub fn (mut s DocSite) open(args DevArgs) ! {

View File

@@ -142,10 +142,6 @@ fn (mut generator SiteGenerator) page_generate(args_ Page) ! {
pagefile.write(c)! pagefile.write(c)!
generator.client.copy_pages(collection_name, page_name, pagefile.path_dir()) or {
generator.error("Couldn't copy pages for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")!
return
}
generator.client.copy_images(collection_name, page_name, pagefile.path_dir()) or { generator.client.copy_images(collection_name, page_name, pagefile.path_dir()) or {
generator.error("Couldn't copy images for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")! generator.error("Couldn't copy images for page:'${page_name}' in collection:'${collection_name}'\nERROR:${err}")!
return return

View File

@@ -0,0 +1,30 @@
module docusaurus
pub interface IDocClient {
mut:
// Path methods - get absolute paths to resources
get_page_path(collection_name string, page_name string) !string
get_file_path(collection_name string, file_name string) !string
get_image_path(collection_name string, image_name string) !string
// Existence checks - verify if resources exist
page_exists(collection_name string, page_name string) bool
file_exists(collection_name string, file_name string) bool
image_exists(collection_name string, image_name string) bool
// Content retrieval
get_page_content(collection_name string, page_name string) !string
// Listing methods - enumerate resources
list_collections() ![]string
list_pages(collection_name string) ![]string
list_files(collection_name string) ![]string
list_images(collection_name string) ![]string
list_pages_map() !map[string][]string
list_markdown() !string
// Image operations
// get_page_paths(collection_name string, page_name string) !(string, []string)
copy_images(collection_name string, page_name string, destination_path string) !
copy_files(collection_name string, page_name string, destination_path string) !
}

View File

@@ -1,7 +1,6 @@
module docusaurus module docusaurus
import incubaid.herolib.core.playbook { PlayBook } import incubaid.herolib.core.playbook { PlayBook }
import os
pub fn play(mut plbook PlayBook) ! { pub fn play(mut plbook PlayBook) ! {
if !plbook.exists(filter: 'docusaurus.') { if !plbook.exists(filter: 'docusaurus.') {
@@ -18,7 +17,7 @@ pub fn play(mut plbook PlayBook) ! {
reset: param_define.get_default_false('reset') reset: param_define.get_default_false('reset')
template_update: param_define.get_default_false('template_update') template_update: param_define.get_default_false('template_update')
install: param_define.get_default_false('install') install: param_define.get_default_false('install')
atlas_dir: param_define.get_default('atlas_dir', '${os.home_dir()}/hero/var/atlas_export')! atlas_dir: param_define.get_default('atlas_dir', '/tmp/atlas_export')!
use_atlas: param_define.get_default_false('use_atlas') use_atlas: param_define.get_default_false('use_atlas')
)! )!

View File

@@ -0,0 +1,536 @@
# AI Instructions for Site Module HeroScript
This document provides comprehensive instructions for AI agents working with the Site module's HeroScript format.
## HeroScript Format Overview
HeroScript is a declarative configuration language with the following characteristics:
### Basic Syntax
```heroscript
!!actor.action
param1: "value1"
param2: "value2"
multiline_param: "
This is a multiline value.
It can span multiple lines.
"
arg1 arg2 // Arguments without keys
```
**Key Rules:**
1. Actions start with `!!` followed by `actor.action` format
2. Parameters are indented and use `key: "value"` or `key: value` format
3. Values with spaces must be quoted
4. Multiline values are supported with quotes
5. Arguments without keys are space-separated
6. Comments start with `//`
## Site Module Actions
### 1. Site Configuration (`!!site.config`)
**Purpose:** Define the main site configuration including title, description, and metadata.
**Required Parameters:**
- `name`: Site identifier (will be normalized to snake_case)
**Optional Parameters:**
- `title`: Site title (default: "Documentation Site")
- `description`: Site description
- `tagline`: Site tagline
- `favicon`: Path to favicon (default: "img/favicon.png")
- `image`: Default site image (default: "img/tf_graph.png")
- `copyright`: Copyright text
- `url`: Main site URL
- `base_url`: Base URL path (default: "/")
- `url_home`: Home page path
**Example:**
```heroscript
!!site.config
name: "my_documentation"
title: "My Documentation Site"
description: "Comprehensive technical documentation"
tagline: "Learn everything you need"
url: "https://docs.example.com"
base_url: "/"
```
**AI Guidelines:**
- Always include `name` parameter
- Use descriptive titles and descriptions
- Ensure URLs are properly formatted with protocol
### 2. Metadata Configuration (`!!site.config_meta`)
**Purpose:** Override specific metadata for SEO purposes.
**Optional Parameters:**
- `title`: SEO-specific title (overrides site.config title for meta tags)
- `image`: SEO-specific image (overrides site.config image for og:image)
- `description`: SEO-specific description
**Example:**
```heroscript
!!site.config_meta
title: "My Docs - Complete Guide"
image: "img/social-preview.png"
description: "The ultimate guide to using our platform"
```
**AI Guidelines:**
- Use only when SEO metadata needs to differ from main config
- Keep titles concise for social media sharing
- Use high-quality images for social previews
### 3. Navigation Bar (`!!site.navbar` or `!!site.menu`)
**Purpose:** Configure the main navigation bar.
**Optional Parameters:**
- `title`: Navigation title (defaults to site.config title)
- `logo_alt`: Logo alt text
- `logo_src`: Logo image path
- `logo_src_dark`: Dark mode logo path
**Example:**
```heroscript
!!site.navbar
title: "My Site"
logo_alt: "My Site Logo"
logo_src: "img/logo.svg"
logo_src_dark: "img/logo-dark.svg"
```
**AI Guidelines:**
- Use `!!site.navbar` for modern syntax (preferred)
- `!!site.menu` is supported for backward compatibility
- Provide both light and dark logos when possible
### 4. Navigation Items (`!!site.navbar_item` or `!!site.menu_item`)
**Purpose:** Add items to the navigation bar.
**Required Parameters (one of):**
- `to`: Internal link path
- `href`: External URL
**Optional Parameters:**
- `label`: Display text (required in practice)
- `position`: "left" or "right" (default: "right")
**Example:**
```heroscript
!!site.navbar_item
label: "Documentation"
to: "docs/intro"
position: "left"
!!site.navbar_item
label: "GitHub"
href: "https://github.com/myorg/repo"
position: "right"
```
**AI Guidelines:**
- Use `to` for internal navigation
- Use `href` for external links
- Position important items on the left, secondary items on the right
### 5. Footer Configuration (`!!site.footer`)
**Purpose:** Configure footer styling.
**Optional Parameters:**
- `style`: "dark" or "light" (default: "dark")
**Example:**
```heroscript
!!site.footer
style: "dark"
```
### 6. Footer Items (`!!site.footer_item`)
**Purpose:** Add links to the footer, grouped by title.
**Required Parameters:**
- `title`: Group title (items with same title are grouped together)
- `label`: Link text
**Required Parameters (one of):**
- `to`: Internal link path
- `href`: External URL
**Example:**
```heroscript
!!site.footer_item
title: "Docs"
label: "Introduction"
to: "intro"
!!site.footer_item
title: "Docs"
label: "API Reference"
to: "api"
!!site.footer_item
title: "Community"
label: "Discord"
href: "https://discord.gg/example"
```
**AI Guidelines:**
- Group related links under the same title
- Use consistent title names across related items
- Provide both internal and external links as appropriate
### 7. Page Categories (`!!site.page_category`)
**Purpose:** Create a section/category to organize pages.
**Required Parameters:**
- `name`: Category identifier (snake_case)
**Optional Parameters:**
- `label`: Display name (auto-generated from name if not provided)
- `position`: Manual sort order (auto-incremented if not specified)
- `path`: URL path segment (defaults to normalized label)
**Example:**
```heroscript
!!site.page_category
name: "getting_started"
label: "Getting Started"
position: 100
!!site.page_category
name: "advanced_topics"
label: "Advanced Topics"
```
**AI Guidelines:**
- Use descriptive snake_case names
- Let label be auto-generated when possible (name_fix converts to Title Case)
- Categories persist for all subsequent pages until a new category is declared
- Position values should leave gaps (100, 200, 300) for future insertions
### 8. Pages (`!!site.page`)
**Purpose:** Define individual pages in the site.
**Required Parameters:**
- `src`: Source reference as `collection:page_name` (required for first page in a collection)
**Optional Parameters:**
- `name`: Page identifier (extracted from src if not provided)
- `title`: Page title (extracted from markdown if not provided)
- `description`: Page description for metadata
- `slug`: Custom URL slug
- `position`: Manual sort order (auto-incremented if not specified)
- `draft`: Mark as draft (default: false)
- `hide_title`: Hide title in rendering (default: false)
- `path`: Custom path (defaults to current category name)
- `category`: Override current category
- `title_nr`: Title numbering level
**Example:**
```heroscript
!!site.page src: "docs:introduction"
description: "Introduction to the platform"
slug: "/"
!!site.page src: "quickstart"
description: "Get started in 5 minutes"
!!site.page src: "installation"
title: "Installation Guide"
description: "How to install and configure"
position: 10
```
**AI Guidelines:**
- **Collection Persistence:** Specify collection once (e.g., `docs:introduction`), then subsequent pages only need page name (e.g., `quickstart`)
- **Category Persistence:** Pages belong to the most recently declared category
- **Title Extraction:** Prefer extracting titles from markdown files
- **Position Management:** Use automatic positioning unless specific order is required
- **Description Required:** Always provide descriptions for SEO
- **Slug Usage:** Use slug for special pages like homepage (`slug: "/"`)
### 9. Import External Content (`!!site.import`)
**Purpose:** Import content from external sources.
**Optional Parameters:**
- `name`: Import identifier
- `url`: Git URL or HTTP URL
- `path`: Local file system path
- `dest`: Destination path in site
- `replace`: Comma-separated key:value pairs for variable replacement
- `visible`: Whether imported content is visible (default: true)
**Example:**
```heroscript
!!site.import
url: "https://github.com/example/docs"
dest: "external"
replace: "VERSION:1.0.0,PROJECT:MyProject"
visible: true
```
**AI Guidelines:**
- Use for shared documentation across multiple sites
- Replace variables using `${VARIABLE}` syntax in source content
- Set `visible: false` for imported templates or partials
### 10. Publish Destinations (`!!site.publish` and `!!site.publish_dev`)
**Purpose:** Define where to publish the built site.
**Optional Parameters:**
- `path`: File system path or URL
- `ssh_name`: SSH connection name for remote deployment
**Example:**
```heroscript
!!site.publish
path: "/var/www/html/docs"
ssh_name: "production_server"
!!site.publish_dev
path: "/tmp/docs-preview"
```
**AI Guidelines:**
- Use `!!site.publish` for production deployments
- Use `!!site.publish_dev` for development/preview deployments
- Can specify multiple destinations
## File Organization Best Practices
### Naming Convention
Use numeric prefixes to control execution order:
```
0_config.heroscript # Site configuration
1_navigation.heroscript # Menu and footer
2_intro.heroscript # Introduction pages
3_guides.heroscript # User guides
4_reference.heroscript # API reference
```
**AI Guidelines:**
- Always use numeric prefixes (0_, 1_, 2_, etc.)
- Leave gaps in numbering (0, 10, 20) for future insertions
- Group related configurations in the same file
- Process order matters: config → navigation → pages
### Execution Order Rules
1. **Configuration First:** `!!site.config` must be processed before other actions
2. **Categories Before Pages:** Declare `!!site.page_category` before pages in that category
3. **Collection Persistence:** First page in a collection must specify `collection:page_name`
4. **Category Persistence:** Pages inherit the most recent category declaration
## Common Patterns
### Pattern 1: Simple Documentation Site
```heroscript
!!site.config
name: "simple_docs"
title: "Simple Documentation"
!!site.navbar
title: "Simple Docs"
!!site.page src: "docs:index"
description: "Welcome page"
slug: "/"
!!site.page src: "getting-started"
description: "Getting started guide"
!!site.page src: "api"
description: "API reference"
```
### Pattern 2: Multi-Section Documentation
```heroscript
!!site.config
name: "multi_section_docs"
title: "Complete Documentation"
!!site.page_category
name: "introduction"
label: "Introduction"
!!site.page src: "docs:welcome"
description: "Welcome to our documentation"
!!site.page src: "overview"
description: "Platform overview"
!!site.page_category
name: "tutorials"
label: "Tutorials"
!!site.page src: "tutorial_basics"
description: "Basic tutorial"
!!site.page src: "tutorial_advanced"
description: "Advanced tutorial"
```
### Pattern 3: Complex Site with External Links
```heroscript
!!site.config
name: "complex_site"
title: "Complex Documentation Site"
url: "https://docs.example.com"
!!site.navbar
title: "My Platform"
logo_src: "img/logo.svg"
!!site.navbar_item
label: "Docs"
to: "docs/intro"
position: "left"
!!site.navbar_item
label: "API"
to: "api"
position: "left"
!!site.navbar_item
label: "GitHub"
href: "https://github.com/example/repo"
position: "right"
!!site.footer
style: "dark"
!!site.footer_item
title: "Documentation"
label: "Getting Started"
to: "docs/intro"
!!site.footer_item
title: "Community"
label: "Discord"
href: "https://discord.gg/example"
!!site.page_category
name: "getting_started"
!!site.page src: "docs:introduction"
description: "Introduction to the platform"
slug: "/"
!!site.page src: "installation"
description: "Installation guide"
```
## Error Prevention
### Common Mistakes to Avoid
1. **Missing Collection on First Page:**
```heroscript
# WRONG - no collection specified
!!site.page src: "introduction"
# CORRECT
!!site.page src: "docs:introduction"
```
2. **Category Without Name:**
```heroscript
# WRONG - missing name
!!site.page_category
label: "Getting Started"
# CORRECT
!!site.page_category
name: "getting_started"
label: "Getting Started"
```
3. **Missing Description:**
```heroscript
# WRONG - no description
!!site.page src: "docs:intro"
# CORRECT
!!site.page src: "docs:intro"
description: "Introduction to the platform"
```
4. **Incorrect File Ordering:**
```
# WRONG - pages before config
pages.heroscript
config.heroscript
# CORRECT - config first
0_config.heroscript
1_pages.heroscript
```
## Validation Checklist
When generating HeroScript for the Site module, verify:
- [ ] `!!site.config` includes `name` parameter
- [ ] All pages have `description` parameter
- [ ] First page in each collection specifies `collection:page_name`
- [ ] Categories are declared before their pages
- [ ] Files use numeric prefixes for ordering
- [ ] Navigation items have either `to` or `href`
- [ ] Footer items are grouped by `title`
- [ ] External URLs include protocol (https://)
- [ ] Paths don't have trailing slashes unless intentional
- [ ] Draft pages are marked with `draft: true`
## Integration with V Code
When working with the Site module in V code:
```v
import incubaid.herolib.web.site
import incubaid.herolib.core.playbook
// Process HeroScript files
mut plbook := playbook.new(path: '/path/to/heroscripts')!
site.play(mut plbook)!
// Access configured site
mut mysite := site.get(name: 'my_site')!
// Iterate through pages
for page in mysite.pages {
println('Page: ${page.name} - ${page.description}')
}
// Iterate through sections
for section in mysite.sections {
println('Section: ${section.label}')
}
```
## Summary
The Site module's HeroScript format provides a declarative way to configure websites with:
- Clear separation of concerns (config, navigation, content)
- Automatic ordering and organization
- Collection and category persistence for reduced repetition
- Flexible metadata and SEO configuration
- Support for both internal and external content
Always follow the execution order rules, use numeric file prefixes, and provide complete metadata for best results.

View File

@@ -3,7 +3,7 @@ module site
import incubaid.herolib.core.texttools import incubaid.herolib.core.texttools
__global ( __global (
mywebsites map[string]&Site websites map[string]&Site
) )
@[params] @[params]
@@ -15,13 +15,7 @@ pub mut:
pub fn new(args FactoryArgs) !&Site { pub fn new(args FactoryArgs) !&Site {
name := texttools.name_fix(args.name) name := texttools.name_fix(args.name)
// Check if a site with this name already exists websites[name] = &Site{
if name in mywebsites {
// Return the existing site instead of creating a new one
return get(name: name)!
}
mywebsites[name] = &Site{
siteconfig: SiteConfig{ siteconfig: SiteConfig{
name: name name: name
} }
@@ -31,17 +25,18 @@ pub fn new(args FactoryArgs) !&Site {
pub fn get(args FactoryArgs) !&Site { pub fn get(args FactoryArgs) !&Site {
name := texttools.name_fix(args.name) name := texttools.name_fix(args.name)
mut sc := mywebsites[name] or { return error('siteconfig with name "${name}" does not exist') } mut sc := websites[name] or { return error('siteconfig with name "${name}" does not exist') }
return sc return sc
} }
pub fn exists(args FactoryArgs) bool { pub fn exists(args FactoryArgs) bool {
name := texttools.name_fix(args.name) name := texttools.name_fix(args.name)
return name in mywebsites mut sc := websites[name] or { return false }
return true
} }
pub fn default() !&Site { pub fn default() !&Site {
if mywebsites.len == 0 { if websites.len == 0 {
return new(name: 'default')! return new(name: 'default')!
} }
return get()! return get()!
@@ -49,5 +44,5 @@ pub fn default() !&Site {
// list returns all site names that have been created // list returns all site names that have been created
pub fn list() []string { pub fn list() []string {
return mywebsites.keys() return websites.keys()
} }

View File

@@ -1,143 +0,0 @@
module site
import json
// Top-level config
pub struct NavConfig {
pub mut:
my_sidebar []NavItem
// myTopbar []NavItem //not used yet
// myFooter []NavItem //not used yet
}
// -------- Variant Type --------
pub type NavItem = NavDoc | NavCat | NavLink
// --------- DOC ITEM ----------
pub struct NavDoc {
pub:
id string // is the page id
label string
}
// --------- CATEGORY ----------
pub struct NavCat {
pub mut:
label string
collapsible bool
collapsed bool
items []NavItem
}
// --------- LINK ----------
pub struct NavLink {
pub:
label string
href string
description string
}
// -------- JSON SERIALIZATION --------
// NavItemJson is used for JSON export with type discrimination
pub struct NavItemJson {
pub mut:
type_field string @[json: 'type']
// For doc
id string @[omitempty]
label string @[omitempty]
// For link
href string @[omitempty]
description string @[omitempty]
// For category
collapsible bool
collapsed bool
items []NavItemJson @[omitempty]
}
// Convert a single NavItem to JSON-serializable format
fn nav_item_to_json(item NavItem) !NavItemJson {
return match item {
NavDoc {
NavItemJson{
type_field: 'doc'
id: item.id
label: item.label
collapsible: false
collapsed: false
}
}
NavLink {
NavItemJson{
type_field: 'link'
label: item.label
href: item.href
description: item.description
collapsible: false
collapsed: false
}
}
NavCat {
mut json_items := []NavItemJson{}
for sub_item in item.items {
json_items << nav_item_to_json(sub_item)!
}
NavItemJson{
type_field: 'category'
label: item.label
collapsible: item.collapsible
collapsed: item.collapsed
items: json_items
}
}
}
}
// Convert entire NavConfig sidebar to JSON string
fn (nc NavConfig) sidebar_to_json() !string {
mut result := []NavItemJson{}
for item in nc.my_sidebar {
result << nav_item_to_json(item)!
}
return json.encode_pretty(result)
}
// // Convert entire NavConfig topbar to JSON-serializable array
// fn (nc NavConfig) topbar_to_json() ![]NavItemJson {
// mut result := []NavItemJson{}
// for item in nc.myTopbar {
// result << nav_item_to_json(item)!
// }
// return result
// }
// // Convert entire NavConfig footer to JSON-serializable array
// fn (nc NavConfig) footer_to_json() ![]NavItemJson {
// mut result := []NavItemJson{}
// for item in nc.myFooter {
// result << nav_item_to_json(item)!
// }
// return result
// }
// port topbar as formatted JSON string
// pub fn (nc NavConfig) jsondump_topbar() !string {
// items := nc.topbar_to_json()!
// return json.encode_pretty(items)
// }
// // Export footer as formatted JSON string
// pub fn (nc NavConfig) jsondump_footer() !string {
// items := nc.footer_to_json()!
// return json.encode_pretty(items)
// }
// // Export all navigation as object with sidebar, topbar, footer
// pub fn (nc NavConfig) jsondump_all() !string {
// all_nav := map[string][]NavItemJson{
// 'sidebar': nc.sidebar_to_json()!
// 'topbar': nc.topbar_to_json()!
// 'footer': nc.footer_to_json()!
// }
// return json.encode_pretty(all_nav)
// }

View File

@@ -1,12 +1,16 @@
module site module site
// Page represents a single documentation page
pub struct Page { pub struct Page {
pub mut: pub mut:
id string // Unique identifier: "collection:page_name" name string
title string // Display title (optional, extracted from markdown if empty) title string
description string // Brief description for metadata description string
draft bool // Mark as draft (hidden from navigation) draft bool
hide_title bool // Hide the title when rendering position int
src string // Source reference (same as id in this format) hide_title bool
src string @[required] // always in format collection:page_name, can use the default collection if no : specified
path string @[required] // is without the page name, so just the path to the folder where the page is in
section_name string
title_nr int
slug string
} }

View File

@@ -1,9 +0,0 @@
module site
@[heap]
pub struct Site {
pub mut:
pages map[string]Page // key: "collection:page_name"
nav NavConfig // Navigation sidebar configuration
siteconfig SiteConfig // Full site configuration
}

View File

@@ -0,0 +1,18 @@
module site
@[heap]
pub struct Site {
pub mut:
pages []Page
sections []Section
siteconfig SiteConfig
}
pub struct Section {
pub mut:
name string
position int
path string
label string
description string
}

View File

@@ -4,93 +4,222 @@ import os
import incubaid.herolib.core.playbook { PlayBook } import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.core.texttools import incubaid.herolib.core.texttools
import time import time
import incubaid.herolib.ui.console
// Main entry point for processing site HeroScript
pub fn play(mut plbook PlayBook) ! { pub fn play(mut plbook PlayBook) ! {
if !plbook.exists(filter: 'site.') { if !plbook.exists(filter: 'site.') {
return return
} }
console.print_header('Processing Site Configuration')
// ============================================================
// STEP 1: Initialize core site configuration
// ============================================================
console.print_item('Step 1: Loading site configuration')
mut config_action := plbook.ensure_once(filter: 'site.config')! mut config_action := plbook.ensure_once(filter: 'site.config')!
mut p := config_action.params
name := p.get_default('name', 'default')! mut p := config_action.params
name := p.get_default('name', 'default')! // Use 'default' as fallback name
// configure the website
mut website := new(name: name)! mut website := new(name: name)!
mut config := &website.siteconfig mut config := &website.siteconfig
// Load core configuration
config.name = texttools.name_fix(name) config.name = texttools.name_fix(name)
config.title = p.get_default('title', 'Documentation Site')! config.title = p.get_default('title', 'Documentation Site')!
config.description = p.get_default('description', 'Comprehensive documentation built with Docusaurus.')! config.description = p.get_default('description', 'Comprehensive documentation built with Docusaurus.')!
config.tagline = p.get_default('tagline', 'Your awesome documentation')! config.tagline = p.get_default('tagline', 'Your awesome documentation')!
config.favicon = p.get_default('favicon', 'img/favicon.png')! config.favicon = p.get_default('favicon', 'img/favicon.png')!
config.image = p.get_default('image', 'img/tf_graph.png')! config.image = p.get_default('image', 'img/tf_graph.png')!
config.copyright = p.get_default('copyright', '© ${time.now().year} Example Organization')! config.copyright = p.get_default('copyright', '© ' + time.now().year.str() +
' Example Organization')!
config.url = p.get_default('url', '')! config.url = p.get_default('url', '')!
config.base_url = p.get_default('base_url', '/')! config.base_url = p.get_default('base_url', '/')!
config.url_home = p.get_default('url_home', '')! config.url_home = p.get_default('url_home', '')!
config_action.done = true // Process !!site.config_meta for specific metadata overrides
mut meta_action := plbook.ensure_once(filter: 'site.config_meta')!
mut p_meta := meta_action.params
// ============================================================ // If 'title' is present in site.config_meta, it overrides. Otherwise, meta_title remains empty or uses site.config.title logic in docusaurus model.
// STEP 2: Apply optional metadata overrides config.meta_title = p_meta.get_default('title', config.title)!
// ============================================================ // If 'image' is present in site.config_meta, it overrides. Otherwise, meta_image remains empty or uses site.config.image logic.
console.print_item('Step 2: Applying metadata overrides') config.meta_image = p_meta.get_default('image', config.image)!
if plbook.exists_once(filter: 'site.config_meta') { // If 'description' is present in site.config_meta, it overrides the main description
mut meta_action := plbook.get(filter: 'site.config_meta')! if p_meta.exists('description') {
mut p_meta := meta_action.params config.description = p_meta.get('description')!
config.meta_title = p_meta.get_default('title', config.title)!
config.meta_image = p_meta.get_default('image', config.image)!
if p_meta.exists('description') {
config.description = p_meta.get('description')!
}
meta_action.done = true
} }
// ============================================================ config_action.done = true // Mark the action as done
// STEP 3: Configure content imports meta_action.done = true
// ============================================================
console.print_item('Step 3: Configuring content imports')
play_imports(mut plbook, mut config)!
// ============================================================ play_import(mut plbook, mut config)!
// STEP 4: Configure navigation menu play_menu(mut plbook, mut config)!
// ============================================================
console.print_item('Step 4: Configuring navigation menu')
play_navbar(mut plbook, mut config)!
// ============================================================
// STEP 5: Configure footer
// ============================================================
console.print_item('Step 5: Configuring footer')
play_footer(mut plbook, mut config)! play_footer(mut plbook, mut config)!
// ============================================================
// STEP 6: Configure announcement bar (optional)
// ============================================================
console.print_item('Step 6: Configuring announcement bar (if present)')
play_announcement(mut plbook, mut config)! play_announcement(mut plbook, mut config)!
play_publish(mut plbook, mut config)!
// ============================================================ play_publish_dev(mut plbook, mut config)!
// STEP 7: Configure publish destinations
// ============================================================
console.print_item('Step 7: Configuring publish destinations')
play_publishing(mut plbook, mut config)!
// ============================================================
// STEP 8: Build pages and navigation structure
// ============================================================
console.print_item('Step 8: Processing pages and building navigation')
play_pages(mut plbook, mut website)! play_pages(mut plbook, mut website)!
}
console.print_green('Site configuration complete')
fn play_import(mut plbook PlayBook, mut config SiteConfig) ! {
mut import_actions := plbook.find(filter: 'site.import')!
// println('import_actions: ${import_actions}')
for mut action in import_actions {
mut p := action.params
mut replace_map := map[string]string{}
if replace_str := p.get_default('replace', '') {
parts := replace_str.split(',')
for part in parts {
kv := part.split(':')
if kv.len == 2 {
replace_map[kv[0].trim_space()] = kv[1].trim_space()
}
}
}
mut importpath := p.get_default('path', '')!
if importpath != '' {
if !importpath.starts_with('/') {
importpath = os.abs_path('${plbook.path}/${importpath}')
}
}
mut import_ := ImportItem{
name: p.get_default('name', '')!
url: p.get_default('url', '')!
path: importpath
dest: p.get_default('dest', '')!
replace: replace_map
visible: p.get_default_false('visible')
}
config.imports << import_
action.done = true // Mark the action as done
}
}
fn play_menu(mut plbook PlayBook, mut config SiteConfig) ! {
mut navbar_actions := plbook.find(filter: 'site.navbar')!
if navbar_actions.len > 0 {
for mut action in navbar_actions { // Should ideally be one, but loop for safety
mut p := action.params
config.menu.title = p.get_default('title', config.title)! // Use existing config.title as ultimate fallback
config.menu.logo_alt = p.get_default('logo_alt', '')!
config.menu.logo_src = p.get_default('logo_src', '')!
config.menu.logo_src_dark = p.get_default('logo_src_dark', '')!
action.done = true // Mark the action as done
}
} else {
// Fallback to site.menu for title if site.navbar is not found
mut menu_actions := plbook.find(filter: 'site.menu')!
for mut action in menu_actions {
mut p := action.params
config.menu.title = p.get_default('title', config.title)!
config.menu.logo_alt = p.get_default('logo_alt', '')!
config.menu.logo_src = p.get_default('logo_src', '')!
config.menu.logo_src_dark = p.get_default('logo_src_dark', '')!
action.done = true // Mark the action as done
}
}
mut menu_item_actions := plbook.find(filter: 'site.navbar_item')!
if menu_item_actions.len == 0 {
// Fallback to site.menu_item if site.navbar_item is not found
menu_item_actions = plbook.find(filter: 'site.menu_item')!
}
// Clear existing menu items to prevent duplication
config.menu.items = []MenuItem{}
for mut action in menu_item_actions {
mut p := action.params
mut item := MenuItem{
label: p.get_default('label', 'Documentation')!
href: p.get_default('href', '')!
to: p.get_default('to', '')!
position: p.get_default('position', 'right')!
}
config.menu.items << item
action.done = true // Mark the action as done
}
}
fn play_footer(mut plbook PlayBook, mut config SiteConfig) ! {
mut footer_actions := plbook.find(filter: 'site.footer')!
for mut action in footer_actions {
mut p := action.params
config.footer.style = p.get_default('style', 'dark')!
action.done = true // Mark the action as done
}
mut footer_item_actions := plbook.find(filter: 'site.footer_item')!
mut links_map := map[string][]FooterItem{}
// Clear existing footer links to prevent duplication
config.footer.links = []FooterLink{}
for mut action in footer_item_actions {
mut p := action.params
title := p.get_default('title', 'Docs')!
mut item := FooterItem{
label: p.get_default('label', 'Introduction')!
href: p.get_default('href', '')!
to: p.get_default('to', '')!
}
if title !in links_map {
links_map[title] = []FooterItem{}
}
links_map[title] << item
action.done = true // Mark the action as done
}
// Convert map to footer links array
for title, items in links_map {
config.footer.links << FooterLink{
title: title
items: items
}
}
}
fn play_announcement(mut plbook PlayBook, mut config SiteConfig) ! {
mut announcement_actions := plbook.find(filter: 'site.announcement')!
if announcement_actions.len > 0 {
// Only process the first announcement action
mut action := announcement_actions[0]
mut p := action.params
config.announcement = AnnouncementBar{
id: p.get_default('id', 'announcement')!
content: p.get_default('content', '')!
background_color: p.get_default('background_color', '#20232a')!
text_color: p.get_default('text_color', '#fff')!
is_closeable: p.get_default_true('is_closeable')
}
action.done = true // Mark the action as done
}
}
fn play_publish(mut plbook PlayBook, mut config SiteConfig) ! {
mut build_dest_actions := plbook.find(filter: 'site.publish')!
for mut action in build_dest_actions {
mut p := action.params
mut dest := BuildDest{
path: p.get_default('path', '')! // can be url
ssh_name: p.get_default('ssh_name', '')!
}
config.build_dest << dest
action.done = true // Mark the action as done
}
}
fn play_publish_dev(mut plbook PlayBook, mut config SiteConfig) ! {
mut build_dest_actions := plbook.find(filter: 'site.publish_dev')!
for mut action in build_dest_actions {
mut p := action.params
mut dest := BuildDest{
path: p.get_default('path', '')! // can be url
ssh_name: p.get_default('ssh_name', '')!
}
config.build_dest_dev << dest
action.done = true // Mark the action as done
}
} }

View File

@@ -1,34 +0,0 @@
module site
import os
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.core.texttools
import time
import incubaid.herolib.ui.console
// ============================================================
// ANNOUNCEMENT: Process announcement bar (optional)
// ============================================================
fn play_announcement(mut plbook PlayBook, mut config SiteConfig) ! {
mut announcement_actions := plbook.find(filter: 'site.announcement')!
if announcement_actions.len > 0 {
// Only process the first announcement action
mut action := announcement_actions[0]
mut p := action.params
content := p.get('content') or {
return error('!!site.announcement: must specify "content"')
}
config.announcement = AnnouncementBar{
id: p.get_default('id', 'announcement')!
content: content
background_color: p.get_default('background_color', '#20232a')!
text_color: p.get_default('text_color', '#fff')!
is_closeable: p.get_default_true('is_closeable')
}
action.done = true
}
}

View File

@@ -1,62 +0,0 @@
module site
import os
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.core.texttools
import time
import incubaid.herolib.ui.console
// ============================================================
// FOOTER: Process footer configuration
// ============================================================
fn play_footer(mut plbook PlayBook, mut config SiteConfig) ! {
// Process footer style (optional)
mut footer_actions := plbook.find(filter: 'site.footer')!
for mut action in footer_actions {
mut p := action.params
config.footer.style = p.get_default('style', 'dark')!
action.done = true
}
// Process footer items (multiple)
mut footer_item_actions := plbook.find(filter: 'site.footer_item')!
mut links_map := map[string][]FooterItem{}
// Clear existing links to prevent duplication
config.footer.links = []FooterLink{}
for mut action in footer_item_actions {
mut p := action.params
title := p.get_default('title', 'Docs')!
label := p.get('label') or {
return error('!!site.footer_item: must specify "label"')
}
mut item := FooterItem{
label: label
href: p.get_default('href', '')!
to: p.get_default('to', '')!
}
// Validate that href or to is specified
if item.href.len == 0 && item.to.len == 0 {
return error('!!site.footer_item for "${label}": must specify either "href" or "to"')
}
if title !in links_map {
links_map[title] = []FooterItem{}
}
links_map[title] << item
action.done = true
}
// Convert map to footer links array
for title, items in links_map {
config.footer.links << FooterLink{
title: title
items: items
}
}
}

View File

@@ -1,51 +0,0 @@
module site
import os
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.core.texttools
import time
import incubaid.herolib.ui.console
// ============================================================
// IMPORTS: Process content imports
// ============================================================
fn play_imports(mut plbook PlayBook, mut config SiteConfig) ! {
mut import_actions := plbook.find(filter: 'site.import')!
for mut action in import_actions {
mut p := action.params
// Parse replacement patterns (comma-separated key:value pairs)
mut replace_map := map[string]string{}
if replace_str := p.get_default('replace', '') {
parts := replace_str.split(',')
for part in parts {
kv := part.split(':')
if kv.len == 2 {
replace_map[kv[0].trim_space()] = kv[1].trim_space()
}
}
}
// Get path (can be relative to playbook path)
mut import_path := p.get_default('path', '')!
if import_path != '' {
if !import_path.starts_with('/') {
import_path = os.abs_path('${plbook.path}/${import_path}')
}
}
// Create import item
mut import_item := ImportItem{
name: p.get_default('name', '')!
url: p.get_default('url', '')!
path: import_path
dest: p.get_default('dest', '')!
replace: replace_map
visible: p.get_default_false('visible')
}
config.imports << import_item
action.done = true
}
}

View File

@@ -1,60 +0,0 @@
module site
import os
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.core.texttools
import time
import incubaid.herolib.ui.console
// ============================================================
// NAVBAR: Process navigation menu
// ============================================================
fn play_navbar(mut plbook PlayBook, mut config SiteConfig) ! {
// Try 'site.navbar' first, then fallback to deprecated 'site.menu'
mut navbar_actions := plbook.find(filter: 'site.navbar')!
if navbar_actions.len == 0 {
navbar_actions = plbook.find(filter: 'site.menu')!
}
// Configure navbar metadata
if navbar_actions.len > 0 {
for mut action in navbar_actions {
mut p := action.params
config.menu.title = p.get_default('title', config.title)!
config.menu.logo_alt = p.get_default('logo_alt', '')!
config.menu.logo_src = p.get_default('logo_src', '')!
config.menu.logo_src_dark = p.get_default('logo_src_dark', '')!
action.done = true
}
}
// Process navbar items
mut navbar_item_actions := plbook.find(filter: 'site.navbar_item')!
if navbar_item_actions.len == 0 {
navbar_item_actions = plbook.find(filter: 'site.menu_item')!
}
// Clear existing items to prevent duplication
config.menu.items = []MenuItem{}
for mut action in navbar_item_actions {
mut p := action.params
label := p.get('label') or { return error('!!site.navbar_item: must specify "label"') }
mut item := MenuItem{
label: label
href: p.get_default('href', '')!
to: p.get_default('to', '')!
position: p.get_default('position', 'right')!
}
// Validate that at least href or to is specified
if item.href.len == 0 && item.to.len == 0 {
return error('!!site.navbar_item: must specify either "href" or "to" for label "${label}"')
}
config.menu.items << item
action.done = true
}
}

135
lib/web/site/play_page.v Normal file
View File

@@ -0,0 +1,135 @@
module site
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.core.texttools
// plays the sections & pages
fn play_pages(mut plbook PlayBook, mut site Site) ! {
// mut siteconfig := &site.siteconfig
// if only 1 doctree is specified, then we use that as the default doctree name
// mut doctreename := 'main' // Not used for now, keep commented for future doctree integration
// if plbook.exists(filter: 'site.doctree') {
// if plbook.exists_once(filter: 'site.doctree') {
// mut action := plbook.get(filter: 'site.doctree')!
// mut p := action.params
// doctreename = p.get('name') or { return error('need to specify name in site.doctree') }
// } else {
// return error("can't have more than one site.doctree")
// }
// }
mut section_current := Section{} // is the category
mut position_section := 1
mut position_category := 100 // Start categories at position 100
mut collection_current := '' // current collection we are working on
mut all_actions := plbook.find(filter: 'site.')!
for mut action in all_actions {
if action.done {
continue
}
mut p := action.params
if action.name == 'page_category' {
mut section := Section{}
section.name = p.get('name') or {
return error('need to specify name in site.page_category. Action: ${action}')
}
position_section = 1 // go back to default position for pages in the category
section.position = p.get_int_default('position', position_category)!
if section.position == position_category {
position_category += 100 // Increment for next category
}
section.label = p.get_default('label', texttools.name_fix_snake_to_pascal(section.name))!
section.path = p.get_default('path', texttools.name_fix(section.label))!
section.description = p.get_default('description', '')!
site.sections << section
action.done = true // Mark the action as done
section_current = section
continue // next action
}
if action.name == 'page' {
mut pagesrc := p.get_default('src', '')!
mut pagename := p.get_default('name', '')!
mut pagecollection := ''
if pagesrc.contains(':') {
pagecollection = pagesrc.split(':')[0]
pagename = pagesrc.split(':')[1]
} else {
if collection_current.len > 0 {
pagecollection = collection_current
pagename = pagesrc // ADD THIS LINE - use pagesrc as the page name
} else {
return error('need to specify collection in page.src path as collection:page_name or make sure someone before you did. Got src="${pagesrc}" with no collection set. Action: ${action}')
}
}
pagecollection = texttools.name_fix(pagecollection)
collection_current = pagecollection
pagename = texttools.name_fix_keepext(pagename)
if pagename.ends_with('.md') {
pagename = pagename.replace('.md', '')
}
if pagename == '' {
return error('need to specify name in page.src or specify in path as collection:page_name. Action: ${action}')
}
if pagecollection == '' {
return error('need to specify collection in page.src or specify in path as collection:page_name. Action: ${action}')
}
// recreate the pagepath
pagesrc = '${pagecollection}:${pagename}'
// get sectionname from category, page_category or section, if not specified use current section
section_name := p.get_default('category', p.get_default('page_category', p.get_default('section',
section_current.name)!)!)!
mut pagepath := p.get_default('path', section_current.path)!
pagepath = pagepath.trim_space().trim('/')
// Only apply name_fix if it's a simple name (no path separators)
// For paths like 'appendix/internet_today', preserve the structure
if !pagepath.contains('/') {
pagepath = texttools.name_fix(pagepath)
}
// Ensure pagepath ends with / to indicate it's a directory path
if pagepath.len > 0 && !pagepath.ends_with('/') {
pagepath += '/'
}
mut mypage := Page{
section_name: section_name
name: pagename
path: pagepath
src: pagesrc
}
mypage.position = p.get_int_default('position', 0)!
if mypage.position == 0 {
mypage.position = section_current.position + position_section
position_section += 1
}
mypage.title = p.get_default('title', '')!
mypage.description = p.get_default('description', '')!
mypage.slug = p.get_default('slug', '')!
mypage.draft = p.get_default_false('draft')
mypage.hide_title = p.get_default_false('hide_title')
mypage.title_nr = p.get_int_default('title_nr', 0)!
site.pages << mypage
action.done = true // Mark the action as done
}
// println(action)
// println(section_current)
// println(site.pages.last())
// $dbg;
}
}

View File

@@ -1,203 +0,0 @@
module site
import os
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.core.texttools
import time
import incubaid.herolib.ui.console
// ============================================================
// Helper function: normalize name while preserving .md extension handling
// ============================================================
fn normalize_page_name(name string) string {
mut result := name
// Remove .md extension if present for processing
if result.ends_with('.md') {
result = result[0..result.len - 3]
}
// Apply name fixing
return texttools.name_fix(result)
}
// ============================================================
// Internal structure for tracking category information
// ============================================================
struct CategoryInfo {
pub mut:
name string
label string
position int
nav_items []NavItem
}
// ============================================================
// PAGES: Process pages and build navigation structure
// ============================================================
fn play_pages(mut plbook PlayBook, mut website Site) ! {
mut collection_current := '' // Track current collection for reuse
mut categories := map[string]CategoryInfo{} // Map of category name -> info
mut category_current := '' // Track current active category
mut root_nav_items := []NavItem{} // Root-level items (pages without category)
mut next_category_position := 100 // Auto-increment position for categories
// ============================================================
// PASS 1: Process all page and category actions
// ============================================================
mut all_actions := plbook.find(filter: 'site.')!
for mut action in all_actions {
if action.done {
continue
}
// ========== PAGE CATEGORY ==========
if action.name == 'page_category' {
mut p := action.params
category_name := p.get('name') or {
return error('!!site.page_category: must specify "name"')
}
category_name_fixed := texttools.name_fix(category_name)
// Get label (derive from name if not specified)
mut label := p.get_default('label', texttools.name_fix_snake_to_pascal(category_name_fixed))!
mut position := p.get_int_default('position', next_category_position)!
// Auto-increment position if using default
if position == next_category_position {
next_category_position += 100
}
// Create and store category info
categories[category_name_fixed] = CategoryInfo{
name: category_name_fixed
label: label
position: position
nav_items: []NavItem{}
}
category_current = category_name_fixed
console.print_item('Created page category: "${label}" (${category_name_fixed})')
action.done = true
continue
}
// ========== PAGE ==========
if action.name == 'page' {
mut p := action.params
mut page_src := p.get_default('src', '')!
mut page_collection := ''
mut page_name := ''
// Parse collection:page format from src
if page_src.contains(':') {
parts := page_src.split(':')
page_collection = texttools.name_fix(parts[0])
page_name = normalize_page_name(parts[1])
} else {
// Use previously specified collection if available
if collection_current.len > 0 {
page_collection = collection_current
page_name = normalize_page_name(page_src)
} else {
return error('!!site.page: must specify source as "collection:page_name" in "src".\nGot src="${page_src}" with no collection previously set.\nEither specify "collection:page_name" or define a collection first.')
}
}
// Validation
if page_name.len == 0 {
return error('!!site.page: could not extract valid page name from src="${page_src}"')
}
if page_collection.len == 0 {
return error('!!site.page: could not determine collection')
}
// Store collection for subsequent pages
collection_current = page_collection
// Build page ID
page_id := '${page_collection}:${page_name}'
// Get optional page metadata
page_title := p.get_default('title', '')!
page_description := p.get_default('description', '')!
page_draft := p.get_default_false('draft')
page_hide_title := p.get_default_false('hide_title')
// Create page
mut page := Page{
id: page_id
title: page_title
description: page_description
draft: page_draft
hide_title: page_hide_title
src: page_id
}
website.pages[page_id] = page
// Create navigation item
nav_doc := NavDoc{
id: page_id
label: if page_title.len > 0 { page_title } else { page_name }
}
// Add to appropriate category or root
if category_current.len > 0 {
if category_current in categories {
mut cat_info := categories[category_current]
cat_info.nav_items << nav_doc
categories[category_current] = cat_info
console.print_debug('Added page "${page_id}" to category "${category_current}"')
}
} else {
root_nav_items << nav_doc
console.print_debug('Added root page "${page_id}"')
}
action.done = true
continue
}
}
// ============================================================
// PASS 2: Build final navigation structure from categories
// ============================================================
console.print_item('Building navigation structure...')
mut final_nav_items := []NavItem{}
// Add root items first
for item in root_nav_items {
final_nav_items << item
}
// Sort categories by position and add them
mut sorted_categories := []CategoryInfo{}
for _, cat_info in categories {
sorted_categories << cat_info
}
// Sort by position
sorted_categories.sort(a.position < b.position)
// Convert categories to NavCat items and add to navigation
for cat_info in sorted_categories {
// Unwrap NavDoc items from cat_info.nav_items (they're already NavItem)
nav_cat := NavCat{
label: cat_info.label
collapsible: true
collapsed: false
items: cat_info.nav_items
}
final_nav_items << nav_cat
console.print_debug('Added category to nav: "${cat_info.label}" with ${cat_info.nav_items.len} items')
}
// Update website navigation
website.nav.my_sidebar = final_nav_items
console.print_green('Navigation structure built with ${website.pages.len} pages in ${categories.len} categories')
}

View File

@@ -1,46 +0,0 @@
module site
import os
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.core.texttools
import time
import incubaid.herolib.ui.console
// ============================================================
// PUBLISHING: Configure build and publish destinations
// ============================================================
fn play_publishing(mut plbook PlayBook, mut config SiteConfig) ! {
// Production publish destinations
mut build_dest_actions := plbook.find(filter: 'site.publish')!
for mut action in build_dest_actions {
mut p := action.params
path := p.get('path') or {
return error('!!site.publish: must specify "path"')
}
mut dest := BuildDest{
path: path
ssh_name: p.get_default('ssh_name', '')!
}
config.build_dest << dest
action.done = true
}
// Development publish destinations
mut build_dest_dev_actions := plbook.find(filter: 'site.publish_dev')!
for mut action in build_dest_dev_actions {
mut p := action.params
path := p.get('path') or {
return error('!!site.publish_dev: must specify "path"')
}
mut dest := BuildDest{
path: path
ssh_name: p.get_default('ssh_name', '')!
}
config.build_dest_dev << dest
action.done = true
}
}

View File

@@ -2,83 +2,43 @@
The Site module provides a structured way to define website configurations, navigation menus, pages, and sections using HeroScript. It's designed to work with static site generators like Docusaurus. The Site module provides a structured way to define website configurations, navigation menus, pages, and sections using HeroScript. It's designed to work with static site generators like Docusaurus.
## Purpose
The Site module allows you to:
- Define website structure and configuration in a declarative way using HeroScript
- Organize pages into sections/categories
- Configure navigation menus and footers
- Manage page metadata (title, description, slug, etc.)
- Support multiple content collections
- Define build and publish destinations
## Quick Start ## Quick Start
### Minimal HeroScript Example
```heroscript
!!site.config
name: "my_docs"
title: "My Documentation"
!!site.page src: "docs:introduction"
title: "Getting Started"
!!site.page src: "setup"
title: "Installation"
```
### Processing with V Code
```v ```v
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run #!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
import incubaid.herolib.core.playbook import incubaid.herolib.develop.gittools
import incubaid.herolib.web.site import incubaid.herolib.web.site
import incubaid.herolib.ui.console import incubaid.herolib.core.playcmds
// Process HeroScript file // Clone or use existing repository with HeroScript files
mut plbook := playbook.new(path: './site_config.heroscript')! mysitepath := gittools.path(
git_url: 'https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech'
git_pull: true
)!
// Execute site configuration // Process all HeroScript files in the path
site.play(mut plbook)! playcmds.run(heroscript_path: mysitepath.path)!
// Access the configured site // Get the configured site
mut mysite := site.get(name: 'my_docs')! mut mysite := site.get(name: 'tfgrid_tech')!
println(mysite)
// Print available pages
pages_map := mysite.list_pages()
for page_id, _ in pages_map {
console.print_item('Page: ${page_id}')
}
println('Site has ${mysite.pages.len} pages')
``` ```
---
## Core Concepts
### Site
A website configuration that contains pages, navigation structure, and metadata.
### Page
A single page with:
- **ID**: `collection:page_name` format
- **Title**: Display name (optional - extracted from markdown if not provided)
- **Description**: SEO metadata
- **Draft**: Hidden from navigation if true
### Category (Section)
Groups related pages together in the navigation sidebar. Automatically collapsed/expandable.
### Collection
A logical group of pages. Pages reuse the collection once specified.
```heroscript
!!site.page src: "tech:intro" # Specifies collection "tech"
!!site.page src: "benefits" # Reuses collection "tech"
!!site.page src: "components" # Still uses collection "tech"
!!site.page src: "api:reference" # Switches to collection "api"
!!site.page src: "endpoints" # Uses collection "api"
```
---
## HeroScript Syntax ## HeroScript Syntax
### 1. Site Configuration (Required) ### Basic Configuration
```heroscript ```heroscript
!!site.config !!site.config
@@ -91,49 +51,20 @@ A logical group of pages. Pages reuse the collection once specified.
copyright: "© 2024 My Organization" copyright: "© 2024 My Organization"
url: "https://docs.example.com" url: "https://docs.example.com"
base_url: "/" base_url: "/"
url_home: "/docs"
``` ```
**Parameters:** ### Navigation Menu
- `name` - Internal site identifier (default: 'default')
- `title` - Main site title (shown in browser tab)
- `description` - Site description for SEO
- `tagline` - Short tagline/subtitle
- `favicon` - Path to favicon image
- `image` - Default OG image for social sharing
- `copyright` - Copyright notice
- `url` - Full site URL for Docusaurus
- `base_url` - Base URL path (e.g., "/" or "/docs/")
- `url_home` - Home page path
### 2. Metadata Overrides (Optional)
```heroscript
!!site.config_meta
title: "My Docs - Technical Reference"
image: "img/tech-og.png"
description: "Technical documentation and API reference"
```
Overrides specific metadata for SEO without changing core config.
### 3. Navigation Bar
```heroscript ```heroscript
!!site.navbar !!site.navbar
title: "My Documentation" title: "My Site"
logo_alt: "Site Logo" logo_alt: "Site Logo"
logo_src: "img/logo.svg" logo_src: "img/logo.svg"
logo_src_dark: "img/logo-dark.svg" logo_src_dark: "img/logo-dark.svg"
!!site.navbar_item !!site.navbar_item
label: "Documentation" label: "Documentation"
to: "intro" to: "docs/intro"
position: "left"
!!site.navbar_item
label: "API Reference"
to: "docs/api"
position: "left" position: "left"
!!site.navbar_item !!site.navbar_item
@@ -142,13 +73,7 @@ Overrides specific metadata for SEO without changing core config.
position: "right" position: "right"
``` ```
**Parameters:** ### Footer Configuration
- `label` - Display text (required)
- `to` - Internal link
- `href` - External URL
- `position` - "left" or "right" in navbar
### 4. Footer Configuration
```heroscript ```heroscript
!!site.footer !!site.footer
@@ -162,234 +87,242 @@ Overrides specific metadata for SEO without changing core config.
!!site.footer_item !!site.footer_item
title: "Docs" title: "Docs"
label: "Getting Started" label: "Getting Started"
to: "getting-started" href: "https://docs.example.com/getting-started"
!!site.footer_item !!site.footer_item
title: "Community" title: "Community"
label: "Discord" label: "Discord"
href: "https://discord.gg/example" href: "https://discord.gg/example"
!!site.footer_item
title: "Legal"
label: "Privacy"
href: "https://example.com/privacy"
``` ```
### 5. Announcement Bar (Optional) ## Page Organization
### Example 1: Simple Pages Without Categories
When you don't need categories, pages are added sequentially. The collection only needs to be specified once, then it's reused for subsequent pages.
```heroscript ```heroscript
!!site.announcement !!site.page src: "mycelium_tech:introduction"
id: "new-release" description: "Introduction to ThreeFold Technology"
content: "🎉 Version 2.0 is now available!" slug: "/"
background_color: "#20232a"
text_color: "#fff" !!site.page src: "vision"
is_closeable: true description: "Our Vision for the Future Internet"
!!site.page src: "what"
description: "What ThreeFold is Building"
!!site.page src: "presentation"
description: "ThreeFold Technology Presentation"
!!site.page src: "status"
description: "Current Development Status"
``` ```
### 6. Pages and Categories **Key Points:**
#### Simple: Pages Without Categories - First page specifies collection as `tech:introduction` (collection:page_name format)
- Subsequent pages only need the page name (e.g., `vision`) - the `tech` collection is reused
- If `title` is not specified, it will be extracted from the markdown file itself
- Pages are ordered by their appearance in the HeroScript file
- `slug` can be used to customize the URL path (e.g., `"/"` for homepage)
```heroscript ### Example 2: Pages with Categories
!!site.page src: "guides:introduction"
title: "Getting Started"
description: "Introduction to the platform"
!!site.page src: "installation" Categories (sections) help organize pages into logical groups with their own navigation structure.
title: "Installation"
!!site.page src: "configuration"
title: "Configuration"
```
#### Advanced: Pages With Categories
```heroscript ```heroscript
!!site.page_category !!site.page_category
name: "basics" name: "first_principle_thinking"
label: "Getting Started" label: "First Principle Thinking"
!!site.page src: "guides:introduction" !!site.page src: "first_principle_thinking:hardware_badly_used"
title: "Introduction" description: "Hardware is not used properly, why it is important to understand hardware"
description: "Learn the basics"
!!site.page src: "installation" !!site.page src: "internet_risk"
title: "Installation" description: "Internet risk, how to mitigate it, and why it is important"
!!site.page src: "configuration" !!site.page src: "onion_analogy"
title: "Configuration" description: "Compare onion with a computer, layers of abstraction"
!!site.page_category
name: "advanced"
label: "Advanced Topics"
!!site.page src: "advanced:performance"
title: "Performance Tuning"
!!site.page src: "scaling"
title: "Scaling Guide"
``` ```
**Page Parameters:** **Key Points:**
- `src` - Source as `collection:page` (first page) or just `page_name` (reuse collection)
- `title` - Page title (optional, extracted from markdown if not provided)
- `description` - Page description
- `draft` - Hide from navigation (default: false)
- `hide_title` - Don't show title in page (default: false)
### 7. Content Imports - `!!site.page_category` creates a new section/category
- `name` is the internal identifier (snake_case)
- `label` is the display name (automatically derived from `name` if not specified)
- Category name is converted to title case: `first_principle_thinking` → "First Principle Thinking"
- Once a category is defined, all subsequent pages belong to it until a new category is declared
- Collection persistence works the same: specify once (e.g., `first_principle_thinking:hardware_badly_used`), then reuse
### Example 3: Advanced Page Configuration
```heroscript
!!site.page_category
name: "components"
label: "System Components"
position: 100
!!site.page src: "mycelium_tech:mycelium"
title: "Mycelium Network"
description: "Peer-to-peer overlay network"
slug: "mycelium-network"
position: 1
draft: false
hide_title: false
!!site.page src: "fungistor"
title: "Fungistor Storage"
description: "Distributed storage system"
position: 2
```
**Available Page Parameters:**
- `src`: Source reference as `collection:page_name` (required for first page in collection)
- `title`: Page title (optional, extracted from markdown if not provided)
- `description`: Page description for metadata
- `slug`: Custom URL slug
- `position`: Manual ordering (auto-incremented if not specified)
- `draft`: Mark page as draft (default: false)
- `hide_title`: Hide the page title in rendering (default: false)
- `path`: Custom path for the page (defaults to category name)
- `category`: Override the current category for this page
## File Organization
HeroScript files should be organized with numeric prefixes to control execution order:
```
docs/
├── 0_config.heroscript # Site configuration
├── 1_menu.heroscript # Navigation and footer
├── 2_intro_pages.heroscript # Introduction pages
├── 3_tech_pages.heroscript # Technical documentation
└── 4_api_pages.heroscript # API reference
```
**Important:** Files are processed in alphabetical order, so use numeric prefixes (0_, 1_, 2_, etc.) to ensure correct execution sequence.
## Import External Content
```heroscript ```heroscript
!!site.import !!site.import
url: "https://github.com/example/external-docs" url: "https://github.com/example/external-docs"
path: "/local/path/to/repo"
dest: "external" dest: "external"
replace: "PROJECT_NAME:My Project,VERSION:1.0.0" replace: "PROJECT_NAME:My Project,VERSION:1.0.0"
visible: true visible: true
``` ```
### 8. Publishing Destinations ## Publish Destinations
```heroscript ```heroscript
!!site.publish !!site.publish
path: "/var/www/html/docs" path: "/var/www/html/docs"
ssh_name: "production" ssh_name: "production_server"
!!site.publish_dev !!site.publish_dev
path: "/tmp/docs-preview" path: "/tmp/docs-preview"
``` ```
--- ## Factory Methods
## Common Patterns ### Create or Get a Site
### Pattern 1: Multi-Section Technical Documentation ```v
import incubaid.herolib.web.site
```heroscript // Create a new site
!!site.config mut mysite := site.new(name: 'my_docs')!
name: "tech_docs"
title: "Technical Documentation"
!!site.page_category // Get an existing site
name: "getting_started" mut mysite := site.get(name: 'my_docs')!
label: "Getting Started"
!!site.page src: "docs:intro" // Get default site
title: "Introduction" mut mysite := site.default()!
!!site.page src: "installation" // Check if site exists
title: "Installation" if site.exists(name: 'my_docs') {
println('Site exists')
}
!!site.page_category // List all sites
name: "concepts" sites := site.list()
label: "Core Concepts" println(sites)
!!site.page src: "concepts:architecture"
title: "Architecture"
!!site.page src: "components"
title: "Components"
!!site.page_category
name: "api"
label: "API Reference"
!!site.page src: "api:rest"
title: "REST API"
!!site.page src: "graphql"
title: "GraphQL"
``` ```
### Pattern 2: Simple Blog/Knowledge Base ### Using with PlayBook
```heroscript ```v
!!site.config import incubaid.herolib.core.playbook
name: "blog" import incubaid.herolib.web.site
title: "Knowledge Base"
!!site.page src: "articles:first_post" // Create playbook from path
title: "Welcome to Our Blog" mut plbook := playbook.new(path: '/path/to/heroscripts')!
!!site.page src: "second_post" // Process site configuration
title: "Understanding the Basics" site.play(mut plbook)!
!!site.page src: "third_post" // Access the configured site
title: "Advanced Techniques" mut mysite := site.get(name: 'my_site')!
``` ```
### Pattern 3: Project with External Imports ## Data Structures
```heroscript ### Site
!!site.config
name: "project_docs"
title: "Project Documentation"
!!site.import ```v
url: "https://github.com/org/shared-docs" pub struct Site {
dest: "shared" pub mut:
visible: true pages []Page
sections []Section
!!site.page_category siteconfig SiteConfig
name: "product" }
label: "Product Guide"
!!site.page src: "docs:overview"
title: "Overview"
!!site.page src: "features"
title: "Features"
!!site.page_category
name: "resources"
label: "Shared Resources"
!!site.page src: "shared:common"
title: "Common Patterns"
``` ```
--- ### Page
## File Organization ```v
pub struct Page {
Organize HeroScript files with numeric prefixes to control execution order: pub mut:
name string // Page identifier
``` title string // Display title
docs/ description string // Page description
├── 0_config.heroscript draft bool // Draft status
│ └── !!site.config and !!site.config_meta position int // Sort order
hide_title bool // Hide title in rendering
├── 1_menu.heroscript src string // Source as collection:page_name
│ └── !!site.navbar and !!site.footer path string // URL path (without page name)
section_name string // Category/section name
├── 2_pages.heroscript title_nr int // Title numbering level
│ └── !!site.page_category and !!site.page actions slug string // Custom URL slug
}
└── 3_publish.heroscript
└── !!site.publish destinations
``` ```
**Why numeric prefixes?** ### Section
Files are processed in alphabetical order. Numeric prefixes ensure: ```v
- Site config runs first pub struct Section {
- Navigation menu configures before pages pub mut:
- Pages build the final structure name string // Internal identifier
- Publishing configured last position int // Sort order
path string // URL path
label string // Display name
}
```
--- ## Best Practices
## Processing Order 1. **File Naming**: Use numeric prefixes (0_, 1_, 2_) to control execution order
2. **Collection Reuse**: Specify collection once, then reuse for subsequent pages
3. **Category Organization**: Group related pages under categories for better navigation
4. **Title Extraction**: Let titles be extracted from markdown files when possible
5. **Position Management**: Use automatic positioning unless you need specific ordering
6. **Description**: Always provide descriptions for better SEO and navigation
7. **Draft Status**: Use `draft: true` for work-in-progress pages
The Site module processes HeroScript in this strict order: ## Complete Example
1. Site Configuration See `examples/web/site/site_example.vsh` for a complete working example.
2. Metadata Overrides
3. Imports
4. Navigation
5. Footer
6. Announcement
7. Publishing
8. Pages & Categories
Each stage depends on previous stages completing successfully. For a real-world example, check: <https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech>

View File

@@ -1,445 +0,0 @@
module site
import incubaid.herolib.core.playbook
import incubaid.herolib.web.site
import incubaid.herolib.ui.console
import os
// Big comprehensive HeroScript for testing
const test_heroscript = '
!!site.config
name: "test_docs"
title: "Test Documentation Site"
description: "A comprehensive test documentation site"
tagline: "Testing everything"
favicon: "img/favicon.png"
image: "img/test-og.png"
copyright: "© 2024 Test Organization"
url: "https://test.example.com"
base_url: "/"
url_home: "/docs"
!!site.config_meta
title: "Test Docs - Advanced"
image: "img/test-og-alternative.png"
description: "Advanced test documentation"
!!site.navbar
title: "Test Documentation"
logo_alt: "Test Logo"
logo_src: "img/logo.svg"
logo_src_dark: "img/logo-dark.svg"
!!site.navbar_item
label: "Getting Started"
to: "intro"
position: "left"
!!site.navbar_item
label: "API Reference"
to: "api"
position: "left"
!!site.navbar_item
label: "GitHub"
href: "https://github.com/example/test"
position: "right"
!!site.navbar_item
label: "Blog"
href: "https://blog.example.com"
position: "right"
!!site.footer
style: "dark"
!!site.footer_item
title: "Documentation"
label: "Introduction"
to: "intro"
!!site.footer_item
title: "Documentation"
label: "Getting Started"
to: "getting-started"
!!site.footer_item
title: "Documentation"
label: "Advanced Topics"
to: "advanced"
!!site.footer_item
title: "Community"
label: "Discord"
href: "https://discord.gg/example"
!!site.footer_item
title: "Community"
label: "Twitter"
href: "https://twitter.com/example"
!!site.footer_item
title: "Legal"
label: "Privacy Policy"
href: "https://example.com/privacy"
!!site.footer_item
title: "Legal"
label: "Terms of Service"
href: "https://example.com/terms"
!!site.announcement
id: "v2-release"
content: "🎉 Version 2.0 is now available! Check out the new features."
background_color: "#1a472a"
text_color: "#fff"
is_closeable: true
!!site.page_category
name: "getting_started"
label: "Getting Started"
position: 10
!!site.page src: "guides:introduction"
title: "Introduction to Test Docs"
description: "Learn what this project is about"
!!site.page src: "installation"
title: "Installation Guide"
description: "How to install and setup"
!!site.page src: "quick_start"
title: "Quick Start"
description: "5 minute quick start guide"
!!site.page_category
name: "concepts"
label: "Core Concepts"
position: 20
!!site.page src: "concepts:architecture"
title: "Architecture Overview"
description: "Understanding the system architecture"
!!site.page src: "components"
title: "Key Components"
description: "Learn about the main components"
!!site.page src: "workflow"
title: "Typical Workflow"
description: "How to use the system"
!!site.page_category
name: "api"
label: "API Reference"
position: 30
!!site.page src: "api:rest"
title: "REST API"
description: "Complete REST API reference"
!!site.page src: "graphql"
title: "GraphQL API"
description: "GraphQL API documentation"
!!site.page src: "webhooks"
title: "Webhooks"
description: "Webhook configuration and examples"
!!site.page_category
name: "advanced"
label: "Advanced Topics"
position: 40
!!site.page src: "advanced:performance"
title: "Performance Optimization"
description: "Tips for optimal performance"
!!site.page src: "scaling"
title: "Scaling Guide"
description: "How to scale the system"
!!site.page src: "security"
title: "Security Best Practices"
description: "Security considerations and best practices"
!!site.page src: "troubleshooting"
title: "Troubleshooting"
description: "Common issues and solutions"
draft: false
!!site.publish
path: "/var/www/html/docs"
ssh_name: "production-server"
!!site.publish_dev
path: "/tmp/docs-dev"
'
fn test_site1() ! {
console.print_header('Site Module Comprehensive Test')
console.lf()
// ========================================================
// TEST 1: Create playbook from heroscript
// ========================================================
console.print_item('TEST 1: Creating playbook from HeroScript')
mut plbook := playbook.new(text: test_heroscript)!
console.print_green(' Playbook created successfully')
console.lf()
// ========================================================
// TEST 2: Process site configuration
// ========================================================
console.print_item('TEST 2: Processing site.play()')
site.play(mut plbook)!
console.print_green(' Site configuration processed successfully')
console.lf()
// ========================================================
// TEST 3: Retrieve site and validate
// ========================================================
console.print_item('TEST 3: Retrieving configured site')
mut test_site := site.get(name: 'test_docs')!
console.print_green(' Site retrieved successfully')
console.lf()
// ========================================================
// TEST 4: Validate SiteConfig
// ========================================================
console.print_header('Validating SiteConfig')
mut config := &test_site.siteconfig
help_test_string('Site Name', config.name, 'test_docs')
help_test_string('Site Title', config.title, 'Test Documentation Site')
help_test_string('Site Description', config.description, 'A comprehensive test documentation site')
help_test_string('Site Tagline', config.tagline, 'Testing everything')
help_test_string('Copyright', config.copyright, '© 2024 Test Organization')
help_test_string('Base URL', config.base_url, '/')
help_test_string('URL Home', config.url_home, '/docs')
help_test_string('Meta Title', config.meta_title, 'Test Docs - Advanced')
help_test_string('Meta Image', config.meta_image, 'img/test-og-alternative.png')
assert config.build_dest.len == 1, 'Should have 1 production build destination'
console.print_green(' Production build dest: ${config.build_dest[0].path}')
assert config.build_dest_dev.len == 1, 'Should have 1 dev build destination'
console.print_green(' Dev build dest: ${config.build_dest_dev[0].path}')
console.lf()
// ========================================================
// TEST 5: Validate Menu Configuration
// ========================================================
console.print_header('Validating Menu Configuration')
mut menu := config.menu
help_test_string('Menu Title', menu.title, 'Test Documentation')
help_test_string('Menu Logo Alt', menu.logo_alt, 'Test Logo')
help_test_string('Menu Logo Src', menu.logo_src, 'img/logo.svg')
help_test_string('Menu Logo Src Dark', menu.logo_src_dark, 'img/logo-dark.svg')
assert menu.items.len == 4, 'Should have 4 navbar items, got ${menu.items.len}'
console.print_green(' Menu has 4 navbar items')
// Validate navbar items
help_test_navbar_item(menu.items[0], 'Getting Started', 'intro', '', 'left')
help_test_navbar_item(menu.items[1], 'API Reference', 'api', '', 'left')
help_test_navbar_item(menu.items[2], 'GitHub', '', 'https://github.com/example/test',
'right')
help_test_navbar_item(menu.items[3], 'Blog', '', 'https://blog.example.com', 'right')
console.lf()
// ========================================================
// TEST 6: Validate Footer Configuration
// ========================================================
console.print_header('Validating Footer Configuration')
mut footer := config.footer
help_test_string('Footer Style', footer.style, 'dark')
assert footer.links.len == 3, 'Should have 3 footer link groups, got ${footer.links.len}'
console.print_green(' Footer has 3 link groups')
// Validate footer structure
for link_group in footer.links {
console.print_item('Footer group: "${link_group.title}" has ${link_group.items.len} items')
}
// Detailed footer validation
mut doc_links := footer.links.filter(it.title == 'Documentation')
assert doc_links.len == 1, 'Should have 1 Documentation link group'
assert doc_links[0].items.len == 3, 'Documentation should have 3 items'
console.print_green(' Documentation footer: 3 items')
mut community_links := footer.links.filter(it.title == 'Community')
assert community_links.len == 1, 'Should have 1 Community link group'
assert community_links[0].items.len == 2, 'Community should have 2 items'
console.print_green(' Community footer: 2 items')
mut legal_links := footer.links.filter(it.title == 'Legal')
assert legal_links.len == 1, 'Should have 1 Legal link group'
assert legal_links[0].items.len == 2, 'Legal should have 2 items'
console.print_green(' Legal footer: 2 items')
console.lf()
// ========================================================
// TEST 7: Validate Announcement Bar
// ========================================================
console.print_header('Validating Announcement Bar')
mut announcement := config.announcement
help_test_string('Announcement ID', announcement.id, 'v2-release')
help_test_string('Announcement Content', announcement.content, '🎉 Version 2.0 is now available! Check out the new features.')
help_test_string('Announcement BG Color', announcement.background_color, '#1a472a')
help_test_string('Announcement Text Color', announcement.text_color, '#fff')
assert announcement.is_closeable == true, 'Announcement should be closeable'
console.print_green(' Announcement bar configured correctly')
console.lf()
// ========================================================
// TEST 8: Validate Pages
// ========================================================
console.print_header('Validating Pages')
mut pages := test_site.pages.clone()
assert pages.len == 13, 'Should have 13 pages, got ${pages.len}'
console.print_green(' Total pages: ${pages.len}')
// List and validate pages
mut page_ids := pages.keys()
page_ids.sort()
for page_id in page_ids {
mut page := pages[page_id]
console.print_debug(' Page: ${page_id} - "${page.title}"')
}
// Validate specific pages
assert 'guides:introduction' in pages, 'guides:introduction page not found'
console.print_green(' Found guides:introduction')
assert 'concepts:architecture' in pages, 'concepts:architecture page not found'
console.print_green(' Found concepts:architecture')
assert 'api:rest' in pages, 'api:rest page not found'
console.print_green(' Found api:rest')
console.lf()
// ========================================================
// TEST 9: Validate Navigation Structure
// ========================================================
console.print_header('Validating Navigation Structure')
mut sidebar := unsafe { test_site.nav.my_sidebar.clone() }
console.print_item('Navigation sidebar has ${sidebar.len} items')
// Count categories
mut category_count := 0
mut doc_count := 0
for item in sidebar {
match item {
site.NavCat {
category_count++
console.print_debug(' Category: "${item.label}" with ${item.items.len} sub-items')
}
site.NavDoc {
doc_count++
console.print_debug(' Doc: "${item.label}" (${item.id})')
}
site.NavLink {
console.print_debug(' Link: "${item.label}" -> ${item.href}')
}
}
}
assert category_count == 4, 'Should have 4 categories, got ${category_count}'
console.print_green(' Navigation has 4 categories')
// Validate category structure
for item in sidebar {
match item {
site.NavCat {
console.print_item('Category: "${item.label}"')
println(' Collapsible: ${item.collapsible}, Collapsed: ${item.collapsed}')
println(' Items: ${item.items.len}')
// Validate sub-items
for sub_item in item.items {
match sub_item {
site.NavDoc {
println(' - ${sub_item.label} (${sub_item.id})')
}
else {
println(' - Unexpected item type')
}
}
}
}
else {}
}
}
console.lf()
// ========================================================
// TEST 10: Validate Site Factory
// ========================================================
console.print_header('Validating Site Factory')
mut all_sites := site.list()
console.print_item('Total sites registered: ${all_sites.len}')
for site_name in all_sites {
console.print_debug(' - ${site_name}')
}
assert all_sites.contains('test_docs'), 'test_docs should be in sites list'
console.print_green(' test_docs found in factory')
assert site.exists(name: 'test_docs'), 'test_docs should exist'
console.print_green(' test_docs verified to exist')
console.lf()
// ========================================================
// FINAL SUMMARY
// ========================================================
console.print_header('Test Summary')
console.print_green(' All tests passed successfully!')
console.print_item('Site Name: ${config.name}')
console.print_item('Pages: ${pages.len}')
console.print_item('Navigation Categories: ${category_count}')
console.print_item('Navbar Items: ${menu.items.len}')
console.print_item('Footer Groups: ${footer.links.len}')
console.print_item('Announcement: Active')
console.print_item('Build Destinations: ${config.build_dest.len} prod, ${config.build_dest_dev.len} dev')
console.lf()
console.print_green('All validations completed successfully!')
}
// ============================================================
// Helper Functions for Testing
// ============================================================
fn help_test_string(label string, actual string, expected string) {
if actual == expected {
console.print_green(' ${label}: "${actual}"')
} else {
console.print_stderr(' ${label}: expected "${expected}", got "${actual}"')
panic('Test failed: ${label}')
}
}
fn help_test_navbar_item(item MenuItem, label string, to string, href string, position string) {
assert item.label == label, 'Expected label "${label}", got "${item.label}"'
assert item.to == to, 'Expected to "${to}", got "${item.to}"'
assert item.href == href, 'Expected href "${href}", got "${item.href}"'
assert item.position == position, 'Expected position "${position}", got "${item.position}"'
console.print_green(' Navbar item: "${label}"')
}