diff --git a/lib/web/site/model_nav.v b/lib/web/site/model_nav.v index 066080b7..28c23a05 100644 --- a/lib/web/site/model_nav.v +++ b/lib/web/site/model_nav.v @@ -5,7 +5,7 @@ import json // Top-level config pub struct NavConfig { pub mut: - mySidebar []NavItem + my_sidebar []NavItem // myTopbar []NavItem //not used yet // myFooter []NavItem //not used yet } @@ -16,8 +16,8 @@ pub type NavItem = NavDoc | NavCat | NavLink // --------- DOC ITEM ---------- pub struct NavDoc { pub: - id string //is the page id - label string + id string // is the page id + label string } // --------- CATEGORY ---------- @@ -32,9 +32,9 @@ pub mut: // --------- LINK ---------- pub struct NavLink { pub: - label string - href string - description string + label string + href string + description string } // -------- JSON SERIALIZATION -------- @@ -42,17 +42,17 @@ pub: // NavItemJson is used for JSON export with type discrimination pub struct NavItemJson { pub mut: - type_field string @[json: 'type'] + type_field string @[json: 'type'] // For doc - id string @[omitempty] - label string @[omitempty] + id string @[omitempty] + label string @[omitempty] // For link - href string @[omitempty] - description string @[omitempty] + href string @[omitempty] + description string @[omitempty] // For category collapsible bool collapsed bool - items []NavItemJson @[omitempty] + items []NavItemJson @[omitempty] } // Convert a single NavItem to JSON-serializable format @@ -60,9 +60,9 @@ fn nav_item_to_json(item NavItem) !NavItemJson { return match item { NavDoc { NavItemJson{ - type_field: 'doc' - id: item.id - label: item.label + type_field: 'doc' + id: item.id + label: item.label collapsible: false collapsed: false } @@ -93,17 +93,15 @@ fn nav_item_to_json(item NavItem) !NavItemJson { } } -// Convert entire NavConfig sidebar to JSON-serializable array -fn (nc NavConfig) sidebar_to_json() ![]NavItemJson { +// Convert entire NavConfig sidebar to JSON string +fn (nc NavConfig) sidebar_to_json() !string { mut result := []NavItemJson{} - for item in nc.mySidebar { + for item in nc.my_sidebar { result << nav_item_to_json(item)! } - return result + return json.encode_pretty(result) } - - // // Convert entire NavConfig topbar to JSON-serializable array // fn (nc NavConfig) topbar_to_json() ![]NavItemJson { // mut result := []NavItemJson{} @@ -122,7 +120,7 @@ fn (nc NavConfig) sidebar_to_json() ![]NavItemJson { // return result // } -port topbar as formatted JSON string +// port topbar as formatted JSON string // pub fn (nc NavConfig) jsondump_topbar() !string { // items := nc.topbar_to_json()! // return json.encode_pretty(items) diff --git a/lib/web/site/model_page.v b/lib/web/site/model_page.v index d82729ab..c13a67bf 100644 --- a/lib/web/site/model_page.v +++ b/lib/web/site/model_page.v @@ -1,11 +1,12 @@ module site +// Page represents a single documentation page pub struct Page { pub mut: - id string //unique identifier, by default as "collection:page_name", we can overrule this from play instructions if needed - title string - description string - draft bool - hide_title bool - src string @[required] // always in format collection:page_name, can use the default collection if no : specified + id string // Unique identifier: "collection:page_name" + title string // Display title (optional, extracted from markdown if empty) + description string // Brief description for metadata + draft bool // Mark as draft (hidden from navigation) + hide_title bool // Hide the title when rendering + src string // Source reference (same as id in this format) } diff --git a/lib/web/site/model_site.v b/lib/web/site/model_site.v index 81665a06..4e4fd723 100644 --- a/lib/web/site/model_site.v +++ b/lib/web/site/model_site.v @@ -3,7 +3,7 @@ module site @[heap] pub struct Site { pub mut: - pages map[string]Page // key: is the id of the page - nav NavConfig //navigation of the site - siteconfig SiteConfig + pages map[string]Page // key: "collection:page_name" + nav NavConfig // Navigation sidebar configuration + siteconfig SiteConfig // Full site configuration } diff --git a/lib/web/site/play.v b/lib/web/site/play.v index 907185ad..96e845d5 100644 --- a/lib/web/site/play.v +++ b/lib/web/site/play.v @@ -4,222 +4,94 @@ import os import incubaid.herolib.core.playbook { PlayBook } import incubaid.herolib.core.texttools import time +import incubaid.herolib.ui.console +// Main entry point for processing site HeroScript pub fn play(mut plbook PlayBook) ! { if !plbook.exists(filter: 'site.') { return } + console.print_header('Processing Site Configuration') + + // ============================================================ + // STEP 1: Initialize core site configuration + // ============================================================ + console.print_item('Step 1: Loading site configuration') mut config_action := plbook.ensure_once(filter: 'site.config')! - mut p := config_action.params - name := p.get_default('name', 'default')! // Use 'default' as fallback name - // configure the website + name := p.get_default('name', 'default')! mut website := new(name: name)! mut config := &website.siteconfig + // Load core configuration 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.copyright = p.get_default('copyright', '© ${time.now().year} 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 + config_action.done = true - // 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')! + // ============================================================ + // STEP 2: Apply optional metadata overrides + // ============================================================ + console.print_item('Step 2: Applying metadata overrides') + if plbook.exists_once(filter: 'site.config_meta') { + mut meta_action := plbook.get(filter: 'site.config_meta')! + mut p_meta := meta_action.params + + config.meta_title = p_meta.get_default('title', config.title)! + config.meta_image = p_meta.get_default('image', config.image)! + if p_meta.exists('description') { + config.description = p_meta.get('description')! + } + + meta_action.done = true } - config_action.done = true // Mark the action as done - meta_action.done = true + // ============================================================ + // STEP 3: Configure content imports + // ============================================================ + console.print_item('Step 3: Configuring content imports') + play_imports(mut plbook, mut config)! - play_import(mut plbook, mut config)! - play_menu(mut plbook, mut config)! + // ============================================================ + // STEP 4: Configure navigation menu + // ============================================================ + console.print_item('Step 4: Configuring navigation menu') + play_navbar(mut plbook, mut config)! + + // ============================================================ + // STEP 5: Configure footer + // ============================================================ + console.print_item('Step 5: Configuring footer') play_footer(mut plbook, mut config)! + + // ============================================================ + // STEP 6: Configure announcement bar (optional) + // ============================================================ + console.print_item('Step 6: Configuring announcement bar (if present)') play_announcement(mut plbook, mut config)! - play_publish(mut plbook, mut config)! - play_publish_dev(mut plbook, mut config)! + + // ============================================================ + // STEP 7: Configure publish destinations + // ============================================================ + console.print_item('Step 7: Configuring publish destinations') + play_publishing(mut plbook, mut config)! + + // ============================================================ + // STEP 8: Build pages and navigation structure + // ============================================================ + console.print_item('Step 8: Processing pages and building navigation') play_pages(mut plbook, mut website)! -} - -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 - } + + console.print_green('Site configuration complete') + } diff --git a/lib/web/site/play_announcement.v b/lib/web/site/play_announcement.v new file mode 100644 index 00000000..f516e7e1 --- /dev/null +++ b/lib/web/site/play_announcement.v @@ -0,0 +1,34 @@ +module site + +import os +import incubaid.herolib.core.playbook { PlayBook } +import incubaid.herolib.core.texttools +import time +import incubaid.herolib.ui.console + +// ============================================================ +// ANNOUNCEMENT: Process announcement bar (optional) +// ============================================================ +fn play_announcement(mut plbook PlayBook, mut config SiteConfig) ! { + mut announcement_actions := plbook.find(filter: 'site.announcement')! + + if announcement_actions.len > 0 { + // Only process the first announcement action + mut action := announcement_actions[0] + mut p := action.params + + content := p.get('content') or { + return error('!!site.announcement: must specify "content"') + } + + config.announcement = AnnouncementBar{ + id: p.get_default('id', 'announcement')! + content: content + background_color: p.get_default('background_color', '#20232a')! + text_color: p.get_default('text_color', '#fff')! + is_closeable: p.get_default_true('is_closeable') + } + + action.done = true + } +} diff --git a/lib/web/site/play_footer.v b/lib/web/site/play_footer.v new file mode 100644 index 00000000..0601eb83 --- /dev/null +++ b/lib/web/site/play_footer.v @@ -0,0 +1,62 @@ +module site + +import os +import incubaid.herolib.core.playbook { PlayBook } +import incubaid.herolib.core.texttools +import time +import incubaid.herolib.ui.console + +// ============================================================ +// FOOTER: Process footer configuration +// ============================================================ +fn play_footer(mut plbook PlayBook, mut config SiteConfig) ! { + // Process footer style (optional) + mut footer_actions := plbook.find(filter: 'site.footer')! + for mut action in footer_actions { + mut p := action.params + config.footer.style = p.get_default('style', 'dark')! + action.done = true + } + + // Process footer items (multiple) + mut footer_item_actions := plbook.find(filter: 'site.footer_item')! + mut links_map := map[string][]FooterItem{} + + // Clear existing links to prevent duplication + config.footer.links = []FooterLink{} + + for mut action in footer_item_actions { + mut p := action.params + + title := p.get_default('title', 'Docs')! + + label := p.get('label') or { + return error('!!site.footer_item: must specify "label"') + } + + mut item := FooterItem{ + label: label + href: p.get_default('href', '')! + to: p.get_default('to', '')! + } + + // Validate that href or to is specified + if item.href.len == 0 && item.to.len == 0 { + return error('!!site.footer_item for "${label}": must specify either "href" or "to"') + } + + if title !in links_map { + links_map[title] = []FooterItem{} + } + links_map[title] << item + action.done = true + } + + // Convert map to footer links array + for title, items in links_map { + config.footer.links << FooterLink{ + title: title + items: items + } + } +} diff --git a/lib/web/site/play_imports.v b/lib/web/site/play_imports.v new file mode 100644 index 00000000..05039c60 --- /dev/null +++ b/lib/web/site/play_imports.v @@ -0,0 +1,51 @@ +module site + +import os +import incubaid.herolib.core.playbook { PlayBook } +import incubaid.herolib.core.texttools +import time +import incubaid.herolib.ui.console + +// ============================================================ +// IMPORTS: Process content imports +// ============================================================ +fn play_imports(mut plbook PlayBook, mut config SiteConfig) ! { + mut import_actions := plbook.find(filter: 'site.import')! + + for mut action in import_actions { + mut p := action.params + + // Parse replacement patterns (comma-separated key:value pairs) + mut replace_map := map[string]string{} + if replace_str := p.get_default('replace', '') { + parts := replace_str.split(',') + for part in parts { + kv := part.split(':') + if kv.len == 2 { + replace_map[kv[0].trim_space()] = kv[1].trim_space() + } + } + } + + // Get path (can be relative to playbook path) + mut import_path := p.get_default('path', '')! + if import_path != '' { + if !import_path.starts_with('/') { + import_path = os.abs_path('${plbook.path}/${import_path}') + } + } + + // Create import item + mut import_item := ImportItem{ + name: p.get_default('name', '')! + url: p.get_default('url', '')! + path: import_path + dest: p.get_default('dest', '')! + replace: replace_map + visible: p.get_default_false('visible') + } + + config.imports << import_item + action.done = true + } +} diff --git a/lib/web/site/play_navbar.v b/lib/web/site/play_navbar.v new file mode 100644 index 00000000..52f2a5b8 --- /dev/null +++ b/lib/web/site/play_navbar.v @@ -0,0 +1,76 @@ +module site + +import os +import incubaid.herolib.core.playbook { PlayBook } +import incubaid.herolib.core.texttools +import time +import incubaid.herolib.ui.console + + + +// ============================================================ +// Internal structure for tracking category information +// ============================================================ +struct CategoryInfo { +pub mut: + name string + label string + position int + nav_items []NavItem +} + + +// ============================================================ +// NAVBAR: Process navigation menu +// ============================================================ +fn play_navbar(mut plbook PlayBook, mut config SiteConfig) ! { + // Try 'site.navbar' first, then fallback to deprecated 'site.menu' + mut navbar_actions := plbook.find(filter: 'site.navbar')! + if navbar_actions.len == 0 { + navbar_actions = plbook.find(filter: 'site.menu')! + } + + // Configure navbar metadata + if navbar_actions.len > 0 { + for mut action in navbar_actions { + mut p := action.params + config.menu.title = p.get_default('title', config.title)! + config.menu.logo_alt = p.get_default('logo_alt', '')! + config.menu.logo_src = p.get_default('logo_src', '')! + config.menu.logo_src_dark = p.get_default('logo_src_dark', '')! + action.done = true + } + } + + // Process navbar items + mut navbar_item_actions := plbook.find(filter: 'site.navbar_item')! + if navbar_item_actions.len == 0 { + navbar_item_actions = plbook.find(filter: 'site.menu_item')! + } + + // Clear existing items to prevent duplication + config.menu.items = []MenuItem{} + + for mut action in navbar_item_actions { + mut p := action.params + + label := p.get('label') or { + return error('!!site.navbar_item: must specify "label"') + } + + mut item := MenuItem{ + label: label + href: p.get_default('href', '')! + to: p.get_default('to', '')! + position: p.get_default('position', 'right')! + } + + // Validate that at least href or to is specified + if item.href.len == 0 && item.to.len == 0 { + return error('!!site.navbar_item: must specify either "href" or "to" for label "${label}"') + } + + config.menu.items << item + action.done = true + } +} diff --git a/lib/web/site/play_page.v b/lib/web/site/play_page.v deleted file mode 100644 index 333293df..00000000 --- a/lib/web/site/play_page.v +++ /dev/null @@ -1,135 +0,0 @@ -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; - } -} diff --git a/lib/web/site/play_pages.v b/lib/web/site/play_pages.v new file mode 100644 index 00000000..ca504e3e --- /dev/null +++ b/lib/web/site/play_pages.v @@ -0,0 +1,142 @@ +module site + +import os +import incubaid.herolib.core.playbook { PlayBook } +import incubaid.herolib.core.texttools +import time +import incubaid.herolib.ui.console + +// ============================================================ +// PAGES: Process pages and build navigation structure +// ============================================================ +fn play_pages(mut plbook PlayBook, mut website Site) ! { + mut collection_current := '' // Track current collection for reuse + mut categories := map[string]CategoryInfo{} // Map of category name -> info + mut category_current := '' // Track current active category + mut root_nav_items := []NavItem{} // Root-level items (pages without category) + mut next_category_position := 100 // Auto-increment position for categories + + // ============================================================ + // PASS 1: Process all page and category actions + // ============================================================ + mut all_actions := plbook.find(filter: 'site.')! + + for mut action in all_actions { + if action.done { + continue + } + + // ========== PAGE CATEGORY ========== + if action.name == 'page_category' { + mut p := action.params + + category_name := p.get('name') or { + return error('!!site.page_category: must specify "name"') + } + + category_name = texttools.name_fix(category_name) + + // Get label (derive from name if not specified) + mut label := p.get_default('label', texttools.name_fix_snake_to_pascal(category_name))! + mut position := p.get_int_default('position', next_category_position)! + + // Auto-increment position if using default + if position == next_category_position { + next_category_position += 100 + } + + // Create and store category info + categories[category_name] = CategoryInfo{ + name: category_name + label: label + position: position + nav_items: []NavItem{} + } + + category_current = category_name + action.done = true + continue + } + + // ========== PAGE ========== + if action.name == 'page' { + mut p := action.params + + mut page_src := p.get_default('src', '')! + mut page_collection := '' + mut page_name := '' + + // Parse collection:page format from src + if page_src.contains(':') { + parts := page_src.split(':') + page_collection = texttools.name_fix(parts[0]) + page_name = texttools.name_fix_keepext(parts[1]) + } else { + // Use previously specified collection if available + if collection_current.len > 0 { + page_collection = collection_current + page_name = texttools.name_fix_keepext(page_src) + } else { + return error('!!site.page: must specify source as "collection:page_name" in "src".\nGot src="${page_src}" with no collection previously set.\nEither specify "collection:page_name" or define a collection first.') + } + } + + // Clean up page name (remove .md if present) + if page_name.ends_with('.md') { + page_name = page_name[0..page_name.len - 3] + } + page_name = texttools.name_fix(page_name) + + // Validation + if page_name.len == 0 { + return error('!!site.page: could not extract valid page name from src="${page_src}"') + } + if page_collection.len == 0 { + return error('!!site.page: could not determine collection') + } + + // Store collection for subsequent pages + collection_current = page_collection + + // Build page ID + page_id := '${page_collection}:${page_name}' + + // Get optional page metadata + page_title := p.get_default('title', '')! + page_description := p.get_default('description', '')! + page_draft := p.get_default_false('draft') + page_hide_title := p.get_default_false('hide_title') + + // Create page + mut page := Page{ + id: page_id + title: page_title + description: page_description + draft: page_draft + hide_title: page_hide_title + src: page_id + } + + website.pages[page_id] = page + + // Create navigation item + nav_doc := NavDoc{ + id: page_id + label: if page_title.len > 0 { page_title } else { page_name } + } + + // Add to appropriate category or root + if category_current.len > 0 { + if category_current in categories { + mut cat_info := categories[category_current] + cat_info.nav_items << nav_doc + categories[category_current] = cat_info + } + } else { + root_nav_items << nav_doc + } + + action.done = true + continue + } + } diff --git a/lib/web/site/play_publish.v b/lib/web/site/play_publish.v new file mode 100644 index 00000000..e1309d1a --- /dev/null +++ b/lib/web/site/play_publish.v @@ -0,0 +1,46 @@ +module site + +import os +import incubaid.herolib.core.playbook { PlayBook } +import incubaid.herolib.core.texttools +import time +import incubaid.herolib.ui.console + +// ============================================================ +// PUBLISHING: Configure build and publish destinations +// ============================================================ +fn play_publishing(mut plbook PlayBook, mut config SiteConfig) ! { + // Production publish destinations + mut build_dest_actions := plbook.find(filter: 'site.publish')! + for mut action in build_dest_actions { + mut p := action.params + + path := p.get('path') or { + return error('!!site.publish: must specify "path"') + } + + mut dest := BuildDest{ + path: path + ssh_name: p.get_default('ssh_name', '')! + } + config.build_dest << dest + action.done = true + } + + // Development publish destinations + mut build_dest_dev_actions := plbook.find(filter: 'site.publish_dev')! + for mut action in build_dest_dev_actions { + mut p := action.params + + path := p.get('path') or { + return error('!!site.publish_dev: must specify "path"') + } + + mut dest := BuildDest{ + path: path + ssh_name: p.get_default('ssh_name', '')! + } + config.build_dest_dev << dest + action.done = true + } +} diff --git a/lib/web/site/readme.md b/lib/web/site/readme.md index 40670c8a..4f055790 100644 --- a/lib/web/site/readme.md +++ b/lib/web/site/readme.md @@ -225,6 +225,75 @@ docs/ path: "/tmp/docs-preview" ``` +## HeroScript Processing Order + +Atlas processes HeroScript actions in a fixed order. Each step depends on previous steps: + +1. **Site Configuration** (`!!site.config`) - Required + - Sets basic site metadata and URLs + +2. **Metadata Overrides** (`!!site.config_meta`) - Optional + - Overrides specific SEO metadata + +3. **Content Imports** (`!!site.import`) - Optional + - Defines external content imports + +4. **Navigation Menu** (`!!site.navbar` + `!!site.navbar_item`) - Recommended + - Configures top navigation bar + +5. **Footer** (`!!site.footer` + `!!site.footer_item`) - Recommended + - Configures footer links + +6. **Announcement Bar** (`!!site.announcement`) - Optional + - Configures optional announcement banner + +7. **Publishing** (`!!site.publish` + `!!site.publish_dev`) - Optional + - Defines deployment destinations + +8. **Pages** (`!!site.page_category` + `!!site.page`) - Recommended + - Defines content pages and navigation structure + +### Error Handling + +The play function validates parameters and provides helpful error messages: + +```/dev/null/error_example.txt#L1-4 +!!site.page: must specify source as "collection:page_name" in "src" +Got src="invalid_page" with no collection previously set +Either specify "collection:page_name" or define a collection first +``` + +### Best Practices for HeroScript + +```heroscript/example.heroscript#L1-20 +# 1. Always start with config +!!site.config + name: "my_docs" + title: "My Documentation" + +# 2. Set up navigation +!!site.navbar title: "MyDocs" + +# 3. Define pages with reusable collection +!!site.page_category name: "intro" + +!!site.page src: "guides:introduction" + title: "Getting Started" + +!!site.page src: "setup" # Reuses "guides" collection + title: "Installation" + +!!site.page src: "tutorial" # Still uses "guides" + title: "Tutorial" + +# 4. Change collection when needed +!!site.page src: "api:reference" + title: "API Reference" + +!!site.page src: "endpoints" # Now uses "api" collection + title: "Endpoints" +``` + ## Factory Methods ### Create or Get a Site