Module: Abqari::SiteConfig

Defined in:
lib/abqari/site_config.rb

Overview

Lightweight schema check for config/site.yml. The goal is not full schema validation β€” yagni for a single-author tool β€” but to catch the typo class of bug:

manifest:
theme_colour: '#000'    # πŸ› should be `theme_color`

…which silently no-ops and leaves the operator wondering why their PWA install icon doesn't pick up the brand colour.

Strategy: known top-level keys are allow-listed below. Unknown keys warn by default (so existing sites don't break on upgrade), and raise when ABQARI_STRICT_CONFIG=true. CI workflows that want the gate enforced flip the env var. The plan is to flip the default to raise once the warn-mode period has passed.

Reserved keys we tolerate without warning:

- `environments` is structural (per-env overrides).
- Anything beginning with `x_` is treated as a user extension
namespace, mirroring HTTP/MIME conventions. Lets a consuming
site park its own metadata in the same file without false-
positive warnings.

Constant Summary collapse

DEFAULTS =

Static defaults applied to every site. User values in config/site.yml deep-merge over these β€” any user-set key wins. Only static defaults belong here; dynamic ones (minify = env == :production) stay in their callers.

Adding a default here removes a config.fetch('key', value) call elsewhere β€” one auditable place for the defaults the engine assumes, instead of scattered ternaries.

{
  'indexable'           => true,
  'block_ai_scraping'   => true,
  'syntax_highlighting' => true,
  'erb_in_markdown'     => false,
  'theme_toggle'        => true,
  # The default here (not buried in AssetPipeline) so EVERY reader
  # of config['theme'] agrees. When it lived pipeline-side, a site
  # with no `theme:` key built the minimal theme but the renderer's
  # theme-mtime scoping read '' β€” so theme CSS edits rebuilt assets
  # without re-rendering pages, and hot reload served stale hrefs.
  'theme'               => 'minimal'
}.freeze
KNOWN_KEYS =

Every top-level key the engine reads anywhere (rb + erb). Keep in lockstep with the readers β€” when adding a new top-level config key, add it here too. CI's audit catches drift.

%w[
  author
  block_ai_scraping
  bundle_assets
  collections
  contact
  csp
  deploy
  description
  drafts
  environments
  erb_in_markdown
  folio
  fonts
  fonts_preload
  footer
  headers
  home
  icons
  images
  indexable
  indieweb
  llms
  locale
  manifest
  media
  minify
  nav
  newsletter
  og_images
  og_locale
  pagination
  plausible
  robots_disallow
  search
  security_contact
  share_image
  social
  speculation_rules
  syndication
  syntax_highlighting
  syntax_theme
  syntax_theme_dark
  taxonomies
  theme
  theme_toggle
  timezone
  title
  twitter
  url
  validate
  visualizations
].freeze

Class Method Summary collapse

Class Method Details

.deep_merge(base, override) ⇒ Object



118
119
120
121
122
# File 'lib/abqari/site_config.rb', line 118

def deep_merge(base, override)
  base.merge(override) do |_, b, o|
    b.is_a?(Hash) && o.is_a?(Hash) ? deep_merge(b, o) : o
  end
end

.validate!(config, strict: nil, env: nil) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/abqari/site_config.rb', line 124

def validate!(config, strict: nil, env: nil)
  strict = ENV['ABQARI_STRICT_CONFIG'] == 'true' if strict.nil?

  validate_required!(config, strict: strict, env: env)

  unknown = config.keys.map(&:to_s).reject do |key|
    KNOWN_KEYS.include?(key) || key.start_with?('x_')
  end

  return if unknown.empty?

  # The advice differs by mode. The old message ended "…or set
  # ABQARI_STRICT_CONFIG=true to make this a hard error" in BOTH
  # branches β€” including the one raised *because* it was already set,
  # which told the user to enable the thing that had just fired.
  base = "Unknown config key(s) in site.yml: #{unknown.sort.join(', ')}. " \
         'Typos silently no-op; add an `x_` prefix for intentional user extensions'

  if strict
    raise UserError, "#{base} (ABQARI_STRICT_CONFIG is on, so this is fatal)."
  end

  Log.warn "#{base}, or set ABQARI_STRICT_CONFIG=true to make this a hard error."
end

.validate_required!(config, strict:, env:) ⇒ Object

Keys that must be present for a correct build. title feeds

, RSS, OG; a blank one ships "Page Β· " everywhere. `url` is the base for every absolute URL (canonical, sitemap, feed, JSON-LD) β€” blank in production means no sitemap, no canonical, and invalid feed URLs. Warn on a blank title always; escalate a blank `url` in production (raise under strict, warn otherwise) since a live build with no base URL is almost always a mistake. </div> </div> <div class="tags"> </div><table class="source_code"> <tr> <td> <pre class="lines"> 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174</pre> </td> <td> <pre class="code"><span class="info file"># File 'lib/abqari/site_config.rb', line 156</span> <span class='kw'>def</span> <span class='id identifier rubyid_validate_required!'>validate_required!</span><span class='lparen'>(</span><span class='id identifier rubyid_config'>config</span><span class='comma'>,</span> <span class='label'>strict:</span><span class='comma'>,</span> <span class='label'>env:</span><span class='rparen'>)</span> <span class='kw'>if</span> <span class='id identifier rubyid_config'>config</span><span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>title</span><span class='tstring_end'>'</span></span><span class='rbracket'>]</span><span class='period'>.</span><span class='id identifier rubyid_to_s'>to_s</span><span class='period'>.</span><span class='id identifier rubyid_strip'>strip</span><span class='period'>.</span><span class='id identifier rubyid_empty?'>empty?</span> <span class='const'><span class='object_link'><a href="Log.html" title="Abqari::Log (module)">Log</a></span></span><span class='period'>.</span><span class='id identifier rubyid_warn'><span class='object_link'><a href="Log.html#warn-class_method" title="Abqari::Log.warn (method)">warn</a></span></span> <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>Missing `title` in site.yml β€” pages will render with an empty <title>.</span><span class='tstring_end'>'</span></span> <span class='kw'>end</span> <span class='kw'>if</span> <span class='id identifier rubyid_config'>config</span><span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>url</span><span class='tstring_end'>'</span></span><span class='rbracket'>]</span><span class='period'>.</span><span class='id identifier rubyid_to_s'>to_s</span><span class='period'>.</span><span class='id identifier rubyid_strip'>strip</span><span class='period'>.</span><span class='id identifier rubyid_empty?'>empty?</span> <span class='id identifier rubyid_url_msg'>url_msg</span> <span class='op'>=</span> <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>Missing `url` in site.yml β€” canonical URLs, sitemap, and feeds </span><span class='tstring_end'>'</span></span> \ <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>need an absolute base URL. Set `url:` (per-environment under </span><span class='tstring_end'>'</span></span> \ <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>`environments:` is fine).</span><span class='tstring_end'>'</span></span> <span class='kw'>if</span> <span class='id identifier rubyid_env'>env</span><span class='period'>.</span><span class='id identifier rubyid_to_s'>to_s</span> <span class='op'>==</span> <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>production</span><span class='tstring_end'>'</span></span> <span class='id identifier rubyid_raise'>raise</span> <span class='const'><span class='object_link'><a href="UserError.html" title="Abqari::UserError (class)">UserError</a></span></span><span class='comma'>,</span> <span class='id identifier rubyid_url_msg'>url_msg</span> <span class='kw'>if</span> <span class='id identifier rubyid_strict'>strict</span> <span class='const'><span class='object_link'><a href="Log.html" title="Abqari::Log (module)">Log</a></span></span><span class='period'>.</span><span class='id identifier rubyid_warn'><span class='object_link'><a href="Log.html#warn-class_method" title="Abqari::Log.warn (method)">warn</a></span></span> <span class='id identifier rubyid_url_msg'>url_msg</span> <span class='kw'>end</span> <span class='kw'>return</span> <span class='kw'>end</span> <span class='id identifier rubyid_validate_url_has_no_path!'>validate_url_has_no_path!</span><span class='lparen'>(</span><span class='id identifier rubyid_config'>config</span><span class='lbracket'>[</span><span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>url</span><span class='tstring_end'>'</span></span><span class='rbracket'>]</span><span class='rparen'>)</span> <span class='kw'>end</span></pre> </td> </tr> </table> </div> <div class="method_details "> <h3 class="signature " id="validate_url_has_no_path!-class_method"> .<strong>validate_url_has_no_path!</strong>(url) ⇒ <tt>Object</tt> </h3><div class="docstring"> <div class="discussion"> <p>Abqari emits root-relative asset and internal-link URLs (<code>/assets/…</code>, <code>/posts/…</code>, <code>/feed.xml</code>), so a <code>url:</code> with a path component (<code>https://user.github.io/repo</code>) produces a site whose every asset and link 404s under the prefix. Subdirectory/base-path deployment is not supported; fail loudly at config time instead of shipping a broken site. A bare host or a lone <code>/</code> path is fine.</p> </div> </div> <div class="tags"> <p class="tag_title">Raises:</p> <ul class="raise"> <li> <span class='type'>(<tt><span class='object_link'><a href="UserError.html" title="Abqari::UserError (class)">UserError</a></span></tt>)</span> </li> </ul> </div><table class="source_code"> <tr> <td> <pre class="lines"> 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210</pre> </td> <td> <pre class="code"><span class="info file"># File 'lib/abqari/site_config.rb', line 182</span> <span class='kw'>def</span> <span class='id identifier rubyid_validate_url_has_no_path!'>validate_url_has_no_path!</span><span class='lparen'>(</span><span class='id identifier rubyid_url'>url</span><span class='rparen'>)</span> <span class='id identifier rubyid_parsed'>parsed</span> <span class='op'>=</span> <span class='kw'>begin</span> <span class='const'>URI</span><span class='period'>.</span><span class='id identifier rubyid_parse'>parse</span><span class='lparen'>(</span><span class='id identifier rubyid_url'>url</span><span class='period'>.</span><span class='id identifier rubyid_to_s'>to_s</span><span class='rparen'>)</span> <span class='kw'>rescue</span> <span class='const'>URI</span><span class='op'>::</span><span class='const'>InvalidURIError</span> <span class='kw'>nil</span> <span class='comment'># a malformed URL is a separate problem; don't crash here </span> <span class='kw'>end</span> <span class='kw'>return</span> <span class='kw'>if</span> <span class='id identifier rubyid_parsed'>parsed</span><span class='period'>.</span><span class='id identifier rubyid_nil?'>nil?</span> <span class='comment'># Check the scheme FIRST. Without one, `URI.parse('example.com')` </span> <span class='comment'># puts the whole string in `path`, so the path check below fired </span> <span class='comment'># and reported that `url` "has a path component ("example.com")" β€” </span> <span class='comment'># technically true, humanly baffling, and pointing at the wrong </span> <span class='comment'># mistake. </span> <span class='kw'>if</span> <span class='id identifier rubyid_parsed'>parsed</span><span class='period'>.</span><span class='id identifier rubyid_scheme'>scheme</span><span class='period'>.</span><span class='id identifier rubyid_to_s'>to_s</span><span class='period'>.</span><span class='id identifier rubyid_empty?'>empty?</span> <span class='id identifier rubyid_raise'>raise</span> <span class='const'><span class='object_link'><a href="UserError.html" title="Abqari::UserError (class)">UserError</a></span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>`url` (</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_url'>url</span><span class='period'>.</span><span class='id identifier rubyid_inspect'>inspect</span><span class='embexpr_end'>}</span><span class='tstring_content'>) is missing a scheme. Use the full origin, </span><span class='tstring_end'>"</span></span> \ <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>e.g. \"https://</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_url'>url</span><span class='period'>.</span><span class='id identifier rubyid_to_s'>to_s</span><span class='period'>.</span><span class='id identifier rubyid_sub'>sub</span><span class='lparen'>(</span><span class='tstring'><span class='regexp_beg'>%r{</span><span class='tstring_content'>\A/+</span><span class='regexp_end'>}</span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_end'>'</span></span><span class='rparen'>)</span><span class='embexpr_end'>}</span><span class='tstring_content'>\".</span><span class='tstring_end'>"</span></span> <span class='kw'>end</span> <span class='id identifier rubyid_path'>path</span> <span class='op'>=</span> <span class='id identifier rubyid_parsed'>parsed</span><span class='period'>.</span><span class='id identifier rubyid_path'>path</span><span class='period'>.</span><span class='id identifier rubyid_to_s'>to_s</span> <span class='kw'>return</span> <span class='kw'>if</span> <span class='id identifier rubyid_path'>path</span><span class='period'>.</span><span class='id identifier rubyid_empty?'>empty?</span> <span class='op'>||</span> <span class='id identifier rubyid_path'>path</span> <span class='op'>==</span> <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>/</span><span class='tstring_end'>'</span></span> <span class='id identifier rubyid_raise'>raise</span> <span class='const'><span class='object_link'><a href="UserError.html" title="Abqari::UserError (class)">UserError</a></span></span><span class='comma'>,</span> <span class='tstring'><span class='tstring_beg'>"</span><span class='tstring_content'>`url` (</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_url'>url</span><span class='period'>.</span><span class='id identifier rubyid_inspect'>inspect</span><span class='embexpr_end'>}</span><span class='tstring_content'>) has a path component (</span><span class='embexpr_beg'>#{</span><span class='id identifier rubyid_path'>path</span><span class='period'>.</span><span class='id identifier rubyid_inspect'>inspect</span><span class='embexpr_end'>}</span><span class='tstring_content'>). </span><span class='tstring_end'>"</span></span> \ <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>Abqari does not support subdirectory/base-path deployment β€” every </span><span class='tstring_end'>'</span></span> \ <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>asset and internal link is root-relative and would 404 under a path </span><span class='tstring_end'>'</span></span> \ <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>prefix. Deploy at a domain root (a custom domain, or a `<user>.github.io` </span><span class='tstring_end'>'</span></span> \ <span class='tstring'><span class='tstring_beg'>'</span><span class='tstring_content'>root repository).</span><span class='tstring_end'>'</span></span> <span class='kw'>end</span></pre> </td> </tr> </table> </div> <div class="method_details "> <h3 class="signature " id="with_defaults-class_method"> .<strong>with_defaults</strong>(config) ⇒ <tt>Object</tt> </h3><div class="docstring"> <div class="discussion"> <p>Deep-merge DEFAULTS under the user's config. Returns a new hash; the input isn't mutated. Use at boot to fold in static defaults so call sites can do <code>config['key']</code> instead of <code>config.fetch('key', sane_default)</code>.</p> </div> </div> <div class="tags"> </div><table class="source_code"> <tr> <td> <pre class="lines"> 114 115 116</pre> </td> <td> <pre class="code"><span class="info file"># File 'lib/abqari/site_config.rb', line 114</span> <span class='kw'>def</span> <span class='id identifier rubyid_with_defaults'>with_defaults</span><span class='lparen'>(</span><span class='id identifier rubyid_config'>config</span><span class='rparen'>)</span> <span class='id identifier rubyid_deep_merge'>deep_merge</span><span class='lparen'>(</span><span class='const'><span class='object_link'><a href="#DEFAULTS-constant" title="Abqari::SiteConfig::DEFAULTS (constant)">DEFAULTS</a></span></span><span class='comma'>,</span> <span class='id identifier rubyid_config'>config</span> <span class='op'>||</span> <span class='lbrace'>{</span><span class='rbrace'>}</span><span class='rparen'>)</span> <span class='kw'>end</span></pre> </td> </tr> </table> </div> </div> </div> <div id="footer"> Generated on Fri Aug 7 00:51:16 2026 by <a href="https://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a> 0.9.45 (ruby-4.0.2). </div> </div> </body> </html>