abqari 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +889 -0
- data/LICENSE +21 -0
- data/README.md +2739 -0
- data/SECURITY.md +73 -0
- data/THIRD_PARTY_NOTICES.md +46 -0
- data/app/assets/css/_common.css +3125 -0
- data/app/assets/js/carousel.js +39 -0
- data/app/assets/js/contact-button.js +28 -0
- data/app/assets/js/random-pool.js +44 -0
- data/app/assets/js/search-init.js +11 -0
- data/app/assets/js/theme-toggle.js +39 -0
- data/app/assets/js/viz-tag-bubble.js +108 -0
- data/app/assets/js/viz-tag-cooccurrence.js +125 -0
- data/app/assets/js/viz-topic-constellation.js +142 -0
- data/app/icons/bluesky.svg +3 -0
- data/app/icons/github.svg +3 -0
- data/app/icons/linkedin.svg +3 -0
- data/app/icons/mail.svg +4 -0
- data/app/icons/mastodon.svg +3 -0
- data/app/icons/moon.svg +3 -0
- data/app/icons/rss.svg +3 -0
- data/app/icons/sun.svg +4 -0
- data/app/icons/x.svg +3 -0
- data/app/views/layouts/about.html.erb +70 -0
- data/app/views/layouts/application.html.erb +65 -0
- data/app/views/layouts/book_series_index.html.erb +43 -0
- data/app/views/layouts/bundle.html.erb +111 -0
- data/app/views/layouts/bundles_index.html.erb +46 -0
- data/app/views/layouts/country_index.html.erb +59 -0
- data/app/views/layouts/gallery_index.html.erb +126 -0
- data/app/views/layouts/home.html.erb +107 -0
- data/app/views/layouts/photo.html.erb +100 -0
- data/app/views/layouts/photos_archive.html.erb +101 -0
- data/app/views/layouts/photos_index.html.erb +204 -0
- data/app/views/layouts/post.html.erb +153 -0
- data/app/views/layouts/posts_archive.html.erb +98 -0
- data/app/views/layouts/posts_index.html.erb +80 -0
- data/app/views/layouts/publication.html.erb +116 -0
- data/app/views/layouts/publications_archive.html.erb +89 -0
- data/app/views/layouts/publications_index.html.erb +246 -0
- data/app/views/layouts/related_index.html.erb +48 -0
- data/app/views/layouts/series.html.erb +110 -0
- data/app/views/layouts/series_index.html.erb +202 -0
- data/app/views/layouts/tag.html.erb +16 -0
- data/app/views/layouts/visualization.html.erb +32 -0
- data/app/views/layouts/workshop.html.erb +163 -0
- data/app/views/layouts/workshops_index.html.erb +209 -0
- data/app/views/partials/_back_link.html.erb +27 -0
- data/app/views/partials/_back_to_index.html.erb +25 -0
- data/app/views/partials/_book_grid.html.erb +76 -0
- data/app/views/partials/_book_jsonld.html.erb +72 -0
- data/app/views/partials/_book_related.html.erb +87 -0
- data/app/views/partials/_breadcrumb_jsonld.html.erb +60 -0
- data/app/views/partials/_bundle_cta.html.erb +26 -0
- data/app/views/partials/_carousel.html.erb +64 -0
- data/app/views/partials/_collection_page_jsonld.html.erb +81 -0
- data/app/views/partials/_contact_button.html.erb +42 -0
- data/app/views/partials/_course_jsonld.html.erb +95 -0
- data/app/views/partials/_draft_banner.html.erb +20 -0
- data/app/views/partials/_event_jsonld.html.erb +122 -0
- data/app/views/partials/_footer.html.erb +88 -0
- data/app/views/partials/_future_banner.html.erb +25 -0
- data/app/views/partials/_head.html.erb +440 -0
- data/app/views/partials/_image_gallery.html.erb +64 -0
- data/app/views/partials/_image_object_jsonld.html.erb +61 -0
- data/app/views/partials/_nav.html.erb +63 -0
- data/app/views/partials/_newsletter_subscribe.html.erb +85 -0
- data/app/views/partials/_next_in_series.html.erb +27 -0
- data/app/views/partials/_next_item.html.erb +38 -0
- data/app/views/partials/_pagination.html.erb +15 -0
- data/app/views/partials/_person_jsonld.html.erb +46 -0
- data/app/views/partials/_plausible.html.erb +24 -0
- data/app/views/partials/_post_list.html.erb +101 -0
- data/app/views/partials/_post_series_previews.html.erb +92 -0
- data/app/views/partials/_publication_card.html.erb +75 -0
- data/app/views/partials/_publication_header.html.erb +61 -0
- data/app/views/partials/_related_item.html.erb +68 -0
- data/app/views/partials/_related_items.html.erb +105 -0
- data/app/views/partials/_search.html.erb +26 -0
- data/app/views/partials/_series_pager.html.erb +48 -0
- data/app/views/partials/_skip_link.html.erb +12 -0
- data/app/views/partials/_taxonomy_row.html.erb +62 -0
- data/app/views/partials/_testimonials.html.erb +149 -0
- data/app/views/partials/_theme_toggle.html.erb +19 -0
- data/app/views/partials/_toc.html.erb +136 -0
- data/app/views/partials/_webmentions.html.erb +131 -0
- data/app/views/partials/_website_jsonld.html.erb +39 -0
- data/app/views/partials/home/_contact.html.erb +19 -0
- data/app/views/partials/home/_featured_publication.html.erb +107 -0
- data/app/views/partials/home/_latest_photo.html.erb +95 -0
- data/app/views/partials/home/_latest_post.html.erb +48 -0
- data/app/views/partials/home/_latest_publication.html.erb +89 -0
- data/app/views/partials/home/_next_workshop.html.erb +119 -0
- data/app/views/partials/home/_photo_gallery.html.erb +71 -0
- data/app/views/partials/home/_random_photo.html.erb +106 -0
- data/app/views/partials/home/_random_post.html.erb +64 -0
- data/exe/abqari +37 -0
- data/lib/abqari/artifact_writer.rb +196 -0
- data/lib/abqari/asset_pipeline.rb +235 -0
- data/lib/abqari/audit/a11y.rb +80 -0
- data/lib/abqari/audit/content.rb +154 -0
- data/lib/abqari/audit/dependencies.rb +127 -0
- data/lib/abqari/audit/links.rb +120 -0
- data/lib/abqari/audit/performance.rb +120 -0
- data/lib/abqari/audit/privacy.rb +42 -0
- data/lib/abqari/audit/rule.rb +56 -0
- data/lib/abqari/audit/seo.rb +105 -0
- data/lib/abqari/audit.rb +149 -0
- data/lib/abqari/bundle_assets.rb +127 -0
- data/lib/abqari/cli/new.rb +541 -0
- data/lib/abqari/cli/theme.rb +181 -0
- data/lib/abqari/cli.rb +130 -0
- data/lib/abqari/erbi_template.rb +72 -0
- data/lib/abqari/feed.rb +159 -0
- data/lib/abqari/folio.rb +195 -0
- data/lib/abqari/font_pipeline.rb +104 -0
- data/lib/abqari/headers_file.rb +324 -0
- data/lib/abqari/helpers.rb +308 -0
- data/lib/abqari/hooks.rb +136 -0
- data/lib/abqari/http_safe.rb +222 -0
- data/lib/abqari/image_pipeline.rb +800 -0
- data/lib/abqari/importers/base.rb +351 -0
- data/lib/abqari/importers/cli.rb +169 -0
- data/lib/abqari/importers/ghost.rb +113 -0
- data/lib/abqari/importers/jekyll.rb +256 -0
- data/lib/abqari/importers/substack.rb +204 -0
- data/lib/abqari/importers/support/html_to_markdown.rb +126 -0
- data/lib/abqari/importers/support/image_downloader.rb +170 -0
- data/lib/abqari/importers/support/report.rb +87 -0
- data/lib/abqari/importers/support/slug_normalizer.rb +53 -0
- data/lib/abqari/importers.rb +64 -0
- data/lib/abqari/indieweb.rb +551 -0
- data/lib/abqari/llms_txt.rb +110 -0
- data/lib/abqari/log.rb +61 -0
- data/lib/abqari/manifest.rb +58 -0
- data/lib/abqari/minifier.rb +108 -0
- data/lib/abqari/og_image_generator.rb +223 -0
- data/lib/abqari/page/bundle.rb +152 -0
- data/lib/abqari/page/incremental.rb +74 -0
- data/lib/abqari/page/parser.rb +129 -0
- data/lib/abqari/page/path.rb +153 -0
- data/lib/abqari/page.rb +424 -0
- data/lib/abqari/paginated_shard.rb +109 -0
- data/lib/abqari/paginator.rb +35 -0
- data/lib/abqari/path_safe.rb +62 -0
- data/lib/abqari/plausible_script.rb +20 -0
- data/lib/abqari/publication_normaliser.rb +111 -0
- data/lib/abqari/publisher_pages.rb +342 -0
- data/lib/abqari/redirects.rb +134 -0
- data/lib/abqari/related_index_page.rb +79 -0
- data/lib/abqari/render_context/cover_helpers.rb +338 -0
- data/lib/abqari/render_context/picture_helpers.rb +178 -0
- data/lib/abqari/render_context/publication_helpers.rb +120 -0
- data/lib/abqari/render_context.rb +251 -0
- data/lib/abqari/renderer.rb +788 -0
- data/lib/abqari/robots.rb +93 -0
- data/lib/abqari/search.rb +68 -0
- data/lib/abqari/security_txt.rb +44 -0
- data/lib/abqari/server.rb +508 -0
- data/lib/abqari/site.rb +952 -0
- data/lib/abqari/site_config.rb +212 -0
- data/lib/abqari/site_loader.rb +265 -0
- data/lib/abqari/sitemap.rb +87 -0
- data/lib/abqari/speculation_rules_guard.rb +26 -0
- data/lib/abqari/syndication.rb +466 -0
- data/lib/abqari/syntax_highlighter.rb +146 -0
- data/lib/abqari/taxonomy_page.rb +129 -0
- data/lib/abqari/templates/site_skeleton/.github/dependabot.yml +53 -0
- data/lib/abqari/templates/site_skeleton/.github/workflows/deploy-cloudflare.yml +107 -0
- data/lib/abqari/templates/site_skeleton/.github/workflows/deploy-github-pages.yml +107 -0
- data/lib/abqari/templates/site_skeleton/.github/workflows/deploy-render.yml +87 -0
- data/lib/abqari/templates/site_skeleton/.gitignore +24 -0
- data/lib/abqari/templates/site_skeleton/.ruby-version.tt +1 -0
- data/lib/abqari/templates/site_skeleton/Gemfile.tt +20 -0
- data/lib/abqari/templates/site_skeleton/README.md.tt +60 -0
- data/lib/abqari/templates/site_skeleton/app/assets/css/site.css +9 -0
- data/lib/abqari/templates/site_skeleton/bin/audit +46 -0
- data/lib/abqari/templates/site_skeleton/bin/build +32 -0
- data/lib/abqari/templates/site_skeleton/bin/fetch-folio +32 -0
- data/lib/abqari/templates/site_skeleton/bin/fetch-fonts +20 -0
- data/lib/abqari/templates/site_skeleton/bin/import +31 -0
- data/lib/abqari/templates/site_skeleton/bin/index +30 -0
- data/lib/abqari/templates/site_skeleton/bin/new +235 -0
- data/lib/abqari/templates/site_skeleton/bin/serve +30 -0
- data/lib/abqari/templates/site_skeleton/config/site.blank.yml.tt +65 -0
- data/lib/abqari/templates/site_skeleton/config/site.yml.tt +266 -0
- data/lib/abqari/templates/site_skeleton/content/404.md +17 -0
- data/lib/abqari/templates/site_skeleton/content/about.blank.md.tt +12 -0
- data/lib/abqari/templates/site_skeleton/content/about.md.tt +18 -0
- data/lib/abqari/templates/site_skeleton/content/index.blank.md.tt +11 -0
- data/lib/abqari/templates/site_skeleton/content/index.md.tt +34 -0
- data/lib/abqari/templates/site_skeleton/content/photos/index.md.tt +12 -0
- data/lib/abqari/templates/site_skeleton/content/posts/index.md +12 -0
- data/lib/abqari/templates/site_skeleton/content/posts/welcome/index.md.tt +50 -0
- data/lib/abqari/templates/site_skeleton/content/publications/index.md.tt +13 -0
- data/lib/abqari/templates/site_skeleton/content/workshops/index.md.tt +13 -0
- data/lib/abqari/templates/site_skeleton/public/robots.txt +6 -0
- data/lib/abqari/templates/site_skeleton/themes/.gitkeep +0 -0
- data/lib/abqari/templates/site_skeleton/vendor/folio/.gitkeep +0 -0
- data/lib/abqari/templates/site_skeleton/vendor/fonts/.gitkeep +0 -0
- data/lib/abqari/theme_bootstrap.rb +19 -0
- data/lib/abqari/theme_fonts.rb +51 -0
- data/lib/abqari/version.rb +18 -0
- data/lib/abqari/visualizations.rb +215 -0
- data/lib/abqari.rb +154 -0
- data/themes/bootstrap/css/app.css +97 -0
- data/themes/bootstrap/public/LICENSE +21 -0
- data/themes/bootstrap/public/css/bootstrap.min.css +6 -0
- data/themes/bootstrap/public/css/bootstrap.min.css.map +1 -0
- data/themes/bootstrap/public/js/bootstrap.bundle.min.js +7 -0
- data/themes/bootstrap/public/js/bootstrap.bundle.min.js.map +1 -0
- data/themes/custom/css/app.css +176 -0
- data/themes/magazine/css/app.css +242 -0
- data/themes/minimal/css/app.css +151 -0
- data/themes/notebook/css/app.css +205 -0
- data/themes/sand/css/app.css +179 -0
- data/vendor/d3/d3.v7.min.js +2 -0
- data/vendor/heroicons/16/solid/academic-cap.svg +4 -0
- data/vendor/heroicons/16/solid/adjustments-horizontal.svg +3 -0
- data/vendor/heroicons/16/solid/adjustments-vertical.svg +3 -0
- data/vendor/heroicons/16/solid/archive-box-arrow-down.svg +4 -0
- data/vendor/heroicons/16/solid/archive-box-x-mark.svg +4 -0
- data/vendor/heroicons/16/solid/archive-box.svg +4 -0
- data/vendor/heroicons/16/solid/arrow-down-circle.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-down-left.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-down-on-square-stack.svg +4 -0
- data/vendor/heroicons/16/solid/arrow-down-on-square.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-down-right.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-down-tray.svg +4 -0
- data/vendor/heroicons/16/solid/arrow-down.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-left-circle.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-left-end-on-rectangle.svg +4 -0
- data/vendor/heroicons/16/solid/arrow-left-start-on-rectangle.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-left.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-long-down.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-long-left.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-long-right.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-long-up.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-path-rounded-square.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-path.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-right-circle.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-right-end-on-rectangle.svg +4 -0
- data/vendor/heroicons/16/solid/arrow-right-start-on-rectangle.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-right.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-top-right-on-square.svg +4 -0
- data/vendor/heroicons/16/solid/arrow-trending-down.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-trending-up.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-turn-down-left.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-turn-down-right.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-turn-left-down.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-turn-left-up.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-turn-right-down.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-turn-right-up.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-turn-up-left.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-turn-up-right.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-up-circle.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-up-left.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-up-on-square-stack.svg +4 -0
- data/vendor/heroicons/16/solid/arrow-up-on-square.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-up-right.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-up-tray.svg +4 -0
- data/vendor/heroicons/16/solid/arrow-up.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-uturn-down.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-uturn-left.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-uturn-right.svg +3 -0
- data/vendor/heroicons/16/solid/arrow-uturn-up.svg +3 -0
- data/vendor/heroicons/16/solid/arrows-pointing-in.svg +3 -0
- data/vendor/heroicons/16/solid/arrows-pointing-out.svg +3 -0
- data/vendor/heroicons/16/solid/arrows-right-left.svg +3 -0
- data/vendor/heroicons/16/solid/arrows-up-down.svg +3 -0
- data/vendor/heroicons/16/solid/at-symbol.svg +3 -0
- data/vendor/heroicons/16/solid/backspace.svg +3 -0
- data/vendor/heroicons/16/solid/backward.svg +3 -0
- data/vendor/heroicons/16/solid/banknotes.svg +4 -0
- data/vendor/heroicons/16/solid/bars-2.svg +3 -0
- data/vendor/heroicons/16/solid/bars-3-bottom-left.svg +3 -0
- data/vendor/heroicons/16/solid/bars-3-bottom-right.svg +3 -0
- data/vendor/heroicons/16/solid/bars-3-center-left.svg +3 -0
- data/vendor/heroicons/16/solid/bars-3.svg +3 -0
- data/vendor/heroicons/16/solid/bars-4.svg +3 -0
- data/vendor/heroicons/16/solid/bars-arrow-down.svg +3 -0
- data/vendor/heroicons/16/solid/bars-arrow-up.svg +3 -0
- data/vendor/heroicons/16/solid/battery-0.svg +3 -0
- data/vendor/heroicons/16/solid/battery-100.svg +4 -0
- data/vendor/heroicons/16/solid/battery-50.svg +4 -0
- data/vendor/heroicons/16/solid/beaker.svg +3 -0
- data/vendor/heroicons/16/solid/bell-alert.svg +4 -0
- data/vendor/heroicons/16/solid/bell-slash.svg +4 -0
- data/vendor/heroicons/16/solid/bell-snooze.svg +3 -0
- data/vendor/heroicons/16/solid/bell.svg +3 -0
- data/vendor/heroicons/16/solid/bold.svg +3 -0
- data/vendor/heroicons/16/solid/bolt-slash.svg +3 -0
- data/vendor/heroicons/16/solid/bolt.svg +3 -0
- data/vendor/heroicons/16/solid/book-open.svg +3 -0
- data/vendor/heroicons/16/solid/bookmark-slash.svg +3 -0
- data/vendor/heroicons/16/solid/bookmark-square.svg +3 -0
- data/vendor/heroicons/16/solid/bookmark.svg +3 -0
- data/vendor/heroicons/16/solid/briefcase.svg +4 -0
- data/vendor/heroicons/16/solid/bug-ant.svg +3 -0
- data/vendor/heroicons/16/solid/building-library.svg +3 -0
- data/vendor/heroicons/16/solid/building-office-2.svg +3 -0
- data/vendor/heroicons/16/solid/building-office.svg +3 -0
- data/vendor/heroicons/16/solid/building-storefront.svg +3 -0
- data/vendor/heroicons/16/solid/cake.svg +3 -0
- data/vendor/heroicons/16/solid/calculator.svg +3 -0
- data/vendor/heroicons/16/solid/calendar-date-range.svg +4 -0
- data/vendor/heroicons/16/solid/calendar-days.svg +4 -0
- data/vendor/heroicons/16/solid/calendar.svg +3 -0
- data/vendor/heroicons/16/solid/camera.svg +4 -0
- data/vendor/heroicons/16/solid/chart-bar-square.svg +3 -0
- data/vendor/heroicons/16/solid/chart-bar.svg +3 -0
- data/vendor/heroicons/16/solid/chart-pie.svg +4 -0
- data/vendor/heroicons/16/solid/chat-bubble-bottom-center-text.svg +3 -0
- data/vendor/heroicons/16/solid/chat-bubble-bottom-center.svg +3 -0
- data/vendor/heroicons/16/solid/chat-bubble-left-ellipsis.svg +3 -0
- data/vendor/heroicons/16/solid/chat-bubble-left-right.svg +4 -0
- data/vendor/heroicons/16/solid/chat-bubble-left.svg +3 -0
- data/vendor/heroicons/16/solid/chat-bubble-oval-left-ellipsis.svg +3 -0
- data/vendor/heroicons/16/solid/chat-bubble-oval-left.svg +3 -0
- data/vendor/heroicons/16/solid/check-badge.svg +3 -0
- data/vendor/heroicons/16/solid/check-circle.svg +3 -0
- data/vendor/heroicons/16/solid/check.svg +3 -0
- data/vendor/heroicons/16/solid/chevron-double-down.svg +3 -0
- data/vendor/heroicons/16/solid/chevron-double-left.svg +3 -0
- data/vendor/heroicons/16/solid/chevron-double-right.svg +3 -0
- data/vendor/heroicons/16/solid/chevron-double-up.svg +3 -0
- data/vendor/heroicons/16/solid/chevron-down.svg +3 -0
- data/vendor/heroicons/16/solid/chevron-left.svg +3 -0
- data/vendor/heroicons/16/solid/chevron-right.svg +3 -0
- data/vendor/heroicons/16/solid/chevron-up-down.svg +3 -0
- data/vendor/heroicons/16/solid/chevron-up.svg +3 -0
- data/vendor/heroicons/16/solid/circle-stack.svg +5 -0
- data/vendor/heroicons/16/solid/clipboard-document-check.svg +4 -0
- data/vendor/heroicons/16/solid/clipboard-document-list.svg +4 -0
- data/vendor/heroicons/16/solid/clipboard-document.svg +4 -0
- data/vendor/heroicons/16/solid/clipboard.svg +3 -0
- data/vendor/heroicons/16/solid/clock.svg +3 -0
- data/vendor/heroicons/16/solid/cloud-arrow-down.svg +3 -0
- data/vendor/heroicons/16/solid/cloud-arrow-up.svg +3 -0
- data/vendor/heroicons/16/solid/cloud.svg +3 -0
- data/vendor/heroicons/16/solid/code-bracket-square.svg +3 -0
- data/vendor/heroicons/16/solid/code-bracket.svg +3 -0
- data/vendor/heroicons/16/solid/cog-6-tooth.svg +3 -0
- data/vendor/heroicons/16/solid/cog-8-tooth.svg +3 -0
- data/vendor/heroicons/16/solid/cog.svg +3 -0
- data/vendor/heroicons/16/solid/command-line.svg +3 -0
- data/vendor/heroicons/16/solid/computer-desktop.svg +3 -0
- data/vendor/heroicons/16/solid/cpu-chip.svg +4 -0
- data/vendor/heroicons/16/solid/credit-card.svg +4 -0
- data/vendor/heroicons/16/solid/cube-transparent.svg +3 -0
- data/vendor/heroicons/16/solid/cube.svg +3 -0
- data/vendor/heroicons/16/solid/currency-bangladeshi.svg +3 -0
- data/vendor/heroicons/16/solid/currency-dollar.svg +4 -0
- data/vendor/heroicons/16/solid/currency-euro.svg +3 -0
- data/vendor/heroicons/16/solid/currency-pound.svg +3 -0
- data/vendor/heroicons/16/solid/currency-rupee.svg +3 -0
- data/vendor/heroicons/16/solid/currency-yen.svg +3 -0
- data/vendor/heroicons/16/solid/cursor-arrow-rays.svg +3 -0
- data/vendor/heroicons/16/solid/cursor-arrow-ripple.svg +5 -0
- data/vendor/heroicons/16/solid/device-phone-mobile.svg +4 -0
- data/vendor/heroicons/16/solid/device-tablet.svg +4 -0
- data/vendor/heroicons/16/solid/divide.svg +4 -0
- data/vendor/heroicons/16/solid/document-arrow-down.svg +3 -0
- data/vendor/heroicons/16/solid/document-arrow-up.svg +3 -0
- data/vendor/heroicons/16/solid/document-chart-bar.svg +3 -0
- data/vendor/heroicons/16/solid/document-check.svg +3 -0
- data/vendor/heroicons/16/solid/document-currency-bangladeshi.svg +3 -0
- data/vendor/heroicons/16/solid/document-currency-dollar.svg +4 -0
- data/vendor/heroicons/16/solid/document-currency-euro.svg +3 -0
- data/vendor/heroicons/16/solid/document-currency-pound.svg +3 -0
- data/vendor/heroicons/16/solid/document-currency-rupee.svg +3 -0
- data/vendor/heroicons/16/solid/document-currency-yen.svg +3 -0
- data/vendor/heroicons/16/solid/document-duplicate.svg +4 -0
- data/vendor/heroicons/16/solid/document-magnifying-glass.svg +4 -0
- data/vendor/heroicons/16/solid/document-minus.svg +3 -0
- data/vendor/heroicons/16/solid/document-plus.svg +3 -0
- data/vendor/heroicons/16/solid/document-text.svg +3 -0
- data/vendor/heroicons/16/solid/document.svg +3 -0
- data/vendor/heroicons/16/solid/ellipsis-horizontal-circle.svg +3 -0
- data/vendor/heroicons/16/solid/ellipsis-horizontal.svg +3 -0
- data/vendor/heroicons/16/solid/ellipsis-vertical.svg +3 -0
- data/vendor/heroicons/16/solid/envelope-open.svg +3 -0
- data/vendor/heroicons/16/solid/envelope.svg +4 -0
- data/vendor/heroicons/16/solid/equals.svg +3 -0
- data/vendor/heroicons/16/solid/exclamation-circle.svg +3 -0
- data/vendor/heroicons/16/solid/exclamation-triangle.svg +3 -0
- data/vendor/heroicons/16/solid/eye-dropper.svg +3 -0
- data/vendor/heroicons/16/solid/eye-slash.svg +4 -0
- data/vendor/heroicons/16/solid/eye.svg +4 -0
- data/vendor/heroicons/16/solid/face-frown.svg +3 -0
- data/vendor/heroicons/16/solid/face-smile.svg +3 -0
- data/vendor/heroicons/16/solid/film.svg +3 -0
- data/vendor/heroicons/16/solid/finger-print.svg +3 -0
- data/vendor/heroicons/16/solid/fire.svg +3 -0
- data/vendor/heroicons/16/solid/flag.svg +3 -0
- data/vendor/heroicons/16/solid/folder-arrow-down.svg +3 -0
- data/vendor/heroicons/16/solid/folder-minus.svg +3 -0
- data/vendor/heroicons/16/solid/folder-open.svg +3 -0
- data/vendor/heroicons/16/solid/folder-plus.svg +3 -0
- data/vendor/heroicons/16/solid/folder.svg +3 -0
- data/vendor/heroicons/16/solid/forward.svg +3 -0
- data/vendor/heroicons/16/solid/funnel.svg +3 -0
- data/vendor/heroicons/16/solid/gif.svg +3 -0
- data/vendor/heroicons/16/solid/gift-top.svg +4 -0
- data/vendor/heroicons/16/solid/gift.svg +4 -0
- data/vendor/heroicons/16/solid/globe-alt.svg +3 -0
- data/vendor/heroicons/16/solid/globe-americas.svg +3 -0
- data/vendor/heroicons/16/solid/globe-asia-australia.svg +3 -0
- data/vendor/heroicons/16/solid/globe-europe-africa.svg +3 -0
- data/vendor/heroicons/16/solid/h1.svg +3 -0
- data/vendor/heroicons/16/solid/h2.svg +3 -0
- data/vendor/heroicons/16/solid/h3.svg +3 -0
- data/vendor/heroicons/16/solid/hand-raised.svg +3 -0
- data/vendor/heroicons/16/solid/hand-thumb-down.svg +3 -0
- data/vendor/heroicons/16/solid/hand-thumb-up.svg +3 -0
- data/vendor/heroicons/16/solid/hashtag.svg +3 -0
- data/vendor/heroicons/16/solid/heart.svg +3 -0
- data/vendor/heroicons/16/solid/home-modern.svg +3 -0
- data/vendor/heroicons/16/solid/home.svg +3 -0
- data/vendor/heroicons/16/solid/identification.svg +3 -0
- data/vendor/heroicons/16/solid/inbox-arrow-down.svg +4 -0
- data/vendor/heroicons/16/solid/inbox-stack.svg +4 -0
- data/vendor/heroicons/16/solid/inbox.svg +3 -0
- data/vendor/heroicons/16/solid/information-circle.svg +3 -0
- data/vendor/heroicons/16/solid/italic.svg +3 -0
- data/vendor/heroicons/16/solid/key.svg +3 -0
- data/vendor/heroicons/16/solid/language.svg +3 -0
- data/vendor/heroicons/16/solid/lifebuoy.svg +3 -0
- data/vendor/heroicons/16/solid/light-bulb.svg +3 -0
- data/vendor/heroicons/16/solid/link-slash.svg +3 -0
- data/vendor/heroicons/16/solid/link.svg +4 -0
- data/vendor/heroicons/16/solid/list-bullet.svg +3 -0
- data/vendor/heroicons/16/solid/lock-closed.svg +3 -0
- data/vendor/heroicons/16/solid/lock-open.svg +3 -0
- data/vendor/heroicons/16/solid/magnifying-glass-circle.svg +4 -0
- data/vendor/heroicons/16/solid/magnifying-glass-minus.svg +4 -0
- data/vendor/heroicons/16/solid/magnifying-glass-plus.svg +4 -0
- data/vendor/heroicons/16/solid/magnifying-glass.svg +3 -0
- data/vendor/heroicons/16/solid/map-pin.svg +3 -0
- data/vendor/heroicons/16/solid/map.svg +3 -0
- data/vendor/heroicons/16/solid/megaphone.svg +3 -0
- data/vendor/heroicons/16/solid/microphone.svg +4 -0
- data/vendor/heroicons/16/solid/minus-circle.svg +3 -0
- data/vendor/heroicons/16/solid/minus.svg +3 -0
- data/vendor/heroicons/16/solid/moon.svg +3 -0
- data/vendor/heroicons/16/solid/musical-note.svg +3 -0
- data/vendor/heroicons/16/solid/newspaper.svg +4 -0
- data/vendor/heroicons/16/solid/no-symbol.svg +3 -0
- data/vendor/heroicons/16/solid/numbered-list.svg +3 -0
- data/vendor/heroicons/16/solid/paint-brush.svg +3 -0
- data/vendor/heroicons/16/solid/paper-airplane.svg +3 -0
- data/vendor/heroicons/16/solid/paper-clip.svg +3 -0
- data/vendor/heroicons/16/solid/pause-circle.svg +3 -0
- data/vendor/heroicons/16/solid/pause.svg +3 -0
- data/vendor/heroicons/16/solid/pencil-square.svg +4 -0
- data/vendor/heroicons/16/solid/pencil.svg +3 -0
- data/vendor/heroicons/16/solid/percent-badge.svg +3 -0
- data/vendor/heroicons/16/solid/phone-arrow-down-left.svg +4 -0
- data/vendor/heroicons/16/solid/phone-arrow-up-right.svg +4 -0
- data/vendor/heroicons/16/solid/phone-x-mark.svg +4 -0
- data/vendor/heroicons/16/solid/phone.svg +3 -0
- data/vendor/heroicons/16/solid/photo.svg +3 -0
- data/vendor/heroicons/16/solid/play-circle.svg +3 -0
- data/vendor/heroicons/16/solid/play-pause.svg +3 -0
- data/vendor/heroicons/16/solid/play.svg +3 -0
- data/vendor/heroicons/16/solid/plus-circle.svg +3 -0
- data/vendor/heroicons/16/solid/plus.svg +3 -0
- data/vendor/heroicons/16/solid/power.svg +3 -0
- data/vendor/heroicons/16/solid/presentation-chart-bar.svg +3 -0
- data/vendor/heroicons/16/solid/presentation-chart-line.svg +3 -0
- data/vendor/heroicons/16/solid/printer.svg +3 -0
- data/vendor/heroicons/16/solid/puzzle-piece.svg +3 -0
- data/vendor/heroicons/16/solid/qr-code.svg +9 -0
- data/vendor/heroicons/16/solid/question-mark-circle.svg +3 -0
- data/vendor/heroicons/16/solid/queue-list.svg +3 -0
- data/vendor/heroicons/16/solid/radio.svg +3 -0
- data/vendor/heroicons/16/solid/receipt-percent.svg +3 -0
- data/vendor/heroicons/16/solid/receipt-refund.svg +3 -0
- data/vendor/heroicons/16/solid/rectangle-group.svg +3 -0
- data/vendor/heroicons/16/solid/rectangle-stack.svg +3 -0
- data/vendor/heroicons/16/solid/rocket-launch.svg +4 -0
- data/vendor/heroicons/16/solid/rss.svg +3 -0
- data/vendor/heroicons/16/solid/scale.svg +3 -0
- data/vendor/heroicons/16/solid/scissors.svg +4 -0
- data/vendor/heroicons/16/solid/server-stack.svg +4 -0
- data/vendor/heroicons/16/solid/server.svg +4 -0
- data/vendor/heroicons/16/solid/share.svg +3 -0
- data/vendor/heroicons/16/solid/shield-check.svg +3 -0
- data/vendor/heroicons/16/solid/shield-exclamation.svg +3 -0
- data/vendor/heroicons/16/solid/shopping-bag.svg +3 -0
- data/vendor/heroicons/16/solid/shopping-cart.svg +3 -0
- data/vendor/heroicons/16/solid/signal-slash.svg +3 -0
- data/vendor/heroicons/16/solid/signal.svg +5 -0
- data/vendor/heroicons/16/solid/slash.svg +3 -0
- data/vendor/heroicons/16/solid/sparkles.svg +3 -0
- data/vendor/heroicons/16/solid/speaker-wave.svg +4 -0
- data/vendor/heroicons/16/solid/speaker-x-mark.svg +3 -0
- data/vendor/heroicons/16/solid/square-2-stack.svg +4 -0
- data/vendor/heroicons/16/solid/square-3-stack-3d.svg +5 -0
- data/vendor/heroicons/16/solid/squares-2x2.svg +3 -0
- data/vendor/heroicons/16/solid/squares-plus.svg +3 -0
- data/vendor/heroicons/16/solid/star.svg +3 -0
- data/vendor/heroicons/16/solid/stop-circle.svg +3 -0
- data/vendor/heroicons/16/solid/stop.svg +3 -0
- data/vendor/heroicons/16/solid/strikethrough.svg +3 -0
- data/vendor/heroicons/16/solid/sun.svg +3 -0
- data/vendor/heroicons/16/solid/swatch.svg +4 -0
- data/vendor/heroicons/16/solid/table-cells.svg +3 -0
- data/vendor/heroicons/16/solid/tag.svg +3 -0
- data/vendor/heroicons/16/solid/ticket.svg +3 -0
- data/vendor/heroicons/16/solid/trash.svg +3 -0
- data/vendor/heroicons/16/solid/trophy.svg +3 -0
- data/vendor/heroicons/16/solid/truck.svg +5 -0
- data/vendor/heroicons/16/solid/tv.svg +4 -0
- data/vendor/heroicons/16/solid/underline.svg +3 -0
- data/vendor/heroicons/16/solid/user-circle.svg +3 -0
- data/vendor/heroicons/16/solid/user-group.svg +3 -0
- data/vendor/heroicons/16/solid/user-minus.svg +3 -0
- data/vendor/heroicons/16/solid/user-plus.svg +3 -0
- data/vendor/heroicons/16/solid/user.svg +3 -0
- data/vendor/heroicons/16/solid/users.svg +3 -0
- data/vendor/heroicons/16/solid/variable.svg +3 -0
- data/vendor/heroicons/16/solid/video-camera-slash.svg +3 -0
- data/vendor/heroicons/16/solid/video-camera.svg +3 -0
- data/vendor/heroicons/16/solid/view-columns.svg +3 -0
- data/vendor/heroicons/16/solid/viewfinder-circle.svg +3 -0
- data/vendor/heroicons/16/solid/wallet.svg +3 -0
- data/vendor/heroicons/16/solid/wifi.svg +3 -0
- data/vendor/heroicons/16/solid/window.svg +3 -0
- data/vendor/heroicons/16/solid/wrench-screwdriver.svg +4 -0
- data/vendor/heroicons/16/solid/wrench.svg +3 -0
- data/vendor/heroicons/16/solid/x-circle.svg +3 -0
- data/vendor/heroicons/16/solid/x-mark.svg +3 -0
- data/vendor/heroicons/20/solid/academic-cap.svg +3 -0
- data/vendor/heroicons/20/solid/adjustments-horizontal.svg +3 -0
- data/vendor/heroicons/20/solid/adjustments-vertical.svg +3 -0
- data/vendor/heroicons/20/solid/archive-box-arrow-down.svg +3 -0
- data/vendor/heroicons/20/solid/archive-box-x-mark.svg +4 -0
- data/vendor/heroicons/20/solid/archive-box.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-down-circle.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-down-left.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-down-on-square-stack.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-down-on-square.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-down-right.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-down-tray.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-down.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-left-circle.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-left-end-on-rectangle.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-left-on-rectangle.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-left-start-on-rectangle.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-left.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-long-down.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-long-left.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-long-right.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-long-up.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-path-rounded-square.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-path.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-right-circle.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-right-end-on-rectangle.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-right-on-rectangle.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-right-start-on-rectangle.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-right.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-small-down.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-small-left.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-small-right.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-small-up.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-top-right-on-square.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-trending-down.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-trending-up.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-turn-down-left.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-turn-down-right.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-turn-left-down.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-turn-left-up.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-turn-right-down.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-turn-right-up.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-turn-up-left.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-turn-up-right.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-up-circle.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-up-left.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-up-on-square-stack.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-up-on-square.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-up-right.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-up-tray.svg +4 -0
- data/vendor/heroicons/20/solid/arrow-up.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-uturn-down.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-uturn-left.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-uturn-right.svg +3 -0
- data/vendor/heroicons/20/solid/arrow-uturn-up.svg +3 -0
- data/vendor/heroicons/20/solid/arrows-pointing-in.svg +3 -0
- data/vendor/heroicons/20/solid/arrows-pointing-out.svg +3 -0
- data/vendor/heroicons/20/solid/arrows-right-left.svg +3 -0
- data/vendor/heroicons/20/solid/arrows-up-down.svg +3 -0
- data/vendor/heroicons/20/solid/at-symbol.svg +3 -0
- data/vendor/heroicons/20/solid/backspace.svg +3 -0
- data/vendor/heroicons/20/solid/backward.svg +3 -0
- data/vendor/heroicons/20/solid/banknotes.svg +3 -0
- data/vendor/heroicons/20/solid/bars-2.svg +3 -0
- data/vendor/heroicons/20/solid/bars-3-bottom-left.svg +3 -0
- data/vendor/heroicons/20/solid/bars-3-bottom-right.svg +3 -0
- data/vendor/heroicons/20/solid/bars-3-center-left.svg +3 -0
- data/vendor/heroicons/20/solid/bars-3.svg +3 -0
- data/vendor/heroicons/20/solid/bars-4.svg +3 -0
- data/vendor/heroicons/20/solid/bars-arrow-down.svg +3 -0
- data/vendor/heroicons/20/solid/bars-arrow-up.svg +3 -0
- data/vendor/heroicons/20/solid/battery-0.svg +3 -0
- data/vendor/heroicons/20/solid/battery-100.svg +4 -0
- data/vendor/heroicons/20/solid/battery-50.svg +4 -0
- data/vendor/heroicons/20/solid/beaker.svg +3 -0
- data/vendor/heroicons/20/solid/bell-alert.svg +4 -0
- data/vendor/heroicons/20/solid/bell-slash.svg +3 -0
- data/vendor/heroicons/20/solid/bell-snooze.svg +3 -0
- data/vendor/heroicons/20/solid/bell.svg +3 -0
- data/vendor/heroicons/20/solid/bold.svg +3 -0
- data/vendor/heroicons/20/solid/bolt-slash.svg +4 -0
- data/vendor/heroicons/20/solid/bolt.svg +3 -0
- data/vendor/heroicons/20/solid/book-open.svg +3 -0
- data/vendor/heroicons/20/solid/bookmark-slash.svg +3 -0
- data/vendor/heroicons/20/solid/bookmark-square.svg +3 -0
- data/vendor/heroicons/20/solid/bookmark.svg +3 -0
- data/vendor/heroicons/20/solid/briefcase.svg +4 -0
- data/vendor/heroicons/20/solid/bug-ant.svg +3 -0
- data/vendor/heroicons/20/solid/building-library.svg +3 -0
- data/vendor/heroicons/20/solid/building-office-2.svg +3 -0
- data/vendor/heroicons/20/solid/building-office.svg +3 -0
- data/vendor/heroicons/20/solid/building-storefront.svg +3 -0
- data/vendor/heroicons/20/solid/cake.svg +3 -0
- data/vendor/heroicons/20/solid/calculator.svg +3 -0
- data/vendor/heroicons/20/solid/calendar-date-range.svg +4 -0
- data/vendor/heroicons/20/solid/calendar-days.svg +4 -0
- data/vendor/heroicons/20/solid/calendar.svg +3 -0
- data/vendor/heroicons/20/solid/camera.svg +3 -0
- data/vendor/heroicons/20/solid/chart-bar-square.svg +3 -0
- data/vendor/heroicons/20/solid/chart-bar.svg +3 -0
- data/vendor/heroicons/20/solid/chart-pie.svg +4 -0
- data/vendor/heroicons/20/solid/chat-bubble-bottom-center-text.svg +3 -0
- data/vendor/heroicons/20/solid/chat-bubble-bottom-center.svg +3 -0
- data/vendor/heroicons/20/solid/chat-bubble-left-ellipsis.svg +3 -0
- data/vendor/heroicons/20/solid/chat-bubble-left-right.svg +4 -0
- data/vendor/heroicons/20/solid/chat-bubble-left.svg +3 -0
- data/vendor/heroicons/20/solid/chat-bubble-oval-left-ellipsis.svg +3 -0
- data/vendor/heroicons/20/solid/chat-bubble-oval-left.svg +3 -0
- data/vendor/heroicons/20/solid/check-badge.svg +3 -0
- data/vendor/heroicons/20/solid/check-circle.svg +3 -0
- data/vendor/heroicons/20/solid/check.svg +3 -0
- data/vendor/heroicons/20/solid/chevron-double-down.svg +3 -0
- data/vendor/heroicons/20/solid/chevron-double-left.svg +3 -0
- data/vendor/heroicons/20/solid/chevron-double-right.svg +3 -0
- data/vendor/heroicons/20/solid/chevron-double-up.svg +3 -0
- data/vendor/heroicons/20/solid/chevron-down.svg +3 -0
- data/vendor/heroicons/20/solid/chevron-left.svg +3 -0
- data/vendor/heroicons/20/solid/chevron-right.svg +3 -0
- data/vendor/heroicons/20/solid/chevron-up-down.svg +3 -0
- data/vendor/heroicons/20/solid/chevron-up.svg +3 -0
- data/vendor/heroicons/20/solid/circle-stack.svg +3 -0
- data/vendor/heroicons/20/solid/clipboard-document-check.svg +4 -0
- data/vendor/heroicons/20/solid/clipboard-document-list.svg +4 -0
- data/vendor/heroicons/20/solid/clipboard-document.svg +4 -0
- data/vendor/heroicons/20/solid/clipboard.svg +3 -0
- data/vendor/heroicons/20/solid/clock.svg +3 -0
- data/vendor/heroicons/20/solid/cloud-arrow-down.svg +3 -0
- data/vendor/heroicons/20/solid/cloud-arrow-up.svg +3 -0
- data/vendor/heroicons/20/solid/cloud.svg +3 -0
- data/vendor/heroicons/20/solid/code-bracket-square.svg +3 -0
- data/vendor/heroicons/20/solid/code-bracket.svg +3 -0
- data/vendor/heroicons/20/solid/cog-6-tooth.svg +3 -0
- data/vendor/heroicons/20/solid/cog-8-tooth.svg +3 -0
- data/vendor/heroicons/20/solid/cog.svg +4 -0
- data/vendor/heroicons/20/solid/command-line.svg +3 -0
- data/vendor/heroicons/20/solid/computer-desktop.svg +3 -0
- data/vendor/heroicons/20/solid/cpu-chip.svg +4 -0
- data/vendor/heroicons/20/solid/credit-card.svg +3 -0
- data/vendor/heroicons/20/solid/cube-transparent.svg +3 -0
- data/vendor/heroicons/20/solid/cube.svg +3 -0
- data/vendor/heroicons/20/solid/currency-bangladeshi.svg +3 -0
- data/vendor/heroicons/20/solid/currency-dollar.svg +4 -0
- data/vendor/heroicons/20/solid/currency-euro.svg +3 -0
- data/vendor/heroicons/20/solid/currency-pound.svg +3 -0
- data/vendor/heroicons/20/solid/currency-rupee.svg +3 -0
- data/vendor/heroicons/20/solid/currency-yen.svg +3 -0
- data/vendor/heroicons/20/solid/cursor-arrow-rays.svg +3 -0
- data/vendor/heroicons/20/solid/cursor-arrow-ripple.svg +5 -0
- data/vendor/heroicons/20/solid/device-phone-mobile.svg +4 -0
- data/vendor/heroicons/20/solid/device-tablet.svg +3 -0
- data/vendor/heroicons/20/solid/divide.svg +3 -0
- data/vendor/heroicons/20/solid/document-arrow-down.svg +3 -0
- data/vendor/heroicons/20/solid/document-arrow-up.svg +3 -0
- data/vendor/heroicons/20/solid/document-chart-bar.svg +3 -0
- data/vendor/heroicons/20/solid/document-check.svg +3 -0
- data/vendor/heroicons/20/solid/document-currency-bangladeshi.svg +3 -0
- data/vendor/heroicons/20/solid/document-currency-dollar.svg +3 -0
- data/vendor/heroicons/20/solid/document-currency-euro.svg +3 -0
- data/vendor/heroicons/20/solid/document-currency-pound.svg +3 -0
- data/vendor/heroicons/20/solid/document-currency-rupee.svg +3 -0
- data/vendor/heroicons/20/solid/document-currency-yen.svg +3 -0
- data/vendor/heroicons/20/solid/document-duplicate.svg +4 -0
- data/vendor/heroicons/20/solid/document-magnifying-glass.svg +4 -0
- data/vendor/heroicons/20/solid/document-minus.svg +3 -0
- data/vendor/heroicons/20/solid/document-plus.svg +3 -0
- data/vendor/heroicons/20/solid/document-text.svg +3 -0
- data/vendor/heroicons/20/solid/document.svg +3 -0
- data/vendor/heroicons/20/solid/ellipsis-horizontal-circle.svg +3 -0
- data/vendor/heroicons/20/solid/ellipsis-horizontal.svg +3 -0
- data/vendor/heroicons/20/solid/ellipsis-vertical.svg +3 -0
- data/vendor/heroicons/20/solid/envelope-open.svg +3 -0
- data/vendor/heroicons/20/solid/envelope.svg +4 -0
- data/vendor/heroicons/20/solid/equals.svg +3 -0
- data/vendor/heroicons/20/solid/exclamation-circle.svg +3 -0
- data/vendor/heroicons/20/solid/exclamation-triangle.svg +3 -0
- data/vendor/heroicons/20/solid/eye-dropper.svg +3 -0
- data/vendor/heroicons/20/solid/eye-slash.svg +4 -0
- data/vendor/heroicons/20/solid/eye.svg +4 -0
- data/vendor/heroicons/20/solid/face-frown.svg +3 -0
- data/vendor/heroicons/20/solid/face-smile.svg +3 -0
- data/vendor/heroicons/20/solid/film.svg +3 -0
- data/vendor/heroicons/20/solid/finger-print.svg +3 -0
- data/vendor/heroicons/20/solid/fire.svg +3 -0
- data/vendor/heroicons/20/solid/flag.svg +3 -0
- data/vendor/heroicons/20/solid/folder-arrow-down.svg +3 -0
- data/vendor/heroicons/20/solid/folder-minus.svg +3 -0
- data/vendor/heroicons/20/solid/folder-open.svg +3 -0
- data/vendor/heroicons/20/solid/folder-plus.svg +3 -0
- data/vendor/heroicons/20/solid/folder.svg +3 -0
- data/vendor/heroicons/20/solid/forward.svg +3 -0
- data/vendor/heroicons/20/solid/funnel.svg +3 -0
- data/vendor/heroicons/20/solid/gif.svg +3 -0
- data/vendor/heroicons/20/solid/gift-top.svg +3 -0
- data/vendor/heroicons/20/solid/gift.svg +4 -0
- data/vendor/heroicons/20/solid/globe-alt.svg +3 -0
- data/vendor/heroicons/20/solid/globe-americas.svg +3 -0
- data/vendor/heroicons/20/solid/globe-asia-australia.svg +3 -0
- data/vendor/heroicons/20/solid/globe-europe-africa.svg +3 -0
- data/vendor/heroicons/20/solid/h1.svg +3 -0
- data/vendor/heroicons/20/solid/h2.svg +3 -0
- data/vendor/heroicons/20/solid/h3.svg +3 -0
- data/vendor/heroicons/20/solid/hand-raised.svg +3 -0
- data/vendor/heroicons/20/solid/hand-thumb-down.svg +3 -0
- data/vendor/heroicons/20/solid/hand-thumb-up.svg +3 -0
- data/vendor/heroicons/20/solid/hashtag.svg +3 -0
- data/vendor/heroicons/20/solid/heart.svg +3 -0
- data/vendor/heroicons/20/solid/home-modern.svg +3 -0
- data/vendor/heroicons/20/solid/home.svg +3 -0
- data/vendor/heroicons/20/solid/identification.svg +3 -0
- data/vendor/heroicons/20/solid/inbox-arrow-down.svg +4 -0
- data/vendor/heroicons/20/solid/inbox-stack.svg +4 -0
- data/vendor/heroicons/20/solid/inbox.svg +3 -0
- data/vendor/heroicons/20/solid/information-circle.svg +3 -0
- data/vendor/heroicons/20/solid/italic.svg +3 -0
- data/vendor/heroicons/20/solid/key.svg +3 -0
- data/vendor/heroicons/20/solid/language.svg +4 -0
- data/vendor/heroicons/20/solid/lifebuoy.svg +3 -0
- data/vendor/heroicons/20/solid/light-bulb.svg +3 -0
- data/vendor/heroicons/20/solid/link-slash.svg +3 -0
- data/vendor/heroicons/20/solid/link.svg +4 -0
- data/vendor/heroicons/20/solid/list-bullet.svg +3 -0
- data/vendor/heroicons/20/solid/lock-closed.svg +3 -0
- data/vendor/heroicons/20/solid/lock-open.svg +3 -0
- data/vendor/heroicons/20/solid/magnifying-glass-circle.svg +4 -0
- data/vendor/heroicons/20/solid/magnifying-glass-minus.svg +4 -0
- data/vendor/heroicons/20/solid/magnifying-glass-plus.svg +4 -0
- data/vendor/heroicons/20/solid/magnifying-glass.svg +3 -0
- data/vendor/heroicons/20/solid/map-pin.svg +3 -0
- data/vendor/heroicons/20/solid/map.svg +3 -0
- data/vendor/heroicons/20/solid/megaphone.svg +3 -0
- data/vendor/heroicons/20/solid/microphone.svg +4 -0
- data/vendor/heroicons/20/solid/minus-circle.svg +3 -0
- data/vendor/heroicons/20/solid/minus-small.svg +3 -0
- data/vendor/heroicons/20/solid/minus.svg +3 -0
- data/vendor/heroicons/20/solid/moon.svg +3 -0
- data/vendor/heroicons/20/solid/musical-note.svg +3 -0
- data/vendor/heroicons/20/solid/newspaper.svg +4 -0
- data/vendor/heroicons/20/solid/no-symbol.svg +3 -0
- data/vendor/heroicons/20/solid/numbered-list.svg +3 -0
- data/vendor/heroicons/20/solid/paint-brush.svg +3 -0
- data/vendor/heroicons/20/solid/paper-airplane.svg +3 -0
- data/vendor/heroicons/20/solid/paper-clip.svg +3 -0
- data/vendor/heroicons/20/solid/pause-circle.svg +3 -0
- data/vendor/heroicons/20/solid/pause.svg +3 -0
- data/vendor/heroicons/20/solid/pencil-square.svg +4 -0
- data/vendor/heroicons/20/solid/pencil.svg +3 -0
- data/vendor/heroicons/20/solid/percent-badge.svg +3 -0
- data/vendor/heroicons/20/solid/phone-arrow-down-left.svg +3 -0
- data/vendor/heroicons/20/solid/phone-arrow-up-right.svg +3 -0
- data/vendor/heroicons/20/solid/phone-x-mark.svg +3 -0
- data/vendor/heroicons/20/solid/phone.svg +3 -0
- data/vendor/heroicons/20/solid/photo.svg +3 -0
- data/vendor/heroicons/20/solid/play-circle.svg +3 -0
- data/vendor/heroicons/20/solid/play-pause.svg +3 -0
- data/vendor/heroicons/20/solid/play.svg +3 -0
- data/vendor/heroicons/20/solid/plus-circle.svg +3 -0
- data/vendor/heroicons/20/solid/plus-small.svg +3 -0
- data/vendor/heroicons/20/solid/plus.svg +3 -0
- data/vendor/heroicons/20/solid/power.svg +3 -0
- data/vendor/heroicons/20/solid/presentation-chart-bar.svg +3 -0
- data/vendor/heroicons/20/solid/presentation-chart-line.svg +3 -0
- data/vendor/heroicons/20/solid/printer.svg +3 -0
- data/vendor/heroicons/20/solid/puzzle-piece.svg +3 -0
- data/vendor/heroicons/20/solid/qr-code.svg +3 -0
- data/vendor/heroicons/20/solid/question-mark-circle.svg +3 -0
- data/vendor/heroicons/20/solid/queue-list.svg +3 -0
- data/vendor/heroicons/20/solid/radio.svg +3 -0
- data/vendor/heroicons/20/solid/receipt-percent.svg +3 -0
- data/vendor/heroicons/20/solid/receipt-refund.svg +3 -0
- data/vendor/heroicons/20/solid/rectangle-group.svg +3 -0
- data/vendor/heroicons/20/solid/rectangle-stack.svg +3 -0
- data/vendor/heroicons/20/solid/rocket-launch.svg +4 -0
- data/vendor/heroicons/20/solid/rss.svg +4 -0
- data/vendor/heroicons/20/solid/scale.svg +3 -0
- data/vendor/heroicons/20/solid/scissors.svg +4 -0
- data/vendor/heroicons/20/solid/server-stack.svg +4 -0
- data/vendor/heroicons/20/solid/server.svg +4 -0
- data/vendor/heroicons/20/solid/share.svg +3 -0
- data/vendor/heroicons/20/solid/shield-check.svg +3 -0
- data/vendor/heroicons/20/solid/shield-exclamation.svg +3 -0
- data/vendor/heroicons/20/solid/shopping-bag.svg +3 -0
- data/vendor/heroicons/20/solid/shopping-cart.svg +3 -0
- data/vendor/heroicons/20/solid/signal-slash.svg +3 -0
- data/vendor/heroicons/20/solid/signal.svg +4 -0
- data/vendor/heroicons/20/solid/slash.svg +3 -0
- data/vendor/heroicons/20/solid/sparkles.svg +3 -0
- data/vendor/heroicons/20/solid/speaker-wave.svg +4 -0
- data/vendor/heroicons/20/solid/speaker-x-mark.svg +3 -0
- data/vendor/heroicons/20/solid/square-2-stack.svg +4 -0
- data/vendor/heroicons/20/solid/square-3-stack-3d.svg +5 -0
- data/vendor/heroicons/20/solid/squares-2x2.svg +3 -0
- data/vendor/heroicons/20/solid/squares-plus.svg +3 -0
- data/vendor/heroicons/20/solid/star.svg +3 -0
- data/vendor/heroicons/20/solid/stop-circle.svg +3 -0
- data/vendor/heroicons/20/solid/stop.svg +3 -0
- data/vendor/heroicons/20/solid/strikethrough.svg +3 -0
- data/vendor/heroicons/20/solid/sun.svg +3 -0
- data/vendor/heroicons/20/solid/swatch.svg +3 -0
- data/vendor/heroicons/20/solid/table-cells.svg +3 -0
- data/vendor/heroicons/20/solid/tag.svg +3 -0
- data/vendor/heroicons/20/solid/ticket.svg +3 -0
- data/vendor/heroicons/20/solid/trash.svg +3 -0
- data/vendor/heroicons/20/solid/trophy.svg +3 -0
- data/vendor/heroicons/20/solid/truck.svg +4 -0
- data/vendor/heroicons/20/solid/tv.svg +4 -0
- data/vendor/heroicons/20/solid/underline.svg +3 -0
- data/vendor/heroicons/20/solid/user-circle.svg +3 -0
- data/vendor/heroicons/20/solid/user-group.svg +3 -0
- data/vendor/heroicons/20/solid/user-minus.svg +3 -0
- data/vendor/heroicons/20/solid/user-plus.svg +3 -0
- data/vendor/heroicons/20/solid/user.svg +3 -0
- data/vendor/heroicons/20/solid/users.svg +3 -0
- data/vendor/heroicons/20/solid/variable.svg +3 -0
- data/vendor/heroicons/20/solid/video-camera-slash.svg +3 -0
- data/vendor/heroicons/20/solid/video-camera.svg +3 -0
- data/vendor/heroicons/20/solid/view-columns.svg +3 -0
- data/vendor/heroicons/20/solid/viewfinder-circle.svg +3 -0
- data/vendor/heroicons/20/solid/wallet.svg +3 -0
- data/vendor/heroicons/20/solid/wifi.svg +3 -0
- data/vendor/heroicons/20/solid/window.svg +3 -0
- data/vendor/heroicons/20/solid/wrench-screwdriver.svg +4 -0
- data/vendor/heroicons/20/solid/wrench.svg +3 -0
- data/vendor/heroicons/20/solid/x-circle.svg +3 -0
- data/vendor/heroicons/20/solid/x-mark.svg +3 -0
- data/vendor/heroicons/24/outline/academic-cap.svg +3 -0
- data/vendor/heroicons/24/outline/adjustments-horizontal.svg +3 -0
- data/vendor/heroicons/24/outline/adjustments-vertical.svg +3 -0
- data/vendor/heroicons/24/outline/archive-box-arrow-down.svg +3 -0
- data/vendor/heroicons/24/outline/archive-box-x-mark.svg +3 -0
- data/vendor/heroicons/24/outline/archive-box.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-down-circle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-down-left.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-down-on-square-stack.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-down-on-square.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-down-right.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-down-tray.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-down.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-left-circle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-left-end-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-left-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-left-start-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-left.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-long-down.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-long-left.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-long-right.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-long-up.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-path-rounded-square.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-path.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-right-circle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-right-end-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-right-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-right-start-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-right.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-small-down.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-small-left.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-small-right.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-small-up.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-top-right-on-square.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-trending-down.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-trending-up.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-turn-down-left.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-turn-down-right.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-turn-left-down.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-turn-left-up.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-turn-right-down.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-turn-right-up.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-turn-up-left.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-turn-up-right.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-up-circle.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-up-left.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-up-on-square-stack.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-up-on-square.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-up-right.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-up-tray.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-up.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-uturn-down.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-uturn-left.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-uturn-right.svg +3 -0
- data/vendor/heroicons/24/outline/arrow-uturn-up.svg +3 -0
- data/vendor/heroicons/24/outline/arrows-pointing-in.svg +3 -0
- data/vendor/heroicons/24/outline/arrows-pointing-out.svg +3 -0
- data/vendor/heroicons/24/outline/arrows-right-left.svg +3 -0
- data/vendor/heroicons/24/outline/arrows-up-down.svg +3 -0
- data/vendor/heroicons/24/outline/at-symbol.svg +3 -0
- data/vendor/heroicons/24/outline/backspace.svg +3 -0
- data/vendor/heroicons/24/outline/backward.svg +3 -0
- data/vendor/heroicons/24/outline/banknotes.svg +3 -0
- data/vendor/heroicons/24/outline/bars-2.svg +3 -0
- data/vendor/heroicons/24/outline/bars-3-bottom-left.svg +3 -0
- data/vendor/heroicons/24/outline/bars-3-bottom-right.svg +3 -0
- data/vendor/heroicons/24/outline/bars-3-center-left.svg +3 -0
- data/vendor/heroicons/24/outline/bars-3.svg +3 -0
- data/vendor/heroicons/24/outline/bars-4.svg +3 -0
- data/vendor/heroicons/24/outline/bars-arrow-down.svg +3 -0
- data/vendor/heroicons/24/outline/bars-arrow-up.svg +3 -0
- data/vendor/heroicons/24/outline/battery-0.svg +3 -0
- data/vendor/heroicons/24/outline/battery-100.svg +3 -0
- data/vendor/heroicons/24/outline/battery-50.svg +3 -0
- data/vendor/heroicons/24/outline/beaker.svg +3 -0
- data/vendor/heroicons/24/outline/bell-alert.svg +3 -0
- data/vendor/heroicons/24/outline/bell-slash.svg +3 -0
- data/vendor/heroicons/24/outline/bell-snooze.svg +3 -0
- data/vendor/heroicons/24/outline/bell.svg +3 -0
- data/vendor/heroicons/24/outline/bold.svg +3 -0
- data/vendor/heroicons/24/outline/bolt-slash.svg +3 -0
- data/vendor/heroicons/24/outline/bolt.svg +3 -0
- data/vendor/heroicons/24/outline/book-open.svg +3 -0
- data/vendor/heroicons/24/outline/bookmark-slash.svg +3 -0
- data/vendor/heroicons/24/outline/bookmark-square.svg +3 -0
- data/vendor/heroicons/24/outline/bookmark.svg +3 -0
- data/vendor/heroicons/24/outline/briefcase.svg +3 -0
- data/vendor/heroicons/24/outline/bug-ant.svg +3 -0
- data/vendor/heroicons/24/outline/building-library.svg +3 -0
- data/vendor/heroicons/24/outline/building-office-2.svg +3 -0
- data/vendor/heroicons/24/outline/building-office.svg +3 -0
- data/vendor/heroicons/24/outline/building-storefront.svg +3 -0
- data/vendor/heroicons/24/outline/cake.svg +3 -0
- data/vendor/heroicons/24/outline/calculator.svg +3 -0
- data/vendor/heroicons/24/outline/calendar-date-range.svg +3 -0
- data/vendor/heroicons/24/outline/calendar-days.svg +3 -0
- data/vendor/heroicons/24/outline/calendar.svg +3 -0
- data/vendor/heroicons/24/outline/camera.svg +4 -0
- data/vendor/heroicons/24/outline/chart-bar-square.svg +3 -0
- data/vendor/heroicons/24/outline/chart-bar.svg +3 -0
- data/vendor/heroicons/24/outline/chart-pie.svg +4 -0
- data/vendor/heroicons/24/outline/chat-bubble-bottom-center-text.svg +3 -0
- data/vendor/heroicons/24/outline/chat-bubble-bottom-center.svg +3 -0
- data/vendor/heroicons/24/outline/chat-bubble-left-ellipsis.svg +3 -0
- data/vendor/heroicons/24/outline/chat-bubble-left-right.svg +3 -0
- data/vendor/heroicons/24/outline/chat-bubble-left.svg +3 -0
- data/vendor/heroicons/24/outline/chat-bubble-oval-left-ellipsis.svg +3 -0
- data/vendor/heroicons/24/outline/chat-bubble-oval-left.svg +3 -0
- data/vendor/heroicons/24/outline/check-badge.svg +3 -0
- data/vendor/heroicons/24/outline/check-circle.svg +3 -0
- data/vendor/heroicons/24/outline/check.svg +3 -0
- data/vendor/heroicons/24/outline/chevron-double-down.svg +3 -0
- data/vendor/heroicons/24/outline/chevron-double-left.svg +3 -0
- data/vendor/heroicons/24/outline/chevron-double-right.svg +3 -0
- data/vendor/heroicons/24/outline/chevron-double-up.svg +4 -0
- data/vendor/heroicons/24/outline/chevron-down.svg +3 -0
- data/vendor/heroicons/24/outline/chevron-left.svg +3 -0
- data/vendor/heroicons/24/outline/chevron-right.svg +3 -0
- data/vendor/heroicons/24/outline/chevron-up-down.svg +3 -0
- data/vendor/heroicons/24/outline/chevron-up.svg +3 -0
- data/vendor/heroicons/24/outline/circle-stack.svg +3 -0
- data/vendor/heroicons/24/outline/clipboard-document-check.svg +3 -0
- data/vendor/heroicons/24/outline/clipboard-document-list.svg +3 -0
- data/vendor/heroicons/24/outline/clipboard-document.svg +3 -0
- data/vendor/heroicons/24/outline/clipboard.svg +3 -0
- data/vendor/heroicons/24/outline/clock.svg +3 -0
- data/vendor/heroicons/24/outline/cloud-arrow-down.svg +3 -0
- data/vendor/heroicons/24/outline/cloud-arrow-up.svg +3 -0
- data/vendor/heroicons/24/outline/cloud.svg +3 -0
- data/vendor/heroicons/24/outline/code-bracket-square.svg +3 -0
- data/vendor/heroicons/24/outline/code-bracket.svg +3 -0
- data/vendor/heroicons/24/outline/cog-6-tooth.svg +4 -0
- data/vendor/heroicons/24/outline/cog-8-tooth.svg +4 -0
- data/vendor/heroicons/24/outline/cog.svg +3 -0
- data/vendor/heroicons/24/outline/command-line.svg +3 -0
- data/vendor/heroicons/24/outline/computer-desktop.svg +3 -0
- data/vendor/heroicons/24/outline/cpu-chip.svg +3 -0
- data/vendor/heroicons/24/outline/credit-card.svg +3 -0
- data/vendor/heroicons/24/outline/cube-transparent.svg +3 -0
- data/vendor/heroicons/24/outline/cube.svg +3 -0
- data/vendor/heroicons/24/outline/currency-bangladeshi.svg +3 -0
- data/vendor/heroicons/24/outline/currency-dollar.svg +3 -0
- data/vendor/heroicons/24/outline/currency-euro.svg +3 -0
- data/vendor/heroicons/24/outline/currency-pound.svg +3 -0
- data/vendor/heroicons/24/outline/currency-rupee.svg +3 -0
- data/vendor/heroicons/24/outline/currency-yen.svg +3 -0
- data/vendor/heroicons/24/outline/cursor-arrow-rays.svg +3 -0
- data/vendor/heroicons/24/outline/cursor-arrow-ripple.svg +3 -0
- data/vendor/heroicons/24/outline/device-phone-mobile.svg +3 -0
- data/vendor/heroicons/24/outline/device-tablet.svg +3 -0
- data/vendor/heroicons/24/outline/divide.svg +3 -0
- data/vendor/heroicons/24/outline/document-arrow-down.svg +3 -0
- data/vendor/heroicons/24/outline/document-arrow-up.svg +3 -0
- data/vendor/heroicons/24/outline/document-chart-bar.svg +3 -0
- data/vendor/heroicons/24/outline/document-check.svg +3 -0
- data/vendor/heroicons/24/outline/document-currency-bangladeshi.svg +3 -0
- data/vendor/heroicons/24/outline/document-currency-dollar.svg +3 -0
- data/vendor/heroicons/24/outline/document-currency-euro.svg +3 -0
- data/vendor/heroicons/24/outline/document-currency-pound.svg +3 -0
- data/vendor/heroicons/24/outline/document-currency-rupee.svg +3 -0
- data/vendor/heroicons/24/outline/document-currency-yen.svg +3 -0
- data/vendor/heroicons/24/outline/document-duplicate.svg +3 -0
- data/vendor/heroicons/24/outline/document-magnifying-glass.svg +3 -0
- data/vendor/heroicons/24/outline/document-minus.svg +3 -0
- data/vendor/heroicons/24/outline/document-plus.svg +3 -0
- data/vendor/heroicons/24/outline/document-text.svg +3 -0
- data/vendor/heroicons/24/outline/document.svg +3 -0
- data/vendor/heroicons/24/outline/ellipsis-horizontal-circle.svg +3 -0
- data/vendor/heroicons/24/outline/ellipsis-horizontal.svg +3 -0
- data/vendor/heroicons/24/outline/ellipsis-vertical.svg +3 -0
- data/vendor/heroicons/24/outline/envelope-open.svg +3 -0
- data/vendor/heroicons/24/outline/envelope.svg +3 -0
- data/vendor/heroicons/24/outline/equals.svg +3 -0
- data/vendor/heroicons/24/outline/exclamation-circle.svg +3 -0
- data/vendor/heroicons/24/outline/exclamation-triangle.svg +3 -0
- data/vendor/heroicons/24/outline/eye-dropper.svg +3 -0
- data/vendor/heroicons/24/outline/eye-slash.svg +3 -0
- data/vendor/heroicons/24/outline/eye.svg +4 -0
- data/vendor/heroicons/24/outline/face-frown.svg +3 -0
- data/vendor/heroicons/24/outline/face-smile.svg +3 -0
- data/vendor/heroicons/24/outline/film.svg +3 -0
- data/vendor/heroicons/24/outline/finger-print.svg +3 -0
- data/vendor/heroicons/24/outline/fire.svg +4 -0
- data/vendor/heroicons/24/outline/flag.svg +3 -0
- data/vendor/heroicons/24/outline/folder-arrow-down.svg +3 -0
- data/vendor/heroicons/24/outline/folder-minus.svg +3 -0
- data/vendor/heroicons/24/outline/folder-open.svg +3 -0
- data/vendor/heroicons/24/outline/folder-plus.svg +3 -0
- data/vendor/heroicons/24/outline/folder.svg +3 -0
- data/vendor/heroicons/24/outline/forward.svg +3 -0
- data/vendor/heroicons/24/outline/funnel.svg +3 -0
- data/vendor/heroicons/24/outline/gif.svg +3 -0
- data/vendor/heroicons/24/outline/gift-top.svg +3 -0
- data/vendor/heroicons/24/outline/gift.svg +3 -0
- data/vendor/heroicons/24/outline/globe-alt.svg +3 -0
- data/vendor/heroicons/24/outline/globe-americas.svg +3 -0
- data/vendor/heroicons/24/outline/globe-asia-australia.svg +3 -0
- data/vendor/heroicons/24/outline/globe-europe-africa.svg +3 -0
- data/vendor/heroicons/24/outline/h1.svg +3 -0
- data/vendor/heroicons/24/outline/h2.svg +3 -0
- data/vendor/heroicons/24/outline/h3.svg +3 -0
- data/vendor/heroicons/24/outline/hand-raised.svg +3 -0
- data/vendor/heroicons/24/outline/hand-thumb-down.svg +3 -0
- data/vendor/heroicons/24/outline/hand-thumb-up.svg +3 -0
- data/vendor/heroicons/24/outline/hashtag.svg +3 -0
- data/vendor/heroicons/24/outline/heart.svg +3 -0
- data/vendor/heroicons/24/outline/home-modern.svg +3 -0
- data/vendor/heroicons/24/outline/home.svg +3 -0
- data/vendor/heroicons/24/outline/identification.svg +3 -0
- data/vendor/heroicons/24/outline/inbox-arrow-down.svg +3 -0
- data/vendor/heroicons/24/outline/inbox-stack.svg +3 -0
- data/vendor/heroicons/24/outline/inbox.svg +3 -0
- data/vendor/heroicons/24/outline/information-circle.svg +3 -0
- data/vendor/heroicons/24/outline/italic.svg +3 -0
- data/vendor/heroicons/24/outline/key.svg +3 -0
- data/vendor/heroicons/24/outline/language.svg +3 -0
- data/vendor/heroicons/24/outline/lifebuoy.svg +3 -0
- data/vendor/heroicons/24/outline/light-bulb.svg +3 -0
- data/vendor/heroicons/24/outline/link-slash.svg +3 -0
- data/vendor/heroicons/24/outline/link.svg +3 -0
- data/vendor/heroicons/24/outline/list-bullet.svg +3 -0
- data/vendor/heroicons/24/outline/lock-closed.svg +3 -0
- data/vendor/heroicons/24/outline/lock-open.svg +3 -0
- data/vendor/heroicons/24/outline/magnifying-glass-circle.svg +3 -0
- data/vendor/heroicons/24/outline/magnifying-glass-minus.svg +3 -0
- data/vendor/heroicons/24/outline/magnifying-glass-plus.svg +3 -0
- data/vendor/heroicons/24/outline/magnifying-glass.svg +3 -0
- data/vendor/heroicons/24/outline/map-pin.svg +4 -0
- data/vendor/heroicons/24/outline/map.svg +3 -0
- data/vendor/heroicons/24/outline/megaphone.svg +3 -0
- data/vendor/heroicons/24/outline/microphone.svg +3 -0
- data/vendor/heroicons/24/outline/minus-circle.svg +3 -0
- data/vendor/heroicons/24/outline/minus-small.svg +3 -0
- data/vendor/heroicons/24/outline/minus.svg +3 -0
- data/vendor/heroicons/24/outline/moon.svg +3 -0
- data/vendor/heroicons/24/outline/musical-note.svg +3 -0
- data/vendor/heroicons/24/outline/newspaper.svg +3 -0
- data/vendor/heroicons/24/outline/no-symbol.svg +3 -0
- data/vendor/heroicons/24/outline/numbered-list.svg +3 -0
- data/vendor/heroicons/24/outline/paint-brush.svg +3 -0
- data/vendor/heroicons/24/outline/paper-airplane.svg +3 -0
- data/vendor/heroicons/24/outline/paper-clip.svg +3 -0
- data/vendor/heroicons/24/outline/pause-circle.svg +3 -0
- data/vendor/heroicons/24/outline/pause.svg +3 -0
- data/vendor/heroicons/24/outline/pencil-square.svg +3 -0
- data/vendor/heroicons/24/outline/pencil.svg +3 -0
- data/vendor/heroicons/24/outline/percent-badge.svg +3 -0
- data/vendor/heroicons/24/outline/phone-arrow-down-left.svg +3 -0
- data/vendor/heroicons/24/outline/phone-arrow-up-right.svg +3 -0
- data/vendor/heroicons/24/outline/phone-x-mark.svg +3 -0
- data/vendor/heroicons/24/outline/phone.svg +3 -0
- data/vendor/heroicons/24/outline/photo.svg +3 -0
- data/vendor/heroicons/24/outline/play-circle.svg +4 -0
- data/vendor/heroicons/24/outline/play-pause.svg +3 -0
- data/vendor/heroicons/24/outline/play.svg +3 -0
- data/vendor/heroicons/24/outline/plus-circle.svg +3 -0
- data/vendor/heroicons/24/outline/plus-small.svg +3 -0
- data/vendor/heroicons/24/outline/plus.svg +3 -0
- data/vendor/heroicons/24/outline/power.svg +3 -0
- data/vendor/heroicons/24/outline/presentation-chart-bar.svg +3 -0
- data/vendor/heroicons/24/outline/presentation-chart-line.svg +3 -0
- data/vendor/heroicons/24/outline/printer.svg +3 -0
- data/vendor/heroicons/24/outline/puzzle-piece.svg +3 -0
- data/vendor/heroicons/24/outline/qr-code.svg +4 -0
- data/vendor/heroicons/24/outline/question-mark-circle.svg +3 -0
- data/vendor/heroicons/24/outline/queue-list.svg +3 -0
- data/vendor/heroicons/24/outline/radio.svg +3 -0
- data/vendor/heroicons/24/outline/receipt-percent.svg +3 -0
- data/vendor/heroicons/24/outline/receipt-refund.svg +3 -0
- data/vendor/heroicons/24/outline/rectangle-group.svg +3 -0
- data/vendor/heroicons/24/outline/rectangle-stack.svg +3 -0
- data/vendor/heroicons/24/outline/rocket-launch.svg +3 -0
- data/vendor/heroicons/24/outline/rss.svg +3 -0
- data/vendor/heroicons/24/outline/scale.svg +3 -0
- data/vendor/heroicons/24/outline/scissors.svg +3 -0
- data/vendor/heroicons/24/outline/server-stack.svg +3 -0
- data/vendor/heroicons/24/outline/server.svg +3 -0
- data/vendor/heroicons/24/outline/share.svg +3 -0
- data/vendor/heroicons/24/outline/shield-check.svg +3 -0
- data/vendor/heroicons/24/outline/shield-exclamation.svg +3 -0
- data/vendor/heroicons/24/outline/shopping-bag.svg +3 -0
- data/vendor/heroicons/24/outline/shopping-cart.svg +3 -0
- data/vendor/heroicons/24/outline/signal-slash.svg +3 -0
- data/vendor/heroicons/24/outline/signal.svg +3 -0
- data/vendor/heroicons/24/outline/slash.svg +3 -0
- data/vendor/heroicons/24/outline/sparkles.svg +3 -0
- data/vendor/heroicons/24/outline/speaker-wave.svg +3 -0
- data/vendor/heroicons/24/outline/speaker-x-mark.svg +3 -0
- data/vendor/heroicons/24/outline/square-2-stack.svg +3 -0
- data/vendor/heroicons/24/outline/square-3-stack-3d.svg +3 -0
- data/vendor/heroicons/24/outline/squares-2x2.svg +3 -0
- data/vendor/heroicons/24/outline/squares-plus.svg +3 -0
- data/vendor/heroicons/24/outline/star.svg +3 -0
- data/vendor/heroicons/24/outline/stop-circle.svg +4 -0
- data/vendor/heroicons/24/outline/stop.svg +3 -0
- data/vendor/heroicons/24/outline/strikethrough.svg +3 -0
- data/vendor/heroicons/24/outline/sun.svg +3 -0
- data/vendor/heroicons/24/outline/swatch.svg +3 -0
- data/vendor/heroicons/24/outline/table-cells.svg +3 -0
- data/vendor/heroicons/24/outline/tag.svg +4 -0
- data/vendor/heroicons/24/outline/ticket.svg +3 -0
- data/vendor/heroicons/24/outline/trash.svg +3 -0
- data/vendor/heroicons/24/outline/trophy.svg +3 -0
- data/vendor/heroicons/24/outline/truck.svg +3 -0
- data/vendor/heroicons/24/outline/tv.svg +3 -0
- data/vendor/heroicons/24/outline/underline.svg +3 -0
- data/vendor/heroicons/24/outline/user-circle.svg +3 -0
- data/vendor/heroicons/24/outline/user-group.svg +3 -0
- data/vendor/heroicons/24/outline/user-minus.svg +3 -0
- data/vendor/heroicons/24/outline/user-plus.svg +3 -0
- data/vendor/heroicons/24/outline/user.svg +3 -0
- data/vendor/heroicons/24/outline/users.svg +3 -0
- data/vendor/heroicons/24/outline/variable.svg +3 -0
- data/vendor/heroicons/24/outline/video-camera-slash.svg +3 -0
- data/vendor/heroicons/24/outline/video-camera.svg +3 -0
- data/vendor/heroicons/24/outline/view-columns.svg +3 -0
- data/vendor/heroicons/24/outline/viewfinder-circle.svg +3 -0
- data/vendor/heroicons/24/outline/wallet.svg +3 -0
- data/vendor/heroicons/24/outline/wifi.svg +3 -0
- data/vendor/heroicons/24/outline/window.svg +3 -0
- data/vendor/heroicons/24/outline/wrench-screwdriver.svg +3 -0
- data/vendor/heroicons/24/outline/wrench.svg +4 -0
- data/vendor/heroicons/24/outline/x-circle.svg +3 -0
- data/vendor/heroicons/24/outline/x-mark.svg +3 -0
- data/vendor/heroicons/24/solid/academic-cap.svg +5 -0
- data/vendor/heroicons/24/solid/adjustments-horizontal.svg +3 -0
- data/vendor/heroicons/24/solid/adjustments-vertical.svg +3 -0
- data/vendor/heroicons/24/solid/archive-box-arrow-down.svg +4 -0
- data/vendor/heroicons/24/solid/archive-box-x-mark.svg +4 -0
- data/vendor/heroicons/24/solid/archive-box.svg +4 -0
- data/vendor/heroicons/24/solid/arrow-down-circle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-down-left.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-down-on-square-stack.svg +4 -0
- data/vendor/heroicons/24/solid/arrow-down-on-square.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-down-right.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-down-tray.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-down.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-left-circle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-left-end-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-left-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-left-start-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-left.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-long-down.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-long-left.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-long-right.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-long-up.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-path-rounded-square.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-path.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-right-circle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-right-end-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-right-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-right-start-on-rectangle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-right.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-small-down.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-small-left.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-small-right.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-small-up.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-top-right-on-square.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-trending-down.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-trending-up.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-turn-down-left.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-turn-down-right.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-turn-left-down.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-turn-left-up.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-turn-right-down.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-turn-right-up.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-turn-up-left.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-turn-up-right.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-up-circle.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-up-left.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-up-on-square-stack.svg +4 -0
- data/vendor/heroicons/24/solid/arrow-up-on-square.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-up-right.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-up-tray.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-up.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-uturn-down.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-uturn-left.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-uturn-right.svg +3 -0
- data/vendor/heroicons/24/solid/arrow-uturn-up.svg +3 -0
- data/vendor/heroicons/24/solid/arrows-pointing-in.svg +3 -0
- data/vendor/heroicons/24/solid/arrows-pointing-out.svg +3 -0
- data/vendor/heroicons/24/solid/arrows-right-left.svg +3 -0
- data/vendor/heroicons/24/solid/arrows-up-down.svg +3 -0
- data/vendor/heroicons/24/solid/at-symbol.svg +3 -0
- data/vendor/heroicons/24/solid/backspace.svg +3 -0
- data/vendor/heroicons/24/solid/backward.svg +3 -0
- data/vendor/heroicons/24/solid/banknotes.svg +5 -0
- data/vendor/heroicons/24/solid/bars-2.svg +3 -0
- data/vendor/heroicons/24/solid/bars-3-bottom-left.svg +3 -0
- data/vendor/heroicons/24/solid/bars-3-bottom-right.svg +3 -0
- data/vendor/heroicons/24/solid/bars-3-center-left.svg +3 -0
- data/vendor/heroicons/24/solid/bars-3.svg +3 -0
- data/vendor/heroicons/24/solid/bars-4.svg +3 -0
- data/vendor/heroicons/24/solid/bars-arrow-down.svg +3 -0
- data/vendor/heroicons/24/solid/bars-arrow-up.svg +3 -0
- data/vendor/heroicons/24/solid/battery-0.svg +3 -0
- data/vendor/heroicons/24/solid/battery-100.svg +3 -0
- data/vendor/heroicons/24/solid/battery-50.svg +4 -0
- data/vendor/heroicons/24/solid/beaker.svg +3 -0
- data/vendor/heroicons/24/solid/bell-alert.svg +4 -0
- data/vendor/heroicons/24/solid/bell-slash.svg +4 -0
- data/vendor/heroicons/24/solid/bell-snooze.svg +3 -0
- data/vendor/heroicons/24/solid/bell.svg +3 -0
- data/vendor/heroicons/24/solid/bold.svg +3 -0
- data/vendor/heroicons/24/solid/bolt-slash.svg +3 -0
- data/vendor/heroicons/24/solid/bolt.svg +3 -0
- data/vendor/heroicons/24/solid/book-open.svg +3 -0
- data/vendor/heroicons/24/solid/bookmark-slash.svg +3 -0
- data/vendor/heroicons/24/solid/bookmark-square.svg +3 -0
- data/vendor/heroicons/24/solid/bookmark.svg +3 -0
- data/vendor/heroicons/24/solid/briefcase.svg +4 -0
- data/vendor/heroicons/24/solid/bug-ant.svg +3 -0
- data/vendor/heroicons/24/solid/building-library.svg +5 -0
- data/vendor/heroicons/24/solid/building-office-2.svg +3 -0
- data/vendor/heroicons/24/solid/building-office.svg +3 -0
- data/vendor/heroicons/24/solid/building-storefront.svg +4 -0
- data/vendor/heroicons/24/solid/cake.svg +3 -0
- data/vendor/heroicons/24/solid/calculator.svg +3 -0
- data/vendor/heroicons/24/solid/calendar-date-range.svg +4 -0
- data/vendor/heroicons/24/solid/calendar-days.svg +4 -0
- data/vendor/heroicons/24/solid/calendar.svg +3 -0
- data/vendor/heroicons/24/solid/camera.svg +4 -0
- data/vendor/heroicons/24/solid/chart-bar-square.svg +3 -0
- data/vendor/heroicons/24/solid/chart-bar.svg +3 -0
- data/vendor/heroicons/24/solid/chart-pie.svg +4 -0
- data/vendor/heroicons/24/solid/chat-bubble-bottom-center-text.svg +3 -0
- data/vendor/heroicons/24/solid/chat-bubble-bottom-center.svg +3 -0
- data/vendor/heroicons/24/solid/chat-bubble-left-ellipsis.svg +3 -0
- data/vendor/heroicons/24/solid/chat-bubble-left-right.svg +4 -0
- data/vendor/heroicons/24/solid/chat-bubble-left.svg +3 -0
- data/vendor/heroicons/24/solid/chat-bubble-oval-left-ellipsis.svg +3 -0
- data/vendor/heroicons/24/solid/chat-bubble-oval-left.svg +3 -0
- data/vendor/heroicons/24/solid/check-badge.svg +3 -0
- data/vendor/heroicons/24/solid/check-circle.svg +3 -0
- data/vendor/heroicons/24/solid/check.svg +3 -0
- data/vendor/heroicons/24/solid/chevron-double-down.svg +4 -0
- data/vendor/heroicons/24/solid/chevron-double-left.svg +4 -0
- data/vendor/heroicons/24/solid/chevron-double-right.svg +4 -0
- data/vendor/heroicons/24/solid/chevron-double-up.svg +4 -0
- data/vendor/heroicons/24/solid/chevron-down.svg +3 -0
- data/vendor/heroicons/24/solid/chevron-left.svg +3 -0
- data/vendor/heroicons/24/solid/chevron-right.svg +3 -0
- data/vendor/heroicons/24/solid/chevron-up-down.svg +3 -0
- data/vendor/heroicons/24/solid/chevron-up.svg +3 -0
- data/vendor/heroicons/24/solid/circle-stack.svg +6 -0
- data/vendor/heroicons/24/solid/clipboard-document-check.svg +4 -0
- data/vendor/heroicons/24/solid/clipboard-document-list.svg +4 -0
- data/vendor/heroicons/24/solid/clipboard-document.svg +5 -0
- data/vendor/heroicons/24/solid/clipboard.svg +3 -0
- data/vendor/heroicons/24/solid/clock.svg +3 -0
- data/vendor/heroicons/24/solid/cloud-arrow-down.svg +3 -0
- data/vendor/heroicons/24/solid/cloud-arrow-up.svg +3 -0
- data/vendor/heroicons/24/solid/cloud.svg +3 -0
- data/vendor/heroicons/24/solid/code-bracket-square.svg +3 -0
- data/vendor/heroicons/24/solid/code-bracket.svg +3 -0
- data/vendor/heroicons/24/solid/cog-6-tooth.svg +3 -0
- data/vendor/heroicons/24/solid/cog-8-tooth.svg +3 -0
- data/vendor/heroicons/24/solid/cog.svg +4 -0
- data/vendor/heroicons/24/solid/command-line.svg +3 -0
- data/vendor/heroicons/24/solid/computer-desktop.svg +3 -0
- data/vendor/heroicons/24/solid/cpu-chip.svg +4 -0
- data/vendor/heroicons/24/solid/credit-card.svg +4 -0
- data/vendor/heroicons/24/solid/cube-transparent.svg +3 -0
- data/vendor/heroicons/24/solid/cube.svg +3 -0
- data/vendor/heroicons/24/solid/currency-bangladeshi.svg +3 -0
- data/vendor/heroicons/24/solid/currency-dollar.svg +4 -0
- data/vendor/heroicons/24/solid/currency-euro.svg +3 -0
- data/vendor/heroicons/24/solid/currency-pound.svg +3 -0
- data/vendor/heroicons/24/solid/currency-rupee.svg +3 -0
- data/vendor/heroicons/24/solid/currency-yen.svg +3 -0
- data/vendor/heroicons/24/solid/cursor-arrow-rays.svg +3 -0
- data/vendor/heroicons/24/solid/cursor-arrow-ripple.svg +3 -0
- data/vendor/heroicons/24/solid/device-phone-mobile.svg +4 -0
- data/vendor/heroicons/24/solid/device-tablet.svg +4 -0
- data/vendor/heroicons/24/solid/divide.svg +3 -0
- data/vendor/heroicons/24/solid/document-arrow-down.svg +4 -0
- data/vendor/heroicons/24/solid/document-arrow-up.svg +4 -0
- data/vendor/heroicons/24/solid/document-chart-bar.svg +4 -0
- data/vendor/heroicons/24/solid/document-check.svg +4 -0
- data/vendor/heroicons/24/solid/document-currency-bangladeshi.svg +3 -0
- data/vendor/heroicons/24/solid/document-currency-dollar.svg +3 -0
- data/vendor/heroicons/24/solid/document-currency-euro.svg +4 -0
- data/vendor/heroicons/24/solid/document-currency-pound.svg +3 -0
- data/vendor/heroicons/24/solid/document-currency-rupee.svg +3 -0
- data/vendor/heroicons/24/solid/document-currency-yen.svg +3 -0
- data/vendor/heroicons/24/solid/document-duplicate.svg +4 -0
- data/vendor/heroicons/24/solid/document-magnifying-glass.svg +5 -0
- data/vendor/heroicons/24/solid/document-minus.svg +4 -0
- data/vendor/heroicons/24/solid/document-plus.svg +4 -0
- data/vendor/heroicons/24/solid/document-text.svg +4 -0
- data/vendor/heroicons/24/solid/document.svg +4 -0
- data/vendor/heroicons/24/solid/ellipsis-horizontal-circle.svg +3 -0
- data/vendor/heroicons/24/solid/ellipsis-horizontal.svg +3 -0
- data/vendor/heroicons/24/solid/ellipsis-vertical.svg +3 -0
- data/vendor/heroicons/24/solid/envelope-open.svg +4 -0
- data/vendor/heroicons/24/solid/envelope.svg +4 -0
- data/vendor/heroicons/24/solid/equals.svg +3 -0
- data/vendor/heroicons/24/solid/exclamation-circle.svg +3 -0
- data/vendor/heroicons/24/solid/exclamation-triangle.svg +3 -0
- data/vendor/heroicons/24/solid/eye-dropper.svg +3 -0
- data/vendor/heroicons/24/solid/eye-slash.svg +5 -0
- data/vendor/heroicons/24/solid/eye.svg +4 -0
- data/vendor/heroicons/24/solid/face-frown.svg +3 -0
- data/vendor/heroicons/24/solid/face-smile.svg +3 -0
- data/vendor/heroicons/24/solid/film.svg +3 -0
- data/vendor/heroicons/24/solid/finger-print.svg +3 -0
- data/vendor/heroicons/24/solid/fire.svg +3 -0
- data/vendor/heroicons/24/solid/flag.svg +3 -0
- data/vendor/heroicons/24/solid/folder-arrow-down.svg +3 -0
- data/vendor/heroicons/24/solid/folder-minus.svg +3 -0
- data/vendor/heroicons/24/solid/folder-open.svg +3 -0
- data/vendor/heroicons/24/solid/folder-plus.svg +3 -0
- data/vendor/heroicons/24/solid/folder.svg +3 -0
- data/vendor/heroicons/24/solid/forward.svg +3 -0
- data/vendor/heroicons/24/solid/funnel.svg +3 -0
- data/vendor/heroicons/24/solid/gif.svg +3 -0
- data/vendor/heroicons/24/solid/gift-top.svg +4 -0
- data/vendor/heroicons/24/solid/gift.svg +3 -0
- data/vendor/heroicons/24/solid/globe-alt.svg +3 -0
- data/vendor/heroicons/24/solid/globe-americas.svg +3 -0
- data/vendor/heroicons/24/solid/globe-asia-australia.svg +4 -0
- data/vendor/heroicons/24/solid/globe-europe-africa.svg +3 -0
- data/vendor/heroicons/24/solid/h1.svg +3 -0
- data/vendor/heroicons/24/solid/h2.svg +3 -0
- data/vendor/heroicons/24/solid/h3.svg +3 -0
- data/vendor/heroicons/24/solid/hand-raised.svg +3 -0
- data/vendor/heroicons/24/solid/hand-thumb-down.svg +3 -0
- data/vendor/heroicons/24/solid/hand-thumb-up.svg +3 -0
- data/vendor/heroicons/24/solid/hashtag.svg +3 -0
- data/vendor/heroicons/24/solid/heart.svg +3 -0
- data/vendor/heroicons/24/solid/home-modern.svg +4 -0
- data/vendor/heroicons/24/solid/home.svg +4 -0
- data/vendor/heroicons/24/solid/identification.svg +3 -0
- data/vendor/heroicons/24/solid/inbox-arrow-down.svg +4 -0
- data/vendor/heroicons/24/solid/inbox-stack.svg +4 -0
- data/vendor/heroicons/24/solid/inbox.svg +3 -0
- data/vendor/heroicons/24/solid/information-circle.svg +3 -0
- data/vendor/heroicons/24/solid/italic.svg +3 -0
- data/vendor/heroicons/24/solid/key.svg +3 -0
- data/vendor/heroicons/24/solid/language.svg +3 -0
- data/vendor/heroicons/24/solid/lifebuoy.svg +3 -0
- data/vendor/heroicons/24/solid/light-bulb.svg +4 -0
- data/vendor/heroicons/24/solid/link-slash.svg +3 -0
- data/vendor/heroicons/24/solid/link.svg +3 -0
- data/vendor/heroicons/24/solid/list-bullet.svg +3 -0
- data/vendor/heroicons/24/solid/lock-closed.svg +3 -0
- data/vendor/heroicons/24/solid/lock-open.svg +3 -0
- data/vendor/heroicons/24/solid/magnifying-glass-circle.svg +4 -0
- data/vendor/heroicons/24/solid/magnifying-glass-minus.svg +3 -0
- data/vendor/heroicons/24/solid/magnifying-glass-plus.svg +3 -0
- data/vendor/heroicons/24/solid/magnifying-glass.svg +3 -0
- data/vendor/heroicons/24/solid/map-pin.svg +3 -0
- data/vendor/heroicons/24/solid/map.svg +3 -0
- data/vendor/heroicons/24/solid/megaphone.svg +3 -0
- data/vendor/heroicons/24/solid/microphone.svg +4 -0
- data/vendor/heroicons/24/solid/minus-circle.svg +3 -0
- data/vendor/heroicons/24/solid/minus-small.svg +3 -0
- data/vendor/heroicons/24/solid/minus.svg +3 -0
- data/vendor/heroicons/24/solid/moon.svg +3 -0
- data/vendor/heroicons/24/solid/musical-note.svg +3 -0
- data/vendor/heroicons/24/solid/newspaper.svg +4 -0
- data/vendor/heroicons/24/solid/no-symbol.svg +3 -0
- data/vendor/heroicons/24/solid/numbered-list.svg +3 -0
- data/vendor/heroicons/24/solid/paint-brush.svg +3 -0
- data/vendor/heroicons/24/solid/paper-airplane.svg +3 -0
- data/vendor/heroicons/24/solid/paper-clip.svg +3 -0
- data/vendor/heroicons/24/solid/pause-circle.svg +3 -0
- data/vendor/heroicons/24/solid/pause.svg +3 -0
- data/vendor/heroicons/24/solid/pencil-square.svg +4 -0
- data/vendor/heroicons/24/solid/pencil.svg +3 -0
- data/vendor/heroicons/24/solid/percent-badge.svg +3 -0
- data/vendor/heroicons/24/solid/phone-arrow-down-left.svg +4 -0
- data/vendor/heroicons/24/solid/phone-arrow-up-right.svg +4 -0
- data/vendor/heroicons/24/solid/phone-x-mark.svg +3 -0
- data/vendor/heroicons/24/solid/phone.svg +3 -0
- data/vendor/heroicons/24/solid/photo.svg +3 -0
- data/vendor/heroicons/24/solid/play-circle.svg +3 -0
- data/vendor/heroicons/24/solid/play-pause.svg +3 -0
- data/vendor/heroicons/24/solid/play.svg +3 -0
- data/vendor/heroicons/24/solid/plus-circle.svg +3 -0
- data/vendor/heroicons/24/solid/plus-small.svg +3 -0
- data/vendor/heroicons/24/solid/plus.svg +3 -0
- data/vendor/heroicons/24/solid/power.svg +3 -0
- data/vendor/heroicons/24/solid/presentation-chart-bar.svg +3 -0
- data/vendor/heroicons/24/solid/presentation-chart-line.svg +3 -0
- data/vendor/heroicons/24/solid/printer.svg +3 -0
- data/vendor/heroicons/24/solid/puzzle-piece.svg +3 -0
- data/vendor/heroicons/24/solid/qr-code.svg +3 -0
- data/vendor/heroicons/24/solid/question-mark-circle.svg +3 -0
- data/vendor/heroicons/24/solid/queue-list.svg +3 -0
- data/vendor/heroicons/24/solid/radio.svg +3 -0
- data/vendor/heroicons/24/solid/receipt-percent.svg +3 -0
- data/vendor/heroicons/24/solid/receipt-refund.svg +3 -0
- data/vendor/heroicons/24/solid/rectangle-group.svg +3 -0
- data/vendor/heroicons/24/solid/rectangle-stack.svg +3 -0
- data/vendor/heroicons/24/solid/rocket-launch.svg +4 -0
- data/vendor/heroicons/24/solid/rss.svg +3 -0
- data/vendor/heroicons/24/solid/scale.svg +3 -0
- data/vendor/heroicons/24/solid/scissors.svg +4 -0
- data/vendor/heroicons/24/solid/server-stack.svg +4 -0
- data/vendor/heroicons/24/solid/server.svg +4 -0
- data/vendor/heroicons/24/solid/share.svg +3 -0
- data/vendor/heroicons/24/solid/shield-check.svg +3 -0
- data/vendor/heroicons/24/solid/shield-exclamation.svg +3 -0
- data/vendor/heroicons/24/solid/shopping-bag.svg +3 -0
- data/vendor/heroicons/24/solid/shopping-cart.svg +3 -0
- data/vendor/heroicons/24/solid/signal-slash.svg +3 -0
- data/vendor/heroicons/24/solid/signal.svg +3 -0
- data/vendor/heroicons/24/solid/slash.svg +3 -0
- data/vendor/heroicons/24/solid/sparkles.svg +3 -0
- data/vendor/heroicons/24/solid/speaker-wave.svg +4 -0
- data/vendor/heroicons/24/solid/speaker-x-mark.svg +3 -0
- data/vendor/heroicons/24/solid/square-2-stack.svg +4 -0
- data/vendor/heroicons/24/solid/square-3-stack-3d.svg +5 -0
- data/vendor/heroicons/24/solid/squares-2x2.svg +3 -0
- data/vendor/heroicons/24/solid/squares-plus.svg +3 -0
- data/vendor/heroicons/24/solid/star.svg +3 -0
- data/vendor/heroicons/24/solid/stop-circle.svg +3 -0
- data/vendor/heroicons/24/solid/stop.svg +3 -0
- data/vendor/heroicons/24/solid/strikethrough.svg +3 -0
- data/vendor/heroicons/24/solid/sun.svg +3 -0
- data/vendor/heroicons/24/solid/swatch.svg +4 -0
- data/vendor/heroicons/24/solid/table-cells.svg +3 -0
- data/vendor/heroicons/24/solid/tag.svg +3 -0
- data/vendor/heroicons/24/solid/ticket.svg +3 -0
- data/vendor/heroicons/24/solid/trash.svg +3 -0
- data/vendor/heroicons/24/solid/trophy.svg +3 -0
- data/vendor/heroicons/24/solid/truck.svg +5 -0
- data/vendor/heroicons/24/solid/tv.svg +4 -0
- data/vendor/heroicons/24/solid/underline.svg +3 -0
- data/vendor/heroicons/24/solid/user-circle.svg +3 -0
- data/vendor/heroicons/24/solid/user-group.svg +4 -0
- data/vendor/heroicons/24/solid/user-minus.svg +3 -0
- data/vendor/heroicons/24/solid/user-plus.svg +3 -0
- data/vendor/heroicons/24/solid/user.svg +3 -0
- data/vendor/heroicons/24/solid/users.svg +3 -0
- data/vendor/heroicons/24/solid/variable.svg +3 -0
- data/vendor/heroicons/24/solid/video-camera-slash.svg +3 -0
- data/vendor/heroicons/24/solid/video-camera.svg +3 -0
- data/vendor/heroicons/24/solid/view-columns.svg +3 -0
- data/vendor/heroicons/24/solid/viewfinder-circle.svg +3 -0
- data/vendor/heroicons/24/solid/wallet.svg +3 -0
- data/vendor/heroicons/24/solid/wifi.svg +3 -0
- data/vendor/heroicons/24/solid/window.svg +3 -0
- data/vendor/heroicons/24/solid/wrench-screwdriver.svg +5 -0
- data/vendor/heroicons/24/solid/wrench.svg +3 -0
- data/vendor/heroicons/24/solid/x-circle.svg +3 -0
- data/vendor/heroicons/24/solid/x-mark.svg +3 -0
- data/vendor/heroicons/LICENSE +21 -0
- data/vendor/heroicons/VERSION +5 -0
- metadata +1664 -0
data/README.md
ADDED
|
@@ -0,0 +1,2739 @@
|
|
|
1
|
+
# Abqari
|
|
2
|
+
|
|
3
|
+
[](https://www.ruby-lang.org/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://github.com/grantrayner/abqari/actions/workflows/ci.yml)
|
|
6
|
+
|
|
7
|
+
A small, opinionated static site generator written in Ruby.
|
|
8
|
+
|
|
9
|
+
Distributed as a Ruby gem: your site stays minimal (content, config,
|
|
10
|
+
optional overrides), while the engine — layouts, themes, helpers, asset
|
|
11
|
+
pipeline — lives in the dependency. Forking the engine is also
|
|
12
|
+
supported for users who want to own all the code.
|
|
13
|
+
|
|
14
|
+
> **Status:** v1.1.0 — first public release, see
|
|
15
|
+
> [CHANGELOG.md](https://github.com/grantrayner/abqari/blob/main/CHANGELOG.md).
|
|
16
|
+
> Distributed via RubyGems (`gem install abqari`) and as a Bundler
|
|
17
|
+
> path/git source. The API follows [semver](https://semver.org/) from
|
|
18
|
+
> this release onward — breaking changes bump the major version, every
|
|
19
|
+
> other change is backwards-compatible within a major.
|
|
20
|
+
|
|
21
|
+
## Documentation
|
|
22
|
+
|
|
23
|
+
This README is the complete guide; these cover single topics in depth.
|
|
24
|
+
They live in the repository rather than in the gem, so the links are
|
|
25
|
+
absolute.
|
|
26
|
+
|
|
27
|
+
| Guide | Covers |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| [Theming](https://github.com/grantrayner/abqari/blob/main/docs/theming.md) | Design tokens, dark mode, ejecting and customising a theme, per-theme webfonts |
|
|
30
|
+
| [Deploying](https://github.com/grantrayner/abqari/blob/main/docs/deploying.md) | Cloudflare Pages, GitHub Pages, Render, custom domains, troubleshooting |
|
|
31
|
+
| [Publications](https://github.com/grantrayner/abqari/blob/main/docs/publications.md) | The canonical publication schema, bundles, series, Folio integration |
|
|
32
|
+
| [Plugins](https://github.com/grantrayner/abqari/blob/main/docs/plugins.md) | Lifecycle hooks, `post_process` filters, writing a site plugin |
|
|
33
|
+
| [Security](https://github.com/grantrayner/abqari/blob/main/docs/security.md) | Trust boundaries, what's hardened, what's powerful by design |
|
|
34
|
+
| [Architecture](https://github.com/grantrayner/abqari/blob/main/docs/ARCHITECTURE.md) | How the build pipeline fits together — for contributors |
|
|
35
|
+
|
|
36
|
+
Reporting a vulnerability:
|
|
37
|
+
[SECURITY.md](https://github.com/grantrayner/abqari/blob/main/SECURITY.md).
|
|
38
|
+
|
|
39
|
+
## Design principles
|
|
40
|
+
|
|
41
|
+
1. **Minimal dependencies.** Seven small runtime gems — three
|
|
42
|
+
(`commonmarker`, `erubi`, `ruby-vips`) do the work on every build;
|
|
43
|
+
the rest cover syntax highlighting, the dev server, and CSV/Base64
|
|
44
|
+
handling. Content importing needs one more (`reverse_markdown`),
|
|
45
|
+
deliberately left out of the runtime set so a normal install doesn't
|
|
46
|
+
pull in nokogiri for a feature it never uses.
|
|
47
|
+
2. **Ruby-first.** ERB templates, real Ruby in views.
|
|
48
|
+
3. **Rails-style structure.** `app/views/`, `app/helpers/`, `config/`.
|
|
49
|
+
4. **SEO baked in.** Sitemap, RSS, JSON Feed, OG, JSON-LD, robots, redirects — all native.
|
|
50
|
+
5. **Privacy by default.** No third-party requests. Self-hosted fonts. No tracking pixels.
|
|
51
|
+
6. **Fast.** WEBrick dev server, content-hashed build cache.
|
|
52
|
+
7. **Standard library of helpers.** truncate, pluralize, time formatting, link helpers.
|
|
53
|
+
|
|
54
|
+
## Quickstart
|
|
55
|
+
|
|
56
|
+
Abqari turns plain Markdown files into a fast, dependency-free
|
|
57
|
+
website you can host anywhere — Cloudflare Pages, Netlify, GitHub
|
|
58
|
+
Pages, your own server. This walkthrough takes you from "nothing
|
|
59
|
+
installed" to "site live on my laptop" in about ten minutes; no
|
|
60
|
+
prior Ruby experience required. If you've used Jekyll, Hugo, or
|
|
61
|
+
Eleventy, the shape will be familiar.
|
|
62
|
+
|
|
63
|
+
### Before you start — open a terminal, pick a projects folder
|
|
64
|
+
|
|
65
|
+
You'll be typing commands into a **terminal** (also called "command
|
|
66
|
+
line" or "shell"). To open one:
|
|
67
|
+
|
|
68
|
+
- **macOS**: press ⌘-Space, type `Terminal`, press Return.
|
|
69
|
+
- **Linux**: most desktops have a terminal app; on Ubuntu/GNOME it's
|
|
70
|
+
Ctrl-Alt-T.
|
|
71
|
+
- **Windows**: open the Ubuntu app you installed via
|
|
72
|
+
[WSL](https://learn.microsoft.com/en-us/windows/wsl/install) —
|
|
73
|
+
that's where you'll run all the commands below.
|
|
74
|
+
|
|
75
|
+
The terminal opens in your home directory (typically `/Users/<you>`
|
|
76
|
+
on macOS or `/home/<you>` on Linux). You'll want a dedicated folder
|
|
77
|
+
for code projects so things stay organised. A common convention is
|
|
78
|
+
`~/apps/` or `~/code/` (the `~` means "your home directory"). Create
|
|
79
|
+
one if you don't already have it:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
mkdir -p ~/apps
|
|
83
|
+
cd ~/apps
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`mkdir -p` creates the folder (the `-p` part is safe to run even if
|
|
87
|
+
it already exists). `cd` "changes directory" — it moves the terminal
|
|
88
|
+
into that folder, so everything that follows happens there.
|
|
89
|
+
|
|
90
|
+
From now on, every command in this walkthrough goes into the
|
|
91
|
+
terminal exactly as shown. Copy a command, paste it (Cmd-V / Ctrl-V),
|
|
92
|
+
press Return. Wait for it to finish before running the next one.
|
|
93
|
+
|
|
94
|
+
### Step 1 — Install the prerequisites (one-time, per machine)
|
|
95
|
+
|
|
96
|
+
Three things need to be on your computer before Abqari can run.
|
|
97
|
+
Most macOS and Linux machines already have at least one of them; the
|
|
98
|
+
commands below are safe to run either way.
|
|
99
|
+
|
|
100
|
+
**Ruby 3.3 or newer** — the language Abqari is written in. Ruby 3.3,
|
|
101
|
+
3.4 and 4.0 are tested on every change: the full suite runs against
|
|
102
|
+
each, and each also gets a clean-container install of the built gem
|
|
103
|
+
following this quickstart.
|
|
104
|
+
|
|
105
|
+
Check what you have:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
ruby --version
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If the output is `ruby 3.3.x` (or newer), you're done with this part.
|
|
112
|
+
Otherwise:
|
|
113
|
+
|
|
114
|
+
- **macOS**: install [Homebrew](https://brew.sh) first if you don't
|
|
115
|
+
have it, then `brew install ruby`. Homebrew prints the exact line
|
|
116
|
+
to add to your shell config (`~/.zshrc` or `~/.bash_profile`) so
|
|
117
|
+
your terminal can find the new Ruby — copy-paste that line, then
|
|
118
|
+
restart your terminal.
|
|
119
|
+
- **Ubuntu / Debian Linux**: `sudo apt install ruby-full`
|
|
120
|
+
- **Alpine Linux**: `apk add ruby ruby-dev build-base`
|
|
121
|
+
- **Windows**: the smoothest path is [WSL](https://learn.microsoft.com/en-us/windows/wsl/install)
|
|
122
|
+
(Windows Subsystem for Linux) — install Ubuntu via WSL, then follow
|
|
123
|
+
the Ubuntu instructions inside it. Native Windows Ruby via
|
|
124
|
+
[RubyInstaller](https://rubyinstaller.org/) also works but has
|
|
125
|
+
rougher edges around libvips and file paths.
|
|
126
|
+
|
|
127
|
+
**libvips** — a small image-processing library Abqari uses to
|
|
128
|
+
generate responsive image variants (AVIF, WebP, multiple widths).
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
brew install vips # macOS
|
|
132
|
+
sudo apt install libvips42 # Ubuntu/Debian
|
|
133
|
+
apk add vips # Alpine
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This step is technically optional — if libvips is missing, Abqari
|
|
137
|
+
falls back to the `vips` / `magick` command-line tools (slower), and
|
|
138
|
+
if neither is available it copies images through unmodified — but the
|
|
139
|
+
optimised pipeline cuts image bytes by 60–80%, so install it once and
|
|
140
|
+
forget about it.
|
|
141
|
+
|
|
142
|
+
**Git** — for downloading Abqari and (eventually) deploying.
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
git --version
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Most modern macOS and Linux installs ship with Git already. If the
|
|
149
|
+
command isn't found: `brew install git` on macOS, `sudo apt install git`
|
|
150
|
+
on Ubuntu/Debian.
|
|
151
|
+
|
|
152
|
+
### Step 2 — Install Abqari (one-time)
|
|
153
|
+
|
|
154
|
+
Install the gem from RubyGems — this puts the global `abqari` command
|
|
155
|
+
on your `PATH`:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
gem install abqari
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Prefer to work from a git checkout of the engine (to read or fork the
|
|
162
|
+
source)? Build and install it locally instead:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git clone https://github.com/grantrayner/abqari.git
|
|
166
|
+
cd abqari
|
|
167
|
+
gem build abqari.gemspec
|
|
168
|
+
gem install ./abqari-*.gem
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Test that it worked:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
abqari version
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
You should see a version number, e.g. `abqari 1.1.0`. If you get
|
|
178
|
+
`command not found`, see [Troubleshooting](#troubleshooting) below.
|
|
179
|
+
|
|
180
|
+
### Step 3 — Create your first site
|
|
181
|
+
|
|
182
|
+
Move back to your projects folder so the new site sits alongside the
|
|
183
|
+
engine, not inside it:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
cd ~/apps
|
|
187
|
+
abqari new my-site
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
(Substitute your own folder name and site name. `my-site` is just an
|
|
191
|
+
example — anything URL-safe works.)
|
|
192
|
+
|
|
193
|
+
This:
|
|
194
|
+
- Creates a new folder called `my-site/`
|
|
195
|
+
- Fills it with everything needed for a working site (`Gemfile`,
|
|
196
|
+
`config/site.yml`, sample home page, about page, example post,
|
|
197
|
+
build/serve scripts)
|
|
198
|
+
- Runs `bundle install` automatically to fetch the gems
|
|
199
|
+
|
|
200
|
+
Takes a few seconds. When it finishes you'll see a "Next:" message.
|
|
201
|
+
|
|
202
|
+
If you'd prefer a totally clean slate — no example post, no
|
|
203
|
+
explanatory text on the home page, just the empty structure — add
|
|
204
|
+
`--blank`:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
abqari new my-site --blank
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
A new site starts posts-only. To enable more collections from the
|
|
211
|
+
start, pass `--with` (comma-separated, any of `photos`,
|
|
212
|
+
`publications`, `workshops`):
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
abqari new my-site --with photos
|
|
216
|
+
abqari new my-site --with publications,workshops
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
For each collection named, the scaffold sets `enabled: true` in
|
|
220
|
+
`config/site.yml` and creates the `content/<name>/index.md` collection
|
|
221
|
+
index page with a nav entry — that index page is a real content file
|
|
222
|
+
the engine doesn't auto-generate, which is the fiddly part of enabling
|
|
223
|
+
a collection by hand. Skipped collections stay opt-in-able later: flip
|
|
224
|
+
`enabled:` in the config and add the index page (the config comments
|
|
225
|
+
show you how).
|
|
226
|
+
|
|
227
|
+
You can also pass your site's name and theme up-front to skip editing
|
|
228
|
+
the config later:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
abqari new my-site --name "Jane's Notebook" --theme magazine --author "Jane Smith"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The available themes are `minimal`, `magazine`, `notebook`, `sand`,
|
|
235
|
+
`bootstrap`, `custom`, and `none`. You can change the theme later by
|
|
236
|
+
editing one line of `config/site.yml`.
|
|
237
|
+
|
|
238
|
+
### Step 4 — See your site in the browser
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
cd my-site
|
|
242
|
+
bin/serve
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Open <http://localhost:4000> in any browser. You'll see your home
|
|
246
|
+
page, with the example post and the about page linked from the nav.
|
|
247
|
+
|
|
248
|
+
`bin/serve` watches for file changes — edit anything in `content/`,
|
|
249
|
+
`config/`, or `app/assets/` and the browser auto-reloads. Press
|
|
250
|
+
`Ctrl-C` in the terminal to stop the server when you're done.
|
|
251
|
+
|
|
252
|
+
### Step 5 — Edit the four files that matter
|
|
253
|
+
|
|
254
|
+
Open `my-site/` in any text editor (VS Code, Sublime Text, plain
|
|
255
|
+
TextEdit, etc.). Four files account for most of what a new site
|
|
256
|
+
needs to customise:
|
|
257
|
+
|
|
258
|
+
| File | What it controls |
|
|
259
|
+
|---|---|
|
|
260
|
+
| `config/site.yml` | Site title, description, author, theme, locale. Comments explain every option. |
|
|
261
|
+
| `content/index.md` | Your home page. Plain Markdown. |
|
|
262
|
+
| `content/about.md` | Your about page. |
|
|
263
|
+
| `app/assets/css/site.css` | Custom CSS that loads after the theme. Empty by default — add brand colours, font swaps, etc. |
|
|
264
|
+
|
|
265
|
+
Save any file and the browser refreshes automatically (as long as
|
|
266
|
+
`bin/serve` is running).
|
|
267
|
+
|
|
268
|
+
To add a new blog post:
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
bin/new post "My new post title"
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
This creates `content/posts/<today's date>-my-new-post-title/index.md`
|
|
275
|
+
with the right frontmatter already filled in. ("Frontmatter" is the
|
|
276
|
+
settings block between the `---` lines at the top of the file — it
|
|
277
|
+
tells Abqari the post's title, date, and other details, and everything
|
|
278
|
+
below it is the post itself.) A freshly scaffolded post looks like:
|
|
279
|
+
|
|
280
|
+
```markdown
|
|
281
|
+
---
|
|
282
|
+
title: My new post title
|
|
283
|
+
date: 2026-08-02
|
|
284
|
+
description:
|
|
285
|
+
tags: []
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
Your post starts here. Plain Markdown.
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Fill in `description:` (one sentence — it shows up in listings,
|
|
292
|
+
search results, and social shares), add tags like
|
|
293
|
+
`tags: [travel, writing]`, write below the second `---`, and save. Drop images (`hero.jpg`,
|
|
294
|
+
`photo.jpg`, etc.) into the same folder as the post, reference them by
|
|
295
|
+
relative name in the Markdown (``), and Abqari's image
|
|
296
|
+
pipeline takes care of responsive variants.
|
|
297
|
+
|
|
298
|
+
To add a standalone page (say, a contact page) and put it in the top
|
|
299
|
+
navigation, run `bin/new page "Contact"`, then add a `nav:` block to
|
|
300
|
+
its frontmatter — the scaffolded `content/about.md` shows the shape.
|
|
301
|
+
|
|
302
|
+
### Step 6 — Build for production
|
|
303
|
+
|
|
304
|
+
When you're ready to put the site online:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
bin/build
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
This writes the finished HTML to a folder called `_site/`. Upload
|
|
311
|
+
the contents of `_site/` to any static host:
|
|
312
|
+
|
|
313
|
+
- **Cloudflare Pages / Netlify / Vercel**: point the host at your
|
|
314
|
+
site's git repository, set the build command to
|
|
315
|
+
`bundle install && bin/build` (the host's build machine needs the
|
|
316
|
+
gems installed first), and the output directory to `_site`. Push to
|
|
317
|
+
the repo and the host takes care of the rest.
|
|
318
|
+
- **GitHub Pages**: same shape, with a workflow file in
|
|
319
|
+
`.github/workflows/`.
|
|
320
|
+
- **Your own server**: `rsync -av _site/ user@server:/var/www/site/`
|
|
321
|
+
or similar.
|
|
322
|
+
|
|
323
|
+
Your scaffolded site ships ready-made GitHub Actions workflows in
|
|
324
|
+
`.github/workflows/` for Cloudflare Pages, GitHub Pages, and Render —
|
|
325
|
+
open the one for your host and follow the comments at the top to
|
|
326
|
+
activate it. There's also a one-command `bin/deploy` script for
|
|
327
|
+
Cloudflare Pages and Netlify in the [engine
|
|
328
|
+
repo](https://github.com/grantrayner/abqari/blob/main/bin/deploy) you
|
|
329
|
+
can copy into your site's `bin/`; it's configured with a `deploy:`
|
|
330
|
+
block in `config/site.yml` (see the comments inside the script).
|
|
331
|
+
|
|
332
|
+
### Troubleshooting
|
|
333
|
+
|
|
334
|
+
**`abqari: command not found`** after `gem install`
|
|
335
|
+
The directory where Ruby installs gem executables isn't on your
|
|
336
|
+
shell's `PATH`. Run `gem env home` — it prints the path. Add
|
|
337
|
+
`<that-path>/bin` to your shell config:
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
# ~/.zshrc or ~/.bashrc
|
|
341
|
+
export PATH="$(gem env home)/bin:$PATH"
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Then restart your terminal or run `source ~/.zshrc`.
|
|
345
|
+
|
|
346
|
+
**`Could not locate Gemfile`** when running `bin/serve` or `bin/build`
|
|
347
|
+
You're running the command from outside your site's directory.
|
|
348
|
+
`cd my-site/` first, then re-run.
|
|
349
|
+
|
|
350
|
+
**`bundle install` fails during `abqari new`**
|
|
351
|
+
Most often this is a Ruby version mismatch — Abqari requires 3.3 or
|
|
352
|
+
newer. Run `ruby --version` to check. If you have multiple Ruby
|
|
353
|
+
versions installed (via rbenv / rvm / asdf), make sure 3.3+ is the
|
|
354
|
+
active one.
|
|
355
|
+
|
|
356
|
+
**`Address already in use` when starting `bin/serve`**
|
|
357
|
+
Something already holds port 4000 — most often another Abqari site's dev
|
|
358
|
+
server. Either stop it, or run this one somewhere else:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
ABQARI_PORT=4001 bin/serve
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
To find out what's holding the port first: `lsof -nP -iTCP:4000 -sTCP:LISTEN`.
|
|
365
|
+
|
|
366
|
+
**Browser shows the old version after I edit a file**
|
|
367
|
+
`bin/serve`'s live-reload depends on the optional `listen` gem. Run
|
|
368
|
+
`bundle install` from inside the site directory — the scaffolded
|
|
369
|
+
Gemfile already includes `listen` in the `:development` group.
|
|
370
|
+
|
|
371
|
+
**`ERROR Errno::ECONNRESET ... io_fillbuf` in the `bin/serve` output**
|
|
372
|
+
Harmless, and filtered out as of 1.1.0 — if you still see it, you're
|
|
373
|
+
on 1.0.0 or have `ABQARI_VERBOSE_LOG=true` set. It means a browser
|
|
374
|
+
closed a connection without a clean shutdown, which happens
|
|
375
|
+
every time you navigate: each tab holds a live-reload stream open, and
|
|
376
|
+
WEBrick logs the teardown as an error with a backtrace. Nothing is
|
|
377
|
+
wrong with your site, and it can't happen in production — the dev
|
|
378
|
+
server isn't part of a deploy.
|
|
379
|
+
|
|
380
|
+
**Images don't appear after I add them**
|
|
381
|
+
Make sure the image file sits in the same folder as the post's
|
|
382
|
+
`index.md` (e.g., `content/posts/<date>-my-post/hero.jpg`) and the
|
|
383
|
+
Markdown references it by relative name only (``,
|
|
384
|
+
not `/posts/.../hero.jpg`).
|
|
385
|
+
|
|
386
|
+
**Build is very slow on the first run**
|
|
387
|
+
Abqari's image pipeline encodes AVIF/WebP variants for every photo
|
|
388
|
+
on first build — slow once, instant afterwards (cached by mtime).
|
|
389
|
+
See [Images](#images) for the cache details.
|
|
390
|
+
|
|
391
|
+
**An error mentions "libvips" or "vips"**
|
|
392
|
+
Re-run `brew install vips` (macOS) or `sudo apt install libvips42`
|
|
393
|
+
(Ubuntu/Debian). The build falls back to plain `<img>` without
|
|
394
|
+
libvips, so you can also temporarily disable the pipeline by setting
|
|
395
|
+
`images.optimise: false` in `config/site.yml`.
|
|
396
|
+
|
|
397
|
+
### What `abqari new` actually creates
|
|
398
|
+
|
|
399
|
+
For reference — these are the files in a fresh scaffold:
|
|
400
|
+
|
|
401
|
+
```
|
|
402
|
+
my-site/
|
|
403
|
+
├── Gemfile # Ruby dependencies; Bundler reads this
|
|
404
|
+
├── README.md # Your site's README (overwrite to taste)
|
|
405
|
+
├── .gitignore
|
|
406
|
+
├── .ruby-version # Pinned to the Ruby that scaffolded the site
|
|
407
|
+
├── .github/
|
|
408
|
+
│ ├── workflows/ # Ready-made deploy workflows (Cloudflare,
|
|
409
|
+
│ │ # GitHub Pages, Render) — opt-in, no-ops
|
|
410
|
+
│ │ # until you enable them (see Deploying)
|
|
411
|
+
│ └── dependabot.yml # Automated engine-update PRs
|
|
412
|
+
├── config/
|
|
413
|
+
│ └── site.yml # Site identity, theme, collections, env URLs
|
|
414
|
+
├── content/
|
|
415
|
+
│ ├── index.md # Home page
|
|
416
|
+
│ ├── about.md # About page
|
|
417
|
+
│ ├── 404.md # Not-found page (served by your host)
|
|
418
|
+
│ └── posts/
|
|
419
|
+
│ ├── index.md # Posts landing
|
|
420
|
+
│ └── welcome/ # Example post (omit with --blank)
|
|
421
|
+
│ └── index.md
|
|
422
|
+
│ # --with photos,publications,workshops adds the matching
|
|
423
|
+
│ # content/<name>/index.md landing pages and enables them in config
|
|
424
|
+
├── app/
|
|
425
|
+
│ └── assets/
|
|
426
|
+
│ └── css/
|
|
427
|
+
│ └── site.css # Your custom CSS override slot
|
|
428
|
+
├── bin/
|
|
429
|
+
│ ├── build # Production build
|
|
430
|
+
│ ├── serve # Dev server with live reload
|
|
431
|
+
│ ├── audit # Build + check links/SEO/a11y/privacy
|
|
432
|
+
│ └── new # Scaffold a new post/page
|
|
433
|
+
├── public/ # Static passthrough (favicons, robots.txt)
|
|
434
|
+
└── vendor/ # Auto-populated caches (fonts, publisher data)
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Engine code (layouts, themes, helpers, asset pipeline) lives in the
|
|
438
|
+
gem — your site repo stays minimal. To upgrade the engine later,
|
|
439
|
+
edit `Gemfile` and run `bundle update abqari`; see
|
|
440
|
+
[Upgrading](#upgrading).
|
|
441
|
+
|
|
442
|
+
If you'd rather assemble a site by hand (migrating from another
|
|
443
|
+
generator, or wanting full control over every file in your repo),
|
|
444
|
+
two manual paths follow — both reach the same end state without the
|
|
445
|
+
scaffolder.
|
|
446
|
+
|
|
447
|
+
### Path A — Add Abqari to your site as a gem (recommended)
|
|
448
|
+
|
|
449
|
+
Create a fresh directory for your site, add Abqari to its `Gemfile`,
|
|
450
|
+
write a minimum site structure, build.
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
mkdir my-site && cd my-site
|
|
454
|
+
git init
|
|
455
|
+
bundle init
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
Edit `Gemfile`:
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
source 'https://rubygems.org'
|
|
462
|
+
|
|
463
|
+
gem 'abqari', '~> 1.1'
|
|
464
|
+
|
|
465
|
+
group :development do
|
|
466
|
+
gem 'listen', '~> 3.8' # optional, for the dev-server file watcher
|
|
467
|
+
end
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Or — pin to a git ref while tracking pre-release work:
|
|
471
|
+
|
|
472
|
+
```ruby
|
|
473
|
+
gem 'abqari', git: 'https://github.com/grantrayner/abqari.git', tag: 'v1.1.0'
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Install the libvips C library once (hard dep for the image pipeline):
|
|
477
|
+
|
|
478
|
+
```bash
|
|
479
|
+
brew install vips # macOS
|
|
480
|
+
sudo apt install libvips42 # Ubuntu/Debian
|
|
481
|
+
apk add vips # Alpine
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
GitHub Actions `ubuntu-latest` ships with libvips preinstalled, so the CI workflow needs nothing more than `bundle install`.
|
|
485
|
+
|
|
486
|
+
Then:
|
|
487
|
+
|
|
488
|
+
```bash
|
|
489
|
+
bundle install
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Your site needs only the bits that are genuinely *yours* — config,
|
|
493
|
+
content, public assets, and thin bin wrappers. The minimum:
|
|
494
|
+
|
|
495
|
+
```
|
|
496
|
+
my-site/
|
|
497
|
+
├── Gemfile, Gemfile.lock
|
|
498
|
+
├── config/
|
|
499
|
+
│ └── site.yml # site config
|
|
500
|
+
├── content/
|
|
501
|
+
│ └── index.md # home page
|
|
502
|
+
├── public/ # favicons, robots.txt, etc.
|
|
503
|
+
└── bin/
|
|
504
|
+
├── build # production build
|
|
505
|
+
└── serve # dev server with livereload
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
`bin/build` (chmod +x):
|
|
509
|
+
|
|
510
|
+
```ruby
|
|
511
|
+
#!/usr/bin/env ruby
|
|
512
|
+
# frozen_string_literal: true
|
|
513
|
+
|
|
514
|
+
ENV['ABQARI_ENV'] ||= 'production'
|
|
515
|
+
ENV['ABQARI_SITE_ROOT'] ||= File.expand_path('..', __dir__)
|
|
516
|
+
|
|
517
|
+
require 'bundler/setup'
|
|
518
|
+
require 'abqari'
|
|
519
|
+
|
|
520
|
+
Abqari::Site.new.build
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
`bin/serve` is the same shape but with `ABQARI_ENV` defaulting to
|
|
524
|
+
`development` and a final line `Abqari::Server.new(site).start`.
|
|
525
|
+
|
|
526
|
+
Minimum `config/site.yml`:
|
|
527
|
+
|
|
528
|
+
```yaml
|
|
529
|
+
title: My Site
|
|
530
|
+
description: A site built with Abqari.
|
|
531
|
+
author: Your Name
|
|
532
|
+
locale: en
|
|
533
|
+
theme: minimal
|
|
534
|
+
|
|
535
|
+
environments:
|
|
536
|
+
development:
|
|
537
|
+
url: http://localhost:4000
|
|
538
|
+
indexable: false
|
|
539
|
+
production:
|
|
540
|
+
url: https://example.com
|
|
541
|
+
indexable: true
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
Minimum `content/index.md`:
|
|
545
|
+
|
|
546
|
+
```markdown
|
|
547
|
+
---
|
|
548
|
+
title: Hello
|
|
549
|
+
layout: application
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
Welcome to my site, built with Abqari.
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
Build and serve:
|
|
556
|
+
|
|
557
|
+
```bash
|
|
558
|
+
bin/build # builds _site/ in production mode
|
|
559
|
+
bin/serve # serves on http://localhost:4000 with livereload
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
Engine code (layouts, themes, helpers, `_common.css`, asset pipeline)
|
|
563
|
+
loads from the gem. Drop a file at the same path inside your site to
|
|
564
|
+
override any specific engine file — see [Overrides](#overrides).
|
|
565
|
+
|
|
566
|
+
**Upgrading is `bundle update abqari`.** See
|
|
567
|
+
[Upgrading](#upgrading) for the full workflow.
|
|
568
|
+
|
|
569
|
+
### Path B — Fork the engine
|
|
570
|
+
|
|
571
|
+
If you'd rather own the engine code directly (every layout, theme,
|
|
572
|
+
and helper in your repo, no gem dependency), clone the engine as
|
|
573
|
+
your starting point:
|
|
574
|
+
|
|
575
|
+
```bash
|
|
576
|
+
git clone https://github.com/grantrayner/abqari my-site
|
|
577
|
+
cd my-site
|
|
578
|
+
bundle install
|
|
579
|
+
bin/new --name "My Blog" --locale en --theme minimal --clean
|
|
580
|
+
bin/serve
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
`bin/new` configures `config/site.yml` and with `--clean` removes the
|
|
584
|
+
example posts and resets git history. You inherit every file in the
|
|
585
|
+
engine repo and edit it directly. Engine updates require git-merge
|
|
586
|
+
from upstream (`bin/upgrade`) — see [Upgrading](#upgrading).
|
|
587
|
+
|
|
588
|
+
This path is the right call when you want to deeply customize engine
|
|
589
|
+
internals. For most sites, Path A is cleaner.
|
|
590
|
+
|
|
591
|
+
### Commands
|
|
592
|
+
|
|
593
|
+
A freshly scaffolded site (`abqari new`) ships eight `bin/` scripts —
|
|
594
|
+
`build`, `serve`, `audit`, `new`, `import`, `fetch-fonts`,
|
|
595
|
+
`fetch-folio`, and `index`. The rest in the table below (`deploy`,
|
|
596
|
+
`upgrade`, `test`) are thin one-line wrappers around the engine; add
|
|
597
|
+
them when you need the feature (each linked section shows the exact
|
|
598
|
+
contents), or inherit them all by
|
|
599
|
+
[forking the engine](#path-b--fork-the-engine). `bin/test` is
|
|
600
|
+
engine-only — it runs the engine's own suite, not your site's.
|
|
601
|
+
|
|
602
|
+
| Command | Scaffolded? | What it does |
|
|
603
|
+
| --- | --- | --- |
|
|
604
|
+
| `bin/new` | ✅ | Configure site identity, locale, theme; `--clean` to wipe examples. See [Generator](#generator). |
|
|
605
|
+
| `bin/new post "Title"` | ✅ | Scaffold a new post bundle: `content/posts/<date>-<slug>/index.md`. |
|
|
606
|
+
| `bin/new page "Title"` | ✅ | Scaffold a top-level page: `content/<slug>.md`. |
|
|
607
|
+
| `bin/import <platform> <source>` | ✅ | Import content from another platform's export (Substack, Ghost, Jekyll). See [Importing content](#importing-content). |
|
|
608
|
+
| `bin/build` | ✅ | Render the site to `_site/`. Defaults to `ABQARI_ENV=production`. |
|
|
609
|
+
| `bin/serve` | ✅ | Build and serve on `:4000`. Defaults to `ABQARI_ENV=development` with file-watch + livereload. |
|
|
610
|
+
| `bin/audit` | ✅ | Build the site, walk the output, write a markdown audit report. See [Audit](#audit). |
|
|
611
|
+
| `bin/fetch-fonts` | ✅ | Download self-hosted Google Fonts to `vendor/fonts/`. See [Fonts](#fonts). |
|
|
612
|
+
| `bin/fetch-folio` | ✅ | Force-refresh the Folio publications cache. See [Folio integration](#folio-integration). |
|
|
613
|
+
| `bin/index` | ✅ | Build the Pagefind search index (also auto-runs in `bin/build` when `search: true`). See [Search](#search). |
|
|
614
|
+
| `bin/deploy` | add as needed | Build and ship `_site/` to your configured host. See [Deploying](#deploying). |
|
|
615
|
+
| `bin/upgrade` | fork only | Pull engine updates from your upstream Abqari repo. See [Upgrading](#upgrading). |
|
|
616
|
+
| `bin/test` | engine only | Run the engine's Minitest suite. See [Testing](#testing). |
|
|
617
|
+
|
|
618
|
+
### Environment variables
|
|
619
|
+
|
|
620
|
+
Abqari reads these at build/serve time. Most sites need none of them —
|
|
621
|
+
they're for CI, previews, and edge cases.
|
|
622
|
+
|
|
623
|
+
| Variable | Default | Purpose |
|
|
624
|
+
| --- | --- | --- |
|
|
625
|
+
| `ABQARI_ENV` | `development` | Build environment. `production` enables indexing + minification and excludes drafts/future posts. |
|
|
626
|
+
| `ABQARI_SITE_ROOT` | current dir | Site root. Set by `bin/*` wrappers; override to build a site elsewhere. |
|
|
627
|
+
| `ABQARI_OUTPUT_DIR` | `<site>/_site` | Output directory. Refuses to wipe the site root or a source dir. |
|
|
628
|
+
| `ABQARI_LOG` | `info` | Log level: `debug` / `info` / `warn` / `error`. |
|
|
629
|
+
| `ABQARI_DEBUG` | unset | `1` prints a backtrace for user-facing errors (bad YAML, invalid config, bad flag), which are otherwise reported as a single line. Unexpected exceptions always keep their backtrace. |
|
|
630
|
+
| `ABQARI_INCLUDE_DRAFTS` | unset | `true` forces `published: false` posts into the build. |
|
|
631
|
+
| `ABQARI_INCLUDE_FUTURE` | unset | `true` forces future-dated posts into the build. |
|
|
632
|
+
| `ABQARI_STRICT_CONFIG` | unset | `true` turns unknown-config-key warnings (and a missing `config/site.yml`) into hard errors. Recommended in CI. |
|
|
633
|
+
| `ABQARI_PORT` | `4000` | Dev-server port. Useful when you run several Abqari sites and 4000 is taken. Rejects anything that isn't a number in 1–65535, rather than silently binding a port you can't guess. |
|
|
634
|
+
| `ABQARI_BIND` | `127.0.0.1` | Dev-server bind address. Set `0.0.0.0` to preview on a phone over LAN. |
|
|
635
|
+
| `ABQARI_VERBOSE_LOG` | unset | `true` restores stock WEBrick dev-server logging, including the client-disconnect backtraces Abqari filters out. For debugging the socket layer. |
|
|
636
|
+
| `ABQARI_SLOW_PAGE_MS` | `500` | Slow-page warning threshold (ms); `0` disables it. |
|
|
637
|
+
| `ABQARI_ALLOW_EXTERNAL_OUTPUT` | unset | `true` permits an output dir outside the site root (CI artifact mounts). |
|
|
638
|
+
| `ABQARI_ALLOW_EXTERNAL_SYMLINKS` | unset | `true` permits symlinks that escape the site root. Avoid. |
|
|
639
|
+
|
|
640
|
+
Boolean variables above accept the literal string `true` (or `1`/`yes`
|
|
641
|
+
for `ABQARI_DEBUG`) — `on`, `TRUE` and `y` are not recognised and read
|
|
642
|
+
as unset. This is deliberate: a build flag that half-matches is worse
|
|
643
|
+
than one that plainly doesn't.
|
|
644
|
+
|
|
645
|
+
**Secrets** (POSSE syndication + webmentions) can come from the
|
|
646
|
+
environment instead of committed `config/site.yml` — set these in your
|
|
647
|
+
CI's secret store:
|
|
648
|
+
|
|
649
|
+
| Variable | Replaces |
|
|
650
|
+
| --- | --- |
|
|
651
|
+
| `ABQARI_MASTODON_TOKEN` | `syndication.mastodon.access_token` |
|
|
652
|
+
| `ABQARI_BLUESKY_APP_PASSWORD` | `syndication.bluesky.app_password` |
|
|
653
|
+
| `ABQARI_WEBMENTION_IO_TOKEN` | `indieweb.webmention_io_token` |
|
|
654
|
+
|
|
655
|
+
> Syndication and webmention sending only fire in `ABQARI_ENV=production`
|
|
656
|
+
> — a dev build (`bin/serve`) never posts to Mastodon/Bluesky, even
|
|
657
|
+
> with `syndication.enabled: true`.
|
|
658
|
+
|
|
659
|
+
## Generator
|
|
660
|
+
|
|
661
|
+
`bin/new` is the one-shot configurator you run on a freshly cloned site. It edits `config/site.yml` in place (preserving comments) and optionally clears the example content.
|
|
662
|
+
|
|
663
|
+
```bash
|
|
664
|
+
bin/new --name "My Blog" \
|
|
665
|
+
--description "Words about things" \
|
|
666
|
+
--locale fr \
|
|
667
|
+
--theme bootstrap \
|
|
668
|
+
--clean
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
| Flag | What it does |
|
|
672
|
+
| --- | --- |
|
|
673
|
+
| `--name NAME` | **Required.** Sets `title:` and `author:` in `config/site.yml`. |
|
|
674
|
+
| `--description TEXT` | Sets `description:` (used in `<meta>`, OG tags, RSS). |
|
|
675
|
+
| `--locale CODE` | ISO 639-1 code (`en`, `es`, `fr`, …). Defaults to `en`. See [Locale](#locale). |
|
|
676
|
+
| `--theme NAME` | One of `minimal`, `magazine`, `notebook`, `sand`, `bootstrap`, `custom`, `none`. Defaults to `minimal`. |
|
|
677
|
+
| `--clean` | Removes example posts, audit reports, vendor fonts, and `_site/`; resets git history on a fresh `main` branch. |
|
|
678
|
+
|
|
679
|
+
Run it once after `git clone`; after that, edit `config/site.yml` directly. The generator only touches site identity and feature switches — it never modifies templates, layouts, or engine code, since those are yours to customise.
|
|
680
|
+
|
|
681
|
+
If you skip `bin/new`, the example content stays. Useful for poking around — but commit your own first commit before the example posts get tangled with your real ones.
|
|
682
|
+
|
|
683
|
+
## Importing content
|
|
684
|
+
|
|
685
|
+
If you're moving from another platform, `bin/import` pulls content from a platform export into your site's `content/<collection>/` tree. Each imported post lands as a regular Abqari bundle — same layout, same frontmatter conventions, same image pipeline — so the imported posts are indistinguishable from posts you'd write fresh.
|
|
686
|
+
|
|
687
|
+
**One-time prerequisite.** Importing needs the `reverse_markdown` gem, which Abqari doesn't install by default — it pulls in nokogiri, and a site that never imports shouldn't carry it. Uncomment the line in your `Gemfile` and `bundle install`, or:
|
|
688
|
+
|
|
689
|
+
```bash
|
|
690
|
+
gem install reverse_markdown
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
`bin/import` tells you this if it's missing, so there's nothing to remember up front.
|
|
694
|
+
|
|
695
|
+
```bash
|
|
696
|
+
bin/import substack ~/Downloads/substack-export.zip
|
|
697
|
+
bin/import ghost ~/Downloads/export.json
|
|
698
|
+
bin/import jekyll ~/old-site/
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
After each run, the script writes `_import_report.md` (gitignored) summarising what was imported, skipped, and warned about. Open it in your editor before committing — imports rarely need post-edit tweaks, but the report tells you exactly where to look if they do.
|
|
702
|
+
|
|
703
|
+
### Supported platforms
|
|
704
|
+
|
|
705
|
+
| Platform | Source format | Notes |
|
|
706
|
+
|---|---|---|
|
|
707
|
+
| `substack` | `.zip` (or unzipped directory) | Reads `posts.csv` + `posts/<id>.html`. Filters drafts and podcasts; paid-only posts come through with `audience: only_paid` in the frontmatter. Strips subscription widgets, share buttons, polls, and other platform chrome before HTML→Markdown conversion. |
|
|
708
|
+
| `ghost` | `export.json` | Reads the standard Ghost 5.x / 6.x JSON export. Filters drafts; members-only and paid posts come through with `visibility:` set in the frontmatter. Resolves tags via the export's `posts_tags` join table. Strips Ghost-specific bookmark, callout, and product cards. |
|
|
709
|
+
| `jekyll` | Site source directory | Reads `_posts/YYYY-MM-DD-slug.md`. Pure-markdown bodies pass through verbatim; `.html` posts run through HTML→Markdown. Merges `categories:` into `tags:`. Copies colocated assets from `assets/posts/<slug>/` (or `assets/<slug>/`) alongside the new bundle. Warns when Liquid template tags (`{% include %}`, `{% post_url %}`) need manual conversion to ERB. |
|
|
710
|
+
|
|
711
|
+
### Options
|
|
712
|
+
|
|
713
|
+
All flags are common across platforms.
|
|
714
|
+
|
|
715
|
+
| Flag | What it does |
|
|
716
|
+
| --- | --- |
|
|
717
|
+
| `--into COLLECTION` | Destination collection (default: `posts`). Use `--into notes` to import into `content/notes/`, `--into archive` into `content/archive/`, etc. |
|
|
718
|
+
| `--keep-remote` | Leave image URLs as remote references instead of downloading. Faster, but source-platform CDN URLs expire over time — recommended only for testing or dry-runs. |
|
|
719
|
+
| `--flat` | Write flat-file posts (`content/<col>/<slug>.md`) instead of the default bundle layout (`content/<col>/<slug>/index.md`). |
|
|
720
|
+
| `--dry-run` | Process the source but write nothing to disk. The report is also skipped — use this to preview the run. |
|
|
721
|
+
| `--force` | Overwrite existing files. Default is to skip files that already exist on disk, so re-running an import is safe. |
|
|
722
|
+
| `--canonical` | Write a `canonical_url:` frontmatter key pointing back at the source URL. For sites mirroring content rather than migrating off the source — search engines treat the source as the canonical version. |
|
|
723
|
+
| `--strip SEL,SEL` | Comma-separated CSS selectors (`tag` or `tag.class`) to strip from each post body before HTML→Markdown conversion. Use when the per-platform defaults don't catch every chrome block your site's export contains. |
|
|
724
|
+
|
|
725
|
+
### What you get
|
|
726
|
+
|
|
727
|
+
For each post, the importer:
|
|
728
|
+
|
|
729
|
+
1. **Parses the source** — Substack's CSV manifest + per-post HTML files, Ghost's JSON tree, or Jekyll's `_posts/` directory.
|
|
730
|
+
2. **Strips platform chrome** — known divs/widgets/CTAs the source platform inserts (subscribe boxes, share buttons, paywalls). Each importer ships its own selector list; extend with `--strip`.
|
|
731
|
+
3. **Converts HTML → Markdown** via `reverse_markdown` with GitHub-flavoured extensions. Unknown tags pass through verbatim rather than being silently dropped, so a tag the importer doesn't recognise is visible for manual cleanup rather than lost.
|
|
732
|
+
4. **Downloads remote images** into the post's bundle directory (default; opt out with `--keep-remote`). The image pipeline then re-encodes them to AVIF + WebP with responsive variants on the next build, exactly as it does for hand-authored posts. See [Images](#images) for the pipeline details.
|
|
733
|
+
5. **Resolves slug collisions** by appending a numeric suffix — duplicates become `my-post`, `my-post-2`, `my-post-3`.
|
|
734
|
+
6. **Writes a bundle** at `content/<collection>/YYYY-MM-DD-<slug>/index.md` containing:
|
|
735
|
+
- `title`, `published: true`, `date`, optional `description` / `tags` / `image`
|
|
736
|
+
- `redirect_from: [/p/<original-slug>/]` (Substack) or `[/<original-slug>/]` (Ghost) so existing inbound links keep working
|
|
737
|
+
- `canonical_url:` if you passed `--canonical`
|
|
738
|
+
7. **Writes `_import_report.md`** in the site root summarising every post with its bundle path plus any warnings (HTTP failures, Liquid tags needing manual conversion, etc.).
|
|
739
|
+
|
|
740
|
+
### Image handling
|
|
741
|
+
|
|
742
|
+
Remote images get downloaded into the post's bundle by default. This is the durable choice: source-platform CDN URLs (`substackcdn.com/image/fetch/...`, Ghost's blob storage, Medium's image proxy) get rotated periodically, and posts that keep remote URLs break six to twenty-four months after migration.
|
|
743
|
+
|
|
744
|
+
Downloaded images sit alongside `index.md` like any hand-authored bundle asset. Abqari's [image pipeline](#images) picks them up on the next build and emits responsive AVIF + WebP variants, just as it does for bundles you authored from scratch.
|
|
745
|
+
|
|
746
|
+
If a download fails (404, timeout, oversized file), the URL is left in place and a warning lands in the report. The post still imports — one missing image doesn't sink the run.
|
|
747
|
+
|
|
748
|
+
### Idempotency
|
|
749
|
+
|
|
750
|
+
Re-running the importer over the same source is safe. Files that already exist on disk are skipped by default and show up in the report as `skipped`. Use `--force` if you've made source-side edits and want to overwrite the local copies.
|
|
751
|
+
|
|
752
|
+
If you've made local edits to imported posts and want to pull in only newly-published source posts, leave `--force` off. If you want to start fresh after a botched run, delete the relevant bundle directories first.
|
|
753
|
+
|
|
754
|
+
### Migration tips
|
|
755
|
+
|
|
756
|
+
- **Dry-run first.** `--dry-run` shows what the import would produce without writing anything. Useful for sanity-checking platform-specific quirks (paid posts, drafts, slug collisions) before you commit.
|
|
757
|
+
- **Commit the import as one change.** Once you've spot-checked the output, commit the new bundle directories together — the next build will optimise the downloaded images and the diff stays clean.
|
|
758
|
+
- **Read `_import_report.md`.** The report is the single source of truth for "what changed in this import." Glance through it for warnings before you delete it.
|
|
759
|
+
- **Consider `--canonical` for mirroring.** If you're keeping the source site live alongside Abqari (cross-posting from Substack, for example), pass `--canonical` so the imported posts carry `canonical_url:` pointing back. Search engines treat the source as the canonical version.
|
|
760
|
+
- **Custom platforms.** No importer for your source? Each importer is a single file under `lib/abqari/importers/`; the base class handles the writer pipeline (HTML→Markdown, image download, frontmatter, idempotency, report) so a new importer is just "parse this source into `ImportedPost` records." Open an issue or contribute one.
|
|
761
|
+
|
|
762
|
+
## Project structure
|
|
763
|
+
|
|
764
|
+
### Path A — Consuming site (gem dependency)
|
|
765
|
+
|
|
766
|
+
A site that consumes Abqari as a gem is small. Only the things that
|
|
767
|
+
are genuinely yours live in the repo; the engine code (layouts,
|
|
768
|
+
themes, helpers, `_common.css`, asset pipeline) is loaded from the
|
|
769
|
+
gem.
|
|
770
|
+
|
|
771
|
+
```
|
|
772
|
+
my-site/
|
|
773
|
+
├── .github/workflows/
|
|
774
|
+
│ ├── deploy-cloudflare.yml Cloudflare Pages deploy (gated by ENABLE_CLOUDFLARE)
|
|
775
|
+
│ ├── deploy-render.yml Render deploy via deploy hook (gated by ENABLE_RENDER)
|
|
776
|
+
│ └── deploy-github-pages.yml GitHub Pages deploy (gated by ENABLE_GITHUB_PAGES)
|
|
777
|
+
├── Gemfile, Gemfile.lock Declares the abqari gem dependency
|
|
778
|
+
├── bin/
|
|
779
|
+
│ ├── build Thin wrapper: requires 'abqari', builds
|
|
780
|
+
│ ├── serve Same, but for dev mode with livereload
|
|
781
|
+
│ ├── audit Site audit → audit/<timestamp>.md
|
|
782
|
+
│ ├── new Scaffold a post or page in content/
|
|
783
|
+
│ ├── import Import from Substack / Ghost / Jekyll exports
|
|
784
|
+
│ ├── fetch-folio Force-refresh the Folio cache
|
|
785
|
+
│ ├── fetch-fonts Download self-hosted Google Fonts
|
|
786
|
+
│ └── index Build the Pagefind search index (if enabled)
|
|
787
|
+
├── config/
|
|
788
|
+
│ └── site.yml Site config + per-environment overrides
|
|
789
|
+
├── content/ Your writing (markdown + frontmatter)
|
|
790
|
+
│ ├── index.md Home page
|
|
791
|
+
│ ├── about.md About page
|
|
792
|
+
│ ├── posts/ Posts collection (bundled directories)
|
|
793
|
+
│ │ └── 2026-05-07-hello/
|
|
794
|
+
│ │ ├── index.md The post body
|
|
795
|
+
│ │ └── hero.jpg Colocated assets (relative refs)
|
|
796
|
+
│ ├── photos/ Photos collection (image-centric posts)
|
|
797
|
+
│ ├── publications/ Publications (books, atlases, etc.)
|
|
798
|
+
│ └── workshops/ Workshops (training offerings)
|
|
799
|
+
├── data/ YAML/JSON data files for nav, footer, etc.
|
|
800
|
+
├── public/ Static files copied verbatim → _site/
|
|
801
|
+
├── app/ Optional — only present when you override
|
|
802
|
+
│ ├── assets/css/site.css Site-local CSS (loads after theme)
|
|
803
|
+
│ ├── icons/<name>.svg Override engine icons or add your own
|
|
804
|
+
│ └── views/ Override layouts or partials
|
|
805
|
+
│ ├── layouts/<name>.html.erb (rare — most users don't need this)
|
|
806
|
+
│ └── partials/<name>.html.erb
|
|
807
|
+
└── vendor/
|
|
808
|
+
├── folio/ Folio API cache (auto-generated)
|
|
809
|
+
└── fonts/ Self-hosted webfont cache (`bin/fetch-fonts`)
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Anything in `app/views/`, `app/assets/`, `app/icons/`, or `themes/` of
|
|
813
|
+
your site overrides the gem's version of the same file. See
|
|
814
|
+
[Overrides](#overrides) for the resolution rule.
|
|
815
|
+
|
|
816
|
+
### Path B — Forked engine
|
|
817
|
+
|
|
818
|
+
The engine repo itself is also a working Abqari site (it's used as
|
|
819
|
+
the fixture for development). If you fork the engine, you inherit
|
|
820
|
+
every file in this list:
|
|
821
|
+
|
|
822
|
+
```
|
|
823
|
+
abqari/
|
|
824
|
+
├── .github/workflows/deploy-*.yml Three host-specific deploy workflows (Cloudflare, Render, Pages)
|
|
825
|
+
├── abqari.gemspec Gem manifest (lib/, app/, themes/, etc.)
|
|
826
|
+
├── app/
|
|
827
|
+
│ ├── assets/css/_common.css Structural CSS (layout, components)
|
|
828
|
+
│ ├── assets/js/ theme-toggle, contact, carousel, …
|
|
829
|
+
│ ├── icons/ Ships GitHub, RSS, mail, X, Mastodon,
|
|
830
|
+
│ │ Bluesky, LinkedIn, sun, moon
|
|
831
|
+
│ └── views/
|
|
832
|
+
│ ├── layouts/ application | post | photo | publication |
|
|
833
|
+
│ │ bundle | series | workshop | …
|
|
834
|
+
│ └── partials/ head | nav | footer | post_list |
|
|
835
|
+
│ publication_card | newsletter_subscribe | …
|
|
836
|
+
├── bin/ build | serve | audit | new | check | console |
|
|
837
|
+
│ fetch-fonts | fetch-folio | icons | import |
|
|
838
|
+
│ index | deploy | upgrade | test
|
|
839
|
+
├── config/site.yml Engine fixture's site config (= the reference)
|
|
840
|
+
├── content/ Engine fixture content
|
|
841
|
+
├── lib/
|
|
842
|
+
│ ├── abqari.rb Top-level loader (defines VERSION)
|
|
843
|
+
│ └── abqari/ Generator engine — see file headers
|
|
844
|
+
├── themes/ minimal | magazine | notebook | sand |
|
|
845
|
+
│ custom | bootstrap
|
|
846
|
+
├── test/ Minitest suite
|
|
847
|
+
├── CHANGELOG.md
|
|
848
|
+
├── CONTRIBUTING.md
|
|
849
|
+
└── LICENSE MIT
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
## How it works
|
|
853
|
+
|
|
854
|
+
`bin/build` does five things:
|
|
855
|
+
|
|
856
|
+
1. Read `config/site.yml`.
|
|
857
|
+
2. Walk `content/**/*.md`, parse YAML front matter and Markdown body.
|
|
858
|
+
3. Convert Markdown → HTML via CommonMark + GFM.
|
|
859
|
+
4. Render each page through its layout (`app/views/layouts/<layout>.html.erb`).
|
|
860
|
+
5. Copy `public/` to `_site/` verbatim.
|
|
861
|
+
|
|
862
|
+
Each Markdown file becomes one HTML file with a "pretty URL" path. Posts are
|
|
863
|
+
directory bundles (`<dir>/index.md`); other content can be flat files:
|
|
864
|
+
|
|
865
|
+
| Source | Output | URL |
|
|
866
|
+
| --- | --- | --- |
|
|
867
|
+
| `content/index.md` | `_site/index.html` | `/` |
|
|
868
|
+
| `content/about.md` | `_site/about/index.html` | `/about/` |
|
|
869
|
+
| `content/posts/index.md` | `_site/posts/index.html` | `/posts/` (canonical) or `/stories/` when `posts.slug: stories` is set |
|
|
870
|
+
| `content/posts/2026-04-15-hello/index.md` | `_site/posts/hello/index.html` | `/posts/hello/` (canonical) or `/stories/hello/` with slug aliasing |
|
|
871
|
+
| `content/posts/2026-04-15-hello/photo.jpg` | `_site/posts/hello/photo.jpg` | `/posts/hello/photo.jpg` |
|
|
872
|
+
|
|
873
|
+
Post directory names follow `YYYY-MM-DD-<slug>` (date prefix optional if you set `date:` in front matter). The slug is the directory name minus the date prefix. `/tags/<tag>/` indexes are generated automatically from each post's `tags:` front matter.
|
|
874
|
+
|
|
875
|
+
Layouts have access to two locals:
|
|
876
|
+
|
|
877
|
+
- `page` — the current `Abqari::Page` (frontmatter, content, url, title)
|
|
878
|
+
- `site` — the `Abqari::Site` (config and all pages)
|
|
879
|
+
|
|
880
|
+
```erb
|
|
881
|
+
<title><%= page.title %> · <%= site.config['title'] %></title>
|
|
882
|
+
<%= page.content %>
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
## Templating
|
|
886
|
+
|
|
887
|
+
### Partials
|
|
888
|
+
|
|
889
|
+
Partials live in `app/views/partials/` with a leading underscore on
|
|
890
|
+
the filename. Render them from a layout or another partial:
|
|
891
|
+
|
|
892
|
+
```erb
|
|
893
|
+
<%= render 'partials/nav' %>
|
|
894
|
+
<%= render 'partials/publication_card', pub: card_data, idx: 0, pubs_prefix: '/books/' %>
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
Locals passed to `render` are exposed as bare identifiers in the partial:
|
|
898
|
+
|
|
899
|
+
```erb
|
|
900
|
+
<!-- _publication_card.html.erb -->
|
|
901
|
+
<article>
|
|
902
|
+
<h2><%= pub[:title] %></h2>
|
|
903
|
+
</article>
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
`page` and `site` are always available; partials nest freely.
|
|
907
|
+
|
|
908
|
+
**Shipped partials** (rendered from default layouts, or callable from your own):
|
|
909
|
+
|
|
910
|
+
| Partial | What it does |
|
|
911
|
+
| --- | --- |
|
|
912
|
+
| `head` | Full `<head>` including OG, Twitter card, JSON-LD, feed autodiscovery, favicons, fonts.css |
|
|
913
|
+
| `nav` | Primary site nav (brand + links + theme toggle); driven by `data/nav.yml` and per-page `nav:` frontmatter |
|
|
914
|
+
| `footer` | Site footer (link row + meta + social icons + signature); driven by `data/footer.yml` and `footer.signature` config |
|
|
915
|
+
| `skip_link` | "Skip to content" a11y affordance |
|
|
916
|
+
| `theme_toggle` | Light/dark toggle button; gated by `theme_toggle` config |
|
|
917
|
+
| `post_list` | Card-list rendering of posts (used by `posts_index` and tag pages) |
|
|
918
|
+
| `pagination` | Prev / next links |
|
|
919
|
+
| `back_to_index` | Slug-alias-aware "back to <collection>" link for show views |
|
|
920
|
+
| `draft_banner`, `future_banner` | Amber banners on unpublished or future-dated pages |
|
|
921
|
+
| `next_item`, `related_items`, `book_related` | Show-view follow-up blocks |
|
|
922
|
+
| `next_in_series` | Forward-only "Next in the series" card (index-card treatment) |
|
|
923
|
+
| `series_pager` | Bidirectional prev/next series nav; the shape to use for ordered runs — see [Ordered series](#ordered-series-courses-multi-part-guides) |
|
|
924
|
+
| `testimonials` | Quote cards filtered by `data/testimonials.yml` and a book slug |
|
|
925
|
+
| `publication_card` | Single book rendered as a horizontal card; shared between publications index, bundle pages, series pages |
|
|
926
|
+
| `publication_header` | Cover + intro + buy buttons grid; used by the publication detail layout |
|
|
927
|
+
| `book_grid` | Card-grid of books on the home page (with optional heading) |
|
|
928
|
+
| `bundle_cta` | "Save with the bundle" callout on book pages |
|
|
929
|
+
| `book_jsonld` | `Book` schema JSON-LD for SEO |
|
|
930
|
+
| `contact_button` | Spam-resistant mailto button (split-half JS reassembly) |
|
|
931
|
+
| `newsletter_subscribe` | Provider-agnostic newsletter signup form |
|
|
932
|
+
| `carousel` | Slideshow component |
|
|
933
|
+
| `home/latest_post`, `home/latest_publication`, etc. | Home-page modules (latest post, random photo, contact, etc.) — opt-in via `home:` config |
|
|
934
|
+
|
|
935
|
+
### Helpers
|
|
936
|
+
|
|
937
|
+
A standard library of view helpers ships in `lib/abqari/helpers.rb`
|
|
938
|
+
and on `Abqari::RenderContext`, available everywhere ERB runs.
|
|
939
|
+
|
|
940
|
+
**Text & content**
|
|
941
|
+
|
|
942
|
+
| Helper | Example |
|
|
943
|
+
| --- | --- |
|
|
944
|
+
| `truncate(text, length:, omission:)` | `truncate(post.body, length: 200)` |
|
|
945
|
+
| `pluralize(count, singular, plural:)` | `pluralize(2, 'post')` → `"2 posts"` |
|
|
946
|
+
| `excerpt(text, words:)` | `excerpt(post.content, words: 30)` |
|
|
947
|
+
| `reading_time(text, wpm:)` | `reading_time(post.body)` → `"4 min read"` |
|
|
948
|
+
| `slugify(text)` | `slugify('Hello World!')` → `"hello-world"` |
|
|
949
|
+
|
|
950
|
+
**Time**
|
|
951
|
+
|
|
952
|
+
| Helper | Example |
|
|
953
|
+
| --- | --- |
|
|
954
|
+
| `local_time(time, format:)` | `local_time(post.date)` → `"15 April 2026"` (default), or pass `format: '%Y'` for year-only |
|
|
955
|
+
| `time_ago(time)` | `time_ago(post.date)` → `"3 days ago"` |
|
|
956
|
+
| `time_until(time)` | `time_until(event.date)` → `"in 2 weeks"` |
|
|
957
|
+
| `years_since(year)` | `years_since(2002)` → `"22"` (rolls over annually) |
|
|
958
|
+
|
|
959
|
+
**Links & URLs**
|
|
960
|
+
|
|
961
|
+
| Helper | Example |
|
|
962
|
+
| --- | --- |
|
|
963
|
+
| `link_to(text, url, **attrs)` | `link_to('Home', '/', class: 'nav-link')` |
|
|
964
|
+
| `taxonomy_url(name, term)` | `taxonomy_url('tags', 'ruby')` → `"/tags/ruby/"` — honors slug aliasing |
|
|
965
|
+
| `sanitized_url(url)` | strips `javascript:` and other unsafe schemes |
|
|
966
|
+
|
|
967
|
+
**Assets & icons**
|
|
968
|
+
|
|
969
|
+
| Helper | Example |
|
|
970
|
+
| --- | --- |
|
|
971
|
+
| `asset_path(name)` | `asset_path('css/app.css')` → fingerprinted URL |
|
|
972
|
+
| `optional_asset_path(name)` | same as `asset_path`, but returns `nil` instead of raising (useful for opt-in assets like `site.css`) |
|
|
973
|
+
| `picture_tag(src, alt:, sizes:, widths:)` | emits responsive `<picture>` markup |
|
|
974
|
+
| `icon(name, **attrs)` | `icon('github', width: 20)` — inline SVG |
|
|
975
|
+
| `heroicon(name, variant:)` | `heroicon('book-open', variant: :solid)` |
|
|
976
|
+
|
|
977
|
+
**Publications**
|
|
978
|
+
|
|
979
|
+
| Helper | Example |
|
|
980
|
+
| --- | --- |
|
|
981
|
+
| `publication_for(slug)` | look up a publication by slug |
|
|
982
|
+
| `bundle_for(slug)` | look up a bundle |
|
|
983
|
+
| `series_for(slug)` | look up a series |
|
|
984
|
+
| `buy_button(subject, label:)` | renders a buy button when the subject has `buy_now_url` |
|
|
985
|
+
| `cart_button(subject, label:)` | renders a cart button when the subject has `add_to_cart_url` |
|
|
986
|
+
| `publication_card_data(slug)` | normalised hash for the `_publication_card` partial |
|
|
987
|
+
|
|
988
|
+
**Page resolution**
|
|
989
|
+
|
|
990
|
+
| Helper | Example |
|
|
991
|
+
| --- | --- |
|
|
992
|
+
| `collection_index_url(page)` | "back to index" URL, slug-alias aware |
|
|
993
|
+
| `collection_index_label(page)` | corresponding label |
|
|
994
|
+
|
|
995
|
+
Helpers are plain Ruby — extend in your site by adding methods to
|
|
996
|
+
`Abqari::Helpers` or `Abqari::RenderContext` in your own override
|
|
997
|
+
file. (For consuming sites: drop a `lib/site_helpers.rb` and require
|
|
998
|
+
it from `bin/build` before `Abqari::Site.new`.)
|
|
999
|
+
|
|
1000
|
+
## Collections
|
|
1001
|
+
|
|
1002
|
+
Four content collections ship as built-in defaults:
|
|
1003
|
+
|
|
1004
|
+
| Collection | URL prefix | Layout | Use case |
|
|
1005
|
+
| --- | --- | --- | --- |
|
|
1006
|
+
| `posts` | `/posts/:slug/` | `post.html.erb` | Prose blog posts — dated, tagged, RSS/JSON feed |
|
|
1007
|
+
| `photos` | `/photos/:slug/` | `photo.html.erb` | Image-centric posts with location / camera / series metadata |
|
|
1008
|
+
| `publications` | `/publications/:slug/` | `publication.html.erb` | Books, atlases, anything with a cover and price; integrates with Folio |
|
|
1009
|
+
| `workshops` | `/workshops/:slug/` | `workshop.html.erb` | Training offerings: dates, location, format, instructors, registration link |
|
|
1010
|
+
|
|
1011
|
+
Two further built-ins — `bundles` and `series` — exist for Folio-sourced
|
|
1012
|
+
publication groupings rather than for files you author. They take no
|
|
1013
|
+
`content/` directory of their own; see
|
|
1014
|
+
[Folio integration](#folio-integration) below.
|
|
1015
|
+
|
|
1016
|
+
Each collection has sensible defaults; configure overrides in
|
|
1017
|
+
`config/site.yml`:
|
|
1018
|
+
|
|
1019
|
+
```yaml
|
|
1020
|
+
collections:
|
|
1021
|
+
posts:
|
|
1022
|
+
source: content/posts # source directory
|
|
1023
|
+
permalink: /posts/:slug/ # URL pattern
|
|
1024
|
+
layout: post # default layout
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
Permalink tokens: `:slug`, `:year`, `:month`, `:day`.
|
|
1028
|
+
|
|
1029
|
+
### Slug aliasing
|
|
1030
|
+
|
|
1031
|
+
To move posts from `/posts/<slug>/` to `/articles/<slug>/`, the
|
|
1032
|
+
simplest path is slug aliasing: set `slug: articles` on the `posts:`
|
|
1033
|
+
block. URL prefix, feed URL (`/articles/feed.xml`), and back-link
|
|
1034
|
+
labels all derive from it. The source directory always stays
|
|
1035
|
+
canonical (`content/posts/`); the layout stays canonical
|
|
1036
|
+
(`post.html.erb`). Override `permalink:`, `layout:`, or `source:`
|
|
1037
|
+
only when you need to depart from the slug-derived defaults.
|
|
1038
|
+
|
|
1039
|
+
```yaml
|
|
1040
|
+
collections:
|
|
1041
|
+
posts:
|
|
1042
|
+
slug: stories # → /stories/, /stories/feed.xml
|
|
1043
|
+
publications:
|
|
1044
|
+
slug: books # → /books/, /books/<slug>/
|
|
1045
|
+
```
|
|
1046
|
+
|
|
1047
|
+
### Custom collections
|
|
1048
|
+
|
|
1049
|
+
Add your own under the same `collections:` map. Set `bundle: false`
|
|
1050
|
+
for flat single-file collections; otherwise items are directory
|
|
1051
|
+
bundles with colocated assets:
|
|
1052
|
+
|
|
1053
|
+
```yaml
|
|
1054
|
+
collections:
|
|
1055
|
+
recipes:
|
|
1056
|
+
enabled: true
|
|
1057
|
+
source: content/recipes
|
|
1058
|
+
permalink: /recipes/:slug/
|
|
1059
|
+
layout: recipe # you supply layouts/recipe.html.erb
|
|
1060
|
+
bundle: true # content/recipes/<slug>/{index.md, photo.jpg}
|
|
1061
|
+
show: # show-page knobs grouped under show:
|
|
1062
|
+
related: # (mirrors index: for listing-page knobs)
|
|
1063
|
+
enabled: true
|
|
1064
|
+
match_by: tags
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
Custom collections work with the same per-collection `index:`,
|
|
1068
|
+
`show:` (which nests `next:`, `related:`, `toc:`, and `cover:`), and
|
|
1069
|
+
`feeds:` toggles as the built-ins.
|
|
1070
|
+
|
|
1071
|
+
### Disabling a built-in
|
|
1072
|
+
|
|
1073
|
+
Set `enabled: false` on any built-in collection your site doesn't
|
|
1074
|
+
use — its source dir is left alone on disk, but no pages render,
|
|
1075
|
+
no taxonomy entries get generated, and the collection's nav entry
|
|
1076
|
+
disappears.
|
|
1077
|
+
|
|
1078
|
+
```yaml
|
|
1079
|
+
collections:
|
|
1080
|
+
photos:
|
|
1081
|
+
enabled: false # site doesn't ship photos
|
|
1082
|
+
workshops:
|
|
1083
|
+
enabled: false # site doesn't run workshops
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
## Posts and bundles
|
|
1087
|
+
|
|
1088
|
+
Every post is a **directory under `content/posts/`** containing an `index.md` and any colocated assets. There's no flat-file alternative — single mode by design, fewer edge cases.
|
|
1089
|
+
|
|
1090
|
+
```
|
|
1091
|
+
content/posts/2026-04-15-hello-from-the-blog/
|
|
1092
|
+
├── index.md
|
|
1093
|
+
├── hero.jpg
|
|
1094
|
+
└── screenshot.png
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
Front matter on `index.md` sets title, description, tags, and optionally date:
|
|
1098
|
+
|
|
1099
|
+
```yaml
|
|
1100
|
+
---
|
|
1101
|
+
title: Hello from the blog
|
|
1102
|
+
description: A first post.
|
|
1103
|
+
tags: [introductions, ruby]
|
|
1104
|
+
---
|
|
1105
|
+
```
|
|
1106
|
+
|
|
1107
|
+
**The directory name is everything.** The slug is the directory name minus the `YYYY-MM-DD-` prefix (if present). Date comes from front matter (`date:`) or that same prefix.
|
|
1108
|
+
|
|
1109
|
+
**Bundle assets are auto-resolved.** Reference colocated files by their short relative name in markdown:
|
|
1110
|
+
|
|
1111
|
+
```markdown
|
|
1112
|
+

|
|
1113
|
+
|
|
1114
|
+
Read the [docs PDF](docs.pdf).
|
|
1115
|
+
```
|
|
1116
|
+
|
|
1117
|
+
At build time:
|
|
1118
|
+
- The asset is copied to `_site/posts/<slug>/hero.jpg`
|
|
1119
|
+
- The `<img src="hero.jpg">` in the rendered HTML is rewritten to `<img src="/posts/<slug>/hero.jpg">`
|
|
1120
|
+
- Absolute URLs (`/assets/...`), external URLs (`https://...`), and anchor links (`#section`) are left untouched
|
|
1121
|
+
|
|
1122
|
+
**Why bundles?**
|
|
1123
|
+
|
|
1124
|
+
- Co-locate post and its assets — delete the directory, delete the post and everything it owned
|
|
1125
|
+
- No filename collisions: every post has its own asset namespace
|
|
1126
|
+
- Editor-friendly: working on a post and its images in one folder
|
|
1127
|
+
- One model, no "which way do I do this?"
|
|
1128
|
+
|
|
1129
|
+
**Templates use `site.posts`** (sorted newest first):
|
|
1130
|
+
|
|
1131
|
+
```erb
|
|
1132
|
+
<% site.posts.each do |post| %>
|
|
1133
|
+
<a href="<%= post.url %>"><%= post.title %></a>
|
|
1134
|
+
<time><%= local_time(post.date) %></time>
|
|
1135
|
+
<% end %>
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
Posts use `app/views/layouts/post.html.erb` by default — override per page with `layout:` in front matter. For each unique tag across all posts, Abqari generates a `/tags/<slug>/` index page using `app/views/layouts/tag.html.erb`.
|
|
1139
|
+
|
|
1140
|
+
A landing page that lists every post is a Markdown file with `layout: posts_index`:
|
|
1141
|
+
|
|
1142
|
+
```yaml
|
|
1143
|
+
---
|
|
1144
|
+
title: Posts
|
|
1145
|
+
layout: posts_index
|
|
1146
|
+
---
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### Drafts and scheduling
|
|
1150
|
+
|
|
1151
|
+
Add `published: false` to a post's front matter to mark it as a draft. Drafts:
|
|
1152
|
+
|
|
1153
|
+
- Are **excluded from production builds** entirely (no HTML, no feed entry, no sitemap entry).
|
|
1154
|
+
- **Render in development** (`bin/serve`) with a visible amber **Draft** banner at the top of the post, so you can preview them as you write — and you can't accidentally forget a draft is unpublished.
|
|
1155
|
+
- Are determined **only by `published: false`** — don't tag them with a `drafts` tag. Use real subject tags as you would for any post; tag taxonomies stay clean.
|
|
1156
|
+
|
|
1157
|
+
```yaml
|
|
1158
|
+
---
|
|
1159
|
+
title: Work in progress
|
|
1160
|
+
tags: [ruby, static-sites] # tag normally — `published: false` is what makes it a draft
|
|
1161
|
+
published: false
|
|
1162
|
+
---
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
When you're ready to publish, change `published: false` to `published: true` (or remove the line — published is the default). The banner disappears, the post starts appearing on `/posts/` (or your aliased URL prefix if you've set `slug:` on the collection), in feeds, and in the sitemap.
|
|
1166
|
+
|
|
1167
|
+
**Hide drafts in dev too.** If you'd rather see what production will look like during preview — e.g. right before deploying — set `drafts.show_in_development: false` in `config/site.yml`:
|
|
1168
|
+
|
|
1169
|
+
```yaml
|
|
1170
|
+
drafts:
|
|
1171
|
+
show_in_development: false
|
|
1172
|
+
```
|
|
1173
|
+
|
|
1174
|
+
**Force-include drafts in a production build.** Useful for catching issues that only surface under real CSP, minification, or asset fingerprinting:
|
|
1175
|
+
|
|
1176
|
+
```bash
|
|
1177
|
+
ABQARI_INCLUDE_DRAFTS=true bin/build
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
### Disabling a page (parking content without deleting it)
|
|
1181
|
+
|
|
1182
|
+
`enabled: false` in a page's frontmatter is the page-level kill switch. The file stays in `content/` but the page produces **no HTML output, no nav entry, no sitemap entry, no feed entry, no taxonomy contribution** — in any environment.
|
|
1183
|
+
|
|
1184
|
+
```yaml
|
|
1185
|
+
---
|
|
1186
|
+
title: About
|
|
1187
|
+
enabled: false # parked: keep the file, ship nothing
|
|
1188
|
+
---
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
This is different from drafts:
|
|
1192
|
+
|
|
1193
|
+
| Flag | Production | Development | Use case |
|
|
1194
|
+
| --- | --- | --- | --- |
|
|
1195
|
+
| `published: false` | hidden | rendered with **Draft** banner | in-progress writing you're previewing |
|
|
1196
|
+
| `enabled: false` | hidden | hidden | parked content (kept in repo, not shipped) |
|
|
1197
|
+
|
|
1198
|
+
Flip back to `enabled: true` (or remove the line — enabled is the default) to ship.
|
|
1199
|
+
|
|
1200
|
+
### Scheduled (future-dated) posts
|
|
1201
|
+
|
|
1202
|
+
Posts with a date later than "now" — set in `date:` front matter or the `YYYY-MM-DD-` prefix on the filename — are scheduled posts. Like drafts, they:
|
|
1203
|
+
|
|
1204
|
+
- Are **excluded from production builds** until time catches up.
|
|
1205
|
+
- **Render in development** with a visible blue **Scheduled** banner showing the exact moment they'll publish (date + timezone) and how far away that is.
|
|
1206
|
+
- Get **real subject tags** like any other post (`ruby`, `releases`) — scheduled state is determined by date, not by a `scheduled` tag.
|
|
1207
|
+
|
|
1208
|
+
A post can be both a draft and scheduled — both banners stack.
|
|
1209
|
+
|
|
1210
|
+
**Important: static sites only update on rebuild.** A post scheduled for June 15 won't actually appear until the next build runs after June 15. If you want true scheduled publishing, set up a daily cron / GitHub Actions schedule that runs `bin/build` + `bin/deploy`.
|
|
1211
|
+
|
|
1212
|
+
**Force-include future posts in a production build:**
|
|
1213
|
+
|
|
1214
|
+
```bash
|
|
1215
|
+
ABQARI_INCLUDE_FUTURE=true bin/build
|
|
1216
|
+
```
|
|
1217
|
+
|
|
1218
|
+
### Timezone
|
|
1219
|
+
|
|
1220
|
+
Naked dates (`date: 2026-06-15`) are interpreted in the timezone configured in `config/site.yml`:
|
|
1221
|
+
|
|
1222
|
+
```yaml
|
|
1223
|
+
timezone: UTC # default
|
|
1224
|
+
# timezone: Asia/Singapore
|
|
1225
|
+
# timezone: America/Los_Angeles
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
The site sets `ENV['TZ']` from this config at boot, so a date string parses to the same wall-clock moment whether the build runs on your laptop, a UTC CI runner, or anywhere else. Without this, naked dates would mean different moments on different machines.
|
|
1229
|
+
|
|
1230
|
+
Dates with explicit offsets (`2026-06-15T08:00:00+08:00`) keep their offset and ignore this setting.
|
|
1231
|
+
|
|
1232
|
+
The scheduled-post banner always shows the resolved moment with its zone, so you can tell at a glance how Abqari interpreted your date string.
|
|
1233
|
+
|
|
1234
|
+
### Custom taxonomies
|
|
1235
|
+
|
|
1236
|
+
`tags` is the default taxonomy and works out of the box. Add more in `site.yml`:
|
|
1237
|
+
|
|
1238
|
+
```yaml
|
|
1239
|
+
taxonomies:
|
|
1240
|
+
tags:
|
|
1241
|
+
permalink: /tags/:slug/
|
|
1242
|
+
layout: tag
|
|
1243
|
+
categories:
|
|
1244
|
+
permalink: /categories/:slug/
|
|
1245
|
+
layout: tag
|
|
1246
|
+
series:
|
|
1247
|
+
permalink: /series/:slug/
|
|
1248
|
+
layout: tag
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
Posts opt into a taxonomy by setting it in front matter:
|
|
1252
|
+
|
|
1253
|
+
```yaml
|
|
1254
|
+
---
|
|
1255
|
+
title: A guide
|
|
1256
|
+
tags: [ruby, ssg]
|
|
1257
|
+
categories: [tutorials]
|
|
1258
|
+
series: [getting-started]
|
|
1259
|
+
---
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
For each unique term across all posts in a taxonomy, Abqari generates an index page at the configured permalink. Layouts use `page.taxonomy`, `page.term`, and `page.posts`.
|
|
1263
|
+
|
|
1264
|
+
### Ordered series (courses, multi-part guides)
|
|
1265
|
+
|
|
1266
|
+
A taxonomy groups pages; `series_position:` orders them. Together they cover the "module of ordered lessons" shape without any extra machinery — a lesson is a page, a module is a series.
|
|
1267
|
+
|
|
1268
|
+
```yaml
|
|
1269
|
+
# config/site.yml
|
|
1270
|
+
collections:
|
|
1271
|
+
lessons:
|
|
1272
|
+
enabled: true
|
|
1273
|
+
source: content/lessons
|
|
1274
|
+
permalink: /lessons/:slug/
|
|
1275
|
+
layout: lesson
|
|
1276
|
+
bundle: true # directory per lesson, so audio/images colocate
|
|
1277
|
+
|
|
1278
|
+
taxonomies:
|
|
1279
|
+
lesson_modules:
|
|
1280
|
+
field: series # read `series:` from frontmatter
|
|
1281
|
+
collection: lessons # scope to lessons — a post in "Module 1" is a different run
|
|
1282
|
+
permalink: /lessons/module/:slug/
|
|
1283
|
+
layout: series_index
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
```yaml
|
|
1287
|
+
---
|
|
1288
|
+
layout: lesson
|
|
1289
|
+
title: Greetings and introductions
|
|
1290
|
+
series: Module 1 — First conversations
|
|
1291
|
+
series_position: 3
|
|
1292
|
+
---
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
Series are scoped **per collection**, matching the `<collection>_series` taxonomies: a lesson and a post that happen to share a `series:` value are independent runs, not one merged series.
|
|
1296
|
+
|
|
1297
|
+
Navigation comes from two `site` methods and a partial:
|
|
1298
|
+
|
|
1299
|
+
| | |
|
|
1300
|
+
| --- | --- |
|
|
1301
|
+
| `site.next_in_series(page)` | Page with the smallest position above this one, or nil |
|
|
1302
|
+
| `site.prev_in_series(page)` | Page with the largest position below this one, or nil |
|
|
1303
|
+
| `render 'partials/series_pager'` | Renders both as a compact prev/next nav |
|
|
1304
|
+
|
|
1305
|
+
```erb
|
|
1306
|
+
<%# in your lesson layout %>
|
|
1307
|
+
<%== render 'partials/series_pager' %>
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
`series_pager` renders nothing outside a series, and drops one side at either end without letting the other change column. For a forward-only card in the index-row style, use `next_in_series` instead — that's what the built-in `post` layout does.
|
|
1311
|
+
|
|
1312
|
+
`series_url(term)` resolves the series index URL for the *current page's* collection, so a layout shared between collections links each one to its own index. It returns nil when the collection has no series taxonomy configured — render the name as plain text in that case.
|
|
1313
|
+
|
|
1314
|
+
**`series_position` must be an unquoted Integer.** `series_position: "3"` is a String, and a String position is invisible to `prev_in_series` / `next_in_series` — the page still builds and still appears on its module index, it just silently drops out of the prev/next chain. Duplicate positions fail the same way: the comparison is strict, so two pages at position 3 can't see each other and the chain skips one. `bin/audit` reports both under **Content**, along with pages in a series that have no position at all.
|
|
1315
|
+
|
|
1316
|
+
## Data files
|
|
1317
|
+
|
|
1318
|
+
Drop YAML or JSON files in `data/` to expose structured data to templates without putting it in `site.config`. Useful for navigation menus, author bios, footer links, or anything that's not page content but isn't site-level config either.
|
|
1319
|
+
|
|
1320
|
+
```yaml
|
|
1321
|
+
# data/nav.yml
|
|
1322
|
+
links:
|
|
1323
|
+
- label: Home
|
|
1324
|
+
url: /
|
|
1325
|
+
- label: Stories
|
|
1326
|
+
url: /stories/
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
Access in any template via `site.data`:
|
|
1330
|
+
|
|
1331
|
+
```erb
|
|
1332
|
+
<% site.data['nav']['links'].each do |link| %>
|
|
1333
|
+
<%= link_to link['label'], link['url'] %>
|
|
1334
|
+
<% end %>
|
|
1335
|
+
```
|
|
1336
|
+
|
|
1337
|
+
The filename (minus extension) is the key. `data/foo/bar.yml` flattens to `site.data['foo_bar']`. Both `.yml`/`.yaml` and `.json` work.
|
|
1338
|
+
|
|
1339
|
+
## Navigation and footer
|
|
1340
|
+
|
|
1341
|
+
Both `<nav>` and `<footer>` are auto-generated from a **three-layer cascade** so most users never write nav code, but anyone who wants full control can take it.
|
|
1342
|
+
|
|
1343
|
+
### How the cascade resolves
|
|
1344
|
+
|
|
1345
|
+
For each of nav and footer, `Site#nav_links` / `Site#footer_links` walks layers in order and stops at the first one that has content:
|
|
1346
|
+
|
|
1347
|
+
1. **`data/<key>.yml` `links:` array** — explicit, used as-is.
|
|
1348
|
+
2. **Frontmatter opt-in** — pages with `nav: { order: N }` (or `footer: { order: N }`) join automatically, sorted by `order`.
|
|
1349
|
+
3. **Empty** — the partial falls back to a sensible default (nav: a single home link; footer: just the meta line).
|
|
1350
|
+
|
|
1351
|
+
### Frontmatter opt-in (the easy path)
|
|
1352
|
+
|
|
1353
|
+
Drop a `nav:` or `footer:` block into any page's front matter:
|
|
1354
|
+
|
|
1355
|
+
```yaml
|
|
1356
|
+
---
|
|
1357
|
+
title: About
|
|
1358
|
+
nav:
|
|
1359
|
+
order: 2 # lower numbers come first
|
|
1360
|
+
label: About me # optional; defaults to the page title
|
|
1361
|
+
footer:
|
|
1362
|
+
order: 1
|
|
1363
|
+
label: Imprint
|
|
1364
|
+
---
|
|
1365
|
+
```
|
|
1366
|
+
|
|
1367
|
+
The page automatically appears in the nav and the footer. To remove it, delete the block. To reorder, change the number.
|
|
1368
|
+
|
|
1369
|
+
The shipped [`content/about.md`](https://github.com/grantrayner/abqari/blob/main/lib/abqari/templates/site_skeleton/content/about.md.tt) is a working example: it joins the nav at order 3 and includes a "Subscribe" section with RSS and JSON Feed links — so RSS discovery isn't hidden behind `<link rel="alternate">` alone. Edit the body to make it yours, or delete the file if you don't want an about page.
|
|
1370
|
+
|
|
1371
|
+
### Explicit override (the power-user path)
|
|
1372
|
+
|
|
1373
|
+
When you need external links, multi-section nav, or a hard-coded order, drop a `links:` array into `data/nav.yml` (or `data/footer.yml`). It overrides the frontmatter cascade entirely:
|
|
1374
|
+
|
|
1375
|
+
```yaml
|
|
1376
|
+
# data/nav.yml
|
|
1377
|
+
links:
|
|
1378
|
+
- label: Home
|
|
1379
|
+
url: /
|
|
1380
|
+
- label: Stories
|
|
1381
|
+
url: /stories/
|
|
1382
|
+
- label: GitHub
|
|
1383
|
+
url: https://github.com/your-handle
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
### Social icons in the footer
|
|
1387
|
+
|
|
1388
|
+
Add a `social:` block to `config/site.yml`. Each key must match an SVG file in `app/icons/<key>.svg`. Out of the box: `github`, `rss`, `mail`, `x`, `mastodon`, `bluesky`, `linkedin`. Drop your own SVGs into `app/icons/` to support more services.
|
|
1389
|
+
|
|
1390
|
+
```yaml
|
|
1391
|
+
# config/site.yml
|
|
1392
|
+
social:
|
|
1393
|
+
github: https://github.com/your-handle
|
|
1394
|
+
mastodon: https://mastodon.social/@your-handle
|
|
1395
|
+
rss: /feed.xml
|
|
1396
|
+
mail: mailto:you@example.com
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
Renders as a row of inline SVG icons, sized 20×20, with `aria-label` and `rel="me"` attributes — privacy-friendly (no third-party scripts) and accessible by default.
|
|
1400
|
+
|
|
1401
|
+
### Customising the layout
|
|
1402
|
+
|
|
1403
|
+
`_nav.html.erb` and `_footer.html.erb` are yours — change the markup, add a logo, split into multi-column footer sections. The helpers (`site.nav_links`, `site.footer_links`, `site.social_links`) just give you the data; the templates control the layout.
|
|
1404
|
+
|
|
1405
|
+
## Pagination
|
|
1406
|
+
|
|
1407
|
+
Pagination is opt-in per page via front matter, with a site-wide default page size in `config/site.yml`. Pagination links only render when there's actually more than one page — a 4-post blog with the default `per_page: 10` shows no pagination at all.
|
|
1408
|
+
|
|
1409
|
+
| Front matter | Behaviour |
|
|
1410
|
+
| --- | --- |
|
|
1411
|
+
| `paginate: true` | Use the site-wide default (`pagination.per_page`, default 10) |
|
|
1412
|
+
| `paginate: 25` | Override — 25 posts per page on this page only |
|
|
1413
|
+
| `paginate: false` (or absent) | No pagination |
|
|
1414
|
+
|
|
1415
|
+
```yaml
|
|
1416
|
+
# content/posts/index.md
|
|
1417
|
+
---
|
|
1418
|
+
title: Posts
|
|
1419
|
+
layout: posts_index
|
|
1420
|
+
paginate: true # uses config default
|
|
1421
|
+
---
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
To change the site-wide default:
|
|
1425
|
+
|
|
1426
|
+
```yaml
|
|
1427
|
+
# config/site.yml
|
|
1428
|
+
pagination:
|
|
1429
|
+
per_page: 20
|
|
1430
|
+
```
|
|
1431
|
+
|
|
1432
|
+
The source page becomes `/posts/` (or `/stories/` if you set `posts.slug: stories`), with overflow at `/posts/page/2/`, `/posts/page/3/`, etc. In templates, `page.paginator` is non-nil when pagination is active and exposes:
|
|
1433
|
+
|
|
1434
|
+
- `paginator.posts` — this page's slice
|
|
1435
|
+
- `paginator.page` / `paginator.total_pages`
|
|
1436
|
+
- `paginator.first?` / `paginator.last?`
|
|
1437
|
+
- `paginator.next_url` / `paginator.prev_url`
|
|
1438
|
+
|
|
1439
|
+
The shipped `_pagination.html.erb` partial wraps this:
|
|
1440
|
+
|
|
1441
|
+
```erb
|
|
1442
|
+
<%= render 'partials/post_list', posts: page.paginator&.posts || site.posts %>
|
|
1443
|
+
<%= render 'partials/pagination', paginator: page.paginator %>
|
|
1444
|
+
```
|
|
1445
|
+
|
|
1446
|
+
## Assets
|
|
1447
|
+
|
|
1448
|
+
Anything under `app/assets/` is **fingerprinted** at build time — content-hashed filename, copied to `_site/assets/`, registered with the asset pipeline. Reference assets via `asset_path`:
|
|
1449
|
+
|
|
1450
|
+
```erb
|
|
1451
|
+
<link rel="stylesheet" href="<%= asset_path('css/app.css') %>">
|
|
1452
|
+
```
|
|
1453
|
+
|
|
1454
|
+
`app/assets/css/app.css` → `_site/assets/css/app.4aab61fc9e.css` and the link tag receives the hashed URL. When the file content changes, the hash changes, and caches invalidate automatically — safe to serve with `Cache-Control: max-age=31536000, immutable` from your host.
|
|
1455
|
+
|
|
1456
|
+
For files that should be served verbatim with no fingerprinting (favicon, robots.txt, downloadable PDFs, social images referenced from external sites), use `public/` — it's copied to `_site/` as-is.
|
|
1457
|
+
|
|
1458
|
+
## Syntax highlighting
|
|
1459
|
+
|
|
1460
|
+
Code fences in Markdown get syntax-highlighted at build time via [Rouge](https://github.com/rouge-ruby/rouge) (pure Ruby, no native deps). The build emits `/_site/assets/css/syntax.css` from a Rouge theme, and the head partial includes it automatically.
|
|
1461
|
+
|
|
1462
|
+
````markdown
|
|
1463
|
+
```ruby
|
|
1464
|
+
def shouty(text)
|
|
1465
|
+
text.upcase + '!'
|
|
1466
|
+
end
|
|
1467
|
+
```
|
|
1468
|
+
````
|
|
1469
|
+
|
|
1470
|
+
…renders with proper `<span class="k">def</span>` token classes scoped under `.highlight`. Choose a Rouge theme in `site.yml`:
|
|
1471
|
+
|
|
1472
|
+
```yaml
|
|
1473
|
+
syntax_theme: github # default — see Rouge's themes for options
|
|
1474
|
+
syntax_highlighting: false # to disable entirely
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
Rouge supports 200+ languages. Code blocks without a language fence (just triple backticks) render as plain monospace.
|
|
1478
|
+
|
|
1479
|
+
## Search
|
|
1480
|
+
|
|
1481
|
+
Static client-side search via [Pagefind](https://pagefind.app/) — a single Rust binary that walks `_site/` after build, generates a chunked search index at `_site/pagefind/`, and ships a tiny JS UI that runs entirely in the visitor's browser. No server, no API, no third-party tracking, free.
|
|
1482
|
+
|
|
1483
|
+
**Enable in `config/site.yml`:**
|
|
1484
|
+
|
|
1485
|
+
```yaml
|
|
1486
|
+
search: true
|
|
1487
|
+
```
|
|
1488
|
+
|
|
1489
|
+
**Install Pagefind once** (it's not a Ruby gem):
|
|
1490
|
+
|
|
1491
|
+
```bash
|
|
1492
|
+
brew install pagefind # macOS
|
|
1493
|
+
curl -LSf https://pagefind.app/install.sh | bash # general
|
|
1494
|
+
cargo install pagefind # via Rust
|
|
1495
|
+
```
|
|
1496
|
+
|
|
1497
|
+
**Render the search box** anywhere in your templates (use the shipped partial):
|
|
1498
|
+
|
|
1499
|
+
```erb
|
|
1500
|
+
<%= render 'partials/search' %>
|
|
1501
|
+
```
|
|
1502
|
+
|
|
1503
|
+
The partial loads `/pagefind/pagefind-ui.{css,js}` and inits the UI inside `#abqari-search`. Both files are served from your own origin, so the strict CSP doesn't need relaxing.
|
|
1504
|
+
|
|
1505
|
+
When `search: true`, `bin/build` automatically runs `pagefind` after rendering. To re-index without rebuilding, run `bin/index` directly. Without Pagefind installed, the build prints a helpful warning and continues — no broken builds for users who haven't installed it yet.
|
|
1506
|
+
|
|
1507
|
+
## Themes
|
|
1508
|
+
|
|
1509
|
+
Visual styling lives in `themes/<name>/`. Pick one in `config/site.yml`:
|
|
1510
|
+
|
|
1511
|
+
```yaml
|
|
1512
|
+
theme: minimal
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
Five hand-tuned themes ship by default, plus a `custom` starter
|
|
1516
|
+
template and `none` for bring-your-own CSS:
|
|
1517
|
+
|
|
1518
|
+
| Theme | Description |
|
|
1519
|
+
| --- | --- |
|
|
1520
|
+
| `minimal` | System fonts, near-white page, blue links. The default. |
|
|
1521
|
+
| `magazine` | Serif body (Charter), display-serif headings (Playfair Display), drop caps, warm cream palette, warm-rust accent |
|
|
1522
|
+
| `notebook` | Mono-typeface headings (JetBrains Mono), paper + ochre palette, dense body type, dashed section dividers — docs-style aesthetic |
|
|
1523
|
+
| `sand` | Warm earth-toned palette (Merriweather body + Merriweather Sans headings, brand-red accents); hand-rolled CSS, no framework dependency |
|
|
1524
|
+
| `bootstrap` | Bootstrap 5.3.8 vendored (~232 KB) — use when you want Bootstrap's component library |
|
|
1525
|
+
| `custom` | Annotated starter template — `abqari new --theme custom` copies it into your site's `themes/custom/`, ready to edit |
|
|
1526
|
+
| `none` | No theme CSS at all — the scaffold gives you an empty `app/assets/css/app.css` to write into |
|
|
1527
|
+
|
|
1528
|
+
Theme files live in `themes/<name>/css/app.css` (in the gem for
|
|
1529
|
+
Path A consumers; in your repo for Path B). At build time the active
|
|
1530
|
+
theme's CSS ships inside the single fingerprinted bundle
|
|
1531
|
+
(`/assets/css/bundle.<hash>.css`) along with `_common.css`, the
|
|
1532
|
+
syntax stylesheet, and your `site.css`.
|
|
1533
|
+
|
|
1534
|
+
### Building your own theme
|
|
1535
|
+
|
|
1536
|
+
The full guide — including the complete token reference — is in
|
|
1537
|
+
[docs/theming.md](https://github.com/grantrayner/abqari/blob/main/docs/theming.md). The short version: copy a theme
|
|
1538
|
+
into your site with
|
|
1539
|
+
|
|
1540
|
+
```bash
|
|
1541
|
+
abqari theme eject custom
|
|
1542
|
+
```
|
|
1543
|
+
|
|
1544
|
+
(or `eject minimal --as mytheme` to fork an existing look under your
|
|
1545
|
+
own name — any lowercase name works, no registration needed). Your
|
|
1546
|
+
site-local copy shadows the gem's automatically, and edits hot-reload
|
|
1547
|
+
under `bin/serve`.
|
|
1548
|
+
|
|
1549
|
+
### Architecture: tokens, not rule overrides
|
|
1550
|
+
|
|
1551
|
+
Themes are designed to be **mostly variable declarations**. Each
|
|
1552
|
+
theme's `app.css` is ~150 lines, of which ~120 are `:root` + dark
|
|
1553
|
+
deltas (palette tokens). The structural CSS — layout, components,
|
|
1554
|
+
typography hierarchy — lives in `_common.css` (in the engine,
|
|
1555
|
+
loaded ahead of the theme) and reads its values from CSS variables.
|
|
1556
|
+
|
|
1557
|
+
Themes typically override:
|
|
1558
|
+
|
|
1559
|
+
| Token | Role | Example |
|
|
1560
|
+
| --- | --- | --- |
|
|
1561
|
+
| `--color-text`, `--color-bg`, `--color-link`, `--color-primary`, … | Palette | `#212529` near-black text on `#f3f0ee` warm beige (sand) |
|
|
1562
|
+
| `--font-body`, `--font-heading`, `--font-mono`, `--font-sans`, `--font-meta` | Typography | `"Merriweather", Georgia, serif` for body in sand |
|
|
1563
|
+
| `--font-size-body`, `--line-height-body` | Body density | `1rem` / `1.55` in notebook (denser docs feel) |
|
|
1564
|
+
| `--shadow-card`, `--shadow-soft`, `--shadow-cover` | Shadow tones | Warm rgba tints in sand vs cool grey in minimal |
|
|
1565
|
+
|
|
1566
|
+
Plus a small refinements section per theme — decorative things that
|
|
1567
|
+
can't be expressed as a token (drop caps in magazine, `[bracket]`
|
|
1568
|
+
notation on banners in notebook, etc.).
|
|
1569
|
+
|
|
1570
|
+
For consuming sites, this means **theme-tuning is one config line**:
|
|
1571
|
+
`theme: sand` and you get the full sand palette + typography. No
|
|
1572
|
+
need to vendor CSS.
|
|
1573
|
+
|
|
1574
|
+
### Light/dark toggle
|
|
1575
|
+
|
|
1576
|
+
Each theme has a built-in dark mode that responds to
|
|
1577
|
+
`prefers-color-scheme` **and** a manual user toggle. The toggle is
|
|
1578
|
+
rendered in the default nav; opt out with
|
|
1579
|
+
`theme_toggle: false` in `config/site.yml`.
|
|
1580
|
+
|
|
1581
|
+
How it works:
|
|
1582
|
+
- A tiny inline `<script>` in `<head>` reads `localStorage['theme']` and sets `<html data-theme="dark">` (or `light`) **before** render — no flash of the wrong theme.
|
|
1583
|
+
- The strict CSP is preserved by pinning the inline script's SHA256 hash in `_headers`. No `'unsafe-inline'`.
|
|
1584
|
+
- Each theme's CSS responds to `[data-theme="dark"]` / `[data-theme="light"]` overrides; without an attribute, `prefers-color-scheme` still wins.
|
|
1585
|
+
- Clicking the toggle button flips `data-theme` and persists in localStorage via `app/assets/js/theme-toggle.js` (deferred, no FOUC risk).
|
|
1586
|
+
|
|
1587
|
+
The button shows a moon when light is active (click → switch to dark)
|
|
1588
|
+
and a sun when dark is active. Customize the icons in
|
|
1589
|
+
`app/icons/sun.svg` and `app/icons/moon.svg`; restyle the button via
|
|
1590
|
+
`.theme-toggle` in your site's CSS.
|
|
1591
|
+
|
|
1592
|
+
## Overrides
|
|
1593
|
+
|
|
1594
|
+
Your site can replace any engine view, asset, or theme file by
|
|
1595
|
+
shipping its own copy at the same relative path. Resolution order —
|
|
1596
|
+
later wins on conflict:
|
|
1597
|
+
|
|
1598
|
+
1. **Engine theme** — `themes/<name>/` inside the gem
|
|
1599
|
+
2. **Site theme** — `themes/<name>/` in your repo (same name)
|
|
1600
|
+
3. **Engine assets** — the gem's `app/assets/`
|
|
1601
|
+
4. **Site assets** — your repo's `app/assets/`
|
|
1602
|
+
|
|
1603
|
+
So `themes/minimal/css/app.css` in your site shadows the gem's minimal
|
|
1604
|
+
theme file-for-file (that's what `abqari theme eject` sets up), and
|
|
1605
|
+
`app/assets/css/site.css` layers on top of whatever theme is active.
|
|
1606
|
+
Views resolve the same way: `app/views/partials/_footer.html.erb` in
|
|
1607
|
+
your site replaces the engine's footer, and `app/icons/<name>.svg`
|
|
1608
|
+
replaces or adds icons.
|
|
1609
|
+
|
|
1610
|
+
Theme names don't need to exist in the engine at all — `theme: mytheme`
|
|
1611
|
+
plus `themes/mytheme/css/app.css` in your site is a complete custom
|
|
1612
|
+
theme. To read the engine's copies of anything (Path A), open the
|
|
1613
|
+
installed gem: `bundle show abqari`.
|
|
1614
|
+
|
|
1615
|
+
## Fonts
|
|
1616
|
+
|
|
1617
|
+
Self-host Google Fonts to keep your site privacy-friendly (no requests to `fonts.googleapis.com` from your visitors' browsers). Configure in `site.yml`:
|
|
1618
|
+
|
|
1619
|
+
```yaml
|
|
1620
|
+
fonts:
|
|
1621
|
+
- family: Inter
|
|
1622
|
+
weights: [400, 600]
|
|
1623
|
+
style: normal
|
|
1624
|
+
```
|
|
1625
|
+
|
|
1626
|
+
Then run once:
|
|
1627
|
+
|
|
1628
|
+
```bash
|
|
1629
|
+
bin/fetch-fonts
|
|
1630
|
+
```
|
|
1631
|
+
|
|
1632
|
+
Abqari downloads the woff2 files to `vendor/fonts/`, generates a `fonts.css` with rewritten URLs, and serves everything from `/assets/fonts/` at build time. The head partial conditionally includes the link tag whenever `fonts:` is configured.
|
|
1633
|
+
|
|
1634
|
+
## Icons
|
|
1635
|
+
|
|
1636
|
+
Inline SVGs live in `app/icons/<name>.svg`. Render from any template:
|
|
1637
|
+
|
|
1638
|
+
```erb
|
|
1639
|
+
<%= icon 'github' %>
|
|
1640
|
+
<%= icon 'rss', class: 'social', width: '20', height: '20' %>
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
Attribute hash gets merged into the SVG tag. SVGs use
|
|
1644
|
+
`fill="currentColor"` so they inherit the surrounding text colour.
|
|
1645
|
+
|
|
1646
|
+
Nine icons ship by default: `github`, `rss`, `mail`, `x`, `mastodon`,
|
|
1647
|
+
`bluesky`, `linkedin`, `sun`, `moon`. Add your own by dropping
|
|
1648
|
+
`.svg` files into `app/icons/` (in your site overrides the gem's
|
|
1649
|
+
version — or shadows it if the name is new).
|
|
1650
|
+
|
|
1651
|
+
For broader needs, `heroicon` renders any [Heroicons](https://heroicons.com)
|
|
1652
|
+
SVG from the vendored set:
|
|
1653
|
+
|
|
1654
|
+
```erb
|
|
1655
|
+
<%= heroicon 'book-open' %>
|
|
1656
|
+
<%= heroicon 'arrow-right', variant: :mini %>
|
|
1657
|
+
```
|
|
1658
|
+
|
|
1659
|
+
Variants: `:outline` (default, 24×24 stroke), `:solid` (24×24 fill),
|
|
1660
|
+
`:mini` (20×20 fill), `:micro` (16×16 fill).
|
|
1661
|
+
|
|
1662
|
+
## Images
|
|
1663
|
+
|
|
1664
|
+
**On by default.** Every raster image the engine knows about — `app/assets/images/`, post bundle images (`content/posts/<slug>/hero.jpg`), photo collection items, flat-collection assets — gets processed into a `<picture>` block with AVIF + WebP + an optimised original-format fallback, across multiple widths.
|
|
1665
|
+
|
|
1666
|
+
Reference an image in markdown the normal way:
|
|
1667
|
+
|
|
1668
|
+
```markdown
|
|
1669
|
+

|
|
1670
|
+
```
|
|
1671
|
+
|
|
1672
|
+
…and the build's post-process pass rewrites it to:
|
|
1673
|
+
|
|
1674
|
+
```html
|
|
1675
|
+
<picture>
|
|
1676
|
+
<source type="image/avif" srcset="/posts/foo/hero-400.abc.avif 400w, /posts/foo/hero-800.def.avif 800w, /posts/foo/hero-1200.ghi.avif 1200w" sizes="(min-width: 768px) 50vw, 100vw">
|
|
1677
|
+
<source type="image/webp" srcset="..." sizes="...">
|
|
1678
|
+
<img src="/posts/foo/hero-1200.xyz.jpg" alt="Hero" width="1200" height="800" loading="lazy" decoding="async">
|
|
1679
|
+
</picture>
|
|
1680
|
+
```
|
|
1681
|
+
|
|
1682
|
+
### Defaults
|
|
1683
|
+
|
|
1684
|
+
```yaml
|
|
1685
|
+
images:
|
|
1686
|
+
optimise: true # turn the pipeline on (default)
|
|
1687
|
+
processor: auto # auto | vips-native | vips | imagemagick
|
|
1688
|
+
formats: [avif, webp] # AVIF first — browser picks first decodable
|
|
1689
|
+
widths: [400, 800, 1200]
|
|
1690
|
+
quality:
|
|
1691
|
+
avif: 50 # perceptually ~= JPEG 85, ~40% smaller
|
|
1692
|
+
webp: 80 # ~30% smaller than JPEG at the same quality
|
|
1693
|
+
original: 85 # the JPEG/PNG fallback
|
|
1694
|
+
sizes: "(min-width: 768px) 50vw, 100vw"
|
|
1695
|
+
source_dir: app/assets/images # site-wide imagery (not bundle images)
|
|
1696
|
+
preserve_metadata: false # strip EXIF / GPS / embedded thumbnails by default
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
### Install libvips
|
|
1700
|
+
|
|
1701
|
+
The pipeline uses **libvips** via the `ruby-vips` gem (added to the gemspec; installed automatically by `bundle install`). The Ruby bindings are tiny pure-FFI code; the install footprint is the libvips C library:
|
|
1702
|
+
|
|
1703
|
+
```bash
|
|
1704
|
+
brew install vips # macOS
|
|
1705
|
+
sudo apt install libvips42 # Ubuntu/Debian (since 20.04, AVIF included)
|
|
1706
|
+
apk add vips # Alpine
|
|
1707
|
+
```
|
|
1708
|
+
|
|
1709
|
+
GitHub Actions `ubuntu-latest` ships with libvips pre-installed. The legacy shell-out path (`vips` / `magick` CLIs) is still supported as a fallback when ruby-vips can't load — set `processor: vips` or `processor: imagemagick` to force it, or just let auto-detection do the right thing.
|
|
1710
|
+
|
|
1711
|
+
### What gets optimised
|
|
1712
|
+
|
|
1713
|
+
- **`app/assets/images/`** — site-wide imagery. Reference via `picture_tag 'hero.jpg', alt: '…'` in any layout, or as `<img src="/assets/images/hero.jpg">` in markdown.
|
|
1714
|
+
- **Post bundle images** — `content/posts/<slug>/hero.jpg` and any other raster siblings of `index.md`. Reference via the relative path (`hero.jpg`) in the post body; `rewrite_bundle_urls` + the image pipeline take care of the rest.
|
|
1715
|
+
- **Flat collection assets** — files under `content/publications/`, `content/workshops/`, etc. with their permalink URL prefix.
|
|
1716
|
+
|
|
1717
|
+
### What doesn't
|
|
1718
|
+
|
|
1719
|
+
- **SVG** is vector — nothing to thumbnail. SVGs pass through untouched.
|
|
1720
|
+
- **Images smaller than the smallest requested width.** A 300px-wide source with `widths: [400, 800, 1200]` produces a single variant at 300px (its native size). Upscaling is skipped — bigger files, blurrier output, no win.
|
|
1721
|
+
- **External URLs** (`<img src="https://…">`) — left alone.
|
|
1722
|
+
|
|
1723
|
+
### Storage and quality
|
|
1724
|
+
|
|
1725
|
+
A typical 4 MB iPhone JPEG becomes:
|
|
1726
|
+
|
|
1727
|
+
| Variant | Size |
|
|
1728
|
+
|---|---|
|
|
1729
|
+
| `hero-400.<hash>.avif` | ~30 KB |
|
|
1730
|
+
| `hero-800.<hash>.avif` | ~80 KB |
|
|
1731
|
+
| `hero-1200.<hash>.avif` | ~150 KB |
|
|
1732
|
+
| `hero-400.<hash>.webp` | ~50 KB |
|
|
1733
|
+
| `hero-800.<hash>.webp` | ~150 KB |
|
|
1734
|
+
| `hero-1200.<hash>.webp` | ~300 KB |
|
|
1735
|
+
| `hero-1200.<hash>.jpg` (fallback) | ~500 KB |
|
|
1736
|
+
|
|
1737
|
+
Roughly half the disk of the original, and each visitor downloads ~150 KB instead of 4 MB. EXIF / GPS / camera serial stripped by default (privacy + 50–500 KB extra savings per photo). Set `preserve_metadata: true` if you publish photography portfolios that surface camera info.
|
|
1738
|
+
|
|
1739
|
+
### Cache + determinism
|
|
1740
|
+
|
|
1741
|
+
Variants are fingerprinted by `<source-path>:<width>:<format>:<quality>:<source-mtime>`. Subsequent builds skip any variant whose source hasn't changed (mtime-based incrementality, sub-millisecond per variant). Two consecutive cold builds produce byte-identical output.
|
|
1742
|
+
|
|
1743
|
+
### Audit
|
|
1744
|
+
|
|
1745
|
+
`bin/audit` flags pages with heavy image payloads:
|
|
1746
|
+
|
|
1747
|
+
- Single image > 500 KB → warn (> 1 MB → error)
|
|
1748
|
+
- Total image weight on one page > 1 MB → warn (> 3 MB → error)
|
|
1749
|
+
|
|
1750
|
+
### Disabling
|
|
1751
|
+
|
|
1752
|
+
`images.optimise: false` reverts to passthrough — originals copied verbatim, `<img>` left as-is. No libvips dependency required. Use when shipping a text-only site, or when you've pre-optimised your imagery upstream.
|
|
1753
|
+
|
|
1754
|
+
## Deploy artifacts
|
|
1755
|
+
|
|
1756
|
+
Every build emits the files a static host needs to serve a real site:
|
|
1757
|
+
|
|
1758
|
+
| File | Purpose |
|
|
1759
|
+
| --- | --- |
|
|
1760
|
+
| `sitemap.xml` | All URLs with accurate `<lastmod>` (max of front-matter date, source mtime, and bundle-asset mtimes). Includes Google's `<image:image>` extension for post bundle images. |
|
|
1761
|
+
| `feed.xml` | RSS 2.0 feed of latest posts |
|
|
1762
|
+
| `feed.json` | JSON Feed 1.1 |
|
|
1763
|
+
| `robots.txt` | Per-environment — `Disallow: /` in dev/staging, real one in production |
|
|
1764
|
+
| `_headers` | Security headers (CSP, HSTS, Permissions-Policy, etc.) + asset cache-control. Read by Cloudflare Pages and Netlify |
|
|
1765
|
+
| `_redirects` | 301 mappings for any post with `redirect_from:` |
|
|
1766
|
+
| `manifest.json` | Web app manifest, populated from your site title + locale. Override colours/names under `manifest:` in `site.yml` |
|
|
1767
|
+
| `/.well-known/security.txt` | Generated when `security_contact:` is set |
|
|
1768
|
+
| `/llms.txt` | Curated machine-readable content index for AI readers — opt-in via `llms.enabled: true` (see [SEO and social sharing](#seo-and-social-sharing)) |
|
|
1769
|
+
| `<page-url>og.svg` | Auto-generated social share cards — opt-in via `og_images.auto: true` (see [SEO and social sharing](#seo-and-social-sharing)) |
|
|
1770
|
+
|
|
1771
|
+
**Redirects** — add `redirect_from:` to a post's front matter:
|
|
1772
|
+
|
|
1773
|
+
```yaml
|
|
1774
|
+
---
|
|
1775
|
+
title: Building with Abqari
|
|
1776
|
+
redirect_from:
|
|
1777
|
+
- /old-tutorial/
|
|
1778
|
+
---
|
|
1779
|
+
```
|
|
1780
|
+
|
|
1781
|
+
Abqari generates a stub HTML page at `/old-tutorial/` with a `<meta refresh>` and `<link rel=canonical>`, plus an entry in `_redirects` for hosts that read it.
|
|
1782
|
+
|
|
1783
|
+
**SEO meta** — every page's `<head>` includes canonical URL, Open Graph tags, Twitter card tags, RSS/JSON Feed alternates, and (for posts) JSON-LD `Article` structured data. All driven from `site.config` and frontmatter — no manual tagging required.
|
|
1784
|
+
|
|
1785
|
+
**Security headers** — the default `_headers` ships with a strict CSP (`'self'` only — works because Abqari has no inline scripts/styles), HSTS (1 year), `Referrer-Policy`, `Permissions-Policy` (camera/mic/geolocation/FLoC all off), `X-Content-Type-Options`, and `frame-ancestors 'none'`.
|
|
1786
|
+
|
|
1787
|
+
The policy is *built* from the features you've configured — enabling Plausible adds `plausible.io`, setting `newsletter.action` adds that provider's origin, and so on. You never hand-write it. Two keys extend it when the engine can't infer what you need:
|
|
1788
|
+
|
|
1789
|
+
```yaml
|
|
1790
|
+
# Off-origin audio / video. Colocated bundle media needs nothing —
|
|
1791
|
+
# it's same-origin, so it inherits `default-src 'self'`. This key
|
|
1792
|
+
# exists for media on a CDN or object store.
|
|
1793
|
+
media:
|
|
1794
|
+
host: https://media.example.com # or a list
|
|
1795
|
+
|
|
1796
|
+
# Any other origin the engine doesn't model.
|
|
1797
|
+
csp:
|
|
1798
|
+
img-src: [https://cdn.example.com]
|
|
1799
|
+
connect-src: [https://api.example.com]
|
|
1800
|
+
```
|
|
1801
|
+
|
|
1802
|
+
Both are **additive**: `'self'`, the pinned inline-script hashes, and every feature-derived origin survive. Values must be `scheme://host` — CSP keywords, wildcards and non-http schemes (`'unsafe-inline'`, `data:`, `blob:`, `*`) are rejected and fail the build rather than being silently dropped, because a malformed CSP fails open in the browser. Extendable directives: `child-src`, `connect-src`, `font-src`, `form-action`, `frame-src`, `img-src`, `manifest-src`, `media-src`, `script-src`, `style-src`, `worker-src`.
|
|
1803
|
+
|
|
1804
|
+
To weaken a hardening directive (`object-src`, `frame-ancestors`, `base-uri`, `default-src`) or use a non-http scheme, take over the whole header with a `headers:` block in `site.yml` — that replaces the computed policy entirely, pinned hashes included, so reach for it last.
|
|
1805
|
+
|
|
1806
|
+
## SEO and social sharing
|
|
1807
|
+
|
|
1808
|
+
Solid SEO ships as standard — there is nothing to install and no
|
|
1809
|
+
plugin to configure. Once `environments.production.url` is set in
|
|
1810
|
+
`config/site.yml`, every page automatically gets:
|
|
1811
|
+
|
|
1812
|
+
- A unique `<title>` (`Page title · Site title`) and a meta
|
|
1813
|
+
description (frontmatter `description:`, falling back to the page's
|
|
1814
|
+
first paragraph).
|
|
1815
|
+
- A canonical URL, plus `rel="prev"/"next"` on paginated pages.
|
|
1816
|
+
- Open Graph and Twitter card tags — including `og:image` with
|
|
1817
|
+
dimensions, and `article:published_time` / `article:modified_time` /
|
|
1818
|
+
`article:tag` on posts.
|
|
1819
|
+
- JSON-LD structured data: `Article` on posts, `WebSite` on the home
|
|
1820
|
+
page, `Person` on the about page, `BreadcrumbList` site-wide, and
|
|
1821
|
+
`Book` / `Event` / `Course` / `ImageObject` for publications,
|
|
1822
|
+
workshops, and photos.
|
|
1823
|
+
- An entry in `sitemap.xml` (with image extensions and `lastmod`),
|
|
1824
|
+
feed autodiscovery links, and a correct `robots.txt`.
|
|
1825
|
+
|
|
1826
|
+
Pages can opt out of indexing with `robots: noindex` in frontmatter —
|
|
1827
|
+
that also drops them from the sitemap.
|
|
1828
|
+
|
|
1829
|
+
**Share images.** The image on a social share resolves per page:
|
|
1830
|
+
frontmatter `share_image:` → the page's own `image:`/cover → the
|
|
1831
|
+
site-wide `share_image:` config. And if you'd rather never think about
|
|
1832
|
+
it, turn on auto-generated cards:
|
|
1833
|
+
|
|
1834
|
+
```yaml
|
|
1835
|
+
og_images:
|
|
1836
|
+
auto: true
|
|
1837
|
+
```
|
|
1838
|
+
|
|
1839
|
+
Every page without its own hero then gets a themed 1200×630 SVG card
|
|
1840
|
+
(site title, page title, author, date, your theme's colours) at
|
|
1841
|
+
`<page-url>og.svg`, wired into `og:image` automatically. Skip a
|
|
1842
|
+
specific page with `og_image: false` in its frontmatter.
|
|
1843
|
+
|
|
1844
|
+
**Twitter / X identity** for share-card attribution:
|
|
1845
|
+
|
|
1846
|
+
```yaml
|
|
1847
|
+
twitter:
|
|
1848
|
+
site: "@yourhandle"
|
|
1849
|
+
creator: "@yourhandle" # per-post override: `twitter_creator:` frontmatter
|
|
1850
|
+
```
|
|
1851
|
+
|
|
1852
|
+
**AI crawlers — blocked by default, welcomed by choice.** Out of the
|
|
1853
|
+
box, `robots.txt` blocks the major AI *training* crawlers (GPTBot,
|
|
1854
|
+
ClaudeBot, CCBot, Google-Extended, Applebot-Extended, PerplexityBot,
|
|
1855
|
+
and friends). Regular search engines are unaffected — blocking
|
|
1856
|
+
Google-Extended does not touch your Google ranking. Two knobs:
|
|
1857
|
+
|
|
1858
|
+
```yaml
|
|
1859
|
+
block_ai_scraping: false # allow AI training on your content (default: true)
|
|
1860
|
+
|
|
1861
|
+
llms:
|
|
1862
|
+
enabled: true # emit /llms.txt — a curated content index
|
|
1863
|
+
# for AI/LLM readers (default: false)
|
|
1864
|
+
```
|
|
1865
|
+
|
|
1866
|
+
The two are independent: you can block training crawlers while still
|
|
1867
|
+
publishing an `/llms.txt` roadmap for AI search tools, or any other
|
|
1868
|
+
combination that matches how you want machines to read your work.
|
|
1869
|
+
|
|
1870
|
+
**Near-instant navigation.** Modern browsers prerender the post links
|
|
1871
|
+
a reader is about to click via [Speculation
|
|
1872
|
+
Rules](https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API),
|
|
1873
|
+
on by default and strict-CSP friendly. It steps aside automatically
|
|
1874
|
+
for readers on slow or data-saving connections. Tune or disable:
|
|
1875
|
+
|
|
1876
|
+
```yaml
|
|
1877
|
+
speculation_rules:
|
|
1878
|
+
enabled: true # false to omit entirely
|
|
1879
|
+
eagerness: moderate # conservative / moderate / eager
|
|
1880
|
+
```
|
|
1881
|
+
|
|
1882
|
+
Run `bin/audit` after building — it checks every page for missing
|
|
1883
|
+
descriptions, broken links, missing alt text, and other SEO and
|
|
1884
|
+
accessibility problems before you deploy.
|
|
1885
|
+
|
|
1886
|
+
## Deploying
|
|
1887
|
+
|
|
1888
|
+
Abqari produces a plain `_site/` directory of static files — deploy it anywhere that serves static HTML.
|
|
1889
|
+
|
|
1890
|
+
Three GitHub Actions workflows ship in `.github/workflows/`, one per first-class host. Each is **gated by an `ENABLE_*` repository variable** so they're inert until you opt in. You can enable one, any combination, or none.
|
|
1891
|
+
|
|
1892
|
+
| Host | Workflow file | Gate variable | Required secrets |
|
|
1893
|
+
| --- | --- | --- | --- |
|
|
1894
|
+
| **Cloudflare Pages** | [`deploy-cloudflare.yml`](https://github.com/grantrayner/abqari/blob/main/lib/abqari/templates/site_skeleton/.github/workflows/deploy-cloudflare.yml) | `ENABLE_CLOUDFLARE = true` | `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID` (+ `CLOUDFLARE_PROJECT_NAME` variable) |
|
|
1895
|
+
| **Render** | [`deploy-render.yml`](https://github.com/grantrayner/abqari/blob/main/lib/abqari/templates/site_skeleton/.github/workflows/deploy-render.yml) | `ENABLE_RENDER = true` | `RENDER_DEPLOY_HOOK_URL` |
|
|
1896
|
+
| **GitHub Pages** | [`deploy-github-pages.yml`](https://github.com/grantrayner/abqari/blob/main/lib/abqari/templates/site_skeleton/.github/workflows/deploy-github-pages.yml) | `ENABLE_GITHUB_PAGES = true` | None (uses OIDC) |
|
|
1897
|
+
|
|
1898
|
+
Gate variables live in **Settings → Secrets and variables → Actions → Variables**. Secrets live in the **Secrets** tab on the same page.
|
|
1899
|
+
|
|
1900
|
+
### Step-by-step setup
|
|
1901
|
+
|
|
1902
|
+
**See [`docs/deploying.md`](https://github.com/grantrayner/abqari/blob/main/docs/deploying.md) for the full walkthrough**, including:
|
|
1903
|
+
|
|
1904
|
+
- Per-host one-time setup with exact button labels and field values
|
|
1905
|
+
- Whether to use Render's dashboard auto-deploy or the Actions-driven path
|
|
1906
|
+
- Cloudflare API token creation (the part where most people get stuck)
|
|
1907
|
+
- DNS configuration: CNAME for `www`, apex domain options, propagation verification
|
|
1908
|
+
- Custom domain setup on each host
|
|
1909
|
+
- A verification checklist for after the first deploy
|
|
1910
|
+
- Troubleshooting reference for the common failure modes
|
|
1911
|
+
|
|
1912
|
+
### Quick start (Render dashboard, no workflow needed)
|
|
1913
|
+
|
|
1914
|
+
If you just want a site live quickly, the simplest path is Render's dashboard. No GitHub secrets, no workflow files:
|
|
1915
|
+
|
|
1916
|
+
1. Sign in at [dashboard.render.com](https://dashboard.render.com), click **New +** → **Static Site**, connect your repo.
|
|
1917
|
+
2. Build command: `sudo apt-get update && sudo apt-get install -y libvips42 libheif1 libheif-plugin-aomenc libheif-plugin-libde265 libheif-plugin-dav1d && bundle install && bundle exec bin/build`
|
|
1918
|
+
(It looks long, but it's just three steps: install the image
|
|
1919
|
+
library on Render's build machine, install the Ruby gems, build
|
|
1920
|
+
the site. Copy it verbatim.)
|
|
1921
|
+
3. Publish directory: `_site`. Environment variable: `ABQARI_ENV=production`.
|
|
1922
|
+
4. Click **Create Static Site**. Wait ~3-5 minutes for the first build.
|
|
1923
|
+
|
|
1924
|
+
The site is live at `<name>.onrender.com`. Add a custom domain via **Settings → Custom Domains** — Render gives you a CNAME target to add at your registrar.
|
|
1925
|
+
|
|
1926
|
+
For everything else (Cloudflare Pages, GitHub Pages, Actions-gated Render, DNS, troubleshooting), see [`docs/deploying.md`](https://github.com/grantrayner/abqari/blob/main/docs/deploying.md).
|
|
1927
|
+
|
|
1928
|
+
### Other hosts
|
|
1929
|
+
|
|
1930
|
+
| Host | Approach |
|
|
1931
|
+
| --- | --- |
|
|
1932
|
+
| **Netlify** | Add a Netlify deploy step using [`nwtgck/actions-netlify`](https://github.com/nwtgck/actions-netlify); same shape as the Cloudflare workflow |
|
|
1933
|
+
| **S3 + CloudFront** | `aws s3 sync _site s3://your-bucket/ --delete` after build |
|
|
1934
|
+
| **Anywhere static** | Upload `_site/` via FTP/rsync/SCP/whatever |
|
|
1935
|
+
|
|
1936
|
+
The audit report is uploaded as a GitHub Actions artifact on every run, so you can review accessibility/SEO/dependency findings even when the site deploys successfully.
|
|
1937
|
+
|
|
1938
|
+
## Visualizations
|
|
1939
|
+
|
|
1940
|
+
Off by default. When enabled, Abqari generates D3-powered visualizations of your site's content at build time — JSON data + a viz page per chart, rendered entirely client-side from vendored D3 (~280 KB, loaded only on the viz routes).
|
|
1941
|
+
|
|
1942
|
+
```yaml
|
|
1943
|
+
visualizations:
|
|
1944
|
+
tag_bubble: true # /visualizations/tag-bubble/
|
|
1945
|
+
tag_cooccurrence: true # /visualizations/tag-cooccurrence/
|
|
1946
|
+
topic_constellation: true # /visualizations/topic-constellation/
|
|
1947
|
+
```
|
|
1948
|
+
|
|
1949
|
+
| Viz | What it shows | Useful when |
|
|
1950
|
+
| --- | --- | --- |
|
|
1951
|
+
| **Tag Bubble** | Circle pack of all tags, area = post count per tag | Spotting where you write most (and tags you haven't returned to) |
|
|
1952
|
+
| **Tag Co-occurrence** | Chord diagram of which tags appear together | Surfacing topic clusters across multi-tagged posts |
|
|
1953
|
+
| **Topic Constellation** | Force-directed graph: posts as nodes, links for shared tags | Exploring how your posts relate; clicking a node navigates to it |
|
|
1954
|
+
|
|
1955
|
+
Each visualization is independent — turn one on without the others. When all three are off, no JSON is written, no pages are generated, D3 isn't pulled into the build.
|
|
1956
|
+
|
|
1957
|
+
**How it works**: at build time, `Site#build` runs `Visualizations#write_data` to compute and emit the JSON files into `_site/visualizations/<name>.json` (data derived from `site.posts` and `site.posts_by_term`). It also adds a `VizPage` to `@generated_pages` for each enabled viz, which renders through the normal pipeline (post-process, minification, incremental check). Each viz page loads `/assets/d3/d3.v7.min.js` and the matching `/assets/js/viz-<name>.<hash>.js`, fetches the JSON, and renders into `#viz-container`.
|
|
1958
|
+
|
|
1959
|
+
**No third-party requests**: D3 is vendored, served from your origin. Strict CSP keeps working.
|
|
1960
|
+
|
|
1961
|
+
**Performance**: the 280 KB D3 bundle only loads on the three viz pages — site-wide pages stay JS-free.
|
|
1962
|
+
|
|
1963
|
+
**Theming**: SVG strokes and text use `currentColor`, so the visualizations honour the active light/dark theme automatically. Fill colours come from a fixed palette suitable for both modes.
|
|
1964
|
+
|
|
1965
|
+
**Linking**: viz pages are not auto-included in the nav. Add a link from anywhere — e.g. in `_footer.html.erb` or an "Explore" page — pointing at `/visualizations/<name>/`.
|
|
1966
|
+
|
|
1967
|
+
## Contact button
|
|
1968
|
+
|
|
1969
|
+
A spam-resistant contact button for your About page. Off by default. To enable, add your email under `contact:` in `config/site.yml`:
|
|
1970
|
+
|
|
1971
|
+
```yaml
|
|
1972
|
+
contact:
|
|
1973
|
+
email: you@example.com
|
|
1974
|
+
label: Get in touch # optional, defaults to "Contact"
|
|
1975
|
+
```
|
|
1976
|
+
|
|
1977
|
+
That's it. The button appears at the bottom of `/about/` on the next build.
|
|
1978
|
+
|
|
1979
|
+
**How the spam protection works**: the address is split at `@` into `data-user` and `data-domain` attributes. The rendered HTML never contains a string with `@` in it, and there's no `mailto:` prefix anywhere. A 16-line JS file rejoins the halves and opens the user's mail client only after the visitor clicks. Naive scrapers (which is most of them) see HTML and walk away with nothing.
|
|
1980
|
+
|
|
1981
|
+
**Removing the button**: any of three works.
|
|
1982
|
+
- Clear the email field in `config/site.yml` — `contact.email:` (no value).
|
|
1983
|
+
- Change `layout: about` to `layout: application` in `content/about.md`.
|
|
1984
|
+
- Delete `app/views/layouts/about.html.erb`.
|
|
1985
|
+
|
|
1986
|
+
**Putting it elsewhere**: render the partial from any layout — footer, sidebar, dedicated contact page:
|
|
1987
|
+
|
|
1988
|
+
```erb
|
|
1989
|
+
<%= render 'partials/contact_button' %>
|
|
1990
|
+
```
|
|
1991
|
+
|
|
1992
|
+
The partial honours the same config and renders nothing when `email` is unset, so dropping it into a shared layout is safe.
|
|
1993
|
+
|
|
1994
|
+
## Newsletter signup
|
|
1995
|
+
|
|
1996
|
+
A provider-agnostic subscription form — works with any service that
|
|
1997
|
+
accepts an HTML form POST (Mailchimp, Buttondown, ConvertKit / Kit,
|
|
1998
|
+
Substack, Cogent Notes, …). Configure the provider in `site.yml`:
|
|
1999
|
+
|
|
2000
|
+
```yaml
|
|
2001
|
+
newsletter:
|
|
2002
|
+
action: https://your-provider.example/subscribe # required
|
|
2003
|
+
email_field: email # the provider's email input name
|
|
2004
|
+
title: Subscribe to the newsletter
|
|
2005
|
+
description: One short email a month — no spam.
|
|
2006
|
+
button: Subscribe
|
|
2007
|
+
# hidden_fields: # provider-specific extras (tags,
|
|
2008
|
+
# - { name: tags, value: blog } # honeypots, list ids)
|
|
2009
|
+
```
|
|
2010
|
+
|
|
2011
|
+
Then render the form wherever you want it — a layout, the footer, a
|
|
2012
|
+
dedicated page:
|
|
2013
|
+
|
|
2014
|
+
```erb
|
|
2015
|
+
<%= render 'partials/newsletter_subscribe' %>
|
|
2016
|
+
```
|
|
2017
|
+
|
|
2018
|
+
Per-call overrides (`title:`, `description:`, `button:`) let one site
|
|
2019
|
+
run different pitches in different places. When `newsletter.action` is
|
|
2020
|
+
unset the partial renders nothing, so it's safe to leave the call in
|
|
2021
|
+
place before you've picked a provider. The provider's origin is added
|
|
2022
|
+
to the CSP `form-action` automatically.
|
|
2023
|
+
|
|
2024
|
+
## Analytics with Plausible
|
|
2025
|
+
|
|
2026
|
+
Privacy-friendly analytics with no cookies and no consent banner
|
|
2027
|
+
(~1 KB tracker). Off by default; both keys are required to enable:
|
|
2028
|
+
|
|
2029
|
+
```yaml
|
|
2030
|
+
plausible:
|
|
2031
|
+
enabled: true
|
|
2032
|
+
key: pa-your-script-key # the slug in https://plausible.io/js/<key>.js
|
|
2033
|
+
```
|
|
2034
|
+
|
|
2035
|
+
Abqari injects the tracker into `<head>` and extends the CSP for
|
|
2036
|
+
`plausible.io` automatically — no manual header work. Because both
|
|
2037
|
+
`enabled:` and `key:` must be set, you can keep the key in config with
|
|
2038
|
+
`enabled: false` and tracking stays off.
|
|
2039
|
+
|
|
2040
|
+
## IndieWeb & federation
|
|
2041
|
+
|
|
2042
|
+
Off by default. When `indieweb.enabled: true`, the build adds:
|
|
2043
|
+
|
|
2044
|
+
- **Microformats2 markup** (`h-entry`, `h-card`, `u-url`, `dt-published`, `e-content`, `p-category`, `p-name`, `p-summary`, `h-feed`) baked into the post, post-list, and about layouts. Invisible class attributes — no visual change. Lets other IndieWeb sites and federated tools parse your content cleanly.
|
|
2045
|
+
- **Webmention discovery** — `<link rel="webmention">` in `<head>` points incoming notifications at [webmention.io](https://webmention.io) on your behalf. Your site stays static; webmention.io catches replies/likes/reposts from other people's sites.
|
|
2046
|
+
- **Webmention rendering** — a "Replies & mentions" section below each post, pulled from webmention.io's cached JF2 feed at build time. Conversational mentions (in-reply-to, mention-of) get the full author + body card; likes / reposts / bookmarks render facepile-style (avatar grid). No JavaScript, no comment backend, no moderation queue — spam is structurally rare because replies have to be published on real websites first.
|
|
2047
|
+
- **Webmention send** — opt-in. When `send_webmentions: true`, the build POSTs notifications to external URLs your posts link to so the authors of those posts see you've cited them. Tracked in `vendor/webmentions/sent.yml` so re-builds don't double-send. Only runs against posts published within the last `send_window_days` (default 30).
|
|
2048
|
+
- **Bridgy Fed link tags** — opt-in. When `bridgy_fed: true`, the head emits `rel="me"` and `rel="alternate" type="application/activity+json"` tags pointing at [Bridgy Fed](https://fed.brid.gy), which speaks ActivityPub on your behalf. Mastodon users can then search `@<your-domain>` and follow the site directly.
|
|
2049
|
+
|
|
2050
|
+
### Setup
|
|
2051
|
+
|
|
2052
|
+
1. Visit [webmention.io](https://webmention.io) and sign in with your domain. The login flow uses `rel="me"` verification — make sure `social:` in `config/site.yml` carries at least one social profile that links back to your domain.
|
|
2053
|
+
2. Copy the API token from your webmention.io account page.
|
|
2054
|
+
3. (Optional) Visit [fed.brid.gy](https://fed.brid.gy) and complete its setup (same `rel="me"` pattern).
|
|
2055
|
+
4. Add to `config/site.yml`:
|
|
2056
|
+
|
|
2057
|
+
```yaml
|
|
2058
|
+
indieweb:
|
|
2059
|
+
enabled: true
|
|
2060
|
+
domain: example.com # bare host, no scheme
|
|
2061
|
+
webmention_io_token: "..." # from webmention.io account page
|
|
2062
|
+
bridgy_fed: false # true once Bridgy Fed setup complete
|
|
2063
|
+
send_webmentions: false # POST to sites your posts link to
|
|
2064
|
+
send_window_days: 30
|
|
2065
|
+
```
|
|
2066
|
+
|
|
2067
|
+
### How it works at build time
|
|
2068
|
+
|
|
2069
|
+
| Step | What happens |
|
|
2070
|
+
|---|---|
|
|
2071
|
+
| 1. Fetch | `Indieweb::Fetcher` pulls `https://webmention.io/api/mentions.jf2?domain=…&token=…` with an ETag header. 304 responses reuse the cached `vendor/webmentions/mentions.json`. |
|
|
2072
|
+
| 2. Index | Mentions are grouped by target URL (path-normalised so `/posts/foo` and `/posts/foo/` collapse to one key). |
|
|
2073
|
+
| 3. Render | Each post layout calls `site.webmentions_for(page.url)`; the `_webmentions` partial renders the matching mentions in a `<section class="webmentions">` below the body. |
|
|
2074
|
+
| 4. Send | After render, `Indieweb::Sender` walks posts published within `send_window_days`, extracts outbound external URLs, discovers each target's webmention endpoint (HEAD `Link:` header → GET `<link rel="webmention">`), POSTs a notification, and logs the result. |
|
|
2075
|
+
|
|
2076
|
+
### Per-post overrides
|
|
2077
|
+
|
|
2078
|
+
- Set `webmentions: false` in a post's frontmatter to suppress the `_webmentions` partial for that post.
|
|
2079
|
+
- The send-window means historical posts aren't replayed on every CI run — they were already notified in earlier builds.
|
|
2080
|
+
|
|
2081
|
+
### Honest limits
|
|
2082
|
+
|
|
2083
|
+
- Webmention adoption is uneven — maybe 10–20% of personal sites support sending them. Growing, but not universal.
|
|
2084
|
+
- Bridgy Fed is a third-party service. You don't control the federation hop; outages affect your Fediverse presence.
|
|
2085
|
+
- `webmention.io` is also a third-party service. The integration falls back to cached data on outages but a long outage means no new mentions render.
|
|
2086
|
+
|
|
2087
|
+
## POSSE — Publish on Own Site, Syndicate Elsewhere
|
|
2088
|
+
|
|
2089
|
+
Off by default. When `syndication.enabled: true`, the build POSTs a short summary + canonical URL of each new post to the configured networks (Mastodon, Bluesky). The originating posts on those networks point back at your canonical URL on your site.
|
|
2090
|
+
|
|
2091
|
+
```yaml
|
|
2092
|
+
# config/site.yml
|
|
2093
|
+
syndication:
|
|
2094
|
+
enabled: true
|
|
2095
|
+
window_days: 14 # only syndicate posts from the last N days
|
|
2096
|
+
mastodon:
|
|
2097
|
+
enabled: true
|
|
2098
|
+
instance: https://mastodon.social
|
|
2099
|
+
access_token: "..." # write:statuses scope
|
|
2100
|
+
visibility: public # public | unlisted | private
|
|
2101
|
+
bluesky:
|
|
2102
|
+
enabled: true
|
|
2103
|
+
handle: yourname.bsky.social
|
|
2104
|
+
app_password: "..." # NOT your account password
|
|
2105
|
+
```
|
|
2106
|
+
|
|
2107
|
+
### Setup
|
|
2108
|
+
|
|
2109
|
+
- **Mastodon.** In your instance UI: Preferences → Development → New Application. Scope: `write:statuses`. Copy the access token.
|
|
2110
|
+
- **Bluesky.** In the Bluesky app: Settings → App Passwords → Add. Use the generated app password (never your account password).
|
|
2111
|
+
|
|
2112
|
+
### How it works at build time
|
|
2113
|
+
|
|
2114
|
+
| Step | What happens |
|
|
2115
|
+
|---|---|
|
|
2116
|
+
| 1. Filter | `Syndication::Dispatcher` walks posts published within `window_days` (default 14). |
|
|
2117
|
+
| 2. Format | `Syndication::Formatter.snippet_for(post, url, limit:)` builds `<title> — <description> <url>` and truncates the description to fit each network's character cap (Mastodon 500, Bluesky 300). |
|
|
2118
|
+
| 3. POST | Each configured network adapter (`Networks::Mastodon`, `Networks::Bluesky`) authenticates and posts. The resulting URL on that network is returned. |
|
|
2119
|
+
| 4. Log | Every successful syndication is recorded in `vendor/syndication/sent.yml` keyed by `<network>|<canonical_url>`. Re-builds skip any post already marked `status: sent`. Failures are logged but retried on the next build — a transient API outage shouldn't permanently skip a post. |
|
|
2120
|
+
|
|
2121
|
+
### Per-post overrides
|
|
2122
|
+
|
|
2123
|
+
- `syndicate: false` in a post's frontmatter excludes that post from POSSE entirely.
|
|
2124
|
+
- `syndication_text: "..."` supplies a custom snippet, bypassing the auto-formatter (still subject to the per-network length cap).
|
|
2125
|
+
|
|
2126
|
+
### Reply collection
|
|
2127
|
+
|
|
2128
|
+
When **both** IndieWeb and POSSE are enabled, the flow is:
|
|
2129
|
+
|
|
2130
|
+
1. You publish a post; the build syndicates to Mastodon + Bluesky.
|
|
2131
|
+
2. A reader replies on Mastodon.
|
|
2132
|
+
3. Bridgy Fed translates the AP reply into a webmention sent to webmention.io.
|
|
2133
|
+
4. Next build, webmention.io's API returns it.
|
|
2134
|
+
5. The post's "Replies & mentions" section now shows the Mastodon reply alongside any direct webmentions from other personal sites.
|
|
2135
|
+
|
|
2136
|
+
You wrote once; conversations from three networks consolidate under the canonical post.
|
|
2137
|
+
|
|
2138
|
+
### Honest limits
|
|
2139
|
+
|
|
2140
|
+
- App tokens expire. Plan for an annual rotation.
|
|
2141
|
+
- Each network has its own character limits and content-policy quirks. Long titles can squeeze out the description.
|
|
2142
|
+
- Bluesky's link-facet rendering uses UTF-8 **byte** offsets, not character offsets — the adapter handles this, but it's worth knowing if you're debugging odd link placement.
|
|
2143
|
+
|
|
2144
|
+
## Folio integration
|
|
2145
|
+
|
|
2146
|
+
Off by default. When `folio:` is configured, Abqari pulls book/bundle data from a [Folio](https://www.foliobooks.app) storefront at build time and auto-generates pages for each entity. The visitor never makes a request to Folio — all data is baked into the static HTML.
|
|
2147
|
+
|
|
2148
|
+
```yaml
|
|
2149
|
+
# config/site.yml
|
|
2150
|
+
folio:
|
|
2151
|
+
base_url: https://www.foliobooks.app
|
|
2152
|
+
author_slug: your-author-slug
|
|
2153
|
+
# covers_host: https://covers.example — only if your Folio instance
|
|
2154
|
+
# serves cover images from a host other than the default
|
|
2155
|
+
# covers.foliobooks.app CDN. Whatever host is in effect is added to
|
|
2156
|
+
# the CSP's img-src automatically.
|
|
2157
|
+
```
|
|
2158
|
+
|
|
2159
|
+
That's it. On the next build:
|
|
2160
|
+
|
|
2161
|
+
```
|
|
2162
|
+
Folio: loaded 4 publication(s), 1 bundle(s), 0 series
|
|
2163
|
+
```
|
|
2164
|
+
|
|
2165
|
+
Auto-generated pages:
|
|
2166
|
+
|
|
2167
|
+
| URL | What it is |
|
|
2168
|
+
| --- | --- |
|
|
2169
|
+
| `/books/` | Index of all your books |
|
|
2170
|
+
| `/books/<slug>/` | One detail page per Folio publication |
|
|
2171
|
+
| `/bundles/` | Index of bundles |
|
|
2172
|
+
| `/bundles/<slug>/` | Detail page per Folio bundle |
|
|
2173
|
+
| `/series/` | Index of series |
|
|
2174
|
+
| `/series/<slug>/` | Detail page per series |
|
|
2175
|
+
|
|
2176
|
+
**Three endpoints, three collections.** Abqari calls `/api/v1/authors/:slug/publications.json`, `/bundles.json`, and `/series.json`. Each response is cached and ETag-revalidated independently.
|
|
2177
|
+
|
|
2178
|
+
**Override any auto-generated page** by creating a markdown file at
|
|
2179
|
+
the matching path. For example, to add a custom intro on
|
|
2180
|
+
`/books/memoirs-of-an-assassin/`:
|
|
2181
|
+
|
|
2182
|
+
```markdown
|
|
2183
|
+
---
|
|
2184
|
+
title: Memoirs of an Assassin
|
|
2185
|
+
---
|
|
2186
|
+
|
|
2187
|
+
A first-person reckoning with the trade. The free sample below…
|
|
2188
|
+
```
|
|
2189
|
+
|
|
2190
|
+
Auto-rendered cover, price, and buy/cart buttons stay where they
|
|
2191
|
+
are; your markdown body is appended after them.
|
|
2192
|
+
|
|
2193
|
+
### Manual bundles and series (no Folio)
|
|
2194
|
+
|
|
2195
|
+
Bundle and series pages don't *require* Folio. You can hand-author
|
|
2196
|
+
them entirely via frontmatter — useful for sites without a
|
|
2197
|
+
storefront, or to add manual bundles alongside externally-sourced
|
|
2198
|
+
ones. Manual pages always win on slug collision.
|
|
2199
|
+
|
|
2200
|
+
For a bundle, create `content/bundles/<slug>/index.md`:
|
|
2201
|
+
|
|
2202
|
+
```yaml
|
|
2203
|
+
---
|
|
2204
|
+
layout: bundle
|
|
2205
|
+
name: The Assassin Books # bundles + series use `name:`,
|
|
2206
|
+
# not `title:` (per docs/publications.md)
|
|
2207
|
+
description: Both novels in the series, packaged together.
|
|
2208
|
+
price: $16.98 # auto-expands to the canonical
|
|
2209
|
+
# { cents:, currency:, formatted: } hash
|
|
2210
|
+
buy_now_url: https://example.com/buy
|
|
2211
|
+
buy_label: Buy the bundle # default "Buy now"
|
|
2212
|
+
publications:
|
|
2213
|
+
- memoirs-of-an-assassin # bare slug
|
|
2214
|
+
- the-russia-assignment # bare slug
|
|
2215
|
+
---
|
|
2216
|
+
```
|
|
2217
|
+
|
|
2218
|
+
For a series, the same shape applies. Each publication entry can
|
|
2219
|
+
also be a hash with `position:` for the per-card "#N" prefix:
|
|
2220
|
+
|
|
2221
|
+
```yaml
|
|
2222
|
+
---
|
|
2223
|
+
layout: series
|
|
2224
|
+
name: Memoirs of an Assassin
|
|
2225
|
+
description: A series of psychological thrillers.
|
|
2226
|
+
tagline: Two novels, one arc.
|
|
2227
|
+
buy_now_url: https://example.com/series
|
|
2228
|
+
publications:
|
|
2229
|
+
- { slug: memoirs-of-an-assassin, position: 1 }
|
|
2230
|
+
- { slug: the-russia-assignment, position: 2 }
|
|
2231
|
+
---
|
|
2232
|
+
```
|
|
2233
|
+
|
|
2234
|
+
Each entry's slug is resolved against `site.collection('publications')`
|
|
2235
|
+
— which automatically merges manual content pages with any
|
|
2236
|
+
externally-sourced entries (Folio, etc.) — so each card carries
|
|
2237
|
+
title + cover + description + price drawn from the publication's
|
|
2238
|
+
own canonical frontmatter regardless of source. **One source of
|
|
2239
|
+
truth per book.**
|
|
2240
|
+
|
|
2241
|
+
The same publication-card markup is used by the publications index,
|
|
2242
|
+
bundle "What's included" lists, and series "Books in this series"
|
|
2243
|
+
lists — driven by the shared `_publication_card` partial. Visual
|
|
2244
|
+
treatment is consistent across all three contexts.
|
|
2245
|
+
|
|
2246
|
+
**Helpers** available in any layout/partial:
|
|
2247
|
+
|
|
2248
|
+
| Helper | What it returns |
|
|
2249
|
+
| --- | --- |
|
|
2250
|
+
| `publication_for(slug)` | Publication frontmatter hash, or nil |
|
|
2251
|
+
| `bundle_for(slug)` | Bundle frontmatter hash, or nil |
|
|
2252
|
+
| `series_for(slug)` | Series frontmatter hash, or nil |
|
|
2253
|
+
| `buy_button(subject, label:)` | `<a>` tag when `buy_now_url` is set, else empty string |
|
|
2254
|
+
| `cart_button(subject, label:)` | `<a>` tag when `add_to_cart_url` is set, else empty string |
|
|
2255
|
+
|
|
2256
|
+
`subject` accepts a frontmatter hash, a page-like object, or a slug string.
|
|
2257
|
+
|
|
2258
|
+
**Cache.** Folio responses are cached in `vendor/folio/{publications,bundles,series}.yml` with ETag conditional-GET. Subsequent builds get a `304 Not Modified` and reuse the cache. If Folio is briefly unreachable (CI runner offline, etc.), Abqari falls back to the cached data and prints a warning rather than failing the build.
|
|
2259
|
+
|
|
2260
|
+
```bash
|
|
2261
|
+
bin/fetch-folio # force-refresh the cache (drops ETags)
|
|
2262
|
+
```
|
|
2263
|
+
|
|
2264
|
+
**JSON-LD.** Each book page emits a `Book` schema with author, ISBN, price, language, and availability — pulled from the page's canonical frontmatter (works equally for manual content and externally-sourced entries). No extra config needed.
|
|
2265
|
+
|
|
2266
|
+
**Layouts and partials.** The engine ships:
|
|
2267
|
+
|
|
2268
|
+
- Layouts — `publication.html.erb`, `bundle.html.erb`, `series.html.erb`, `publications_index.html.erb`, `bundles_index.html.erb`, `book_series_index.html.erb`. (`publication` is named for the canonical collection; `slug: books` on the collection puts it at `/books/<slug>/` without renaming the layout file.)
|
|
2269
|
+
- Partials — `_publication_header.html.erb` (cover + intro grid on publication detail pages), `_publication_card.html.erb` (shared between the publications index, bundle "What's included", and series "Books in this series"), `_book_jsonld.html.erb` (Book schema for SEO), `_bundle_cta.html.erb` ("save with the bundle" callout).
|
|
2270
|
+
|
|
2271
|
+
Customise by overriding in your site (Path A — drop a same-pathed file in `app/views/`) or editing in place (Path B — fork).
|
|
2272
|
+
|
|
2273
|
+
## Accessibility
|
|
2274
|
+
|
|
2275
|
+
Abqari ships with a WCAG-aware baseline so an out-of-the-box site is usable by keyboard and screen-reader visitors without you having to think about it.
|
|
2276
|
+
|
|
2277
|
+
What's already wired in:
|
|
2278
|
+
|
|
2279
|
+
| Feature | What it does |
|
|
2280
|
+
| --- | --- |
|
|
2281
|
+
| **Skip-to-content link** | First focusable element on every page; visually hidden until tabbed to. Targets `<main id="main-content">` so keyboard users bypass the nav. Defined in [`_skip_link.html.erb`](https://github.com/grantrayner/abqari/blob/main/app/views/partials/_skip_link.html.erb) |
|
|
2282
|
+
| **`<main>` landmark** | Every layout wraps content in `<main id="main-content" tabindex="-1">` for the skip-link target and screen-reader "jump to main" navigation |
|
|
2283
|
+
| **`aria-label="Primary"`** on the site nav | Disambiguates from pagination / footer navs in screen readers |
|
|
2284
|
+
| **`aria-label="Pagination"`** on pagination | Same — pagination shows up as "Pagination, navigation" instead of generic "navigation" |
|
|
2285
|
+
| **`aria-pressed`** on the theme toggle | Screen-reader users hear the current state ("pressed" = dark mode on); JS keeps it in sync with `data-theme` |
|
|
2286
|
+
| **`:focus-visible` outline** | Keyboard users see where focus is; mouse clicks don't show an outline (the modern `:focus-visible` rule, not `:focus`). Defined per theme in `themes/<name>/css/app.css` |
|
|
2287
|
+
| **`prefers-reduced-motion`** | All themes honour the OS-level "reduce motion" preference for users with vestibular disorders |
|
|
2288
|
+
| **Icons** | Every shipped SVG has `aria-hidden="true"` so screen readers don't announce them; the surrounding link/button has the accessible label |
|
|
2289
|
+
| **`<html lang>`** | Set on every layout from `locale:` config — required for screen-reader pronunciation |
|
|
2290
|
+
| **Image alt text enforcement** | `bin/audit` errors on any `<img>` without `alt`; helpers like `picture_tag` require an `alt:` argument |
|
|
2291
|
+
| **Heading hierarchy check** | `bin/audit` warns on multiple `<h1>` per page |
|
|
2292
|
+
| **Landmark + skip-link check** | `bin/audit` warns on missing skip link, missing `<main>`, or unlabeled multi-`<nav>` pages |
|
|
2293
|
+
|
|
2294
|
+
If you build your own layout or partial, the audit catches the common mistakes. The four pieces of advice that the audit *doesn't* enforce automatically:
|
|
2295
|
+
|
|
2296
|
+
1. Don't put text inside `<button>` as an icon-only graphic — the button needs text or `aria-label`.
|
|
2297
|
+
2. Form inputs need `<label>` elements (Pagefind's search UI handles this internally).
|
|
2298
|
+
3. Color contrast is the theme's responsibility — the built-in themes are designed to meet WCAG AA in both light and dark modes; verify yours if you customise.
|
|
2299
|
+
4. If you add motion (animations, parallax), gate it behind `@media (prefers-reduced-motion: no-preference)`.
|
|
2300
|
+
|
|
2301
|
+
To remove any of the baseline pieces (e.g. you don't want the skip link), edit the relevant partial or layout. Everything is yours after `git clone`.
|
|
2302
|
+
|
|
2303
|
+
## Audit
|
|
2304
|
+
|
|
2305
|
+
```bash
|
|
2306
|
+
bin/audit
|
|
2307
|
+
```
|
|
2308
|
+
|
|
2309
|
+
```
|
|
2310
|
+
Built 9 page(s) in production → _site/
|
|
2311
|
+
Audit complete: audit/2026-05-07-1928.md (0 error, 1 warn, 3 info)
|
|
2312
|
+
```
|
|
2313
|
+
|
|
2314
|
+
Rebuilds the site, walks the output, and writes a markdown report to `audit/<YYYY-MM-DD-HHMM>.md` covering:
|
|
2315
|
+
|
|
2316
|
+
| Category | Checks |
|
|
2317
|
+
| --- | --- |
|
|
2318
|
+
| **Links** | Broken internal links and asset references |
|
|
2319
|
+
| **SEO** | Title length (>60), description length (>160 or missing), duplicate titles |
|
|
2320
|
+
| **Accessibility** | Images without `alt`, multiple `<h1>` per page |
|
|
2321
|
+
| **Privacy** | External resource loads (URLs not on the site host) |
|
|
2322
|
+
| **Dependencies** | Outdated gems via `bundle outdated`, CVEs via `bundle-audit` (if installed) |
|
|
2323
|
+
| **Content** | Posts without descriptions, single-use tags, `series_position` problems that break prev/next silently (non-Integer, duplicate, or missing) |
|
|
2324
|
+
|
|
2325
|
+
The report opens with a summary table, a **Suggested fixes** code block listing every `bundle update <gem>` command needed to clear the dependency findings, and then groups findings by category with severity (`error`, `warn`, `info`) and a suggestion where applicable. Reports accumulate in `audit/` — useful to commit so you have a diffable history of site quality over time.
|
|
2326
|
+
|
|
2327
|
+
### Dependency security check
|
|
2328
|
+
|
|
2329
|
+
For CVE scanning, install `bundler-audit` once:
|
|
2330
|
+
|
|
2331
|
+
```bash
|
|
2332
|
+
gem install bundler-audit
|
|
2333
|
+
```
|
|
2334
|
+
|
|
2335
|
+
When it's available, the audit runs `bundle-audit check --update` (which refreshes the `ruby-advisory-db` of ~1000 known advisories) and parses every finding into a structured report:
|
|
2336
|
+
|
|
2337
|
+
- **Severity** — Critical/High → `error`, Medium → `warn`, Low → `info` — driven by the CVE's own criticality
|
|
2338
|
+
- **Identifier** — GHSA or CVE number, with a link to the advisory
|
|
2339
|
+
- **Vulnerable version** — what's currently in your `Gemfile.lock`
|
|
2340
|
+
- **Title** — the human-readable advisory description
|
|
2341
|
+
- **Upgrade path** — the version constraint that resolves the issue (e.g., `~> 5.2.4.4, >= 6.0.3.3`)
|
|
2342
|
+
- **Suggested command** — `bundle update <gem>` you can copy/paste
|
|
2343
|
+
|
|
2344
|
+
Plus, every outdated (non-security) gem is listed via `bundle outdated --strict`, with installed-vs-latest versions and the `bundle update` command per gem.
|
|
2345
|
+
|
|
2346
|
+
Without `bundle-audit` installed, the audit still runs — the dependencies section just notes the CVE check was skipped.
|
|
2347
|
+
|
|
2348
|
+
### Compared to Dependabot
|
|
2349
|
+
|
|
2350
|
+
GitHub Dependabot does three things abqari's audit does **not**:
|
|
2351
|
+
|
|
2352
|
+
- **Runs continuously.** Dependabot scans your repo daily and notifies on new advisories. `bin/audit` is on-demand — run it before deploying, or wire it into CI.
|
|
2353
|
+
- **Opens pull requests.** Dependabot creates per-gem PRs with the changelog and patch. The audit gives you the exact `bundle update` command, but you run it yourself.
|
|
2354
|
+
- **Aggregates across the org.** Dependabot is a GitHub-hosted service. The audit is local.
|
|
2355
|
+
|
|
2356
|
+
What `bin/audit` does that Dependabot doesn't:
|
|
2357
|
+
|
|
2358
|
+
- **Same report covers links, SEO, accessibility, privacy, and content.** Dependabot only watches dependencies.
|
|
2359
|
+
- **Uses the same `ruby-advisory-db`** Dependabot's Ruby ecosystem coverage uses, just at a single point in time.
|
|
2360
|
+
- **Outputs to git-trackable markdown.** A diffable `audit/` history of every check you've run.
|
|
2361
|
+
|
|
2362
|
+
Use both: Dependabot (or similar) for continuous monitoring and PR creation; `bin/audit` for a single comprehensive snapshot you can review before deploys.
|
|
2363
|
+
|
|
2364
|
+
## Testing
|
|
2365
|
+
|
|
2366
|
+
```bash
|
|
2367
|
+
bin/test
|
|
2368
|
+
```
|
|
2369
|
+
|
|
2370
|
+
The engine ships with a Minitest suite under `test/` — around 80 test
|
|
2371
|
+
files and 900 tests, running in about a minute. Coverage
|
|
2372
|
+
spans helpers, paginator logic, full site builds, deploy artifacts,
|
|
2373
|
+
SEO meta (canonicals, OG cards, feeds, sitemap escaping), the audit
|
|
2374
|
+
run, the CLI scaffolder, content importers, IndieWeb/syndication,
|
|
2375
|
+
security hardening, and theme resolution. Test dependencies
|
|
2376
|
+
(`minitest`, plus `ostruct`/`rexml` for stubs and XML validation,
|
|
2377
|
+
`listen` for the watcher, and opt-in `simplecov` for coverage) are
|
|
2378
|
+
declared under `group :development` in the Gemfile, so production
|
|
2379
|
+
deploys don't pull them.
|
|
2380
|
+
|
|
2381
|
+
Tests only ship with the engine fork (Path B). For consuming sites
|
|
2382
|
+
(Path A), the engine's test suite isn't relevant — the gem itself is
|
|
2383
|
+
tested upstream; your site has its own tests if you write them.
|
|
2384
|
+
|
|
2385
|
+
A few orientation points in the tree:
|
|
2386
|
+
|
|
2387
|
+
```
|
|
2388
|
+
test/
|
|
2389
|
+
├── test_helper.rb Shared bootstrap; builds the fixture site once per run to a tmpdir
|
|
2390
|
+
├── helpers_test.rb truncate / pluralize / slugify / time / link_to / excerpt
|
|
2391
|
+
├── paginator_test.rb prev/next URL generation across edge cases
|
|
2392
|
+
├── site_test.rb Full-build integration: pages, pagination, tags, theme, SEO, deploy artifacts
|
|
2393
|
+
├── audit_test.rb Audit runs cleanly and writes a report
|
|
2394
|
+
├── cli_new_test.rb `abqari new` scaffolding, --with/--blank modes, config drift guards
|
|
2395
|
+
├── importers/ Substack / Ghost / Jekyll import pipelines
|
|
2396
|
+
└── … One topic per file — grep for the feature name
|
|
2397
|
+
```
|
|
2398
|
+
|
|
2399
|
+
Tests build to `$TMPDIR/abqari-test-site/` so they never clobber `_site/`; the temp dir is removed automatically when the run ends.
|
|
2400
|
+
|
|
2401
|
+
Add your own under `test/<name>_test.rb` — `bin/test` picks them up automatically. Test files just need to `require_relative 'test_helper'` and define a `Minitest::Test` subclass:
|
|
2402
|
+
|
|
2403
|
+
```ruby
|
|
2404
|
+
require_relative 'test_helper'
|
|
2405
|
+
|
|
2406
|
+
class MyFeatureTest < Minitest::Test
|
|
2407
|
+
def test_something
|
|
2408
|
+
assert_equal 42, 6 * 7
|
|
2409
|
+
end
|
|
2410
|
+
end
|
|
2411
|
+
```
|
|
2412
|
+
|
|
2413
|
+
Run a single test file: `bundle exec ruby -Ilib -Itest test/helpers_test.rb`. Run a single test method: `bundle exec ruby -Ilib -Itest test/helpers_test.rb -n test_slugify_lowercases_and_dashes`.
|
|
2414
|
+
|
|
2415
|
+
## Livereload
|
|
2416
|
+
|
|
2417
|
+
`bin/serve` watches `content/`, `app/`, `themes/`, `config/`, `lib/`, and `data/` for changes. On change:
|
|
2418
|
+
|
|
2419
|
+
1. The build runs incrementally (no `_site/` wipe; mtime-aware copies skip unchanged work).
|
|
2420
|
+
2. The build version bumps and the watcher classifies the change scope.
|
|
2421
|
+
3. Each rendered page in dev opens a Server-Sent Events stream to `/__reload__`; when the server pushes a rebuild event the client either:
|
|
2422
|
+
- **Swaps the `<link rel="stylesheet">` href** in place if only CSS files changed (preserves scroll position and form state), or
|
|
2423
|
+
- **Reloads the page** if anything else changed.
|
|
2424
|
+
|
|
2425
|
+
**Watcher backend.** When the `listen` gem is installed (it's a development-group dep), the watcher uses native FS events (FSEvents on macOS, inotify on Linux) — zero overhead between changes. Without `listen`, it falls back to 500ms mtime polling. Both modes debounce events by 100ms so editor tools that fire two events per save don't double-rebuild.
|
|
2426
|
+
|
|
2427
|
+
**Transport.** Server-Sent Events (SSE) over plain HTTP — no WebSocket dep, works through any proxy. The browser opens a single persistent EventSource per tab; the server pushes events with sub-50ms median latency. Connection auto-reconnects if the dev server restarts. The script is only injected when `ABQARI_ENV=development`, so production builds are clean.
|
|
2428
|
+
|
|
2429
|
+
## Plugins
|
|
2430
|
+
|
|
2431
|
+
Drop a Ruby file into `<site_root>/plugins/`. It's auto-loaded once at `Site.new` and can register callbacks for lifecycle events:
|
|
2432
|
+
|
|
2433
|
+
```ruby
|
|
2434
|
+
# plugins/word_counter.rb
|
|
2435
|
+
Abqari::Hooks.register(:post_process) do |html, _site, page|
|
|
2436
|
+
next html unless page.respond_to?(:post?) && page.post?
|
|
2437
|
+
|
|
2438
|
+
words = html.gsub(/<[^>]+>/, '').split(/\s+/).size
|
|
2439
|
+
html.sub('</article>', %(<footer>~#{words} words</footer></article>))
|
|
2440
|
+
end
|
|
2441
|
+
```
|
|
2442
|
+
|
|
2443
|
+
Lifecycle events: `:before_build`, `:after_load_pages`, `:after_generate`, `:after_build`. Filters: `:post_process(html, site, page)`. Full reference in [`docs/plugins.md`](https://github.com/grantrayner/abqari/blob/main/docs/plugins.md).
|
|
2444
|
+
|
|
2445
|
+
## Performance
|
|
2446
|
+
|
|
2447
|
+
Build-time and runtime perf are already tuned for typical sites (~50–200 pages). What's baked in:
|
|
2448
|
+
|
|
2449
|
+
**Build-time:**
|
|
2450
|
+
- **Compiled-template cache.** ERB layouts and partials are compiled once per build and reused across all pages — so 500 pages × 10 partials becomes 10 disk reads, not 5000.
|
|
2451
|
+
- **Memoized markdown rendering.** Each post's body is parsed once, even if a layout calls `page.content` plus `excerpt(page.content)` plus `reading_time(page.content)`.
|
|
2452
|
+
- **Content-hashed asset caching.** Fingerprinted assets that already exist with a matching name (matching content) are skipped — no rehash, no recopy.
|
|
2453
|
+
- **Mtime-aware bundle/public/font copying.** Files only copy when the source is newer than the destination.
|
|
2454
|
+
- **Parallel page rendering.** For sites with 16+ pages, rendering runs on a thread pool sized to `Etc.nprocessors`. Below that threshold, sequential (thread-spawn overhead would dominate).
|
|
2455
|
+
- **Incremental mode.** `bin/serve`'s watcher rebuilds with `incremental: true` — `_site/` isn't wiped; all the mtime/hash checks above pay off. The tradeoff: because nothing is wiped, output for a **deleted or renamed** source file stays on disk, so the dev server keeps serving a page you've removed until you restart or run `bin/build`. Production builds are always full builds, so a deploy never carries a stale page.
|
|
2456
|
+
- **HTML minification.** Production builds strip whitespace between tags, remove comments, collapse runs of whitespace. `<pre>`, `<textarea>`, `<script>`, and `<style>` content is preserved. Disable with `minify: false` in `site.yml`.
|
|
2457
|
+
- **CSS/JS minification.** Production builds strip comments and collapse whitespace in stylesheets and scripts before fingerprinting. Conservative on purpose: quoted CSS strings, `calc()` spacing, and descendant selectors survive; JS files using template literals ship untouched. Same `minify:` switch as HTML.
|
|
2458
|
+
|
|
2459
|
+
**Runtime (page load):**
|
|
2460
|
+
- **One render-blocking stylesheet.** The syntax theme, `_common.css`, theme `app.css`, and `site.css` ship as a single fingerprinted bundle (`/assets/css/bundle.<hash>.css`) — one request instead of four, and the Rouge styles get a content hash so a syntax-theme change busts the immutable cache.
|
|
2461
|
+
- **Image dimensions.** `<img width="..." height="...">` is set whether or not the image pipeline is enabled — pure-Ruby parsers read PNG/JPEG/GIF headers. No CLS regardless of optimisation toggle.
|
|
2462
|
+
- **Lazy-loaded images.** `loading="lazy" decoding="async"` on every `<img>`.
|
|
2463
|
+
- **Strict CSP.** No inline scripts/styles → strict CSP works without `'unsafe-inline'`.
|
|
2464
|
+
- **Long-cache assets.** Fingerprinted assets get `Cache-Control: public, max-age=31536000, immutable` via `_headers`.
|
|
2465
|
+
- **No JS by default.** The livereload script is injected only in dev.
|
|
2466
|
+
|
|
2467
|
+
Configurable knobs:
|
|
2468
|
+
|
|
2469
|
+
```yaml
|
|
2470
|
+
minify: false # disable HTML/CSS/JS minification (default: on in production)
|
|
2471
|
+
syntax_highlighting: false # disable Rouge highlighting
|
|
2472
|
+
images:
|
|
2473
|
+
optimise: true # enable libvips/magick image pipeline
|
|
2474
|
+
```
|
|
2475
|
+
|
|
2476
|
+
### Slow-page detection
|
|
2477
|
+
|
|
2478
|
+
Each page render is timed. If a single page's `write` (render +
|
|
2479
|
+
post-process + file write) takes longer than `SLOW_PAGE_THRESHOLD_MS`
|
|
2480
|
+
(default 500ms), the build logs a warning in real time with the URL,
|
|
2481
|
+
elapsed time, and a short list of likely causes:
|
|
2482
|
+
|
|
2483
|
+
```
|
|
2484
|
+
[warn] Slow page: /posts/the-big-one/ took 1.2s — 47 inline images · 12 code blocks · 340 KB output · TOC
|
|
2485
|
+
```
|
|
2486
|
+
|
|
2487
|
+
End-of-build summary, slowest first (top 5), with one "what this
|
|
2488
|
+
means + what to try" block per unique cause across the slow set
|
|
2489
|
+
— deduped so each explanation appears once even when several pages
|
|
2490
|
+
share a cause:
|
|
2491
|
+
|
|
2492
|
+
```
|
|
2493
|
+
Slow pages (>500ms): 3
|
|
2494
|
+
1.2s /posts/the-big-one/ (47 inline images · 12 code blocks · 340 KB output)
|
|
2495
|
+
780ms /posts/photo-essay/ (32 inline images)
|
|
2496
|
+
620ms /publications/the-field-guide/ (12 code blocks)
|
|
2497
|
+
|
|
2498
|
+
What this means and what to try:
|
|
2499
|
+
many_images:
|
|
2500
|
+
Every `<img>` is matched against the image-pipeline URL registry
|
|
2501
|
+
and rewritten to a responsive `<picture>` block. Per-tag cost is
|
|
2502
|
+
small; it adds up.
|
|
2503
|
+
Try: Reduce images on this page, move large galleries to their
|
|
2504
|
+
own page, or set `images.optimise: false` site-wide if the
|
|
2505
|
+
pipeline isn't needed.
|
|
2506
|
+
many_code_blocks:
|
|
2507
|
+
Rouge tokenises every `<pre>` block separately — long examples
|
|
2508
|
+
and many small blocks both add measurable time.
|
|
2509
|
+
Try: Shorten code examples, link to a gist for the long ones,
|
|
2510
|
+
or set `syntax_highlighting: false` site-wide if you don't need it.
|
|
2511
|
+
large_output:
|
|
2512
|
+
HTML minification and any plugin `:post_process` filters scan
|
|
2513
|
+
the whole document — cost scales with output size.
|
|
2514
|
+
Try: Trim the body, split into multiple pages, or set
|
|
2515
|
+
`minify: false` if the site doesn't need minified output.
|
|
2516
|
+
```
|
|
2517
|
+
|
|
2518
|
+
Goal: turn a generic "your page is slow" into a specific "here's
|
|
2519
|
+
why, and here's what to try." The heuristics that surface causes
|
|
2520
|
+
are five sampled signals (inline images, code blocks, output size,
|
|
2521
|
+
TOC presence, registered `:post_process` plugin filters); they
|
|
2522
|
+
correlate with the most common slow-path explanations. Each carries
|
|
2523
|
+
its own one-line "Try:" suggestion in the summary.
|
|
2524
|
+
|
|
2525
|
+
Override the threshold per-build, or disable detection entirely:
|
|
2526
|
+
|
|
2527
|
+
```bash
|
|
2528
|
+
ABQARI_SLOW_PAGE_MS=1000 bin/build # only flag pages slower than 1s
|
|
2529
|
+
ABQARI_SLOW_PAGE_MS=0 bin/build # disable slow-page warnings
|
|
2530
|
+
```
|
|
2531
|
+
|
|
2532
|
+
## Upgrading
|
|
2533
|
+
|
|
2534
|
+
Two paths, mirroring the [Quickstart](#quickstart):
|
|
2535
|
+
|
|
2536
|
+
### Path A — Sites consuming Abqari as a gem
|
|
2537
|
+
|
|
2538
|
+
Pull engine updates the standard Bundler way:
|
|
2539
|
+
|
|
2540
|
+
```bash
|
|
2541
|
+
bundle update abqari # pulls the latest release within your pin
|
|
2542
|
+
bin/build # verify your site still builds
|
|
2543
|
+
bin/audit # verify nothing regressed
|
|
2544
|
+
```
|
|
2545
|
+
|
|
2546
|
+
`abqari new` scaffolds the Gemfile pinned to the current major.minor
|
|
2547
|
+
(`gem 'abqari', '~> 1.1'` as of this release), so `bundle update abqari`
|
|
2548
|
+
picks up patch releases and bumping the pin is the deliberate action of
|
|
2549
|
+
taking a feature release. To track a fork or an unreleased branch
|
|
2550
|
+
instead, swap the line for a git source:
|
|
2551
|
+
|
|
2552
|
+
```ruby
|
|
2553
|
+
gem 'abqari', git: 'https://github.com/grantrayner/abqari.git', tag: 'v1.1.0'
|
|
2554
|
+
```
|
|
2555
|
+
|
|
2556
|
+
The cadence: read [CHANGELOG.md](https://github.com/grantrayner/abqari/blob/main/CHANGELOG.md) before each upgrade,
|
|
2557
|
+
note any breaking changes (`Removed:` and `Changed:` entries), then
|
|
2558
|
+
bump the pin and run the build.
|
|
2559
|
+
|
|
2560
|
+
**Your customizations are unaffected** because they live in your
|
|
2561
|
+
site repo (`config/`, `content/`, `data/`, `public/`, and any
|
|
2562
|
+
overrides under `app/`). Only the gem's internals — layouts,
|
|
2563
|
+
themes, helpers, `_common.css` — get updated.
|
|
2564
|
+
|
|
2565
|
+
**Site-local override drift** is the one thing to watch. If you've
|
|
2566
|
+
copied an engine file into your site to override it (e.g.
|
|
2567
|
+
`app/views/layouts/post.html.erb`), the engine's version may have
|
|
2568
|
+
changed in ways your local copy doesn't reflect. The audit catches
|
|
2569
|
+
broken links / missing partials; subtler shifts (CSS classes
|
|
2570
|
+
renamed, frontmatter fields renamed) need a manual diff against the
|
|
2571
|
+
gem's current copy. Run `bundle show abqari` to find the gem's
|
|
2572
|
+
installed path, then diff your override against the upstream file.
|
|
2573
|
+
|
|
2574
|
+
### Path B — Forked engine
|
|
2575
|
+
|
|
2576
|
+
If you forked the engine, you treat your site as a fork of upstream
|
|
2577
|
+
Abqari and pull updates explicitly via git.
|
|
2578
|
+
|
|
2579
|
+
```bash
|
|
2580
|
+
# One-time setup
|
|
2581
|
+
git remote rename origin upstream # the abqari source
|
|
2582
|
+
git remote add origin git@github.com:you/your-site.git # your own repo
|
|
2583
|
+
git push -u origin main
|
|
2584
|
+
```
|
|
2585
|
+
|
|
2586
|
+
Then for each upgrade, the shipped `bin/upgrade` wraps the common
|
|
2587
|
+
flows:
|
|
2588
|
+
|
|
2589
|
+
```bash
|
|
2590
|
+
bin/upgrade --check # show what would change; modify nothing
|
|
2591
|
+
bin/upgrade --engine-only # overwrite lib/, bin/, Gemfile from upstream — safest
|
|
2592
|
+
bin/upgrade # full git merge — for users who want everything
|
|
2593
|
+
bin/upgrade --to v1.0.0 # target a specific tag
|
|
2594
|
+
```
|
|
2595
|
+
|
|
2596
|
+
`--engine-only` is the recommended default for sites that have
|
|
2597
|
+
customized templates or themes. It overwrites only the files Abqari
|
|
2598
|
+
maintains (`lib/`, `bin/`, `Gemfile`) and leaves your `app/views/`,
|
|
2599
|
+
`themes/`, `content/`, `config/`, `data/` untouched. Engine bug
|
|
2600
|
+
fixes and new features land cleanly; your customizations survive.
|
|
2601
|
+
|
|
2602
|
+
After any upgrade:
|
|
2603
|
+
|
|
2604
|
+
```bash
|
|
2605
|
+
bundle install
|
|
2606
|
+
bundle exec bin/test
|
|
2607
|
+
bundle exec bin/audit
|
|
2608
|
+
git commit -am "Update Abqari engine to vX.Y.Z"
|
|
2609
|
+
```
|
|
2610
|
+
|
|
2611
|
+
#### What conflicts where (fork path)
|
|
2612
|
+
|
|
2613
|
+
If you skip `--engine-only` and run a full `git merge upstream/main`,
|
|
2614
|
+
conflicts are likely in the following areas:
|
|
2615
|
+
|
|
2616
|
+
| Path | Who edits it | Conflict resolution |
|
|
2617
|
+
| --- | --- | --- |
|
|
2618
|
+
| `lib/abqari/*.rb`, `lib/abqari.rb` | Abqari only | Always take upstream (`git checkout --theirs`) |
|
|
2619
|
+
| `bin/build`, `bin/serve`, etc. | Abqari mostly | Take upstream unless you've made specific changes |
|
|
2620
|
+
| `app/views/layouts/*` | Both | Resolve manually — usually keep yours, port new features |
|
|
2621
|
+
| `app/views/partials/*` | Both | Same — keep yours, copy new partials over |
|
|
2622
|
+
| `themes/*` | Abqari ships, you customize | Keep yours; new themes appear automatically |
|
|
2623
|
+
| `content/`, `config/`, `data/`, `app/icons/` | You only | Always keep yours (`git checkout --ours`) |
|
|
2624
|
+
| `Gemfile` | Both | Merge — keep your additions, accept new Abqari deps |
|
|
2625
|
+
| `.github/workflows/*`, `LICENSE`, `CHANGELOG.md`, `CONTRIBUTING.md` | Abqari, if you took them | Take upstream |
|
|
2626
|
+
|
|
2627
|
+
#### When to prefer which path
|
|
2628
|
+
|
|
2629
|
+
| You want… | Pick |
|
|
2630
|
+
| --- | --- |
|
|
2631
|
+
| Fast, low-friction upgrades; minimal site repo | **Path A — gem** |
|
|
2632
|
+
| Engine code in your repo so you can edit it freely | **Path B — fork** |
|
|
2633
|
+
| To deeply customize layouts / themes for a single site | Either works; Path A keeps your overrides in `app/`, Path B lets you edit in place |
|
|
2634
|
+
| To run on a machine that can't reach the engine git source at install time | **Path B — fork** (everything's local) |
|
|
2635
|
+
|
|
2636
|
+
For most sites, **Path A** is the right call. Path B is for the
|
|
2637
|
+
unusual case where you want full code ownership and accept the
|
|
2638
|
+
manual-merge upgrade cost.
|
|
2639
|
+
|
|
2640
|
+
## Configuration
|
|
2641
|
+
|
|
2642
|
+
### Site config
|
|
2643
|
+
|
|
2644
|
+
`config/site.yml` has shared keys at the top, with per-environment overrides under `environments:`:
|
|
2645
|
+
|
|
2646
|
+
```yaml
|
|
2647
|
+
title: My Site
|
|
2648
|
+
description: A site built with Abqari
|
|
2649
|
+
|
|
2650
|
+
environments:
|
|
2651
|
+
development:
|
|
2652
|
+
url: http://localhost:4000
|
|
2653
|
+
|
|
2654
|
+
production:
|
|
2655
|
+
url: https://example.com
|
|
2656
|
+
```
|
|
2657
|
+
|
|
2658
|
+
`Site#config` is the merged result of shared keys + the active environment's keys (deep merged). Templates just call `site.config['url']` — they never know which environment they're in.
|
|
2659
|
+
|
|
2660
|
+
### Environments
|
|
2661
|
+
|
|
2662
|
+
The active environment is `ABQARI_ENV` (default `development`). The bin scripts set sensible defaults:
|
|
2663
|
+
|
|
2664
|
+
| Command | Default `ABQARI_ENV` |
|
|
2665
|
+
| --- | --- |
|
|
2666
|
+
| `bin/serve` | `development` |
|
|
2667
|
+
| `bin/build` | `production` |
|
|
2668
|
+
|
|
2669
|
+
Override either way: `ABQARI_ENV=production bin/serve` or `ABQARI_ENV=staging bin/build`.
|
|
2670
|
+
|
|
2671
|
+
### Locale
|
|
2672
|
+
|
|
2673
|
+
Single-locale sites (Spanish, French, Japanese, etc.) work out of the box — markdown body text, front matter, and slugs are language-agnostic. Set the language code in `config/site.yml`:
|
|
2674
|
+
|
|
2675
|
+
```yaml
|
|
2676
|
+
locale: fr
|
|
2677
|
+
```
|
|
2678
|
+
|
|
2679
|
+
This wires up:
|
|
2680
|
+
- `<html lang="fr">` on every page
|
|
2681
|
+
- `<language>fr</language>` in the RSS feed and JSON Feed
|
|
2682
|
+
- `Preferred-Languages: fr` in `/.well-known/security.txt`
|
|
2683
|
+
|
|
2684
|
+
Defaults to `en` when unset. UI strings in the shipped templates (`Posts tagged`, `Older →`, `Built with Abqari`, etc.) are deliberately not translated by the engine — they're plain text in `app/views/`, edit them in your language. We don't bake in translations we'd then have to maintain.
|
|
2685
|
+
|
|
2686
|
+
Abqari is **not** multilingual: one site, one locale. If you need both English and Spanish on one domain, that's full i18n — out of scope.
|
|
2687
|
+
|
|
2688
|
+
### Front matter
|
|
2689
|
+
|
|
2690
|
+
Per-page settings on each Markdown file:
|
|
2691
|
+
|
|
2692
|
+
```yaml
|
|
2693
|
+
---
|
|
2694
|
+
title: About
|
|
2695
|
+
description: Who I am and what I do
|
|
2696
|
+
layout: application # optional, defaults to 'application'
|
|
2697
|
+
---
|
|
2698
|
+
```
|
|
2699
|
+
|
|
2700
|
+
## Roadmap
|
|
2701
|
+
|
|
2702
|
+
### What's in 1.0
|
|
2703
|
+
|
|
2704
|
+
Stable, semver-tracked surface area as of `1.0.0`. The
|
|
2705
|
+
[CHANGELOG](https://github.com/grantrayner/abqari/blob/main/CHANGELOG.md) lists every shipping feature; broadly:
|
|
2706
|
+
four built-in collections (`posts`, `photos`, `publications`,
|
|
2707
|
+
`workshops`) plus the `bundles`/`series` meta-collections, ERB
|
|
2708
|
+
templating with the standard helper library,
|
|
2709
|
+
the libvips image pipeline, full SEO suite, IndieWeb + POSSE,
|
|
2710
|
+
content importers, `bin/audit` quality gates, and shipped deploy
|
|
2711
|
+
templates for Cloudflare Pages, GitHub Pages, and Render.
|
|
2712
|
+
|
|
2713
|
+
### Looking ahead (post-1.0)
|
|
2714
|
+
|
|
2715
|
+
- **Multi-locale support** — single-locale only today. Multi-locale
|
|
2716
|
+
would touch sitemap, JSON-LD, head meta, and the taxonomy layer.
|
|
2717
|
+
- **Search by collection** — Pagefind currently indexes the entire
|
|
2718
|
+
site. Per-collection search scopes would let visitors restrict to
|
|
2719
|
+
e.g. cookbook only.
|
|
2720
|
+
- **First-class comment moderation surface** — webmentions land
|
|
2721
|
+
raw today; a moderation queue (block-list + approval gate) would
|
|
2722
|
+
let authors curate before render.
|
|
2723
|
+
- **More publisher backends** — Folio is the only first-class
|
|
2724
|
+
integration. The `PublisherShowPage` / `PublisherIndexPage`
|
|
2725
|
+
abstraction is generic; another backend (Shopify? Gumroad?)
|
|
2726
|
+
could plug in without engine changes.
|
|
2727
|
+
|
|
2728
|
+
## Why "Abqari"?
|
|
2729
|
+
|
|
2730
|
+
*ʿAbqarī* (عبقري) is Arabic for "genius." The word traces back to
|
|
2731
|
+
Wādī ʿAbqar — the legendary valley of the jinn where, in classical
|
|
2732
|
+
Arab lore, poets received their inspiration. A site generator built
|
|
2733
|
+
for working authors could hardly ask for a better namesake.
|
|
2734
|
+
|
|
2735
|
+
The mark is a rub el hizb (۞), the eight-pointed star Arabic
|
|
2736
|
+
manuscripts use to divide and structure text — which is, at heart,
|
|
2737
|
+
what this engine does to yours. You'll find it faintly in the corner
|
|
2738
|
+
of the auto-generated share cards, and as the favicon on
|
|
2739
|
+
[abqari.dev](https://www.abqari.dev).
|