Compare commits

...

13 Commits

Author SHA1 Message Date
Scott Yeager
1688c4f9b0 bump version to 1.0.36 2025-10-14 14:37:14 -07:00
Scott Yeager
9a7b9b8a10 Fix release flow 2025-10-14 14:35:59 -07:00
Scott Yeager
2b5f53b83f bump version to 1.0.35 2025-10-14 14:05:36 -07:00
Scott Yeager
d65c795ccb Update test workflow 2025-10-14 13:50:14 -07:00
Scott Yeager
3918148378 Update workflows 2025-10-14 13:36:56 -07:00
Mahmoud-Emad
f5694dd4d7 refactor: Remove Docusaurus path logic
- Remove Redis Docusaurus path lookup
- Simplify path generation logic
2025-10-14 17:45:45 +03:00
Mahmoud-Emad
5fc0909ce7 refactor: Rename docusaurus command to docs
- Change command name from 'docusaurus' to 'docs'
- Update path handling for empty Docusaurus paths
- Adjust example usage in documentation
2025-10-14 13:08:03 +03:00
230c684725 ... 2025-10-14 13:34:29 +04:00
081aafa8c6 ... 2025-10-14 11:41:16 +04:00
25eb9bbb86 ... 2025-10-14 11:25:11 +04:00
5d4fe2fa2f Merge branch 'development' of github.com:incubaid/herolib into development
* 'development' of github.com:incubaid/herolib:
  bump version to 1.0.34
  feat: Add heroscript serialization/deserialization functions
  fix: Remove the seurity workflow
  update the ci security workfolw
  feat: Add encoderhero and heroscript_dumps/loads
2025-10-14 09:21:27 +04:00
956c92ee79 ... 2025-10-14 09:14:14 +04:00
Omdanii
c6ea409bd8 Merge pull request #180 from Incubaid/development_fix_installers
Fix clients hero generate and Add encoderhero and heroscript_dumps/loads
2025-10-13 22:33:35 +03:00
23 changed files with 1177 additions and 644 deletions

0
, Normal file
View File

View File

@@ -5,71 +5,112 @@ permissions:
on:
push:
tags:
- "*"
workflow_dispatch:
jobs:
build:
timeout-minutes: 60
if: startsWith(github.ref, 'refs/tags/')
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-musl
os: alpine-latest
short-name: linux-i64
- target: aarch64-unknown-linux-musl
os: alpine-latest
short-name: linux-arm64
arch: arm64
- target: x86_64-linux
os: ubuntu-latest
- target: aarch64-linux
os: ubuntu-24.04-arm
- target: aarch64-apple-darwin
os: macos-latest
short-name: macos-arm64
# - target: x86_64-apple-darwin
# os: macos-13
# short-name: macos-i64
runs-on: ${{ matrix.os }}
container: ${{ matrix.container }}
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref_name }} and your repository is ${{ github.repository }}."
- uses: maxim-lobanov/setup-xcode@v1
if: runner.os == 'macOS'
with:
xcode-version: latest-stable
- name: Check out repository code
- name: Checkout code
uses: actions/checkout@v4
# - name: Setup dependencies on Alpine
# if: runner.os == 'Linux'
# run: |
# sudo apk add --no-cache gcc musl-dev openssl-dev openssl-libs-static
# We do the workaround as described here https://github.com/Incubaid/herolib?tab=readme-ov-file#tcc-compiler-error-on-macos
# gcc and clang also don't work on macOS due to https://github.com/vlang/v/issues/25467
# We can change the compiler or remove this when one is fixed
- name: Setup V & Herolib
id: setup
shell: bash
run: |
./install_v.sh
git clone https://github.com/vlang/v
cd v
make
./v symlink
if [ "${{ runner.os }}" = "macOS" ]; then
sudo sed -i '' '618,631d' /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/math.h
fi
cd -
mkdir -p ~/.vmodules/incubaid
ln -s $(pwd)/lib ~/.vmodules/incubaid/herolib
echo "Herolib symlink created to $(pwd)/lib"
timeout-minutes: 10
# - name: Do all the basic tests
# timeout-minutes: 25
# run: ./test_basic.vsh
# We can't make static builds for Linux easily, since we link to libql
# (Postgres) and this has no static version available in the Alpine
# repos. Therefore we build dynamic binaries for both glibc and musl.
#
# Again we work around a bug limiting our choice of C compiler tcc won't
# work on Alpine due to https://github.com/vlang/v/issues/24866
# So always use gcc for Linux
#
# For macOS, we can only use tcc (see above), but then we hit issues using
# the garbage collector, so disable that
- name: Build Hero
timeout-minutes: 15
run: |
set -e
if [ "${{ runner.os }}" = "Linux" ]; then
v -w -d use_openssl -enable-globals -cflags -cc gcc -static cli/hero.v -o cli/hero-${{ matrix.target }}
sudo apt-get install libpq-dev
# Build for glibc
v -w -d use_openssl -enable-globals -cc gcc cli/hero.v -o cli/hero-${{ matrix.target }}
# Build for musl using Alpine in Docker
docker run --rm \
-v ${{ github.workspace }}/lib:/root/.vmodules/incubaid/herolib \
-v ${{ github.workspace }}:/herolib \
-w /herolib \
alpine \
sh -c '
apk add --no-cache bash git build-base openssl-dev libpq-dev
cd v
make clean
make
./v symlink
cd ..
v -w -d use_openssl -enable-globals -cc gcc cli/hero.v -o cli/hero-${{ matrix.target }}-musl
'
else
v -w -d use_openssl -enable-globals cli/hero.v -o cli/hero-${{ matrix.target }}
v -w -d use_openssl -enable-globals -gc none -cc tcc cli/hero.v -o cli/hero-${{ matrix.target }}
fi
- name: Upload glibc binary
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: hero-${{ matrix.target }}
path: cli/hero-${{ matrix.target }}
- name: Upload musl binary
if: runner.os == 'Linux'
uses: actions/upload-artifact@v4
with:
name: hero-${{ matrix.target }}-musl
path: cli/hero-${{ matrix.target }}-musl
- name: Upload
if: runner.os != 'Linux'
uses: actions/upload-artifact@v4
with:
name: hero-${{ matrix.target }}
@@ -80,12 +121,8 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Check out repository code
uses: actions/checkout@v4
- name: Download Artifacts
uses: actions/download-artifact@v4
with:

View File

@@ -5,24 +5,20 @@ permissions:
on:
push:
branches:
- "**"
workflow_dispatch:
jobs:
build:
strategy:
matrix:
include:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
short-name: linux-i64
runs-on: ${{ matrix.os }}
runs-on: ubuntu-latest
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
- run: echo "🔎 The name of your branch is ${{ github.ref_name }} and your repository is ${{ github.repository }}."
- name: Check out repository code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Setup V
run: |

View File

@@ -53,7 +53,7 @@ fn do() ! {
mut cmd := Command{
name: 'hero'
description: 'Your HERO toolset.'
version: '1.0.34'
version: '1.0.36'
}
// herocmds.cmd_run_add_flags(mut cmd)

View File

@@ -0,0 +1,21 @@
!!docusaurus.define
path_build: "/tmp/docusaurus_build"
path_publish: "/tmp/docusaurus_publish"
// reset: 1
// install: 1
// template_update: 1
!!docusaurus.add sitename:"owh_intro"
git_url:"https://git.ourworld.tf/ourworld_holding/docs_owh/src/branch/main/ebooks/owh_intro"
git_root:"/tmp/code"
git_reset:1
git_pull:1
play:true
// !!docusaurus.build
!!docusaurus.dev site:"owh_intro" open:true

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env -S v -n -w -gc none -cg -cc tcc -d use_openssl -enable-globals run
import incubaid.herolib.core.playcmds
playcmds.run(
heroscript: '
!!docusaurus.define
path_build: "/tmp/docusaurus_build"
path_publish: "/tmp/docusaurus_publish"
// reset: 1
// install: 1
// template_update: 1
!!docusaurus.add sitename:"owh_intro"
git_url:"https://git.ourworld.tf/ourworld_holding/docs_owh/src/branch/main/ebooks/owh_intro"
git_root:"/tmp/code"
git_reset:1
git_pull:1
play:true
// !!docusaurus.build
!!docusaurus.dev site:"owh_intro" open:true
'
)!

View File

@@ -0,0 +1,99 @@
#!/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
// Example 1: Load site configuration from a Git repository
println('=== Example 1: Loading from Git Repository ===')
url := 'https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech'
mysitepath := gittools.path(
git_url: url
// git_pull: true
// git_reset: true // Uncomment to reset to latest
)!
// Process all HeroScript files in the repository
playcmds.run(heroscript_path: mysitepath.path)!
// Get the configured site
mut mysite := site.get(name: 'tfgrid_tech')!
println('Site loaded: ${mysite.siteconfig.name}')
println('Title: ${mysite.siteconfig.title}')
println('Pages: ${mysite.pages.len}')
println('Sections: ${mysite.sections.len}')
println('')
// Example 2: Inspect site structure
println('=== Example 2: Site Structure ===')
println('Sections:')
for section in mysite.sections {
println(' - ${section.label} (${section.name})')
}
println('')
println('Pages (first 5):')
for i, page in mysite.pages {
if i >= 5 {
break
}
println(' - ${page.name}: ${page.description}')
println(' Section: ${page.section_name}, Position: ${page.position}')
}
println('')
// Example 3: Access menu configuration
println('=== Example 3: Navigation Menu ===')
println('Menu Title: ${mysite.siteconfig.menu.title}')
println('Menu Items:')
for item in mysite.siteconfig.menu.items {
if item.href != '' {
println(' - ${item.label} -> ${item.href} (external)')
} else {
println(' - ${item.label} -> ${item.to} (internal)')
}
}
println('')
// Example 4: Access footer configuration
println('=== Example 4: Footer Configuration ===')
println('Footer Style: ${mysite.siteconfig.footer.style}')
println('Footer Links:')
for link in mysite.siteconfig.footer.links {
println(' ${link.title}:')
for item in link.items {
if item.href != '' {
println(' - ${item.label} -> ${item.href}')
} else {
println(' - ${item.label} -> ${item.to}')
}
}
}
println('')
// Example 5: List all configured sites
println('=== Example 5: All Configured Sites ===')
all_sites := site.list()
println('Total sites: ${all_sites.len}')
for site_name in all_sites {
println(' - ${site_name}')
}
println('')
// Example 6: Check if a site exists
println('=== Example 6: Site Existence Check ===')
if site.exists(name: 'tfgrid_tech') {
println('Site "tfgrid_tech" exists')
} else {
println('Site "tfgrid_tech" does not exist')
}
if site.exists(name: 'nonexistent_site') {
println('Site "nonexistent_site" exists')
} else {
println('Site "nonexistent_site" does not exist')
}
println('')
println('=== Example Complete ===')

View File

@@ -4,7 +4,7 @@ set -e
os_name="$(uname -s)"
arch_name="$(uname -m)"
version='1.0.34'
version='1.0.36'
# Base URL for GitHub releases

View File

@@ -9,7 +9,7 @@ import cli { Command, Flag }
pub fn cmd_docusaurus(mut cmdroot Command) Command {
mut cmd_run := Command{
name: 'docusaurus'
name: 'docs'
description: 'Generate, build, run docusaurus sites.'
required_args: 0
execute: cmd_docusaurus_execute

View File

@@ -42,7 +42,6 @@ you can now just execute this script and hero will interprete the content
```v
import incubaid.herolib.core.playbook
import incubaid.herolib.core.playcmds
mut plbook := playbook.new(path: "....") or { panic(err) }

View File

@@ -1,5 +1,7 @@
# how to use the playcmds
## simplified
```v
import incubaid.herolib.core.playcmds
@@ -11,3 +13,29 @@ playcmds.run(
)!
```
## with more control
```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
url := "https://git.ourworld.tf/tfgrid/docs_tfgrid4/src/branch/main/ebooks/tech"
mysitepath := gittools.path(
// git_pull: true
// git_reset: true
git_url: url
git_root: '/tmp/code'
)!
playcmds.run(heroscript_path:mysitepath.path)!
mut mysite:=site.get(name:"tfgrid_tech")!
println(mysite)
```

View File

@@ -1,7 +1,6 @@
module data
import incubaid.herolib.core.texttools
import incubaid.herolib.core.base
import incubaid.herolib.data.markdown.elements
import incubaid.herolib.data.doctree.pointer
@@ -33,27 +32,11 @@ pub fn (page Page) process_links(paths map[string]string) ![]string {
doc.linked_pages << ptr.str()
}
// Check if Docusaurus-specific paths are available in Redis
mut context := base.context() or { base.context_new()! }
mut redis := context.redis() or { panic('Redis not available') }
// Try to get Docusaurus-specific path from Redis
if docusaurus_path := redis.hget('doctree_docusaurus_paths', ptr.str()) {
// Use Docusaurus path (already without .md extension)
// Ensure it starts with / for absolute path
if docusaurus_path.starts_with('/') {
path = docusaurus_path
} else {
path = '/' + docusaurus_path
}
if ptr.collection == page.collection_name {
// same directory
path = './' + path.all_after_first('/')
} else {
// Fall back to default behavior: relative paths with .md
if ptr.collection == page.collection_name {
// same directory
path = './' + path.all_after_first('/')
} else {
path = '../${path}'
}
path = '../${path}'
}
if ptr.cat == .image && element.extra.trim_space() != '' {

View File

@@ -9,3 +9,4 @@
build: true
startupmanager: true
supported_platforms: ""
// active: false

View File

@@ -15,16 +15,15 @@ For quick setup and development, use the hero command:
```bash
# Start development server
hero docusaurus -d -path /path/to/your/site
hero docs -d -path /path/to/your/site
# Build for production
hero docusaurus -b -path /path/to/your/site
hero docs -b -path /path/to/your/site
# Build and publish
hero docusaurus -bp -path /path/to/your/site
hero docs -bp -path /path/to/your/site
```
### Example HeroScript
```heroscript

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

@@ -1,100 +0,0 @@
!!site.config
name:"depin"
description:"ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet."
tagline:"Geo Aware Internet Platform"
favicon:"img/favicon.png"
image:"img/tf_graph.png"
copyright:"ThreeFold"
!!site.menu
title:"ThreeFold DePIN Tech"
logo_alt:"ThreeFold Logo"
logo_src:"img/logo.svg"
logo_src_dark:"img/new_logo_tft.png"
!!site.menu_item
label:"ThreeFold.io"
href:"https://threefold.io"
position:"right"
!!site.menu_item
label:"Mycelium Network"
href:"https://mycelium.threefold.io/"
position:"right"
!!site.menu_item
label:"AI Box"
href:"https://aibox.threefold.io/"
position:"right"
!!site.footer
style:"dark"
!!site.footer_item
title:"Docs"
label:"Introduction"
to:"intro"
!!site.footer_item
title:"Docs"
label:"Litepaper"
href:"https://docs.threefold.io/docs/litepaper/"
!!site.footer_item
title:"Docs"
label:"Roadmap"
href:"https://docs.threefold.io/docs/roadmap"
!!site.footer_item
title:"Docs"
label:"Manual"
href:"https://manual.grid.tf/"
!!site.footer_item
title:"Features"
label:"Become a Farmer"
href:"https://docs.threefold.io/docs/category/become-a-farmer"
!!site.footer_item
title:"Features"
label:"Components"
href:"https://docs.threefold.io/docs/category/components"
!!site.footer_item
title:"Features"
label:"Technology"
href:"https://threefold.info/tech/"
!!site.footer_item
title:"Features"
label:"Tokenomics"
href:"https://docs.threefold.io/docs/tokens/tokenomics"
!!site.footer_item
title:"Web"
label:"ThreeFold.io"
href:"https://threefold.io"
!!site.footer_item
title:"Web"
label:"Dashboard"
href:"https://dashboard.grid.tf"
!!site.footer_item
title:"Web"
label:"GitHub"
href:"https://github.com/threefoldtech/home"
!!site.footer_item
title:"Web"
label:"Mycelium Network"
href:"https://mycelium.threefold.io/"
!!site.footer_item
title:"Web"
label:"AI Box"
href:"https://www2.aibox.threefold.io/"
!!site.collections
url:"https://github.com/example/external-docs"
replace:"PROJECT_NAME:My Project, VERSION:1.0.0"

View File

@@ -1,18 +0,0 @@
!!site.page name:intro
description:"ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet."
//next is example where we use all properties, folder is where the page is located, prio is the order of the page, if not used the filled in from order in which we parse this config file
!!site.page name:mycelium draft:true folder:"/specs/components" prio:4
content:"the page content itself, only for small pages"
title:"Mycelium as Title"
description:"..."
!!site.page name:fungistor folder:"/specs/components" prio:1
src:"mycollection:mycelium.md"
title:"fungistor as Title"
description:"...."
!!site.page name:fungistor folder:"/specs/components" prio:1
src:"mycollection:mycelium" //can be without .md
title:"fungistor as Title"
description:"..."

View File

@@ -1,29 +1,16 @@
module site
@[heap]
pub struct Site {
pub mut:
pages []Page
sections []Section
siteconfig SiteConfig
}
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
path string @[required]
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
}
pub struct Section {
pub mut:
position int
path string
label string
}

View File

@@ -0,0 +1,17 @@
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
}

View File

@@ -73,6 +73,7 @@ pub mut:
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

View File

@@ -1,89 +1,126 @@
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'
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 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")
// }
// }
// LETS FIRST DO THE CATEGORIES
mut category_actions := plbook.find(filter: 'site.page_category')!
mut section := Section{}
for mut action in category_actions {
// println(action)
mut p := action.params
section.position = p.get_int_default('position', 20)!
section.label = p.get('label') or {
return error('need to specify label in site.page_category')
}
section.path = p.get('path') or {
return error('need to specify path in site.page_category')
}
site.sections << section
action.done = true // Mark the action as done
}
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 page_actions := plbook.find(filter: 'site.page')!
mut mypage := Page{
src: ''
path: ''
}
mut position_next := 1
mut position := 0
mut path := ''
for mut action in page_actions {
// println(action)
mut p := action.params
pathnew := p.get_default('path', '')!
if pathnew != '' {
mypage.path = path
if pathnew.ends_with('.md') {
// means we fully specified the name
mypage.path = pathnew
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))!
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 {
// only remember path if no .md file specified
path = pathnew
if !path.ends_with('/') {
path += '/'
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}')
}
// println(' -- NEW PATH: ${path}')
mypage.path = path
}
} else {
mypage.path = path
}
position = p.get_int_default('position', 0)!
if position == 0 {
position = position_next
position_next += 1
} else {
if position > position_next {
position_next = position + 1
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_name)!
pagepath = pagepath.trim_space().trim('/')
pagepath = texttools.name_fix(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
}
mypage.position = position
mypage.src = p.get('src') or { return error('need to specify src in site.page') }
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,298 +1,324 @@
# Site Module
## config heroscript
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.
```yaml
## 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:"ThreeFold DePIN Tech"
description:"ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet."
tagline:"Geo Aware Internet Platform"
favicon:"img/favicon.png"
image:"img/tf_graph.png"
copyright:"ThreeFold"
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: "/"
```
!!site.menu
title:"ThreeFold DePIN Tech"
logo_alt:"ThreeFold Logo"
logo_src:"img/logo.svg"
logo_src_dark:"img/new_logo_tft.png"
### Navigation Menu
!!site.menu_item
label:"ThreeFold.io"
href:"https://threefold.io"
position:"right"
```heroscript
!!site.navbar
title: "My Site"
logo_alt: "Site Logo"
logo_src: "img/logo.svg"
logo_src_dark: "img/logo-dark.svg"
!!site.menu_item
label:"Mycelium Network"
href:"https://mycelium.threefold.io/"
position:"right"
!!site.navbar_item
label: "Documentation"
to: "docs/intro"
position: "left"
!!site.menu_item
label:"AI Box"
href:"https://aibox.threefold.io/"
position:"right"
!!site.navbar_item
label: "GitHub"
href: "https://github.com/myorg/myrepo"
position: "right"
```
### Footer Configuration
```heroscript
!!site.footer
style:"dark"
style: "dark"
!!site.footer_item
title:"Docs"
label:"Introduction"
href:"https://docs.threefold.io/docs/introduction"
title: "Docs"
label: "Introduction"
to: "intro"
!!site.footer_item
title:"Docs"
label:"Litepaper"
href:"https://docs.threefold.io/docs/litepaper/"
title: "Docs"
label: "Getting Started"
href: "https://docs.example.com/getting-started"
!!site.footer_item
title:"Features"
label:"Become a Farmer"
href:"https://docs.threefold.io/docs/category/become-a-farmer"
!!site.footer_item
title:"Features"
label:"Components"
href:"https://docs.threefold.io/docs/category/components"
!!site.footer_item
title:"Web"
label:"ThreeFold.io"
href:"https://threefold.io"
!!site.footer_item
title:"Web"
label:"Dashboard"
href:"https://dashboard.grid.tf"
!!site.collections
url:"https://github.com/example/external-docs"
replace:"PROJECT_NAME:My Project, VERSION:1.0.0"
title: "Community"
label: "Discord"
href: "https://discord.gg/example"
```
## site structure
## Page Organization
```yaml
!!site.page name:intro
description:"ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet."
### Example 1: Simple Pages Without Categories
#next is example where we use all properties, folder is where the page is located, prio is the order of the page, if not used the filled in from order in which we parse this config file
!!site.page name:mycelium draft:true folder:"/specs/components" prio:4
content:"the page content itself, only for small pages"
title:"Mycelium as Title"
description:"..."
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.
!!site.page name:fungistor folder:"/specs/components" prio:1
src:"mycollection:mycelium.md"
title:"fungistor as Title"
description:"...."
```heroscript
!!site.page src: "tech:introduction"
description: "Introduction to ThreeFold Technology"
slug: "/"
!!site.page name:fungistor folder:"/specs/components" prio:1
src:"mycollection:mycelium" //can be without .md
title:"fungistor as Title"
description:"..."
!!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"
```
## factory
**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: "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
mut mysite := site.new()!
// 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)
```
## how to use with plbook
### Using with PlayBook
```v
import incubaid.herolib.core.playbook
import incubaid.herolib.web.site
// path string
// text string
// git_url string
// git_pull bool
// git_branch string
// git_reset bool
// session ?&base.Session is optional
mut plbook := playbook.new( "....")!
// 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')!
```
## example json
## Data Structures
```json
{
"name": "depin",
"title": "Documentation Site",
"description": "ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet.",
"tagline": "Geo Aware Internet Platform",
"favicon": "img/favicon.png",
"image": "img/tf_graph.png",
"copyright": "ThreeFold",
"footer": {
"style": "dark",
"links": [
{
"title": "Docs",
"items": [
{
"label": "Introduction",
"to": "intro",
"href": ""
},
{
"label": "Litepaper",
"to": "",
"href": "https://docs.threefold.io/docs/litepaper/"
},
{
"label": "Roadmap",
"to": "",
"href": "https://docs.threefold.io/docs/roadmap"
},
{
"label": "Manual",
"to": "",
"href": "https://manual.grid.tf/"
}
]
},
{
"title": "Features",
"items": [
{
"label": "Become a Farmer",
"to": "",
"href": "https://docs.threefold.io/docs/category/become-a-farmer"
},
{
"label": "Components",
"to": "",
"href": "https://docs.threefold.io/docs/category/components"
},
{
"label": "Technology",
"to": "",
"href": "https://threefold.info/tech/"
},
{
"label": "Tokenomics",
"to": "",
"href": "https://docs.threefold.io/docs/tokens/tokenomics"
}
]
},
{
"title": "Web",
"items": [
{
"label": "ThreeFold.io",
"to": "",
"href": "https://threefold.io"
},
{
"label": "Dashboard",
"to": "",
"href": "https://dashboard.grid.tf"
},
{
"label": "GitHub",
"to": "",
"href": "https://github.com/threefoldtech/home"
},
{
"label": "Mycelium Network",
"to": "",
"href": "https://mycelium.threefold.io/"
},
{
"label": "AI Box",
"to": "",
"href": "https://www2.aibox.threefold.io/"
}
]
}
]
},
"menu": {
"title": "ThreeFold DePIN Tech",
"items": [
{
"href": "https://threefold.io",
"to": "",
"label": "ThreeFold.io",
"position": "right"
},
{
"href": "https://mycelium.threefold.io/",
"to": "",
"label": "Mycelium Network",
"position": "right"
},
{
"href": "https://aibox.threefold.io/",
"to": "",
"label": "AI Box",
"position": "right"
}
]
},
"import_collections": [
{
"url": "https://github.com/example/external-docs",
"path": "",
"dest": "",
"replace": {
"PROJECT_NAME": "My Project",
"VERSION": "1.0.0"
},
"visible": false
}
],
"pages": [
{
"name": "intro",
"content": "",
"title": "",
"description": "ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet.",
"draft": false,
"folder": "",
"prio": 0,
"src": ""
},
{
"name": "mycelium",
"content": "the page content itself, only for small pages",
"title": "Mycelium as Title",
"description": "...",
"draft": true,
"folder": "/specs/components",
"prio": 4,
"src": ""
},
{
"name": "fungistor",
"content": "",
"title": "fungistor as Title",
"description": "....",
"draft": false,
"folder": "/specs/components",
"prio": 1,
"src": "mycollection:mycelium.md"
},
{
"name": "fungistor",
"content": "",
"title": "fungistor as Title",
"description": "...",
"draft": false,
"folder": "/specs/components",
"prio": 1,
"src": "mycollection:mycelium"
}
]
### 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

View File

@@ -1,91 +0,0 @@
module site
import os
fn test_play_collections() ! {
mypath := '${os.dir(@FILE)}/example'
mut sc := new(mypath)!
// Add assertions here based on the expected site config structure
assert sc.name == 'depin'
assert sc.description == 'ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet.'
assert sc.tagline == 'Geo Aware Internet Platform'
assert sc.favicon == 'img/favicon.png'
assert sc.image == 'img/tf_graph.png'
assert sc.copyright == 'ThreeFold'
// Assertions for menu
assert sc.menu.title == 'ThreeFold DePIN Tech'
assert sc.menu.items.len == 3 // Based on the three !!site.menu_item entries
assert sc.menu.items[0].label == 'ThreeFold.io'
assert sc.menu.items[0].href == 'https://threefold.io'
assert sc.menu.items[0].position == 'right'
assert sc.menu.items[1].label == 'Mycelium Network'
assert sc.menu.items[1].href == 'https://mycelium.threefold.io/'
assert sc.menu.items[1].position == 'right'
assert sc.menu.items[2].label == 'AI Box'
assert sc.menu.items[2].href == 'https://aibox.threefold.io/'
assert sc.menu.items[2].position == 'right'
// Assertions for footer
assert sc.footer.style == 'dark'
assert sc.footer.links.len == 3 // Based on the three unique titles in !!site.footer_item
assert sc.footer.links[0].title == 'Docs'
assert sc.footer.links[0].items.len == 4
assert sc.footer.links[1].title == 'Features'
assert sc.footer.links[1].items.len == 4
assert sc.footer.links[2].title == 'Web'
assert sc.footer.links[2].items.len == 5
// Assertions for collections
assert sc.import_collections.len == 1 // Based on the !!site.collections entry
assert sc.import_collections[0].url == 'https://github.com/example/external-docs'
assert sc.import_collections[0].replace.len == 2
assert sc.import_collections[0].replace['PROJECT_NAME'] == 'My Project'
assert sc.import_collections[0].replace['VERSION'] == '1.0.0'
assert sc.import_collections[0].visible == false // Default value
// Assertions for pages
assert sc.pages.len == 4 // Based on the four !!site.page entries in site.heroscript
// Assertions for the first page (intro)
assert sc.pages[0].name == 'intro'
assert sc.pages[0].description == 'ThreeFold is laying the foundation for a geo aware Web 4, the next generation of the Internet.'
assert sc.pages[0].title == '' // No title specified in site.heroscript
assert sc.pages[0].draft == false // Default value
assert sc.pages[0].folder == '' // Default value
assert sc.pages[0].prio == 0 // Default value
assert sc.pages[0].src == '' // No src specified
assert sc.pages[0].content == '' // No content specified
// Assertions for the second page (mycelium)
assert sc.pages[1].name == 'mycelium'
assert sc.pages[1].description == '...'
assert sc.pages[1].title == 'Mycelium as Title'
assert sc.pages[1].draft == true
assert sc.pages[1].folder == '/specs/components'
assert sc.pages[1].prio == 4
assert sc.pages[1].src == '' // No src specified
assert sc.pages[1].content == 'the page content itself, only for small pages'
// Assertions for the third page (fungistor with .md)
assert sc.pages[2].name == 'fungistor'
assert sc.pages[2].description == '....'
assert sc.pages[2].title == 'fungistor as Title'
assert sc.pages[2].draft == false // Default value
assert sc.pages[2].folder == '/specs/components'
assert sc.pages[2].prio == 1
assert sc.pages[2].src == 'mycollection:mycelium.md'
assert sc.pages[2].content == '' // No content specified
// Assertions for the fourth page (fungistor without .md)
assert sc.pages[3].name == 'fungistor'
assert sc.pages[3].description == '...'
assert sc.pages[3].title == 'fungistor as Title'
assert sc.pages[3].draft == false // Default value
assert sc.pages[3].folder == '/specs/components'
assert sc.pages[3].prio == 1
assert sc.pages[3].src == 'mycollection:mycelium'
assert sc.pages[3].content == '' // No content specified
}