docyard 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -1
  3. data/CHANGELOG.md +20 -1
  4. data/lib/docyard/build/asset_bundler.rb +22 -7
  5. data/lib/docyard/build/file_copier.rb +49 -27
  6. data/lib/docyard/build/sitemap_generator.rb +6 -6
  7. data/lib/docyard/build/static_generator.rb +85 -12
  8. data/lib/docyard/builder.rb +6 -6
  9. data/lib/docyard/config/branding_resolver.rb +126 -17
  10. data/lib/docyard/config/constants.rb +6 -4
  11. data/lib/docyard/config/validator.rb +122 -99
  12. data/lib/docyard/config.rb +36 -43
  13. data/lib/docyard/initializer.rb +15 -76
  14. data/lib/docyard/navigation/breadcrumb_builder.rb +133 -0
  15. data/lib/docyard/navigation/prev_next_builder.rb +4 -1
  16. data/lib/docyard/navigation/sidebar/children_discoverer.rb +51 -0
  17. data/lib/docyard/navigation/sidebar/config_parser.rb +136 -108
  18. data/lib/docyard/navigation/sidebar/file_resolver.rb +78 -0
  19. data/lib/docyard/navigation/sidebar/file_system_scanner.rb +2 -1
  20. data/lib/docyard/navigation/sidebar/item.rb +45 -7
  21. data/lib/docyard/navigation/sidebar/local_config_loader.rb +51 -0
  22. data/lib/docyard/navigation/sidebar/metadata_extractor.rb +69 -0
  23. data/lib/docyard/navigation/sidebar/metadata_reader.rb +47 -0
  24. data/lib/docyard/navigation/sidebar/path_prefixer.rb +34 -0
  25. data/lib/docyard/navigation/sidebar/renderer.rb +55 -37
  26. data/lib/docyard/navigation/sidebar/sorter.rb +21 -0
  27. data/lib/docyard/navigation/sidebar/tree_builder.rb +99 -26
  28. data/lib/docyard/navigation/sidebar/tree_filter.rb +55 -0
  29. data/lib/docyard/navigation/sidebar_builder.rb +105 -36
  30. data/lib/docyard/rendering/icon_helpers.rb +13 -0
  31. data/lib/docyard/rendering/icons/phosphor.rb +23 -1
  32. data/lib/docyard/rendering/markdown.rb +5 -0
  33. data/lib/docyard/rendering/renderer.rb +74 -34
  34. data/lib/docyard/rendering/template_resolver.rb +172 -0
  35. data/lib/docyard/routing/fallback_resolver.rb +92 -0
  36. data/lib/docyard/search/build_indexer.rb +1 -1
  37. data/lib/docyard/search/dev_indexer.rb +51 -6
  38. data/lib/docyard/search/pagefind_support.rb +2 -0
  39. data/lib/docyard/server/asset_handler.rb +24 -19
  40. data/lib/docyard/server/pagefind_handler.rb +63 -0
  41. data/lib/docyard/server/preview_server.rb +1 -1
  42. data/lib/docyard/server/rack_application.rb +81 -64
  43. data/lib/docyard/templates/assets/css/code.css +18 -51
  44. data/lib/docyard/templates/assets/css/components/breadcrumbs.css +143 -0
  45. data/lib/docyard/templates/assets/css/components/callout.css +67 -67
  46. data/lib/docyard/templates/assets/css/components/code-block.css +180 -282
  47. data/lib/docyard/templates/assets/css/components/heading-anchor.css +28 -15
  48. data/lib/docyard/templates/assets/css/components/icon.css +0 -1
  49. data/lib/docyard/templates/assets/css/components/logo.css +0 -2
  50. data/lib/docyard/templates/assets/css/components/nav-menu.css +237 -0
  51. data/lib/docyard/templates/assets/css/components/navigation.css +186 -167
  52. data/lib/docyard/templates/assets/css/components/prev-next.css +76 -47
  53. data/lib/docyard/templates/assets/css/components/search.css +186 -174
  54. data/lib/docyard/templates/assets/css/components/tab-bar.css +163 -0
  55. data/lib/docyard/templates/assets/css/components/table-of-contents.css +127 -114
  56. data/lib/docyard/templates/assets/css/components/tabs.css +119 -160
  57. data/lib/docyard/templates/assets/css/components/theme-toggle.css +48 -44
  58. data/lib/docyard/templates/assets/css/landing.css +815 -0
  59. data/lib/docyard/templates/assets/css/layout.css +489 -87
  60. data/lib/docyard/templates/assets/css/main.css +1 -3
  61. data/lib/docyard/templates/assets/css/markdown.css +111 -93
  62. data/lib/docyard/templates/assets/css/reset.css +0 -3
  63. data/lib/docyard/templates/assets/css/typography.css +43 -41
  64. data/lib/docyard/templates/assets/css/variables.css +268 -208
  65. data/lib/docyard/templates/assets/favicon.svg +7 -8
  66. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
  67. data/lib/docyard/templates/assets/js/components/code-block.js +24 -42
  68. data/lib/docyard/templates/assets/js/components/heading-anchor.js +26 -24
  69. data/lib/docyard/templates/assets/js/components/navigation.js +181 -70
  70. data/lib/docyard/templates/assets/js/components/search.js +0 -75
  71. data/lib/docyard/templates/assets/js/components/sidebar-toggle.js +29 -0
  72. data/lib/docyard/templates/assets/js/components/tab-navigation.js +145 -0
  73. data/lib/docyard/templates/assets/js/components/table-of-contents.js +153 -66
  74. data/lib/docyard/templates/assets/js/components/tabs.js +31 -69
  75. data/lib/docyard/templates/assets/js/theme.js +0 -3
  76. data/lib/docyard/templates/assets/logo-dark.svg +8 -2
  77. data/lib/docyard/templates/assets/logo.svg +7 -4
  78. data/lib/docyard/templates/config/docyard.yml.erb +37 -34
  79. data/lib/docyard/templates/errors/404.html.erb +1 -1
  80. data/lib/docyard/templates/errors/500.html.erb +1 -1
  81. data/lib/docyard/templates/layouts/default.html.erb +18 -67
  82. data/lib/docyard/templates/layouts/splash.html.erb +176 -0
  83. data/lib/docyard/templates/partials/_breadcrumbs.html.erb +24 -0
  84. data/lib/docyard/templates/partials/_code_block.html.erb +5 -3
  85. data/lib/docyard/templates/partials/_doc_footer.html.erb +25 -0
  86. data/lib/docyard/templates/partials/_features.html.erb +15 -0
  87. data/lib/docyard/templates/partials/_footer.html.erb +42 -0
  88. data/lib/docyard/templates/partials/_head.html.erb +22 -0
  89. data/lib/docyard/templates/partials/_header.html.erb +49 -0
  90. data/lib/docyard/templates/partials/_heading_anchor.html.erb +3 -1
  91. data/lib/docyard/templates/partials/_hero.html.erb +27 -0
  92. data/lib/docyard/templates/partials/_nav_group.html.erb +25 -11
  93. data/lib/docyard/templates/partials/_nav_leaf.html.erb +1 -1
  94. data/lib/docyard/templates/partials/_nav_menu.html.erb +42 -0
  95. data/lib/docyard/templates/partials/_nav_nested_section.html.erb +11 -0
  96. data/lib/docyard/templates/partials/_nav_section.html.erb +1 -1
  97. data/lib/docyard/templates/partials/_prev_next.html.erb +8 -2
  98. data/lib/docyard/templates/partials/_scripts.html.erb +7 -0
  99. data/lib/docyard/templates/partials/_search_modal.html.erb +2 -6
  100. data/lib/docyard/templates/partials/_search_trigger.html.erb +2 -6
  101. data/lib/docyard/templates/partials/_sidebar.html.erb +21 -4
  102. data/lib/docyard/templates/partials/_tab_bar.html.erb +25 -0
  103. data/lib/docyard/templates/partials/_table_of_contents.html.erb +12 -12
  104. data/lib/docyard/templates/partials/_table_of_contents_toggle.html.erb +1 -3
  105. data/lib/docyard/templates/partials/_tabs.html.erb +2 -2
  106. data/lib/docyard/templates/partials/_theme_toggle.html.erb +2 -11
  107. data/lib/docyard/version.rb +1 -1
  108. metadata +33 -5
  109. data/lib/docyard/templates/markdown/getting-started/installation.md.erb +0 -77
  110. data/lib/docyard/templates/markdown/guides/configuration.md.erb +0 -202
  111. data/lib/docyard/templates/markdown/guides/markdown-features.md.erb +0 -247
  112. data/lib/docyard/templates/markdown/index.md.erb +0 -82
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e127990af44c6db555b00cfd2c7a89be408e6e4bfddf2bed9a753387f708ee87
4
- data.tar.gz: ed54f92691fe29fc8dd83b8b797504162283bba12840c49c120a4341ac22a66c
3
+ metadata.gz: 77d317cd25d544eef47b5bdcecb0515b8937da93b347d39cac6fd1f87bb43b19
4
+ data.tar.gz: bf97d9449013ba8bf370682271990f64b2c2092bfdc9bf0deb99cf2b67056cc2
5
5
  SHA512:
6
- metadata.gz: 7ae09bfea3ce92e5b5b8cee3e7317d6d57b253e17314adcfbf9f468cc0635a54d3cbd55a90f28807514875359e4d93b4e1c68fee99b7ec65a9ab12d34a21c6c2
7
- data.tar.gz: 3c2148376ab27c804a148666795420afe24fe23cb890fa76fa2602af5434d9e60d589fcbb460454210d2dfe1cc38f1f8584db41b1209db14b8b21ec3d7abeb68
6
+ metadata.gz: 2e30597716798800240f4436c423284d1972fe5fa0abf1de8007b3279dde0a87ef32210cb830c84769c15db0c82e6c372337ae06baaecdb71fa866342c9697a3
7
+ data.tar.gz: f0ae1edf5937153c9ed3ccb1cc7c599c7f650e16470ad976ef0918c09bc030502ecf89dad6a8a7c79ee8055530b61a65d556bbfd147e08a86349a2475777ee54
data/.rubocop.yml CHANGED
@@ -22,7 +22,11 @@ Metrics/ClassLength:
22
22
  Max: 150
23
23
 
24
24
  Metrics/MethodLength:
25
- Max: 15
25
+ Max: 16
26
+
27
+ Metrics/ParameterLists:
28
+ MaxOptionalParameters: 6
29
+ CountKeywordArgs: false
26
30
 
27
31
  Style/Documentation:
28
32
  Enabled: false
data/CHANGELOG.md CHANGED
@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.8.0] - 2026-01-13
11
+
12
+ ### Added
13
+ - **Landing Pages** - Hero sections, feature grids, and custom footer layouts for documentation homepages (#45)
14
+ - **Tab Navigation** - Top-level navigation tabs for organizing documentation into sections like Guide, API, Components (#52)
15
+ - **Header CTAs** - Configurable call-to-action buttons in the header with primary/secondary variants (#51)
16
+ - **Breadcrumbs** - Path navigation with auto-truncation for deep nesting and configurable via `navigation.breadcrumbs` (#54)
17
+ - **Doc Page Footer** - Social icons, "Built with Docyard" attribution, and copyright text in TOC column (#55)
18
+ - **Auto-detect Branding** - Automatic logo and favicon detection from `docs/public/` directory (#49)
19
+ - **Social Icon Mapping** - 16 social platform icons with automatic platform-to-icon mapping (#55)
20
+
21
+ ### Changed
22
+ - **Sidebar Overhaul** - Per-section `_sidebar.yml` files, improved collapsible behavior, and better active state handling (#50, #53)
23
+ - **Config Schema** - Reorganized configuration with `branding`, `navigation`, and `socials` sections (#48)
24
+ - **Sidebar Convention** - Section-based sidebar configuration in `docs/<section>/_sidebar.yml` (#47)
25
+ - **UI Refresh** - Updated typography, spacing, and visual consistency across components (#44)
26
+ - **Logo Update** - New logo with cyan accent and dark mode support (#46)
27
+
10
28
  ## [0.7.0] - 2026-01-01
11
29
 
12
30
  ### Added
@@ -122,7 +140,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
122
140
  - Initial gem structure
123
141
  - Project scaffolding
124
142
 
125
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.7.0...HEAD
143
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.8.0...HEAD
144
+ [0.8.0]: https://github.com/sanifhimani/docyard/compare/v0.7.0...v0.8.0
126
145
  [0.7.0]: https://github.com/sanifhimani/docyard/compare/v0.6.0...v0.7.0
127
146
  [0.6.0]: https://github.com/sanifhimani/docyard/compare/v0.5.0...v0.6.0
128
147
  [0.5.0]: https://github.com/sanifhimani/docyard/compare/v0.4.0...v0.5.0
@@ -35,6 +35,8 @@ module Docyard
35
35
  main_css = File.read(File.join(ASSETS_PATH, "css", "main.css"))
36
36
  css_content = resolve_css_imports(main_css)
37
37
  minified = CSSminify.compress(css_content)
38
+ minified = fix_calc_whitespace(minified)
39
+ minified = fix_css_math_functions(minified)
38
40
  hash = generate_hash(minified)
39
41
 
40
42
  write_bundled_asset(minified, hash, "css")
@@ -43,6 +45,19 @@ module Docyard
43
45
  hash
44
46
  end
45
47
 
48
+ def fix_calc_whitespace(css)
49
+ css
50
+ .gsub(/\)\+(?!\s)/, ") + ")
51
+ .gsub(/\)-(?![-\s])/, ") - ")
52
+ .gsub(/(\d[a-z]*)\+(?=[\w(])/, '\1 + ')
53
+ .gsub(/([lch])\+(?=[\d.])/, '\1 + ')
54
+ .gsub(/([lch])-(?=[\d.])/, '\1 - ')
55
+ end
56
+
57
+ def fix_css_math_functions(css)
58
+ css.gsub(/\bmax\(0,/, "max(0px,").gsub(/\bmin\(0,/, "min(0px,)")
59
+ end
60
+
46
61
  def resolve_css_imports(css_content)
47
62
  css_content.gsub(/@import url\('([^']+)'\);/) do |match|
48
63
  import_file = Regexp.last_match(1)
@@ -92,8 +107,8 @@ module Docyard
92
107
  end
93
108
 
94
109
  def update_html_references(css_hash, js_hash)
95
- html_files = Dir.glob(File.join(config.build.output_dir, "**", "*.html"))
96
- base_url = normalize_base_url(config.build.base_url)
110
+ html_files = Dir.glob(File.join(config.build.output, "**", "*.html"))
111
+ base_url = normalize_base_url(config.build.base)
97
112
 
98
113
  html_files.each do |file|
99
114
  content = replace_asset_references(File.read(file), css_hash, js_hash, base_url)
@@ -104,15 +119,15 @@ module Docyard
104
119
  end
105
120
 
106
121
  def replace_asset_references(content, css_hash, js_hash, base_url)
107
- content.gsub(%r{/assets/css/main\.css}, "#{base_url}assets/bundle.#{css_hash}.css")
108
- .gsub(%r{/assets/js/theme\.js}, "#{base_url}assets/bundle.#{js_hash}.js")
109
- .gsub(%r{/assets/js/components\.js}, "")
110
- .gsub(%r{<script src="/assets/js/reload\.js"></script>}, "")
122
+ content.gsub(%r{/_docyard/css/main\.css}, "#{base_url}_docyard/bundle.#{css_hash}.css")
123
+ .gsub(%r{/_docyard/js/theme\.js}, "#{base_url}_docyard/bundle.#{js_hash}.js")
124
+ .gsub(%r{/_docyard/js/components\.js}, "")
125
+ .gsub(%r{<script src="/_docyard/js/reload\.js"></script>}, "")
111
126
  end
112
127
 
113
128
  def write_bundled_asset(content, hash, extension)
114
129
  filename = "bundle.#{hash}.#{extension}"
115
- output_path = File.join(config.build.output_dir, "assets", filename)
130
+ output_path = File.join(config.build.output, "_docyard", filename)
116
131
  FileUtils.mkdir_p(File.dirname(output_path))
117
132
  File.write(output_path, content)
118
133
  end
@@ -3,6 +3,8 @@
3
3
  module Docyard
4
4
  module Build
5
5
  class FileCopier
6
+ DOCYARD_OUTPUT_DIR = "_docyard"
7
+
6
8
  attr_reader :config, :verbose
7
9
 
8
10
  def initialize(config, verbose: false)
@@ -14,7 +16,7 @@ module Docyard
14
16
  puts "\nCopying static assets..."
15
17
 
16
18
  count = 0
17
- count += copy_user_assets
19
+ count += copy_public_files
18
20
  count += copy_branding_assets
19
21
 
20
22
  log "[✓] Copied #{count} static files"
@@ -23,25 +25,22 @@ module Docyard
23
25
 
24
26
  private
25
27
 
26
- def copy_user_assets
27
- user_assets_dir = "docs/assets"
28
- return 0 unless Dir.exist?(user_assets_dir)
29
-
30
- output_assets_dir = File.join(config.build.output_dir, "assets")
31
- FileUtils.mkdir_p(output_assets_dir)
28
+ def copy_public_files
29
+ public_dir = Constants::PUBLIC_DIR
30
+ return 0 unless Dir.exist?(public_dir)
32
31
 
33
- files = find_user_asset_files(user_assets_dir)
34
- files.each { |file| copy_single_asset(file, "docs/assets/", output_assets_dir) }
32
+ files = find_files_in_dir(public_dir)
33
+ files.each { |file| copy_single_file(file, "#{public_dir}/", config.build.output) }
35
34
 
36
- log "[✓] Copied #{files.size} user assets from docs/assets/" if files.any?
35
+ log "[✓] Copied #{files.size} public files from #{public_dir}/" if files.any?
37
36
  files.size
38
37
  end
39
38
 
40
- def find_user_asset_files(assets_dir)
41
- Dir.glob(File.join(assets_dir, "**", "*")).select { |f| File.file?(f) }
39
+ def find_files_in_dir(dir)
40
+ Dir.glob(File.join(dir, "**", "*")).select { |f| File.file?(f) }
42
41
  end
43
42
 
44
- def copy_single_asset(file, prefix, output_dir)
43
+ def copy_single_file(file, prefix, output_dir)
45
44
  relative_path = file.delete_prefix(prefix)
46
45
  dest_path = File.join(output_dir, relative_path)
47
46
 
@@ -60,26 +59,49 @@ module Docyard
60
59
  end
61
60
 
62
61
  def copy_default_branding_assets
63
- templates_assets = File.join(__dir__, "..", "templates", "assets")
64
- count = 0
62
+ templates_assets = templates_assets_path
63
+ count = copy_branding_files(templates_assets)
64
+ count + copy_fonts(templates_assets)
65
+ end
65
66
 
66
- ["logo.svg", "logo-dark.svg", "favicon.svg"].each do |asset_file|
67
- source_path = File.join(templates_assets, asset_file)
68
- next unless File.exist?(source_path)
67
+ def templates_assets_path
68
+ File.join(__dir__, "..", "templates", "assets")
69
+ end
69
70
 
70
- dest_path = File.join(config.build.output_dir, "assets", asset_file)
71
- FileUtils.mkdir_p(File.dirname(dest_path))
72
- FileUtils.cp(source_path, dest_path)
71
+ def copy_branding_files(templates_assets)
72
+ branding_files = %w[logo.svg logo-dark.svg favicon.svg]
73
+ branding_files.sum { |asset_file| copy_asset_to_docyard(templates_assets, asset_file, "default branding") }
74
+ end
73
75
 
74
- log " Copied default branding: #{asset_file}" if verbose
75
- count += 1
76
- end
76
+ def copy_asset_to_docyard(source_dir, filename, label)
77
+ source_path = File.join(source_dir, filename)
78
+ return 0 unless File.exist?(source_path)
77
79
 
78
- count
80
+ dest_path = File.join(config.build.output, DOCYARD_OUTPUT_DIR, filename)
81
+ FileUtils.mkdir_p(File.dirname(dest_path))
82
+ FileUtils.cp(source_path, dest_path)
83
+ log " Copied #{label}: #{filename}" if verbose
84
+ 1
85
+ end
86
+
87
+ def copy_fonts(templates_assets)
88
+ fonts_dir = File.join(templates_assets, "fonts")
89
+ return 0 unless Dir.exist?(fonts_dir)
90
+
91
+ font_files = Dir.glob(File.join(fonts_dir, "*")).select { |f| File.file?(f) }
92
+ font_files.sum { |font_file| copy_single_font(font_file) }
93
+ end
94
+
95
+ def copy_single_font(font_file)
96
+ dest_path = File.join(config.build.output, DOCYARD_OUTPUT_DIR, "fonts", File.basename(font_file))
97
+ FileUtils.mkdir_p(File.dirname(dest_path))
98
+ FileUtils.cp(font_file, dest_path)
99
+ log " Copied font: #{File.basename(font_file)}" if verbose
100
+ 1
79
101
  end
80
102
 
81
103
  def copy_user_branding_assets
82
- %w[logo logo_dark favicon].sum { |asset_key| copy_single_branding_asset(asset_key) }
104
+ %w[logo favicon].sum { |asset_key| copy_single_branding_asset(asset_key) }
83
105
  end
84
106
 
85
107
  def copy_single_branding_asset(asset_key)
@@ -89,7 +111,7 @@ module Docyard
89
111
  full_path = File.join("docs", asset_path)
90
112
  return 0 unless File.exist?(full_path)
91
113
 
92
- dest_path = File.join(config.build.output_dir, "assets", asset_path)
114
+ dest_path = File.join(config.build.output, asset_path)
93
115
  FileUtils.mkdir_p(File.dirname(dest_path))
94
116
  FileUtils.cp(full_path, dest_path)
95
117
 
@@ -15,7 +15,7 @@ module Docyard
15
15
  urls = collect_urls
16
16
  sitemap_content = build_sitemap(urls)
17
17
 
18
- output_path = File.join(config.build.output_dir, "sitemap.xml")
18
+ output_path = File.join(config.build.output, "sitemap.xml")
19
19
  File.write(output_path, sitemap_content)
20
20
 
21
21
  puts "[✓] Generated sitemap.xml (#{urls.size} URLs)"
@@ -24,10 +24,10 @@ module Docyard
24
24
  private
25
25
 
26
26
  def collect_urls
27
- html_files = Dir.glob(File.join(config.build.output_dir, "**", "index.html"))
27
+ html_files = Dir.glob(File.join(config.build.output, "**", "index.html"))
28
28
 
29
29
  html_files.map do |file|
30
- relative_path = file.delete_prefix(config.build.output_dir).delete_suffix("/index.html")
30
+ relative_path = file.delete_prefix(config.build.output).delete_suffix("/index.html")
31
31
  url_path = relative_path.empty? ? "/" : relative_path
32
32
  lastmod = File.mtime(file).utc.iso8601
33
33
 
@@ -36,15 +36,15 @@ module Docyard
36
36
  end
37
37
 
38
38
  def build_sitemap(urls)
39
- base_url = config.build.base_url
40
- base_url = base_url.chop if base_url.end_with?("/")
39
+ base = config.build.base
40
+ base = base.chop if base.end_with?("/")
41
41
 
42
42
  xml = ['<?xml version="1.0" encoding="UTF-8"?>']
43
43
  xml << '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
44
44
 
45
45
  urls.each do |url|
46
46
  xml << " <url>"
47
- xml << " <loc>#{base_url}#{url[:loc]}</loc>"
47
+ xml << " <loc>#{base}#{url[:loc]}</loc>"
48
48
  xml << " <lastmod>#{url[:lastmod]}</lastmod>"
49
49
  xml << " </url>"
50
50
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "tty-progressbar"
4
+ require_relative "../rendering/template_resolver"
5
+ require_relative "../navigation/prev_next_builder"
6
+ require_relative "../navigation/breadcrumb_builder"
4
7
 
5
8
  module Docyard
6
9
  module Build
@@ -10,10 +13,12 @@ module Docyard
10
13
  def initialize(config, verbose: false)
11
14
  @config = config
12
15
  @verbose = verbose
13
- @renderer = Renderer.new(base_url: config.build.base_url, config: config)
16
+ @renderer = Renderer.new(base_url: config.build.base, config: config)
14
17
  end
15
18
 
16
19
  def generate
20
+ copy_custom_landing_page if custom_landing_page?
21
+
17
22
  markdown_files = collect_markdown_files
18
23
  puts "\n[✓] Found #{markdown_files.size} markdown files"
19
24
 
@@ -33,24 +38,72 @@ module Docyard
33
38
 
34
39
  private
35
40
 
41
+ def custom_landing_page?
42
+ File.file?("docs/index.html")
43
+ end
44
+
45
+ def copy_custom_landing_page
46
+ output_path = File.join(config.build.output, "index.html")
47
+ FileUtils.mkdir_p(File.dirname(output_path))
48
+ FileUtils.cp("docs/index.html", output_path)
49
+ log "[✓] Copied custom landing page (index.html)"
50
+ end
51
+
36
52
  def collect_markdown_files
37
- Dir.glob(File.join("docs", "**", "*.md"))
53
+ files = Dir.glob(File.join("docs", "**", "*.md"))
54
+ files.reject! { |f| f == "docs/index.md" } if custom_landing_page?
55
+ files
38
56
  end
39
57
 
40
58
  def generate_page(markdown_file_path)
41
59
  output_path = determine_output_path(markdown_file_path)
42
60
  current_path = determine_current_path(markdown_file_path)
43
61
 
44
- sidebar_html = build_sidebar(current_path)
45
- html_content = renderer.render_file(
46
- markdown_file_path,
47
- sidebar_html: sidebar_html,
48
- branding: branding_options
49
- )
62
+ html_content = render_markdown_file(markdown_file_path, current_path)
63
+ html_content = apply_search_exclusion(html_content, current_path)
64
+ write_output(output_path, html_content)
65
+ end
66
+
67
+ def apply_search_exclusion(html_content, current_path)
68
+ return html_content unless excluded_from_search?(current_path)
69
+
70
+ html_content.gsub("data-pagefind-body", "data-pagefind-ignore")
71
+ end
72
+
73
+ def excluded_from_search?(path)
74
+ exclude_patterns = config.search.exclude || []
75
+ exclude_patterns.any? do |pattern|
76
+ next false unless pattern.start_with?("/")
77
+
78
+ File.fnmatch(pattern, path, File::FNM_PATHNAME)
79
+ end
80
+ end
81
+
82
+ def render_markdown_file(markdown_file_path, current_path)
83
+ markdown = Markdown.new(File.read(markdown_file_path))
84
+ template_resolver = TemplateResolver.new(markdown.frontmatter, config.data)
85
+ branding = branding_options
50
86
 
87
+ navigation = build_navigation_html(template_resolver, current_path, markdown, branding[:header_ctas])
88
+ renderer.render_file(markdown_file_path, **navigation, branding: branding,
89
+ template_options: template_resolver.to_options,
90
+ current_path: current_path)
91
+ end
92
+
93
+ def build_navigation_html(template_resolver, current_path, markdown, header_ctas)
94
+ return { sidebar_html: "", prev_next_html: "", breadcrumbs: nil } unless template_resolver.show_sidebar?
95
+
96
+ sidebar_builder = build_sidebar_instance(current_path, header_ctas)
97
+ {
98
+ sidebar_html: sidebar_builder.to_html,
99
+ prev_next_html: build_prev_next(sidebar_builder, current_path, markdown),
100
+ breadcrumbs: build_breadcrumbs(sidebar_builder.tree, current_path)
101
+ }
102
+ end
103
+
104
+ def write_output(output_path, html_content)
51
105
  FileUtils.mkdir_p(File.dirname(output_path))
52
106
  File.write(output_path, html_content)
53
-
54
107
  log "Generated: #{output_path}" if verbose
55
108
  end
56
109
 
@@ -59,7 +112,7 @@ module Docyard
59
112
  base_name = File.basename(relative_path, ".md")
60
113
  dir_name = File.dirname(relative_path)
61
114
 
62
- output_dir = config.build.output_dir
115
+ output_dir = config.build.output
63
116
 
64
117
  if base_name == "index"
65
118
  File.join(output_dir, dir_name, "index.html")
@@ -80,14 +133,34 @@ module Docyard
80
133
  end
81
134
  end
82
135
 
83
- def build_sidebar(current_path)
136
+ def build_sidebar_instance(current_path, header_ctas = [])
84
137
  SidebarBuilder.new(
85
138
  docs_path: "docs",
86
139
  current_path: current_path,
87
- config: config
140
+ config: config,
141
+ header_ctas: header_ctas
142
+ )
143
+ end
144
+
145
+ def build_prev_next(sidebar_builder, current_path, markdown)
146
+ PrevNextBuilder.new(
147
+ sidebar_tree: sidebar_builder.tree,
148
+ current_path: current_path,
149
+ frontmatter: markdown.frontmatter,
150
+ config: {}
88
151
  ).to_html
89
152
  end
90
153
 
154
+ def build_breadcrumbs(sidebar_tree, current_path)
155
+ return nil unless breadcrumbs_enabled?
156
+
157
+ BreadcrumbBuilder.new(sidebar_tree: sidebar_tree, current_path: current_path)
158
+ end
159
+
160
+ def breadcrumbs_enabled?
161
+ config.navigation.breadcrumbs != false
162
+ end
163
+
91
164
  def branding_options
92
165
  BrandingResolver.new(config).resolve
93
166
  end
@@ -35,7 +35,7 @@ module Docyard
35
35
  private
36
36
 
37
37
  def prepare_output_directory
38
- output_dir = config.build.output_dir
38
+ output_dir = config.build.output
39
39
 
40
40
  if clean && Dir.exist?(output_dir)
41
41
  log "[✓] Cleaning #{output_dir}/ directory"
@@ -68,7 +68,7 @@ module Docyard
68
68
  sitemap_gen = Build::SitemapGenerator.new(config)
69
69
  sitemap_gen.generate
70
70
 
71
- File.write(File.join(config.build.output_dir, "robots.txt"), robots_txt_content)
71
+ File.write(File.join(config.build.output, "robots.txt"), robots_txt_content)
72
72
  log "[+] Generated robots.txt"
73
73
  end
74
74
 
@@ -78,14 +78,14 @@ module Docyard
78
78
  end
79
79
 
80
80
  def robots_txt_content
81
- base_url = config.build.base_url
82
- base_url = "#{base_url}/" unless base_url.end_with?("/")
81
+ base = config.build.base
82
+ base = "#{base}/" unless base.end_with?("/")
83
83
 
84
84
  <<~ROBOTS
85
85
  User-agent: *
86
86
  Allow: /
87
87
 
88
- Sitemap: #{base_url}sitemap.xml
88
+ Sitemap: #{base}sitemap.xml
89
89
  ROBOTS
90
90
  end
91
91
 
@@ -94,7 +94,7 @@ module Docyard
94
94
 
95
95
  puts "\n#{'=' * 50}"
96
96
  puts "Build complete in #{format('%.2f', elapsed)}s"
97
- puts "Output: #{config.build.output_dir}/"
97
+ puts "Output: #{config.build.output}/"
98
98
 
99
99
  summary = "#{pages} pages, #{bundles} bundles, #{assets} static files"
100
100
  summary += ", #{indexed} pages indexed" if indexed.positive?
@@ -6,6 +6,15 @@ module Docyard
6
6
  @config = config
7
7
  end
8
8
 
9
+ SOCIAL_ICON_MAP = {
10
+ "x" => "x-logo", "twitter" => "x-logo", "discord" => "discord-logo",
11
+ "linkedin" => "linkedin-logo", "youtube" => "youtube-logo", "instagram" => "instagram-logo",
12
+ "facebook" => "facebook-logo", "tiktok" => "tiktok-logo", "twitch" => "twitch-logo",
13
+ "reddit" => "reddit-logo", "mastodon" => "mastodon-logo", "threads" => "threads-logo",
14
+ "pinterest" => "pinterest-logo", "medium" => "medium-logo", "slack" => "slack-logo",
15
+ "gitlab" => "gitlab-logo"
16
+ }.freeze
17
+
9
18
  def resolve
10
19
  return default_branding unless config
11
20
 
@@ -23,52 +32,152 @@ module Docyard
23
32
  logo: Constants::DEFAULT_LOGO_PATH,
24
33
  logo_dark: Constants::DEFAULT_LOGO_DARK_PATH,
25
34
  favicon: nil,
26
- display_logo: true,
27
- display_title: true
35
+ credits: true,
36
+ social: []
28
37
  }
29
38
  end
30
39
 
31
40
  def config_branding_options
32
- site_options.merge(logo_options).merge(search_options).merge(appearance_options)
41
+ site_options
42
+ .merge(logo_options)
43
+ .merge(search_options)
44
+ .merge(credits_options)
45
+ .merge(social_options)
46
+ .merge(navigation_options)
47
+ .merge(tabs_options)
33
48
  end
34
49
 
35
50
  def site_options
36
51
  {
37
- site_title: config.site.title || Constants::DEFAULT_SITE_TITLE,
38
- site_description: config.site.description || "",
39
- favicon: config.branding.favicon
52
+ site_title: config.title || Constants::DEFAULT_SITE_TITLE,
53
+ site_description: config.description || "",
54
+ favicon: config.branding.favicon || auto_detect_favicon
40
55
  }
41
56
  end
42
57
 
43
58
  def logo_options
44
59
  branding = config.branding
60
+ logo = branding.logo || auto_detect_logo
61
+ has_custom_logo = !logo.nil?
45
62
  {
46
- logo: resolve_logo(branding.logo, branding.logo_dark),
47
- logo_dark: resolve_logo_dark(branding.logo, branding.logo_dark)
63
+ logo: logo || Constants::DEFAULT_LOGO_PATH,
64
+ logo_dark: detect_dark_logo(logo) || Constants::DEFAULT_LOGO_DARK_PATH,
65
+ has_custom_logo: has_custom_logo
48
66
  }
49
67
  end
50
68
 
69
+ def auto_detect_logo
70
+ detect_public_file("logo", %w[svg png])
71
+ end
72
+
73
+ def auto_detect_favicon
74
+ detect_public_file("favicon", %w[ico svg png])
75
+ end
76
+
77
+ def detect_public_file(name, extensions)
78
+ extensions.each do |ext|
79
+ path = File.join(Constants::PUBLIC_DIR, "#{name}.#{ext}")
80
+ return "#{name}.#{ext}" if File.exist?(path)
81
+ end
82
+ nil
83
+ end
84
+
85
+ def detect_dark_logo(logo)
86
+ return nil unless logo
87
+
88
+ ext = File.extname(logo)
89
+ base = File.basename(logo, ext)
90
+ dark_filename = "#{base}-dark#{ext}"
91
+
92
+ if File.absolute_path?(logo)
93
+ dark_path = File.join(File.dirname(logo), dark_filename)
94
+ File.exist?(dark_path) ? dark_path : logo
95
+ else
96
+ dark_path = File.join("docs/public", dark_filename)
97
+ File.exist?(dark_path) ? dark_filename : logo
98
+ end
99
+ end
100
+
51
101
  def search_options
52
102
  {
53
103
  search_enabled: config.search.enabled != false,
54
- search_placeholder: config.search.placeholder || "Search documentation..."
104
+ search_placeholder: config.search.placeholder || "Search..."
55
105
  }
56
106
  end
57
107
 
58
- def appearance_options
59
- appearance = config.branding.appearance || {}
108
+ def credits_options
60
109
  {
61
- display_logo: appearance["logo"] != false,
62
- display_title: appearance["title"] != false
110
+ credits: config.branding.credits != false,
111
+ copyright: config.branding.copyright
63
112
  }
64
113
  end
65
114
 
66
- def resolve_logo(logo, logo_dark)
67
- logo || logo_dark || Constants::DEFAULT_LOGO_PATH
115
+ def social_options
116
+ socials = config.socials || {}
117
+ {
118
+ social: normalize_social_links(socials)
119
+ }
68
120
  end
69
121
 
70
- def resolve_logo_dark(logo, logo_dark)
71
- logo_dark || logo || Constants::DEFAULT_LOGO_DARK_PATH
122
+ def normalize_social_links(socials)
123
+ return [] unless socials.is_a?(Hash) && socials.any?
124
+
125
+ socials.filter_map { |platform, url| build_social_link(platform.to_s, url) }
126
+ end
127
+
128
+ def build_social_link(platform, url)
129
+ return if platform == "custom" || !valid_url?(url)
130
+
131
+ { platform: platform, url: url, icon: SOCIAL_ICON_MAP[platform] || platform }
132
+ end
133
+
134
+ def valid_url?(url)
135
+ url.is_a?(String) && !url.strip.empty?
136
+ end
137
+
138
+ def navigation_options
139
+ cta_items = config.navigation.cta || []
140
+ {
141
+ header_ctas: normalize_cta_items(cta_items)
142
+ }
143
+ end
144
+
145
+ def normalize_cta_items(items)
146
+ return [] unless items.is_a?(Array)
147
+
148
+ items.first(2).filter_map do |item|
149
+ next unless item.is_a?(Hash) && item["text"] && item["href"]
150
+
151
+ {
152
+ text: item["text"],
153
+ href: item["href"],
154
+ variant: item["variant"] || "primary",
155
+ external: item["external"] == true
156
+ }
157
+ end
158
+ end
159
+
160
+ def tabs_options
161
+ tab_items = config.tabs || []
162
+ {
163
+ tabs: normalize_tab_items(tab_items),
164
+ has_tabs: tab_items.any?
165
+ }
166
+ end
167
+
168
+ def normalize_tab_items(items)
169
+ return [] unless items.is_a?(Array)
170
+
171
+ items.filter_map do |item|
172
+ next unless item.is_a?(Hash) && item["text"] && item["href"]
173
+
174
+ {
175
+ text: item["text"],
176
+ href: item["href"],
177
+ icon: item["icon"],
178
+ external: item["external"] == true
179
+ }
180
+ end
72
181
  end
73
182
  end
74
183
  end