joys 0.1.1 → 0.1.3

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.
data/lib/joys/cli.rb ADDED
@@ -0,0 +1,1554 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+ # bin/joys - CLI for Joys static site generator
4
+
5
+ require 'optparse'
6
+ require 'fileutils'
7
+ require 'json'
8
+
9
+ module Joys
10
+ class CLI
11
+ VERSION = "1.0.0"
12
+
13
+ def initialize(args)
14
+ @args = args
15
+ @options = {}
16
+ end
17
+
18
+ def run
19
+ case @args.first
20
+ when 'new', 'n'
21
+ run_new
22
+ when 'build', 'b'
23
+ run_build
24
+ when 'serve', 's', 'dev'
25
+ run_serve
26
+ when 'version', '-v', '--version'
27
+ puts VERSION
28
+ when 'help', '-h', '--help', nil
29
+ show_help
30
+ else
31
+ puts "Unknown command: #{@args.first}"
32
+ show_help
33
+ exit 1
34
+ end
35
+ rescue Interrupt
36
+ puts "\nCancelled."
37
+ exit 1
38
+ rescue => e
39
+ puts "Error: #{e.message}"
40
+ puts " #{e.backtrace.first}" if ENV['DEBUG']
41
+ exit 1
42
+ end
43
+
44
+ private
45
+
46
+ def run_new
47
+ project_name = @args[1]
48
+
49
+ unless project_name
50
+ puts "Error: Project name required"
51
+ puts "Usage: joys new PROJECT_NAME [options]"
52
+ exit 1
53
+ end
54
+
55
+ parse_new_options
56
+
57
+ # Prevent overwriting existing directories
58
+ if Dir.exist?(project_name)
59
+ puts "Error: Directory '#{project_name}' already exists"
60
+ exit 1
61
+ end
62
+
63
+ generator = ProjectGenerator.new(project_name, @options)
64
+ generator.generate!
65
+
66
+ puts ""
67
+ puts "✨ Project '#{project_name}' created successfully!"
68
+ puts ""
69
+ puts "Next steps:"
70
+ puts " cd #{project_name}"
71
+ puts " gem install joys"
72
+ puts " joys build"
73
+ puts " joys serve"
74
+ puts ""
75
+
76
+ if @options[:github_pages]
77
+ puts "GitHub Pages setup:"
78
+ puts " 1. Create a new GitHub repository"
79
+ puts " 2. git init && git add . && git commit -m 'Initial commit'"
80
+ puts " 3. git remote add origin <your-repo-url>"
81
+ puts " 4. git push -u origin main"
82
+ puts " 5. Enable GitHub Pages in repository settings"
83
+ puts ""
84
+ end
85
+ end
86
+
87
+ def run_build
88
+ parse_build_options
89
+
90
+ content_dir = @options[:content] || 'content'
91
+ output_dir = @options[:output] || 'dist'
92
+
93
+ unless Dir.exist?(content_dir)
94
+ puts "Error: Content directory '#{content_dir}' not found"
95
+ exit 1
96
+ end
97
+
98
+ require_relative '../lib/joys/ssg'
99
+
100
+ start_time = Time.now
101
+ built_files = Joys::SSG.build(content_dir, output_dir, force: @options[:force])
102
+ elapsed = Time.now - start_time
103
+
104
+ puts ""
105
+ puts "✅ Built #{built_files.size} pages in #{elapsed.round(2)}s"
106
+ puts "📁 Output: #{output_dir}/"
107
+ end
108
+
109
+ def run_serve
110
+ parse_serve_options
111
+
112
+ content_dir = @options[:content] || 'content'
113
+ output_dir = @options[:output] || 'dist'
114
+ port = @options[:port] || 5000
115
+
116
+ unless Dir.exist?(content_dir)
117
+ puts "Error: Content directory '#{content_dir}' not found"
118
+ puts "Run 'joys new PROJECT_NAME' to create a new project"
119
+ exit 1
120
+ end
121
+
122
+ require_relative '../lib/joys/dev_server'
123
+
124
+ puts "Starting Joys development server..."
125
+ puts "Press Ctrl+C to stop"
126
+ puts ""
127
+
128
+ Joys::SSGDev.start(content_dir, output_dir, port: port)
129
+ end
130
+
131
+ def parse_new_options
132
+ OptionParser.new do |opts|
133
+ opts.banner = "Usage: joys new PROJECT_NAME [options]"
134
+
135
+ opts.on("--type=TYPE", ["blog", "docs", "portfolio", "basic"],
136
+ "Project type (blog, docs, portfolio, basic)") do |type|
137
+ @options[:type] = type
138
+ end
139
+
140
+ opts.on("--domains=DOMAINS", Array, "Domains for multi-site setup") do |domains|
141
+ @options[:domains] = domains
142
+ end
143
+
144
+ opts.on("--github-pages", "Setup GitHub Pages deployment") do
145
+ @options[:github_pages] = true
146
+ end
147
+
148
+ opts.on("-h", "--help", "Show help") do
149
+ puts opts
150
+ exit
151
+ end
152
+ end.parse!(@args[2..-1] || [])
153
+ end
154
+
155
+ def parse_build_options
156
+ OptionParser.new do |opts|
157
+ opts.banner = "Usage: joys build [options]"
158
+
159
+ opts.on("-c", "--content=DIR", "Content directory (default: content)") do |dir|
160
+ @options[:content] = dir
161
+ end
162
+
163
+ opts.on("-o", "--output=DIR", "Output directory (default: dist)") do |dir|
164
+ @options[:output] = dir
165
+ end
166
+
167
+ opts.on("-f", "--force", "Force rebuild (clear output directory)") do
168
+ @options[:force] = true
169
+ end
170
+
171
+ opts.on("-h", "--help", "Show help") do
172
+ puts opts
173
+ exit
174
+ end
175
+ end.parse!(@args[1..-1] || [])
176
+ end
177
+
178
+ def parse_serve_options
179
+ OptionParser.new do |opts|
180
+ opts.banner = "Usage: joys serve [options]"
181
+
182
+ opts.on("-c", "--content=DIR", "Content directory (default: content)") do |dir|
183
+ @options[:content] = dir
184
+ end
185
+
186
+ opts.on("-o", "--output=DIR", "Output directory (default: dist)") do |dir|
187
+ @options[:output] = dir
188
+ end
189
+
190
+ opts.on("-p", "--port=PORT", Integer, "Port (default: 5000)") do |port|
191
+ @options[:port] = port
192
+ end
193
+
194
+ opts.on("-h", "--help", "Show help") do
195
+ puts opts
196
+ exit
197
+ end
198
+ end.parse!(@args[1..-1] || [])
199
+ end
200
+
201
+ def show_help
202
+ puts <<~HELP
203
+ Joys Static Site Generator v#{VERSION}
204
+
205
+ USAGE:
206
+ joys COMMAND [options]
207
+
208
+ COMMANDS:
209
+ new, n Generate a new Joys project
210
+ build, b Build the static site
211
+ serve, s, dev Start development server
212
+ version Show version
213
+ help Show this help
214
+
215
+ EXAMPLES:
216
+ joys new my-blog --type=blog --github-pages
217
+ joys new my-site --domains blog_site_com docs_site_com
218
+ joys build --force
219
+ joys serve --port=3000
220
+
221
+ For more help on a command:
222
+ joys COMMAND --help
223
+ HELP
224
+ end
225
+ end
226
+
227
+ class ProjectGenerator
228
+ VALID_TYPES = %w[blog docs portfolio basic].freeze
229
+
230
+ def initialize(project_name, options)
231
+ @project_name = project_name
232
+ @options = options
233
+ @type = @options[:type] || prompt_for_type
234
+ @domains = @options[:domains] || ['default']
235
+ @github_pages = @options[:github_pages] || false
236
+
237
+ # GitHub Pages only works with default domain
238
+ if @github_pages && @domains != ['default']
239
+ puts "Warning: GitHub Pages setup only supports single domain. Using 'default' domain."
240
+ @domains = ['default']
241
+ end
242
+ end
243
+
244
+ def generate!
245
+ create_directory_structure
246
+ generate_core_files
247
+ generate_content_files
248
+ generate_sample_data
249
+ generate_github_pages_setup if @github_pages
250
+
251
+ puts "📁 Generated #{@project_name}/ with #{@type} template"
252
+ puts "🌍 Domains: #{@domains.join(', ')}" if @domains != ['default']
253
+ puts "🚀 GitHub Pages: Ready" if @github_pages
254
+ end
255
+
256
+ private
257
+
258
+ def prompt_for_type
259
+ puts "What type of site are you building?"
260
+ puts " 1. Blog (posts, categories, pagination)"
261
+ puts " 2. Documentation (guides, API docs, navigation)"
262
+ puts " 3. Portfolio (projects, gallery, contact)"
263
+ puts " 4. Basic (minimal setup)"
264
+ print "Choice [1-4]: "
265
+
266
+ choice = STDIN.gets.chomp
267
+ case choice
268
+ when '1' then 'blog'
269
+ when '2' then 'docs'
270
+ when '3' then 'portfolio'
271
+ when '4' then 'basic'
272
+ else
273
+ puts "Invalid choice. Using 'basic'."
274
+ 'basic'
275
+ end
276
+ end
277
+
278
+ def create_directory_structure
279
+ FileUtils.mkdir_p(@project_name)
280
+ Dir.chdir(@project_name) do
281
+ # Core directories
282
+ FileUtils.mkdir_p(['data', 'public'])
283
+
284
+ # Domain-specific content directories
285
+ @domains.each do |domain|
286
+ FileUtils.mkdir_p([
287
+ "content/#{domain}",
288
+ "content/#{domain}/assets",
289
+ "content/_components",
290
+ "content/_layouts"
291
+ ])
292
+ end
293
+
294
+ # GitHub Pages specific
295
+ FileUtils.mkdir_p('.github/workflows') if @github_pages
296
+ end
297
+ end
298
+
299
+ def generate_core_files
300
+ Dir.chdir(@project_name) do
301
+ # Gemfile
302
+ File.write('Gemfile', gemfile_content)
303
+
304
+ # Build script
305
+ File.write('build.rb', build_script_content)
306
+ File.chmod(0755, 'build.rb')
307
+
308
+ # README
309
+ File.write('README.md', readme_content)
310
+
311
+ # .gitignore
312
+ File.write('.gitignore', gitignore_content)
313
+
314
+ # Basic public assets
315
+ File.write('public/favicon.ico', '')
316
+ File.write('public/robots.txt', robots_txt_content)
317
+ end
318
+ end
319
+
320
+ def generate_content_files
321
+ Dir.chdir(@project_name) do
322
+ generate_layouts
323
+ generate_components
324
+
325
+ @domains.each do |domain|
326
+ generate_domain_pages(domain)
327
+ generate_domain_assets(domain)
328
+ end
329
+
330
+ generate_error_pages
331
+ end
332
+ end
333
+
334
+ def generate_layouts
335
+ # Main layout
336
+ File.write('content/_layouts/main.rb', main_layout_content)
337
+
338
+ # Type-specific layouts
339
+ case @type
340
+ when 'blog'
341
+ File.write('content/_layouts/post.rb', post_layout_content)
342
+ when 'docs'
343
+ File.write('content/_layouts/docs.rb', docs_layout_content)
344
+ when 'portfolio'
345
+ File.write('content/_layouts/project.rb', project_layout_content)
346
+ end
347
+ end
348
+
349
+ def generate_components
350
+ # Header component
351
+ File.write('content/_components/header.rb', header_component_content)
352
+
353
+ # Footer component
354
+ File.write('content/_components/footer.rb', footer_component_content)
355
+
356
+ # Type-specific components
357
+ case @type
358
+ when 'blog'
359
+ File.write('content/_components/post_card.rb', post_card_component_content)
360
+ when 'docs'
361
+ File.write('content/_components/doc_nav.rb', doc_nav_component_content)
362
+ when 'portfolio'
363
+ File.write('content/_components/project_card.rb', project_card_component_content)
364
+ end
365
+ end
366
+
367
+ def generate_domain_pages(domain)
368
+ case @type
369
+ when 'blog'
370
+ generate_blog_pages(domain)
371
+ when 'docs'
372
+ generate_docs_pages(domain)
373
+ when 'portfolio'
374
+ generate_portfolio_pages(domain)
375
+ else
376
+ generate_basic_pages(domain)
377
+ end
378
+ end
379
+
380
+ def generate_blog_pages(domain)
381
+ # Homepage
382
+ File.write("content/#{domain}/index.rb", blog_homepage_content)
383
+
384
+ # Blog listing
385
+ FileUtils.mkdir_p("content/#{domain}/blog")
386
+ File.write("content/#{domain}/blog/index.rb", blog_listing_content)
387
+
388
+ # Individual post template
389
+ File.write("content/#{domain}/blog/post.rb", blog_post_content)
390
+
391
+ # About page
392
+ File.write("content/#{domain}/about.rb", about_page_content)
393
+ end
394
+
395
+ def generate_docs_pages(domain)
396
+ # Homepage
397
+ File.write("content/#{domain}/index.rb", docs_homepage_content)
398
+
399
+ # Getting started
400
+ File.write("content/#{domain}/getting-started.rb", getting_started_content)
401
+
402
+ # API reference
403
+ FileUtils.mkdir_p("content/#{domain}/api")
404
+ File.write("content/#{domain}/api/index.rb", api_index_content)
405
+ end
406
+
407
+ def generate_portfolio_pages(domain)
408
+ # Homepage
409
+ File.write("content/#{domain}/index.rb", portfolio_homepage_content)
410
+
411
+ # Projects
412
+ FileUtils.mkdir_p("content/#{domain}/projects")
413
+ File.write("content/#{domain}/projects/index.rb", projects_listing_content)
414
+
415
+ # About
416
+ File.write("content/#{domain}/about.rb", portfolio_about_content)
417
+
418
+ # Contact
419
+ File.write("content/#{domain}/contact.rb", contact_page_content)
420
+ end
421
+
422
+ def generate_basic_pages(domain)
423
+ # Homepage
424
+ File.write("content/#{domain}/index.rb", basic_homepage_content)
425
+
426
+ # About page
427
+ File.write("content/#{domain}/about.rb", basic_about_content)
428
+ end
429
+
430
+ def generate_domain_assets(domain)
431
+ File.write("content/#{domain}/assets/style.css", css_content)
432
+ end
433
+
434
+ def generate_error_pages
435
+ @domains.each do |domain|
436
+ File.write("content/#{domain}/404.rb", error_404_content)
437
+ File.write("content/#{domain}/500.rb", error_500_content)
438
+ end
439
+ end
440
+
441
+ def generate_sample_data
442
+ case @type
443
+ when 'blog'
444
+ File.write('data/posts_data.rb', blog_data_content)
445
+ File.write('data/config_data.rb', config_data_content)
446
+ when 'docs'
447
+ File.write('data/docs_data.rb', docs_data_content)
448
+ File.write('data/config_data.rb', config_data_content)
449
+ when 'portfolio'
450
+ File.write('data/projects_data.rb', portfolio_data_content)
451
+ File.write('data/config_data.rb', config_data_content)
452
+ else
453
+ File.write('data/config_data.rb', basic_config_data_content)
454
+ end
455
+ end
456
+
457
+ def generate_github_pages_setup
458
+ # GitHub Actions workflow
459
+ File.write('.github/workflows/deploy.yml', github_actions_content)
460
+
461
+ # GitHub Pages build script (simpler version of build.rb)
462
+ File.write('build.rb', github_build_script_content)
463
+ end
464
+
465
+ # Content generation methods follow...
466
+ def gemfile_content
467
+ <<~GEMFILE
468
+ source 'https://rubygems.org'
469
+
470
+ gem 'joys'
471
+ GEMFILE
472
+ end
473
+
474
+ def build_script_content
475
+ <<~RUBY
476
+ #!/usr/bin/env ruby
477
+ require 'joys'
478
+
479
+ puts "🔨 Building #{@type} site..."
480
+ start_time = Time.now
481
+
482
+ built_files = Joys.build_static_site('content', 'dist')
483
+ elapsed = Time.now - start_time
484
+
485
+ puts "✅ Built \#{built_files.size} pages in \#{elapsed.round(2)}s"
486
+ puts "📁 Output: dist/"
487
+ puts ""
488
+ puts "To preview: joys serve"
489
+ RUBY
490
+ end
491
+
492
+ def github_build_script_content
493
+ <<~RUBY
494
+ #!/usr/bin/env ruby
495
+ require 'joys'
496
+
497
+ puts "🔨 Building for GitHub Pages..."
498
+ built_files = Joys.build_static_site('content', 'dist')
499
+ puts "✅ Built \#{built_files.size} pages"
500
+ RUBY
501
+ end
502
+
503
+ def readme_content
504
+ site_title = @project_name.tr('_-', ' ').split.map(&:capitalize).join(' ')
505
+
506
+ <<~MARKDOWN
507
+ # #{site_title}
508
+
509
+ A #{@type} site built with [Joys](https://github.com/fractaledmind/joys) static site generator.
510
+
511
+ ## Development
512
+
513
+ ```bash
514
+ # Install dependencies
515
+ gem install joys
516
+
517
+ # Build the site
518
+ joys build
519
+
520
+ # Start development server with live reload
521
+ joys serve
522
+ ```
523
+
524
+ ## Structure
525
+
526
+ - `content/` - Page templates and content
527
+ - `data/` - Data files
528
+ - `public/` - Static assets
529
+ - `dist/` - Generated site (created by build)
530
+
531
+ #{github_pages_readme_section if @github_pages}
532
+ ## Customization
533
+
534
+ - Edit `content/_layouts/` for page layouts
535
+ - Edit `content/_components/` for reusable components
536
+ - Edit `data/` files for site data
537
+ - Add assets to `content/[domain]/assets/` or `public/`
538
+
539
+ ## Deployment
540
+
541
+ The `dist/` directory contains the built site. Deploy it to any static hosting service.
542
+ MARKDOWN
543
+ end
544
+
545
+ def github_pages_readme_section
546
+ <<~MARKDOWN
547
+ ## GitHub Pages Deployment
548
+
549
+ This project is configured for GitHub Pages deployment:
550
+
551
+ 1. Push to the `main` branch
552
+ 2. GitHub Actions will automatically build and deploy
553
+ 3. Site will be available at `https://YOUR_USERNAME.github.io/#{@project_name}`
554
+
555
+ The deployment workflow is in `.github/workflows/deploy.yml`.
556
+
557
+ MARKDOWN
558
+ end
559
+
560
+ def gitignore_content
561
+ <<~GITIGNORE
562
+ dist/
563
+ .DS_Store
564
+ *.log
565
+ .env
566
+ GITIGNORE
567
+ end
568
+
569
+ def robots_txt_content
570
+ <<~TXT
571
+ User-agent: *
572
+ Allow: /
573
+ TXT
574
+ end
575
+
576
+ def main_layout_content
577
+ <<~RUBY
578
+ Joys.define :layout, :main do
579
+ doctype
580
+ html(lang: "en") {
581
+ head {
582
+ meta(charset: "utf-8")
583
+ meta(name: "viewport", content: "width=device-width, initial-scale=1")
584
+ title { pull(:title) }
585
+ link(rel: "stylesheet", href: "/style.css")
586
+ link(rel: "icon", href: "/favicon.ico")
587
+ }
588
+ body {
589
+ comp(:header)
590
+ main(cs: "container") {
591
+ pull(:content)
592
+ }
593
+ comp(:footer)
594
+ }
595
+ }
596
+ end
597
+ RUBY
598
+ end
599
+
600
+ def post_layout_content
601
+ <<~RUBY
602
+ Joys.define :layout, :post do
603
+ layout(:main) {
604
+ push(:title) { "\#{@post[:title]} - \#{data(:config).site_title}" }
605
+ push(:content) {
606
+ article(cs: "post") {
607
+ header {
608
+ h1(@post[:title])
609
+ time(@post[:published_at].strftime("%B %d, %Y"))
610
+ }
611
+ div(cs: "content") {
612
+ raw @post[:content]
613
+ }
614
+ }
615
+ }
616
+ }
617
+ end
618
+ RUBY
619
+ end
620
+
621
+ def docs_layout_content
622
+ <<~RUBY
623
+ Joys.define :layout, :docs do
624
+ layout(:main) {
625
+ push(:title) { "\#{@doc[:title]} - \#{data(:config).site_title}" }
626
+ push(:content) {
627
+ div(cs: "docs-layout") {
628
+ aside(cs: "sidebar") {
629
+ comp(:doc_nav)
630
+ }
631
+ article(cs: "doc-content") {
632
+ header {
633
+ h1(@doc[:title])
634
+ }
635
+ div(cs: "content") {
636
+ raw @doc[:content]
637
+ }
638
+ }
639
+ }
640
+ }
641
+ }
642
+ end
643
+ RUBY
644
+ end
645
+
646
+ def project_layout_content
647
+ <<~RUBY
648
+ Joys.define :layout, :project do
649
+ layout(:main) {
650
+ push(:title) { "\#{@project[:title]} - \#{data(:config).site_title}" }
651
+ push(:content) {
652
+ article(cs: "project") {
653
+ header {
654
+ h1(@project[:title])
655
+ p(@project[:description])
656
+ }
657
+ div(cs: "project-content") {
658
+ raw @project[:content]
659
+ }
660
+ footer(cs: "project-meta") {
661
+ if @project[:github_url]
662
+ a("View on GitHub", href: @project[:github_url], target: "_blank")
663
+ end
664
+ if @project[:demo_url]
665
+ a("Live Demo", href: @project[:demo_url], target: "_blank")
666
+ end
667
+ }
668
+ }
669
+ }
670
+ }
671
+ end
672
+ RUBY
673
+ end
674
+
675
+ def header_component_content
676
+ <<~RUBY
677
+ Joys.define :comp, :header do
678
+ header(cs: "site-header") {
679
+ div(cs: "container") {
680
+ a(data(:config).site_title, href: "/", cs: "logo")
681
+ nav {
682
+ a("Home", href: "/")
683
+ #{navigation_links}
684
+ }
685
+ }
686
+ }
687
+ end
688
+ RUBY
689
+ end
690
+
691
+ def navigation_links
692
+ case @type
693
+ when 'blog'
694
+ 'a("Blog", href: "/blog/")
695
+ a("About", href: "/about/")'
696
+ when 'docs'
697
+ 'a("Documentation", href: "/getting-started/")
698
+ a("API", href: "/api/")'
699
+ when 'portfolio'
700
+ 'a("Projects", href: "/projects/")
701
+ a("About", href: "/about/")
702
+ a("Contact", href: "/contact/")'
703
+ else
704
+ 'a("About", href: "/about/")'
705
+ end
706
+ end
707
+
708
+ def footer_component_content
709
+ <<~RUBY
710
+ Joys.define :comp, :footer do
711
+ footer(cs: "site-footer") {
712
+ div(cs: "container") {
713
+ p {
714
+ txt "© \#{Date.current.year} \#{data(:config).site_title}. "
715
+ txt "Built with "
716
+ a("Joys", href: "https://github.com/fractaledmind/joys", target: "_blank")
717
+ txt "."
718
+ }
719
+ }
720
+ }
721
+ end
722
+ RUBY
723
+ end
724
+
725
+ def post_card_component_content
726
+ <<~RUBY
727
+ Joys.define :comp, :post_card do |post|
728
+ article(cs: "post-card") {
729
+ h3 {
730
+ a(post[:title], href: "/blog/\#{post[:slug]}/")
731
+ }
732
+ time(post[:published_at].strftime("%B %d, %Y"))
733
+ p(post[:excerpt])
734
+ a("Read more →", href: "/blog/\#{post[:slug]}/", cs: "read-more")
735
+ }
736
+ end
737
+ RUBY
738
+ end
739
+
740
+ def doc_nav_component_content
741
+ <<~RUBY
742
+ Joys.define :comp, :doc_nav do
743
+ nav(cs: "doc-nav") {
744
+ h3 "Documentation"
745
+ ul {
746
+ data(:docs).each do |doc|
747
+ li {
748
+ a(doc[:title], href: "/\#{doc[:slug]}/")
749
+ }
750
+ end
751
+ }
752
+ }
753
+ end
754
+ RUBY
755
+ end
756
+
757
+ def project_card_component_content
758
+ <<~RUBY
759
+ Joys.define :comp, :project_card do |project|
760
+ article(cs: "project-card") {
761
+ h3 {
762
+ a(project[:title], href: "/projects/\#{project[:slug]}/")
763
+ }
764
+ p(project[:description])
765
+ div(cs: "project-meta") {
766
+ if project[:tech]
767
+ project[:tech].each do |tech|
768
+ span(tech, cs: "tech-tag")
769
+ end
770
+ end
771
+ }
772
+ }
773
+ end
774
+ RUBY
775
+ end
776
+
777
+ def blog_homepage_content
778
+ <<~RUBY
779
+ recent_posts = data(:posts).recent(3)
780
+ site_config = data(:config)
781
+
782
+ Joys.html do
783
+ layout(:main) {
784
+ push(:title) { site_config.site_title }
785
+ push(:content) {
786
+ section(cs: "hero") {
787
+ h1(site_config.site_title)
788
+ p(site_config.description)
789
+ a("Read the Blog →", href: "/blog/", cs: "btn btn-primary")
790
+ }
791
+
792
+ if recent_posts.any?
793
+ section(cs: "recent-posts") {
794
+ h2 "Recent Posts"
795
+ div(cs: "posts-grid") {
796
+ recent_posts.each do |post|
797
+ comp(:post_card, post)
798
+ end
799
+ }
800
+ a("View All Posts →", href: "/blog/", cs: "btn btn-secondary")
801
+ }
802
+ end
803
+ }
804
+ }
805
+ end
806
+ RUBY
807
+ end
808
+
809
+ def blog_listing_content
810
+ <<~RUBY
811
+ site_config = data(:config)
812
+
813
+ Joys.html do
814
+ layout(:main) {
815
+ push(:title) { "Blog - \#{site_config.site_title}" }
816
+ push(:content) {
817
+ h1 "Blog"
818
+
819
+ paginate(data(:posts).published, per_page: 5) do |page|
820
+ div(cs: "posts-list") {
821
+ page.items.each do |post|
822
+ comp(:post_card, post)
823
+ end
824
+ }
825
+
826
+ if page.total_pages > 1
827
+ nav(cs: "pagination") {
828
+ if page.prev_page
829
+ a("← Previous", href: page.prev_page == 1 ? "/blog/" : "/blog/page-\#{page.prev_page}/")
830
+ end
831
+
832
+ span "Page \#{page.current_page} of \#{page.total_pages}"
833
+
834
+ if page.next_page
835
+ a("Next →", href: "/blog/page-\#{page.next_page}/")
836
+ end
837
+ }
838
+ end
839
+ end
840
+ }
841
+ }
842
+ end
843
+ RUBY
844
+ end
845
+
846
+ def blog_post_content
847
+ <<~RUBY
848
+ # This would typically get the post slug from URL params in a real app
849
+ # For static generation, you'd need to generate individual post pages
850
+ post = data(:posts).find_by(slug: params[:slug]) || data(:posts).first
851
+
852
+ Joys.html do
853
+ layout(:post) {
854
+ # Content is handled by the post layout
855
+ }
856
+ end
857
+ RUBY
858
+ end
859
+
860
+ def about_page_content
861
+ <<~RUBY
862
+ site_config = data(:config)
863
+
864
+ Joys.html do
865
+ layout(:main) {
866
+ push(:title) { "About - \#{site_config.site_title}" }
867
+ push(:content) {
868
+ article {
869
+ h1 "About"
870
+ p "This is the about page for \#{site_config.site_title}."
871
+ p "Edit this page in content/default/about.rb"
872
+ }
873
+ }
874
+ }
875
+ end
876
+ RUBY
877
+ end
878
+
879
+ def docs_homepage_content
880
+ <<~RUBY
881
+ site_config = data(:config)
882
+ recent_docs = data(:docs).limit(3)
883
+
884
+ Joys.html do
885
+ layout(:main) {
886
+ push(:title) { site_config.site_title }
887
+ push(:content) {
888
+ section(cs: "hero") {
889
+ h1(site_config.site_title)
890
+ p(site_config.description)
891
+ a("Get Started →", href: "/getting-started/", cs: "btn btn-primary")
892
+ }
893
+
894
+ if recent_docs.any?
895
+ section(cs: "recent-docs") {
896
+ h2 "Documentation"
897
+ div(cs: "docs-grid") {
898
+ recent_docs.each do |doc|
899
+ article(cs: "doc-card") {
900
+ h3 {
901
+ a(doc[:title], href: "/\#{doc[:slug]}/")
902
+ }
903
+ p(doc[:description])
904
+ }
905
+ end
906
+ }
907
+ }
908
+ end
909
+ }
910
+ }
911
+ end
912
+ RUBY
913
+ end
914
+
915
+ def getting_started_content
916
+ <<~RUBY
917
+ site_config = data(:config)
918
+
919
+ Joys.html do
920
+ layout(:main) {
921
+ push(:title) { "Getting Started - \#{site_config.site_title}" }
922
+ push(:content) {
923
+ article {
924
+ h1 "Getting Started"
925
+ p "Welcome to the documentation!"
926
+
927
+ h2 "Installation"
928
+ pre {
929
+ code "gem install joys"
930
+ }
931
+
932
+ h2 "Quick Start"
933
+ p "Get up and running in minutes:"
934
+ ol {
935
+ li "Clone this repository"
936
+ li "Run `joys build`"
937
+ li "Open `dist/index.html` in your browser"
938
+ }
939
+
940
+ p "Edit content in the `content/` directory to customize your site."
941
+ }
942
+ }
943
+ }
944
+ end
945
+ RUBY
946
+ end
947
+
948
+ def api_index_content
949
+ <<~RUBY
950
+ site_config = data(:config)
951
+
952
+ Joys.html do
953
+ layout(:main) {
954
+ push(:title) { "API Reference - \#{site_config.site_title}" }
955
+ push(:content) {
956
+ article {
957
+ h1 "API Reference"
958
+ p "Complete API documentation for your project."
959
+
960
+ h2 "Endpoints"
961
+ p "Coming soon - add your API documentation here."
962
+ }
963
+ }
964
+ }
965
+ end
966
+ RUBY
967
+ end
968
+
969
+ def portfolio_homepage_content
970
+ <<~RUBY
971
+ site_config = data(:config)
972
+ featured_projects = data(:projects).featured.limit(3)
973
+
974
+ Joys.html do
975
+ layout(:main) {
976
+ push(:title) { site_config.site_title }
977
+ push(:content) {
978
+ section(cs: "hero") {
979
+ h1(site_config.site_title)
980
+ p(site_config.description)
981
+ a("View My Work →", href: "/projects/", cs: "btn btn-primary")
982
+ }
983
+
984
+ if featured_projects.any?
985
+ section(cs: "featured-projects") {
986
+ h2 "Featured Projects"
987
+ div(cs: "projects-grid") {
988
+ featured_projects.each do |project|
989
+ comp(:project_card, project)
990
+ end
991
+ }
992
+ a("View All Projects →", href: "/projects/", cs: "btn btn-secondary")
993
+ }
994
+ end
995
+ }
996
+ }
997
+ end
998
+ RUBY
999
+ end
1000
+
1001
+ def projects_listing_content
1002
+ <<~RUBY
1003
+ site_config = data(:config)
1004
+
1005
+ Joys.html do
1006
+ layout(:main) {
1007
+ push(:title) { "Projects - \#{site_config.site_title}" }
1008
+ push(:content) {
1009
+ h1 "Projects"
1010
+
1011
+ div(cs: "projects-grid") {
1012
+ data(:projects).each do |project|
1013
+ comp(:project_card, project)
1014
+ end
1015
+ }
1016
+ }
1017
+ }
1018
+ end
1019
+ RUBY
1020
+ end
1021
+
1022
+ def portfolio_about_content
1023
+ <<~RUBY
1024
+ site_config = data(:config)
1025
+
1026
+ Joys.html do
1027
+ layout(:main) {
1028
+ push(:title) { "About - \#{site_config.site_title}" }
1029
+ push(:content) {
1030
+ article {
1031
+ h1 "About Me"
1032
+ p "I'm a developer who loves building things."
1033
+ p "Check out my projects and get in touch!"
1034
+ }
1035
+ }
1036
+ }
1037
+ end
1038
+ RUBY
1039
+ end
1040
+
1041
+ def contact_page_content
1042
+ <<~RUBY
1043
+ site_config = data(:config)
1044
+
1045
+ Joys.html do
1046
+ layout(:main) {
1047
+ push(:title) { "Contact - \#{site_config.site_title}" }
1048
+ push(:content) {
1049
+ article {
1050
+ h1 "Contact"
1051
+ p "Get in touch!"
1052
+
1053
+ ul {
1054
+ li { a("Email: hello@example.com", href: "mailto:hello@example.com") }
1055
+ li { a("GitHub", href: "https://github.com/yourusername", target: "_blank") }
1056
+ li { a("Twitter", href: "https://twitter.com/yourusername", target: "_blank") }
1057
+ }
1058
+ }
1059
+ }
1060
+ }
1061
+ end
1062
+ RUBY
1063
+ end
1064
+
1065
+ def basic_homepage_content
1066
+ <<~RUBY
1067
+ site_config = data(:config)
1068
+
1069
+ Joys.html do
1070
+ layout(:main) {
1071
+ push(:title) { site_config.site_title }
1072
+ push(:content) {
1073
+ article {
1074
+ h1(site_config.site_title)
1075
+ p "Welcome to your new Joys site!"
1076
+ p "Edit this page in content/default/index.rb to get started."
1077
+ }
1078
+ }
1079
+ }
1080
+ end
1081
+ RUBY
1082
+ end
1083
+
1084
+ def basic_about_content
1085
+ <<~RUBY
1086
+ site_config = data(:config)
1087
+
1088
+ Joys.html do
1089
+ layout(:main) {
1090
+ push(:title) { "About - \#{site_config.site_title}" }
1091
+ push(:content) {
1092
+ article {
1093
+ h1 "About"
1094
+ p "This is the about page."
1095
+ p "Edit content/default/about.rb to customize it."
1096
+ }
1097
+ }
1098
+ }
1099
+ end
1100
+ RUBY
1101
+ end
1102
+
1103
+ def error_404_content
1104
+ <<~RUBY
1105
+ site_config = data(:config)
1106
+
1107
+ Joys.html do
1108
+ layout(:main) {
1109
+ push(:title) { "Page Not Found - \#{site_config.site_title}" }
1110
+ push(:content) {
1111
+ article(cs: "error-page") {
1112
+ h1 "Page Not Found"
1113
+ p "The page you're looking for doesn't exist."
1114
+ a("← Back to Home", href: "/", cs: "btn btn-primary")
1115
+ }
1116
+ }
1117
+ }
1118
+ end
1119
+ RUBY
1120
+ end
1121
+
1122
+ def error_500_content
1123
+ <<~RUBY
1124
+ site_config = data(:config)
1125
+
1126
+ Joys.html do
1127
+ layout(:main) {
1128
+ push(:title) { "Server Error - \#{site_config.site_title}" }
1129
+ push(:content) {
1130
+ article(cs: "error-page") {
1131
+ h1 "Server Error"
1132
+ p "Something went wrong while building this page."
1133
+ a("← Back to Home", href: "/", cs: "btn btn-primary")
1134
+ }
1135
+ }
1136
+ }
1137
+ end
1138
+ RUBY
1139
+ end
1140
+
1141
+ def css_content
1142
+ <<~CSS
1143
+ /* Basic styles for #{@project_name} */
1144
+
1145
+ * {
1146
+ margin: 0;
1147
+ padding: 0;
1148
+ box-sizing: border-box;
1149
+ }
1150
+
1151
+ body {
1152
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1153
+ line-height: 1.6;
1154
+ color: #333;
1155
+ }
1156
+
1157
+ .container {
1158
+ max-width: 1200px;
1159
+ margin: 0 auto;
1160
+ padding: 0 2rem;
1161
+ }
1162
+
1163
+ .site-header {
1164
+ background: #fff;
1165
+ border-bottom: 1px solid #eee;
1166
+ padding: 1rem 0;
1167
+ }
1168
+
1169
+ .site-header .container {
1170
+ display: flex;
1171
+ justify-content: space-between;
1172
+ align-items: center;
1173
+ }
1174
+
1175
+ .logo {
1176
+ font-size: 1.5rem;
1177
+ font-weight: 600;
1178
+ text-decoration: none;
1179
+ color: #333;
1180
+ }
1181
+
1182
+ nav a {
1183
+ margin-left: 2rem;
1184
+ text-decoration: none;
1185
+ color: #666;
1186
+ }
1187
+
1188
+ nav a:hover {
1189
+ color: #333;
1190
+ }
1191
+
1192
+ main {
1193
+ padding: 2rem 0;
1194
+ min-height: calc(100vh - 200px);
1195
+ }
1196
+
1197
+ .hero {
1198
+ text-align: center;
1199
+ padding: 4rem 0;
1200
+ background: #f8f9fa;
1201
+ margin: -2rem -2rem 2rem -2rem;
1202
+ }
1203
+
1204
+ .hero h1 {
1205
+ font-size: 3rem;
1206
+ margin-bottom: 1rem;
1207
+ }
1208
+
1209
+ .hero p {
1210
+ font-size: 1.2rem;
1211
+ color: #666;
1212
+ margin-bottom: 2rem;
1213
+ }
1214
+
1215
+ .btn {
1216
+ display: inline-block;
1217
+ padding: 0.75rem 1.5rem;
1218
+ text-decoration: none;
1219
+ border-radius: 4px;
1220
+ font-weight: 500;
1221
+ }
1222
+
1223
+ .btn-primary {
1224
+ background: #007bff;
1225
+ color: white;
1226
+ }
1227
+
1228
+ .btn-secondary {
1229
+ background: #6c757d;
1230
+ color: white;
1231
+ }
1232
+
1233
+ .btn:hover {
1234
+ opacity: 0.9;
1235
+ }
1236
+
1237
+ .site-footer {
1238
+ background: #f8f9fa;
1239
+ padding: 2rem 0;
1240
+ margin-top: 4rem;
1241
+ border-top: 1px solid #eee;
1242
+ text-align: center;
1243
+ color: #666;
1244
+ }
1245
+
1246
+ #{type_specific_css}
1247
+ CSS
1248
+ end
1249
+
1250
+ def type_specific_css
1251
+ case @type
1252
+ when 'blog'
1253
+ <<~CSS
1254
+
1255
+ .posts-grid {
1256
+ display: grid;
1257
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1258
+ gap: 2rem;
1259
+ margin: 2rem 0;
1260
+ }
1261
+
1262
+ .post-card {
1263
+ border: 1px solid #eee;
1264
+ border-radius: 8px;
1265
+ padding: 1.5rem;
1266
+ background: #fff;
1267
+ }
1268
+
1269
+ .post-card h3 {
1270
+ margin-bottom: 0.5rem;
1271
+ }
1272
+
1273
+ .post-card time {
1274
+ color: #666;
1275
+ font-size: 0.9rem;
1276
+ }
1277
+
1278
+ .post-card p {
1279
+ margin: 1rem 0;
1280
+ }
1281
+
1282
+ .read-more {
1283
+ color: #007bff;
1284
+ text-decoration: none;
1285
+ font-weight: 500;
1286
+ }
1287
+ CSS
1288
+ when 'portfolio'
1289
+ <<~CSS
1290
+
1291
+ .projects-grid {
1292
+ display: grid;
1293
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
1294
+ gap: 2rem;
1295
+ margin: 2rem 0;
1296
+ }
1297
+
1298
+ .project-card {
1299
+ border: 1px solid #eee;
1300
+ border-radius: 8px;
1301
+ padding: 1.5rem;
1302
+ background: #fff;
1303
+ }
1304
+
1305
+ .tech-tag {
1306
+ display: inline-block;
1307
+ background: #e9ecef;
1308
+ color: #495057;
1309
+ padding: 0.25rem 0.5rem;
1310
+ border-radius: 3px;
1311
+ font-size: 0.8rem;
1312
+ margin-right: 0.5rem;
1313
+ margin-bottom: 0.5rem;
1314
+ }
1315
+ CSS
1316
+ else
1317
+ ""
1318
+ end
1319
+ end
1320
+
1321
+ def blog_data_content
1322
+ <<~RUBY
1323
+ Joys::Data.define :posts do
1324
+ from_array([
1325
+ {
1326
+ title: "Welcome to Your New Blog",
1327
+ slug: "welcome",
1328
+ excerpt: "This is your first post. Edit hash/posts_hash.rb to add more content.",
1329
+ content: "<p>Welcome to your new blog built with Joys!</p><p>This is your first post. You can edit this content in <code>hash/posts_hash.rb</code> or replace this file-based approach with your preferred data source.</p><p>Joys makes it easy to create fast, beautiful static sites with Ruby.</p>",
1330
+ published_at: Date.current,
1331
+ featured: true,
1332
+ published: true
1333
+ },
1334
+ {
1335
+ title: "Getting Started with Joys",
1336
+ slug: "getting-started",
1337
+ excerpt: "Learn the basics of building sites with Joys static site generator.",
1338
+ content: "<p>Joys is a powerful static site generator built for Ruby developers.</p><h2>Key Features</h2><ul><li>Ruby-based templates</li><li>Component system</li><li>Asset pipeline with hashing</li><li>Live reload development server</li></ul>",
1339
+ published_at: Date.current - 1,
1340
+ featured: false,
1341
+ published: true
1342
+ },
1343
+ {
1344
+ title: "Customizing Your Site",
1345
+ slug: "customizing",
1346
+ excerpt: "How to customize layouts, components, and styling in your Joys site.",
1347
+ content: "<p>Customizing your Joys site is straightforward:</p><h2>Layouts</h2><p>Edit files in <code>content/_layouts/</code> to change page structure.</p><h2>Components</h2><p>Create reusable components in <code>content/_components/</code>.</p><h2>Styling</h2><p>Add CSS to <code>content/default/assets/style.css</code> or use individual component styles.</p>",
1348
+ published_at: Date.current - 7,
1349
+ featured: false,
1350
+ published: true
1351
+ }
1352
+ ])
1353
+
1354
+ scope({
1355
+ recent: ->(n=5) { order(:published_at, desc: true).limit(n) },
1356
+ featured: -> { where(featured: true) },
1357
+ published: -> { where(published: true) }
1358
+ })
1359
+ end
1360
+ RUBY
1361
+ end
1362
+
1363
+ def docs_data_content
1364
+ <<~RUBY
1365
+ Joys::Data.define :docs do
1366
+ from_array([
1367
+ {
1368
+ title: "Getting Started",
1369
+ slug: "getting-started",
1370
+ description: "Learn the basics of using this project",
1371
+ content: "<p>Welcome to the documentation!</p><p>This is a sample documentation site built with Joys.</p>",
1372
+ order: 1
1373
+ },
1374
+ {
1375
+ title: "API Reference",
1376
+ slug: "api",
1377
+ description: "Complete API documentation",
1378
+ content: "<p>API documentation goes here.</p>",
1379
+ order: 2
1380
+ },
1381
+ {
1382
+ title: "Examples",
1383
+ slug: "examples",
1384
+ description: "Code examples and tutorials",
1385
+ content: "<p>Examples and tutorials will be added here.</p>",
1386
+ order: 3
1387
+ }
1388
+ ])
1389
+
1390
+ scope({
1391
+ ordered: -> { order(:order) }
1392
+ })
1393
+ end
1394
+ RUBY
1395
+ end
1396
+
1397
+ def portfolio_data_content
1398
+ <<~RUBY
1399
+ Joys::Data.define :projects do
1400
+ from_array([
1401
+ {
1402
+ title: "My Awesome App",
1403
+ slug: "awesome-app",
1404
+ description: "A web application built with modern technologies",
1405
+ content: "<p>This is a detailed description of my awesome app.</p><h2>Features</h2><ul><li>Feature 1</li><li>Feature 2</li><li>Feature 3</li></ul>",
1406
+ tech: ["Ruby", "Rails", "JavaScript"],
1407
+ github_url: "https://github.com/yourusername/awesome-app",
1408
+ demo_url: "https://awesome-app.example.com",
1409
+ featured: true
1410
+ },
1411
+ {
1412
+ title: "Cool Library",
1413
+ slug: "cool-library",
1414
+ description: "An open source library for developers",
1415
+ content: "<p>A useful library that solves common problems.</p>",
1416
+ tech: ["Ruby", "Gem"],
1417
+ github_url: "https://github.com/yourusername/cool-library",
1418
+ demo_url: nil,
1419
+ featured: true
1420
+ },
1421
+ {
1422
+ title: "Static Site",
1423
+ slug: "static-site",
1424
+ description: "A beautiful static site built with Joys",
1425
+ content: "<p>This portfolio site itself, built with Joys!</p>",
1426
+ tech: ["Ruby", "Joys", "CSS"],
1427
+ github_url: "https://github.com/yourusername/portfolio",
1428
+ demo_url: nil,
1429
+ featured: false
1430
+ }
1431
+ ])
1432
+
1433
+ scope({
1434
+ featured: -> { where(featured: true) },
1435
+ by_tech: ->(tech) { where(tech: { contains: tech }) }
1436
+ })
1437
+ end
1438
+ RUBY
1439
+ end
1440
+
1441
+ def config_data_content
1442
+ title = @project_name.tr('_-', ' ').split.map(&:capitalize).join(' ')
1443
+
1444
+ <<~RUBY
1445
+ Joys::Data.define :config do
1446
+ from_array([{
1447
+ site_title: "#{title}",
1448
+ description: "#{description_for_type}",
1449
+ author: "Your Name",
1450
+ url: "https://#{@github_pages ? 'yourusername.github.io/' + @project_name : 'example.com'}"
1451
+ }])
1452
+
1453
+ def method_missing(method, *args)
1454
+ data = first
1455
+ data[method] if data&.key?(method)
1456
+ end
1457
+ end
1458
+ RUBY
1459
+ end
1460
+
1461
+ def basic_config_data_content
1462
+ title = @project_name.tr('_-', ' ').split.map(&:capitalize).join(' ')
1463
+
1464
+ <<~RUBY
1465
+ Joys::Data.define :config do
1466
+ from_array([{
1467
+ site_title: "#{title}",
1468
+ description: "A site built with Joys",
1469
+ author: "Your Name"
1470
+ }])
1471
+
1472
+ def method_missing(method, *args)
1473
+ data = first
1474
+ data[method] if data&.key?(method)
1475
+ end
1476
+ end
1477
+ RUBY
1478
+ end
1479
+
1480
+ def description_for_type
1481
+ case @type
1482
+ when 'blog'
1483
+ "A blog built with Joys static site generator"
1484
+ when 'docs'
1485
+ "Documentation site built with Joys"
1486
+ when 'portfolio'
1487
+ "Portfolio site showcasing my work"
1488
+ else
1489
+ "A site built with Joys"
1490
+ end
1491
+ end
1492
+
1493
+ def github_actions_content
1494
+ <<~YAML
1495
+ name: Build and Deploy to GitHub Pages
1496
+
1497
+ on:
1498
+ push:
1499
+ branches: [ main ]
1500
+ pull_request:
1501
+ branches: [ main ]
1502
+
1503
+ permissions:
1504
+ contents: read
1505
+ pages: write
1506
+ id-token: write
1507
+
1508
+ concurrency:
1509
+ group: "pages"
1510
+ cancel-in-progress: false
1511
+
1512
+ jobs:
1513
+ build:
1514
+ runs-on: ubuntu-latest
1515
+ steps:
1516
+ - name: Checkout
1517
+ uses: actions/checkout@v4
1518
+
1519
+ - name: Setup Ruby
1520
+ uses: ruby/setup-ruby@v1
1521
+ with:
1522
+ ruby-version: '3.3'
1523
+ bundler-cache: true
1524
+
1525
+ - name: Setup Pages
1526
+ uses: actions/configure-pages@v4
1527
+
1528
+ - name: Build site
1529
+ run: ruby build.rb
1530
+
1531
+ - name: Upload artifact
1532
+ uses: actions/upload-pages-artifact@v3
1533
+ with:
1534
+ path: ./dist
1535
+
1536
+ deploy:
1537
+ environment:
1538
+ name: github-pages
1539
+ url: \\${{ steps.deployment.outputs.page_url }}
1540
+ runs-on: ubuntu-latest
1541
+ needs: build
1542
+ steps:
1543
+ - name: Deploy to GitHub Pages
1544
+ id: deployment
1545
+ uses: actions/deploy-pages@v4
1546
+ YAML
1547
+ end
1548
+ end
1549
+ end
1550
+
1551
+ # Run CLI if called directly
1552
+ if __FILE__ == $0
1553
+ Joys::CLI.new(ARGV).run
1554
+ end