Class: Abqari::SiteLoader

Inherits:
Object
  • Object
show all
Defined in:
lib/abqari/site_loader.rb

Overview

Site-loading collaborator. Owns reading the three classes of input that bootstrap a build:

- `config/site.yml` (and environment overrides)
- `content/**/*.md` (every page the build will render)
- `data/**/*.{yml,yaml,json}` (operator-curated reference tables)

Plus the nav/footer link cascade, which reads from both config (data/ files) and frontmatter — it's loader-shaped, so it lives here too.

No build-time mutation of pages happens in this class. Loaders return arrays/hashes; Site stores them. Filtering for drafts / future-dated / disabled / external-symlink pages happens at load time so downstream code sees a clean list.

Instance Method Summary collapse

Constructor Details

#initialize(site) ⇒ SiteLoader

Returns a new instance of SiteLoader.



22
23
24
# File 'lib/abqari/site_loader.rb', line 22

def initialize(site)
  @site = site
end

Instance Method Details

Footer cascade: same shape as nav.



181
182
183
# File 'lib/abqari/site_loader.rb', line 181

def footer_links
  explicit_links_from('footer') || frontmatter_links_for('footer')
end

#load_configObject

Parse config/site.yml, apply per-environment overrides via deep-merge. Returns a Hash. No env-var coercion or schema work happens here — SiteConfig.validate! handles that downstream.



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
# File 'lib/abqari/site_loader.rb', line 29

def load_config
  # Site config is site-only (no engine fallback) — every consuming
  # site declares its own theme, Folio config, etc.
  site_root = ENV['ABQARI_SITE_ROOT'] || Dir.pwd
  path = File.join(site_root, 'config', 'site.yml')
  unless File.exist?(path)
    # Silent `{}` here means "wrong directory" or a typo'd
    # ABQARI_SITE_ROOT produces a defaults-only build with no signal.
    # Warn loudly (and let ABQARI_STRICT_CONFIG make it fatal).
    msg = "config/site.yml not found at #{path} — building with defaults only " \
          '(no title, no url). Are you in the site directory?'
    raise UserError, msg if ENV['ABQARI_STRICT_CONFIG'] == 'true'

    Log.warn msg
    return {}
  end

  raw = load_yaml_file(path) || {}
  envs = raw.delete('environments') || {}
  env  = @site.env.to_s
  overrides = envs[env] || {}

  warn_on_unknown_env(env, envs)

  deep_merge(raw, overrides)
end

#load_dataObject

Read every data/**/*.{yml,yaml,json} file into a hash keyed by the path-relative-with-extension-stripped form:

data/nav.yml             → result['nav']
data/foo/bar.yml         → result['foo_bar']
data/testimonials.yml    → result['testimonials']

YAML files use YAML.safe_load with permitted_classes: [Date, Time] (the engine-wide allow-list) so naked dates and ISO timestamps round-trip without opening up arbitrary class instantiation.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/abqari/site_loader.rb', line 153

def load_data
  result = {}
  data_dir = File.join(@site.site_root, 'data')
  return result unless Dir.exist?(data_dir)

  Dir.glob(File.join(data_dir, '**', '*.{yml,yaml,json}')).sort.each do |path|
    rel = path.sub("#{data_dir}/", '')
    key = rel.sub(/\.(yml|yaml|json)\z/, '').tr('/', '_')
    result[key] = if path.end_with?('.json')
                    begin
                      JSON.parse(File.read(path, encoding: 'UTF-8'))
                    rescue JSON::ParserError => e
                      raise UserError,
                            "data/#{rel} — invalid JSON: #{e.message.lines.first.to_s.strip}"
                    end
                  else
                    load_yaml_file(path)
                  end
  end
  result
end

#load_pagesObject

Walk content/**/*.md. For each markdown file:

- Skip if it lives under a disabled collection's source dir.
- Skip if it's an external symlink (escapes site_root).
- Skip drafts unless ABQARI_INCLUDE_DRAFTS=true or drafts config
allows it in this env.
- Skip future-dated posts unless we're in dev or the env var
is set.
- Skip pages with `enabled: false` in frontmatter — total opt-out.

Returns the surviving array of Page instances.



81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/abqari/site_loader.rb', line 81

def load_pages
  pattern = File.join(@site.site_root, 'content', '**', '*.md')
  pages = []

  Dir.glob(pattern).sort.each do |path|
    next if disabled_collection_path?(path)
    next if @site.external_symlink?(path)
    next if stray_bundle_markdown?(path)

    page = Page.new(path, @site)
    next if page.disabled?
    next if page.draft?  && !include_drafts?
    next if page.future? && !include_future?

    pages << page
  end

  pages
end

#load_yaml_file(path) ⇒ Object

YAML.safe_load_file with the engine-wide permitted classes, and a syntax error turned into something a person can act on.

Psych's own message already names the file, line and column — the problem was that it arrived at the top of a ten-frame trace through Psych internals, which reads as an engine crash rather than "line 12 of your config has a stray colon". Keep Psych's detail, drop the frames (ABQARI_DEBUG=1 brings them back).



64
65
66
67
68
69
# File 'lib/abqari/site_loader.rb', line 64

def load_yaml_file(path)
  YAML.safe_load_file(path, permitted_classes: Abqari::YAML_PERMITTED_CLASSES)
rescue Psych::SyntaxError => e
  rel = path.sub("#{@site.site_root}/", '')
  raise UserError, "#{rel}:#{e.line}:#{e.column} — invalid YAML: #{e.problem} #{e.context}".strip
end

Nav cascade: data/nav.yml links: array > frontmatter nav: blocks.



176
177
178
# File 'lib/abqari/site_loader.rb', line 176

def nav_links
  explicit_links_from('nav') || frontmatter_links_for('nav')
end

#stray_bundle_markdown?(path) ⇒ Boolean

True for a markdown file sitting INSIDE a bundle directory that isn't that bundle's index.md — e.g. content/posts/my-post/notes.md.

A bundle is one content file plus its assets, so a second .md has no defined role and used to get two contradictory ones: it was copied verbatim into the output as a "bundle asset" (frontmatter and all, even when marked published: false), AND loaded here as a stray page rendering at /posts/my-post/notes/ — because Page#collection rejects it for not being an index, leaving it uncollected rather than dropping it. Published twice, in two forms, neither intended.

The asset half is fixed by the publishable-extension allow-list. This is the page half. Warn rather than skip silently: the author put the file there for a reason and needs to know it does nothing.

Returns:

  • (Boolean)


117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/abqari/site_loader.rb', line 117

def stray_bundle_markdown?(path)
  return false if File.basename(path) == 'index.md'

  @site.collections.each do |_name, cfg|
    next unless cfg.fetch('enabled', true)
    next if cfg['bundle'] == false # flat collections: `<slug>.md` IS the page

    source = File.join(@site.site_root, cfg['source'].to_s)
    next unless path.start_with?(source + File::SEPARATOR)

    # Depth >= 2 means it's nested inside a slug directory rather
    # than sitting at the collection root.
    next unless path.sub("#{source}/", '').split('/').length >= 2

    rel = path.sub("#{@site.site_root}/", '')
    Log.warn "Skipping #{rel} — only `index.md` is rendered inside a bundle " \
             'directory, so this file is neither published nor rendered. Move it ' \
             'out to publish it, or rename it with a leading dot ' \
             "(`.#{File.basename(path)}`) to keep it local and silence this."
    return true
  end

  false
end