open_blog 0.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 (257) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +52 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +101 -0
  5. data/app/assets/builds/open_blog/blog.css +2 -0
  6. data/app/assets/javascripts/open_blog/controllers/code_copy_controller.js +40 -0
  7. data/app/assets/javascripts/open_blog/controllers/reading_progress_controller.js +36 -0
  8. data/app/assets/javascripts/open_blog/controllers/search_controller.js +49 -0
  9. data/app/assets/javascripts/open_blog/controllers/share_controller.js +48 -0
  10. data/app/assets/javascripts/open_blog/controllers/theme_controller.js +51 -0
  11. data/app/assets/javascripts/open_blog/controllers/toc_controller.js +33 -0
  12. data/app/controllers/concerns/open_blog/reader_controller.rb +49 -0
  13. data/app/controllers/open_blog/api/v1/adoptions_controller.rb +27 -0
  14. data/app/controllers/open_blog/api/v1/approvals_controller.rb +15 -0
  15. data/app/controllers/open_blog/api/v1/authors_controller.rb +62 -0
  16. data/app/controllers/open_blog/api/v1/base_controller.rb +100 -0
  17. data/app/controllers/open_blog/api/v1/categories_controller.rb +33 -0
  18. data/app/controllers/open_blog/api/v1/connections_controller.rb +45 -0
  19. data/app/controllers/open_blog/api/v1/doctor_controller.rb +13 -0
  20. data/app/controllers/open_blog/api/v1/faq_extractions_controller.rb +13 -0
  21. data/app/controllers/open_blog/api/v1/findings_controller.rb +13 -0
  22. data/app/controllers/open_blog/api/v1/images_controller.rb +22 -0
  23. data/app/controllers/open_blog/api/v1/pages_controller.rb +67 -0
  24. data/app/controllers/open_blog/api/v1/posts_controller.rb +114 -0
  25. data/app/controllers/open_blog/api/v1/previews_controller.rb +13 -0
  26. data/app/controllers/open_blog/api/v1/records_controller.rb +13 -0
  27. data/app/controllers/open_blog/api/v1/redirects_controller.rb +51 -0
  28. data/app/controllers/open_blog/api/v1/report_controller.rb +20 -0
  29. data/app/controllers/open_blog/api/v1/resources_controller.rb +39 -0
  30. data/app/controllers/open_blog/api/v1/series_controller.rb +32 -0
  31. data/app/controllers/open_blog/api/v1/tags_controller.rb +12 -0
  32. data/app/controllers/open_blog/api/v1/views_controller.rb +19 -0
  33. data/app/controllers/open_blog/application_controller.rb +5 -0
  34. data/app/controllers/open_blog/authors_controller.rb +14 -0
  35. data/app/controllers/open_blog/categories_controller.rb +15 -0
  36. data/app/controllers/open_blog/errors_controller.rb +7 -0
  37. data/app/controllers/open_blog/feeds_controller.rb +22 -0
  38. data/app/controllers/open_blog/mcp_controller.rb +38 -0
  39. data/app/controllers/open_blog/media_controller.rb +20 -0
  40. data/app/controllers/open_blog/pages_controller.rb +12 -0
  41. data/app/controllers/open_blog/posts_controller.rb +57 -0
  42. data/app/controllers/open_blog/previews_controller.rb +35 -0
  43. data/app/controllers/open_blog/search_controller.rb +32 -0
  44. data/app/controllers/open_blog/series_controller.rb +15 -0
  45. data/app/controllers/open_blog/sitemaps_controller.rb +10 -0
  46. data/app/controllers/open_blog/tags_controller.rb +15 -0
  47. data/app/helpers/open_blog/content_helper.rb +79 -0
  48. data/app/helpers/open_blog/dates_helper.rb +18 -0
  49. data/app/helpers/open_blog/faq_helper.rb +59 -0
  50. data/app/helpers/open_blog/head_helper.rb +75 -0
  51. data/app/helpers/open_blog/icons_helper.rb +10 -0
  52. data/app/helpers/open_blog/notices_helper.rb +47 -0
  53. data/app/helpers/open_blog/pagination_helper.rb +18 -0
  54. data/app/helpers/open_blog/structured_data_helper.rb +63 -0
  55. data/app/helpers/open_blog/theme_helper.rb +52 -0
  56. data/app/helpers/open_blog/url_helper.rb +58 -0
  57. data/app/jobs/open_blog/publish_scheduled_post_job.rb +14 -0
  58. data/app/models/concerns/open_blog/immutable.rb +13 -0
  59. data/app/models/concerns/open_blog/immutable_image_attachment.rb +43 -0
  60. data/app/models/concerns/open_blog/previewable.rb +38 -0
  61. data/app/models/open_blog/api_token.rb +34 -0
  62. data/app/models/open_blog/application_record.rb +5 -0
  63. data/app/models/open_blog/approval.rb +19 -0
  64. data/app/models/open_blog/author.rb +11 -0
  65. data/app/models/open_blog/baseline.rb +26 -0
  66. data/app/models/open_blog/category.rb +10 -0
  67. data/app/models/open_blog/connection_declaration.rb +16 -0
  68. data/app/models/open_blog/faq.rb +22 -0
  69. data/app/models/open_blog/image.rb +45 -0
  70. data/app/models/open_blog/page.rb +23 -0
  71. data/app/models/open_blog/page_view.rb +7 -0
  72. data/app/models/open_blog/post.rb +132 -0
  73. data/app/models/open_blog/publication.rb +17 -0
  74. data/app/models/open_blog/redirect.rb +16 -0
  75. data/app/models/open_blog/revision.rb +12 -0
  76. data/app/models/open_blog/series.rb +10 -0
  77. data/app/models/open_blog/tag.rb +11 -0
  78. data/app/models/open_blog/tagging.rb +7 -0
  79. data/app/queries/open_blog/api_post_query.rb +54 -0
  80. data/app/serializers/open_blog/adoption_result_serializer.rb +9 -0
  81. data/app/serializers/open_blog/author_serializer.rb +10 -0
  82. data/app/serializers/open_blog/category_serializer.rb +7 -0
  83. data/app/serializers/open_blog/doctor_serializer.rb +7 -0
  84. data/app/serializers/open_blog/error_serializer.rb +7 -0
  85. data/app/serializers/open_blog/extraction_result_serializer.rb +7 -0
  86. data/app/serializers/open_blog/findings_serializer.rb +13 -0
  87. data/app/serializers/open_blog/image_serializer.rb +9 -0
  88. data/app/serializers/open_blog/page_serializer.rb +9 -0
  89. data/app/serializers/open_blog/post_serializer.rb +59 -0
  90. data/app/serializers/open_blog/preview_serializer.rb +11 -0
  91. data/app/serializers/open_blog/records_serializer.rb +28 -0
  92. data/app/serializers/open_blog/redirect_serializer.rb +7 -0
  93. data/app/serializers/open_blog/series_serializer.rb +7 -0
  94. data/app/serializers/open_blog/tag_serializer.rb +7 -0
  95. data/app/serializers/open_blog/write_result_serializer.rb +9 -0
  96. data/app/views/open_blog/attachments/_blob.html.erb +1 -0
  97. data/app/views/open_blog/feeds/atom.xml.builder +27 -0
  98. data/app/views/open_blog/sitemaps/show.xml.builder +9 -0
  99. data/config/locales/open_blog.en.yml +95 -0
  100. data/config/routes.rb +52 -0
  101. data/db/migrate/20261003012512_create_open_blog_authors.rb +16 -0
  102. data/db/migrate/20261003012513_create_open_blog_categories.rb +13 -0
  103. data/db/migrate/20261003012514_create_open_blog_series.rb +11 -0
  104. data/db/migrate/20261003012515_create_open_blog_images.rb +17 -0
  105. data/db/migrate/20261003012516_create_open_blog_posts.rb +47 -0
  106. data/db/migrate/20261003012517_create_open_blog_faqs.rb +13 -0
  107. data/db/migrate/20261003012518_create_open_blog_tags.rb +17 -0
  108. data/db/migrate/20261003012519_create_open_blog_redirects.rb +14 -0
  109. data/db/migrate/20261003012520_create_open_blog_revisions.rb +14 -0
  110. data/db/migrate/20261003012521_create_open_blog_approvals.rb +21 -0
  111. data/db/migrate/20261003012522_create_open_blog_publications.rb +18 -0
  112. data/db/migrate/20261003012523_create_open_blog_baselines.rb +24 -0
  113. data/db/migrate/20261003012524_create_open_blog_connection_declarations.rb +13 -0
  114. data/db/migrate/20261003031410_add_body_image_manifest_to_open_blog_posts.rb +7 -0
  115. data/db/migrate/20261003164418_create_open_blog_api_tokens.rb +15 -0
  116. data/db/migrate/20261003175454_create_open_blog_pages.rb +16 -0
  117. data/db/migrate/20261003182919_create_open_blog_page_views.rb +11 -0
  118. data/lib/generators/open_blog/admin_suite/admin_suite_generator.rb +27 -0
  119. data/lib/generators/open_blog/admin_suite/templates/author_resource.rb.tt +29 -0
  120. data/lib/generators/open_blog/admin_suite/templates/blog_portal.rb.tt +16 -0
  121. data/lib/generators/open_blog/admin_suite/templates/category_resource.rb.tt +28 -0
  122. data/lib/generators/open_blog/admin_suite/templates/initializer.rb.tt +19 -0
  123. data/lib/generators/open_blog/admin_suite/templates/page_resource.rb.tt +32 -0
  124. data/lib/generators/open_blog/admin_suite/templates/post_resource.rb.tt +58 -0
  125. data/lib/generators/open_blog/generator_support.rb +64 -0
  126. data/lib/generators/open_blog/install/install_generator.rb +188 -0
  127. data/lib/generators/open_blog/install/templates/initializer.rb.tt +16 -0
  128. data/lib/generators/open_blog/install/templates/sample_cover.png +0 -0
  129. data/lib/generators/open_blog/install/templates/sample_post.md +54 -0
  130. data/lib/generators/open_blog/install/templates/theme/blog.css +193 -0
  131. data/lib/generators/open_blog/install/templates/theme/open_blog_theme.css +82 -0
  132. data/lib/generators/open_blog/install/templates/theme/syntax.css +358 -0
  133. data/lib/generators/open_blog/install/templates/theme/theme.css +120 -0
  134. data/lib/generators/open_blog/install/templates/views/layouts/open_blog.html.erb +16 -0
  135. data/lib/generators/open_blog/install/templates/views/open_blog/authors/show.html.erb +17 -0
  136. data/lib/generators/open_blog/install/templates/views/open_blog/categories/show.html.erb +13 -0
  137. data/lib/generators/open_blog/install/templates/views/open_blog/errors/not_found.html.erb +11 -0
  138. data/lib/generators/open_blog/install/templates/views/open_blog/errors/preview_expired.html.erb +5 -0
  139. data/lib/generators/open_blog/install/templates/views/open_blog/icons/_check.svg.erb +1 -0
  140. data/lib/generators/open_blog/install/templates/views/open_blog/icons/_copy.svg.erb +1 -0
  141. data/lib/generators/open_blog/install/templates/views/open_blog/icons/_link.svg.erb +1 -0
  142. data/lib/generators/open_blog/install/templates/views/open_blog/icons/_monitor.svg.erb +1 -0
  143. data/lib/generators/open_blog/install/templates/views/open_blog/icons/_moon.svg.erb +1 -0
  144. data/lib/generators/open_blog/install/templates/views/open_blog/icons/_share.svg.erb +1 -0
  145. data/lib/generators/open_blog/install/templates/views/open_blog/icons/_sun.svg.erb +1 -0
  146. data/lib/generators/open_blog/install/templates/views/open_blog/pages/show.html.erb +5 -0
  147. data/lib/generators/open_blog/install/templates/views/open_blog/posts/_faq.html.erb +1 -0
  148. data/lib/generators/open_blog/install/templates/views/open_blog/posts/_highlights.html.erb +7 -0
  149. data/lib/generators/open_blog/install/templates/views/open_blog/posts/_preview_bar.html.erb +1 -0
  150. data/lib/generators/open_blog/install/templates/views/open_blog/posts/_series_nav.html.erb +12 -0
  151. data/lib/generators/open_blog/install/templates/views/open_blog/posts/_tags.html.erb +5 -0
  152. data/lib/generators/open_blog/install/templates/views/open_blog/posts/_toc.html.erb +1 -0
  153. data/lib/generators/open_blog/install/templates/views/open_blog/posts/index.html.erb +13 -0
  154. data/lib/generators/open_blog/install/templates/views/open_blog/posts/show.html.erb +29 -0
  155. data/lib/generators/open_blog/install/templates/views/open_blog/search/show.html.erb +13 -0
  156. data/lib/generators/open_blog/install/templates/views/open_blog/series/show.html.erb +13 -0
  157. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_author_box.html.erb +6 -0
  158. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_breadcrumbs.html.erb +1 -0
  159. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_call_to_action.html.erb +7 -0
  160. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_featured_card.html.erb +11 -0
  161. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_footer.html.erb +11 -0
  162. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_header.html.erb +7 -0
  163. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_pagination.html.erb +1 -0
  164. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_post_card.html.erb +9 -0
  165. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_related_posts.html.erb +10 -0
  166. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_search.html.erb +9 -0
  167. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_share.html.erb +13 -0
  168. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_sidebar.html.erb +38 -0
  169. data/lib/generators/open_blog/install/templates/views/open_blog/shared/_theme_toggle.html.erb +10 -0
  170. data/lib/generators/open_blog/install/templates/views/open_blog/tags/show.html.erb +13 -0
  171. data/lib/generators/open_blog/views/views_generator.rb +21 -0
  172. data/lib/open_blog/actor.rb +7 -0
  173. data/lib/open_blog/admin_suite.rb +73 -0
  174. data/lib/open_blog/adopt/contract.rb +152 -0
  175. data/lib/open_blog/adopt/snapshot.rb +35 -0
  176. data/lib/open_blog/adopt.rb +159 -0
  177. data/lib/open_blog/api_fields.rb +40 -0
  178. data/lib/open_blog/approve.rb +18 -0
  179. data/lib/open_blog/authentication.rb +18 -0
  180. data/lib/open_blog/build_css.rb +53 -0
  181. data/lib/open_blog/configuration.rb +185 -0
  182. data/lib/open_blog/configuration_error.rb +4 -0
  183. data/lib/open_blog/content_guard.rb +77 -0
  184. data/lib/open_blog/doctor/files.rb +61 -0
  185. data/lib/open_blog/doctor/javascript.rb +57 -0
  186. data/lib/open_blog/doctor.rb +177 -0
  187. data/lib/open_blog/engine.rb +45 -0
  188. data/lib/open_blog/errors.rb +40 -0
  189. data/lib/open_blog/faq_extraction.rb +184 -0
  190. data/lib/open_blog/findings/body_checks.rb +58 -0
  191. data/lib/open_blog/findings/check.rb +19 -0
  192. data/lib/open_blog/findings/link_check.rb +78 -0
  193. data/lib/open_blog/findings/record_checks.rb +94 -0
  194. data/lib/open_blog/findings/registry.rb +18 -0
  195. data/lib/open_blog/findings.rb +27 -0
  196. data/lib/open_blog/image_fetch.rb +115 -0
  197. data/lib/open_blog/image_import.rb +164 -0
  198. data/lib/open_blog/image_resolution.rb +171 -0
  199. data/lib/open_blog/instructions.rb +7 -0
  200. data/lib/open_blog/json_feed.rb +36 -0
  201. data/lib/open_blog/label_policy.rb +41 -0
  202. data/lib/open_blog/markdown_view.rb +57 -0
  203. data/lib/open_blog/mcp/definition.rb +28 -0
  204. data/lib/open_blog/mcp/dispatcher.rb +33 -0
  205. data/lib/open_blog/mcp/image_input.rb +30 -0
  206. data/lib/open_blog/mcp/registry.rb +139 -0
  207. data/lib/open_blog/mcp/schemas.rb +72 -0
  208. data/lib/open_blog/mcp.rb +24 -0
  209. data/lib/open_blog/not_found.rb +4 -0
  210. data/lib/open_blog/operation.rb +36 -0
  211. data/lib/open_blog/page_views.rb +72 -0
  212. data/lib/open_blog/pagination.rb +18 -0
  213. data/lib/open_blog/plain_text.rb +36 -0
  214. data/lib/open_blog/policy_url.rb +9 -0
  215. data/lib/open_blog/post_attributes.rb +172 -0
  216. data/lib/open_blog/post_identity.rb +28 -0
  217. data/lib/open_blog/publish.rb +7 -0
  218. data/lib/open_blog/reader_dates.rb +21 -0
  219. data/lib/open_blog/reader_page.rb +21 -0
  220. data/lib/open_blog/reader_queries.rb +90 -0
  221. data/lib/open_blog/record_release.rb +66 -0
  222. data/lib/open_blog/redirect_target.rb +58 -0
  223. data/lib/open_blog/remove.rb +43 -0
  224. data/lib/open_blog/renderer/highlighter.rb +44 -0
  225. data/lib/open_blog/renderer/markdown.rb +14 -0
  226. data/lib/open_blog/renderer/rich_text.rb +19 -0
  227. data/lib/open_blog/renderer/sanitizer.rb +72 -0
  228. data/lib/open_blog/renderer/scrubber.rb +35 -0
  229. data/lib/open_blog/renderer/transform.rb +115 -0
  230. data/lib/open_blog/renderer.rb +51 -0
  231. data/lib/open_blog/result.rb +11 -0
  232. data/lib/open_blog/revision_payload.rb +67 -0
  233. data/lib/open_blog/rich_text_guard.rb +26 -0
  234. data/lib/open_blog/sample.rb +56 -0
  235. data/lib/open_blog/save_draft.rb +7 -0
  236. data/lib/open_blog/search.rb +21 -0
  237. data/lib/open_blog/sitemap_entries.rb +66 -0
  238. data/lib/open_blog/surface_report/expected_content.rb +27 -0
  239. data/lib/open_blog/surface_report/http.rb +92 -0
  240. data/lib/open_blog/surface_report/page_checks.rb +303 -0
  241. data/lib/open_blog/surface_report/reach_checks.rb +141 -0
  242. data/lib/open_blog/surface_report/record_checks.rb +248 -0
  243. data/lib/open_blog/surface_report/robots.rb +49 -0
  244. data/lib/open_blog/surface_report.rb +223 -0
  245. data/lib/open_blog/syntax_css.rb +55 -0
  246. data/lib/open_blog/unpublish.rb +13 -0
  247. data/lib/open_blog/version.rb +3 -0
  248. data/lib/open_blog/write_post.rb +205 -0
  249. data/lib/open_blog.rb +91 -0
  250. data/lib/tasks/open_blog.rake +71 -0
  251. data/skills/open-blog-adopt/SKILL.md +12 -0
  252. data/skills/open-blog-install/SKILL.md +10 -0
  253. data/skills/open-blog-policy-pages/SKILL.md +10 -0
  254. data/skills/open-blog-publish/SKILL.md +14 -0
  255. data/skills/open-blog-report/SKILL.md +10 -0
  256. data/skills/open-blog-update/SKILL.md +16 -0
  257. metadata +515 -0
@@ -0,0 +1,13 @@
1
+ class CreateOpenBlogFaqs < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_faqs do |t|
4
+ t.references :post, null: false, foreign_key: { to_table: :open_blog_posts }
5
+ t.integer :position, null: false
6
+ t.text :question, null: false
7
+ t.text :answer, null: false
8
+ t.timestamps
9
+ end
10
+ add_index :open_blog_faqs, [ :post_id, :position ], unique: true
11
+ add_check_constraint :open_blog_faqs, "position > 0", name: "open_blog_faqs_position"
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ class CreateOpenBlogTags < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_tags do |t|
4
+ t.string :name, null: false, **(connection.adapter_name == "SQLite" ? { collation: "NOCASE" } : {})
5
+ t.string :slug, null: false
6
+ t.datetime :created_at, null: false
7
+ end
8
+ add_index :open_blog_tags, connection.adapter_name == "PostgreSQL" ? "lower(name)" : :name, unique: true, name: "index_open_blog_tags_on_name"
9
+ add_index :open_blog_tags, :slug, unique: true
10
+ create_table :open_blog_taggings do |t|
11
+ t.references :post, null: false, foreign_key: { to_table: :open_blog_posts }
12
+ t.references :tag, null: false, foreign_key: { to_table: :open_blog_tags }
13
+ t.datetime :created_at, null: false
14
+ end
15
+ add_index :open_blog_taggings, [ :post_id, :tag_id ], unique: true
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ class CreateOpenBlogRedirects < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_redirects do |t|
4
+ t.string :old_path, null: false
5
+ t.string :new_path
6
+ t.string :source, null: false
7
+ t.date :occurred_on, null: false
8
+ t.references :post, foreign_key: { to_table: :open_blog_posts }
9
+ t.timestamps
10
+ end
11
+ add_index :open_blog_redirects, :old_path, unique: true
12
+ add_check_constraint :open_blog_redirects, "source IN ('slug_change', 'removal', 'unpublish', 'adoption', 'manual')", name: "open_blog_redirects_source"
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ class CreateOpenBlogRevisions < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_revisions do |t|
4
+ t.references :post, null: false, foreign_key: { to_table: :open_blog_posts }
5
+ t.string :identifier, limit: 64, null: false
6
+ t.text :payload, null: false
7
+ t.string :actor
8
+ t.boolean :made_by_ai
9
+ t.datetime :created_at, null: false
10
+ end
11
+ add_index :open_blog_revisions, [ :post_id, :identifier ], unique: true
12
+ add_foreign_key :open_blog_posts, :open_blog_revisions, column: :public_revision_id
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ class CreateOpenBlogApprovals < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_approvals do |t|
4
+ t.references :post, null: false, foreign_key: { to_table: :open_blog_posts }
5
+ t.references :revision, null: false, foreign_key: { to_table: :open_blog_revisions }
6
+ t.string :kind, null: false
7
+ t.string :reviewer_name, null: false
8
+ t.boolean :facts_checked, null: false
9
+ t.datetime :approved_at, null: false
10
+ t.date :declared_on
11
+ t.string :declared_by
12
+ t.string :confirmed_by
13
+ t.text :evidence
14
+ t.string :recorded_by
15
+ t.datetime :created_at, null: false
16
+ end
17
+ add_check_constraint :open_blog_approvals, "kind IN ('sent', 'imported', 'declared')", name: "open_blog_approvals_kind"
18
+ add_check_constraint :open_blog_approvals, "kind != 'declared' OR (declared_on IS NOT NULL AND declared_by IS NOT NULL)", name: "open_blog_approvals_declaration"
19
+ add_check_constraint :open_blog_approvals, "kind != 'imported' OR (confirmed_by IS NOT NULL AND evidence IS NOT NULL)", name: "open_blog_approvals_import"
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ class CreateOpenBlogPublications < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_publications do |t|
4
+ t.references :post, null: false, foreign_key: { to_table: :open_blog_posts }
5
+ t.references :revision, null: false, foreign_key: { to_table: :open_blog_revisions }
6
+ t.string :entry_type, null: false
7
+ t.datetime :occurred_at, null: false
8
+ t.string :released_by
9
+ t.string :description
10
+ t.text :note
11
+ t.datetime :created_at, null: false
12
+ end
13
+ add_index :open_blog_publications, [ :post_id, :occurred_at ]
14
+ add_index :open_blog_publications, :post_id, unique: true, where: "entry_type = 'first'", name: "index_open_blog_publications_first"
15
+ add_check_constraint :open_blog_publications, "entry_type IN ('first', 'substantive', 'correction', 'maintenance', 'adopted')", name: "open_blog_publications_entry_type"
16
+ add_check_constraint :open_blog_publications, "entry_type != 'correction' OR note IS NOT NULL", name: "open_blog_publications_correction"
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ class CreateOpenBlogBaselines < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_baselines do |t|
4
+ t.references :post, null: false, index: { unique: true }, foreign_key: { to_table: :open_blog_posts }
5
+ t.datetime :adopted_at, null: false
6
+ t.references :adopted_revision, null: false, foreign_key: { to_table: :open_blog_revisions }
7
+ t.string :provenance, null: false
8
+ t.text :provenance_evidence
9
+ t.datetime :first_published_at
10
+ t.text :first_published_evidence
11
+ t.datetime :declared_first_published_at
12
+ t.datetime :last_modified_at
13
+ t.text :last_modified_evidence
14
+ t.string :source_system
15
+ t.string :source_id
16
+ t.string :source_body_sha256, limit: 64
17
+ t.string :adopted_by
18
+ t.datetime :created_at, null: false
19
+ end
20
+ add_index :open_blog_baselines, [ :source_system, :source_id ], unique: true
21
+ add_check_constraint :open_blog_baselines, "first_published_at IS NULL OR declared_first_published_at IS NULL", name: "open_blog_baselines_first_date"
22
+ add_check_constraint :open_blog_baselines, "provenance IN ('ai_assisted', 'human_written', 'unknown')", name: "open_blog_baselines_provenance"
23
+ end
24
+ end
@@ -0,0 +1,13 @@
1
+ class CreateOpenBlogConnectionDeclarations < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_connection_declarations do |t|
4
+ t.references :post, null: false, foreign_key: { to_table: :open_blog_posts }
5
+ t.public_send(connection.adapter_name == "PostgreSQL" ? :jsonb : :json, :connections, null: false, default: [])
6
+ t.boolean :third_party_paid, null: false
7
+ t.string :declared_by, null: false
8
+ t.date :declared_on, null: false
9
+ t.string :recorded_by
10
+ t.datetime :created_at, null: false
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ class AddBodyImageManifestToOpenBlogPosts < ActiveRecord::Migration[8.0]
2
+ def change
3
+ format = connection.adapter_name == "PostgreSQL" ? :jsonb : :json
4
+ add_column :open_blog_posts, :body_image_manifest, format, null: false, default: []
5
+ add_column :open_blog_posts, :body_image_source_digest, :string, limit: 64
6
+ end
7
+ end
@@ -0,0 +1,15 @@
1
+ class CreateOpenBlogApiTokens < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_api_tokens do |t|
4
+ t.string :name, null: false
5
+ t.string :token_digest, limit: 64, null: false
6
+ t.string :token_prefix, limit: 8, null: false
7
+ t.public_send(connection.adapter_name == "PostgreSQL" ? :jsonb : :json, :scopes, null: false, default: %w[read write publish])
8
+ t.datetime :last_used_at
9
+ t.datetime :expires_at
10
+ t.datetime :revoked_at
11
+ t.timestamps
12
+ end
13
+ add_index :open_blog_api_tokens, :token_digest, unique: true
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ class CreateOpenBlogPages < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_pages do |t|
4
+ t.string :kind, null: false
5
+ t.string :slug, null: false
6
+ t.string :title, null: false
7
+ t.text :body_markdown, null: false, default: ""
8
+ t.string :status, null: false, default: "draft"
9
+ t.string :approved_by
10
+ t.date :approved_on
11
+ t.datetime :updated_at, null: false
12
+ end
13
+ add_index :open_blog_pages, :kind, unique: true
14
+ add_index :open_blog_pages, :slug, unique: true
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ class CreateOpenBlogPageViews < ActiveRecord::Migration[8.0]
2
+ def change
3
+ create_table :open_blog_page_views do |t|
4
+ t.references :post, null: false, foreign_key: { to_table: :open_blog_posts }, index: false
5
+ t.date :day, null: false
6
+ t.integer :views, null: false, default: 0
7
+ end
8
+ add_index :open_blog_page_views, [ :post_id, :day ], unique: true
9
+ add_index :open_blog_page_views, :day
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ require "rails/generators"
2
+ require "pathname"
3
+ require_relative "../generator_support"
4
+
5
+ module OpenBlog
6
+ module Generators
7
+ class AdminSuiteGenerator < Rails::Generators::Base
8
+ include GeneratorSupport
9
+ source_root File.expand_path("templates", __dir__)
10
+ class_option :dir, type: :string, default: "app/admin", desc: "Host directory for admin definitions"
11
+
12
+ def copy_definitions
13
+ path = Pathname.new(options[:dir])
14
+ if path.absolute? || path.each_filename.include?("..") || options[:dir].match?(/[\x00-\x1f*?\[\]{}]/)
15
+ raise Thor::Error, "Choose a relative host directory without traversal or glob characters."
16
+ end
17
+ @admin_dir = path.cleanpath.to_s
18
+ %w[post category author page].each do |name|
19
+ template "#{name}_resource.rb.tt", "#{@admin_dir}/resources/open_blog/#{name}_resource.rb", **copy_options
20
+ end
21
+ template "blog_portal.rb.tt", "#{@admin_dir}/portals/open_blog_portal.rb", **copy_options
22
+ template "initializer.rb.tt", "config/initializers/open_blog_admin_suite.rb", **copy_options
23
+ say "Admin definitions written. Install AdminSuite 0.6.1 or later to activate them; restart the host after adding definitions."
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ module Admin
2
+ module Resources
3
+ module OpenBlog
4
+ class AuthorResource < ::Admin::Base::Resource
5
+ model ::OpenBlog::Author
6
+ portal :open_blog
7
+ section :content
8
+ def self.resource_name = "open_blog_author"
9
+
10
+ index do
11
+ searchable :name, :slug
12
+ sortable :name, default: :name, direction: :asc
13
+ columns do
14
+ column :name
15
+ column :slug
16
+ column :author_type
17
+ end
18
+ end
19
+ form do
20
+ field :name, required: true
21
+ field :slug
22
+ field :author_type, type: :select, collection: %w[person organization]
23
+ field :bio, type: :textarea
24
+ field :url, type: :url
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ module Admin
2
+ module Portals
3
+ class OpenBlogPortal
4
+ def self.register!
5
+ ::AdminSuite.portal :open_blog do
6
+ label "Blog"
7
+ icon "book-open"
8
+ section :content do
9
+ label "Publishing"
10
+ end
11
+ end
12
+ end
13
+ register!
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,28 @@
1
+ module Admin
2
+ module Resources
3
+ module OpenBlog
4
+ class CategoryResource < ::Admin::Base::Resource
5
+ model ::OpenBlog::Category
6
+ portal :open_blog
7
+ section :content
8
+ def self.resource_name = "open_blog_category"
9
+
10
+ index do
11
+ searchable :name, :slug
12
+ sortable :position, default: :position, direction: :asc
13
+ columns do
14
+ column :name
15
+ column :slug
16
+ column :position
17
+ end
18
+ end
19
+ form do
20
+ field :name, required: true
21
+ field :slug
22
+ field :description, type: :textarea
23
+ field :position, type: :number
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ Rails.autoloaders.main.ignore(
2
+ Rails.root.join(<%= "#{@admin_dir}/resources/open_blog".inspect %>),
3
+ Rails.root.join(<%= "#{@admin_dir}/portals/open_blog_portal.rb".inspect %>)
4
+ )
5
+
6
+ if defined?(::AdminSuite)
7
+ Rails.application.config.after_initialize do
8
+ require "open_blog/admin_suite"
9
+ ::OpenBlog::AdminSuite.install!
10
+ ::AdminSuite.configure do |config|
11
+ config.resource_globs |= [ Rails.root.join(<%= "#{@admin_dir}/resources/open_blog/*_resource.rb".inspect %>).to_s ]
12
+ config.portal_globs |= [ Rails.root.join(<%= "#{@admin_dir}/portals/open_blog_portal.rb".inspect %>).to_s ]
13
+ end
14
+ ::AdminSuite::DefinitionLoader.load!(:resources)
15
+ ::AdminSuite::DefinitionLoader.load!(:portals)
16
+ Dir[Rails.root.join(<%= "#{@admin_dir}/resources/open_blog/*_resource.rb".inspect %>)].sort.each { |path| require path }
17
+ require Rails.root.join(<%= "#{@admin_dir}/portals/open_blog_portal.rb".inspect %>).to_s
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ module Admin
2
+ module Resources
3
+ module OpenBlog
4
+ class PageResource < ::Admin::Base::Resource
5
+ model ::OpenBlog::Page
6
+ portal :open_blog
7
+ section :content
8
+ def self.resource_name = "open_blog_page"
9
+
10
+ index do
11
+ searchable :title, :slug
12
+ sortable :updated_at, default: :updated_at, direction: :desc
13
+ columns do
14
+ column :title
15
+ column :kind
16
+ column :status
17
+ column :updated_at
18
+ end
19
+ end
20
+ form do
21
+ field :kind, type: :select, required: true, collection: ::OpenBlog::Page::KINDS
22
+ field :title, required: true
23
+ field :slug
24
+ field :body_markdown, type: :textarea, rows: 18
25
+ field :status, type: :select, collection: %w[draft published]
26
+ field :approved_by
27
+ field :approved_on, type: :date
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,58 @@
1
+ require "open_blog/admin_suite"
2
+ ::OpenBlog::AdminSuite.install!
3
+
4
+ module Admin
5
+ module Resources
6
+ module OpenBlog
7
+ class PostResource < ::Admin::Base::Resource
8
+ model ::OpenBlog::Post
9
+ portal :open_blog
10
+ section :content
11
+ def self.resource_name = "open_blog_post"
12
+
13
+ index do
14
+ searchable :title, :slug
15
+ sortable :updated_at, default: :updated_at, direction: :desc
16
+ columns do
17
+ column :title
18
+ column :status
19
+ column :author_name
20
+ column :updated_at
21
+ end
22
+ end
23
+
24
+ form do
25
+ field :title, required: true
26
+ field :slug, required: true, if: ->(post) { post.new_record? }
27
+ field :description, type: :textarea
28
+ field :author_id, type: :select, required: true, collection: -> { ::OpenBlog::Author.order(:name).pluck(:name, :id) }
29
+ field :author_name, required: true, label: "Byline"
30
+ field :category_id, type: :select, collection: -> { ::OpenBlog::Category.order(:name).pluck(:name, :id) }
31
+ field :body_markdown, type: :textarea, rows: 18, if: ->(post) { post.markdown? }
32
+ field :rich_body, type: :rich_text, if: ->(post) { post.rich_text? }
33
+ field :faqs_attributes, type: :open_blog_faq, help: "Each FAQ needs a unique positive position."
34
+ field :search_title
35
+ field :search_description, type: :textarea
36
+ field :featured, type: :toggle
37
+ field :status, type: :select, collection: %w[draft published archived], unless: ->(post) { post.scheduled? }
38
+ end
39
+
40
+ show do
41
+ panel :article, fields: %i[title slug status author_name published_at modified_at]
42
+ panel :revisions, title: "Recent revisions", association: :revisions, render: :table_from,
43
+ source: ->(post) { post.revisions.order(id: :desc).limit(20).map { |row| row.attributes.symbolize_keys } },
44
+ columns: %i[id identifier actor created_at]
45
+ panel :approvals, title: "Recent approvals", association: :approvals, render: :table_from,
46
+ source: ->(post) { post.approvals.order(id: :desc).limit(20).map { |row| row.attributes.symbolize_keys } },
47
+ columns: %i[id revision_id kind reviewer_name facts_checked approved_at]
48
+ panel :publications, title: "Recent publications", association: :publications, render: :table_from,
49
+ source: ->(post) { post.publications.order(id: :desc).limit(20).map { |row| row.attributes.symbolize_keys } },
50
+ columns: %i[id revision_id entry_type occurred_at released_by note]
51
+ panel :baseline, association: :baseline, render: :table_from,
52
+ source: ->(post) { Array(post.baseline).map { |row| row.attributes.symbolize_keys } },
53
+ columns: %i[id source_system source_id provenance first_published_at declared_first_published_at adopted_at]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,64 @@
1
+ require "json"
2
+
3
+ module OpenBlog
4
+ module Generators
5
+ module GeneratorSupport
6
+ private
7
+
8
+ def host_file(path)
9
+ File.join(destination_root, path)
10
+ end
11
+
12
+ def host_read(path)
13
+ File.file?(host_file(path)) ? File.read(host_file(path)) : ""
14
+ end
15
+
16
+ def javascript_setup
17
+ return :importmap if File.file?(host_file("config/importmap.rb"))
18
+ return :bundler if File.file?(host_file("package.json"))
19
+ :none
20
+ end
21
+
22
+ def css_setup
23
+ return :fallback if options[:skip_tailwind]
24
+ package = JSON.parse(host_read("package.json").presence || "{}")
25
+ version = package.fetch("dependencies", {}).merge(package.fetch("devDependencies", {}))["tailwindcss"].to_s
26
+ gemfile = host_read("Gemfile")
27
+ old_gem = gemfile.match?(/^\s*gem\s+["']tailwindcss-rails["']\s*,\s*["'][~>\s]*3(?:\.|["'])/)
28
+ old_gem ||= host_read("Gemfile.lock").match?(/^ tailwindcss-rails \(3\./)
29
+ old_gem ||= host_read("app/assets/stylesheets/application.tailwind.css").match?(/@tailwind\s+(?:base|components|utilities)\b/)
30
+ return :legacy if version.match?(/\A[~^>=\s]*3(?:\.|\z)/) || old_gem
31
+ return :rails if File.file?(host_file("app/assets/tailwind/application.css"))
32
+ if version.match?(/\A[~^>=\s]*4(?:\.|\z)/) && File.file?(host_file("app/assets/stylesheets/application.tailwind.css"))
33
+ return :bundler
34
+ end
35
+ :none
36
+ rescue JSON::ParserError => error
37
+ raise Thor::Error, "Cannot read package.json: #{error.message}"
38
+ end
39
+
40
+ def copy_options
41
+ { force: options[:force], skip: options[:skip] || (!options[:force] && !$stdin.tty?) }
42
+ end
43
+
44
+ def copy_reader_views(css: css_setup, javascript: javascript_setup)
45
+ directory "views/open_blog", "app/views/open_blog", **copy_options
46
+ content = File.read(find_in_source_paths("views/layouts/open_blog.html.erb"))
47
+ stylesheet = case css
48
+ when :rails then 'stylesheet_link_tag "tailwind", "data-turbo-track": "reload"'
49
+ when :bundler then 'stylesheet_link_tag "application", "data-turbo-track": "reload"'
50
+ else "open_blog_stylesheets"
51
+ end
52
+ content = content.sub("open_blog_stylesheets", stylesheet)
53
+ if javascript == :bundler
54
+ content = content.sub("javascript_importmap_tags", 'javascript_include_tag "application", "data-turbo-track": "reload", defer: true')
55
+ end
56
+ create_file "app/views/layouts/open_blog.html.erb", content, **copy_options
57
+ end
58
+
59
+ def append_once(path, content)
60
+ append_to_file(path, content) unless host_read(path).include?(content.strip)
61
+ end
62
+ end
63
+ end
64
+ end