docyard 1.0.2 → 1.2.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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -1
  3. data/README.md +56 -11
  4. data/lib/docyard/build/asset_bundler.rb +19 -29
  5. data/lib/docyard/build/file_copier.rb +7 -15
  6. data/lib/docyard/build/llms_txt_generator.rb +0 -2
  7. data/lib/docyard/build/sitemap_generator.rb +1 -1
  8. data/lib/docyard/build/social_cards/card_renderer.rb +132 -0
  9. data/lib/docyard/build/social_cards/doc_card.rb +97 -0
  10. data/lib/docyard/build/social_cards/homepage_card.rb +98 -0
  11. data/lib/docyard/build/social_cards_generator.rb +188 -0
  12. data/lib/docyard/build/static_generator.rb +30 -32
  13. data/lib/docyard/build/step_runner.rb +90 -0
  14. data/lib/docyard/build/validator.rb +98 -0
  15. data/lib/docyard/builder.rb +92 -55
  16. data/lib/docyard/cli.rb +63 -4
  17. data/lib/docyard/components/aliases.rb +0 -4
  18. data/lib/docyard/components/processors/callout_processor.rb +1 -1
  19. data/lib/docyard/components/processors/code_block_diff_preprocessor.rb +1 -1
  20. data/lib/docyard/components/processors/code_block_focus_preprocessor.rb +1 -1
  21. data/lib/docyard/components/processors/code_block_options_preprocessor.rb +2 -2
  22. data/lib/docyard/components/processors/code_group_processor.rb +1 -1
  23. data/lib/docyard/components/processors/icon_processor.rb +2 -2
  24. data/lib/docyard/components/processors/tabs_processor.rb +1 -1
  25. data/lib/docyard/config/branding_resolver.rb +1 -1
  26. data/lib/docyard/config/schema/definition.rb +30 -0
  27. data/lib/docyard/config/schema/sections.rb +62 -0
  28. data/lib/docyard/config/schema/simple_sections.rb +85 -0
  29. data/lib/docyard/config/schema.rb +28 -31
  30. data/lib/docyard/config/type_validators.rb +121 -0
  31. data/lib/docyard/config/validator.rb +136 -61
  32. data/lib/docyard/config.rb +4 -14
  33. data/lib/docyard/customizer.rb +196 -0
  34. data/lib/docyard/diagnostic.rb +89 -0
  35. data/lib/docyard/diagnostic_context.rb +48 -0
  36. data/lib/docyard/doctor/code_block_checker.rb +136 -0
  37. data/lib/docyard/doctor/component_checker.rb +49 -0
  38. data/lib/docyard/doctor/component_checkers/abbreviation_checker.rb +74 -0
  39. data/lib/docyard/doctor/component_checkers/badge_checker.rb +71 -0
  40. data/lib/docyard/doctor/component_checkers/base.rb +111 -0
  41. data/lib/docyard/doctor/component_checkers/callout_checker.rb +34 -0
  42. data/lib/docyard/doctor/component_checkers/cards_checker.rb +57 -0
  43. data/lib/docyard/doctor/component_checkers/code_group_checker.rb +47 -0
  44. data/lib/docyard/doctor/component_checkers/details_checker.rb +51 -0
  45. data/lib/docyard/doctor/component_checkers/icon_checker.rb +36 -0
  46. data/lib/docyard/doctor/component_checkers/image_attrs_checker.rb +46 -0
  47. data/lib/docyard/doctor/component_checkers/space_after_colons_checker.rb +45 -0
  48. data/lib/docyard/doctor/component_checkers/steps_checker.rb +35 -0
  49. data/lib/docyard/doctor/component_checkers/tabs_checker.rb +35 -0
  50. data/lib/docyard/doctor/component_checkers/tooltip_checker.rb +67 -0
  51. data/lib/docyard/doctor/component_checkers/unknown_type_checker.rb +34 -0
  52. data/lib/docyard/doctor/config_checker.rb +19 -0
  53. data/lib/docyard/doctor/config_fixer.rb +87 -0
  54. data/lib/docyard/doctor/content_checker.rb +164 -0
  55. data/lib/docyard/doctor/file_scanner.rb +113 -0
  56. data/lib/docyard/doctor/image_checker.rb +103 -0
  57. data/lib/docyard/doctor/link_checker.rb +91 -0
  58. data/lib/docyard/doctor/markdown_fixer.rb +62 -0
  59. data/lib/docyard/doctor/orphan_checker.rb +82 -0
  60. data/lib/docyard/doctor/reporter.rb +152 -0
  61. data/lib/docyard/doctor/sidebar_checker.rb +127 -0
  62. data/lib/docyard/doctor/sidebar_fixer.rb +47 -0
  63. data/lib/docyard/doctor.rb +178 -0
  64. data/lib/docyard/editor_launcher.rb +119 -0
  65. data/lib/docyard/errors.rb +0 -49
  66. data/lib/docyard/initializer.rb +32 -39
  67. data/lib/docyard/navigation/sidebar/local_config_loader.rb +44 -21
  68. data/lib/docyard/rendering/icon_helpers.rb +1 -3
  69. data/lib/docyard/rendering/markdown.rb +4 -0
  70. data/lib/docyard/rendering/og_helpers.rb +19 -1
  71. data/lib/docyard/rendering/renderer.rb +10 -1
  72. data/lib/docyard/search/build_indexer.rb +45 -25
  73. data/lib/docyard/search/dev_indexer.rb +12 -25
  74. data/lib/docyard/search/pagefind_binary.rb +185 -0
  75. data/lib/docyard/search/pagefind_support.rb +9 -7
  76. data/lib/docyard/server/asset_handler.rb +28 -2
  77. data/lib/docyard/server/dev_server.rb +56 -13
  78. data/lib/docyard/server/error_overlay.rb +73 -0
  79. data/lib/docyard/server/file_watcher.rb +10 -6
  80. data/lib/docyard/server/page_diagnostics.rb +27 -0
  81. data/lib/docyard/server/preview_server.rb +17 -13
  82. data/lib/docyard/server/rack_application.rb +65 -4
  83. data/lib/docyard/server/resolution_result.rb +0 -4
  84. data/lib/docyard/templates/assets/css/error-overlay.css +669 -0
  85. data/lib/docyard/templates/assets/css/variables.css +1 -7
  86. data/lib/docyard/templates/assets/fonts/Inter-Variable.woff2 +0 -0
  87. data/lib/docyard/templates/assets/js/components/abbreviation.js +20 -11
  88. data/lib/docyard/templates/assets/js/components/code-block.js +8 -3
  89. data/lib/docyard/templates/assets/js/components/code-group.js +20 -3
  90. data/lib/docyard/templates/assets/js/components/file-tree.js +9 -3
  91. data/lib/docyard/templates/assets/js/components/heading-anchor.js +71 -72
  92. data/lib/docyard/templates/assets/js/components/lightbox.js +10 -3
  93. data/lib/docyard/templates/assets/js/components/relative-time.js +42 -0
  94. data/lib/docyard/templates/assets/js/components/tabs.js +8 -3
  95. data/lib/docyard/templates/assets/js/components/tooltip.js +32 -23
  96. data/lib/docyard/templates/assets/js/error-overlay.js +547 -0
  97. data/lib/docyard/templates/assets/js/hot-reload.js +77 -9
  98. data/lib/docyard/templates/errors/404.html.erb +1 -1
  99. data/lib/docyard/templates/errors/500.html.erb +1 -1
  100. data/lib/docyard/templates/partials/_head.html.erb +5 -1
  101. data/lib/docyard/templates/partials/_page_actions.html.erb +1 -1
  102. data/lib/docyard/templates/partials/_scripts.html.erb +3 -0
  103. data/lib/docyard/ui.rb +80 -0
  104. data/lib/docyard/utils/logging.rb +5 -1
  105. data/lib/docyard/utils/text_formatter.rb +0 -6
  106. data/lib/docyard/version.rb +1 -1
  107. data/lib/docyard.rb +6 -0
  108. metadata +53 -25
  109. data/lib/docyard/config/key_validator.rb +0 -30
  110. data/lib/docyard/config/validation_helpers.rb +0 -83
  111. data/lib/docyard/config/validators/navigation.rb +0 -43
  112. data/lib/docyard/config/validators/section.rb +0 -114
  113. data/lib/docyard/templates/assets/fonts/Inter-Variable.ttf +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69c121af95a08c379f260d0d444aa4bc5272fdfe2d0699866091885c9f503601
4
- data.tar.gz: 4550cdcc1d6863b545c4900aac9adb10f14d96a7394ccd3fe4c8924969b278dc
3
+ metadata.gz: fe9582d35d963462ff818674735f3b131607368f6adc6fdb70eac6152365efe8
4
+ data.tar.gz: a8e8b424702b4ca3ff2726a938ea0696f0b15bed3d82f449f803f64923bbbafc
5
5
  SHA512:
6
- metadata.gz: 690e11d0792b71540f09d26fe45dc46d57558b3aad1b3cf80f3e08c592a4056408b6b8a57bfc47f2459feba94e481038f11ccef287534a9f82e557c255728b53
7
- data.tar.gz: 8baa1a707f2ff94152499b66c19596a57984c5ff763d2cf43390d986030e2e2eea994febac3089766a764ec1d6314a61479fe2caa54434ee5e64c86ade6431e1
6
+ metadata.gz: 4c6ae9f9e88f186d43aa13ef62c2c96d3c4ed65e0320558413df73915bd33478b673dae25dc4f3e3fdf6b0fe627ca595a8fc53cded6186ed3afb03ae08ef7f5d
7
+ data.tar.gz: 17ad431713b55f78b87a67b5de17db1e58c949895a08ff9d31b6f658aa277a8df8cab84f3d3ee178f9c70a9eb30557089fc0b732e2793b6c9b3c8054bf513b1c
data/CHANGELOG.md CHANGED
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.0] - 2026-02-03
11
+
12
+ ### Added
13
+ - **Social Cards** - Auto-generate Open Graph images for social sharing with `social_cards.enabled: true` config (#146)
14
+ - **Customize Command** - Generate theme customization files with `docyard customize` for custom CSS variables and scripts (#147)
15
+ - **Pagefind Standalone Binary** - Download and cache Pagefind binary in `~/.docyard/bin/` for faster, more reliable search indexing without npx (#148)
16
+
17
+ ### Fixed
18
+ - **Hot Reload JS Reinitialization** - Components now properly reinitialize when content changes during development (#149)
19
+
20
+ ### Documentation
21
+ - Added new pages: Abbreviations component, Docs for AI (llms.txt)
22
+ - Removed Troubleshooting page
23
+ - Removed unsupported bluesky platform (33 platforms now supported)
24
+
25
+ ## [1.1.0] - 2026-01-30
26
+
27
+ ### Added
28
+ - **Doctor Command** - Health check CLI with `docyard doctor` to find broken links, missing images, orphan pages, and config issues (#135)
29
+ - **Auto-fix** - Automatically fix common issues with `docyard doctor --fix` including typos, string booleans, and missing slashes (#135)
30
+ - **Config Validation** - Validate `docyard.yml` and `_sidebar.yml` with helpful error messages and suggestions (#138)
31
+ - **Dev Server Error Overlay** - Bottom sheet overlay showing errors with syntax-highlighted code snippets, line numbers, and clickable file paths (#143)
32
+ - **Strict Mode** - Fail builds on validation errors with `--strict` flag or `build.strict` config (#143)
33
+ - **Colored CLI Output** - ANSI colors for better readability with `--no-color` flag and `NO_COLOR` env support (#142)
34
+ - **CLI Progress Spinners** - Animated spinners and step-by-step progress display for build and serve commands (#136)
35
+ - **Verbose Timing Breakdown** - Per-page timing in `docyard build --verbose` output (#141)
36
+
37
+ ### Fixed
38
+ - "Last updated" timestamps now calculate relative time client-side for accurate display (#134)
39
+
40
+ ### Changed
41
+ - Build output format updated with progress indicators (#136, #141)
42
+ - Config validation now runs on every build (#138)
43
+
44
+ ### Documentation
45
+ - Added `docyard doctor` command reference
46
+ - Added strict mode documentation to CLI and configuration references
47
+ - Added error overlay mention to dev server documentation
48
+ - Updated build output examples to match current format
49
+
10
50
  ## [1.0.2] - 2026-01-23
11
51
 
12
52
  ### Fixed
@@ -224,7 +264,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
224
264
  - Initial gem structure
225
265
  - Project scaffolding
226
266
 
227
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.0.2...HEAD
267
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.2.0...HEAD
268
+ [1.2.0]: https://github.com/sanifhimani/docyard/compare/v1.1.0...v1.2.0
269
+ [1.1.0]: https://github.com/sanifhimani/docyard/compare/v1.0.2...v1.1.0
228
270
  [1.0.2]: https://github.com/sanifhimani/docyard/compare/v1.0.1...v1.0.2
229
271
  [1.0.1]: https://github.com/sanifhimani/docyard/compare/v1.0.0...v1.0.1
230
272
  [1.0.0]: https://github.com/sanifhimani/docyard/compare/v0.9.0...v1.0.0
data/README.md CHANGED
@@ -1,28 +1,73 @@
1
- # Docyard
1
+ <p align="center">
2
+ <a href="https://docyard.dev">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/public/logo-dark.svg">
5
+ <img src="docs/public/logo.svg" height="60" alt="Docyard">
6
+ </picture>
7
+ </a>
8
+ </p>
2
9
 
3
- [![CI](https://github.com/sanifhimani/docyard/actions/workflows/ci.yml/badge.svg)](https://github.com/sanifhimani/docyard/actions/workflows/ci.yml)
4
- [![Gem Version](https://badge.fury.io/rb/docyard.svg)](https://badge.fury.io/rb/docyard)
10
+ <p align="center">
11
+ Markdown to docs in seconds. No Node.js required.
12
+ </p>
5
13
 
6
- Beautiful documentation sites from Markdown. Fast, simple, no configuration required.
14
+ <p align="center">
15
+ <a href="https://github.com/sanifhimani/docyard/actions/workflows/ci.yml"><img src="https://github.com/sanifhimani/docyard/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
16
+ <a href="https://badge.fury.io/rb/docyard"><img src="https://badge.fury.io/rb/docyard.svg" alt="Gem Version"></a>
17
+ <a href="https://github.com/sanifhimani/docyard/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License"></a>
18
+ </p>
7
19
 
8
- ## Quick Start
20
+ <p align="center">
21
+ <a href="https://docyard.dev">Docs</a> ·
22
+ <a href="https://docyard.dev/getting-started/quickstart">Quickstart</a> ·
23
+ <a href="https://github.com/sanifhimani/docyard/blob/main/CHANGELOG.md">Changelog</a>
24
+ </p>
25
+
26
+ ---
27
+
28
+ ## Install
9
29
 
10
30
  ```bash
11
31
  gem install docyard
12
- docyard init my-docs
13
- cd my-docs
32
+ docyard init
14
33
  docyard serve
15
34
  ```
16
35
 
17
- Open http://localhost:4200 and start writing.
36
+ Open `localhost:4200`. Edits reload instantly.
37
+
38
+ ## Example
39
+
40
+ ```markdown
41
+ :::note
42
+ Requires Ruby 3.2 or higher.
43
+ :::
44
+
45
+ :::tabs
46
+ == macOS
47
+ brew install ruby
48
+
49
+ == Linux
50
+ sudo apt install ruby-full
51
+ :::
52
+ ```
53
+
54
+ Callouts, tabs, steps, cards, code groups, accordions, and more. [See all components](https://docyard.dev/write-content/components)
55
+
56
+ ## Build
57
+
58
+ ```bash
59
+ docyard build
60
+ ```
61
+
62
+ Static HTML, search index, sitemap, and social cards in `dist/`. Deploy anywhere.
18
63
 
19
- ## Documentation
64
+ ## Docs
20
65
 
21
- Visit [docyard.dev](https://docyard.dev) for the full documentation.
66
+ [docyard.dev](https://docyard.dev) is built with Docyard.
22
67
 
23
68
  ## Contributing
24
69
 
25
- See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
70
+ See [CONTRIBUTING.md](CONTRIBUTING.md).
26
71
 
27
72
  ## License
28
73
 
@@ -7,6 +7,8 @@ require "digest"
7
7
  module Docyard
8
8
  module Build
9
9
  class AssetBundler
10
+ include Utils::UrlHelpers
11
+
10
12
  ASSETS_PATH = File.join(__dir__, "..", "templates", "assets")
11
13
 
12
14
  attr_reader :config, :verbose
@@ -17,23 +19,20 @@ module Docyard
17
19
  end
18
20
 
19
21
  def bundle
20
- Docyard.logger.info("\nBundling assets...")
21
-
22
- css_hash = bundle_css
23
- js_hash = bundle_js
22
+ css_hash, css_size = bundle_css
23
+ js_hash, js_size = bundle_js
24
24
 
25
25
  update_html_references(css_hash, js_hash)
26
26
 
27
- 2
27
+ [css_size, js_size]
28
28
  end
29
29
 
30
30
  private
31
31
 
32
32
  def bundle_css
33
- log " Bundling CSS..."
34
-
35
33
  main_css = File.read(File.join(ASSETS_PATH, "css", "main.css"))
36
34
  css_content = resolve_css_imports(main_css)
35
+ css_content = append_custom_css(css_content)
37
36
  minified = CSSminify.compress(css_content)
38
37
  minified = fix_calc_whitespace(minified)
39
38
  minified = fix_css_math_functions(minified)
@@ -41,9 +40,8 @@ module Docyard
41
40
  hash = generate_hash(minified)
42
41
 
43
42
  write_bundled_asset(minified, hash, "css")
44
- log_compression_stats(css_content, minified, "CSS")
45
43
 
46
- hash
44
+ [hash, minified.bytesize]
47
45
  end
48
46
 
49
47
  def fix_calc_whitespace(css)
@@ -86,19 +84,17 @@ module Docyard
86
84
  end
87
85
 
88
86
  def bundle_js
89
- log " Bundling JS..."
90
-
91
87
  theme_js = File.read(File.join(ASSETS_PATH, "js", "theme.js"))
92
88
  components_js = concatenate_component_js
93
- js_content = [theme_js, components_js].join("\n")
89
+ custom_js = load_custom_js
90
+ js_content = [theme_js, components_js, custom_js].compact.join("\n")
94
91
  minified = Terser.compile(js_content)
95
92
  minified = replace_js_asset_urls(minified)
96
93
  hash = generate_hash(minified)
97
94
 
98
95
  write_bundled_asset(minified, hash, "js")
99
- log_compression_stats(js_content, minified, "JS")
100
96
 
101
- hash
97
+ [hash, minified.bytesize]
102
98
  end
103
99
 
104
100
  def replace_js_asset_urls(js_content)
@@ -127,8 +123,6 @@ module Docyard
127
123
  content = replace_asset_references(File.read(file), css_hash, js_hash, base_url)
128
124
  File.write(file, content)
129
125
  end
130
-
131
- log " [✓] Updated asset references in #{html_files.size} HTML files"
132
126
  end
133
127
 
134
128
  def replace_asset_references(content, css_hash, js_hash, base_url)
@@ -155,22 +149,18 @@ module Docyard
155
149
  File.write(output_path, content)
156
150
  end
157
151
 
158
- def log_compression_stats(original, minified, label)
159
- original_size = (original.bytesize / 1024.0).round(1)
160
- minified_size = (minified.bytesize / 1024.0).round(1)
161
- reduction = (((original_size - minified_size) / original_size) * 100).round(0)
162
- log " [✓] #{label}: #{original_size} KB -> #{minified_size} KB (-#{reduction}%)"
163
- end
164
-
165
- def normalize_base_url(url)
166
- return "/" if url.nil? || url.empty? || url == "/"
152
+ def append_custom_css(css_content)
153
+ custom_path = File.join(config.source, "_custom", "styles.css")
154
+ return css_content unless File.exist?(custom_path)
167
155
 
168
- url = "/#{url}" unless url.start_with?("/")
169
- url.end_with?("/") ? url : "#{url}/"
156
+ "#{css_content}\n#{File.read(custom_path)}"
170
157
  end
171
158
 
172
- def log(message)
173
- Docyard.logger.info(message)
159
+ def load_custom_js
160
+ custom_path = File.join(config.source, "_custom", "scripts.js")
161
+ return nil unless File.exist?(custom_path)
162
+
163
+ File.read(custom_path)
174
164
  end
175
165
  end
176
166
  end
@@ -13,14 +13,12 @@ module Docyard
13
13
  end
14
14
 
15
15
  def copy
16
- Docyard.logger.info("\nCopying static assets...")
17
-
16
+ @copied_files = []
18
17
  count = 0
19
18
  count += copy_public_files
20
19
  count += copy_branding_assets
21
-
22
- log "[✓] Copied #{count} static files"
23
- count
20
+ details = verbose ? @copied_files : nil
21
+ [count, details]
24
22
  end
25
23
 
26
24
  private
@@ -32,7 +30,6 @@ module Docyard
32
30
  files = find_files_in_dir(public_dir)
33
31
  files.each { |file| copy_single_file(file, "#{public_dir}/", config.build.output) }
34
32
 
35
- log "[✓] Copied #{files.size} public files from #{public_dir}/" if files.any?
36
33
  files.size
37
34
  end
38
35
 
@@ -47,14 +44,13 @@ module Docyard
47
44
  FileUtils.mkdir_p(File.dirname(dest_path))
48
45
  FileUtils.cp(file, dest_path)
49
46
 
50
- log " Copied: #{relative_path}" if verbose
47
+ @copied_files << relative_path if verbose
51
48
  end
52
49
 
53
50
  def copy_branding_assets
54
51
  count = 0
55
52
  count += copy_default_branding_assets
56
53
  count += copy_user_branding_assets
57
- log "[✓] Copied #{count} branding assets" if count.positive?
58
54
  count
59
55
  end
60
56
 
@@ -80,7 +76,7 @@ module Docyard
80
76
  dest_path = File.join(config.build.output, DOCYARD_OUTPUT_DIR, filename)
81
77
  FileUtils.mkdir_p(File.dirname(dest_path))
82
78
  FileUtils.cp(source_path, dest_path)
83
- log " Copied #{label}: #{filename}" if verbose
79
+ @copied_files << "#{filename} (#{label})" if verbose
84
80
  1
85
81
  end
86
82
 
@@ -96,7 +92,7 @@ module Docyard
96
92
  dest_path = File.join(config.build.output, DOCYARD_OUTPUT_DIR, "fonts", File.basename(font_file))
97
93
  FileUtils.mkdir_p(File.dirname(dest_path))
98
94
  FileUtils.cp(font_file, dest_path)
99
- log " Copied font: #{File.basename(font_file)}" if verbose
95
+ @copied_files << "#{File.basename(font_file)} (font)" if verbose
100
96
  1
101
97
  end
102
98
 
@@ -122,13 +118,9 @@ module Docyard
122
118
  FileUtils.mkdir_p(File.dirname(dest_path))
123
119
  FileUtils.cp(full_path, dest_path)
124
120
 
125
- log " Copied user branding: #{asset_path}" if verbose
121
+ @copied_files << "#{asset_path} (user branding)" if verbose
126
122
  1
127
123
  end
128
-
129
- def log(message)
130
- Docyard.logger.info(message)
131
- end
132
124
  end
133
125
  end
134
126
  end
@@ -50,7 +50,6 @@ module Docyard
50
50
  def generate_llms_txt(pages)
51
51
  output_path = File.join(config.build.output, "llms.txt")
52
52
  File.write(output_path, build_llms_txt_content(pages))
53
- Docyard.logger.info("[✓] Generated llms.txt (#{pages.size} pages)")
54
53
  end
55
54
 
56
55
  def build_llms_txt_content(pages)
@@ -70,7 +69,6 @@ module Docyard
70
69
  def generate_llms_full_txt(pages)
71
70
  output_path = File.join(config.build.output, "llms-full.txt")
72
71
  File.write(output_path, build_llms_full_txt_content(pages))
73
- Docyard.logger.info("[✓] Generated llms-full.txt")
74
72
  end
75
73
 
76
74
  def build_llms_full_txt_content(pages)
@@ -18,7 +18,7 @@ module Docyard
18
18
  output_path = File.join(config.build.output, "sitemap.xml")
19
19
  File.write(output_path, sitemap_content)
20
20
 
21
- Docyard.logger.info("[✓] Generated sitemap.xml (#{urls.size} URLs)")
21
+ urls.size
22
22
  end
23
23
 
24
24
  private
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Build
5
+ module SocialCards
6
+ class CardRenderer
7
+ VIPS_INSTALL_INSTRUCTIONS = <<~MSG.strip
8
+ Social cards require libvips to be installed.
9
+
10
+ Install libvips:
11
+ macOS: brew install vips
12
+ Ubuntu: sudo apt install libvips-dev
13
+ Fedora: sudo dnf install vips-devel
14
+
15
+ Then run: bundle install
16
+
17
+ Or disable social cards in docyard.yml:
18
+ social_cards:
19
+ enabled: false
20
+ MSG
21
+ WIDTH = 1200
22
+ HEIGHT = 630
23
+ BACKGROUND_COLOR = "#121212"
24
+ DEFAULT_BRAND_COLOR = "#22D3EE"
25
+ WHITE = "#FFFFFF"
26
+ GRAY = "#71717A"
27
+
28
+ PADDING = 88
29
+ LOGO_BOTTOM_OFFSET = 88
30
+
31
+ LOGO_ICON_WIDTH = 40
32
+ LOGO_ICON_HEIGHT = 58
33
+ LOGO_TEXT_SIZE = 28
34
+ LOGO_GAP = 16
35
+
36
+ attr_reader :config
37
+
38
+ def initialize(config)
39
+ @config = config
40
+ end
41
+
42
+ def render(output_path)
43
+ svg_content = build_svg
44
+ save_as_png(svg_content, output_path)
45
+ end
46
+
47
+ protected
48
+
49
+ def build_svg
50
+ <<~SVG
51
+ <svg xmlns="http://www.w3.org/2000/svg" width="#{WIDTH}" height="#{HEIGHT}" viewBox="0 0 #{WIDTH} #{HEIGHT}">
52
+ <defs>
53
+ <style>
54
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&amp;display=swap');
55
+ .inter { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
56
+ </style>
57
+ </defs>
58
+ <rect width="#{WIDTH}" height="#{HEIGHT}" fill="#{BACKGROUND_COLOR}"/>
59
+ #{content_svg}
60
+ #{logo_svg}
61
+ </svg>
62
+ SVG
63
+ end
64
+
65
+ def content_svg
66
+ raise NotImplementedError, "Subclasses must implement content_svg"
67
+ end
68
+
69
+ def logo_svg
70
+ site_title = escape_xml(config.title || "Docyard")
71
+ logo_y = HEIGHT - LOGO_BOTTOM_OFFSET
72
+ logo_x, text_anchor = logo_position_and_anchor
73
+
74
+ <<~SVG
75
+ <g transform="translate(#{logo_x}, #{logo_y})">
76
+ #{logo_icon_svg(0, -LOGO_ICON_HEIGHT)}
77
+ <text x="#{LOGO_ICON_WIDTH + LOGO_GAP}" y="-#{(LOGO_ICON_HEIGHT / 2) - 10}" class="inter" font-size="#{LOGO_TEXT_SIZE}" font-weight="700" fill="#{WHITE}" text-anchor="#{text_anchor}">#{site_title}</text>
78
+ </g>
79
+ SVG
80
+ end
81
+
82
+ def logo_position_and_anchor
83
+ [PADDING, "start"]
84
+ end
85
+
86
+ def logo_icon_svg(x_offset, y_offset)
87
+ color = brand_color
88
+ scale = LOGO_ICON_WIDTH.to_f / 531
89
+ <<~SVG
90
+ <g transform="translate(#{x_offset}, #{y_offset}) scale(#{scale})">
91
+ <path fill="#{color}" d="M359.643 59.1798C402.213 89.4398 449.713 123.6 502.063 160.99C510.793 167.23 515.873 170.31 519.293 178.05C523.253 187.02 521.733 198.11 515.883 205.77C513.77 208.536 510.93 211.2 507.363 213.76C379.643 305.353 309.413 355.73 296.673 364.89C287.987 371.136 282.07 374.8 278.923 375.88C269.703 379.026 260.263 378.636 250.603 374.71C248.243 373.75 244.497 371.416 239.363 367.71C199.963 339.29 177.32 322.99 171.433 318.81C128.863 288.54 81.3733 254.39 29.0233 216.99C20.2833 210.75 15.2033 207.67 11.7833 199.93C7.82332 190.96 9.34332 179.87 15.1933 172.21C17.3067 169.443 20.1467 166.78 23.7133 164.22C151.433 72.6264 221.663 22.2498 234.403 13.0898C243.09 6.84309 249.007 3.17976 252.153 2.09976C261.373 -1.04691 270.813 -0.656912 280.473 3.26976C282.833 4.22976 286.58 6.56309 291.713 10.2698C331.113 38.6898 353.757 54.9931 359.643 59.1798Z"/>
92
+ <path fill="#{WHITE}" d="M467.383 298.01C483.943 286.23 505.033 289.93 519.063 303.51C524.457 308.723 528.033 314.713 529.793 321.48C530.433 323.92 530.733 330.946 530.693 342.56C530.647 356.206 530.657 427.233 530.723 555.64C530.723 566.633 530.513 573 530.093 574.74C527.033 587.29 518.333 592.61 506.693 601.06C504.313 602.786 430.877 656.346 286.383 761.74C275.623 769.59 261.793 770.79 250.113 764.36C249.18 763.846 245.86 761.513 240.153 757.36C150.56 692.066 74.8667 637.046 13.0733 592.3C6.70001 587.68 2.65667 581.73 0.943337 574.45C0.316671 571.783 0.00333476 564.803 0.00333476 553.51C-0.00333191 421.323 -4.06895e-06 348.98 0.0133293 336.48C0.0133293 332.84 -0.0766665 327.18 0.783334 323.18C4.59333 305.51 20.1033 293.29 37.4533 291.15C42.9467 290.476 48.8667 291.276 55.2133 293.55C58.28 294.643 63.3533 297.8 70.4333 303.02C75.98 307.113 82.4433 311.78 89.8233 317.02C128.563 344.526 178.703 380.303 240.243 424.35C242.73 426.13 245.853 428.246 249.613 430.7C257.443 435.8 268.453 436.24 277.213 433.14C279.8 432.22 284.54 429.283 291.433 424.33C394.46 350.276 453.11 308.17 467.383 298.01Z"/>
93
+ </g>
94
+ SVG
95
+ end
96
+
97
+ def brand_color
98
+ color = config.branding.color
99
+ if color.is_a?(Hash)
100
+ color["dark"] || color["light"] || DEFAULT_BRAND_COLOR
101
+ elsif color.is_a?(String) && !color.strip.empty?
102
+ color.strip
103
+ else
104
+ DEFAULT_BRAND_COLOR
105
+ end
106
+ end
107
+
108
+ def escape_xml(text)
109
+ text.to_s
110
+ .gsub("&", "&amp;")
111
+ .gsub("<", "&lt;")
112
+ .gsub(">", "&gt;")
113
+ .gsub('"', "&quot;")
114
+ .gsub("'", "&apos;")
115
+ end
116
+
117
+ def save_as_png(svg_content, output_path)
118
+ require_vips!
119
+ FileUtils.mkdir_p(File.dirname(output_path))
120
+ image = Vips::Image.svgload_buffer(svg_content, dpi: 96)
121
+ image.write_to_file(output_path, compression: 9, palette: true)
122
+ end
123
+
124
+ def require_vips!
125
+ require "vips"
126
+ rescue LoadError
127
+ raise Docyard::Error, VIPS_INSTALL_INSTRUCTIONS
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "card_renderer"
4
+
5
+ module Docyard
6
+ module Build
7
+ module SocialCards
8
+ class DocCard < CardRenderer
9
+ SECTION_LABEL_SIZE = 26
10
+ TITLE_SIZE = 92
11
+ DESCRIPTION_SIZE = 30
12
+
13
+ SECTION_TO_TITLE_GAP = 16
14
+ TITLE_TO_DESC_GAP = 24
15
+
16
+ LOGO_AREA_HEIGHT = 160
17
+
18
+ TITLE_MAX_CHARS = 22
19
+ DESCRIPTION_MAX_CHARS = 70
20
+
21
+ def initialize(config, title:, section: nil, description: nil)
22
+ super(config)
23
+ @title = truncate_text(title, TITLE_MAX_CHARS)
24
+ @section = section
25
+ @description = truncate_text(description, DESCRIPTION_MAX_CHARS)
26
+ end
27
+
28
+ protected
29
+
30
+ def content_svg
31
+ start_y = calculate_start_y
32
+ elements = []
33
+ y_pos = start_y
34
+
35
+ if @section && !@section.empty?
36
+ elements << section_svg(y_pos)
37
+ y_pos += SECTION_TO_TITLE_GAP + TITLE_SIZE
38
+ else
39
+ y_pos += TITLE_SIZE
40
+ end
41
+
42
+ elements << title_svg(y_pos)
43
+
44
+ if @description && !@description.empty?
45
+ y_pos += TITLE_TO_DESC_GAP + DESCRIPTION_SIZE
46
+ elements << description_svg(y_pos)
47
+ end
48
+
49
+ elements.join("\n")
50
+ end
51
+
52
+ private
53
+
54
+ def calculate_start_y
55
+ total_height = calculate_content_height
56
+ available_height = HEIGHT - LOGO_AREA_HEIGHT
57
+ (available_height - total_height) / 2
58
+ end
59
+
60
+ def calculate_content_height
61
+ height = TITLE_SIZE
62
+ height += SECTION_LABEL_SIZE + SECTION_TO_TITLE_GAP if @section && !@section.empty?
63
+ height += TITLE_TO_DESC_GAP + DESCRIPTION_SIZE if @description && !@description.empty?
64
+ height
65
+ end
66
+
67
+ def section_svg(y_pos)
68
+ section_text = escape_xml(@section.upcase)
69
+ <<~SVG
70
+ <text x="#{PADDING}" y="#{y_pos}" class="inter" font-size="#{SECTION_LABEL_SIZE}" font-weight="600" fill="#{brand_color}" letter-spacing="0.5">#{section_text}</text>
71
+ SVG
72
+ end
73
+
74
+ def title_svg(y_pos)
75
+ title_text = escape_xml(@title)
76
+ <<~SVG
77
+ <text x="#{PADDING}" y="#{y_pos}" class="inter" font-size="#{TITLE_SIZE}" font-weight="800" fill="#{WHITE}" letter-spacing="-0.02em">#{title_text}</text>
78
+ SVG
79
+ end
80
+
81
+ def description_svg(y_pos)
82
+ desc_text = escape_xml(@description)
83
+ <<~SVG
84
+ <text x="#{PADDING}" y="#{y_pos}" class="inter" font-size="#{DESCRIPTION_SIZE}" font-weight="400" fill="#{GRAY}">#{desc_text}</text>
85
+ SVG
86
+ end
87
+
88
+ def truncate_text(text, max_chars)
89
+ return nil if text.nil?
90
+ return text if text.length <= max_chars
91
+
92
+ "#{text[0, max_chars - 3].strip}..."
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end