joys 0.1.2 → 0.1.4
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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +321 -150
- data/joys-0.1.2.gem +0 -0
- data/joys-0.1.3.gem +0 -0
- data/lib/.DS_Store +0 -0
- data/lib/joys/cli.rb +1554 -0
- data/lib/joys/config.rb +133 -0
- data/lib/joys/core.rb +70 -42
- data/lib/joys/data.rb +477 -0
- data/lib/joys/helpers.rb +36 -9
- data/lib/joys/ssg.rb +597 -0
- data/lib/joys/tags.rb +4 -1
- data/lib/joys/toys.rb +328 -0
- data/lib/joys/version.rb +1 -1
- data/lib/joys.rb +2 -1
- metadata +9 -3
- data/.DS_Store +0 -0
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
|