atlas back

This commit is contained in:
2025-12-02 09:53:35 +01:00
parent c3690f3d53
commit ce3bb5cd9e
82 changed files with 3155 additions and 5184 deletions

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.

48
lib/web/site/factory.v Normal file
View File

@@ -0,0 +1,48 @@
module site
import incubaid.herolib.core.texttools
__global (
websites map[string]&Site
)
@[params]
pub struct FactoryArgs {
pub mut:
name string = 'default'
}
pub fn new(args FactoryArgs) !&Site {
name := texttools.name_fix(args.name)
websites[name] = &Site{
siteconfig: SiteConfig{
name: name
}
}
return get(name: name)!
}
pub fn get(args FactoryArgs) !&Site {
name := texttools.name_fix(args.name)
mut sc := websites[name] or { return error('siteconfig with name "${name}" does not exist') }
return sc
}
pub fn exists(args FactoryArgs) bool {
name := texttools.name_fix(args.name)
mut sc := websites[name] or { return false }
return true
}
pub fn default() !&Site {
if websites.len == 0 {
return new(name: 'default')!
}
return get()!
}
// list returns all site names that have been created
pub fn list() []string {
return websites.keys()
}

16
lib/web/site/model_page.v Normal file
View File

@@ -0,0 +1,16 @@
module site
pub struct Page {
pub mut:
name string
title string
description string
draft bool
position int
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

@@ -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

@@ -0,0 +1,97 @@
module site
import os
// Combined config structure
@[heap]
pub struct SiteConfig {
pub mut:
name string
title string = 'My Documentation Site' // General site title
description string // General site description, can be used for meta if meta_description not set
tagline string
favicon string = 'img/favicon.png'
image string = 'img/tf_graph.png' // General site image, can be used for meta if meta_image not set
copyright string = 'someone'
footer Footer
menu Menu
imports []ImportItem
// New fields for Docusaurus compatibility
url string // The main URL of the site (from !!site.config url:)
base_url string // The base URL for Docusaurus (from !!site.config base_url:)
url_home string // The home page path relative to base_url (from !!site.config url_home:)
meta_title string // Specific title for SEO metadata (from !!site.config_meta title:)
meta_image string // Specific image for SEO metadata (og:image) (from !!site.config_meta image:)
build_dest []BuildDest // Production build destinations (from !!site.build_dest)
build_dest_dev []BuildDest // Development build destinations (from !!site.build_dest_dev)
announcement AnnouncementBar // Announcement bar configuration (from !!site.announcement)
}
// Announcement bar config structure
pub struct AnnouncementBar {
pub mut:
id string @[json: 'id']
content string @[json: 'content']
background_color string @[json: 'backgroundColor']
text_color string @[json: 'textColor']
is_closeable bool @[json: 'isCloseable']
}
// Footer config structures
pub struct FooterItem {
pub mut:
label string
to string
href string
}
pub struct FooterLink {
pub mut:
title string
items []FooterItem
}
pub struct Footer {
pub mut:
style string = 'dark'
links []FooterLink
}
// menu config structures
pub struct MenuItem {
pub mut:
href string
to string
label string
position string
}
pub struct Menu {
pub mut:
title string
items []MenuItem
logo_alt string @[json: 'logoAlt']
logo_src string @[json: 'logoSrc']
logo_src_dark string @[json: 'logoSrcDark']
}
pub struct BuildDest {
pub mut:
path string
ssh_name string
}
// is to import one docusaurus site into another, can be used to e.g. import static parts from one location into the build one we are building
pub struct ImportItem {
pub mut:
name string // will normally be empty
url string // http git url can be to specific path
path string
dest string // location in the docs folder of the place where we will build the documentation site e.g. docusaurus
replace map[string]string // will replace ${NAME} in the imported content
visible bool = true
}

225
lib/web/site/play.v Normal file
View File

@@ -0,0 +1,225 @@
module site
import os
import incubaid.herolib.core.playbook { PlayBook }
import incubaid.herolib.core.texttools
import time
pub fn play(mut plbook PlayBook) ! {
if !plbook.exists(filter: 'site.') {
return
}
mut config_action := plbook.ensure_once(filter: 'site.config')!
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 config := &website.siteconfig
config.name = texttools.name_fix(name)
config.title = p.get_default('title', 'Documentation Site')!
config.description = p.get_default('description', 'Comprehensive documentation built with Docusaurus.')!
config.tagline = p.get_default('tagline', 'Your awesome documentation')!
config.favicon = p.get_default('favicon', 'img/favicon.png')!
config.image = p.get_default('image', 'img/tf_graph.png')!
config.copyright = p.get_default('copyright', '© ' + time.now().year.str() +
' Example Organization')!
config.url = p.get_default('url', '')!
config.base_url = p.get_default('base_url', '/')!
config.url_home = p.get_default('url_home', '')!
// 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.
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.
config.meta_image = p_meta.get_default('image', config.image)!
// If 'description' is present in site.config_meta, it overrides the main description
if p_meta.exists('description') {
config.description = p_meta.get('description')!
}
config_action.done = true // Mark the action as done
meta_action.done = true
play_import(mut plbook, mut config)!
play_menu(mut plbook, mut config)!
play_footer(mut plbook, mut config)!
play_announcement(mut plbook, mut config)!
play_publish(mut plbook, mut config)!
play_publish_dev(mut plbook, mut config)!
play_pages(mut plbook, mut website)!
}
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
}
}

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;
}
}

328
lib/web/site/readme.md Normal file
View File

@@ -0,0 +1,328 @@
# Site Module
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
```v
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
import incubaid.herolib.develop.gittools
import incubaid.herolib.web.site
import incubaid.herolib.core.playcmds
// Clone or use existing repository with HeroScript files
mysitepath := gittools.path(
git_url: 'https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech'
git_pull: true
)!
// Process all HeroScript files in the path
playcmds.run(heroscript_path: mysitepath.path)!
// Get the configured site
mut mysite := site.get(name: 'tfgrid_tech')!
println(mysite)
```
## HeroScript Syntax
### Basic Configuration
```heroscript
!!site.config
name: "my_site"
title: "My Documentation Site"
description: "Comprehensive documentation"
tagline: "Your awesome documentation"
favicon: "img/favicon.png"
image: "img/site-image.png"
copyright: "© 2024 My Organization"
url: "https://docs.example.com"
base_url: "/"
```
### Navigation Menu
```heroscript
!!site.navbar
title: "My Site"
logo_alt: "Site Logo"
logo_src: "img/logo.svg"
logo_src_dark: "img/logo-dark.svg"
!!site.navbar_item
label: "Documentation"
to: "docs/intro"
position: "left"
!!site.navbar_item
label: "GitHub"
href: "https://github.com/myorg/myrepo"
position: "right"
```
### Footer Configuration
```heroscript
!!site.footer
style: "dark"
!!site.footer_item
title: "Docs"
label: "Introduction"
to: "intro"
!!site.footer_item
title: "Docs"
label: "Getting Started"
href: "https://docs.example.com/getting-started"
!!site.footer_item
title: "Community"
label: "Discord"
href: "https://discord.gg/example"
```
## 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
!!site.page src: "mycelium_tech:introduction"
description: "Introduction to ThreeFold Technology"
slug: "/"
!!site.page src: "vision"
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"
```
**Key Points:**
- 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)
### Example 2: Pages with Categories
Categories (sections) help organize pages into logical groups with their own navigation structure.
```heroscript
!!site.page_category
name: "first_principle_thinking"
label: "First Principle Thinking"
!!site.page src: "first_principle_thinking:hardware_badly_used"
description: "Hardware is not used properly, why it is important to understand hardware"
!!site.page src: "internet_risk"
description: "Internet risk, how to mitigate it, and why it is important"
!!site.page src: "onion_analogy"
description: "Compare onion with a computer, layers of abstraction"
```
**Key Points:**
- `!!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
!!site.import
url: "https://github.com/example/external-docs"
dest: "external"
replace: "PROJECT_NAME:My Project,VERSION:1.0.0"
visible: true
```
## Publish Destinations
```heroscript
!!site.publish
path: "/var/www/html/docs"
ssh_name: "production_server"
!!site.publish_dev
path: "/tmp/docs-preview"
```
## Factory Methods
### Create or Get a Site
```v
import incubaid.herolib.web.site
// Create a new site
mut mysite := site.new(name: 'my_docs')!
// Get an existing site
mut mysite := site.get(name: 'my_docs')!
// Get default site
mut mysite := site.default()!
// Check if site exists
if site.exists(name: 'my_docs') {
println('Site exists')
}
// List all sites
sites := site.list()
println(sites)
```
### Using with PlayBook
```v
import incubaid.herolib.core.playbook
import incubaid.herolib.web.site
// Create playbook from path
mut plbook := playbook.new(path: '/path/to/heroscripts')!
// Process site configuration
site.play(mut plbook)!
// Access the configured site
mut mysite := site.get(name: 'my_site')!
```
## Data Structures
### Site
```v
pub struct Site {
pub mut:
pages []Page
sections []Section
siteconfig SiteConfig
}
```
### Page
```v
pub struct Page {
pub mut:
name string // Page identifier
title string // Display title
description string // Page description
draft bool // Draft status
position int // Sort order
hide_title bool // Hide title in rendering
src string // Source as collection:page_name
path string // URL path (without page name)
section_name string // Category/section name
title_nr int // Title numbering level
slug string // Custom URL slug
}
```
### Section
```v
pub struct Section {
pub mut:
name string // Internal identifier
position int // Sort order
path string // URL path
label string // Display name
}
```
## Best Practices
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
## Complete Example
See `examples/web/site/site_example.vsh` for a complete working example.
For a real-world example, check: <https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech>