docyard 1.1.0 → 1.3.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/README.md +56 -11
  4. data/lib/docyard/build/asset_bundler.rb +17 -1
  5. data/lib/docyard/build/social_cards/card_renderer.rb +132 -0
  6. data/lib/docyard/build/social_cards/doc_card.rb +97 -0
  7. data/lib/docyard/build/social_cards/homepage_card.rb +98 -0
  8. data/lib/docyard/build/social_cards_generator.rb +188 -0
  9. data/lib/docyard/build/step_runner.rb +2 -0
  10. data/lib/docyard/builder.rb +10 -0
  11. data/lib/docyard/cli.rb +44 -0
  12. data/lib/docyard/config/branding_resolver.rb +1 -1
  13. data/lib/docyard/config/schema/definition.rb +2 -1
  14. data/lib/docyard/config/schema/sections.rb +0 -1
  15. data/lib/docyard/config/schema/simple_sections.rb +7 -0
  16. data/lib/docyard/config.rb +3 -1
  17. data/lib/docyard/customizer.rb +196 -0
  18. data/lib/docyard/deploy/adapters/base.rb +56 -0
  19. data/lib/docyard/deploy/adapters/cloudflare.rb +38 -0
  20. data/lib/docyard/deploy/adapters/github_pages.rb +60 -0
  21. data/lib/docyard/deploy/adapters/netlify.rb +37 -0
  22. data/lib/docyard/deploy/adapters/vercel.rb +36 -0
  23. data/lib/docyard/deploy/deployer.rb +95 -0
  24. data/lib/docyard/deploy/platform_detector.rb +32 -0
  25. data/lib/docyard/errors.rb +2 -0
  26. data/lib/docyard/rendering/markdown.rb +4 -0
  27. data/lib/docyard/rendering/og_helpers.rb +19 -1
  28. data/lib/docyard/rendering/renderer.rb +10 -1
  29. data/lib/docyard/search/build_indexer.rb +8 -3
  30. data/lib/docyard/search/dev_indexer.rb +3 -2
  31. data/lib/docyard/search/pagefind_binary.rb +185 -0
  32. data/lib/docyard/search/pagefind_support.rb +9 -7
  33. data/lib/docyard/server/asset_handler.rb +28 -2
  34. data/lib/docyard/server/dev_server.rb +1 -0
  35. data/lib/docyard/server/file_watcher.rb +10 -5
  36. data/lib/docyard/server/rack_application.rb +1 -1
  37. data/lib/docyard/templates/assets/css/variables.css +0 -6
  38. data/lib/docyard/templates/assets/js/components/abbreviation.js +20 -11
  39. data/lib/docyard/templates/assets/js/components/code-block.js +8 -3
  40. data/lib/docyard/templates/assets/js/components/code-group.js +20 -3
  41. data/lib/docyard/templates/assets/js/components/file-tree.js +9 -3
  42. data/lib/docyard/templates/assets/js/components/heading-anchor.js +71 -72
  43. data/lib/docyard/templates/assets/js/components/lightbox.js +10 -3
  44. data/lib/docyard/templates/assets/js/components/tabs.js +8 -3
  45. data/lib/docyard/templates/assets/js/components/tooltip.js +32 -23
  46. data/lib/docyard/templates/assets/js/hot-reload.js +42 -2
  47. data/lib/docyard/templates/partials/_head.html.erb +4 -0
  48. data/lib/docyard/templates/partials/_scripts.html.erb +3 -0
  49. data/lib/docyard/version.rb +1 -1
  50. data/lib/docyard.rb +2 -0
  51. metadata +14 -1
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "yaml"
5
+ require "parallel"
6
+ require_relative "social_cards/card_renderer"
7
+ require_relative "social_cards/homepage_card"
8
+ require_relative "social_cards/doc_card"
9
+
10
+ module Docyard
11
+ module Build
12
+ class SocialCardsGenerator
13
+ OUTPUT_DIR = "_docyard/og"
14
+ FRONTMATTER_REGEX = /\A---\s*\n(.*?\n)---\s*\n/m
15
+ HOMEPAGE_PATHS = ["/", "/index"].freeze
16
+ PARALLEL_THRESHOLD = 5
17
+
18
+ attr_reader :config, :verbose
19
+
20
+ def initialize(config, verbose: false)
21
+ @config = config
22
+ @verbose = verbose
23
+ @generated_cards = []
24
+ @failed_cards = []
25
+ end
26
+
27
+ def generate
28
+ ensure_vips_available!
29
+ pages = collect_pages
30
+ generate_cards_for(pages)
31
+ [successful_count, build_verbose_details]
32
+ end
33
+
34
+ def successful_count
35
+ @generated_cards.compact.size
36
+ end
37
+
38
+ def card_path_for(url_path)
39
+ File.join("/", OUTPUT_DIR, url_to_filename(url_path))
40
+ end
41
+
42
+ private
43
+
44
+ def generate_cards_for(pages)
45
+ if pages.size >= PARALLEL_THRESHOLD
46
+ results = Parallel.map(pages, in_threads: Parallel.processor_count) do |page|
47
+ generate_card(page)
48
+ end
49
+ @generated_cards = results.compact
50
+ else
51
+ pages.each { |page| @generated_cards << generate_card(page) }
52
+ end
53
+ end
54
+
55
+ def collect_pages
56
+ pages = []
57
+
58
+ Dir.glob(File.join(docs_path, "**", "*.md")).each do |file_path|
59
+ frontmatter = extract_frontmatter(file_path)
60
+ relative_path = file_path.delete_prefix("#{docs_path}/")
61
+ url_path = markdown_to_url(relative_path)
62
+
63
+ pages << build_page_data(file_path, url_path, frontmatter)
64
+ end
65
+
66
+ pages
67
+ end
68
+
69
+ def build_page_data(file_path, url_path, frontmatter)
70
+ social_cards_config = frontmatter["social_cards"] || {}
71
+ section = derive_section(url_path)
72
+
73
+ {
74
+ file_path: file_path,
75
+ url_path: url_path,
76
+ title: resolve_title(social_cards_config, frontmatter, url_path),
77
+ description: social_cards_config["description"] || frontmatter["description"],
78
+ section: section,
79
+ is_homepage: homepage?(url_path)
80
+ }
81
+ end
82
+
83
+ def resolve_title(social_cards_config, frontmatter, url_path)
84
+ title = social_cards_config["title"]
85
+ return title if title && !title.strip.empty?
86
+
87
+ title = frontmatter["title"]
88
+ return title if title && !title.strip.empty?
89
+
90
+ derive_title_from_path(url_path)
91
+ end
92
+
93
+ def ensure_vips_available!
94
+ require "vips"
95
+ rescue LoadError
96
+ raise Docyard::Error, SocialCards::CardRenderer::VIPS_INSTALL_INSTRUCTIONS
97
+ end
98
+
99
+ def generate_card(page)
100
+ output_path = File.join(config.build.output, OUTPUT_DIR, url_to_filename(page[:url_path]))
101
+ card = build_card_for(page)
102
+ card.render(output_path)
103
+ output_path
104
+ rescue StandardError => e
105
+ log_card_error(page, e.message)
106
+ nil
107
+ end
108
+
109
+ def log_card_error(page, message)
110
+ @failed_cards << page[:url_path]
111
+ Docyard.logger.warn("Social card generation failed for #{page[:url_path]}: #{message}")
112
+ end
113
+
114
+ def build_card_for(page)
115
+ if page[:is_homepage]
116
+ SocialCards::HomepageCard.new(config, title: page[:title])
117
+ else
118
+ SocialCards::DocCard.new(
119
+ config,
120
+ title: page[:title],
121
+ section: page[:section],
122
+ description: page[:description]
123
+ )
124
+ end
125
+ end
126
+
127
+ def extract_frontmatter(file_path)
128
+ content = File.read(file_path)
129
+ match = content.match(FRONTMATTER_REGEX)
130
+ return {} unless match
131
+
132
+ YAML.safe_load(match[1]) || {}
133
+ rescue Psych::SyntaxError
134
+ {}
135
+ end
136
+
137
+ def markdown_to_url(relative_path)
138
+ path = relative_path
139
+ .delete_suffix(".md")
140
+ .delete_suffix("/index")
141
+
142
+ path = "index" if path == "index" || path.empty?
143
+ "/#{path}"
144
+ end
145
+
146
+ def url_to_filename(url_path)
147
+ path = url_path.delete_prefix("/")
148
+ path = "index" if path.empty?
149
+ "#{path}.png"
150
+ end
151
+
152
+ def homepage?(url_path)
153
+ HOMEPAGE_PATHS.include?(url_path)
154
+ end
155
+
156
+ def derive_section(url_path)
157
+ parts = url_path.delete_prefix("/").split("/")
158
+ return nil if parts.size <= 1
159
+
160
+ parts.first.tr("-", " ").split.map(&:capitalize).join(" ")
161
+ end
162
+
163
+ def derive_title_from_path(url_path)
164
+ filename = File.basename(url_path)
165
+ filename = "Home" if filename == "index" || filename.empty?
166
+ filename.tr("-", " ").split.map(&:capitalize).join(" ")
167
+ end
168
+
169
+ def docs_path
170
+ config.source
171
+ end
172
+
173
+ def build_verbose_details
174
+ return nil unless verbose
175
+
176
+ details = @generated_cards.compact.map do |path|
177
+ path.delete_prefix("#{config.build.output}/")
178
+ end
179
+
180
+ @failed_cards.each do |url_path|
181
+ details << "FAILED: #{url_path}"
182
+ end
183
+
184
+ details
185
+ end
186
+ end
187
+ end
188
+ end
@@ -5,6 +5,7 @@ module Docyard
5
5
  class StepRunner
6
6
  STEP_SHORT_LABELS = {
7
7
  "Generating pages" => "Pages",
8
+ "Social cards" => "Cards",
8
9
  "Bundling assets" => "Assets",
9
10
  "Copying files" => "Files",
10
11
  "Generating SEO" => "SEO",
@@ -59,6 +60,7 @@ module Docyard
59
60
  def format_result(label, result)
60
61
  case label
61
62
  when "Generating pages" then "done (#{result} pages)"
63
+ when "Social cards" then "done (#{result} cards)"
62
64
  when "Bundling assets" then format_assets_result(result)
63
65
  when "Copying files" then "done (#{result} files)"
64
66
  when "Generating SEO" then "done (#{result.join(', ')})"
@@ -52,6 +52,7 @@ module Docyard
52
52
  @step_runner.run("Generating pages") { generate_static_pages }
53
53
  @step_runner.run("Bundling assets") { bundle_assets }
54
54
  @step_runner.run("Copying files") { copy_static_files }
55
+ @step_runner.run("Social cards") { generate_social_cards } if social_cards_enabled?
55
56
  @step_runner.run("Generating SEO") { generate_seo_files }
56
57
  @step_runner.run("Indexing search") { generate_search_index }
57
58
  end
@@ -130,6 +131,15 @@ module Docyard
130
131
  Search::BuildIndexer.new(config, verbose: verbose).index
131
132
  end
132
133
 
134
+ def generate_social_cards
135
+ require_relative "build/social_cards_generator"
136
+ Build::SocialCardsGenerator.new(config, verbose: verbose).generate
137
+ end
138
+
139
+ def social_cards_enabled?
140
+ config.social_cards&.enabled == true
141
+ end
142
+
133
143
  def robots_txt_content
134
144
  base = config.build.base
135
145
  base = "#{base}/" unless base.end_with?("/")
data/lib/docyard/cli.rb CHANGED
@@ -80,6 +80,40 @@ module Docyard
80
80
  exit(doctor.run)
81
81
  end
82
82
 
83
+ desc "deploy", "Deploy the built site to a hosting platform"
84
+ method_option :to, type: :string, desc: "Target platform (vercel, netlify, cloudflare, github-pages)"
85
+ method_option :prod, type: :boolean, default: true, desc: "Deploy to production"
86
+ method_option :skip_build, type: :boolean, default: false, desc: "Skip building before deploy"
87
+ def deploy
88
+ apply_global_options
89
+ require_relative "deploy/deployer"
90
+ deployer = Deploy::Deployer.new(
91
+ to: options[:to],
92
+ production: options[:prod],
93
+ skip_build: options[:skip_build]
94
+ )
95
+ exit(1) unless deployer.deploy
96
+ rescue ConfigError => e
97
+ print_config_error(e)
98
+ end
99
+
100
+ desc "customize", "Generate theme customization files"
101
+ method_option :minimal, type: :boolean, default: false, aliases: "-m",
102
+ desc: "Generate minimal files without comments"
103
+ def customize
104
+ apply_global_options
105
+ require_relative "customizer"
106
+ Docyard::Customizer.new(minimal: options[:minimal]).generate
107
+ rescue ConfigError => e
108
+ print_config_error(e)
109
+ rescue Errno::EACCES => e
110
+ print_file_error("Permission denied", e.message)
111
+ rescue Errno::ENOSPC
112
+ print_file_error("Disk full", "No space left on device")
113
+ rescue SystemCallError => e
114
+ print_file_error("File operation failed", e.message)
115
+ end
116
+
83
117
  private
84
118
 
85
119
  def apply_global_options
@@ -94,5 +128,15 @@ module Docyard
94
128
  puts
95
129
  exit(1)
96
130
  end
131
+
132
+ def print_file_error(title, message)
133
+ puts
134
+ puts " #{UI.bold('Docyard')} v#{VERSION}"
135
+ puts
136
+ puts " #{UI.red("#{title}:")}"
137
+ puts " #{message}"
138
+ puts
139
+ exit(1)
140
+ end
97
141
  end
98
142
  end
@@ -38,7 +38,7 @@ module Docyard
38
38
  site_options, logo_options, search_options, credits_options, social_options,
39
39
  navigation_options, tabs_options, announcement_options, repo_options, analytics_options,
40
40
  color_options
41
- ].reduce({}, :merge)
41
+ ].reduce({}, :merge).merge(social_cards_enabled: config.social_cards&.enabled == true)
42
42
  end
43
43
 
44
44
  def site_options
@@ -22,7 +22,8 @@ module Docyard
22
22
  announcement: ANNOUNCEMENT_SCHEMA,
23
23
  repo: REPO_SCHEMA,
24
24
  analytics: ANALYTICS_SCHEMA,
25
- feedback: FEEDBACK_SCHEMA
25
+ feedback: FEEDBACK_SCHEMA,
26
+ social_cards: SOCIAL_CARDS_SCHEMA
26
27
  }.freeze
27
28
  end
28
29
  end
@@ -15,7 +15,6 @@ module Docyard
15
15
  slack: { type: :url },
16
16
  linkedin: { type: :url },
17
17
  youtube: { type: :url },
18
- bluesky: { type: :url },
19
18
  custom: {
20
19
  type: :array,
21
20
  items: {
@@ -73,6 +73,13 @@ module Docyard
73
73
  question: { type: :string }
74
74
  }
75
75
  }.freeze
76
+
77
+ SOCIAL_CARDS_SCHEMA = {
78
+ type: :hash,
79
+ keys: {
80
+ enabled: { type: :boolean }
81
+ }
82
+ }.freeze
76
83
  end
77
84
  end
78
85
  end
@@ -26,7 +26,8 @@ module Docyard
26
26
  "repo" => { "url" => nil, "branch" => "main", "edit_path" => nil, "edit_link" => true,
27
27
  "last_updated" => true },
28
28
  "analytics" => { "google" => nil, "plausible" => nil, "fathom" => nil, "script" => nil },
29
- "feedback" => { "enabled" => false, "question" => "Was this page helpful?" }
29
+ "feedback" => { "enabled" => false, "question" => "Was this page helpful?" },
30
+ "social_cards" => { "enabled" => false }
30
31
  }.freeze
31
32
 
32
33
  attr_reader :data, :file_path
@@ -63,6 +64,7 @@ module Docyard
63
64
  def repo = @repo ||= Section.new(data["repo"])
64
65
  def analytics = @analytics ||= Section.new(data["analytics"])
65
66
  def feedback = @feedback ||= Section.new(data["feedback"])
67
+ def social_cards = @social_cards ||= Section.new(data["social_cards"])
66
68
 
67
69
  def announcement
68
70
  @announcement ||= data["announcement"] ? Section.new(data["announcement"]) : nil
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class Customizer
5
+ CUSTOM_DIR = "_custom"
6
+ STYLES_FILE = "styles.css"
7
+ SCRIPTS_FILE = "scripts.js"
8
+ VARIABLES_PATH = File.join(__dir__, "templates", "assets", "css", "variables.css")
9
+
10
+ CATEGORY_PATTERNS = {
11
+ /^--sidebar-width/ => "Layout",
12
+ /^--sidebar/ => "Sidebar",
13
+ /^--code|^--diff/ => "Code",
14
+ /^--callout/ => "Callouts",
15
+ /^--table|^--hr/ => "Tables",
16
+ /^--search|^--overlay/ => "Search",
17
+ /^--font|^--text|^--leading/ => "Typography",
18
+ /^--spacing/ => "Spacing",
19
+ /^--toc|^--header|^--content|^--layout|^--tab-bar|^--secondary-header/ => "Layout",
20
+ /^--radius/ => "Radius",
21
+ /^--shadow/ => "Shadows",
22
+ /^--transition/ => "Transitions",
23
+ /^--z-/ => "Z-Index",
24
+ /^--ring/ => "Other"
25
+ }.freeze
26
+
27
+ CATEGORIES = %w[
28
+ Colors Sidebar Code Callouts Tables Search
29
+ Typography Spacing Layout Radius Shadows Transitions Z-Index Other
30
+ ].freeze
31
+
32
+ attr_reader :config, :minimal
33
+
34
+ def initialize(minimal: false)
35
+ @config = Config.load
36
+ @minimal = minimal
37
+ end
38
+
39
+ def generate
40
+ validate_source_directory
41
+ create_custom_directory
42
+ write_styles_file
43
+ write_scripts_file
44
+ print_success
45
+ end
46
+
47
+ private
48
+
49
+ def validate_source_directory
50
+ return if File.directory?(config.source)
51
+
52
+ raise ConfigError, "Source directory '#{config.source}' does not exist"
53
+ end
54
+
55
+ def custom_dir_path
56
+ @custom_dir_path ||= File.join(config.source, CUSTOM_DIR)
57
+ end
58
+
59
+ def styles_path
60
+ @styles_path ||= File.join(custom_dir_path, STYLES_FILE)
61
+ end
62
+
63
+ def scripts_path
64
+ @scripts_path ||= File.join(custom_dir_path, SCRIPTS_FILE)
65
+ end
66
+
67
+ def create_custom_directory
68
+ FileUtils.mkdir_p(custom_dir_path)
69
+ end
70
+
71
+ def write_styles_file
72
+ File.write(styles_path, minimal ? minimal_styles : annotated_styles)
73
+ end
74
+
75
+ def write_scripts_file
76
+ File.write(scripts_path, minimal ? minimal_scripts : annotated_scripts)
77
+ end
78
+
79
+ def annotated_styles
80
+ build_annotated_css(parse_variables)
81
+ end
82
+
83
+ def minimal_styles
84
+ build_minimal_css(parse_variables)
85
+ end
86
+
87
+ def parse_variables
88
+ content = File.read(VARIABLES_PATH)
89
+ { light: extract_variables(content, ":root"), dark: extract_variables(content, ".dark") }
90
+ end
91
+
92
+ def extract_variables(content, selector)
93
+ match = content.match(/#{Regexp.escape(selector)}\s*\{([^}]+)\}/m)
94
+ return [] unless match
95
+
96
+ match[1].scan(/(--[\w-]+):\s*([^;]+);/).map { |name, value| { name: name, value: value.strip } }
97
+ end
98
+
99
+ def build_annotated_css(variables)
100
+ light = format_annotated(variables[:light])
101
+ dark = format_annotated(variables[:dark])
102
+ "#{css_header}:root {\n#{light}\n}\n\n.dark {\n#{dark}\n}\n"
103
+ end
104
+
105
+ def build_minimal_css(variables)
106
+ ":root {\n#{format_minimal(variables[:light])}\n}\n\n.dark {\n#{format_minimal(variables[:dark])}\n}\n"
107
+ end
108
+
109
+ def css_header
110
+ <<~HEADER
111
+ /* =============================================================================
112
+ DOCYARD THEME CUSTOMIZATION
113
+
114
+ Uncomment and modify variables to customize your site.
115
+ Delete any variables you don't need to change.
116
+
117
+ Generated with: docyard customize
118
+ ============================================================================= */
119
+
120
+ HEADER
121
+ end
122
+
123
+ def format_annotated(vars)
124
+ group_variables(vars).flat_map do |category, category_vars|
125
+ [" /* #{category} */"] + category_vars.map { |v| " /* #{v[:name]}: #{v[:value]}; */" } + [""]
126
+ end.join("\n").rstrip
127
+ end
128
+
129
+ def format_minimal(vars)
130
+ vars.map { |var| " /* #{var[:name]}: #{var[:value]}; */" }.join("\n")
131
+ end
132
+
133
+ def group_variables(vars)
134
+ groups = CATEGORIES.to_h { |cat| [cat, []] }
135
+ vars.each { |var| groups[categorize_variable(var[:name])] << var }
136
+ groups.reject { |_, v| v.empty? }
137
+ end
138
+
139
+ def categorize_variable(name)
140
+ CATEGORY_PATTERNS.each { |pattern, category| return category if name.match?(pattern) }
141
+ "Colors"
142
+ end
143
+
144
+ def annotated_scripts
145
+ "#{js_header}document.addEventListener('DOMContentLoaded', function() {\n // Your custom JavaScript here\n});\n"
146
+ end
147
+
148
+ def js_header
149
+ <<~HEADER
150
+ /* =============================================================================
151
+ DOCYARD CUSTOM SCRIPTS
152
+
153
+ This file is loaded on every page after the default scripts.
154
+
155
+ Generated with: docyard customize
156
+ ============================================================================= */
157
+
158
+ HEADER
159
+ end
160
+
161
+ def minimal_scripts
162
+ "document.addEventListener('DOMContentLoaded', function() {\n // Your custom JavaScript here\n});\n"
163
+ end
164
+
165
+ def print_success
166
+ puts
167
+ print_header
168
+ print_created_files
169
+ print_next_steps
170
+ end
171
+
172
+ def print_header
173
+ puts " #{UI.bold('Docyard')} v#{VERSION}"
174
+ puts
175
+ end
176
+
177
+ def print_created_files
178
+ puts " #{UI.success('Created:')}"
179
+ puts UI.dim(" #{relative_path(custom_dir_path)}/")
180
+ puts UI.dim(" #{STYLES_FILE}")
181
+ puts UI.dim(" #{SCRIPTS_FILE}")
182
+ puts
183
+ end
184
+
185
+ def print_next_steps
186
+ puts " #{UI.bold('Next steps:')}"
187
+ puts " Edit #{UI.cyan(relative_path(styles_path))} to customize theme"
188
+ puts " Changes apply on next serve or build"
189
+ puts
190
+ end
191
+
192
+ def relative_path(path)
193
+ path.sub("#{Dir.pwd}/", "")
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Docyard
6
+ module Deploy
7
+ module Adapters
8
+ class Base
9
+ attr_reader :output_dir, :production, :config
10
+
11
+ def initialize(output_dir:, production:, config:)
12
+ @output_dir = output_dir
13
+ @production = production
14
+ @config = config
15
+ end
16
+
17
+ def deploy
18
+ check_cli_installed!
19
+ run_deploy
20
+ end
21
+
22
+ def platform_name
23
+ raise NotImplementedError
24
+ end
25
+
26
+ private
27
+
28
+ def cli_name
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def cli_install_hint
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def run_deploy
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def check_cli_installed!
41
+ _, _, status = Open3.capture3("which", cli_name)
42
+ return if status.success?
43
+
44
+ raise DeployError, "'#{cli_name}' CLI not found. Install it with: #{cli_install_hint}"
45
+ end
46
+
47
+ def execute_command(*)
48
+ stdout, stderr, status = Open3.capture3(*)
49
+ return stdout if status.success?
50
+
51
+ raise DeployError, "Deploy command failed: #{stderr.strip.empty? ? stdout.strip : stderr.strip}"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module Docyard
6
+ module Deploy
7
+ module Adapters
8
+ class Cloudflare < Base
9
+ def platform_name
10
+ "Cloudflare Pages"
11
+ end
12
+
13
+ private
14
+
15
+ def cli_name
16
+ "wrangler"
17
+ end
18
+
19
+ def cli_install_hint
20
+ "npm i -g wrangler"
21
+ end
22
+
23
+ def run_deploy
24
+ output = execute_command("wrangler", "pages", "deploy", output_dir, "--project-name=#{project_name}")
25
+ extract_url(output)
26
+ end
27
+
28
+ def project_name
29
+ config.title.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
30
+ end
31
+
32
+ def extract_url(output)
33
+ output.match(%r{https://\S+\.pages\.dev\S*})&.to_s
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+ require "fileutils"
5
+ require_relative "base"
6
+
7
+ module Docyard
8
+ module Deploy
9
+ module Adapters
10
+ class GithubPages < Base
11
+ def platform_name
12
+ "GitHub Pages"
13
+ end
14
+
15
+ private
16
+
17
+ def cli_name
18
+ "gh"
19
+ end
20
+
21
+ def cli_install_hint
22
+ "https://cli.github.com"
23
+ end
24
+
25
+ def run_deploy
26
+ remote_url = fetch_remote_url
27
+ Dir.mktmpdir do |tmp|
28
+ prepare_deploy_dir(tmp)
29
+ push_to_gh_pages(tmp, remote_url)
30
+ end
31
+ build_pages_url(remote_url)
32
+ end
33
+
34
+ def fetch_remote_url
35
+ execute_command("git", "remote", "get-url", "origin").strip
36
+ end
37
+
38
+ def prepare_deploy_dir(tmp)
39
+ FileUtils.cp_r("#{output_dir}/.", tmp)
40
+ execute_command("git", "-C", tmp, "init", "-b", "gh-pages")
41
+ execute_command("git", "-C", tmp, "add", ".")
42
+ execute_command("git", "-C", tmp, "commit", "-m", "Deploy via docyard")
43
+ end
44
+
45
+ def push_to_gh_pages(tmp, remote_url)
46
+ execute_command("git", "-C", tmp, "remote", "add", "origin", remote_url)
47
+ execute_command("git", "-C", tmp, "push", "--force", "origin", "gh-pages")
48
+ end
49
+
50
+ def build_pages_url(remote_url)
51
+ match = remote_url.match(%r{github\.com[:/]([^/]+)/([^/.]+)})
52
+ return nil unless match
53
+
54
+ owner, repo = match.captures
55
+ "https://#{owner}.github.io/#{repo}/"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end