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.
Files changed (1509) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +889 -0
  3. data/LICENSE +21 -0
  4. data/README.md +2739 -0
  5. data/SECURITY.md +73 -0
  6. data/THIRD_PARTY_NOTICES.md +46 -0
  7. data/app/assets/css/_common.css +3125 -0
  8. data/app/assets/js/carousel.js +39 -0
  9. data/app/assets/js/contact-button.js +28 -0
  10. data/app/assets/js/random-pool.js +44 -0
  11. data/app/assets/js/search-init.js +11 -0
  12. data/app/assets/js/theme-toggle.js +39 -0
  13. data/app/assets/js/viz-tag-bubble.js +108 -0
  14. data/app/assets/js/viz-tag-cooccurrence.js +125 -0
  15. data/app/assets/js/viz-topic-constellation.js +142 -0
  16. data/app/icons/bluesky.svg +3 -0
  17. data/app/icons/github.svg +3 -0
  18. data/app/icons/linkedin.svg +3 -0
  19. data/app/icons/mail.svg +4 -0
  20. data/app/icons/mastodon.svg +3 -0
  21. data/app/icons/moon.svg +3 -0
  22. data/app/icons/rss.svg +3 -0
  23. data/app/icons/sun.svg +4 -0
  24. data/app/icons/x.svg +3 -0
  25. data/app/views/layouts/about.html.erb +70 -0
  26. data/app/views/layouts/application.html.erb +65 -0
  27. data/app/views/layouts/book_series_index.html.erb +43 -0
  28. data/app/views/layouts/bundle.html.erb +111 -0
  29. data/app/views/layouts/bundles_index.html.erb +46 -0
  30. data/app/views/layouts/country_index.html.erb +59 -0
  31. data/app/views/layouts/gallery_index.html.erb +126 -0
  32. data/app/views/layouts/home.html.erb +107 -0
  33. data/app/views/layouts/photo.html.erb +100 -0
  34. data/app/views/layouts/photos_archive.html.erb +101 -0
  35. data/app/views/layouts/photos_index.html.erb +204 -0
  36. data/app/views/layouts/post.html.erb +153 -0
  37. data/app/views/layouts/posts_archive.html.erb +98 -0
  38. data/app/views/layouts/posts_index.html.erb +80 -0
  39. data/app/views/layouts/publication.html.erb +116 -0
  40. data/app/views/layouts/publications_archive.html.erb +89 -0
  41. data/app/views/layouts/publications_index.html.erb +246 -0
  42. data/app/views/layouts/related_index.html.erb +48 -0
  43. data/app/views/layouts/series.html.erb +110 -0
  44. data/app/views/layouts/series_index.html.erb +202 -0
  45. data/app/views/layouts/tag.html.erb +16 -0
  46. data/app/views/layouts/visualization.html.erb +32 -0
  47. data/app/views/layouts/workshop.html.erb +163 -0
  48. data/app/views/layouts/workshops_index.html.erb +209 -0
  49. data/app/views/partials/_back_link.html.erb +27 -0
  50. data/app/views/partials/_back_to_index.html.erb +25 -0
  51. data/app/views/partials/_book_grid.html.erb +76 -0
  52. data/app/views/partials/_book_jsonld.html.erb +72 -0
  53. data/app/views/partials/_book_related.html.erb +87 -0
  54. data/app/views/partials/_breadcrumb_jsonld.html.erb +60 -0
  55. data/app/views/partials/_bundle_cta.html.erb +26 -0
  56. data/app/views/partials/_carousel.html.erb +64 -0
  57. data/app/views/partials/_collection_page_jsonld.html.erb +81 -0
  58. data/app/views/partials/_contact_button.html.erb +42 -0
  59. data/app/views/partials/_course_jsonld.html.erb +95 -0
  60. data/app/views/partials/_draft_banner.html.erb +20 -0
  61. data/app/views/partials/_event_jsonld.html.erb +122 -0
  62. data/app/views/partials/_footer.html.erb +88 -0
  63. data/app/views/partials/_future_banner.html.erb +25 -0
  64. data/app/views/partials/_head.html.erb +440 -0
  65. data/app/views/partials/_image_gallery.html.erb +64 -0
  66. data/app/views/partials/_image_object_jsonld.html.erb +61 -0
  67. data/app/views/partials/_nav.html.erb +63 -0
  68. data/app/views/partials/_newsletter_subscribe.html.erb +85 -0
  69. data/app/views/partials/_next_in_series.html.erb +27 -0
  70. data/app/views/partials/_next_item.html.erb +38 -0
  71. data/app/views/partials/_pagination.html.erb +15 -0
  72. data/app/views/partials/_person_jsonld.html.erb +46 -0
  73. data/app/views/partials/_plausible.html.erb +24 -0
  74. data/app/views/partials/_post_list.html.erb +101 -0
  75. data/app/views/partials/_post_series_previews.html.erb +92 -0
  76. data/app/views/partials/_publication_card.html.erb +75 -0
  77. data/app/views/partials/_publication_header.html.erb +61 -0
  78. data/app/views/partials/_related_item.html.erb +68 -0
  79. data/app/views/partials/_related_items.html.erb +105 -0
  80. data/app/views/partials/_search.html.erb +26 -0
  81. data/app/views/partials/_series_pager.html.erb +48 -0
  82. data/app/views/partials/_skip_link.html.erb +12 -0
  83. data/app/views/partials/_taxonomy_row.html.erb +62 -0
  84. data/app/views/partials/_testimonials.html.erb +149 -0
  85. data/app/views/partials/_theme_toggle.html.erb +19 -0
  86. data/app/views/partials/_toc.html.erb +136 -0
  87. data/app/views/partials/_webmentions.html.erb +131 -0
  88. data/app/views/partials/_website_jsonld.html.erb +39 -0
  89. data/app/views/partials/home/_contact.html.erb +19 -0
  90. data/app/views/partials/home/_featured_publication.html.erb +107 -0
  91. data/app/views/partials/home/_latest_photo.html.erb +95 -0
  92. data/app/views/partials/home/_latest_post.html.erb +48 -0
  93. data/app/views/partials/home/_latest_publication.html.erb +89 -0
  94. data/app/views/partials/home/_next_workshop.html.erb +119 -0
  95. data/app/views/partials/home/_photo_gallery.html.erb +71 -0
  96. data/app/views/partials/home/_random_photo.html.erb +106 -0
  97. data/app/views/partials/home/_random_post.html.erb +64 -0
  98. data/exe/abqari +37 -0
  99. data/lib/abqari/artifact_writer.rb +196 -0
  100. data/lib/abqari/asset_pipeline.rb +235 -0
  101. data/lib/abqari/audit/a11y.rb +80 -0
  102. data/lib/abqari/audit/content.rb +154 -0
  103. data/lib/abqari/audit/dependencies.rb +127 -0
  104. data/lib/abqari/audit/links.rb +120 -0
  105. data/lib/abqari/audit/performance.rb +120 -0
  106. data/lib/abqari/audit/privacy.rb +42 -0
  107. data/lib/abqari/audit/rule.rb +56 -0
  108. data/lib/abqari/audit/seo.rb +105 -0
  109. data/lib/abqari/audit.rb +149 -0
  110. data/lib/abqari/bundle_assets.rb +127 -0
  111. data/lib/abqari/cli/new.rb +541 -0
  112. data/lib/abqari/cli/theme.rb +181 -0
  113. data/lib/abqari/cli.rb +130 -0
  114. data/lib/abqari/erbi_template.rb +72 -0
  115. data/lib/abqari/feed.rb +159 -0
  116. data/lib/abqari/folio.rb +195 -0
  117. data/lib/abqari/font_pipeline.rb +104 -0
  118. data/lib/abqari/headers_file.rb +324 -0
  119. data/lib/abqari/helpers.rb +308 -0
  120. data/lib/abqari/hooks.rb +136 -0
  121. data/lib/abqari/http_safe.rb +222 -0
  122. data/lib/abqari/image_pipeline.rb +800 -0
  123. data/lib/abqari/importers/base.rb +351 -0
  124. data/lib/abqari/importers/cli.rb +169 -0
  125. data/lib/abqari/importers/ghost.rb +113 -0
  126. data/lib/abqari/importers/jekyll.rb +256 -0
  127. data/lib/abqari/importers/substack.rb +204 -0
  128. data/lib/abqari/importers/support/html_to_markdown.rb +126 -0
  129. data/lib/abqari/importers/support/image_downloader.rb +170 -0
  130. data/lib/abqari/importers/support/report.rb +87 -0
  131. data/lib/abqari/importers/support/slug_normalizer.rb +53 -0
  132. data/lib/abqari/importers.rb +64 -0
  133. data/lib/abqari/indieweb.rb +551 -0
  134. data/lib/abqari/llms_txt.rb +110 -0
  135. data/lib/abqari/log.rb +61 -0
  136. data/lib/abqari/manifest.rb +58 -0
  137. data/lib/abqari/minifier.rb +108 -0
  138. data/lib/abqari/og_image_generator.rb +223 -0
  139. data/lib/abqari/page/bundle.rb +152 -0
  140. data/lib/abqari/page/incremental.rb +74 -0
  141. data/lib/abqari/page/parser.rb +129 -0
  142. data/lib/abqari/page/path.rb +153 -0
  143. data/lib/abqari/page.rb +424 -0
  144. data/lib/abqari/paginated_shard.rb +109 -0
  145. data/lib/abqari/paginator.rb +35 -0
  146. data/lib/abqari/path_safe.rb +62 -0
  147. data/lib/abqari/plausible_script.rb +20 -0
  148. data/lib/abqari/publication_normaliser.rb +111 -0
  149. data/lib/abqari/publisher_pages.rb +342 -0
  150. data/lib/abqari/redirects.rb +134 -0
  151. data/lib/abqari/related_index_page.rb +79 -0
  152. data/lib/abqari/render_context/cover_helpers.rb +338 -0
  153. data/lib/abqari/render_context/picture_helpers.rb +178 -0
  154. data/lib/abqari/render_context/publication_helpers.rb +120 -0
  155. data/lib/abqari/render_context.rb +251 -0
  156. data/lib/abqari/renderer.rb +788 -0
  157. data/lib/abqari/robots.rb +93 -0
  158. data/lib/abqari/search.rb +68 -0
  159. data/lib/abqari/security_txt.rb +44 -0
  160. data/lib/abqari/server.rb +508 -0
  161. data/lib/abqari/site.rb +952 -0
  162. data/lib/abqari/site_config.rb +212 -0
  163. data/lib/abqari/site_loader.rb +265 -0
  164. data/lib/abqari/sitemap.rb +87 -0
  165. data/lib/abqari/speculation_rules_guard.rb +26 -0
  166. data/lib/abqari/syndication.rb +466 -0
  167. data/lib/abqari/syntax_highlighter.rb +146 -0
  168. data/lib/abqari/taxonomy_page.rb +129 -0
  169. data/lib/abqari/templates/site_skeleton/.github/dependabot.yml +53 -0
  170. data/lib/abqari/templates/site_skeleton/.github/workflows/deploy-cloudflare.yml +107 -0
  171. data/lib/abqari/templates/site_skeleton/.github/workflows/deploy-github-pages.yml +107 -0
  172. data/lib/abqari/templates/site_skeleton/.github/workflows/deploy-render.yml +87 -0
  173. data/lib/abqari/templates/site_skeleton/.gitignore +24 -0
  174. data/lib/abqari/templates/site_skeleton/.ruby-version.tt +1 -0
  175. data/lib/abqari/templates/site_skeleton/Gemfile.tt +20 -0
  176. data/lib/abqari/templates/site_skeleton/README.md.tt +60 -0
  177. data/lib/abqari/templates/site_skeleton/app/assets/css/site.css +9 -0
  178. data/lib/abqari/templates/site_skeleton/bin/audit +46 -0
  179. data/lib/abqari/templates/site_skeleton/bin/build +32 -0
  180. data/lib/abqari/templates/site_skeleton/bin/fetch-folio +32 -0
  181. data/lib/abqari/templates/site_skeleton/bin/fetch-fonts +20 -0
  182. data/lib/abqari/templates/site_skeleton/bin/import +31 -0
  183. data/lib/abqari/templates/site_skeleton/bin/index +30 -0
  184. data/lib/abqari/templates/site_skeleton/bin/new +235 -0
  185. data/lib/abqari/templates/site_skeleton/bin/serve +30 -0
  186. data/lib/abqari/templates/site_skeleton/config/site.blank.yml.tt +65 -0
  187. data/lib/abqari/templates/site_skeleton/config/site.yml.tt +266 -0
  188. data/lib/abqari/templates/site_skeleton/content/404.md +17 -0
  189. data/lib/abqari/templates/site_skeleton/content/about.blank.md.tt +12 -0
  190. data/lib/abqari/templates/site_skeleton/content/about.md.tt +18 -0
  191. data/lib/abqari/templates/site_skeleton/content/index.blank.md.tt +11 -0
  192. data/lib/abqari/templates/site_skeleton/content/index.md.tt +34 -0
  193. data/lib/abqari/templates/site_skeleton/content/photos/index.md.tt +12 -0
  194. data/lib/abqari/templates/site_skeleton/content/posts/index.md +12 -0
  195. data/lib/abqari/templates/site_skeleton/content/posts/welcome/index.md.tt +50 -0
  196. data/lib/abqari/templates/site_skeleton/content/publications/index.md.tt +13 -0
  197. data/lib/abqari/templates/site_skeleton/content/workshops/index.md.tt +13 -0
  198. data/lib/abqari/templates/site_skeleton/public/robots.txt +6 -0
  199. data/lib/abqari/templates/site_skeleton/themes/.gitkeep +0 -0
  200. data/lib/abqari/templates/site_skeleton/vendor/folio/.gitkeep +0 -0
  201. data/lib/abqari/templates/site_skeleton/vendor/fonts/.gitkeep +0 -0
  202. data/lib/abqari/theme_bootstrap.rb +19 -0
  203. data/lib/abqari/theme_fonts.rb +51 -0
  204. data/lib/abqari/version.rb +18 -0
  205. data/lib/abqari/visualizations.rb +215 -0
  206. data/lib/abqari.rb +154 -0
  207. data/themes/bootstrap/css/app.css +97 -0
  208. data/themes/bootstrap/public/LICENSE +21 -0
  209. data/themes/bootstrap/public/css/bootstrap.min.css +6 -0
  210. data/themes/bootstrap/public/css/bootstrap.min.css.map +1 -0
  211. data/themes/bootstrap/public/js/bootstrap.bundle.min.js +7 -0
  212. data/themes/bootstrap/public/js/bootstrap.bundle.min.js.map +1 -0
  213. data/themes/custom/css/app.css +176 -0
  214. data/themes/magazine/css/app.css +242 -0
  215. data/themes/minimal/css/app.css +151 -0
  216. data/themes/notebook/css/app.css +205 -0
  217. data/themes/sand/css/app.css +179 -0
  218. data/vendor/d3/d3.v7.min.js +2 -0
  219. data/vendor/heroicons/16/solid/academic-cap.svg +4 -0
  220. data/vendor/heroicons/16/solid/adjustments-horizontal.svg +3 -0
  221. data/vendor/heroicons/16/solid/adjustments-vertical.svg +3 -0
  222. data/vendor/heroicons/16/solid/archive-box-arrow-down.svg +4 -0
  223. data/vendor/heroicons/16/solid/archive-box-x-mark.svg +4 -0
  224. data/vendor/heroicons/16/solid/archive-box.svg +4 -0
  225. data/vendor/heroicons/16/solid/arrow-down-circle.svg +3 -0
  226. data/vendor/heroicons/16/solid/arrow-down-left.svg +3 -0
  227. data/vendor/heroicons/16/solid/arrow-down-on-square-stack.svg +4 -0
  228. data/vendor/heroicons/16/solid/arrow-down-on-square.svg +3 -0
  229. data/vendor/heroicons/16/solid/arrow-down-right.svg +3 -0
  230. data/vendor/heroicons/16/solid/arrow-down-tray.svg +4 -0
  231. data/vendor/heroicons/16/solid/arrow-down.svg +3 -0
  232. data/vendor/heroicons/16/solid/arrow-left-circle.svg +3 -0
  233. data/vendor/heroicons/16/solid/arrow-left-end-on-rectangle.svg +4 -0
  234. data/vendor/heroicons/16/solid/arrow-left-start-on-rectangle.svg +3 -0
  235. data/vendor/heroicons/16/solid/arrow-left.svg +3 -0
  236. data/vendor/heroicons/16/solid/arrow-long-down.svg +3 -0
  237. data/vendor/heroicons/16/solid/arrow-long-left.svg +3 -0
  238. data/vendor/heroicons/16/solid/arrow-long-right.svg +3 -0
  239. data/vendor/heroicons/16/solid/arrow-long-up.svg +3 -0
  240. data/vendor/heroicons/16/solid/arrow-path-rounded-square.svg +3 -0
  241. data/vendor/heroicons/16/solid/arrow-path.svg +3 -0
  242. data/vendor/heroicons/16/solid/arrow-right-circle.svg +3 -0
  243. data/vendor/heroicons/16/solid/arrow-right-end-on-rectangle.svg +4 -0
  244. data/vendor/heroicons/16/solid/arrow-right-start-on-rectangle.svg +3 -0
  245. data/vendor/heroicons/16/solid/arrow-right.svg +3 -0
  246. data/vendor/heroicons/16/solid/arrow-top-right-on-square.svg +4 -0
  247. data/vendor/heroicons/16/solid/arrow-trending-down.svg +3 -0
  248. data/vendor/heroicons/16/solid/arrow-trending-up.svg +3 -0
  249. data/vendor/heroicons/16/solid/arrow-turn-down-left.svg +3 -0
  250. data/vendor/heroicons/16/solid/arrow-turn-down-right.svg +3 -0
  251. data/vendor/heroicons/16/solid/arrow-turn-left-down.svg +3 -0
  252. data/vendor/heroicons/16/solid/arrow-turn-left-up.svg +3 -0
  253. data/vendor/heroicons/16/solid/arrow-turn-right-down.svg +3 -0
  254. data/vendor/heroicons/16/solid/arrow-turn-right-up.svg +3 -0
  255. data/vendor/heroicons/16/solid/arrow-turn-up-left.svg +3 -0
  256. data/vendor/heroicons/16/solid/arrow-turn-up-right.svg +3 -0
  257. data/vendor/heroicons/16/solid/arrow-up-circle.svg +3 -0
  258. data/vendor/heroicons/16/solid/arrow-up-left.svg +3 -0
  259. data/vendor/heroicons/16/solid/arrow-up-on-square-stack.svg +4 -0
  260. data/vendor/heroicons/16/solid/arrow-up-on-square.svg +3 -0
  261. data/vendor/heroicons/16/solid/arrow-up-right.svg +3 -0
  262. data/vendor/heroicons/16/solid/arrow-up-tray.svg +4 -0
  263. data/vendor/heroicons/16/solid/arrow-up.svg +3 -0
  264. data/vendor/heroicons/16/solid/arrow-uturn-down.svg +3 -0
  265. data/vendor/heroicons/16/solid/arrow-uturn-left.svg +3 -0
  266. data/vendor/heroicons/16/solid/arrow-uturn-right.svg +3 -0
  267. data/vendor/heroicons/16/solid/arrow-uturn-up.svg +3 -0
  268. data/vendor/heroicons/16/solid/arrows-pointing-in.svg +3 -0
  269. data/vendor/heroicons/16/solid/arrows-pointing-out.svg +3 -0
  270. data/vendor/heroicons/16/solid/arrows-right-left.svg +3 -0
  271. data/vendor/heroicons/16/solid/arrows-up-down.svg +3 -0
  272. data/vendor/heroicons/16/solid/at-symbol.svg +3 -0
  273. data/vendor/heroicons/16/solid/backspace.svg +3 -0
  274. data/vendor/heroicons/16/solid/backward.svg +3 -0
  275. data/vendor/heroicons/16/solid/banknotes.svg +4 -0
  276. data/vendor/heroicons/16/solid/bars-2.svg +3 -0
  277. data/vendor/heroicons/16/solid/bars-3-bottom-left.svg +3 -0
  278. data/vendor/heroicons/16/solid/bars-3-bottom-right.svg +3 -0
  279. data/vendor/heroicons/16/solid/bars-3-center-left.svg +3 -0
  280. data/vendor/heroicons/16/solid/bars-3.svg +3 -0
  281. data/vendor/heroicons/16/solid/bars-4.svg +3 -0
  282. data/vendor/heroicons/16/solid/bars-arrow-down.svg +3 -0
  283. data/vendor/heroicons/16/solid/bars-arrow-up.svg +3 -0
  284. data/vendor/heroicons/16/solid/battery-0.svg +3 -0
  285. data/vendor/heroicons/16/solid/battery-100.svg +4 -0
  286. data/vendor/heroicons/16/solid/battery-50.svg +4 -0
  287. data/vendor/heroicons/16/solid/beaker.svg +3 -0
  288. data/vendor/heroicons/16/solid/bell-alert.svg +4 -0
  289. data/vendor/heroicons/16/solid/bell-slash.svg +4 -0
  290. data/vendor/heroicons/16/solid/bell-snooze.svg +3 -0
  291. data/vendor/heroicons/16/solid/bell.svg +3 -0
  292. data/vendor/heroicons/16/solid/bold.svg +3 -0
  293. data/vendor/heroicons/16/solid/bolt-slash.svg +3 -0
  294. data/vendor/heroicons/16/solid/bolt.svg +3 -0
  295. data/vendor/heroicons/16/solid/book-open.svg +3 -0
  296. data/vendor/heroicons/16/solid/bookmark-slash.svg +3 -0
  297. data/vendor/heroicons/16/solid/bookmark-square.svg +3 -0
  298. data/vendor/heroicons/16/solid/bookmark.svg +3 -0
  299. data/vendor/heroicons/16/solid/briefcase.svg +4 -0
  300. data/vendor/heroicons/16/solid/bug-ant.svg +3 -0
  301. data/vendor/heroicons/16/solid/building-library.svg +3 -0
  302. data/vendor/heroicons/16/solid/building-office-2.svg +3 -0
  303. data/vendor/heroicons/16/solid/building-office.svg +3 -0
  304. data/vendor/heroicons/16/solid/building-storefront.svg +3 -0
  305. data/vendor/heroicons/16/solid/cake.svg +3 -0
  306. data/vendor/heroicons/16/solid/calculator.svg +3 -0
  307. data/vendor/heroicons/16/solid/calendar-date-range.svg +4 -0
  308. data/vendor/heroicons/16/solid/calendar-days.svg +4 -0
  309. data/vendor/heroicons/16/solid/calendar.svg +3 -0
  310. data/vendor/heroicons/16/solid/camera.svg +4 -0
  311. data/vendor/heroicons/16/solid/chart-bar-square.svg +3 -0
  312. data/vendor/heroicons/16/solid/chart-bar.svg +3 -0
  313. data/vendor/heroicons/16/solid/chart-pie.svg +4 -0
  314. data/vendor/heroicons/16/solid/chat-bubble-bottom-center-text.svg +3 -0
  315. data/vendor/heroicons/16/solid/chat-bubble-bottom-center.svg +3 -0
  316. data/vendor/heroicons/16/solid/chat-bubble-left-ellipsis.svg +3 -0
  317. data/vendor/heroicons/16/solid/chat-bubble-left-right.svg +4 -0
  318. data/vendor/heroicons/16/solid/chat-bubble-left.svg +3 -0
  319. data/vendor/heroicons/16/solid/chat-bubble-oval-left-ellipsis.svg +3 -0
  320. data/vendor/heroicons/16/solid/chat-bubble-oval-left.svg +3 -0
  321. data/vendor/heroicons/16/solid/check-badge.svg +3 -0
  322. data/vendor/heroicons/16/solid/check-circle.svg +3 -0
  323. data/vendor/heroicons/16/solid/check.svg +3 -0
  324. data/vendor/heroicons/16/solid/chevron-double-down.svg +3 -0
  325. data/vendor/heroicons/16/solid/chevron-double-left.svg +3 -0
  326. data/vendor/heroicons/16/solid/chevron-double-right.svg +3 -0
  327. data/vendor/heroicons/16/solid/chevron-double-up.svg +3 -0
  328. data/vendor/heroicons/16/solid/chevron-down.svg +3 -0
  329. data/vendor/heroicons/16/solid/chevron-left.svg +3 -0
  330. data/vendor/heroicons/16/solid/chevron-right.svg +3 -0
  331. data/vendor/heroicons/16/solid/chevron-up-down.svg +3 -0
  332. data/vendor/heroicons/16/solid/chevron-up.svg +3 -0
  333. data/vendor/heroicons/16/solid/circle-stack.svg +5 -0
  334. data/vendor/heroicons/16/solid/clipboard-document-check.svg +4 -0
  335. data/vendor/heroicons/16/solid/clipboard-document-list.svg +4 -0
  336. data/vendor/heroicons/16/solid/clipboard-document.svg +4 -0
  337. data/vendor/heroicons/16/solid/clipboard.svg +3 -0
  338. data/vendor/heroicons/16/solid/clock.svg +3 -0
  339. data/vendor/heroicons/16/solid/cloud-arrow-down.svg +3 -0
  340. data/vendor/heroicons/16/solid/cloud-arrow-up.svg +3 -0
  341. data/vendor/heroicons/16/solid/cloud.svg +3 -0
  342. data/vendor/heroicons/16/solid/code-bracket-square.svg +3 -0
  343. data/vendor/heroicons/16/solid/code-bracket.svg +3 -0
  344. data/vendor/heroicons/16/solid/cog-6-tooth.svg +3 -0
  345. data/vendor/heroicons/16/solid/cog-8-tooth.svg +3 -0
  346. data/vendor/heroicons/16/solid/cog.svg +3 -0
  347. data/vendor/heroicons/16/solid/command-line.svg +3 -0
  348. data/vendor/heroicons/16/solid/computer-desktop.svg +3 -0
  349. data/vendor/heroicons/16/solid/cpu-chip.svg +4 -0
  350. data/vendor/heroicons/16/solid/credit-card.svg +4 -0
  351. data/vendor/heroicons/16/solid/cube-transparent.svg +3 -0
  352. data/vendor/heroicons/16/solid/cube.svg +3 -0
  353. data/vendor/heroicons/16/solid/currency-bangladeshi.svg +3 -0
  354. data/vendor/heroicons/16/solid/currency-dollar.svg +4 -0
  355. data/vendor/heroicons/16/solid/currency-euro.svg +3 -0
  356. data/vendor/heroicons/16/solid/currency-pound.svg +3 -0
  357. data/vendor/heroicons/16/solid/currency-rupee.svg +3 -0
  358. data/vendor/heroicons/16/solid/currency-yen.svg +3 -0
  359. data/vendor/heroicons/16/solid/cursor-arrow-rays.svg +3 -0
  360. data/vendor/heroicons/16/solid/cursor-arrow-ripple.svg +5 -0
  361. data/vendor/heroicons/16/solid/device-phone-mobile.svg +4 -0
  362. data/vendor/heroicons/16/solid/device-tablet.svg +4 -0
  363. data/vendor/heroicons/16/solid/divide.svg +4 -0
  364. data/vendor/heroicons/16/solid/document-arrow-down.svg +3 -0
  365. data/vendor/heroicons/16/solid/document-arrow-up.svg +3 -0
  366. data/vendor/heroicons/16/solid/document-chart-bar.svg +3 -0
  367. data/vendor/heroicons/16/solid/document-check.svg +3 -0
  368. data/vendor/heroicons/16/solid/document-currency-bangladeshi.svg +3 -0
  369. data/vendor/heroicons/16/solid/document-currency-dollar.svg +4 -0
  370. data/vendor/heroicons/16/solid/document-currency-euro.svg +3 -0
  371. data/vendor/heroicons/16/solid/document-currency-pound.svg +3 -0
  372. data/vendor/heroicons/16/solid/document-currency-rupee.svg +3 -0
  373. data/vendor/heroicons/16/solid/document-currency-yen.svg +3 -0
  374. data/vendor/heroicons/16/solid/document-duplicate.svg +4 -0
  375. data/vendor/heroicons/16/solid/document-magnifying-glass.svg +4 -0
  376. data/vendor/heroicons/16/solid/document-minus.svg +3 -0
  377. data/vendor/heroicons/16/solid/document-plus.svg +3 -0
  378. data/vendor/heroicons/16/solid/document-text.svg +3 -0
  379. data/vendor/heroicons/16/solid/document.svg +3 -0
  380. data/vendor/heroicons/16/solid/ellipsis-horizontal-circle.svg +3 -0
  381. data/vendor/heroicons/16/solid/ellipsis-horizontal.svg +3 -0
  382. data/vendor/heroicons/16/solid/ellipsis-vertical.svg +3 -0
  383. data/vendor/heroicons/16/solid/envelope-open.svg +3 -0
  384. data/vendor/heroicons/16/solid/envelope.svg +4 -0
  385. data/vendor/heroicons/16/solid/equals.svg +3 -0
  386. data/vendor/heroicons/16/solid/exclamation-circle.svg +3 -0
  387. data/vendor/heroicons/16/solid/exclamation-triangle.svg +3 -0
  388. data/vendor/heroicons/16/solid/eye-dropper.svg +3 -0
  389. data/vendor/heroicons/16/solid/eye-slash.svg +4 -0
  390. data/vendor/heroicons/16/solid/eye.svg +4 -0
  391. data/vendor/heroicons/16/solid/face-frown.svg +3 -0
  392. data/vendor/heroicons/16/solid/face-smile.svg +3 -0
  393. data/vendor/heroicons/16/solid/film.svg +3 -0
  394. data/vendor/heroicons/16/solid/finger-print.svg +3 -0
  395. data/vendor/heroicons/16/solid/fire.svg +3 -0
  396. data/vendor/heroicons/16/solid/flag.svg +3 -0
  397. data/vendor/heroicons/16/solid/folder-arrow-down.svg +3 -0
  398. data/vendor/heroicons/16/solid/folder-minus.svg +3 -0
  399. data/vendor/heroicons/16/solid/folder-open.svg +3 -0
  400. data/vendor/heroicons/16/solid/folder-plus.svg +3 -0
  401. data/vendor/heroicons/16/solid/folder.svg +3 -0
  402. data/vendor/heroicons/16/solid/forward.svg +3 -0
  403. data/vendor/heroicons/16/solid/funnel.svg +3 -0
  404. data/vendor/heroicons/16/solid/gif.svg +3 -0
  405. data/vendor/heroicons/16/solid/gift-top.svg +4 -0
  406. data/vendor/heroicons/16/solid/gift.svg +4 -0
  407. data/vendor/heroicons/16/solid/globe-alt.svg +3 -0
  408. data/vendor/heroicons/16/solid/globe-americas.svg +3 -0
  409. data/vendor/heroicons/16/solid/globe-asia-australia.svg +3 -0
  410. data/vendor/heroicons/16/solid/globe-europe-africa.svg +3 -0
  411. data/vendor/heroicons/16/solid/h1.svg +3 -0
  412. data/vendor/heroicons/16/solid/h2.svg +3 -0
  413. data/vendor/heroicons/16/solid/h3.svg +3 -0
  414. data/vendor/heroicons/16/solid/hand-raised.svg +3 -0
  415. data/vendor/heroicons/16/solid/hand-thumb-down.svg +3 -0
  416. data/vendor/heroicons/16/solid/hand-thumb-up.svg +3 -0
  417. data/vendor/heroicons/16/solid/hashtag.svg +3 -0
  418. data/vendor/heroicons/16/solid/heart.svg +3 -0
  419. data/vendor/heroicons/16/solid/home-modern.svg +3 -0
  420. data/vendor/heroicons/16/solid/home.svg +3 -0
  421. data/vendor/heroicons/16/solid/identification.svg +3 -0
  422. data/vendor/heroicons/16/solid/inbox-arrow-down.svg +4 -0
  423. data/vendor/heroicons/16/solid/inbox-stack.svg +4 -0
  424. data/vendor/heroicons/16/solid/inbox.svg +3 -0
  425. data/vendor/heroicons/16/solid/information-circle.svg +3 -0
  426. data/vendor/heroicons/16/solid/italic.svg +3 -0
  427. data/vendor/heroicons/16/solid/key.svg +3 -0
  428. data/vendor/heroicons/16/solid/language.svg +3 -0
  429. data/vendor/heroicons/16/solid/lifebuoy.svg +3 -0
  430. data/vendor/heroicons/16/solid/light-bulb.svg +3 -0
  431. data/vendor/heroicons/16/solid/link-slash.svg +3 -0
  432. data/vendor/heroicons/16/solid/link.svg +4 -0
  433. data/vendor/heroicons/16/solid/list-bullet.svg +3 -0
  434. data/vendor/heroicons/16/solid/lock-closed.svg +3 -0
  435. data/vendor/heroicons/16/solid/lock-open.svg +3 -0
  436. data/vendor/heroicons/16/solid/magnifying-glass-circle.svg +4 -0
  437. data/vendor/heroicons/16/solid/magnifying-glass-minus.svg +4 -0
  438. data/vendor/heroicons/16/solid/magnifying-glass-plus.svg +4 -0
  439. data/vendor/heroicons/16/solid/magnifying-glass.svg +3 -0
  440. data/vendor/heroicons/16/solid/map-pin.svg +3 -0
  441. data/vendor/heroicons/16/solid/map.svg +3 -0
  442. data/vendor/heroicons/16/solid/megaphone.svg +3 -0
  443. data/vendor/heroicons/16/solid/microphone.svg +4 -0
  444. data/vendor/heroicons/16/solid/minus-circle.svg +3 -0
  445. data/vendor/heroicons/16/solid/minus.svg +3 -0
  446. data/vendor/heroicons/16/solid/moon.svg +3 -0
  447. data/vendor/heroicons/16/solid/musical-note.svg +3 -0
  448. data/vendor/heroicons/16/solid/newspaper.svg +4 -0
  449. data/vendor/heroicons/16/solid/no-symbol.svg +3 -0
  450. data/vendor/heroicons/16/solid/numbered-list.svg +3 -0
  451. data/vendor/heroicons/16/solid/paint-brush.svg +3 -0
  452. data/vendor/heroicons/16/solid/paper-airplane.svg +3 -0
  453. data/vendor/heroicons/16/solid/paper-clip.svg +3 -0
  454. data/vendor/heroicons/16/solid/pause-circle.svg +3 -0
  455. data/vendor/heroicons/16/solid/pause.svg +3 -0
  456. data/vendor/heroicons/16/solid/pencil-square.svg +4 -0
  457. data/vendor/heroicons/16/solid/pencil.svg +3 -0
  458. data/vendor/heroicons/16/solid/percent-badge.svg +3 -0
  459. data/vendor/heroicons/16/solid/phone-arrow-down-left.svg +4 -0
  460. data/vendor/heroicons/16/solid/phone-arrow-up-right.svg +4 -0
  461. data/vendor/heroicons/16/solid/phone-x-mark.svg +4 -0
  462. data/vendor/heroicons/16/solid/phone.svg +3 -0
  463. data/vendor/heroicons/16/solid/photo.svg +3 -0
  464. data/vendor/heroicons/16/solid/play-circle.svg +3 -0
  465. data/vendor/heroicons/16/solid/play-pause.svg +3 -0
  466. data/vendor/heroicons/16/solid/play.svg +3 -0
  467. data/vendor/heroicons/16/solid/plus-circle.svg +3 -0
  468. data/vendor/heroicons/16/solid/plus.svg +3 -0
  469. data/vendor/heroicons/16/solid/power.svg +3 -0
  470. data/vendor/heroicons/16/solid/presentation-chart-bar.svg +3 -0
  471. data/vendor/heroicons/16/solid/presentation-chart-line.svg +3 -0
  472. data/vendor/heroicons/16/solid/printer.svg +3 -0
  473. data/vendor/heroicons/16/solid/puzzle-piece.svg +3 -0
  474. data/vendor/heroicons/16/solid/qr-code.svg +9 -0
  475. data/vendor/heroicons/16/solid/question-mark-circle.svg +3 -0
  476. data/vendor/heroicons/16/solid/queue-list.svg +3 -0
  477. data/vendor/heroicons/16/solid/radio.svg +3 -0
  478. data/vendor/heroicons/16/solid/receipt-percent.svg +3 -0
  479. data/vendor/heroicons/16/solid/receipt-refund.svg +3 -0
  480. data/vendor/heroicons/16/solid/rectangle-group.svg +3 -0
  481. data/vendor/heroicons/16/solid/rectangle-stack.svg +3 -0
  482. data/vendor/heroicons/16/solid/rocket-launch.svg +4 -0
  483. data/vendor/heroicons/16/solid/rss.svg +3 -0
  484. data/vendor/heroicons/16/solid/scale.svg +3 -0
  485. data/vendor/heroicons/16/solid/scissors.svg +4 -0
  486. data/vendor/heroicons/16/solid/server-stack.svg +4 -0
  487. data/vendor/heroicons/16/solid/server.svg +4 -0
  488. data/vendor/heroicons/16/solid/share.svg +3 -0
  489. data/vendor/heroicons/16/solid/shield-check.svg +3 -0
  490. data/vendor/heroicons/16/solid/shield-exclamation.svg +3 -0
  491. data/vendor/heroicons/16/solid/shopping-bag.svg +3 -0
  492. data/vendor/heroicons/16/solid/shopping-cart.svg +3 -0
  493. data/vendor/heroicons/16/solid/signal-slash.svg +3 -0
  494. data/vendor/heroicons/16/solid/signal.svg +5 -0
  495. data/vendor/heroicons/16/solid/slash.svg +3 -0
  496. data/vendor/heroicons/16/solid/sparkles.svg +3 -0
  497. data/vendor/heroicons/16/solid/speaker-wave.svg +4 -0
  498. data/vendor/heroicons/16/solid/speaker-x-mark.svg +3 -0
  499. data/vendor/heroicons/16/solid/square-2-stack.svg +4 -0
  500. data/vendor/heroicons/16/solid/square-3-stack-3d.svg +5 -0
  501. data/vendor/heroicons/16/solid/squares-2x2.svg +3 -0
  502. data/vendor/heroicons/16/solid/squares-plus.svg +3 -0
  503. data/vendor/heroicons/16/solid/star.svg +3 -0
  504. data/vendor/heroicons/16/solid/stop-circle.svg +3 -0
  505. data/vendor/heroicons/16/solid/stop.svg +3 -0
  506. data/vendor/heroicons/16/solid/strikethrough.svg +3 -0
  507. data/vendor/heroicons/16/solid/sun.svg +3 -0
  508. data/vendor/heroicons/16/solid/swatch.svg +4 -0
  509. data/vendor/heroicons/16/solid/table-cells.svg +3 -0
  510. data/vendor/heroicons/16/solid/tag.svg +3 -0
  511. data/vendor/heroicons/16/solid/ticket.svg +3 -0
  512. data/vendor/heroicons/16/solid/trash.svg +3 -0
  513. data/vendor/heroicons/16/solid/trophy.svg +3 -0
  514. data/vendor/heroicons/16/solid/truck.svg +5 -0
  515. data/vendor/heroicons/16/solid/tv.svg +4 -0
  516. data/vendor/heroicons/16/solid/underline.svg +3 -0
  517. data/vendor/heroicons/16/solid/user-circle.svg +3 -0
  518. data/vendor/heroicons/16/solid/user-group.svg +3 -0
  519. data/vendor/heroicons/16/solid/user-minus.svg +3 -0
  520. data/vendor/heroicons/16/solid/user-plus.svg +3 -0
  521. data/vendor/heroicons/16/solid/user.svg +3 -0
  522. data/vendor/heroicons/16/solid/users.svg +3 -0
  523. data/vendor/heroicons/16/solid/variable.svg +3 -0
  524. data/vendor/heroicons/16/solid/video-camera-slash.svg +3 -0
  525. data/vendor/heroicons/16/solid/video-camera.svg +3 -0
  526. data/vendor/heroicons/16/solid/view-columns.svg +3 -0
  527. data/vendor/heroicons/16/solid/viewfinder-circle.svg +3 -0
  528. data/vendor/heroicons/16/solid/wallet.svg +3 -0
  529. data/vendor/heroicons/16/solid/wifi.svg +3 -0
  530. data/vendor/heroicons/16/solid/window.svg +3 -0
  531. data/vendor/heroicons/16/solid/wrench-screwdriver.svg +4 -0
  532. data/vendor/heroicons/16/solid/wrench.svg +3 -0
  533. data/vendor/heroicons/16/solid/x-circle.svg +3 -0
  534. data/vendor/heroicons/16/solid/x-mark.svg +3 -0
  535. data/vendor/heroicons/20/solid/academic-cap.svg +3 -0
  536. data/vendor/heroicons/20/solid/adjustments-horizontal.svg +3 -0
  537. data/vendor/heroicons/20/solid/adjustments-vertical.svg +3 -0
  538. data/vendor/heroicons/20/solid/archive-box-arrow-down.svg +3 -0
  539. data/vendor/heroicons/20/solid/archive-box-x-mark.svg +4 -0
  540. data/vendor/heroicons/20/solid/archive-box.svg +4 -0
  541. data/vendor/heroicons/20/solid/arrow-down-circle.svg +3 -0
  542. data/vendor/heroicons/20/solid/arrow-down-left.svg +3 -0
  543. data/vendor/heroicons/20/solid/arrow-down-on-square-stack.svg +3 -0
  544. data/vendor/heroicons/20/solid/arrow-down-on-square.svg +3 -0
  545. data/vendor/heroicons/20/solid/arrow-down-right.svg +3 -0
  546. data/vendor/heroicons/20/solid/arrow-down-tray.svg +4 -0
  547. data/vendor/heroicons/20/solid/arrow-down.svg +3 -0
  548. data/vendor/heroicons/20/solid/arrow-left-circle.svg +3 -0
  549. data/vendor/heroicons/20/solid/arrow-left-end-on-rectangle.svg +4 -0
  550. data/vendor/heroicons/20/solid/arrow-left-on-rectangle.svg +4 -0
  551. data/vendor/heroicons/20/solid/arrow-left-start-on-rectangle.svg +4 -0
  552. data/vendor/heroicons/20/solid/arrow-left.svg +3 -0
  553. data/vendor/heroicons/20/solid/arrow-long-down.svg +3 -0
  554. data/vendor/heroicons/20/solid/arrow-long-left.svg +3 -0
  555. data/vendor/heroicons/20/solid/arrow-long-right.svg +3 -0
  556. data/vendor/heroicons/20/solid/arrow-long-up.svg +3 -0
  557. data/vendor/heroicons/20/solid/arrow-path-rounded-square.svg +3 -0
  558. data/vendor/heroicons/20/solid/arrow-path.svg +3 -0
  559. data/vendor/heroicons/20/solid/arrow-right-circle.svg +3 -0
  560. data/vendor/heroicons/20/solid/arrow-right-end-on-rectangle.svg +4 -0
  561. data/vendor/heroicons/20/solid/arrow-right-on-rectangle.svg +4 -0
  562. data/vendor/heroicons/20/solid/arrow-right-start-on-rectangle.svg +4 -0
  563. data/vendor/heroicons/20/solid/arrow-right.svg +3 -0
  564. data/vendor/heroicons/20/solid/arrow-small-down.svg +3 -0
  565. data/vendor/heroicons/20/solid/arrow-small-left.svg +3 -0
  566. data/vendor/heroicons/20/solid/arrow-small-right.svg +3 -0
  567. data/vendor/heroicons/20/solid/arrow-small-up.svg +3 -0
  568. data/vendor/heroicons/20/solid/arrow-top-right-on-square.svg +4 -0
  569. data/vendor/heroicons/20/solid/arrow-trending-down.svg +3 -0
  570. data/vendor/heroicons/20/solid/arrow-trending-up.svg +3 -0
  571. data/vendor/heroicons/20/solid/arrow-turn-down-left.svg +3 -0
  572. data/vendor/heroicons/20/solid/arrow-turn-down-right.svg +3 -0
  573. data/vendor/heroicons/20/solid/arrow-turn-left-down.svg +3 -0
  574. data/vendor/heroicons/20/solid/arrow-turn-left-up.svg +3 -0
  575. data/vendor/heroicons/20/solid/arrow-turn-right-down.svg +3 -0
  576. data/vendor/heroicons/20/solid/arrow-turn-right-up.svg +3 -0
  577. data/vendor/heroicons/20/solid/arrow-turn-up-left.svg +3 -0
  578. data/vendor/heroicons/20/solid/arrow-turn-up-right.svg +3 -0
  579. data/vendor/heroicons/20/solid/arrow-up-circle.svg +3 -0
  580. data/vendor/heroicons/20/solid/arrow-up-left.svg +3 -0
  581. data/vendor/heroicons/20/solid/arrow-up-on-square-stack.svg +3 -0
  582. data/vendor/heroicons/20/solid/arrow-up-on-square.svg +3 -0
  583. data/vendor/heroicons/20/solid/arrow-up-right.svg +3 -0
  584. data/vendor/heroicons/20/solid/arrow-up-tray.svg +4 -0
  585. data/vendor/heroicons/20/solid/arrow-up.svg +3 -0
  586. data/vendor/heroicons/20/solid/arrow-uturn-down.svg +3 -0
  587. data/vendor/heroicons/20/solid/arrow-uturn-left.svg +3 -0
  588. data/vendor/heroicons/20/solid/arrow-uturn-right.svg +3 -0
  589. data/vendor/heroicons/20/solid/arrow-uturn-up.svg +3 -0
  590. data/vendor/heroicons/20/solid/arrows-pointing-in.svg +3 -0
  591. data/vendor/heroicons/20/solid/arrows-pointing-out.svg +3 -0
  592. data/vendor/heroicons/20/solid/arrows-right-left.svg +3 -0
  593. data/vendor/heroicons/20/solid/arrows-up-down.svg +3 -0
  594. data/vendor/heroicons/20/solid/at-symbol.svg +3 -0
  595. data/vendor/heroicons/20/solid/backspace.svg +3 -0
  596. data/vendor/heroicons/20/solid/backward.svg +3 -0
  597. data/vendor/heroicons/20/solid/banknotes.svg +3 -0
  598. data/vendor/heroicons/20/solid/bars-2.svg +3 -0
  599. data/vendor/heroicons/20/solid/bars-3-bottom-left.svg +3 -0
  600. data/vendor/heroicons/20/solid/bars-3-bottom-right.svg +3 -0
  601. data/vendor/heroicons/20/solid/bars-3-center-left.svg +3 -0
  602. data/vendor/heroicons/20/solid/bars-3.svg +3 -0
  603. data/vendor/heroicons/20/solid/bars-4.svg +3 -0
  604. data/vendor/heroicons/20/solid/bars-arrow-down.svg +3 -0
  605. data/vendor/heroicons/20/solid/bars-arrow-up.svg +3 -0
  606. data/vendor/heroicons/20/solid/battery-0.svg +3 -0
  607. data/vendor/heroicons/20/solid/battery-100.svg +4 -0
  608. data/vendor/heroicons/20/solid/battery-50.svg +4 -0
  609. data/vendor/heroicons/20/solid/beaker.svg +3 -0
  610. data/vendor/heroicons/20/solid/bell-alert.svg +4 -0
  611. data/vendor/heroicons/20/solid/bell-slash.svg +3 -0
  612. data/vendor/heroicons/20/solid/bell-snooze.svg +3 -0
  613. data/vendor/heroicons/20/solid/bell.svg +3 -0
  614. data/vendor/heroicons/20/solid/bold.svg +3 -0
  615. data/vendor/heroicons/20/solid/bolt-slash.svg +4 -0
  616. data/vendor/heroicons/20/solid/bolt.svg +3 -0
  617. data/vendor/heroicons/20/solid/book-open.svg +3 -0
  618. data/vendor/heroicons/20/solid/bookmark-slash.svg +3 -0
  619. data/vendor/heroicons/20/solid/bookmark-square.svg +3 -0
  620. data/vendor/heroicons/20/solid/bookmark.svg +3 -0
  621. data/vendor/heroicons/20/solid/briefcase.svg +4 -0
  622. data/vendor/heroicons/20/solid/bug-ant.svg +3 -0
  623. data/vendor/heroicons/20/solid/building-library.svg +3 -0
  624. data/vendor/heroicons/20/solid/building-office-2.svg +3 -0
  625. data/vendor/heroicons/20/solid/building-office.svg +3 -0
  626. data/vendor/heroicons/20/solid/building-storefront.svg +3 -0
  627. data/vendor/heroicons/20/solid/cake.svg +3 -0
  628. data/vendor/heroicons/20/solid/calculator.svg +3 -0
  629. data/vendor/heroicons/20/solid/calendar-date-range.svg +4 -0
  630. data/vendor/heroicons/20/solid/calendar-days.svg +4 -0
  631. data/vendor/heroicons/20/solid/calendar.svg +3 -0
  632. data/vendor/heroicons/20/solid/camera.svg +3 -0
  633. data/vendor/heroicons/20/solid/chart-bar-square.svg +3 -0
  634. data/vendor/heroicons/20/solid/chart-bar.svg +3 -0
  635. data/vendor/heroicons/20/solid/chart-pie.svg +4 -0
  636. data/vendor/heroicons/20/solid/chat-bubble-bottom-center-text.svg +3 -0
  637. data/vendor/heroicons/20/solid/chat-bubble-bottom-center.svg +3 -0
  638. data/vendor/heroicons/20/solid/chat-bubble-left-ellipsis.svg +3 -0
  639. data/vendor/heroicons/20/solid/chat-bubble-left-right.svg +4 -0
  640. data/vendor/heroicons/20/solid/chat-bubble-left.svg +3 -0
  641. data/vendor/heroicons/20/solid/chat-bubble-oval-left-ellipsis.svg +3 -0
  642. data/vendor/heroicons/20/solid/chat-bubble-oval-left.svg +3 -0
  643. data/vendor/heroicons/20/solid/check-badge.svg +3 -0
  644. data/vendor/heroicons/20/solid/check-circle.svg +3 -0
  645. data/vendor/heroicons/20/solid/check.svg +3 -0
  646. data/vendor/heroicons/20/solid/chevron-double-down.svg +3 -0
  647. data/vendor/heroicons/20/solid/chevron-double-left.svg +3 -0
  648. data/vendor/heroicons/20/solid/chevron-double-right.svg +3 -0
  649. data/vendor/heroicons/20/solid/chevron-double-up.svg +3 -0
  650. data/vendor/heroicons/20/solid/chevron-down.svg +3 -0
  651. data/vendor/heroicons/20/solid/chevron-left.svg +3 -0
  652. data/vendor/heroicons/20/solid/chevron-right.svg +3 -0
  653. data/vendor/heroicons/20/solid/chevron-up-down.svg +3 -0
  654. data/vendor/heroicons/20/solid/chevron-up.svg +3 -0
  655. data/vendor/heroicons/20/solid/circle-stack.svg +3 -0
  656. data/vendor/heroicons/20/solid/clipboard-document-check.svg +4 -0
  657. data/vendor/heroicons/20/solid/clipboard-document-list.svg +4 -0
  658. data/vendor/heroicons/20/solid/clipboard-document.svg +4 -0
  659. data/vendor/heroicons/20/solid/clipboard.svg +3 -0
  660. data/vendor/heroicons/20/solid/clock.svg +3 -0
  661. data/vendor/heroicons/20/solid/cloud-arrow-down.svg +3 -0
  662. data/vendor/heroicons/20/solid/cloud-arrow-up.svg +3 -0
  663. data/vendor/heroicons/20/solid/cloud.svg +3 -0
  664. data/vendor/heroicons/20/solid/code-bracket-square.svg +3 -0
  665. data/vendor/heroicons/20/solid/code-bracket.svg +3 -0
  666. data/vendor/heroicons/20/solid/cog-6-tooth.svg +3 -0
  667. data/vendor/heroicons/20/solid/cog-8-tooth.svg +3 -0
  668. data/vendor/heroicons/20/solid/cog.svg +4 -0
  669. data/vendor/heroicons/20/solid/command-line.svg +3 -0
  670. data/vendor/heroicons/20/solid/computer-desktop.svg +3 -0
  671. data/vendor/heroicons/20/solid/cpu-chip.svg +4 -0
  672. data/vendor/heroicons/20/solid/credit-card.svg +3 -0
  673. data/vendor/heroicons/20/solid/cube-transparent.svg +3 -0
  674. data/vendor/heroicons/20/solid/cube.svg +3 -0
  675. data/vendor/heroicons/20/solid/currency-bangladeshi.svg +3 -0
  676. data/vendor/heroicons/20/solid/currency-dollar.svg +4 -0
  677. data/vendor/heroicons/20/solid/currency-euro.svg +3 -0
  678. data/vendor/heroicons/20/solid/currency-pound.svg +3 -0
  679. data/vendor/heroicons/20/solid/currency-rupee.svg +3 -0
  680. data/vendor/heroicons/20/solid/currency-yen.svg +3 -0
  681. data/vendor/heroicons/20/solid/cursor-arrow-rays.svg +3 -0
  682. data/vendor/heroicons/20/solid/cursor-arrow-ripple.svg +5 -0
  683. data/vendor/heroicons/20/solid/device-phone-mobile.svg +4 -0
  684. data/vendor/heroicons/20/solid/device-tablet.svg +3 -0
  685. data/vendor/heroicons/20/solid/divide.svg +3 -0
  686. data/vendor/heroicons/20/solid/document-arrow-down.svg +3 -0
  687. data/vendor/heroicons/20/solid/document-arrow-up.svg +3 -0
  688. data/vendor/heroicons/20/solid/document-chart-bar.svg +3 -0
  689. data/vendor/heroicons/20/solid/document-check.svg +3 -0
  690. data/vendor/heroicons/20/solid/document-currency-bangladeshi.svg +3 -0
  691. data/vendor/heroicons/20/solid/document-currency-dollar.svg +3 -0
  692. data/vendor/heroicons/20/solid/document-currency-euro.svg +3 -0
  693. data/vendor/heroicons/20/solid/document-currency-pound.svg +3 -0
  694. data/vendor/heroicons/20/solid/document-currency-rupee.svg +3 -0
  695. data/vendor/heroicons/20/solid/document-currency-yen.svg +3 -0
  696. data/vendor/heroicons/20/solid/document-duplicate.svg +4 -0
  697. data/vendor/heroicons/20/solid/document-magnifying-glass.svg +4 -0
  698. data/vendor/heroicons/20/solid/document-minus.svg +3 -0
  699. data/vendor/heroicons/20/solid/document-plus.svg +3 -0
  700. data/vendor/heroicons/20/solid/document-text.svg +3 -0
  701. data/vendor/heroicons/20/solid/document.svg +3 -0
  702. data/vendor/heroicons/20/solid/ellipsis-horizontal-circle.svg +3 -0
  703. data/vendor/heroicons/20/solid/ellipsis-horizontal.svg +3 -0
  704. data/vendor/heroicons/20/solid/ellipsis-vertical.svg +3 -0
  705. data/vendor/heroicons/20/solid/envelope-open.svg +3 -0
  706. data/vendor/heroicons/20/solid/envelope.svg +4 -0
  707. data/vendor/heroicons/20/solid/equals.svg +3 -0
  708. data/vendor/heroicons/20/solid/exclamation-circle.svg +3 -0
  709. data/vendor/heroicons/20/solid/exclamation-triangle.svg +3 -0
  710. data/vendor/heroicons/20/solid/eye-dropper.svg +3 -0
  711. data/vendor/heroicons/20/solid/eye-slash.svg +4 -0
  712. data/vendor/heroicons/20/solid/eye.svg +4 -0
  713. data/vendor/heroicons/20/solid/face-frown.svg +3 -0
  714. data/vendor/heroicons/20/solid/face-smile.svg +3 -0
  715. data/vendor/heroicons/20/solid/film.svg +3 -0
  716. data/vendor/heroicons/20/solid/finger-print.svg +3 -0
  717. data/vendor/heroicons/20/solid/fire.svg +3 -0
  718. data/vendor/heroicons/20/solid/flag.svg +3 -0
  719. data/vendor/heroicons/20/solid/folder-arrow-down.svg +3 -0
  720. data/vendor/heroicons/20/solid/folder-minus.svg +3 -0
  721. data/vendor/heroicons/20/solid/folder-open.svg +3 -0
  722. data/vendor/heroicons/20/solid/folder-plus.svg +3 -0
  723. data/vendor/heroicons/20/solid/folder.svg +3 -0
  724. data/vendor/heroicons/20/solid/forward.svg +3 -0
  725. data/vendor/heroicons/20/solid/funnel.svg +3 -0
  726. data/vendor/heroicons/20/solid/gif.svg +3 -0
  727. data/vendor/heroicons/20/solid/gift-top.svg +3 -0
  728. data/vendor/heroicons/20/solid/gift.svg +4 -0
  729. data/vendor/heroicons/20/solid/globe-alt.svg +3 -0
  730. data/vendor/heroicons/20/solid/globe-americas.svg +3 -0
  731. data/vendor/heroicons/20/solid/globe-asia-australia.svg +3 -0
  732. data/vendor/heroicons/20/solid/globe-europe-africa.svg +3 -0
  733. data/vendor/heroicons/20/solid/h1.svg +3 -0
  734. data/vendor/heroicons/20/solid/h2.svg +3 -0
  735. data/vendor/heroicons/20/solid/h3.svg +3 -0
  736. data/vendor/heroicons/20/solid/hand-raised.svg +3 -0
  737. data/vendor/heroicons/20/solid/hand-thumb-down.svg +3 -0
  738. data/vendor/heroicons/20/solid/hand-thumb-up.svg +3 -0
  739. data/vendor/heroicons/20/solid/hashtag.svg +3 -0
  740. data/vendor/heroicons/20/solid/heart.svg +3 -0
  741. data/vendor/heroicons/20/solid/home-modern.svg +3 -0
  742. data/vendor/heroicons/20/solid/home.svg +3 -0
  743. data/vendor/heroicons/20/solid/identification.svg +3 -0
  744. data/vendor/heroicons/20/solid/inbox-arrow-down.svg +4 -0
  745. data/vendor/heroicons/20/solid/inbox-stack.svg +4 -0
  746. data/vendor/heroicons/20/solid/inbox.svg +3 -0
  747. data/vendor/heroicons/20/solid/information-circle.svg +3 -0
  748. data/vendor/heroicons/20/solid/italic.svg +3 -0
  749. data/vendor/heroicons/20/solid/key.svg +3 -0
  750. data/vendor/heroicons/20/solid/language.svg +4 -0
  751. data/vendor/heroicons/20/solid/lifebuoy.svg +3 -0
  752. data/vendor/heroicons/20/solid/light-bulb.svg +3 -0
  753. data/vendor/heroicons/20/solid/link-slash.svg +3 -0
  754. data/vendor/heroicons/20/solid/link.svg +4 -0
  755. data/vendor/heroicons/20/solid/list-bullet.svg +3 -0
  756. data/vendor/heroicons/20/solid/lock-closed.svg +3 -0
  757. data/vendor/heroicons/20/solid/lock-open.svg +3 -0
  758. data/vendor/heroicons/20/solid/magnifying-glass-circle.svg +4 -0
  759. data/vendor/heroicons/20/solid/magnifying-glass-minus.svg +4 -0
  760. data/vendor/heroicons/20/solid/magnifying-glass-plus.svg +4 -0
  761. data/vendor/heroicons/20/solid/magnifying-glass.svg +3 -0
  762. data/vendor/heroicons/20/solid/map-pin.svg +3 -0
  763. data/vendor/heroicons/20/solid/map.svg +3 -0
  764. data/vendor/heroicons/20/solid/megaphone.svg +3 -0
  765. data/vendor/heroicons/20/solid/microphone.svg +4 -0
  766. data/vendor/heroicons/20/solid/minus-circle.svg +3 -0
  767. data/vendor/heroicons/20/solid/minus-small.svg +3 -0
  768. data/vendor/heroicons/20/solid/minus.svg +3 -0
  769. data/vendor/heroicons/20/solid/moon.svg +3 -0
  770. data/vendor/heroicons/20/solid/musical-note.svg +3 -0
  771. data/vendor/heroicons/20/solid/newspaper.svg +4 -0
  772. data/vendor/heroicons/20/solid/no-symbol.svg +3 -0
  773. data/vendor/heroicons/20/solid/numbered-list.svg +3 -0
  774. data/vendor/heroicons/20/solid/paint-brush.svg +3 -0
  775. data/vendor/heroicons/20/solid/paper-airplane.svg +3 -0
  776. data/vendor/heroicons/20/solid/paper-clip.svg +3 -0
  777. data/vendor/heroicons/20/solid/pause-circle.svg +3 -0
  778. data/vendor/heroicons/20/solid/pause.svg +3 -0
  779. data/vendor/heroicons/20/solid/pencil-square.svg +4 -0
  780. data/vendor/heroicons/20/solid/pencil.svg +3 -0
  781. data/vendor/heroicons/20/solid/percent-badge.svg +3 -0
  782. data/vendor/heroicons/20/solid/phone-arrow-down-left.svg +3 -0
  783. data/vendor/heroicons/20/solid/phone-arrow-up-right.svg +3 -0
  784. data/vendor/heroicons/20/solid/phone-x-mark.svg +3 -0
  785. data/vendor/heroicons/20/solid/phone.svg +3 -0
  786. data/vendor/heroicons/20/solid/photo.svg +3 -0
  787. data/vendor/heroicons/20/solid/play-circle.svg +3 -0
  788. data/vendor/heroicons/20/solid/play-pause.svg +3 -0
  789. data/vendor/heroicons/20/solid/play.svg +3 -0
  790. data/vendor/heroicons/20/solid/plus-circle.svg +3 -0
  791. data/vendor/heroicons/20/solid/plus-small.svg +3 -0
  792. data/vendor/heroicons/20/solid/plus.svg +3 -0
  793. data/vendor/heroicons/20/solid/power.svg +3 -0
  794. data/vendor/heroicons/20/solid/presentation-chart-bar.svg +3 -0
  795. data/vendor/heroicons/20/solid/presentation-chart-line.svg +3 -0
  796. data/vendor/heroicons/20/solid/printer.svg +3 -0
  797. data/vendor/heroicons/20/solid/puzzle-piece.svg +3 -0
  798. data/vendor/heroicons/20/solid/qr-code.svg +3 -0
  799. data/vendor/heroicons/20/solid/question-mark-circle.svg +3 -0
  800. data/vendor/heroicons/20/solid/queue-list.svg +3 -0
  801. data/vendor/heroicons/20/solid/radio.svg +3 -0
  802. data/vendor/heroicons/20/solid/receipt-percent.svg +3 -0
  803. data/vendor/heroicons/20/solid/receipt-refund.svg +3 -0
  804. data/vendor/heroicons/20/solid/rectangle-group.svg +3 -0
  805. data/vendor/heroicons/20/solid/rectangle-stack.svg +3 -0
  806. data/vendor/heroicons/20/solid/rocket-launch.svg +4 -0
  807. data/vendor/heroicons/20/solid/rss.svg +4 -0
  808. data/vendor/heroicons/20/solid/scale.svg +3 -0
  809. data/vendor/heroicons/20/solid/scissors.svg +4 -0
  810. data/vendor/heroicons/20/solid/server-stack.svg +4 -0
  811. data/vendor/heroicons/20/solid/server.svg +4 -0
  812. data/vendor/heroicons/20/solid/share.svg +3 -0
  813. data/vendor/heroicons/20/solid/shield-check.svg +3 -0
  814. data/vendor/heroicons/20/solid/shield-exclamation.svg +3 -0
  815. data/vendor/heroicons/20/solid/shopping-bag.svg +3 -0
  816. data/vendor/heroicons/20/solid/shopping-cart.svg +3 -0
  817. data/vendor/heroicons/20/solid/signal-slash.svg +3 -0
  818. data/vendor/heroicons/20/solid/signal.svg +4 -0
  819. data/vendor/heroicons/20/solid/slash.svg +3 -0
  820. data/vendor/heroicons/20/solid/sparkles.svg +3 -0
  821. data/vendor/heroicons/20/solid/speaker-wave.svg +4 -0
  822. data/vendor/heroicons/20/solid/speaker-x-mark.svg +3 -0
  823. data/vendor/heroicons/20/solid/square-2-stack.svg +4 -0
  824. data/vendor/heroicons/20/solid/square-3-stack-3d.svg +5 -0
  825. data/vendor/heroicons/20/solid/squares-2x2.svg +3 -0
  826. data/vendor/heroicons/20/solid/squares-plus.svg +3 -0
  827. data/vendor/heroicons/20/solid/star.svg +3 -0
  828. data/vendor/heroicons/20/solid/stop-circle.svg +3 -0
  829. data/vendor/heroicons/20/solid/stop.svg +3 -0
  830. data/vendor/heroicons/20/solid/strikethrough.svg +3 -0
  831. data/vendor/heroicons/20/solid/sun.svg +3 -0
  832. data/vendor/heroicons/20/solid/swatch.svg +3 -0
  833. data/vendor/heroicons/20/solid/table-cells.svg +3 -0
  834. data/vendor/heroicons/20/solid/tag.svg +3 -0
  835. data/vendor/heroicons/20/solid/ticket.svg +3 -0
  836. data/vendor/heroicons/20/solid/trash.svg +3 -0
  837. data/vendor/heroicons/20/solid/trophy.svg +3 -0
  838. data/vendor/heroicons/20/solid/truck.svg +4 -0
  839. data/vendor/heroicons/20/solid/tv.svg +4 -0
  840. data/vendor/heroicons/20/solid/underline.svg +3 -0
  841. data/vendor/heroicons/20/solid/user-circle.svg +3 -0
  842. data/vendor/heroicons/20/solid/user-group.svg +3 -0
  843. data/vendor/heroicons/20/solid/user-minus.svg +3 -0
  844. data/vendor/heroicons/20/solid/user-plus.svg +3 -0
  845. data/vendor/heroicons/20/solid/user.svg +3 -0
  846. data/vendor/heroicons/20/solid/users.svg +3 -0
  847. data/vendor/heroicons/20/solid/variable.svg +3 -0
  848. data/vendor/heroicons/20/solid/video-camera-slash.svg +3 -0
  849. data/vendor/heroicons/20/solid/video-camera.svg +3 -0
  850. data/vendor/heroicons/20/solid/view-columns.svg +3 -0
  851. data/vendor/heroicons/20/solid/viewfinder-circle.svg +3 -0
  852. data/vendor/heroicons/20/solid/wallet.svg +3 -0
  853. data/vendor/heroicons/20/solid/wifi.svg +3 -0
  854. data/vendor/heroicons/20/solid/window.svg +3 -0
  855. data/vendor/heroicons/20/solid/wrench-screwdriver.svg +4 -0
  856. data/vendor/heroicons/20/solid/wrench.svg +3 -0
  857. data/vendor/heroicons/20/solid/x-circle.svg +3 -0
  858. data/vendor/heroicons/20/solid/x-mark.svg +3 -0
  859. data/vendor/heroicons/24/outline/academic-cap.svg +3 -0
  860. data/vendor/heroicons/24/outline/adjustments-horizontal.svg +3 -0
  861. data/vendor/heroicons/24/outline/adjustments-vertical.svg +3 -0
  862. data/vendor/heroicons/24/outline/archive-box-arrow-down.svg +3 -0
  863. data/vendor/heroicons/24/outline/archive-box-x-mark.svg +3 -0
  864. data/vendor/heroicons/24/outline/archive-box.svg +3 -0
  865. data/vendor/heroicons/24/outline/arrow-down-circle.svg +3 -0
  866. data/vendor/heroicons/24/outline/arrow-down-left.svg +3 -0
  867. data/vendor/heroicons/24/outline/arrow-down-on-square-stack.svg +3 -0
  868. data/vendor/heroicons/24/outline/arrow-down-on-square.svg +3 -0
  869. data/vendor/heroicons/24/outline/arrow-down-right.svg +3 -0
  870. data/vendor/heroicons/24/outline/arrow-down-tray.svg +3 -0
  871. data/vendor/heroicons/24/outline/arrow-down.svg +3 -0
  872. data/vendor/heroicons/24/outline/arrow-left-circle.svg +3 -0
  873. data/vendor/heroicons/24/outline/arrow-left-end-on-rectangle.svg +3 -0
  874. data/vendor/heroicons/24/outline/arrow-left-on-rectangle.svg +3 -0
  875. data/vendor/heroicons/24/outline/arrow-left-start-on-rectangle.svg +3 -0
  876. data/vendor/heroicons/24/outline/arrow-left.svg +3 -0
  877. data/vendor/heroicons/24/outline/arrow-long-down.svg +3 -0
  878. data/vendor/heroicons/24/outline/arrow-long-left.svg +3 -0
  879. data/vendor/heroicons/24/outline/arrow-long-right.svg +3 -0
  880. data/vendor/heroicons/24/outline/arrow-long-up.svg +3 -0
  881. data/vendor/heroicons/24/outline/arrow-path-rounded-square.svg +3 -0
  882. data/vendor/heroicons/24/outline/arrow-path.svg +3 -0
  883. data/vendor/heroicons/24/outline/arrow-right-circle.svg +3 -0
  884. data/vendor/heroicons/24/outline/arrow-right-end-on-rectangle.svg +3 -0
  885. data/vendor/heroicons/24/outline/arrow-right-on-rectangle.svg +3 -0
  886. data/vendor/heroicons/24/outline/arrow-right-start-on-rectangle.svg +3 -0
  887. data/vendor/heroicons/24/outline/arrow-right.svg +3 -0
  888. data/vendor/heroicons/24/outline/arrow-small-down.svg +3 -0
  889. data/vendor/heroicons/24/outline/arrow-small-left.svg +3 -0
  890. data/vendor/heroicons/24/outline/arrow-small-right.svg +3 -0
  891. data/vendor/heroicons/24/outline/arrow-small-up.svg +3 -0
  892. data/vendor/heroicons/24/outline/arrow-top-right-on-square.svg +3 -0
  893. data/vendor/heroicons/24/outline/arrow-trending-down.svg +3 -0
  894. data/vendor/heroicons/24/outline/arrow-trending-up.svg +3 -0
  895. data/vendor/heroicons/24/outline/arrow-turn-down-left.svg +3 -0
  896. data/vendor/heroicons/24/outline/arrow-turn-down-right.svg +3 -0
  897. data/vendor/heroicons/24/outline/arrow-turn-left-down.svg +3 -0
  898. data/vendor/heroicons/24/outline/arrow-turn-left-up.svg +3 -0
  899. data/vendor/heroicons/24/outline/arrow-turn-right-down.svg +3 -0
  900. data/vendor/heroicons/24/outline/arrow-turn-right-up.svg +3 -0
  901. data/vendor/heroicons/24/outline/arrow-turn-up-left.svg +3 -0
  902. data/vendor/heroicons/24/outline/arrow-turn-up-right.svg +3 -0
  903. data/vendor/heroicons/24/outline/arrow-up-circle.svg +3 -0
  904. data/vendor/heroicons/24/outline/arrow-up-left.svg +3 -0
  905. data/vendor/heroicons/24/outline/arrow-up-on-square-stack.svg +3 -0
  906. data/vendor/heroicons/24/outline/arrow-up-on-square.svg +3 -0
  907. data/vendor/heroicons/24/outline/arrow-up-right.svg +3 -0
  908. data/vendor/heroicons/24/outline/arrow-up-tray.svg +3 -0
  909. data/vendor/heroicons/24/outline/arrow-up.svg +3 -0
  910. data/vendor/heroicons/24/outline/arrow-uturn-down.svg +3 -0
  911. data/vendor/heroicons/24/outline/arrow-uturn-left.svg +3 -0
  912. data/vendor/heroicons/24/outline/arrow-uturn-right.svg +3 -0
  913. data/vendor/heroicons/24/outline/arrow-uturn-up.svg +3 -0
  914. data/vendor/heroicons/24/outline/arrows-pointing-in.svg +3 -0
  915. data/vendor/heroicons/24/outline/arrows-pointing-out.svg +3 -0
  916. data/vendor/heroicons/24/outline/arrows-right-left.svg +3 -0
  917. data/vendor/heroicons/24/outline/arrows-up-down.svg +3 -0
  918. data/vendor/heroicons/24/outline/at-symbol.svg +3 -0
  919. data/vendor/heroicons/24/outline/backspace.svg +3 -0
  920. data/vendor/heroicons/24/outline/backward.svg +3 -0
  921. data/vendor/heroicons/24/outline/banknotes.svg +3 -0
  922. data/vendor/heroicons/24/outline/bars-2.svg +3 -0
  923. data/vendor/heroicons/24/outline/bars-3-bottom-left.svg +3 -0
  924. data/vendor/heroicons/24/outline/bars-3-bottom-right.svg +3 -0
  925. data/vendor/heroicons/24/outline/bars-3-center-left.svg +3 -0
  926. data/vendor/heroicons/24/outline/bars-3.svg +3 -0
  927. data/vendor/heroicons/24/outline/bars-4.svg +3 -0
  928. data/vendor/heroicons/24/outline/bars-arrow-down.svg +3 -0
  929. data/vendor/heroicons/24/outline/bars-arrow-up.svg +3 -0
  930. data/vendor/heroicons/24/outline/battery-0.svg +3 -0
  931. data/vendor/heroicons/24/outline/battery-100.svg +3 -0
  932. data/vendor/heroicons/24/outline/battery-50.svg +3 -0
  933. data/vendor/heroicons/24/outline/beaker.svg +3 -0
  934. data/vendor/heroicons/24/outline/bell-alert.svg +3 -0
  935. data/vendor/heroicons/24/outline/bell-slash.svg +3 -0
  936. data/vendor/heroicons/24/outline/bell-snooze.svg +3 -0
  937. data/vendor/heroicons/24/outline/bell.svg +3 -0
  938. data/vendor/heroicons/24/outline/bold.svg +3 -0
  939. data/vendor/heroicons/24/outline/bolt-slash.svg +3 -0
  940. data/vendor/heroicons/24/outline/bolt.svg +3 -0
  941. data/vendor/heroicons/24/outline/book-open.svg +3 -0
  942. data/vendor/heroicons/24/outline/bookmark-slash.svg +3 -0
  943. data/vendor/heroicons/24/outline/bookmark-square.svg +3 -0
  944. data/vendor/heroicons/24/outline/bookmark.svg +3 -0
  945. data/vendor/heroicons/24/outline/briefcase.svg +3 -0
  946. data/vendor/heroicons/24/outline/bug-ant.svg +3 -0
  947. data/vendor/heroicons/24/outline/building-library.svg +3 -0
  948. data/vendor/heroicons/24/outline/building-office-2.svg +3 -0
  949. data/vendor/heroicons/24/outline/building-office.svg +3 -0
  950. data/vendor/heroicons/24/outline/building-storefront.svg +3 -0
  951. data/vendor/heroicons/24/outline/cake.svg +3 -0
  952. data/vendor/heroicons/24/outline/calculator.svg +3 -0
  953. data/vendor/heroicons/24/outline/calendar-date-range.svg +3 -0
  954. data/vendor/heroicons/24/outline/calendar-days.svg +3 -0
  955. data/vendor/heroicons/24/outline/calendar.svg +3 -0
  956. data/vendor/heroicons/24/outline/camera.svg +4 -0
  957. data/vendor/heroicons/24/outline/chart-bar-square.svg +3 -0
  958. data/vendor/heroicons/24/outline/chart-bar.svg +3 -0
  959. data/vendor/heroicons/24/outline/chart-pie.svg +4 -0
  960. data/vendor/heroicons/24/outline/chat-bubble-bottom-center-text.svg +3 -0
  961. data/vendor/heroicons/24/outline/chat-bubble-bottom-center.svg +3 -0
  962. data/vendor/heroicons/24/outline/chat-bubble-left-ellipsis.svg +3 -0
  963. data/vendor/heroicons/24/outline/chat-bubble-left-right.svg +3 -0
  964. data/vendor/heroicons/24/outline/chat-bubble-left.svg +3 -0
  965. data/vendor/heroicons/24/outline/chat-bubble-oval-left-ellipsis.svg +3 -0
  966. data/vendor/heroicons/24/outline/chat-bubble-oval-left.svg +3 -0
  967. data/vendor/heroicons/24/outline/check-badge.svg +3 -0
  968. data/vendor/heroicons/24/outline/check-circle.svg +3 -0
  969. data/vendor/heroicons/24/outline/check.svg +3 -0
  970. data/vendor/heroicons/24/outline/chevron-double-down.svg +3 -0
  971. data/vendor/heroicons/24/outline/chevron-double-left.svg +3 -0
  972. data/vendor/heroicons/24/outline/chevron-double-right.svg +3 -0
  973. data/vendor/heroicons/24/outline/chevron-double-up.svg +4 -0
  974. data/vendor/heroicons/24/outline/chevron-down.svg +3 -0
  975. data/vendor/heroicons/24/outline/chevron-left.svg +3 -0
  976. data/vendor/heroicons/24/outline/chevron-right.svg +3 -0
  977. data/vendor/heroicons/24/outline/chevron-up-down.svg +3 -0
  978. data/vendor/heroicons/24/outline/chevron-up.svg +3 -0
  979. data/vendor/heroicons/24/outline/circle-stack.svg +3 -0
  980. data/vendor/heroicons/24/outline/clipboard-document-check.svg +3 -0
  981. data/vendor/heroicons/24/outline/clipboard-document-list.svg +3 -0
  982. data/vendor/heroicons/24/outline/clipboard-document.svg +3 -0
  983. data/vendor/heroicons/24/outline/clipboard.svg +3 -0
  984. data/vendor/heroicons/24/outline/clock.svg +3 -0
  985. data/vendor/heroicons/24/outline/cloud-arrow-down.svg +3 -0
  986. data/vendor/heroicons/24/outline/cloud-arrow-up.svg +3 -0
  987. data/vendor/heroicons/24/outline/cloud.svg +3 -0
  988. data/vendor/heroicons/24/outline/code-bracket-square.svg +3 -0
  989. data/vendor/heroicons/24/outline/code-bracket.svg +3 -0
  990. data/vendor/heroicons/24/outline/cog-6-tooth.svg +4 -0
  991. data/vendor/heroicons/24/outline/cog-8-tooth.svg +4 -0
  992. data/vendor/heroicons/24/outline/cog.svg +3 -0
  993. data/vendor/heroicons/24/outline/command-line.svg +3 -0
  994. data/vendor/heroicons/24/outline/computer-desktop.svg +3 -0
  995. data/vendor/heroicons/24/outline/cpu-chip.svg +3 -0
  996. data/vendor/heroicons/24/outline/credit-card.svg +3 -0
  997. data/vendor/heroicons/24/outline/cube-transparent.svg +3 -0
  998. data/vendor/heroicons/24/outline/cube.svg +3 -0
  999. data/vendor/heroicons/24/outline/currency-bangladeshi.svg +3 -0
  1000. data/vendor/heroicons/24/outline/currency-dollar.svg +3 -0
  1001. data/vendor/heroicons/24/outline/currency-euro.svg +3 -0
  1002. data/vendor/heroicons/24/outline/currency-pound.svg +3 -0
  1003. data/vendor/heroicons/24/outline/currency-rupee.svg +3 -0
  1004. data/vendor/heroicons/24/outline/currency-yen.svg +3 -0
  1005. data/vendor/heroicons/24/outline/cursor-arrow-rays.svg +3 -0
  1006. data/vendor/heroicons/24/outline/cursor-arrow-ripple.svg +3 -0
  1007. data/vendor/heroicons/24/outline/device-phone-mobile.svg +3 -0
  1008. data/vendor/heroicons/24/outline/device-tablet.svg +3 -0
  1009. data/vendor/heroicons/24/outline/divide.svg +3 -0
  1010. data/vendor/heroicons/24/outline/document-arrow-down.svg +3 -0
  1011. data/vendor/heroicons/24/outline/document-arrow-up.svg +3 -0
  1012. data/vendor/heroicons/24/outline/document-chart-bar.svg +3 -0
  1013. data/vendor/heroicons/24/outline/document-check.svg +3 -0
  1014. data/vendor/heroicons/24/outline/document-currency-bangladeshi.svg +3 -0
  1015. data/vendor/heroicons/24/outline/document-currency-dollar.svg +3 -0
  1016. data/vendor/heroicons/24/outline/document-currency-euro.svg +3 -0
  1017. data/vendor/heroicons/24/outline/document-currency-pound.svg +3 -0
  1018. data/vendor/heroicons/24/outline/document-currency-rupee.svg +3 -0
  1019. data/vendor/heroicons/24/outline/document-currency-yen.svg +3 -0
  1020. data/vendor/heroicons/24/outline/document-duplicate.svg +3 -0
  1021. data/vendor/heroicons/24/outline/document-magnifying-glass.svg +3 -0
  1022. data/vendor/heroicons/24/outline/document-minus.svg +3 -0
  1023. data/vendor/heroicons/24/outline/document-plus.svg +3 -0
  1024. data/vendor/heroicons/24/outline/document-text.svg +3 -0
  1025. data/vendor/heroicons/24/outline/document.svg +3 -0
  1026. data/vendor/heroicons/24/outline/ellipsis-horizontal-circle.svg +3 -0
  1027. data/vendor/heroicons/24/outline/ellipsis-horizontal.svg +3 -0
  1028. data/vendor/heroicons/24/outline/ellipsis-vertical.svg +3 -0
  1029. data/vendor/heroicons/24/outline/envelope-open.svg +3 -0
  1030. data/vendor/heroicons/24/outline/envelope.svg +3 -0
  1031. data/vendor/heroicons/24/outline/equals.svg +3 -0
  1032. data/vendor/heroicons/24/outline/exclamation-circle.svg +3 -0
  1033. data/vendor/heroicons/24/outline/exclamation-triangle.svg +3 -0
  1034. data/vendor/heroicons/24/outline/eye-dropper.svg +3 -0
  1035. data/vendor/heroicons/24/outline/eye-slash.svg +3 -0
  1036. data/vendor/heroicons/24/outline/eye.svg +4 -0
  1037. data/vendor/heroicons/24/outline/face-frown.svg +3 -0
  1038. data/vendor/heroicons/24/outline/face-smile.svg +3 -0
  1039. data/vendor/heroicons/24/outline/film.svg +3 -0
  1040. data/vendor/heroicons/24/outline/finger-print.svg +3 -0
  1041. data/vendor/heroicons/24/outline/fire.svg +4 -0
  1042. data/vendor/heroicons/24/outline/flag.svg +3 -0
  1043. data/vendor/heroicons/24/outline/folder-arrow-down.svg +3 -0
  1044. data/vendor/heroicons/24/outline/folder-minus.svg +3 -0
  1045. data/vendor/heroicons/24/outline/folder-open.svg +3 -0
  1046. data/vendor/heroicons/24/outline/folder-plus.svg +3 -0
  1047. data/vendor/heroicons/24/outline/folder.svg +3 -0
  1048. data/vendor/heroicons/24/outline/forward.svg +3 -0
  1049. data/vendor/heroicons/24/outline/funnel.svg +3 -0
  1050. data/vendor/heroicons/24/outline/gif.svg +3 -0
  1051. data/vendor/heroicons/24/outline/gift-top.svg +3 -0
  1052. data/vendor/heroicons/24/outline/gift.svg +3 -0
  1053. data/vendor/heroicons/24/outline/globe-alt.svg +3 -0
  1054. data/vendor/heroicons/24/outline/globe-americas.svg +3 -0
  1055. data/vendor/heroicons/24/outline/globe-asia-australia.svg +3 -0
  1056. data/vendor/heroicons/24/outline/globe-europe-africa.svg +3 -0
  1057. data/vendor/heroicons/24/outline/h1.svg +3 -0
  1058. data/vendor/heroicons/24/outline/h2.svg +3 -0
  1059. data/vendor/heroicons/24/outline/h3.svg +3 -0
  1060. data/vendor/heroicons/24/outline/hand-raised.svg +3 -0
  1061. data/vendor/heroicons/24/outline/hand-thumb-down.svg +3 -0
  1062. data/vendor/heroicons/24/outline/hand-thumb-up.svg +3 -0
  1063. data/vendor/heroicons/24/outline/hashtag.svg +3 -0
  1064. data/vendor/heroicons/24/outline/heart.svg +3 -0
  1065. data/vendor/heroicons/24/outline/home-modern.svg +3 -0
  1066. data/vendor/heroicons/24/outline/home.svg +3 -0
  1067. data/vendor/heroicons/24/outline/identification.svg +3 -0
  1068. data/vendor/heroicons/24/outline/inbox-arrow-down.svg +3 -0
  1069. data/vendor/heroicons/24/outline/inbox-stack.svg +3 -0
  1070. data/vendor/heroicons/24/outline/inbox.svg +3 -0
  1071. data/vendor/heroicons/24/outline/information-circle.svg +3 -0
  1072. data/vendor/heroicons/24/outline/italic.svg +3 -0
  1073. data/vendor/heroicons/24/outline/key.svg +3 -0
  1074. data/vendor/heroicons/24/outline/language.svg +3 -0
  1075. data/vendor/heroicons/24/outline/lifebuoy.svg +3 -0
  1076. data/vendor/heroicons/24/outline/light-bulb.svg +3 -0
  1077. data/vendor/heroicons/24/outline/link-slash.svg +3 -0
  1078. data/vendor/heroicons/24/outline/link.svg +3 -0
  1079. data/vendor/heroicons/24/outline/list-bullet.svg +3 -0
  1080. data/vendor/heroicons/24/outline/lock-closed.svg +3 -0
  1081. data/vendor/heroicons/24/outline/lock-open.svg +3 -0
  1082. data/vendor/heroicons/24/outline/magnifying-glass-circle.svg +3 -0
  1083. data/vendor/heroicons/24/outline/magnifying-glass-minus.svg +3 -0
  1084. data/vendor/heroicons/24/outline/magnifying-glass-plus.svg +3 -0
  1085. data/vendor/heroicons/24/outline/magnifying-glass.svg +3 -0
  1086. data/vendor/heroicons/24/outline/map-pin.svg +4 -0
  1087. data/vendor/heroicons/24/outline/map.svg +3 -0
  1088. data/vendor/heroicons/24/outline/megaphone.svg +3 -0
  1089. data/vendor/heroicons/24/outline/microphone.svg +3 -0
  1090. data/vendor/heroicons/24/outline/minus-circle.svg +3 -0
  1091. data/vendor/heroicons/24/outline/minus-small.svg +3 -0
  1092. data/vendor/heroicons/24/outline/minus.svg +3 -0
  1093. data/vendor/heroicons/24/outline/moon.svg +3 -0
  1094. data/vendor/heroicons/24/outline/musical-note.svg +3 -0
  1095. data/vendor/heroicons/24/outline/newspaper.svg +3 -0
  1096. data/vendor/heroicons/24/outline/no-symbol.svg +3 -0
  1097. data/vendor/heroicons/24/outline/numbered-list.svg +3 -0
  1098. data/vendor/heroicons/24/outline/paint-brush.svg +3 -0
  1099. data/vendor/heroicons/24/outline/paper-airplane.svg +3 -0
  1100. data/vendor/heroicons/24/outline/paper-clip.svg +3 -0
  1101. data/vendor/heroicons/24/outline/pause-circle.svg +3 -0
  1102. data/vendor/heroicons/24/outline/pause.svg +3 -0
  1103. data/vendor/heroicons/24/outline/pencil-square.svg +3 -0
  1104. data/vendor/heroicons/24/outline/pencil.svg +3 -0
  1105. data/vendor/heroicons/24/outline/percent-badge.svg +3 -0
  1106. data/vendor/heroicons/24/outline/phone-arrow-down-left.svg +3 -0
  1107. data/vendor/heroicons/24/outline/phone-arrow-up-right.svg +3 -0
  1108. data/vendor/heroicons/24/outline/phone-x-mark.svg +3 -0
  1109. data/vendor/heroicons/24/outline/phone.svg +3 -0
  1110. data/vendor/heroicons/24/outline/photo.svg +3 -0
  1111. data/vendor/heroicons/24/outline/play-circle.svg +4 -0
  1112. data/vendor/heroicons/24/outline/play-pause.svg +3 -0
  1113. data/vendor/heroicons/24/outline/play.svg +3 -0
  1114. data/vendor/heroicons/24/outline/plus-circle.svg +3 -0
  1115. data/vendor/heroicons/24/outline/plus-small.svg +3 -0
  1116. data/vendor/heroicons/24/outline/plus.svg +3 -0
  1117. data/vendor/heroicons/24/outline/power.svg +3 -0
  1118. data/vendor/heroicons/24/outline/presentation-chart-bar.svg +3 -0
  1119. data/vendor/heroicons/24/outline/presentation-chart-line.svg +3 -0
  1120. data/vendor/heroicons/24/outline/printer.svg +3 -0
  1121. data/vendor/heroicons/24/outline/puzzle-piece.svg +3 -0
  1122. data/vendor/heroicons/24/outline/qr-code.svg +4 -0
  1123. data/vendor/heroicons/24/outline/question-mark-circle.svg +3 -0
  1124. data/vendor/heroicons/24/outline/queue-list.svg +3 -0
  1125. data/vendor/heroicons/24/outline/radio.svg +3 -0
  1126. data/vendor/heroicons/24/outline/receipt-percent.svg +3 -0
  1127. data/vendor/heroicons/24/outline/receipt-refund.svg +3 -0
  1128. data/vendor/heroicons/24/outline/rectangle-group.svg +3 -0
  1129. data/vendor/heroicons/24/outline/rectangle-stack.svg +3 -0
  1130. data/vendor/heroicons/24/outline/rocket-launch.svg +3 -0
  1131. data/vendor/heroicons/24/outline/rss.svg +3 -0
  1132. data/vendor/heroicons/24/outline/scale.svg +3 -0
  1133. data/vendor/heroicons/24/outline/scissors.svg +3 -0
  1134. data/vendor/heroicons/24/outline/server-stack.svg +3 -0
  1135. data/vendor/heroicons/24/outline/server.svg +3 -0
  1136. data/vendor/heroicons/24/outline/share.svg +3 -0
  1137. data/vendor/heroicons/24/outline/shield-check.svg +3 -0
  1138. data/vendor/heroicons/24/outline/shield-exclamation.svg +3 -0
  1139. data/vendor/heroicons/24/outline/shopping-bag.svg +3 -0
  1140. data/vendor/heroicons/24/outline/shopping-cart.svg +3 -0
  1141. data/vendor/heroicons/24/outline/signal-slash.svg +3 -0
  1142. data/vendor/heroicons/24/outline/signal.svg +3 -0
  1143. data/vendor/heroicons/24/outline/slash.svg +3 -0
  1144. data/vendor/heroicons/24/outline/sparkles.svg +3 -0
  1145. data/vendor/heroicons/24/outline/speaker-wave.svg +3 -0
  1146. data/vendor/heroicons/24/outline/speaker-x-mark.svg +3 -0
  1147. data/vendor/heroicons/24/outline/square-2-stack.svg +3 -0
  1148. data/vendor/heroicons/24/outline/square-3-stack-3d.svg +3 -0
  1149. data/vendor/heroicons/24/outline/squares-2x2.svg +3 -0
  1150. data/vendor/heroicons/24/outline/squares-plus.svg +3 -0
  1151. data/vendor/heroicons/24/outline/star.svg +3 -0
  1152. data/vendor/heroicons/24/outline/stop-circle.svg +4 -0
  1153. data/vendor/heroicons/24/outline/stop.svg +3 -0
  1154. data/vendor/heroicons/24/outline/strikethrough.svg +3 -0
  1155. data/vendor/heroicons/24/outline/sun.svg +3 -0
  1156. data/vendor/heroicons/24/outline/swatch.svg +3 -0
  1157. data/vendor/heroicons/24/outline/table-cells.svg +3 -0
  1158. data/vendor/heroicons/24/outline/tag.svg +4 -0
  1159. data/vendor/heroicons/24/outline/ticket.svg +3 -0
  1160. data/vendor/heroicons/24/outline/trash.svg +3 -0
  1161. data/vendor/heroicons/24/outline/trophy.svg +3 -0
  1162. data/vendor/heroicons/24/outline/truck.svg +3 -0
  1163. data/vendor/heroicons/24/outline/tv.svg +3 -0
  1164. data/vendor/heroicons/24/outline/underline.svg +3 -0
  1165. data/vendor/heroicons/24/outline/user-circle.svg +3 -0
  1166. data/vendor/heroicons/24/outline/user-group.svg +3 -0
  1167. data/vendor/heroicons/24/outline/user-minus.svg +3 -0
  1168. data/vendor/heroicons/24/outline/user-plus.svg +3 -0
  1169. data/vendor/heroicons/24/outline/user.svg +3 -0
  1170. data/vendor/heroicons/24/outline/users.svg +3 -0
  1171. data/vendor/heroicons/24/outline/variable.svg +3 -0
  1172. data/vendor/heroicons/24/outline/video-camera-slash.svg +3 -0
  1173. data/vendor/heroicons/24/outline/video-camera.svg +3 -0
  1174. data/vendor/heroicons/24/outline/view-columns.svg +3 -0
  1175. data/vendor/heroicons/24/outline/viewfinder-circle.svg +3 -0
  1176. data/vendor/heroicons/24/outline/wallet.svg +3 -0
  1177. data/vendor/heroicons/24/outline/wifi.svg +3 -0
  1178. data/vendor/heroicons/24/outline/window.svg +3 -0
  1179. data/vendor/heroicons/24/outline/wrench-screwdriver.svg +3 -0
  1180. data/vendor/heroicons/24/outline/wrench.svg +4 -0
  1181. data/vendor/heroicons/24/outline/x-circle.svg +3 -0
  1182. data/vendor/heroicons/24/outline/x-mark.svg +3 -0
  1183. data/vendor/heroicons/24/solid/academic-cap.svg +5 -0
  1184. data/vendor/heroicons/24/solid/adjustments-horizontal.svg +3 -0
  1185. data/vendor/heroicons/24/solid/adjustments-vertical.svg +3 -0
  1186. data/vendor/heroicons/24/solid/archive-box-arrow-down.svg +4 -0
  1187. data/vendor/heroicons/24/solid/archive-box-x-mark.svg +4 -0
  1188. data/vendor/heroicons/24/solid/archive-box.svg +4 -0
  1189. data/vendor/heroicons/24/solid/arrow-down-circle.svg +3 -0
  1190. data/vendor/heroicons/24/solid/arrow-down-left.svg +3 -0
  1191. data/vendor/heroicons/24/solid/arrow-down-on-square-stack.svg +4 -0
  1192. data/vendor/heroicons/24/solid/arrow-down-on-square.svg +3 -0
  1193. data/vendor/heroicons/24/solid/arrow-down-right.svg +3 -0
  1194. data/vendor/heroicons/24/solid/arrow-down-tray.svg +3 -0
  1195. data/vendor/heroicons/24/solid/arrow-down.svg +3 -0
  1196. data/vendor/heroicons/24/solid/arrow-left-circle.svg +3 -0
  1197. data/vendor/heroicons/24/solid/arrow-left-end-on-rectangle.svg +3 -0
  1198. data/vendor/heroicons/24/solid/arrow-left-on-rectangle.svg +3 -0
  1199. data/vendor/heroicons/24/solid/arrow-left-start-on-rectangle.svg +3 -0
  1200. data/vendor/heroicons/24/solid/arrow-left.svg +3 -0
  1201. data/vendor/heroicons/24/solid/arrow-long-down.svg +3 -0
  1202. data/vendor/heroicons/24/solid/arrow-long-left.svg +3 -0
  1203. data/vendor/heroicons/24/solid/arrow-long-right.svg +3 -0
  1204. data/vendor/heroicons/24/solid/arrow-long-up.svg +3 -0
  1205. data/vendor/heroicons/24/solid/arrow-path-rounded-square.svg +3 -0
  1206. data/vendor/heroicons/24/solid/arrow-path.svg +3 -0
  1207. data/vendor/heroicons/24/solid/arrow-right-circle.svg +3 -0
  1208. data/vendor/heroicons/24/solid/arrow-right-end-on-rectangle.svg +3 -0
  1209. data/vendor/heroicons/24/solid/arrow-right-on-rectangle.svg +3 -0
  1210. data/vendor/heroicons/24/solid/arrow-right-start-on-rectangle.svg +3 -0
  1211. data/vendor/heroicons/24/solid/arrow-right.svg +3 -0
  1212. data/vendor/heroicons/24/solid/arrow-small-down.svg +3 -0
  1213. data/vendor/heroicons/24/solid/arrow-small-left.svg +3 -0
  1214. data/vendor/heroicons/24/solid/arrow-small-right.svg +3 -0
  1215. data/vendor/heroicons/24/solid/arrow-small-up.svg +3 -0
  1216. data/vendor/heroicons/24/solid/arrow-top-right-on-square.svg +3 -0
  1217. data/vendor/heroicons/24/solid/arrow-trending-down.svg +3 -0
  1218. data/vendor/heroicons/24/solid/arrow-trending-up.svg +3 -0
  1219. data/vendor/heroicons/24/solid/arrow-turn-down-left.svg +3 -0
  1220. data/vendor/heroicons/24/solid/arrow-turn-down-right.svg +3 -0
  1221. data/vendor/heroicons/24/solid/arrow-turn-left-down.svg +3 -0
  1222. data/vendor/heroicons/24/solid/arrow-turn-left-up.svg +3 -0
  1223. data/vendor/heroicons/24/solid/arrow-turn-right-down.svg +3 -0
  1224. data/vendor/heroicons/24/solid/arrow-turn-right-up.svg +3 -0
  1225. data/vendor/heroicons/24/solid/arrow-turn-up-left.svg +3 -0
  1226. data/vendor/heroicons/24/solid/arrow-turn-up-right.svg +3 -0
  1227. data/vendor/heroicons/24/solid/arrow-up-circle.svg +3 -0
  1228. data/vendor/heroicons/24/solid/arrow-up-left.svg +3 -0
  1229. data/vendor/heroicons/24/solid/arrow-up-on-square-stack.svg +4 -0
  1230. data/vendor/heroicons/24/solid/arrow-up-on-square.svg +3 -0
  1231. data/vendor/heroicons/24/solid/arrow-up-right.svg +3 -0
  1232. data/vendor/heroicons/24/solid/arrow-up-tray.svg +3 -0
  1233. data/vendor/heroicons/24/solid/arrow-up.svg +3 -0
  1234. data/vendor/heroicons/24/solid/arrow-uturn-down.svg +3 -0
  1235. data/vendor/heroicons/24/solid/arrow-uturn-left.svg +3 -0
  1236. data/vendor/heroicons/24/solid/arrow-uturn-right.svg +3 -0
  1237. data/vendor/heroicons/24/solid/arrow-uturn-up.svg +3 -0
  1238. data/vendor/heroicons/24/solid/arrows-pointing-in.svg +3 -0
  1239. data/vendor/heroicons/24/solid/arrows-pointing-out.svg +3 -0
  1240. data/vendor/heroicons/24/solid/arrows-right-left.svg +3 -0
  1241. data/vendor/heroicons/24/solid/arrows-up-down.svg +3 -0
  1242. data/vendor/heroicons/24/solid/at-symbol.svg +3 -0
  1243. data/vendor/heroicons/24/solid/backspace.svg +3 -0
  1244. data/vendor/heroicons/24/solid/backward.svg +3 -0
  1245. data/vendor/heroicons/24/solid/banknotes.svg +5 -0
  1246. data/vendor/heroicons/24/solid/bars-2.svg +3 -0
  1247. data/vendor/heroicons/24/solid/bars-3-bottom-left.svg +3 -0
  1248. data/vendor/heroicons/24/solid/bars-3-bottom-right.svg +3 -0
  1249. data/vendor/heroicons/24/solid/bars-3-center-left.svg +3 -0
  1250. data/vendor/heroicons/24/solid/bars-3.svg +3 -0
  1251. data/vendor/heroicons/24/solid/bars-4.svg +3 -0
  1252. data/vendor/heroicons/24/solid/bars-arrow-down.svg +3 -0
  1253. data/vendor/heroicons/24/solid/bars-arrow-up.svg +3 -0
  1254. data/vendor/heroicons/24/solid/battery-0.svg +3 -0
  1255. data/vendor/heroicons/24/solid/battery-100.svg +3 -0
  1256. data/vendor/heroicons/24/solid/battery-50.svg +4 -0
  1257. data/vendor/heroicons/24/solid/beaker.svg +3 -0
  1258. data/vendor/heroicons/24/solid/bell-alert.svg +4 -0
  1259. data/vendor/heroicons/24/solid/bell-slash.svg +4 -0
  1260. data/vendor/heroicons/24/solid/bell-snooze.svg +3 -0
  1261. data/vendor/heroicons/24/solid/bell.svg +3 -0
  1262. data/vendor/heroicons/24/solid/bold.svg +3 -0
  1263. data/vendor/heroicons/24/solid/bolt-slash.svg +3 -0
  1264. data/vendor/heroicons/24/solid/bolt.svg +3 -0
  1265. data/vendor/heroicons/24/solid/book-open.svg +3 -0
  1266. data/vendor/heroicons/24/solid/bookmark-slash.svg +3 -0
  1267. data/vendor/heroicons/24/solid/bookmark-square.svg +3 -0
  1268. data/vendor/heroicons/24/solid/bookmark.svg +3 -0
  1269. data/vendor/heroicons/24/solid/briefcase.svg +4 -0
  1270. data/vendor/heroicons/24/solid/bug-ant.svg +3 -0
  1271. data/vendor/heroicons/24/solid/building-library.svg +5 -0
  1272. data/vendor/heroicons/24/solid/building-office-2.svg +3 -0
  1273. data/vendor/heroicons/24/solid/building-office.svg +3 -0
  1274. data/vendor/heroicons/24/solid/building-storefront.svg +4 -0
  1275. data/vendor/heroicons/24/solid/cake.svg +3 -0
  1276. data/vendor/heroicons/24/solid/calculator.svg +3 -0
  1277. data/vendor/heroicons/24/solid/calendar-date-range.svg +4 -0
  1278. data/vendor/heroicons/24/solid/calendar-days.svg +4 -0
  1279. data/vendor/heroicons/24/solid/calendar.svg +3 -0
  1280. data/vendor/heroicons/24/solid/camera.svg +4 -0
  1281. data/vendor/heroicons/24/solid/chart-bar-square.svg +3 -0
  1282. data/vendor/heroicons/24/solid/chart-bar.svg +3 -0
  1283. data/vendor/heroicons/24/solid/chart-pie.svg +4 -0
  1284. data/vendor/heroicons/24/solid/chat-bubble-bottom-center-text.svg +3 -0
  1285. data/vendor/heroicons/24/solid/chat-bubble-bottom-center.svg +3 -0
  1286. data/vendor/heroicons/24/solid/chat-bubble-left-ellipsis.svg +3 -0
  1287. data/vendor/heroicons/24/solid/chat-bubble-left-right.svg +4 -0
  1288. data/vendor/heroicons/24/solid/chat-bubble-left.svg +3 -0
  1289. data/vendor/heroicons/24/solid/chat-bubble-oval-left-ellipsis.svg +3 -0
  1290. data/vendor/heroicons/24/solid/chat-bubble-oval-left.svg +3 -0
  1291. data/vendor/heroicons/24/solid/check-badge.svg +3 -0
  1292. data/vendor/heroicons/24/solid/check-circle.svg +3 -0
  1293. data/vendor/heroicons/24/solid/check.svg +3 -0
  1294. data/vendor/heroicons/24/solid/chevron-double-down.svg +4 -0
  1295. data/vendor/heroicons/24/solid/chevron-double-left.svg +4 -0
  1296. data/vendor/heroicons/24/solid/chevron-double-right.svg +4 -0
  1297. data/vendor/heroicons/24/solid/chevron-double-up.svg +4 -0
  1298. data/vendor/heroicons/24/solid/chevron-down.svg +3 -0
  1299. data/vendor/heroicons/24/solid/chevron-left.svg +3 -0
  1300. data/vendor/heroicons/24/solid/chevron-right.svg +3 -0
  1301. data/vendor/heroicons/24/solid/chevron-up-down.svg +3 -0
  1302. data/vendor/heroicons/24/solid/chevron-up.svg +3 -0
  1303. data/vendor/heroicons/24/solid/circle-stack.svg +6 -0
  1304. data/vendor/heroicons/24/solid/clipboard-document-check.svg +4 -0
  1305. data/vendor/heroicons/24/solid/clipboard-document-list.svg +4 -0
  1306. data/vendor/heroicons/24/solid/clipboard-document.svg +5 -0
  1307. data/vendor/heroicons/24/solid/clipboard.svg +3 -0
  1308. data/vendor/heroicons/24/solid/clock.svg +3 -0
  1309. data/vendor/heroicons/24/solid/cloud-arrow-down.svg +3 -0
  1310. data/vendor/heroicons/24/solid/cloud-arrow-up.svg +3 -0
  1311. data/vendor/heroicons/24/solid/cloud.svg +3 -0
  1312. data/vendor/heroicons/24/solid/code-bracket-square.svg +3 -0
  1313. data/vendor/heroicons/24/solid/code-bracket.svg +3 -0
  1314. data/vendor/heroicons/24/solid/cog-6-tooth.svg +3 -0
  1315. data/vendor/heroicons/24/solid/cog-8-tooth.svg +3 -0
  1316. data/vendor/heroicons/24/solid/cog.svg +4 -0
  1317. data/vendor/heroicons/24/solid/command-line.svg +3 -0
  1318. data/vendor/heroicons/24/solid/computer-desktop.svg +3 -0
  1319. data/vendor/heroicons/24/solid/cpu-chip.svg +4 -0
  1320. data/vendor/heroicons/24/solid/credit-card.svg +4 -0
  1321. data/vendor/heroicons/24/solid/cube-transparent.svg +3 -0
  1322. data/vendor/heroicons/24/solid/cube.svg +3 -0
  1323. data/vendor/heroicons/24/solid/currency-bangladeshi.svg +3 -0
  1324. data/vendor/heroicons/24/solid/currency-dollar.svg +4 -0
  1325. data/vendor/heroicons/24/solid/currency-euro.svg +3 -0
  1326. data/vendor/heroicons/24/solid/currency-pound.svg +3 -0
  1327. data/vendor/heroicons/24/solid/currency-rupee.svg +3 -0
  1328. data/vendor/heroicons/24/solid/currency-yen.svg +3 -0
  1329. data/vendor/heroicons/24/solid/cursor-arrow-rays.svg +3 -0
  1330. data/vendor/heroicons/24/solid/cursor-arrow-ripple.svg +3 -0
  1331. data/vendor/heroicons/24/solid/device-phone-mobile.svg +4 -0
  1332. data/vendor/heroicons/24/solid/device-tablet.svg +4 -0
  1333. data/vendor/heroicons/24/solid/divide.svg +3 -0
  1334. data/vendor/heroicons/24/solid/document-arrow-down.svg +4 -0
  1335. data/vendor/heroicons/24/solid/document-arrow-up.svg +4 -0
  1336. data/vendor/heroicons/24/solid/document-chart-bar.svg +4 -0
  1337. data/vendor/heroicons/24/solid/document-check.svg +4 -0
  1338. data/vendor/heroicons/24/solid/document-currency-bangladeshi.svg +3 -0
  1339. data/vendor/heroicons/24/solid/document-currency-dollar.svg +3 -0
  1340. data/vendor/heroicons/24/solid/document-currency-euro.svg +4 -0
  1341. data/vendor/heroicons/24/solid/document-currency-pound.svg +3 -0
  1342. data/vendor/heroicons/24/solid/document-currency-rupee.svg +3 -0
  1343. data/vendor/heroicons/24/solid/document-currency-yen.svg +3 -0
  1344. data/vendor/heroicons/24/solid/document-duplicate.svg +4 -0
  1345. data/vendor/heroicons/24/solid/document-magnifying-glass.svg +5 -0
  1346. data/vendor/heroicons/24/solid/document-minus.svg +4 -0
  1347. data/vendor/heroicons/24/solid/document-plus.svg +4 -0
  1348. data/vendor/heroicons/24/solid/document-text.svg +4 -0
  1349. data/vendor/heroicons/24/solid/document.svg +4 -0
  1350. data/vendor/heroicons/24/solid/ellipsis-horizontal-circle.svg +3 -0
  1351. data/vendor/heroicons/24/solid/ellipsis-horizontal.svg +3 -0
  1352. data/vendor/heroicons/24/solid/ellipsis-vertical.svg +3 -0
  1353. data/vendor/heroicons/24/solid/envelope-open.svg +4 -0
  1354. data/vendor/heroicons/24/solid/envelope.svg +4 -0
  1355. data/vendor/heroicons/24/solid/equals.svg +3 -0
  1356. data/vendor/heroicons/24/solid/exclamation-circle.svg +3 -0
  1357. data/vendor/heroicons/24/solid/exclamation-triangle.svg +3 -0
  1358. data/vendor/heroicons/24/solid/eye-dropper.svg +3 -0
  1359. data/vendor/heroicons/24/solid/eye-slash.svg +5 -0
  1360. data/vendor/heroicons/24/solid/eye.svg +4 -0
  1361. data/vendor/heroicons/24/solid/face-frown.svg +3 -0
  1362. data/vendor/heroicons/24/solid/face-smile.svg +3 -0
  1363. data/vendor/heroicons/24/solid/film.svg +3 -0
  1364. data/vendor/heroicons/24/solid/finger-print.svg +3 -0
  1365. data/vendor/heroicons/24/solid/fire.svg +3 -0
  1366. data/vendor/heroicons/24/solid/flag.svg +3 -0
  1367. data/vendor/heroicons/24/solid/folder-arrow-down.svg +3 -0
  1368. data/vendor/heroicons/24/solid/folder-minus.svg +3 -0
  1369. data/vendor/heroicons/24/solid/folder-open.svg +3 -0
  1370. data/vendor/heroicons/24/solid/folder-plus.svg +3 -0
  1371. data/vendor/heroicons/24/solid/folder.svg +3 -0
  1372. data/vendor/heroicons/24/solid/forward.svg +3 -0
  1373. data/vendor/heroicons/24/solid/funnel.svg +3 -0
  1374. data/vendor/heroicons/24/solid/gif.svg +3 -0
  1375. data/vendor/heroicons/24/solid/gift-top.svg +4 -0
  1376. data/vendor/heroicons/24/solid/gift.svg +3 -0
  1377. data/vendor/heroicons/24/solid/globe-alt.svg +3 -0
  1378. data/vendor/heroicons/24/solid/globe-americas.svg +3 -0
  1379. data/vendor/heroicons/24/solid/globe-asia-australia.svg +4 -0
  1380. data/vendor/heroicons/24/solid/globe-europe-africa.svg +3 -0
  1381. data/vendor/heroicons/24/solid/h1.svg +3 -0
  1382. data/vendor/heroicons/24/solid/h2.svg +3 -0
  1383. data/vendor/heroicons/24/solid/h3.svg +3 -0
  1384. data/vendor/heroicons/24/solid/hand-raised.svg +3 -0
  1385. data/vendor/heroicons/24/solid/hand-thumb-down.svg +3 -0
  1386. data/vendor/heroicons/24/solid/hand-thumb-up.svg +3 -0
  1387. data/vendor/heroicons/24/solid/hashtag.svg +3 -0
  1388. data/vendor/heroicons/24/solid/heart.svg +3 -0
  1389. data/vendor/heroicons/24/solid/home-modern.svg +4 -0
  1390. data/vendor/heroicons/24/solid/home.svg +4 -0
  1391. data/vendor/heroicons/24/solid/identification.svg +3 -0
  1392. data/vendor/heroicons/24/solid/inbox-arrow-down.svg +4 -0
  1393. data/vendor/heroicons/24/solid/inbox-stack.svg +4 -0
  1394. data/vendor/heroicons/24/solid/inbox.svg +3 -0
  1395. data/vendor/heroicons/24/solid/information-circle.svg +3 -0
  1396. data/vendor/heroicons/24/solid/italic.svg +3 -0
  1397. data/vendor/heroicons/24/solid/key.svg +3 -0
  1398. data/vendor/heroicons/24/solid/language.svg +3 -0
  1399. data/vendor/heroicons/24/solid/lifebuoy.svg +3 -0
  1400. data/vendor/heroicons/24/solid/light-bulb.svg +4 -0
  1401. data/vendor/heroicons/24/solid/link-slash.svg +3 -0
  1402. data/vendor/heroicons/24/solid/link.svg +3 -0
  1403. data/vendor/heroicons/24/solid/list-bullet.svg +3 -0
  1404. data/vendor/heroicons/24/solid/lock-closed.svg +3 -0
  1405. data/vendor/heroicons/24/solid/lock-open.svg +3 -0
  1406. data/vendor/heroicons/24/solid/magnifying-glass-circle.svg +4 -0
  1407. data/vendor/heroicons/24/solid/magnifying-glass-minus.svg +3 -0
  1408. data/vendor/heroicons/24/solid/magnifying-glass-plus.svg +3 -0
  1409. data/vendor/heroicons/24/solid/magnifying-glass.svg +3 -0
  1410. data/vendor/heroicons/24/solid/map-pin.svg +3 -0
  1411. data/vendor/heroicons/24/solid/map.svg +3 -0
  1412. data/vendor/heroicons/24/solid/megaphone.svg +3 -0
  1413. data/vendor/heroicons/24/solid/microphone.svg +4 -0
  1414. data/vendor/heroicons/24/solid/minus-circle.svg +3 -0
  1415. data/vendor/heroicons/24/solid/minus-small.svg +3 -0
  1416. data/vendor/heroicons/24/solid/minus.svg +3 -0
  1417. data/vendor/heroicons/24/solid/moon.svg +3 -0
  1418. data/vendor/heroicons/24/solid/musical-note.svg +3 -0
  1419. data/vendor/heroicons/24/solid/newspaper.svg +4 -0
  1420. data/vendor/heroicons/24/solid/no-symbol.svg +3 -0
  1421. data/vendor/heroicons/24/solid/numbered-list.svg +3 -0
  1422. data/vendor/heroicons/24/solid/paint-brush.svg +3 -0
  1423. data/vendor/heroicons/24/solid/paper-airplane.svg +3 -0
  1424. data/vendor/heroicons/24/solid/paper-clip.svg +3 -0
  1425. data/vendor/heroicons/24/solid/pause-circle.svg +3 -0
  1426. data/vendor/heroicons/24/solid/pause.svg +3 -0
  1427. data/vendor/heroicons/24/solid/pencil-square.svg +4 -0
  1428. data/vendor/heroicons/24/solid/pencil.svg +3 -0
  1429. data/vendor/heroicons/24/solid/percent-badge.svg +3 -0
  1430. data/vendor/heroicons/24/solid/phone-arrow-down-left.svg +4 -0
  1431. data/vendor/heroicons/24/solid/phone-arrow-up-right.svg +4 -0
  1432. data/vendor/heroicons/24/solid/phone-x-mark.svg +3 -0
  1433. data/vendor/heroicons/24/solid/phone.svg +3 -0
  1434. data/vendor/heroicons/24/solid/photo.svg +3 -0
  1435. data/vendor/heroicons/24/solid/play-circle.svg +3 -0
  1436. data/vendor/heroicons/24/solid/play-pause.svg +3 -0
  1437. data/vendor/heroicons/24/solid/play.svg +3 -0
  1438. data/vendor/heroicons/24/solid/plus-circle.svg +3 -0
  1439. data/vendor/heroicons/24/solid/plus-small.svg +3 -0
  1440. data/vendor/heroicons/24/solid/plus.svg +3 -0
  1441. data/vendor/heroicons/24/solid/power.svg +3 -0
  1442. data/vendor/heroicons/24/solid/presentation-chart-bar.svg +3 -0
  1443. data/vendor/heroicons/24/solid/presentation-chart-line.svg +3 -0
  1444. data/vendor/heroicons/24/solid/printer.svg +3 -0
  1445. data/vendor/heroicons/24/solid/puzzle-piece.svg +3 -0
  1446. data/vendor/heroicons/24/solid/qr-code.svg +3 -0
  1447. data/vendor/heroicons/24/solid/question-mark-circle.svg +3 -0
  1448. data/vendor/heroicons/24/solid/queue-list.svg +3 -0
  1449. data/vendor/heroicons/24/solid/radio.svg +3 -0
  1450. data/vendor/heroicons/24/solid/receipt-percent.svg +3 -0
  1451. data/vendor/heroicons/24/solid/receipt-refund.svg +3 -0
  1452. data/vendor/heroicons/24/solid/rectangle-group.svg +3 -0
  1453. data/vendor/heroicons/24/solid/rectangle-stack.svg +3 -0
  1454. data/vendor/heroicons/24/solid/rocket-launch.svg +4 -0
  1455. data/vendor/heroicons/24/solid/rss.svg +3 -0
  1456. data/vendor/heroicons/24/solid/scale.svg +3 -0
  1457. data/vendor/heroicons/24/solid/scissors.svg +4 -0
  1458. data/vendor/heroicons/24/solid/server-stack.svg +4 -0
  1459. data/vendor/heroicons/24/solid/server.svg +4 -0
  1460. data/vendor/heroicons/24/solid/share.svg +3 -0
  1461. data/vendor/heroicons/24/solid/shield-check.svg +3 -0
  1462. data/vendor/heroicons/24/solid/shield-exclamation.svg +3 -0
  1463. data/vendor/heroicons/24/solid/shopping-bag.svg +3 -0
  1464. data/vendor/heroicons/24/solid/shopping-cart.svg +3 -0
  1465. data/vendor/heroicons/24/solid/signal-slash.svg +3 -0
  1466. data/vendor/heroicons/24/solid/signal.svg +3 -0
  1467. data/vendor/heroicons/24/solid/slash.svg +3 -0
  1468. data/vendor/heroicons/24/solid/sparkles.svg +3 -0
  1469. data/vendor/heroicons/24/solid/speaker-wave.svg +4 -0
  1470. data/vendor/heroicons/24/solid/speaker-x-mark.svg +3 -0
  1471. data/vendor/heroicons/24/solid/square-2-stack.svg +4 -0
  1472. data/vendor/heroicons/24/solid/square-3-stack-3d.svg +5 -0
  1473. data/vendor/heroicons/24/solid/squares-2x2.svg +3 -0
  1474. data/vendor/heroicons/24/solid/squares-plus.svg +3 -0
  1475. data/vendor/heroicons/24/solid/star.svg +3 -0
  1476. data/vendor/heroicons/24/solid/stop-circle.svg +3 -0
  1477. data/vendor/heroicons/24/solid/stop.svg +3 -0
  1478. data/vendor/heroicons/24/solid/strikethrough.svg +3 -0
  1479. data/vendor/heroicons/24/solid/sun.svg +3 -0
  1480. data/vendor/heroicons/24/solid/swatch.svg +4 -0
  1481. data/vendor/heroicons/24/solid/table-cells.svg +3 -0
  1482. data/vendor/heroicons/24/solid/tag.svg +3 -0
  1483. data/vendor/heroicons/24/solid/ticket.svg +3 -0
  1484. data/vendor/heroicons/24/solid/trash.svg +3 -0
  1485. data/vendor/heroicons/24/solid/trophy.svg +3 -0
  1486. data/vendor/heroicons/24/solid/truck.svg +5 -0
  1487. data/vendor/heroicons/24/solid/tv.svg +4 -0
  1488. data/vendor/heroicons/24/solid/underline.svg +3 -0
  1489. data/vendor/heroicons/24/solid/user-circle.svg +3 -0
  1490. data/vendor/heroicons/24/solid/user-group.svg +4 -0
  1491. data/vendor/heroicons/24/solid/user-minus.svg +3 -0
  1492. data/vendor/heroicons/24/solid/user-plus.svg +3 -0
  1493. data/vendor/heroicons/24/solid/user.svg +3 -0
  1494. data/vendor/heroicons/24/solid/users.svg +3 -0
  1495. data/vendor/heroicons/24/solid/variable.svg +3 -0
  1496. data/vendor/heroicons/24/solid/video-camera-slash.svg +3 -0
  1497. data/vendor/heroicons/24/solid/video-camera.svg +3 -0
  1498. data/vendor/heroicons/24/solid/view-columns.svg +3 -0
  1499. data/vendor/heroicons/24/solid/viewfinder-circle.svg +3 -0
  1500. data/vendor/heroicons/24/solid/wallet.svg +3 -0
  1501. data/vendor/heroicons/24/solid/wifi.svg +3 -0
  1502. data/vendor/heroicons/24/solid/window.svg +3 -0
  1503. data/vendor/heroicons/24/solid/wrench-screwdriver.svg +5 -0
  1504. data/vendor/heroicons/24/solid/wrench.svg +3 -0
  1505. data/vendor/heroicons/24/solid/x-circle.svg +3 -0
  1506. data/vendor/heroicons/24/solid/x-mark.svg +3 -0
  1507. data/vendor/heroicons/LICENSE +21 -0
  1508. data/vendor/heroicons/VERSION +5 -0
  1509. metadata +1664 -0
data/README.md ADDED
@@ -0,0 +1,2739 @@
1
+ # Abqari
2
+
3
+ [![Ruby](https://img.shields.io/badge/ruby-%E2%89%A53.3-CC342D.svg?logo=ruby)](https://www.ruby-lang.org/)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ [![CI](https://github.com/grantrayner/abqari/actions/workflows/ci.yml/badge.svg)](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 (`![alt](hero.jpg)`), 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 (`![alt](hero.jpg)`,
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
+ ![A hero image](hero.jpg)
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
+ ![Hero](hero.jpg)
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).