Class: Seo::Output::Reporter

Inherits:
Object
  • Object
show all
Defined in:
lib/seo/output/reporter.rb

Constant Summary collapse

INDIGO =
"4f46e5"
RED =
"dc2626"
AMBER =
"d97706"
GREEN =
"16a34a"
SLATE =
"475569"
LIGHT =
"f1f5f9"
WHITE =
"ffffff"

Instance Method Summary collapse

Instance Method Details

#render(results, format:) ⇒ Object



17
18
19
20
21
22
23
# File 'lib/seo/output/reporter.rb', line 17

def render(results, format:)
  case format.to_s
  when "json" then render_json(results)
  when "html" then render_html(results)
  else raise ArgumentError, "Unknown format: #{format}"
  end
end

#render_pdf(results, output_path:) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/seo/output/reporter.rb', line 25

def render_pdf(results, output_path:)
  avg_score     = results.sum(&:score) / [results.length, 1].max
  total_issues  = results.sum { |r| r.issues.length }
  clean_count   = results.count { |r| r.issues.empty? }

  Prawn::Document.generate(output_path, page_size: "A4", margin: [36, 40, 36, 40]) do |pdf|
    # ── Header ───────────────────────────────────────────────────────
    pdf.fill_color INDIGO
    pdf.fill_rectangle [pdf.bounds.left, pdf.bounds.top + 36], pdf.bounds.width + 80, 70
    pdf.fill_color WHITE
    pdf.move_to [pdf.bounds.left, pdf.bounds.top + 20]
    pdf.font_size(18) { pdf.text "SEO Report", style: :bold }
    pdf.font_size(9)  { pdf.text "Generated #{Time.now.strftime("%d %b %Y %H:%M")}  ·  #{results.length} files scanned", color: "c7d2fe" }
    pdf.move_down 20

    # ── Summary cards ────────────────────────────────────────────────
    pdf.fill_color "000000"
    summary_data = [
      ["Average Score", "#{avg_score}/100", score_color(avg_score)],
      ["Total Issues",  total_issues.to_s,  total_issues.zero? ? GREEN : RED],
      ["Clean Files",   "#{clean_count}/#{results.length}", clean_count == results.length ? GREEN : AMBER]
    ]

    card_width  = (pdf.bounds.width - 20) / 3.0
    card_height = 52

    summary_data.each_with_index do |(label, value, color), i|
      x = pdf.bounds.left + i * (card_width + 10)
      y = pdf.cursor

      pdf.fill_color LIGHT
      pdf.fill_rounded_rectangle [x, y], card_width, card_height, 4
      pdf.fill_color "94a3b8"
      pdf.font_size(7) { pdf.draw_text label.upcase, at: [x + 10, y - 14] }
      pdf.fill_color color
      pdf.font_size(20) { pdf.draw_text value, at: [x + 10, y - 38], style: :bold }
    end

    pdf.move_down card_height + 16

    # ── File results ─────────────────────────────────────────────────
    results.each do |result|
      pdf.fill_color "000000"

      # Card background
      card_start_y = pdf.cursor
      pdf.fill_color LIGHT
      pdf.fill_rounded_rectangle [pdf.bounds.left, card_start_y], pdf.bounds.width, 10, 4

      # File name
      pdf.fill_color INDIGO
      pdf.font_size(8) { pdf.font("Courier") { pdf.text result.file, style: :bold } }
      pdf.move_down 4

      if result.issues.empty?
        pdf.fill_color GREEN
        pdf.font_size(9) { pdf.text "✓ No issues found" }
      else
        result.issues.each do |issue|
          sev_color = issue[:severity] == :critical ? RED : AMBER
          pdf.fill_color sev_color
          pdf.font_size(7.5) do
            pdf.text "[#{issue[:severity].to_s.upcase}] [#{issue[:check]}]  #{issue[:message]}", indent_paragraphs: 8
          end

          if issue[:ai_risk] && issue[:ai_risk] != "none"
            risk_color = { "high" => RED, "medium" => AMBER, "low" => GREEN }.fetch(issue[:ai_risk], SLATE)
            pdf.fill_color risk_color
            pdf.font_size(7) { pdf.text "  AI Overview risk: #{issue[:ai_risk].upcase}", indent_paragraphs: 16 }
          end
        end

        unless result.suggestions.empty?
          pdf.move_down 3
          pdf.fill_color SLATE
          result.suggestions.each do |s|
            pdf.font_size(7.5) { pdf.text "#{s}", indent_paragraphs: 8 }
          end
        end
      end

      # Score
      pdf.move_down 4
      pdf.fill_color "94a3b8"
      pdf.font_size(7) { pdf.text "Score: #{result.score}/100", align: :right, color: score_color(result.score) }
      pdf.move_down 10

      # Stretch card bg to actual height
      card_height_actual = card_start_y - pdf.cursor
      pdf.fill_color LIGHT
      pdf.fill_rounded_rectangle [pdf.bounds.left, card_start_y], pdf.bounds.width, card_height_actual, 4
      pdf.move_down 8

      pdf.start_new_page if pdf.cursor < 80
    end
  end
end