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
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Routing
5
+ class FallbackResolver
6
+ attr_reader :docs_path, :sidebar_builder
7
+
8
+ def initialize(docs_path:, sidebar_builder:)
9
+ @docs_path = docs_path
10
+ @sidebar_builder = sidebar_builder
11
+ end
12
+
13
+ def resolve_fallback(request_path)
14
+ return nil if file_exists?(request_path)
15
+
16
+ find_first_item_in_section(request_path)
17
+ end
18
+
19
+ private
20
+
21
+ def file_exists?(request_path)
22
+ clean_path = sanitize_path(request_path)
23
+
24
+ file_path = File.join(docs_path, "#{clean_path}.md")
25
+ return true if File.file?(file_path)
26
+
27
+ index_path = File.join(docs_path, clean_path, "index.md")
28
+ File.file?(index_path)
29
+ end
30
+
31
+ def sanitize_path(request_path)
32
+ clean = request_path.to_s.delete_prefix("/").delete_suffix("/")
33
+ clean = "index" if clean.empty?
34
+ clean.delete_suffix(".md")
35
+ end
36
+
37
+ def find_first_item_in_section(request_path)
38
+ tree = sidebar_builder.tree
39
+
40
+ if root_path?(request_path)
41
+ find_first_navigable_item(tree)
42
+ else
43
+ section = find_section_in_tree(tree, request_path)
44
+ section ? find_first_navigable_item(section[:children] || []) : nil
45
+ end
46
+ end
47
+
48
+ def root_path?(request_path)
49
+ request_path.nil? || request_path == "/" || request_path.empty?
50
+ end
51
+
52
+ def find_section_in_tree(tree, path)
53
+ normalized_path = normalize_path(path)
54
+
55
+ tree.each do |item|
56
+ return item if path_matches_section?(item, normalized_path)
57
+
58
+ if item[:children]&.any?
59
+ found = find_section_in_tree(item[:children], path)
60
+ return found if found
61
+ end
62
+ end
63
+
64
+ nil
65
+ end
66
+
67
+ def normalize_path(path)
68
+ path.to_s.delete_prefix("/").delete_suffix("/").downcase
69
+ end
70
+
71
+ def path_matches_section?(item, normalized_path)
72
+ return false unless item[:type] == :directory
73
+
74
+ item_path = item[:title].to_s.downcase.gsub(/\s+/, "-")
75
+ item_path == normalized_path
76
+ end
77
+
78
+ def find_first_navigable_item(items)
79
+ items.each do |item|
80
+ return item[:path] if item[:path] && item[:type] == :file
81
+
82
+ if item[:children]&.any?
83
+ found = find_first_navigable_item(item[:children])
84
+ return found if found
85
+ end
86
+ end
87
+
88
+ nil
89
+ end
90
+ end
91
+ end
92
+ end
@@ -13,7 +13,7 @@ module Docyard
13
13
 
14
14
  def initialize(config, verbose: false)
15
15
  @config = config
16
- @output_dir = config.build.output_dir
16
+ @output_dir = config.build.output
17
17
  @verbose = verbose
18
18
  end
19
19
 
@@ -25,10 +25,10 @@ module Docyard
25
25
 
26
26
  @temp_dir = Dir.mktmpdir("docyard-search-")
27
27
  generate_html_files
28
- run_pagefind
28
+ page_count = run_pagefind
29
29
  @pagefind_path = File.join(temp_dir, "pagefind")
30
30
 
31
- log_success
31
+ log_success(page_count)
32
32
  pagefind_path
33
33
  rescue StandardError => e
34
34
  warn "[!] Search index generation failed: #{e.message}"
@@ -52,6 +52,8 @@ module Docyard
52
52
 
53
53
  def generate_html_files
54
54
  markdown_files = Dir.glob(File.join(docs_path, "**", "*.md"))
55
+ markdown_files = filter_excluded_files(markdown_files)
56
+ markdown_files = filter_non_indexable_files(markdown_files)
55
57
  renderer = Renderer.new(base_url: "/", config: config)
56
58
 
57
59
  progress = TTY::ProgressBar.new(
@@ -66,6 +68,45 @@ module Docyard
66
68
  end
67
69
  end
68
70
 
71
+ def filter_excluded_files(files)
72
+ exclude_patterns = config.search.exclude || []
73
+ return files if exclude_patterns.empty?
74
+
75
+ files.reject do |file_path|
76
+ url_path = file_to_url_path(file_path)
77
+ exclude_patterns.any? { |pattern| File.fnmatch(pattern, url_path, File::FNM_PATHNAME) }
78
+ end
79
+ end
80
+
81
+ def filter_non_indexable_files(files)
82
+ files.reject do |file_path|
83
+ content = File.read(file_path)
84
+ markdown = Markdown.new(content)
85
+ frontmatter = markdown.frontmatter
86
+
87
+ uses_splash_template?(frontmatter)
88
+ end
89
+ end
90
+
91
+ def uses_splash_template?(frontmatter)
92
+ return true if frontmatter["template"] == "splash"
93
+ return true if frontmatter.key?("landing")
94
+
95
+ frontmatter.key?("hero") || frontmatter.key?("features")
96
+ end
97
+
98
+ def file_to_url_path(file_path)
99
+ relative_path = file_path.delete_prefix("#{docs_path}/")
100
+ base_name = File.basename(relative_path, ".md")
101
+ dir_name = File.dirname(relative_path)
102
+
103
+ if base_name == "index"
104
+ dir_name == "." ? "/" : "/#{dir_name}"
105
+ else
106
+ dir_name == "." ? "/#{base_name}" : "/#{dir_name}/#{base_name}"
107
+ end
108
+ end
109
+
69
110
  def generate_html_file(markdown_file, renderer)
70
111
  relative_path = markdown_file.delete_prefix("#{docs_path}/")
71
112
  output_path = determine_output_path(relative_path)
@@ -97,12 +138,16 @@ module Docyard
97
138
 
98
139
  raise "Pagefind failed: #{stderr}" unless status.success?
99
140
 
100
- stdout
141
+ extract_page_count(stdout)
142
+ end
143
+
144
+ def extract_page_count(output)
145
+ match = output.match(/Indexed (\d+) page/i)
146
+ match ? match[1].to_i : 0
101
147
  end
102
148
 
103
- def log_success
104
- page_count = Dir.glob(File.join(temp_dir, "**", "*.html")).size
105
- puts "=> Search index generated (#{page_count} pages)"
149
+ def log_success(page_count)
150
+ puts "=> Search index generated (#{page_count} pages indexed)"
106
151
  puts "=> Temp directory: #{temp_dir}" if ENV["DOCYARD_DEBUG"]
107
152
  end
108
153
  end
@@ -21,6 +21,8 @@ module Docyard
21
21
 
22
22
  exclusions = config.search.exclude || []
23
23
  exclusions.each do |pattern|
24
+ next if pattern.start_with?("/")
25
+
24
26
  args += ["--exclude-selectors", pattern]
25
27
  end
26
28
 
@@ -2,8 +2,7 @@
2
2
 
3
3
  module Docyard
4
4
  class AssetHandler
5
- ASSETS_PATH = File.join(__dir__, "../templates", "assets")
6
- USER_ASSETS_PATH = "docs/assets"
5
+ TEMPLATES_ASSETS_PATH = File.join(__dir__, "../templates", "assets")
7
6
 
8
7
  CONTENT_TYPES = {
9
8
  ".css" => "text/css; charset=utf-8",
@@ -11,41 +10,47 @@ module Docyard
11
10
  ".png" => "image/png",
12
11
  ".jpg" => "image/jpeg",
13
12
  ".jpeg" => "image/jpeg",
13
+ ".gif" => "image/gif",
14
+ ".webp" => "image/webp",
14
15
  ".svg" => "image/svg+xml",
15
- ".woff" => "font/woff2",
16
+ ".woff" => "font/woff",
16
17
  ".woff2" => "font/woff2",
17
- ".ico" => "image/x-icon"
18
+ ".ttf" => "font/ttf",
19
+ ".ico" => "image/x-icon",
20
+ ".pdf" => "application/pdf",
21
+ ".mp4" => "video/mp4",
22
+ ".webm" => "video/webm"
18
23
  }.freeze
19
24
 
20
- def serve(request_path)
21
- asset_path = extract_asset_path(request_path)
25
+ def serve_docyard_assets(request_path)
26
+ asset_path = request_path.delete_prefix("/_docyard/")
22
27
 
23
28
  return forbidden_response if directory_traversal?(asset_path)
24
29
 
25
30
  return serve_components_css if asset_path == "css/components.css"
26
31
  return serve_components_js if asset_path == "js/components.js"
27
32
 
28
- file_path = build_file_path(asset_path)
33
+ file_path = File.join(TEMPLATES_ASSETS_PATH, asset_path)
29
34
  return not_found_response unless File.file?(file_path)
30
35
 
31
36
  serve_file(file_path)
32
37
  end
33
38
 
34
- private
39
+ def serve_public_file(request_path)
40
+ asset_path = request_path.delete_prefix("/")
35
41
 
36
- def extract_asset_path(request_path)
37
- request_path.delete_prefix("/assets/")
38
- end
42
+ return nil if directory_traversal?(asset_path)
39
43
 
40
- def directory_traversal?(path)
41
- path.include?("..")
44
+ file_path = File.join(Constants::PUBLIC_DIR, asset_path)
45
+ return nil unless File.file?(file_path)
46
+
47
+ serve_file(file_path)
42
48
  end
43
49
 
44
- def build_file_path(asset_path)
45
- user_path = File.join(USER_ASSETS_PATH, asset_path)
46
- return user_path if File.file?(user_path)
50
+ private
47
51
 
48
- File.join(ASSETS_PATH, asset_path)
52
+ def directory_traversal?(path)
53
+ path.include?("..")
49
54
  end
50
55
 
51
56
  def serve_file(file_path)
@@ -61,7 +66,7 @@ module Docyard
61
66
  end
62
67
 
63
68
  def concatenate_component_css
64
- components_dir = File.join(ASSETS_PATH, "css", "components")
69
+ components_dir = File.join(TEMPLATES_ASSETS_PATH, "css", "components")
65
70
  return "" unless Dir.exist?(components_dir)
66
71
 
67
72
  css_files = Dir.glob(File.join(components_dir, "*.css"))
@@ -74,7 +79,7 @@ module Docyard
74
79
  end
75
80
 
76
81
  def concatenate_component_js
77
- components_dir = File.join(ASSETS_PATH, "js", "components")
82
+ components_dir = File.join(TEMPLATES_ASSETS_PATH, "js", "components")
78
83
  return "" unless Dir.exist?(components_dir)
79
84
 
80
85
  js_files = Dir.glob(File.join(components_dir, "*.js"))
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class PagefindHandler
5
+ CONTENT_TYPES = {
6
+ ".js" => "application/javascript; charset=utf-8",
7
+ ".css" => "text/css; charset=utf-8",
8
+ ".json" => "application/json; charset=utf-8"
9
+ }.freeze
10
+
11
+ def initialize(pagefind_path:, config:)
12
+ @pagefind_path = pagefind_path
13
+ @config = config
14
+ end
15
+
16
+ def serve(path)
17
+ relative_path = path.delete_prefix(Constants::PAGEFIND_PREFIX)
18
+ return not_found if relative_path.include?("..")
19
+
20
+ file_path = resolve_file(relative_path)
21
+ return not_found unless file_path && File.file?(file_path)
22
+
23
+ serve_file(file_path)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :pagefind_path, :config
29
+
30
+ def resolve_file(relative_path)
31
+ return File.join(pagefind_path, relative_path) if pagefind_path && Dir.exist?(pagefind_path)
32
+
33
+ output_dir = config&.build&.output_dir || "dist"
34
+ File.join(output_dir, "pagefind", relative_path)
35
+ end
36
+
37
+ def serve_file(file_path)
38
+ content = File.binread(file_path)
39
+ content_type = content_type_for(file_path)
40
+
41
+ [Constants::STATUS_OK, build_headers(content_type), [content]]
42
+ end
43
+
44
+ def build_headers(content_type)
45
+ {
46
+ "Content-Type" => content_type,
47
+ "Cache-Control" => "no-cache, no-store, must-revalidate",
48
+ "Pragma" => "no-cache",
49
+ "Expires" => "0"
50
+ }
51
+ end
52
+
53
+ def content_type_for(file_path)
54
+ extension = File.extname(file_path)
55
+ CONTENT_TYPES.fetch(extension, "application/octet-stream")
56
+ end
57
+
58
+ def not_found
59
+ message = "Pagefind not found. Run 'docyard build' first."
60
+ [Constants::STATUS_NOT_FOUND, { "Content-Type" => "text/plain" }, [message]]
61
+ end
62
+ end
63
+ end
@@ -12,7 +12,7 @@ module Docyard
12
12
  def initialize(port: DEFAULT_PORT)
13
13
  @port = port
14
14
  @config = Config.load
15
- @output_dir = File.expand_path(@config.build.output_dir)
15
+ @output_dir = File.expand_path(@config.build.output)
16
16
  end
17
17
 
18
18
  def start
@@ -4,25 +4,23 @@ require "json"
4
4
  require "rack"
5
5
  require_relative "../navigation/sidebar_builder"
6
6
  require_relative "../navigation/prev_next_builder"
7
+ require_relative "../navigation/breadcrumb_builder"
7
8
  require_relative "../config/branding_resolver"
8
9
  require_relative "../config/constants"
10
+ require_relative "../rendering/template_resolver"
11
+ require_relative "../routing/fallback_resolver"
12
+ require_relative "pagefind_handler"
9
13
 
10
14
  module Docyard
11
15
  class RackApplication
12
- PAGEFIND_CONTENT_TYPES = {
13
- ".js" => "application/javascript; charset=utf-8",
14
- ".css" => "text/css; charset=utf-8",
15
- ".json" => "application/json; charset=utf-8"
16
- }.freeze
17
-
18
16
  def initialize(docs_path:, file_watcher:, config: nil, pagefind_path: nil)
19
17
  @docs_path = docs_path
20
18
  @file_watcher = file_watcher
21
19
  @config = config
22
- @pagefind_path = pagefind_path
23
20
  @router = Router.new(docs_path: docs_path)
24
- @renderer = Renderer.new(base_url: config&.build&.base_url || "/", config: config)
21
+ @renderer = Renderer.new(base_url: config&.build&.base || "/", config: config)
25
22
  @asset_handler = AssetHandler.new
23
+ @pagefind_handler = PagefindHandler.new(pagefind_path: pagefind_path, config: config)
26
24
  end
27
25
 
28
26
  def call(env)
@@ -31,14 +29,17 @@ module Docyard
31
29
 
32
30
  private
33
31
 
34
- attr_reader :docs_path, :file_watcher, :config, :pagefind_path, :router, :renderer, :asset_handler
32
+ attr_reader :docs_path, :file_watcher, :config, :router, :renderer, :asset_handler, :pagefind_handler
35
33
 
36
34
  def handle_request(env)
37
35
  path = env["PATH_INFO"]
38
36
 
39
37
  return handle_reload_check(env) if path == Constants::RELOAD_ENDPOINT
40
- return asset_handler.serve(path) if path.start_with?(Constants::ASSETS_PREFIX)
41
- return serve_pagefind(path) if path.start_with?(Constants::PAGEFIND_PREFIX)
38
+ return asset_handler.serve_docyard_assets(path) if path.start_with?(Constants::DOCYARD_ASSETS_PREFIX)
39
+ return pagefind_handler.serve(path) if path.start_with?(Constants::PAGEFIND_PREFIX)
40
+
41
+ public_response = asset_handler.serve_public_file(path)
42
+ return public_response if public_response
42
43
 
43
44
  handle_documentation_request(path)
44
45
  rescue StandardError => e
@@ -46,45 +47,90 @@ module Docyard
46
47
  end
47
48
 
48
49
  def handle_documentation_request(path)
50
+ if root_path?(path)
51
+ html_response = serve_custom_landing_page
52
+ return html_response if html_response
53
+ end
54
+
49
55
  result = router.resolve(path)
50
56
 
51
57
  if result.found?
52
58
  render_documentation_page(result.file_path, path)
59
+ else
60
+ try_fallback_redirect(path)
61
+ end
62
+ end
63
+
64
+ def root_path?(path)
65
+ path == "/" || path.empty?
66
+ end
67
+
68
+ def serve_custom_landing_page
69
+ html_path = File.join(docs_path, "index.html")
70
+ return nil unless File.file?(html_path)
71
+
72
+ html = File.read(html_path)
73
+ [Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
74
+ end
75
+
76
+ def try_fallback_redirect(path)
77
+ sidebar_builder = build_sidebar_instance(path)
78
+ fallback_resolver = Routing::FallbackResolver.new(
79
+ docs_path: docs_path,
80
+ sidebar_builder: sidebar_builder
81
+ )
82
+
83
+ fallback_path = fallback_resolver.resolve_fallback(path)
84
+ if fallback_path
85
+ redirect_to(fallback_path)
53
86
  else
54
87
  render_not_found_page
55
88
  end
56
89
  end
57
90
 
91
+ def redirect_to(path)
92
+ [Constants::STATUS_REDIRECT, { "Location" => path }, []]
93
+ end
94
+
58
95
  def render_documentation_page(file_path, current_path)
59
- sidebar_builder = build_sidebar_instance(current_path)
96
+ markdown = Markdown.new(File.read(file_path))
97
+ template_resolver = TemplateResolver.new(markdown.frontmatter, @config&.data)
98
+ branding = branding_options
60
99
 
61
- html = renderer.render_file(
62
- file_path,
63
- sidebar_html: sidebar_builder.to_html,
64
- prev_next_html: build_prev_next(sidebar_builder, current_path, file_path),
65
- branding: branding_options
66
- )
100
+ navigation = build_navigation_html(template_resolver, current_path, markdown, branding[:header_ctas])
101
+ html = renderer.render_file(file_path, **navigation, branding: branding,
102
+ template_options: template_resolver.to_options,
103
+ current_path: current_path)
67
104
 
68
105
  [Constants::STATUS_OK, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
69
106
  end
70
107
 
108
+ def build_navigation_html(template_resolver, current_path, markdown, header_ctas)
109
+ return { sidebar_html: "", prev_next_html: "", breadcrumbs: nil } unless template_resolver.show_sidebar?
110
+
111
+ sidebar_builder = build_sidebar_instance(current_path, header_ctas)
112
+ {
113
+ sidebar_html: sidebar_builder.to_html,
114
+ prev_next_html: build_prev_next(sidebar_builder, current_path, markdown),
115
+ breadcrumbs: build_breadcrumbs(sidebar_builder.tree, current_path)
116
+ }
117
+ end
118
+
71
119
  def render_not_found_page
72
120
  html = renderer.render_not_found
73
121
  [Constants::STATUS_NOT_FOUND, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
74
122
  end
75
123
 
76
- def build_sidebar_instance(current_path)
124
+ def build_sidebar_instance(current_path, header_ctas = [])
77
125
  SidebarBuilder.new(
78
126
  docs_path: docs_path,
79
127
  current_path: current_path,
80
- config: config
128
+ config: config,
129
+ header_ctas: header_ctas
81
130
  )
82
131
  end
83
132
 
84
- def build_prev_next(sidebar_builder, current_path, file_path)
85
- markdown_content = File.read(file_path)
86
- markdown = Markdown.new(markdown_content)
87
-
133
+ def build_prev_next(sidebar_builder, current_path, markdown)
88
134
  PrevNextBuilder.new(
89
135
  sidebar_tree: sidebar_builder.tree,
90
136
  current_path: current_path,
@@ -93,10 +139,18 @@ module Docyard
93
139
  ).to_html
94
140
  end
95
141
 
96
- def navigation_config
97
- return {} unless config
142
+ def build_breadcrumbs(sidebar_tree, current_path)
143
+ return nil unless breadcrumbs_enabled?
144
+
145
+ BreadcrumbBuilder.new(sidebar_tree: sidebar_tree, current_path: current_path)
146
+ end
98
147
 
99
- config.navigation&.footer || {}
148
+ def breadcrumbs_enabled?
149
+ config&.navigation&.breadcrumbs != false
150
+ end
151
+
152
+ def navigation_config
153
+ {}
100
154
  end
101
155
 
102
156
  def branding_options
@@ -134,42 +188,5 @@ module Docyard
134
188
  [Constants::STATUS_INTERNAL_ERROR, { "Content-Type" => Constants::CONTENT_TYPE_HTML },
135
189
  [renderer.render_server_error(error)]]
136
190
  end
137
-
138
- def serve_pagefind(path)
139
- relative_path = path.delete_prefix(Constants::PAGEFIND_PREFIX)
140
- return pagefind_not_found if relative_path.include?("..")
141
-
142
- file_path = resolve_pagefind_file(relative_path)
143
- return pagefind_not_found unless file_path && File.file?(file_path)
144
-
145
- content = File.binread(file_path)
146
- content_type = pagefind_content_type(file_path)
147
-
148
- headers = {
149
- "Content-Type" => content_type,
150
- "Cache-Control" => "no-cache, no-store, must-revalidate",
151
- "Pragma" => "no-cache",
152
- "Expires" => "0"
153
- }
154
-
155
- [Constants::STATUS_OK, headers, [content]]
156
- end
157
-
158
- def resolve_pagefind_file(relative_path)
159
- return File.join(pagefind_path, relative_path) if pagefind_path && Dir.exist?(pagefind_path)
160
-
161
- output_dir = config&.build&.output_dir || "dist"
162
- File.join(output_dir, "pagefind", relative_path)
163
- end
164
-
165
- def pagefind_content_type(file_path)
166
- extension = File.extname(file_path)
167
- PAGEFIND_CONTENT_TYPES.fetch(extension, "application/octet-stream")
168
- end
169
-
170
- def pagefind_not_found
171
- message = "Pagefind not found. Run 'docyard build' first."
172
- [Constants::STATUS_NOT_FOUND, { "Content-Type" => "text/plain" }, [message]]
173
- end
174
191
  end
175
192
  end