Class: Seo::ArticleLinkAuditor

Inherits:
BaseService show all
Defined in:
app/services/seo/article_link_auditor.rb

Overview

Unified per-article link auditor that combines internal and external link checking.

Extracts all links from an article's HTML content, validates internal links
via InternalLinkValidator and external links via LinkAnalyzer, and returns
a unified result. Optionally persists the audit result as a JSONB column
on the article for fast re-display without re-scanning.

Usage:
result = Seo::ArticleLinkAuditor.new.audit(article)
result.internal_broken # => [{ path:, suggestion: }, ...]
result.external_broken # => [{ href:, status:, redirect: }, ...]
result.all_valid? # => true/false

Persist result for cache (reads without re-audit)

Seo::ArticleLinkAuditor.new.audit_and_persist!(article)

Defined Under Namespace

Classes: AuditResult

Instance Method Summary collapse

Methods inherited from BaseService

#initialize, #log_debug, #log_error, #log_info, #log_warning, #logger, #options, #process, #tagged_logger

Constructor Details

This class inherits a constructor from BaseService

Instance Method Details

#audit(article, skip_http_ping: false, skip_external: false) ⇒ AuditResult

Run a full audit on the given article (internal + external links).

Parameters:

  • article (Article)
  • skip_http_ping (Boolean) (defaults to: false)

    skip HTTP fallback for internal link checks

  • skip_external (Boolean) (defaults to: false)

    skip external link checking entirely (faster)

Returns:



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
# File 'app/services/seo/article_link_auditor.rb', line 60

def audit(article, skip_http_ping: false, skip_external: false)
  html = active_html(article)
  return AuditResult.new if html.blank?

  # Internal link validation
  internal_result = Seo::InternalLinkValidator.new.process(html, skip_http_ping: skip_http_ping)

  internal_broken = internal_result.broken_links.map do |bl|
    { path: bl.path, href: bl.href, suggestion: bl.suggestion }
  end

  # External link validation
  external_broken = []
  external_checked = 0

  unless skip_external
    analyzer = Seo::LinkAnalyzer.new
    link_analysis_result = analyzer.process(html)

    if link_analysis_result.status == :ok
      link_analysis_result.link_analysis.each do |href, check|
        external_checked += 1
        status_code = check[:result].to_i
        next if status_code.in?(200..399)

        external_broken << {
          href: href,
          status: status_code,
          redirect: check[:location]
        }
      end
    end
  end

  AuditResult.new(
    internal_checked: internal_result.checked_count,
    internal_broken: internal_broken,
    external_checked: external_checked,
    external_broken: external_broken
  )
end

#audit_and_persist!(article, skip_external: false) ⇒ AuditResult

Run audit and persist the result to the article's link_audit_result column.
Also upserts the editorial link graph as a side effect.

Parameters:

  • article (Article)
  • skip_external (Boolean) (defaults to: false)

Returns:



108
109
110
111
112
113
114
115
116
117
118
119
# File 'app/services/seo/article_link_auditor.rb', line 108

def audit_and_persist!(article, skip_external: false)
  result = audit(article, skip_external: skip_external)

  begin
    article.update_column(:link_audit_result, result.to_h) if article.respond_to?(:link_audit_result)
    Seo::InternalLinkValidator.upsert_editorial_links!(article)
  rescue StandardError => e
    Rails.logger.warn "[ArticleLinkAuditor] Failed to persist audit for Article##{article.id}: #{e.message}"
  end

  result
end