docyard 1.0.0 → 1.0.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: edade5380162369781998f1693e7658c34eb7275092cd709fce68ee25393f0af
4
- data.tar.gz: ec1c12df9c38802e881404526b4bff6d10f860320f0f0e3b459344f04c980f51
3
+ metadata.gz: 9ea61eefa97dc9199d17929def1315fdf2ce71f75ddf4f03833b9291fa64ce75
4
+ data.tar.gz: '0538fae77efffb84a8aac4ba979510419ff2a8cd8eec1b5aaf9effc2950595d8'
5
5
  SHA512:
6
- metadata.gz: 656e618b495b1b657bb002857c030dff8929546f453acd559268c9fb25f5d4f355938cf0f705914b54d22d3ca3c5a601635c5168fe85cd1d574f72b1f0c7a897
7
- data.tar.gz: f86343f57d8a64520652351ff1d8fa43518af247227f96092b5feff5b0ec2b185b6295017c8d18eb62f12fe28a22c66b71c1390834d296baed643181972a3480
6
+ metadata.gz: f184c67f1be8766cdd6339da6b25ccd8b7f1d0150ce3f6c0db0de8dd68c590b82e29f76a4ec1928c29151018d318225f2769cea58be9ad1dc2c721a2593663d5
7
+ data.tar.gz: 982f4bdcd4170dd013dd5ee10fb21db935eeafcb486372a45db7ae58a3f8d75e6c716555585695ea9527cc7fbd2bf0e43c5a7982894160ec6832c591c2db45a1
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.1] - 2026-01-22
11
+
12
+ ### Added
13
+ - **Root Fallback Redirect** - Auto-redirect to first page when no index.md exists (#127)
14
+
15
+ ### Fixed
16
+ - Breadcrumb sections without index pages now link to first navigable child (#126)
17
+ - Error pages (404/500) now use custom branding colors (#125)
18
+ - Base URL now correctly applied to all assets including fonts and search (#124)
19
+ - Dev server no longer applies build.base config, always uses root path (#123)
20
+
10
21
  ## [1.0.0] - 2026-01-22
11
22
 
12
23
  ### Added
@@ -205,7 +216,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
205
216
  - Initial gem structure
206
217
  - Project scaffolding
207
218
 
208
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.9.0...HEAD
219
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.0.1...HEAD
220
+ [1.0.1]: https://github.com/sanifhimani/docyard/compare/v1.0.0...v1.0.1
221
+ [1.0.0]: https://github.com/sanifhimani/docyard/compare/v0.9.0...v1.0.0
209
222
  [0.9.0]: https://github.com/sanifhimani/docyard/compare/v0.8.0...v0.9.0
210
223
  [0.8.0]: https://github.com/sanifhimani/docyard/compare/v0.7.0...v0.8.0
211
224
  [0.7.0]: https://github.com/sanifhimani/docyard/compare/v0.6.0...v0.7.0
@@ -37,6 +37,7 @@ module Docyard
37
37
  minified = CSSminify.compress(css_content)
38
38
  minified = fix_calc_whitespace(minified)
39
39
  minified = fix_css_math_functions(minified)
40
+ minified = replace_css_asset_urls(minified)
40
41
  hash = generate_hash(minified)
41
42
 
42
43
  write_bundled_asset(minified, hash, "css")
@@ -58,6 +59,11 @@ module Docyard
58
59
  css.gsub(/\bmax\(0,/, "max(0px,").gsub(/\bmin\(0,/, "min(0px,)")
59
60
  end
60
61
 
62
+ def replace_css_asset_urls(css)
63
+ base_url = normalize_base_url(config.build.base)
64
+ css.gsub(%r{/_docyard/fonts/}, "#{base_url}_docyard/fonts/")
65
+ end
66
+
61
67
  def resolve_css_imports(css_content)
62
68
  css_content.gsub(/@import url\('([^']+)'\);/) do |match|
63
69
  import_file = Regexp.last_match(1)
@@ -86,6 +92,7 @@ module Docyard
86
92
  components_js = concatenate_component_js
87
93
  js_content = [theme_js, components_js].join("\n")
88
94
  minified = Terser.compile(js_content)
95
+ minified = replace_js_asset_urls(minified)
89
96
  hash = generate_hash(minified)
90
97
 
91
98
  write_bundled_asset(minified, hash, "js")
@@ -94,6 +101,12 @@ module Docyard
94
101
  hash
95
102
  end
96
103
 
104
+ def replace_js_asset_urls(js_content)
105
+ base_url = normalize_base_url(config.build.base)
106
+ js_content.gsub(%r{"/_docyard/pagefind/}, "\"#{base_url}_docyard/pagefind/")
107
+ .gsub(%r{baseUrl:\s*["']/["']}, "baseUrl:\"#{base_url}\"")
108
+ end
109
+
97
110
  def concatenate_component_js
98
111
  components_dir = File.join(ASSETS_PATH, "js", "components")
99
112
  return "" unless Dir.exist?(components_dir)
@@ -123,6 +136,15 @@ module Docyard
123
136
  .gsub(%r{/_docyard/js/theme\.js}, "#{base_url}_docyard/bundle.#{js_hash}.js")
124
137
  .gsub(%r{/_docyard/js/components\.js}, "")
125
138
  .gsub(%r{<script src="/_docyard/js/reload\.js"></script>}, "")
139
+ .then { |html| replace_content_image_paths(html, base_url) }
140
+ end
141
+
142
+ def replace_content_image_paths(content, base_url)
143
+ return content if base_url == "/"
144
+
145
+ content.gsub(%r{(<img[^>]*\ssrc=")/(?!_docyard/)([^"]*")}) do
146
+ "#{Regexp.last_match(1)}#{base_url}#{Regexp.last_match(2)}"
147
+ end
126
148
  end
127
149
 
128
150
  def write_bundled_asset(content, hash, extension)
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Build
5
+ class ErrorPageGenerator
6
+ attr_reader :config, :docs_path, :renderer
7
+
8
+ def initialize(config:, docs_path:, renderer:)
9
+ @config = config
10
+ @docs_path = docs_path
11
+ @renderer = renderer
12
+ end
13
+
14
+ def generate
15
+ output_path = File.join(config.build.output, "404.html")
16
+ html_content = load_content
17
+
18
+ FileUtils.mkdir_p(File.dirname(output_path))
19
+ File.write(output_path, html_content)
20
+ end
21
+
22
+ private
23
+
24
+ def load_content
25
+ custom_error_page = File.join(docs_path, "404.html")
26
+ return File.read(custom_error_page) if File.exist?(custom_error_page)
27
+
28
+ branding = BrandingResolver.new(config).resolve
29
+ renderer.render_not_found(branding: branding)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Build
5
+ module FileWriter
6
+ def safe_file_write(path)
7
+ yield
8
+ rescue Errno::EACCES => e
9
+ raise BuildError, "Permission denied writing to #{path}: #{e.message}"
10
+ rescue Errno::ENOSPC => e
11
+ raise BuildError, "Disk full, cannot write to #{path}: #{e.message}"
12
+ rescue Errno::EROFS => e
13
+ raise BuildError, "Read-only filesystem, cannot write to #{path}: #{e.message}"
14
+ rescue SystemCallError => e
15
+ raise BuildError, "Failed to write #{path}: #{e.message}"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Build
5
+ class RootFallbackGenerator
6
+ attr_reader :config, :docs_path, :sidebar_cache, :renderer
7
+
8
+ def initialize(config:, docs_path:, sidebar_cache:, renderer:)
9
+ @config = config
10
+ @docs_path = docs_path
11
+ @sidebar_cache = sidebar_cache
12
+ @renderer = renderer
13
+ end
14
+
15
+ def generate_if_needed
16
+ return if root_index_exists?
17
+
18
+ first_path = find_first_navigable_path
19
+ return unless first_path
20
+
21
+ generate_redirect_page(first_path)
22
+ end
23
+
24
+ private
25
+
26
+ def root_index_exists?
27
+ File.exist?(File.join(docs_path, "index.md")) ||
28
+ File.exist?(File.join(docs_path, "index.html"))
29
+ end
30
+
31
+ def find_first_navigable_path
32
+ return nil unless sidebar_cache&.tree&.any?
33
+
34
+ find_first_file_in_tree(sidebar_cache.tree)
35
+ end
36
+
37
+ def find_first_file_in_tree(items)
38
+ items.each do |item|
39
+ return item[:path] if item[:type] == :file && item[:path]
40
+
41
+ if item[:children]&.any?
42
+ nested = find_first_file_in_tree(item[:children])
43
+ return nested if nested
44
+ end
45
+ end
46
+
47
+ nil
48
+ end
49
+
50
+ def generate_redirect_page(target_path)
51
+ output_path = File.join(config.build.output, "index.html")
52
+ full_target = build_full_target_url(target_path)
53
+
54
+ FileUtils.mkdir_p(File.dirname(output_path))
55
+ File.write(output_path, renderer.render_redirect(full_target))
56
+
57
+ full_target
58
+ end
59
+
60
+ def build_full_target_url(target_path)
61
+ base = config.build.base&.chomp("/") || ""
62
+ "#{base}#{target_path}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -7,10 +7,15 @@ require_relative "../navigation/page_navigation_builder"
7
7
  require_relative "../navigation/sidebar/cache"
8
8
  require_relative "../utils/path_utils"
9
9
  require_relative "../utils/git_info"
10
+ require_relative "root_fallback_generator"
11
+ require_relative "error_page_generator"
12
+ require_relative "file_writer"
10
13
 
11
14
  module Docyard
12
15
  module Build
13
16
  class StaticGenerator
17
+ include FileWriter
18
+
14
19
  PARALLEL_THRESHOLD = 10
15
20
 
16
21
  attr_reader :config, :verbose, :sidebar_cache
@@ -31,6 +36,7 @@ module Docyard
31
36
 
32
37
  generate_all_pages(markdown_files)
33
38
  generate_error_page
39
+ generate_root_fallback_if_needed
34
40
 
35
41
  markdown_files.size
36
42
  ensure
@@ -161,18 +167,6 @@ module Docyard
161
167
  log "Generated: #{output_path}" if verbose
162
168
  end
163
169
 
164
- def safe_file_write(path, &block)
165
- block.call
166
- rescue Errno::EACCES => e
167
- raise BuildError, "Permission denied writing to #{path}: #{e.message}"
168
- rescue Errno::ENOSPC => e
169
- raise BuildError, "Disk full, cannot write to #{path}: #{e.message}"
170
- rescue Errno::EROFS => e
171
- raise BuildError, "Read-only filesystem, cannot write to #{path}: #{e.message}"
172
- rescue SystemCallError => e
173
- raise BuildError, "Failed to write #{path}: #{e.message}"
174
- end
175
-
176
170
  def build_sidebar_cache
177
171
  @sidebar_cache = Sidebar::Cache.new(
178
172
  docs_path: docs_path,
@@ -194,15 +188,23 @@ module Docyard
194
188
  end
195
189
 
196
190
  def generate_error_page
197
- output_path = File.join(config.build.output, "404.html")
198
- html_content = load_error_page_content
199
- safe_file_write(output_path) { File.write(output_path, html_content) }
191
+ ErrorPageGenerator.new(
192
+ config: config,
193
+ docs_path: docs_path,
194
+ renderer: build_renderer
195
+ ).generate
200
196
  log "[✓] Generated 404.html"
201
197
  end
202
198
 
203
- def load_error_page_content
204
- error_page = File.join(docs_path, "404.html")
205
- File.exist?(error_page) ? File.read(error_page) : build_renderer.render_not_found
199
+ def generate_root_fallback_if_needed
200
+ generator = RootFallbackGenerator.new(
201
+ config: config,
202
+ docs_path: docs_path,
203
+ sidebar_cache: sidebar_cache,
204
+ renderer: build_renderer
205
+ )
206
+ target = generator.generate_if_needed
207
+ log "[✓] Generated root redirect to #{target}" if target
206
208
  end
207
209
  end
208
210
  end
@@ -76,9 +76,10 @@ module Docyard
76
76
  def search_in_ancestors(node, path, title, href)
77
77
  return unless node[:children]&.any?
78
78
 
79
- effective_href = href == "/" ? derive_section_path(node) : href
80
- return unless path_is_ancestor?(effective_href)
79
+ section_path = derive_section_path(node, href)
80
+ return unless path_is_ancestor?(section_path)
81
81
 
82
+ effective_href = resolve_section_href(node, href, section_path)
82
83
  result = find_breadcrumb_path(
83
84
  node[:children],
84
85
  path + [Item.new(title: title, href: effective_href, current: false)]
@@ -86,14 +87,52 @@ module Docyard
86
87
  result.any? ? result : nil
87
88
  end
88
89
 
89
- def derive_section_path(node)
90
- first_child = node[:children]&.first
90
+ def derive_section_path(node, href)
91
+ return href if section_has_index?(href)
92
+
93
+ derive_section_path_from_children(node[:children])
94
+ end
95
+
96
+ def derive_section_path_from_children(children)
97
+ first_child = children&.first
91
98
  return nil unless first_child
92
99
 
93
100
  child_path = first_child[:path]
94
- return nil if child_path.nil? || child_path.empty?
101
+ return nil unless navigable_path?(child_path)
102
+
103
+ parent_dir = File.dirname(child_path)
104
+ root_parent?(parent_dir) ? child_path : parent_dir
105
+ end
106
+
107
+ def root_parent?(parent_dir)
108
+ parent_dir == "/" || parent_dir.empty?
109
+ end
110
+
111
+ def resolve_section_href(node, href, section_path)
112
+ return href if section_has_index?(href)
113
+
114
+ find_first_navigable_child(node[:children]) || section_path
115
+ end
116
+
117
+ def section_has_index?(path)
118
+ path && !path.empty? && path != "/"
119
+ end
120
+
121
+ def find_first_navigable_child(children)
122
+ return nil unless children&.any?
123
+
124
+ children.each do |child|
125
+ return child[:path] if navigable_path?(child[:path])
126
+
127
+ nested = find_first_navigable_child(child[:children])
128
+ return nested if nested
129
+ end
130
+
131
+ nil
132
+ end
95
133
 
96
- File.dirname(child_path)
134
+ def navigable_path?(path)
135
+ path && !path.empty? && path != "/"
97
136
  end
98
137
 
99
138
  def search_in_children(node, path)
@@ -83,16 +83,23 @@ module Docyard
83
83
  ERB.new(template).result(binding)
84
84
  end
85
85
 
86
- def render_not_found
86
+ def render_not_found(branding: nil)
87
+ @primary_color = branding&.dig(:primary_color)
87
88
  render_error_template(404)
88
89
  end
89
90
 
90
- def render_server_error(error)
91
+ def render_server_error(error, branding: nil)
91
92
  @error_message = error.message
92
93
  @backtrace = error.backtrace&.join("\n") || "No backtrace available"
94
+ @primary_color = branding&.dig(:primary_color)
93
95
  render_error_template(500)
94
96
  end
95
97
 
98
+ def render_redirect(target_url)
99
+ @target_url = target_url
100
+ render_error_template(:redirect)
101
+ end
102
+
96
103
  def render_error_template(status)
97
104
  error_template_path = File.join(ERRORS_PATH, "#{status}.html.erb")
98
105
  template = File.read(error_template_path)
@@ -19,7 +19,7 @@ module Docyard
19
19
  @dev_mode = !sse_port.nil?
20
20
  @sidebar_cache = sidebar_cache
21
21
  @router = Router.new(docs_path: docs_path)
22
- @renderer = Renderer.new(base_url: config&.build&.base || "/", config: config, dev_mode: @dev_mode,
22
+ @renderer = Renderer.new(base_url: "/", config: config, dev_mode: @dev_mode,
23
23
  sse_port: sse_port)
24
24
  @asset_handler = AssetHandler.new(public_dir: config&.public_dir || "docs/public")
25
25
  @pagefind_handler = PagefindHandler.new(pagefind_path: pagefind_path, config: config)
@@ -124,7 +124,7 @@ module Docyard
124
124
  end
125
125
 
126
126
  def render_not_found_page
127
- html = renderer.render_not_found
127
+ html = renderer.render_not_found(branding: branding_options)
128
128
  [Constants::STATUS_NOT_FOUND, { "Content-Type" => Constants::CONTENT_TYPE_HTML }, [html]]
129
129
  end
130
130
 
@@ -147,7 +147,7 @@ module Docyard
147
147
  Docyard.logger.error("Request error: #{error.message} [#{request_context}]")
148
148
  Docyard.logger.debug(error.backtrace.join("\n"))
149
149
  [Constants::STATUS_INTERNAL_ERROR, { "Content-Type" => Constants::CONTENT_TYPE_HTML },
150
- [renderer.render_server_error(error)]]
150
+ [renderer.render_server_error(error, branding: branding_options)]]
151
151
  end
152
152
 
153
153
  def build_request_context(env)
@@ -76,11 +76,11 @@ body.has-announcement:not(.has-tabs) .layout {
76
76
 
77
77
  @media (max-width: 1280px) and (min-width: 1025px) {
78
78
  body.has-announcement .layout {
79
- padding-top: calc(var(--header-height) + var(--tab-bar-height) + var(--announcement-height) + 3rem);
79
+ padding-top: calc(var(--header-height) + var(--tab-bar-height) + var(--announcement-height) + var(--secondary-header-height));
80
80
  }
81
81
 
82
82
  body.has-announcement:not(.has-tabs) .layout {
83
- padding-top: calc(var(--header-height) + var(--announcement-height) + 3rem);
83
+ padding-top: calc(var(--header-height) + var(--announcement-height) + var(--secondary-header-height));
84
84
  }
85
85
  }
86
86
 
@@ -91,7 +91,7 @@ body.has-announcement:not(.has-tabs) .layout {
91
91
  }
92
92
 
93
93
  body.has-announcement .layout {
94
- padding-top: calc(var(--header-height) + var(--announcement-height) + 3rem);
94
+ padding-top: calc(var(--header-height) + var(--announcement-height) + var(--secondary-header-height));
95
95
  }
96
96
 
97
97
  body.has-announcement .sidebar {
@@ -12,7 +12,7 @@
12
12
  .has-tabs .content h4[id],
13
13
  .has-tabs .content h5[id],
14
14
  .has-tabs .content h6[id] {
15
- scroll-margin-top: calc(var(--header-height) + 3rem + var(--spacing-6));
15
+ scroll-margin-top: calc(var(--header-height) + var(--tab-bar-height) + var(--spacing-6));
16
16
  }
17
17
 
18
18
  @media (max-width: 1280px) and (min-width: 1025px) {
@@ -22,7 +22,7 @@
22
22
  .content h4[id],
23
23
  .content h5[id],
24
24
  .content h6[id] {
25
- scroll-margin-top: calc(var(--header-height) + 3rem + var(--spacing-6));
25
+ scroll-margin-top: calc(var(--header-height) + var(--secondary-header-height) + var(--spacing-6));
26
26
  }
27
27
  }
28
28
 
@@ -123,7 +123,7 @@
123
123
 
124
124
  @media (max-width: 1280px) and (min-width: 1025px) {
125
125
  .has-tabs .layout {
126
- padding-top: calc(var(--header-height) + var(--tab-bar-height) + 3rem);
126
+ padding-top: calc(var(--header-height) + var(--tab-bar-height) + var(--secondary-header-height));
127
127
  }
128
128
  }
129
129
 
@@ -143,7 +143,7 @@
143
143
  }
144
144
 
145
145
  .has-tabs .layout {
146
- padding-top: calc(var(--header-height) + 3rem);
146
+ padding-top: calc(var(--header-height) + var(--secondary-header-height));
147
147
  }
148
148
 
149
149
  .has-tabs .sidebar {
@@ -257,30 +257,30 @@
257
257
 
258
258
  @media (max-width: 1280px) and (min-width: 1025px) {
259
259
  .docyard-toc__nav {
260
- top: calc(var(--header-height) + 3rem + var(--spacing-2));
260
+ top: calc(var(--header-height) + var(--secondary-header-height) + var(--spacing-2));
261
261
  right: var(--spacing-6);
262
262
  width: 280px;
263
- max-height: calc(100vh - var(--header-height) - 3rem - var(--spacing-8));
263
+ max-height: calc(100vh - var(--header-height) - var(--secondary-header-height) - var(--spacing-8));
264
264
  }
265
265
 
266
266
  .has-tabs .docyard-toc__nav {
267
- top: calc(var(--header-height) + var(--tab-bar-height) + 3rem + var(--spacing-2));
268
- max-height: calc(100vh - var(--header-height) - var(--tab-bar-height) - 3rem - var(--spacing-8));
267
+ top: calc(var(--header-height) + var(--tab-bar-height) + var(--secondary-header-height) + var(--spacing-2));
268
+ max-height: calc(100vh - var(--header-height) - var(--tab-bar-height) - var(--secondary-header-height) - var(--spacing-8));
269
269
  }
270
270
  }
271
271
 
272
272
  @media (max-width: 1024px) {
273
273
  .docyard-toc__nav {
274
- top: calc(var(--header-height) + 3rem - 2px);
274
+ top: calc(var(--header-height) + var(--secondary-header-height) - 2px);
275
275
  left: var(--spacing-4);
276
276
  right: var(--spacing-4);
277
- max-height: calc(100vh - var(--header-height) - 3rem - var(--spacing-2));
277
+ max-height: calc(100vh - var(--header-height) - var(--secondary-header-height) - var(--spacing-2));
278
278
  transform-origin: top center;
279
279
  }
280
280
 
281
281
  .secondary-header.shift-up ~ .layout .docyard-toc__nav {
282
- top: calc(3rem - 2px);
283
- max-height: calc(100vh - 3rem - var(--spacing-2));
282
+ top: calc(var(--secondary-header-height) - 2px);
283
+ max-height: calc(100vh - var(--secondary-header-height) - var(--spacing-2));
284
284
  }
285
285
 
286
286
  .docyard-toc__list .docyard-toc__list {
@@ -292,25 +292,25 @@
292
292
  /* Adjust TOC dropdown position when announcement banner is visible */
293
293
  @media (max-width: 1280px) and (min-width: 1025px) {
294
294
  body.has-announcement .docyard-toc__nav {
295
- top: calc(var(--header-height) + var(--announcement-height) + 3rem + var(--spacing-2));
296
- max-height: calc(100vh - var(--header-height) - var(--announcement-height) - 3rem - var(--spacing-8));
295
+ top: calc(var(--header-height) + var(--announcement-height) + var(--secondary-header-height) + var(--spacing-2));
296
+ max-height: calc(100vh - var(--header-height) - var(--announcement-height) - var(--secondary-header-height) - var(--spacing-8));
297
297
  }
298
298
 
299
299
  body.has-announcement.has-tabs .docyard-toc__nav {
300
- top: calc(var(--header-height) + var(--tab-bar-height) + var(--announcement-height) + 3rem + var(--spacing-2));
301
- max-height: calc(100vh - var(--header-height) - var(--tab-bar-height) - var(--announcement-height) - 3rem - var(--spacing-8));
300
+ top: calc(var(--header-height) + var(--tab-bar-height) + var(--announcement-height) + var(--secondary-header-height) + var(--spacing-2));
301
+ max-height: calc(100vh - var(--header-height) - var(--tab-bar-height) - var(--announcement-height) - var(--secondary-header-height) - var(--spacing-8));
302
302
  }
303
303
  }
304
304
 
305
305
  @media (max-width: 1024px) {
306
306
  body.has-announcement .docyard-toc__nav {
307
- top: calc(var(--header-height) + var(--announcement-height) + 3rem - 2px);
308
- max-height: calc(100vh - var(--header-height) - var(--announcement-height) - 3rem - var(--spacing-2));
307
+ top: calc(var(--header-height) + var(--announcement-height) + var(--secondary-header-height) - 2px);
308
+ max-height: calc(100vh - var(--header-height) - var(--announcement-height) - var(--secondary-header-height) - var(--spacing-2));
309
309
  }
310
310
 
311
311
  body.has-announcement .secondary-header.shift-up ~ .layout .docyard-toc__nav {
312
- top: calc(var(--announcement-height) + 3rem - 2px);
313
- max-height: calc(100vh - var(--announcement-height) - 3rem - var(--spacing-2));
312
+ top: calc(var(--announcement-height) + var(--secondary-header-height) - 2px);
313
+ max-height: calc(100vh - var(--announcement-height) - var(--secondary-header-height) - var(--spacing-2));
314
314
  }
315
315
  }
316
316
 
@@ -179,6 +179,15 @@ html:has(.has-tabs) {
179
179
  display: none;
180
180
  }
181
181
 
182
+ .layout {
183
+ display: flex;
184
+ min-height: calc(100vh - var(--header-height));
185
+ padding-top: var(--header-height);
186
+ width: 100%;
187
+ max-width: var(--layout-max-width);
188
+ margin: 0 auto;
189
+ position: relative;
190
+ }
182
191
 
183
192
  .secondary-header {
184
193
  display: none;
@@ -193,8 +202,8 @@ html:has(.has-tabs) {
193
202
  top: var(--header-height);
194
203
  left: calc(max(0px, (100vw - var(--layout-max-width)) / 2) + var(--sidebar-width));
195
204
  right: max(0px, calc((100vw - var(--layout-max-width)) / 2));
196
- height: 3rem;
197
- min-height: 3rem;
205
+ height: var(--secondary-header-height);
206
+ min-height: var(--secondary-header-height);
198
207
  background: var(--background);
199
208
  border-bottom: 1px solid var(--border);
200
209
  padding: 0 var(--spacing-6);
@@ -213,7 +222,7 @@ html:has(.has-tabs) {
213
222
  }
214
223
 
215
224
  .layout {
216
- padding-top: calc(var(--header-height) + 3rem);
225
+ padding-top: calc(var(--header-height) + var(--secondary-header-height));
217
226
  }
218
227
  }
219
228
 
@@ -226,8 +235,8 @@ html:has(.has-tabs) {
226
235
  top: var(--header-height);
227
236
  left: 0;
228
237
  right: 0;
229
- height: 3rem;
230
- min-height: 3rem;
238
+ height: var(--secondary-header-height);
239
+ min-height: var(--secondary-header-height);
231
240
  background: var(--background);
232
241
  border-bottom: 1px solid var(--border);
233
242
  padding: 0 var(--spacing-6);
@@ -278,16 +287,6 @@ html:has(.has-tabs) {
278
287
  }
279
288
  }
280
289
 
281
- .layout {
282
- display: flex;
283
- min-height: calc(100vh - var(--header-height));
284
- padding-top: var(--header-height);
285
- width: 100%;
286
- max-width: var(--layout-max-width);
287
- margin: 0 auto;
288
- position: relative;
289
- }
290
-
291
290
  .sidebar {
292
291
  width: var(--sidebar-width);
293
292
  height: calc(100vh - var(--header-height));
@@ -424,7 +423,7 @@ main.content {
424
423
  }
425
424
 
426
425
  .layout {
427
- padding-top: calc(var(--header-height) + 3rem);
426
+ padding-top: calc(var(--header-height) + var(--secondary-header-height));
428
427
  flex-direction: column;
429
428
  }
430
429
 
@@ -157,6 +157,7 @@
157
157
  --toc-width: 17.5rem;
158
158
  --header-height: 4rem;
159
159
  --tab-bar-height: 3rem;
160
+ --secondary-header-height: 3rem;
160
161
  --content-max-width: 50rem;
161
162
  --layout-max-width: 88rem;
162
163
 
@@ -7,7 +7,7 @@
7
7
  <style>
8
8
  @font-face {
9
9
  font-family: 'Inter';
10
- src: url('/_docyard/fonts/Inter-Variable.ttf') format('truetype');
10
+ src: url('<%= base_url %>_docyard/fonts/Inter-Variable.ttf') format('truetype');
11
11
  font-weight: 100 900;
12
12
  font-style: normal;
13
13
  }
@@ -31,6 +31,17 @@
31
31
  --primary-foreground: oklch(0.30 0.05 230);
32
32
  }
33
33
 
34
+ <% if @primary_color && (@primary_color[:light] || @primary_color[:dark]) %>
35
+ <% light_color = @primary_color[:light] %>
36
+ <% dark_color = @primary_color[:dark] || light_color %>
37
+ <% if light_color %>
38
+ :root { --primary: <%= light_color %>; }
39
+ .dark { --primary: <%= dark_color %>; }
40
+ <% elsif dark_color %>
41
+ .dark { --primary: <%= dark_color %>; }
42
+ <% end %>
43
+ <% end %>
44
+
34
45
  * {
35
46
  margin: 0;
36
47
  padding: 0;
@@ -116,7 +127,7 @@
116
127
  <p class="error-code">404</p>
117
128
  <h1 class="error-title">Page not found</h1>
118
129
  <p class="error-message">This page doesn't exist or has been moved.</p>
119
- <a href="/" class="btn">
130
+ <a href="<%= base_url %>" class="btn">
120
131
  <%= icon(:house_line) %>
121
132
  Back to home
122
133
  </a>
@@ -7,7 +7,7 @@
7
7
  <style>
8
8
  @font-face {
9
9
  font-family: 'Inter';
10
- src: url('/_docyard/fonts/Inter-Variable.ttf') format('truetype');
10
+ src: url('<%= base_url %>_docyard/fonts/Inter-Variable.ttf') format('truetype');
11
11
  font-weight: 100 900;
12
12
  font-style: normal;
13
13
  }
@@ -35,6 +35,17 @@
35
35
  --destructive: #f87171;
36
36
  }
37
37
 
38
+ <% if @primary_color && (@primary_color[:light] || @primary_color[:dark]) %>
39
+ <% light_color = @primary_color[:light] %>
40
+ <% dark_color = @primary_color[:dark] || light_color %>
41
+ <% if light_color %>
42
+ :root { --primary: <%= light_color %>; }
43
+ .dark { --primary: <%= dark_color %>; }
44
+ <% elsif dark_color %>
45
+ .dark { --primary: <%= dark_color %>; }
46
+ <% end %>
47
+ <% end %>
48
+
38
49
  * {
39
50
  margin: 0;
40
51
  padding: 0;
@@ -166,7 +177,7 @@
166
177
  <p class="error-code">500</p>
167
178
  <h1 class="error-title">Something went wrong</h1>
168
179
  <p class="error-message">We encountered an unexpected error. Please try again.</p>
169
- <a href="/" class="btn">
180
+ <a href="<%= base_url %>" class="btn">
170
181
  <%= icon(:house_line) %>
171
182
  Back to home
172
183
  </a>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta http-equiv="refresh" content="0;url=<%= @target_url %>">
6
+ <title>Redirecting...</title>
7
+ <script>window.location.href = "<%= @target_url %>";</script>
8
+ </head>
9
+ <body>
10
+ <p>Redirecting to <a href="<%= @target_url %>"><%= @target_url %></a>...</p>
11
+ </body>
12
+ </html>
@@ -32,7 +32,7 @@
32
32
  <% end %>
33
33
  <% end %>
34
34
 
35
- <link rel="preload" href="/_docyard/fonts/Inter-Variable.ttf" as="font" type="font/ttf" crossorigin>
35
+ <link rel="preload" href="<%= asset_path('_docyard/fonts/Inter-Variable.ttf') %>" as="font" type="font/ttf" crossorigin>
36
36
 
37
37
  <%= render_partial('_icon_library') %>
38
38
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docyard
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docyard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sanif Himani
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-22 00:00:00.000000000 Z
11
+ date: 2026-01-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cssminify
@@ -192,8 +192,11 @@ files:
192
192
  - exe/docyard
193
193
  - lib/docyard.rb
194
194
  - lib/docyard/build/asset_bundler.rb
195
+ - lib/docyard/build/error_page_generator.rb
195
196
  - lib/docyard/build/file_copier.rb
197
+ - lib/docyard/build/file_writer.rb
196
198
  - lib/docyard/build/llms_txt_generator.rb
199
+ - lib/docyard/build/root_fallback_generator.rb
197
200
  - lib/docyard/build/sitemap_generator.rb
198
201
  - lib/docyard/build/static_generator.rb
199
202
  - lib/docyard/builder.rb
@@ -347,6 +350,7 @@ files:
347
350
  - lib/docyard/templates/config/docyard.yml.erb
348
351
  - lib/docyard/templates/errors/404.html.erb
349
352
  - lib/docyard/templates/errors/500.html.erb
353
+ - lib/docyard/templates/errors/redirect.html.erb
350
354
  - lib/docyard/templates/init/_sidebar.yml
351
355
  - lib/docyard/templates/init/docyard.yml
352
356
  - lib/docyard/templates/init/pages/components.md