Module: Abqari::Page::Bundle
- Included in:
- Abqari::Page
- Defined in:
- lib/abqari/page/bundle.rb
Overview
Bundle-asset discovery. A "bundle" is a directory containing
index.md plus colocated assets (hero image, body figures, AVIF/
WebP variants, etc.). Bundles are the default layout for posts
and photos; flat collections (publications, workshops) use a
single .md file with assets in the collection root.
External symlinks (real path escapes site_root) are filtered
via Site#external_symlink? so a hostile or accidental link
can't pull external files into the build.
Constant Summary collapse
- IMAGE_EXTENSIONS =
%w[.jpg .jpeg .png .gif .webp .avif .svg].freeze
- PUBLISHABLE_EXTENSIONS =
Extensions published from a bundle directory. An ALLOW-list, not a deny-list, and that direction is the whole point.
This used to publish everything that wasn't a directory, dotfile or
index.md. That shipped whatever an author happened to leave in the folder — including anotes.mdmarkedpublished: false, which went to production verbatim, frontmatter and all, at a guessable URL.published: falsehas to be a promise; a rule that publishes the file anyway makes the flag lie.Excluding
.mdalone would fix that case and leave the class: the next.psd,.sketch,.docxorworking-notes.rtfwould ship the same way. A deny-list has to be extended forever and fails open on everything nobody thought of; an allow-list fails closed.Generous on purpose, and extensible —
bundle_assets.extensions:in site.yml adds to this list, so a site publishing.gpxor.stlisn't stuck. Skipped files are WARNED about, never silently dropped: silent omission is the failure mode this is meant to end, and swapping one silence for another would miss the point.Note
.mdis absent deliberately — markdown is content, never an asset.BundleAssets#copy_flat_collection_assetshas always taken that line for flat collections; bundles simply never did, and the inconsistency is what let the leak through. %w[ .jpg .jpeg .png .gif .webp .avif .svg .ico .mp4 .webm .mov .m4v .mp3 .m4a .oga .ogg .opus .wav .flac .vtt .srt .pdf .epub .zip ].freeze
- CONTENT_EXTENSIONS =
Content, never assets — not publishable via
bundle_assets.extensions:either. Allowing markdown back in would republish exactly thepublished: falsesibling the allow-list exists to hold back, soSite#bundle_asset_extensionsfilters these out of user config rather than trusting nobody will try. %w[.md .markdown].freeze
Instance Method Summary collapse
-
#bundle_assets ⇒ Object
Files in the bundle dir that are published alongside the page — those whose extension is in the site's publishable set.
-
#bundle_dir ⇒ Object
Filesystem directory containing this page's bundle, or nil for flat-collection pages and top-level pages (no bundle).
-
#bundle_entries ⇒ Object
Candidate files in the bundle dir, before the extension filter.
-
#bundle_images ⇒ Object
URL paths (not filesystem paths) for every image the sitemap should advertise as belonging to this page.
- #publishable_bundle_file?(path) ⇒ Boolean
-
#unpublishable_bundle_files ⇒ Object
The complement: bundle files that will NOT be published.
Instance Method Details
#bundle_assets ⇒ Object
Files in the bundle dir that are published alongside the page — those whose extension is in the site's publishable set.
72 73 74 |
# File 'lib/abqari/page/bundle.rb', line 72 def bundle_assets bundle_entries.select { |entry| publishable_bundle_file?(entry) } end |
#bundle_dir ⇒ Object
Filesystem directory containing this page's bundle, or nil for flat-collection pages and top-level pages (no bundle).
63 64 65 66 67 68 |
# File 'lib/abqari/page/bundle.rb', line 63 def bundle_dir return nil unless collection cfg = site.collections[collection] cfg['bundle'] != false ? File.dirname(source_path) : nil end |
#bundle_entries ⇒ Object
Candidate files in the bundle dir, before the extension filter.
Skips directories, the index.md itself, dotfiles, and any symlink
that escapes site_root.
Dotfiles are the deliberate escape hatch for genuinely private
working files: Dir.glob skips them without FNM_DOTMATCH, so
a .notes.md is neither published as an asset nor loaded as a
page. That's what the skip warning points authors at.
92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/abqari/page/bundle.rb', line 92 def bundle_entries return [] unless bundle_dir # Sort for deterministic iteration order — without it, sitemap # image entries can shuffle between builds on the same machine # when the filesystem reorders inodes. Same content, different # bytes-on-disk → noisy diffs on every deploy. Dir.glob(File.join(bundle_dir, '*')).sort.reject do |entry| File.directory?(entry) || File.basename(entry) == 'index.md' || File.basename(entry).start_with?('.') || site.external_symlink?(entry) end end |
#bundle_images ⇒ Object
URL paths (not filesystem paths) for every image the sitemap should advertise as belonging to this page. Two sources:
1. Files in the bundle dir matching IMAGE_EXTENSIONS — the
common case (colocated hero + body figures). Converted
from filesystem path to URL path: `<page.url><basename>`.
2. Frontmatter `image:` / `cover:` / `cover_image:` values
that point at absolute same-origin paths (e.g.
`/assets/hero.jpg`). Bundle-relative values are skipped
because they're already covered by source #1 above.
External URLs (http/https) aren't claimed.
Inline body images aren't extracted — parsing markdown is expensive and the image sitemap is meant for primary content, not embedded illustrations.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/abqari/page/bundle.rb', line 126 def bundle_images results = [] bundle_assets.each do |path| next unless IMAGE_EXTENSIONS.include?(File.extname(path).downcase) # Percent-encode the basename: it comes off the filesystem, so # it can contain spaces or non-ASCII, and a sitemap # `<image:loc>` with a literal space is invalid (XML-escaping # doesn't help — that's a different layer). Only the # filesystem-derived part is encoded; frontmatter values below # are author-written URLs and may already be encoded, so # touching them risks double-encoding. results << "#{url}#{URI::DEFAULT_PARSER.escape(File.basename(path))}" end %w[image cover cover_image].each do |key| raw = frontmatter[key].to_s next if raw.empty? next if raw.start_with?('http://', 'https://') next unless raw.start_with?('/') results << raw unless results.include?(raw) end results end |
#publishable_bundle_file?(path) ⇒ Boolean
107 108 109 |
# File 'lib/abqari/page/bundle.rb', line 107 def publishable_bundle_file?(path) site.bundle_asset_extensions.include?(File.extname(path).downcase) end |
#unpublishable_bundle_files ⇒ Object
The complement: bundle files that will NOT be published. The
caller (BundleAssets#copy_bundle_assets) warns about these once
per build so an author can see what was left behind and either
allow the extension or move the file out.
80 81 82 |
# File 'lib/abqari/page/bundle.rb', line 80 def unpublishable_bundle_files bundle_entries.reject { |entry| publishable_bundle_file?(entry) } end |