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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +52 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/app/assets/builds/open_blog/blog.css +2 -0
- data/app/assets/javascripts/open_blog/controllers/code_copy_controller.js +40 -0
- data/app/assets/javascripts/open_blog/controllers/reading_progress_controller.js +36 -0
- data/app/assets/javascripts/open_blog/controllers/search_controller.js +49 -0
- data/app/assets/javascripts/open_blog/controllers/share_controller.js +48 -0
- data/app/assets/javascripts/open_blog/controllers/theme_controller.js +51 -0
- data/app/assets/javascripts/open_blog/controllers/toc_controller.js +33 -0
- data/app/controllers/concerns/open_blog/reader_controller.rb +49 -0
- data/app/controllers/open_blog/api/v1/adoptions_controller.rb +27 -0
- data/app/controllers/open_blog/api/v1/approvals_controller.rb +15 -0
- data/app/controllers/open_blog/api/v1/authors_controller.rb +62 -0
- data/app/controllers/open_blog/api/v1/base_controller.rb +100 -0
- data/app/controllers/open_blog/api/v1/categories_controller.rb +33 -0
- data/app/controllers/open_blog/api/v1/connections_controller.rb +45 -0
- data/app/controllers/open_blog/api/v1/doctor_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/faq_extractions_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/findings_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/images_controller.rb +22 -0
- data/app/controllers/open_blog/api/v1/pages_controller.rb +67 -0
- data/app/controllers/open_blog/api/v1/posts_controller.rb +114 -0
- data/app/controllers/open_blog/api/v1/previews_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/records_controller.rb +13 -0
- data/app/controllers/open_blog/api/v1/redirects_controller.rb +51 -0
- data/app/controllers/open_blog/api/v1/report_controller.rb +20 -0
- data/app/controllers/open_blog/api/v1/resources_controller.rb +39 -0
- data/app/controllers/open_blog/api/v1/series_controller.rb +32 -0
- data/app/controllers/open_blog/api/v1/tags_controller.rb +12 -0
- data/app/controllers/open_blog/api/v1/views_controller.rb +19 -0
- data/app/controllers/open_blog/application_controller.rb +5 -0
- data/app/controllers/open_blog/authors_controller.rb +14 -0
- data/app/controllers/open_blog/categories_controller.rb +15 -0
- data/app/controllers/open_blog/errors_controller.rb +7 -0
- data/app/controllers/open_blog/feeds_controller.rb +22 -0
- data/app/controllers/open_blog/mcp_controller.rb +38 -0
- data/app/controllers/open_blog/media_controller.rb +20 -0
- data/app/controllers/open_blog/pages_controller.rb +12 -0
- data/app/controllers/open_blog/posts_controller.rb +57 -0
- data/app/controllers/open_blog/previews_controller.rb +35 -0
- data/app/controllers/open_blog/search_controller.rb +32 -0
- data/app/controllers/open_blog/series_controller.rb +15 -0
- data/app/controllers/open_blog/sitemaps_controller.rb +10 -0
- data/app/controllers/open_blog/tags_controller.rb +15 -0
- data/app/helpers/open_blog/content_helper.rb +79 -0
- data/app/helpers/open_blog/dates_helper.rb +18 -0
- data/app/helpers/open_blog/faq_helper.rb +59 -0
- data/app/helpers/open_blog/head_helper.rb +75 -0
- data/app/helpers/open_blog/icons_helper.rb +10 -0
- data/app/helpers/open_blog/notices_helper.rb +47 -0
- data/app/helpers/open_blog/pagination_helper.rb +18 -0
- data/app/helpers/open_blog/structured_data_helper.rb +63 -0
- data/app/helpers/open_blog/theme_helper.rb +52 -0
- data/app/helpers/open_blog/url_helper.rb +58 -0
- data/app/jobs/open_blog/publish_scheduled_post_job.rb +14 -0
- data/app/models/concerns/open_blog/immutable.rb +13 -0
- data/app/models/concerns/open_blog/immutable_image_attachment.rb +43 -0
- data/app/models/concerns/open_blog/previewable.rb +38 -0
- data/app/models/open_blog/api_token.rb +34 -0
- data/app/models/open_blog/application_record.rb +5 -0
- data/app/models/open_blog/approval.rb +19 -0
- data/app/models/open_blog/author.rb +11 -0
- data/app/models/open_blog/baseline.rb +26 -0
- data/app/models/open_blog/category.rb +10 -0
- data/app/models/open_blog/connection_declaration.rb +16 -0
- data/app/models/open_blog/faq.rb +22 -0
- data/app/models/open_blog/image.rb +45 -0
- data/app/models/open_blog/page.rb +23 -0
- data/app/models/open_blog/page_view.rb +7 -0
- data/app/models/open_blog/post.rb +132 -0
- data/app/models/open_blog/publication.rb +17 -0
- data/app/models/open_blog/redirect.rb +16 -0
- data/app/models/open_blog/revision.rb +12 -0
- data/app/models/open_blog/series.rb +10 -0
- data/app/models/open_blog/tag.rb +11 -0
- data/app/models/open_blog/tagging.rb +7 -0
- data/app/queries/open_blog/api_post_query.rb +54 -0
- data/app/serializers/open_blog/adoption_result_serializer.rb +9 -0
- data/app/serializers/open_blog/author_serializer.rb +10 -0
- data/app/serializers/open_blog/category_serializer.rb +7 -0
- data/app/serializers/open_blog/doctor_serializer.rb +7 -0
- data/app/serializers/open_blog/error_serializer.rb +7 -0
- data/app/serializers/open_blog/extraction_result_serializer.rb +7 -0
- data/app/serializers/open_blog/findings_serializer.rb +13 -0
- data/app/serializers/open_blog/image_serializer.rb +9 -0
- data/app/serializers/open_blog/page_serializer.rb +9 -0
- data/app/serializers/open_blog/post_serializer.rb +59 -0
- data/app/serializers/open_blog/preview_serializer.rb +11 -0
- data/app/serializers/open_blog/records_serializer.rb +28 -0
- data/app/serializers/open_blog/redirect_serializer.rb +7 -0
- data/app/serializers/open_blog/series_serializer.rb +7 -0
- data/app/serializers/open_blog/tag_serializer.rb +7 -0
- data/app/serializers/open_blog/write_result_serializer.rb +9 -0
- data/app/views/open_blog/attachments/_blob.html.erb +1 -0
- data/app/views/open_blog/feeds/atom.xml.builder +27 -0
- data/app/views/open_blog/sitemaps/show.xml.builder +9 -0
- data/config/locales/open_blog.en.yml +95 -0
- data/config/routes.rb +52 -0
- data/db/migrate/20261003012512_create_open_blog_authors.rb +16 -0
- data/db/migrate/20261003012513_create_open_blog_categories.rb +13 -0
- data/db/migrate/20261003012514_create_open_blog_series.rb +11 -0
- data/db/migrate/20261003012515_create_open_blog_images.rb +17 -0
- data/db/migrate/20261003012516_create_open_blog_posts.rb +47 -0
- data/db/migrate/20261003012517_create_open_blog_faqs.rb +13 -0
- data/db/migrate/20261003012518_create_open_blog_tags.rb +17 -0
- data/db/migrate/20261003012519_create_open_blog_redirects.rb +14 -0
- data/db/migrate/20261003012520_create_open_blog_revisions.rb +14 -0
- data/db/migrate/20261003012521_create_open_blog_approvals.rb +21 -0
- data/db/migrate/20261003012522_create_open_blog_publications.rb +18 -0
- data/db/migrate/20261003012523_create_open_blog_baselines.rb +24 -0
- data/db/migrate/20261003012524_create_open_blog_connection_declarations.rb +13 -0
- data/db/migrate/20261003031410_add_body_image_manifest_to_open_blog_posts.rb +7 -0
- data/db/migrate/20261003164418_create_open_blog_api_tokens.rb +15 -0
- data/db/migrate/20261003175454_create_open_blog_pages.rb +16 -0
- data/db/migrate/20261003182919_create_open_blog_page_views.rb +11 -0
- data/lib/generators/open_blog/admin_suite/admin_suite_generator.rb +27 -0
- data/lib/generators/open_blog/admin_suite/templates/author_resource.rb.tt +29 -0
- data/lib/generators/open_blog/admin_suite/templates/blog_portal.rb.tt +16 -0
- data/lib/generators/open_blog/admin_suite/templates/category_resource.rb.tt +28 -0
- data/lib/generators/open_blog/admin_suite/templates/initializer.rb.tt +19 -0
- data/lib/generators/open_blog/admin_suite/templates/page_resource.rb.tt +32 -0
- data/lib/generators/open_blog/admin_suite/templates/post_resource.rb.tt +58 -0
- data/lib/generators/open_blog/generator_support.rb +64 -0
- data/lib/generators/open_blog/install/install_generator.rb +188 -0
- data/lib/generators/open_blog/install/templates/initializer.rb.tt +16 -0
- data/lib/generators/open_blog/install/templates/sample_cover.png +0 -0
- data/lib/generators/open_blog/install/templates/sample_post.md +54 -0
- data/lib/generators/open_blog/install/templates/theme/blog.css +193 -0
- data/lib/generators/open_blog/install/templates/theme/open_blog_theme.css +82 -0
- data/lib/generators/open_blog/install/templates/theme/syntax.css +358 -0
- data/lib/generators/open_blog/install/templates/theme/theme.css +120 -0
- data/lib/generators/open_blog/install/templates/views/layouts/open_blog.html.erb +16 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/authors/show.html.erb +17 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/categories/show.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/errors/not_found.html.erb +11 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/errors/preview_expired.html.erb +5 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_check.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_copy.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_link.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_monitor.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_moon.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_share.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/icons/_sun.svg.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/pages/show.html.erb +5 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_faq.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_highlights.html.erb +7 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_preview_bar.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_series_nav.html.erb +12 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_tags.html.erb +5 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/_toc.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/index.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/posts/show.html.erb +29 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/search/show.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/series/show.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_author_box.html.erb +6 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_breadcrumbs.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_call_to_action.html.erb +7 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_featured_card.html.erb +11 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_footer.html.erb +11 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_header.html.erb +7 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_pagination.html.erb +1 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_post_card.html.erb +9 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_related_posts.html.erb +10 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_search.html.erb +9 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_share.html.erb +13 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_sidebar.html.erb +38 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/shared/_theme_toggle.html.erb +10 -0
- data/lib/generators/open_blog/install/templates/views/open_blog/tags/show.html.erb +13 -0
- data/lib/generators/open_blog/views/views_generator.rb +21 -0
- data/lib/open_blog/actor.rb +7 -0
- data/lib/open_blog/admin_suite.rb +73 -0
- data/lib/open_blog/adopt/contract.rb +152 -0
- data/lib/open_blog/adopt/snapshot.rb +35 -0
- data/lib/open_blog/adopt.rb +159 -0
- data/lib/open_blog/api_fields.rb +40 -0
- data/lib/open_blog/approve.rb +18 -0
- data/lib/open_blog/authentication.rb +18 -0
- data/lib/open_blog/build_css.rb +53 -0
- data/lib/open_blog/configuration.rb +185 -0
- data/lib/open_blog/configuration_error.rb +4 -0
- data/lib/open_blog/content_guard.rb +77 -0
- data/lib/open_blog/doctor/files.rb +61 -0
- data/lib/open_blog/doctor/javascript.rb +57 -0
- data/lib/open_blog/doctor.rb +177 -0
- data/lib/open_blog/engine.rb +45 -0
- data/lib/open_blog/errors.rb +40 -0
- data/lib/open_blog/faq_extraction.rb +184 -0
- data/lib/open_blog/findings/body_checks.rb +58 -0
- data/lib/open_blog/findings/check.rb +19 -0
- data/lib/open_blog/findings/link_check.rb +78 -0
- data/lib/open_blog/findings/record_checks.rb +94 -0
- data/lib/open_blog/findings/registry.rb +18 -0
- data/lib/open_blog/findings.rb +27 -0
- data/lib/open_blog/image_fetch.rb +115 -0
- data/lib/open_blog/image_import.rb +164 -0
- data/lib/open_blog/image_resolution.rb +171 -0
- data/lib/open_blog/instructions.rb +7 -0
- data/lib/open_blog/json_feed.rb +36 -0
- data/lib/open_blog/label_policy.rb +41 -0
- data/lib/open_blog/markdown_view.rb +57 -0
- data/lib/open_blog/mcp/definition.rb +28 -0
- data/lib/open_blog/mcp/dispatcher.rb +33 -0
- data/lib/open_blog/mcp/image_input.rb +30 -0
- data/lib/open_blog/mcp/registry.rb +139 -0
- data/lib/open_blog/mcp/schemas.rb +72 -0
- data/lib/open_blog/mcp.rb +24 -0
- data/lib/open_blog/not_found.rb +4 -0
- data/lib/open_blog/operation.rb +36 -0
- data/lib/open_blog/page_views.rb +72 -0
- data/lib/open_blog/pagination.rb +18 -0
- data/lib/open_blog/plain_text.rb +36 -0
- data/lib/open_blog/policy_url.rb +9 -0
- data/lib/open_blog/post_attributes.rb +172 -0
- data/lib/open_blog/post_identity.rb +28 -0
- data/lib/open_blog/publish.rb +7 -0
- data/lib/open_blog/reader_dates.rb +21 -0
- data/lib/open_blog/reader_page.rb +21 -0
- data/lib/open_blog/reader_queries.rb +90 -0
- data/lib/open_blog/record_release.rb +66 -0
- data/lib/open_blog/redirect_target.rb +58 -0
- data/lib/open_blog/remove.rb +43 -0
- data/lib/open_blog/renderer/highlighter.rb +44 -0
- data/lib/open_blog/renderer/markdown.rb +14 -0
- data/lib/open_blog/renderer/rich_text.rb +19 -0
- data/lib/open_blog/renderer/sanitizer.rb +72 -0
- data/lib/open_blog/renderer/scrubber.rb +35 -0
- data/lib/open_blog/renderer/transform.rb +115 -0
- data/lib/open_blog/renderer.rb +51 -0
- data/lib/open_blog/result.rb +11 -0
- data/lib/open_blog/revision_payload.rb +67 -0
- data/lib/open_blog/rich_text_guard.rb +26 -0
- data/lib/open_blog/sample.rb +56 -0
- data/lib/open_blog/save_draft.rb +7 -0
- data/lib/open_blog/search.rb +21 -0
- data/lib/open_blog/sitemap_entries.rb +66 -0
- data/lib/open_blog/surface_report/expected_content.rb +27 -0
- data/lib/open_blog/surface_report/http.rb +92 -0
- data/lib/open_blog/surface_report/page_checks.rb +303 -0
- data/lib/open_blog/surface_report/reach_checks.rb +141 -0
- data/lib/open_blog/surface_report/record_checks.rb +248 -0
- data/lib/open_blog/surface_report/robots.rb +49 -0
- data/lib/open_blog/surface_report.rb +223 -0
- data/lib/open_blog/syntax_css.rb +55 -0
- data/lib/open_blog/unpublish.rb +13 -0
- data/lib/open_blog/version.rb +3 -0
- data/lib/open_blog/write_post.rb +205 -0
- data/lib/open_blog.rb +91 -0
- data/lib/tasks/open_blog.rake +71 -0
- data/skills/open-blog-adopt/SKILL.md +12 -0
- data/skills/open-blog-install/SKILL.md +10 -0
- data/skills/open-blog-policy-pages/SKILL.md +10 -0
- data/skills/open-blog-publish/SKILL.md +14 -0
- data/skills/open-blog-report/SKILL.md +10 -0
- data/skills/open-blog-update/SKILL.md +16 -0
- metadata +515 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class Redirect < ApplicationRecord
|
|
3
|
+
belongs_to :post, optional: true
|
|
4
|
+
validates :old_path, :occurred_on, presence: true
|
|
5
|
+
validates :old_path, uniqueness: true, format: { with: /\A\/(?!\/)/ }
|
|
6
|
+
validates :source, inclusion: { in: %w[slug_change removal unpublish adoption manual] }
|
|
7
|
+
validate :preserve_history, on: :update
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
def preserve_history
|
|
11
|
+
if (changed - %w[new_path updated_at]).any?
|
|
12
|
+
errors.add(:base, "Only the redirect target can change")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class Revision < ApplicationRecord
|
|
3
|
+
include Immutable
|
|
4
|
+
belongs_to :post
|
|
5
|
+
has_many :approvals, dependent: :restrict_with_exception
|
|
6
|
+
has_many :publications, dependent: :restrict_with_exception
|
|
7
|
+
|
|
8
|
+
validates :identifier, presence: true, uniqueness: { scope: :post_id }, format: { with: /\A[0-9a-f]{64}\z/ }
|
|
9
|
+
validates :payload, presence: true
|
|
10
|
+
validates :made_by_ai, inclusion: { in: [ true, false ] }, allow_nil: true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class Series < ApplicationRecord
|
|
3
|
+
self.table_name = "open_blog_series"
|
|
4
|
+
has_many :posts, dependent: :nullify
|
|
5
|
+
|
|
6
|
+
before_validation { self.slug = name.to_s.parameterize if slug.blank? }
|
|
7
|
+
validates :name, :slug, presence: true
|
|
8
|
+
validates :slug, uniqueness: true, format: { with: /\A[a-z0-9]+(?:[-_][a-z0-9]+)*\z/ }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class Tag < ApplicationRecord
|
|
3
|
+
has_many :taggings, dependent: :destroy
|
|
4
|
+
has_many :posts, through: :taggings
|
|
5
|
+
|
|
6
|
+
before_validation { self.slug = name.to_s.parameterize if slug.blank? }
|
|
7
|
+
validates :name, :slug, presence: true
|
|
8
|
+
validates :name, uniqueness: { case_sensitive: false }
|
|
9
|
+
validates :slug, uniqueness: true, format: { with: /\A[a-z0-9]+(?:[-_][a-z0-9]+)*\z/ }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class ApiPostQuery
|
|
3
|
+
FILTERS = ApiFields::POST_LIST
|
|
4
|
+
|
|
5
|
+
def self.call(filters, base_url: nil)
|
|
6
|
+
new(filters, base_url: base_url).call
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def initialize(filters, base_url:)
|
|
10
|
+
@filters, @base_url = filters, base_url
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def call
|
|
14
|
+
page = positive_integer(:page, 1)
|
|
15
|
+
per_page = [ positive_integer(:per_page, 25), 100 ].min
|
|
16
|
+
relation = Post.all
|
|
17
|
+
if @filters.key?(:status)
|
|
18
|
+
invalid(:status) unless Post.statuses.key?(@filters[:status])
|
|
19
|
+
relation = relation.where(status: @filters[:status])
|
|
20
|
+
end
|
|
21
|
+
{ category: :category, tag: :tags, author: :author, series: :series }.each do |field, association|
|
|
22
|
+
next unless @filters.key?(field)
|
|
23
|
+
value = @filters[field]
|
|
24
|
+
invalid(field) unless value.is_a?(String) && value.present?
|
|
25
|
+
model = Post.reflect_on_association(association).klass
|
|
26
|
+
matches = model.where(slug: value).or(model.where(name: value))
|
|
27
|
+
matches = matches.or(model.where(id: value.to_i)) if value.match?(/\A[1-9]\d*\z/)
|
|
28
|
+
relation = relation.where(id: Post.joins(association).merge(matches).select(:id))
|
|
29
|
+
end
|
|
30
|
+
if @filters.key?(:q)
|
|
31
|
+
query = @filters[:q]
|
|
32
|
+
invalid(:q) unless query.is_a?(String)
|
|
33
|
+
relation = Search.call(query, scope: relation)
|
|
34
|
+
end
|
|
35
|
+
total = relation.count
|
|
36
|
+
posts = relation.order(created_at: :desc, id: :desc).offset((page - 1) * per_page).limit(per_page)
|
|
37
|
+
.includes(:author, :category, :tags, :public_revision)
|
|
38
|
+
{ posts: posts.map { |post| PostSerializer.call(post, card: true, base_url: @base_url) }, page: page, per_page: per_page, total: total }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def positive_integer(field, default)
|
|
44
|
+
return default unless @filters.key?(field)
|
|
45
|
+
value = @filters[field]
|
|
46
|
+
invalid(field) unless (value.is_a?(Integer) || value.is_a?(String)) && value.to_s.match?(/\A[1-9]\d*\z/) && value.to_s.length <= 9
|
|
47
|
+
value.to_i
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def invalid(field)
|
|
51
|
+
raise Error::ValidationFailed.new(details: [ field.to_s ])
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class AdoptionResultSerializer
|
|
3
|
+
def self.call(result, base_url: nil)
|
|
4
|
+
{ post: PostSerializer.call(result.post, base_url: base_url, label: result.label), created: result.created,
|
|
5
|
+
records: result.records.slice(:revision, :publication, :approval, :baseline, :redirects),
|
|
6
|
+
findings: FindingsSerializer.entries(result.findings), dry_run: result.dry_run }
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class AuthorSerializer
|
|
3
|
+
def self.call(record)
|
|
4
|
+
image = ImageResolution.image_for_attachment(record.avatar.blob) if record.avatar.attached?
|
|
5
|
+
{ id: record.id, name: record.name, slug: record.slug, type: record.author_type, bio: record.bio,
|
|
6
|
+
url: record.url, profile_urls: record.profile_urls, avatar: ImageSerializer.call(image),
|
|
7
|
+
host_reference: record.host_reference, posts_count: record.posts.count }
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class FindingsSerializer
|
|
3
|
+
def self.call(findings)
|
|
4
|
+
{ findings: entries(findings) }
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def self.entries(findings)
|
|
8
|
+
findings.map do |finding|
|
|
9
|
+
{ code: finding[:code], rule: finding[:rule], message: finding[:message], location: finding[:location] }
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class ImageSerializer
|
|
3
|
+
def self.call(image)
|
|
4
|
+
return unless image
|
|
5
|
+
{ image_id: image.id, url: image.path, sha256: image.sha256, filename: image.filename,
|
|
6
|
+
content_type: image.content_type, byte_size: image.byte_size, width: image.width, height: image.height }
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class PageSerializer
|
|
3
|
+
def self.call(page, base_url: nil)
|
|
4
|
+
{ kind: page.kind, slug: page.slug, url: page.url(base: OpenBlog.config.public_base_url || base_url),
|
|
5
|
+
title: page.title, body: page.body_markdown, status: page.status,
|
|
6
|
+
approved_by: page.approved_by, approved_on: page.approved_on, updated_at: page.updated_at&.iso8601 }
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class PostSerializer
|
|
3
|
+
def self.call(post, base_url: nil, card: false, label: nil)
|
|
4
|
+
new(post, base_url: base_url, label: label).call(card: card)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def initialize(post, base_url:, label: nil)
|
|
8
|
+
@post, @base_url, @label = post, OpenBlog.config.public_base_url || base_url, label
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def call(card: false)
|
|
12
|
+
result = {
|
|
13
|
+
id: @post.id, slug: @post.slug, url: @post.url(base: @base_url), status: @post.status,
|
|
14
|
+
title: @post.title, description: @post.description, author: author,
|
|
15
|
+
category: taxonomy(@post.category), tags: @post.tags.map(&:name),
|
|
16
|
+
published_at: timestamp(@post.published_at), modified_at: timestamp(@post.modified_at),
|
|
17
|
+
revision_identifier: @post.current_revision_identifier, label: (@label || LabelPolicy.for(@post)).to_s
|
|
18
|
+
}
|
|
19
|
+
return result if card
|
|
20
|
+
|
|
21
|
+
result.merge(search_title: @post.search_title, search_description: @post.search_description,
|
|
22
|
+
body_format: @post.body_format, body: @post.body_for_payload,
|
|
23
|
+
series: @post.series && taxonomy(@post.series).merge(position: @post.series_position),
|
|
24
|
+
featured: @post.featured, canonical_url: @post.canonical_url,
|
|
25
|
+
cover_image: ImageSerializer.call(@post.cover_image), cover_alt: @post.cover_alt,
|
|
26
|
+
social_image: ImageSerializer.call(@post.social_image), faq: @post.faq_list,
|
|
27
|
+
provenance: @post.provenance, provenance_evidence: @post.provenance_evidence,
|
|
28
|
+
external_id: @post.external_id, publish_at: timestamp(@post.publish_at),
|
|
29
|
+
public_revision_identifier: @post.public_revision&.identifier,
|
|
30
|
+
approved: approved?,
|
|
31
|
+
preview_url: @post.previewable? ? PreviewSerializer.call(@post, base_url: @base_url)[:preview_url] : nil, reading_time_minutes: @post.reading_time_minutes, word_count: @post.word_count,
|
|
32
|
+
created_at: timestamp(@post.created_at), updated_at: timestamp(@post.updated_at))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def approved?
|
|
38
|
+
return false unless @post.public_revision_id
|
|
39
|
+
if @post.association(:approvals).loaded?
|
|
40
|
+
@post.approvals.any? { |approval| approval.revision_id == @post.public_revision_id && approval.facts_checked }
|
|
41
|
+
else
|
|
42
|
+
@post.approvals.exists?(revision_id: @post.public_revision_id, facts_checked: true)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def author
|
|
47
|
+
record = @post.author
|
|
48
|
+
{ id: record.id, name: record.name, slug: record.slug, type: record.author_type, url: record.url }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def taxonomy(record)
|
|
52
|
+
{ id: record.id, name: record.name, slug: record.slug } if record
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def timestamp(value)
|
|
56
|
+
value&.iso8601
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class PreviewSerializer
|
|
3
|
+
def self.call(post, base_url: nil)
|
|
4
|
+
credentials = post.preview_credentials
|
|
5
|
+
origin = OpenBlog.config.public_base_url || base_url
|
|
6
|
+
path = "#{OpenBlog.mount_path.chomp('/')}/preview/#{ERB::Util.url_encode(credentials.fetch(:token))}"
|
|
7
|
+
{ preview_url: "#{origin.to_s.chomp('/')}#{path}", revision_identifier: post.current_revision_identifier,
|
|
8
|
+
expires_at: credentials.fetch(:expires_at).iso8601 }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class RecordsSerializer
|
|
3
|
+
BASELINE_FIELDS = %i[post_id adopted_at adopted_revision_id provenance provenance_evidence first_published_at
|
|
4
|
+
first_published_evidence declared_first_published_at last_modified_at last_modified_evidence
|
|
5
|
+
source_system source_id source_body_sha256 adopted_by].freeze
|
|
6
|
+
CONNECTION_FIELDS = %i[post_id connections third_party_paid declared_by declared_on recorded_by].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(post)
|
|
9
|
+
{
|
|
10
|
+
revisions: post.revisions.order(:id).map { |row| fields(row, %i[identifier actor made_by_ai created_at]) },
|
|
11
|
+
approvals: post.approvals.includes(:revision).order(:id).map do |row|
|
|
12
|
+
fields(row, %i[kind reviewer_name facts_checked approved_at declared_on declared_by confirmed_by evidence recorded_by])
|
|
13
|
+
.merge(revision_identifier: row.revision.identifier)
|
|
14
|
+
end,
|
|
15
|
+
publications: post.publications.includes(:revision).order(:id).map do |row|
|
|
16
|
+
fields(row, %i[entry_type occurred_at released_by description note]).merge(revision_identifier: row.revision&.identifier)
|
|
17
|
+
end,
|
|
18
|
+
baseline: post.baseline && fields(post.baseline, BASELINE_FIELDS),
|
|
19
|
+
connections: post.connection_declarations.order(:id).map { |row| fields(row, CONNECTION_FIELDS) }
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.fields(record, names)
|
|
24
|
+
names.to_h { |name| [ name, record.public_send(name) ] }
|
|
25
|
+
end
|
|
26
|
+
private_class_method :fields
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class SeriesSerializer
|
|
3
|
+
def self.call(record)
|
|
4
|
+
{ id: record.id, name: record.name, slug: record.slug, description: record.description, posts: record.posts.order(:series_position, :id).map { |post| { id: post.id, slug: post.slug, title: post.title, position: post.series_position } } }
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class WriteResultSerializer
|
|
3
|
+
def self.call(result, base_url: nil)
|
|
4
|
+
{ post: PostSerializer.call(result.post, base_url: base_url), created: result.created,
|
|
5
|
+
records: result.records.slice(:revision, :publication, :approval), label: result.label.to_s,
|
|
6
|
+
findings: FindingsSerializer.entries(result.findings) }
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<img src="<%= image.path %>" alt="<%= alt %>" title="<%= caption %>"<% if image.width %> width="<%= image.width %>"<% end %><% if image.height %> height="<%= image.height %>"<% end %>>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
2
|
+
xml.feed xmlns: "http://www.w3.org/2005/Atom" do
|
|
3
|
+
xml.id "#{@base_url}#{OpenBlog.mount_path}"
|
|
4
|
+
xml.title @feed_title
|
|
5
|
+
xml.updated @feed_updated_at.iso8601
|
|
6
|
+
xml.author { xml.name(OpenBlog.config.publisher&.fetch(:name, nil) || OpenBlog.config.site_name) }
|
|
7
|
+
xml.link href: "#{@base_url}#{request.path}", rel: "self", type: "application/atom+xml"
|
|
8
|
+
xml.link href: "#{@base_url}#{OpenBlog.mount_path}", rel: "alternate", type: "text/html"
|
|
9
|
+
@posts.each do |post|
|
|
10
|
+
xml.entry do
|
|
11
|
+
xml.id post.url(base: @base_url)
|
|
12
|
+
xml.title post.title
|
|
13
|
+
xml.link href: post.url(base: @base_url), rel: "alternate", type: "text/html"
|
|
14
|
+
xml.published OpenBlog::ReaderDates.published_at(post).iso8601 if OpenBlog::ReaderDates.published_at(post)
|
|
15
|
+
xml.updated OpenBlog::ReaderDates.feed_updated_at(post).iso8601
|
|
16
|
+
xml.author do
|
|
17
|
+
xml.name post.author_name
|
|
18
|
+
xml.uri "#{@base_url}#{OpenBlog::ReaderQueries.author_path(post.author)}"
|
|
19
|
+
end
|
|
20
|
+
if OpenBlog.config.feed_content == :full
|
|
21
|
+
xml.content OpenBlog::Renderer.render(post), type: "html"
|
|
22
|
+
else
|
|
23
|
+
xml.summary post.description, type: "text"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
xml.instruct! :xml, version: "1.0", encoding: "UTF-8"
|
|
2
|
+
xml.urlset xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" do
|
|
3
|
+
@entries.each do |entry|
|
|
4
|
+
xml.url do
|
|
5
|
+
xml.loc entry.fetch(:loc)
|
|
6
|
+
xml.lastmod entry[:lastmod].iso8601 if entry[:lastmod]
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
en:
|
|
2
|
+
open_blog:
|
|
3
|
+
search:
|
|
4
|
+
heading: "Search articles"
|
|
5
|
+
label: "Search the blog"
|
|
6
|
+
submit: "Search"
|
|
7
|
+
suggestions: "Suggested articles"
|
|
8
|
+
description: "Find articles by their title, text, or frequently asked questions."
|
|
9
|
+
empty: "No matching articles. Try another word or phrase."
|
|
10
|
+
notices:
|
|
11
|
+
ai_assisted: "AI helped to make this post."
|
|
12
|
+
ai_unknown: "The use of AI for this post is not known."
|
|
13
|
+
paid: "A third party paid for this post."
|
|
14
|
+
disclosure: "Disclosure"
|
|
15
|
+
connections_none: "The author declared no relevant connections."
|
|
16
|
+
correction: "Correction"
|
|
17
|
+
dates:
|
|
18
|
+
published: "Published"
|
|
19
|
+
updated: "Updated"
|
|
20
|
+
reading_time:
|
|
21
|
+
one: "%{count} minute read"
|
|
22
|
+
other: "%{count} minute read"
|
|
23
|
+
navigation:
|
|
24
|
+
blog: "Blog navigation"
|
|
25
|
+
footer: "Blog resources"
|
|
26
|
+
breadcrumbs: "Breadcrumbs"
|
|
27
|
+
categories: "Categories"
|
|
28
|
+
all_posts: "All posts"
|
|
29
|
+
popular: "Popular articles"
|
|
30
|
+
tags: "Tags"
|
|
31
|
+
feed: "Atom feed"
|
|
32
|
+
responsible_party: "Responsible party"
|
|
33
|
+
toc: "On this page"
|
|
34
|
+
pagination: "Pagination"
|
|
35
|
+
series: "In this series"
|
|
36
|
+
previous_post: "Previous post"
|
|
37
|
+
next_post: "Next post"
|
|
38
|
+
page: "Page %{page}"
|
|
39
|
+
titles:
|
|
40
|
+
preview_expired: "This preview link has stopped"
|
|
41
|
+
page: "%{title} — Page %{page}"
|
|
42
|
+
tag: "Tagged: %{name}"
|
|
43
|
+
not_found: "Page not found"
|
|
44
|
+
gone: "This page has been removed"
|
|
45
|
+
descriptions:
|
|
46
|
+
index: "The blog of %{site_name}."
|
|
47
|
+
index_page: "Page %{page} of the blog of %{site_name}."
|
|
48
|
+
category: "Posts about %{name} — %{site_name}."
|
|
49
|
+
category_page: "Page %{page} of the posts about %{name} — %{site_name}."
|
|
50
|
+
tag: "Posts tagged %{name} — %{site_name}."
|
|
51
|
+
tag_page: "Page %{page} of the posts tagged %{name} — %{site_name}."
|
|
52
|
+
author: "Posts by %{name} — %{site_name}."
|
|
53
|
+
author_page: "Page %{page} of the posts by %{name} — %{site_name}."
|
|
54
|
+
series: "Posts in %{name} — %{site_name}."
|
|
55
|
+
series_page: "Page %{page} of the posts in %{name} — %{site_name}."
|
|
56
|
+
headings:
|
|
57
|
+
link: "Link to %{heading}"
|
|
58
|
+
highlights: "Article highlights"
|
|
59
|
+
about_author: "About the author"
|
|
60
|
+
related: "Related posts"
|
|
61
|
+
recent: "Recent posts"
|
|
62
|
+
cards:
|
|
63
|
+
featured: "Featured"
|
|
64
|
+
faq:
|
|
65
|
+
heading: "Frequently asked questions"
|
|
66
|
+
lists:
|
|
67
|
+
empty: "There are no posts here yet."
|
|
68
|
+
share:
|
|
69
|
+
heading: "Share this post"
|
|
70
|
+
x: "Share on X"
|
|
71
|
+
linkedin: "Share on LinkedIn"
|
|
72
|
+
device: "Share"
|
|
73
|
+
copy: "Copy link"
|
|
74
|
+
copied: "Copied!"
|
|
75
|
+
failed: "Sharing failed. Please try again."
|
|
76
|
+
theme:
|
|
77
|
+
system: "Theme: System"
|
|
78
|
+
light: "Theme: Light"
|
|
79
|
+
dark: "Theme: Dark"
|
|
80
|
+
code:
|
|
81
|
+
copy: "Copy"
|
|
82
|
+
copy_label: "Copy code"
|
|
83
|
+
copied: "Copied"
|
|
84
|
+
copy_failed: "Copy failed"
|
|
85
|
+
policies:
|
|
86
|
+
responsible_party: "Responsible party"
|
|
87
|
+
corrections: "Corrections policy"
|
|
88
|
+
editorial: "Editorial policy"
|
|
89
|
+
ai_use: "AI use policy"
|
|
90
|
+
errors:
|
|
91
|
+
not_found: "We could not find this page. Browse the latest posts below."
|
|
92
|
+
gone: "This page is no longer available. Browse the latest posts below."
|
|
93
|
+
preview:
|
|
94
|
+
heading: "Draft preview"
|
|
95
|
+
stopped: "Ask the author for a new preview link."
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
OpenBlog::Engine.routes.draw do
|
|
2
|
+
match "/mcp", to: "mcp#create", via: [ :post, :get, :delete ]
|
|
3
|
+
get "/preview/:token", to: "previews#show", as: :preview
|
|
4
|
+
get "/policies/:slug", to: "pages#show", as: :policy_page, format: false
|
|
5
|
+
namespace :api do
|
|
6
|
+
namespace :v1 do
|
|
7
|
+
patch "posts/:id", to: "posts#update"
|
|
8
|
+
resources :posts, only: [ :index, :show, :create, :destroy ] do
|
|
9
|
+
post :publish, on: :member
|
|
10
|
+
post :unpublish, on: :member
|
|
11
|
+
resources :approvals, only: :create
|
|
12
|
+
resources :connections, only: :create
|
|
13
|
+
resource :views, only: :show, controller: "views"
|
|
14
|
+
resource :preview, only: :show, controller: "previews"
|
|
15
|
+
resource :records, only: :show, controller: "records"
|
|
16
|
+
resource :findings, only: :show, controller: "findings"
|
|
17
|
+
end
|
|
18
|
+
resources :images, only: :create
|
|
19
|
+
%i[categories authors series].each do |resource|
|
|
20
|
+
resources resource, only: [ :index, :create ]
|
|
21
|
+
patch "#{resource}/:id", to: "#{resource}#update"
|
|
22
|
+
end
|
|
23
|
+
resources :tags, only: :index
|
|
24
|
+
resources :redirects, only: [ :index, :create, :destroy ]
|
|
25
|
+
resources :adoptions, only: :create
|
|
26
|
+
resources :faq_extractions, only: :create
|
|
27
|
+
get "pages", to: "pages#index"
|
|
28
|
+
get "pages/:kind", to: "pages#show"
|
|
29
|
+
put "pages/:kind", to: "pages#update"
|
|
30
|
+
get "views/top", to: "views#top"
|
|
31
|
+
get "report", to: "report#show"
|
|
32
|
+
get "doctor", to: "doctor#show"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
root "posts#index"
|
|
36
|
+
get "/search", to: "search#show", as: :search
|
|
37
|
+
get "/#{OpenBlog.config.route_segments.fetch(:series)}/:slug", to: "series#show", as: :series
|
|
38
|
+
get "/feed.json", to: "feeds#show", as: :feed_json, format: false, defaults: { format: :json }
|
|
39
|
+
get "/feed.xml", to: "feeds#show", as: :feed, format: false, defaults: { format: :xml }
|
|
40
|
+
get "/sitemap.xml", to: "sitemaps#show", as: :sitemap, format: false, defaults: { format: :xml }
|
|
41
|
+
get "/media/:sha256/:filename", to: "media#show", as: :media, format: false, constraints: { sha256: /[0-9a-f]{64}/, filename: /[^\/]+/ }
|
|
42
|
+
get "/#{OpenBlog.config.route_segments.fetch(:category)}/:slug/feed.json", to: "feeds#show", as: :category_feed_json, format: false, defaults: { format: :json }
|
|
43
|
+
get "/#{OpenBlog.config.route_segments.fetch(:category)}/:slug/feed.xml", to: "feeds#show", as: :category_feed, format: false, defaults: { format: :xml }
|
|
44
|
+
get "/#{OpenBlog.config.route_segments.fetch(:category)}/:slug", to: "categories#show", as: :category
|
|
45
|
+
get "/#{OpenBlog.config.route_segments.fetch(:tag)}/:slug", to: "tags#show", as: :tag
|
|
46
|
+
get "/#{OpenBlog.config.route_segments.fetch(:author)}/:slug", to: "authors#show", as: :author
|
|
47
|
+
get "/:slug", to: "posts#show", as: :post, constraints: ->(request) {
|
|
48
|
+
slug = request.path_parameters[:slug]
|
|
49
|
+
slug.match?(/\A[a-z0-9]+(?:[-_][a-z0-9]+)*\z/) && !(OpenBlog::Post::RESERVED_SLUGS + OpenBlog.config.route_segments.values).include?(slug)
|
|
50
|
+
}
|
|
51
|
+
get "/*path", to: "errors#not_found"
|
|
52
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateOpenBlogAuthors < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :open_blog_authors do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.string :slug, null: false
|
|
6
|
+
t.string :author_type, null: false, default: "person"
|
|
7
|
+
t.text :bio
|
|
8
|
+
t.string :url
|
|
9
|
+
t.public_send(connection.adapter_name == "PostgreSQL" ? :jsonb : :json, :profile_urls, null: false, default: [])
|
|
10
|
+
t.string :host_reference
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :open_blog_authors, :slug, unique: true
|
|
14
|
+
add_check_constraint :open_blog_authors, "author_type IN ('person', 'organization')", name: "open_blog_authors_type"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class CreateOpenBlogCategories < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :open_blog_categories do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.string :slug, null: false
|
|
6
|
+
t.text :description
|
|
7
|
+
t.integer :position, null: false, default: 0
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
add_index :open_blog_categories, :name, unique: true
|
|
11
|
+
add_index :open_blog_categories, :slug, unique: true
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
class CreateOpenBlogSeries < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :open_blog_series do |t|
|
|
4
|
+
t.string :name, null: false
|
|
5
|
+
t.string :slug, null: false
|
|
6
|
+
t.text :description
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
add_index :open_blog_series, :slug, unique: true
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateOpenBlogImages < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :open_blog_images do |t|
|
|
4
|
+
t.string :sha256, limit: 64, null: false
|
|
5
|
+
t.string :filename, null: false
|
|
6
|
+
t.string :content_type, null: false
|
|
7
|
+
t.bigint :byte_size, null: false
|
|
8
|
+
t.integer :width
|
|
9
|
+
t.integer :height
|
|
10
|
+
t.string :source_url
|
|
11
|
+
t.string :uploaded_by
|
|
12
|
+
t.datetime :created_at, null: false
|
|
13
|
+
end
|
|
14
|
+
add_index :open_blog_images, :sha256, unique: true
|
|
15
|
+
add_check_constraint :open_blog_images, "byte_size >= 0", name: "open_blog_images_size"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class CreateOpenBlogPosts < ActiveRecord::Migration[8.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :open_blog_posts do |t|
|
|
4
|
+
t.string :slug, null: false
|
|
5
|
+
t.string :title, null: false
|
|
6
|
+
t.text :description, null: false, default: ""
|
|
7
|
+
t.string :search_title, null: false, default: ""
|
|
8
|
+
t.text :search_description, null: false, default: ""
|
|
9
|
+
t.string :body_format, null: false
|
|
10
|
+
t.text :body_markdown
|
|
11
|
+
t.references :author, null: false, foreign_key: { to_table: :open_blog_authors }
|
|
12
|
+
t.string :author_name, null: false
|
|
13
|
+
t.references :category, foreign_key: { to_table: :open_blog_categories }
|
|
14
|
+
t.references :series, foreign_key: { to_table: :open_blog_series }
|
|
15
|
+
t.integer :series_position
|
|
16
|
+
t.string :status, null: false, default: "draft"
|
|
17
|
+
t.datetime :published_at
|
|
18
|
+
t.datetime :modified_at
|
|
19
|
+
t.datetime :publish_at
|
|
20
|
+
t.string :provenance, null: false, default: "unknown"
|
|
21
|
+
t.text :provenance_evidence
|
|
22
|
+
t.boolean :featured, null: false, default: false
|
|
23
|
+
t.string :canonical_url, limit: 2048
|
|
24
|
+
t.string :external_id
|
|
25
|
+
t.references :cover_image, foreign_key: { to_table: :open_blog_images }
|
|
26
|
+
t.string :cover_alt, null: false, default: ""
|
|
27
|
+
t.references :social_image, foreign_key: { to_table: :open_blog_images }
|
|
28
|
+
t.string :current_revision_identifier, limit: 64
|
|
29
|
+
t.references :public_revision
|
|
30
|
+
t.integer :word_count, null: false, default: 0
|
|
31
|
+
t.integer :reading_time_minutes, null: false, default: 1
|
|
32
|
+
t.text :search_text
|
|
33
|
+
t.timestamps
|
|
34
|
+
end
|
|
35
|
+
add_index :open_blog_posts, :slug, unique: true
|
|
36
|
+
add_index :open_blog_posts, :external_id, unique: true
|
|
37
|
+
add_index :open_blog_posts, [ :status, :published_at ]
|
|
38
|
+
add_index :open_blog_posts, [ :status, :publish_at ]
|
|
39
|
+
add_index :open_blog_posts, [ :series_id, :series_position ], unique: true
|
|
40
|
+
if connection.adapter_name == "PostgreSQL"
|
|
41
|
+
add_index :open_blog_posts, "to_tsvector('simple', search_text)", using: :gin, name: "index_open_blog_posts_search"
|
|
42
|
+
end
|
|
43
|
+
add_check_constraint :open_blog_posts, "body_format IN ('markdown', 'rich_text')", name: "open_blog_posts_body_format"
|
|
44
|
+
add_check_constraint :open_blog_posts, "status IN ('draft', 'scheduled', 'published', 'archived')", name: "open_blog_posts_status"
|
|
45
|
+
add_check_constraint :open_blog_posts, "provenance IN ('ai_assisted', 'human_written', 'unknown')", name: "open_blog_posts_provenance"
|
|
46
|
+
end
|
|
47
|
+
end
|