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,205 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class WritePost
|
|
3
|
+
def self.call(attributes, post:, actor:, now:, publish:, authorize: nil)
|
|
4
|
+
attributes = PostAttributes.normalize(attributes)
|
|
5
|
+
attributes = attributes.except(:external_id) if post&.persisted?
|
|
6
|
+
source = post&.persisted? ? Post.find(post.id) : PostIdentity.resolve(attributes, post: post)
|
|
7
|
+
authorize&.call(source)
|
|
8
|
+
image_inputs = ImageResolution.prepare_inputs(attributes)
|
|
9
|
+
images = ImageResolution.prepare(attributes: attributes, post: source)
|
|
10
|
+
context = {}
|
|
11
|
+
Operation.run(post: post, attributes: attributes, actor: actor, now: now, context: context, authorize: authorize) do |current, created|
|
|
12
|
+
old_slug, was_public = current.slug, current.published?
|
|
13
|
+
context[:author_default_used] = created && !attributes.key?(:author)
|
|
14
|
+
approval = attributes[:approval]
|
|
15
|
+
context[:approval_incomplete] = approval.is_a?(Hash) && approval.with_indifferent_access[:facts_checked] == false
|
|
16
|
+
release = publish == :preserve ? current.published? : publish
|
|
17
|
+
records = RedirectTarget.synchronize do
|
|
18
|
+
new(current, attributes, actor: actor, now: now, publish: release, images: images, image_inputs: image_inputs).call
|
|
19
|
+
end
|
|
20
|
+
context[:slug_changed] = was_public && old_slug != current.slug
|
|
21
|
+
records
|
|
22
|
+
end
|
|
23
|
+
rescue Error => error
|
|
24
|
+
Operation.failure(post, error)
|
|
25
|
+
rescue ActiveRecord::RecordNotFound
|
|
26
|
+
Operation.failure(post, Error::NotFound.new)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(post, attributes, actor:, now:, publish:, images:, image_inputs: {})
|
|
30
|
+
@post, @attributes, @actor, @now, @publish = post, attributes, actor, now, publish
|
|
31
|
+
@scheduled_release = publish == :scheduled
|
|
32
|
+
@images, @image_inputs = images, image_inputs
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def call
|
|
36
|
+
raise Error::PostIsPublic if !@publish && @post.published?
|
|
37
|
+
old_path = @post.path if @post.persisted?
|
|
38
|
+
was_public = @post.published?
|
|
39
|
+
previous_schedule = @post.publish_at
|
|
40
|
+
PostAttributes.assign(@post, @attributes.except(*@image_inputs.keys), actor: @actor, now: @now)
|
|
41
|
+
@image_inputs.each { |field, prepared| @post.public_send("#{field}=", prepared.image) }
|
|
42
|
+
PostIdentity.resolve({ slug: @post.slug }, post: @post)
|
|
43
|
+
apply_status
|
|
44
|
+
validate_records
|
|
45
|
+
@image_inputs.each { |field, prepared| @post.public_send("#{field}=", prepared.persist(uploaded_by: @actor)) }
|
|
46
|
+
@images.materialize(@post, actor: @actor)
|
|
47
|
+
@post.send(:compute_derived)
|
|
48
|
+
validate_approval
|
|
49
|
+
validate_change
|
|
50
|
+
enforce_gates if @publish && !cancel_schedule?
|
|
51
|
+
@images.preview(@post)
|
|
52
|
+
@post.send(:compute_derived)
|
|
53
|
+
validate_approval
|
|
54
|
+
validate_change
|
|
55
|
+
enforce_approval_gate if @publish && !cancel_schedule?
|
|
56
|
+
@post.release_context = { actor: @actor, now: @now, change: @attributes[:change],
|
|
57
|
+
note: @attributes[:note], description: @attributes[:change_description], made_by_ai: @attributes[:made_by_ai] }
|
|
58
|
+
@post.save!
|
|
59
|
+
records = @post.release_records || { revision: nil, publication: nil, approval: nil }
|
|
60
|
+
store_approval(records) if @attributes.key?(:approval)
|
|
61
|
+
store_connections if @attributes.key?(:connections)
|
|
62
|
+
update_redirects(old_path, was_public)
|
|
63
|
+
enqueue if @post.scheduled? && previous_schedule != @post.publish_at
|
|
64
|
+
records
|
|
65
|
+
ensure
|
|
66
|
+
@post.release_context = nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def cancel_schedule?
|
|
72
|
+
@cancel_schedule
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def apply_status
|
|
76
|
+
@cancel_schedule = @post.scheduled? && @attributes.key?(:publish_at) && @attributes[:publish_at].nil?
|
|
77
|
+
if @attributes.key?(:publish_at)
|
|
78
|
+
value = @attributes[:publish_at]
|
|
79
|
+
time = value.is_a?(String) ? Time.zone.parse(value) : value
|
|
80
|
+
unless value.nil? || time.respond_to?(:to_time)
|
|
81
|
+
raise Error::ValidationFailed.new(details: [ "publish_at" ])
|
|
82
|
+
end
|
|
83
|
+
@post.publish_at = time
|
|
84
|
+
end
|
|
85
|
+
if @cancel_schedule
|
|
86
|
+
@post.status = "draft"
|
|
87
|
+
elsif @post.publish_at && @attributes.key?(:publish_at) && @post.publish_at > @now
|
|
88
|
+
raise Error::ValidationFailed.new(details: [ "publish_at" ]) unless @publish || @post.scheduled?
|
|
89
|
+
@post.status = "scheduled"
|
|
90
|
+
elsif @publish
|
|
91
|
+
@post.status = "published"
|
|
92
|
+
@post.publish_at = nil
|
|
93
|
+
end
|
|
94
|
+
rescue ArgumentError, TypeError
|
|
95
|
+
raise Error::ValidationFailed.new(details: [ "publish_at" ])
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def validate_records
|
|
99
|
+
if @attributes.key?(:change) && !%w[substantive correction maintenance].include?(@attributes[:change].to_s)
|
|
100
|
+
raise Error::ValidationFailed.new(details: [ "change" ])
|
|
101
|
+
end
|
|
102
|
+
if @attributes[:change].to_s == "correction" && @attributes[:note].blank?
|
|
103
|
+
raise Error::CorrectionNoteRequired
|
|
104
|
+
end
|
|
105
|
+
if @attributes.key?(:made_by_ai) && ![ true, false, nil ].include?(@attributes[:made_by_ai])
|
|
106
|
+
raise Error::ValidationFailed.new(details: [ "made_by_ai" ])
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def validate_approval
|
|
111
|
+
return unless @attributes.key?(:approval)
|
|
112
|
+
value = @attributes[:approval]
|
|
113
|
+
unless value.is_a?(Hash)
|
|
114
|
+
raise Error::ApprovalIncomplete
|
|
115
|
+
end
|
|
116
|
+
@approval = value.symbolize_keys
|
|
117
|
+
unless @approval[:name].is_a?(String) && @approval[:name].present? && [ true, false ].include?(@approval[:facts_checked])
|
|
118
|
+
raise Error::ApprovalIncomplete
|
|
119
|
+
end
|
|
120
|
+
if @approval.key?(:revision_identifier) && @approval[:revision_identifier] != @post.current_revision_identifier
|
|
121
|
+
raise Error::RevisionMismatch
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def validate_change
|
|
126
|
+
return if @scheduled_release
|
|
127
|
+
return unless @publish && @post.published? && @post.public_revision
|
|
128
|
+
if @post.public_revision.identifier != @post.current_revision_identifier && @attributes[:change].blank?
|
|
129
|
+
raise Error::ChangeTypeRequired
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def enforce_gates
|
|
134
|
+
enforce_approval_gate
|
|
135
|
+
messages = OpenBlog.config.before_publish&.call(@post, { actor: @actor, now: @now, attributes: @attributes })
|
|
136
|
+
raise Error::RefusedByHost.new(details: Array(messages)) if messages.present?
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def enforce_approval_gate
|
|
140
|
+
return if @scheduled_release
|
|
141
|
+
if OpenBlog.config.require_approval && !@post.provenance_human_written?
|
|
142
|
+
approved = @approval && @approval[:facts_checked] == true
|
|
143
|
+
approved ||= @post.approvals.joins(:revision).exists?(facts_checked: true, open_blog_revisions: { identifier: @post.current_revision_identifier }) if @post.persisted?
|
|
144
|
+
raise Error::ApprovalRequired unless approved
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def store_approval(records)
|
|
149
|
+
revision = @post.revisions.find_by(identifier: @post.current_revision_identifier)
|
|
150
|
+
if revision
|
|
151
|
+
records[:revision] ||= "same"
|
|
152
|
+
else
|
|
153
|
+
revision = @post.revisions.create!(identifier: @post.current_revision_identifier,
|
|
154
|
+
payload: RevisionPayload.new(@post).to_json, actor: @actor, made_by_ai: @attributes[:made_by_ai], created_at: @now)
|
|
155
|
+
records[:revision] = "new"
|
|
156
|
+
end
|
|
157
|
+
@post.approvals.create!(revision: revision, kind: "sent", reviewer_name: @approval[:name],
|
|
158
|
+
facts_checked: @approval[:facts_checked], approved_at: @now, recorded_by: @actor)
|
|
159
|
+
records[:approval] = "sent"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def store_connections
|
|
163
|
+
input = @attributes[:connections]
|
|
164
|
+
unless input.is_a?(Hash)
|
|
165
|
+
raise Error::ValidationFailed.new(details: [ "connections" ])
|
|
166
|
+
end
|
|
167
|
+
input = input.symbolize_keys
|
|
168
|
+
if (input.keys - %i[connections third_party_paid declared_by declared_on]).any? || ![ true, false ].include?(input[:third_party_paid])
|
|
169
|
+
raise Error::ValidationFailed.new(details: [ "connections" ])
|
|
170
|
+
end
|
|
171
|
+
if input[:connections].is_a?(Array)
|
|
172
|
+
input[:connections].each do |entry|
|
|
173
|
+
next unless entry.is_a?(Hash)
|
|
174
|
+
unknown = entry.keys.map(&:to_s) - %w[party relation]
|
|
175
|
+
raise Error::UnknownField.new(details: unknown.map { |key| "connections.connections.#{key}" }) if unknown.any?
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
input[:declared_on] = @now.to_date unless input.key?(:declared_on)
|
|
179
|
+
@post.connection_declarations.create!(**input, recorded_by: @actor, created_at: @now)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def update_redirects(old_path, was_public)
|
|
183
|
+
if old_path && old_path != @post.path && was_public
|
|
184
|
+
redirect = Redirect.find_or_initialize_by(old_path: old_path)
|
|
185
|
+
raise Error::SlugReserved if redirect.persisted? && redirect.post_id != @post.id
|
|
186
|
+
if redirect.new_record?
|
|
187
|
+
redirect.assign_attributes(post: @post, source: "slug_change", occurred_on: @now.to_date)
|
|
188
|
+
end
|
|
189
|
+
redirect.update!(new_path: @post.path)
|
|
190
|
+
Redirect.where(new_path: old_path).update_all(new_path: @post.path, updated_at: @now)
|
|
191
|
+
end
|
|
192
|
+
if @post.published?
|
|
193
|
+
Redirect.where(old_path: @post.path, post: @post).destroy_all
|
|
194
|
+
Redirect.where(post: @post, source: %w[slug_change adoption unpublish removal]).update_all(new_path: @post.path, updated_at: @now)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def enqueue
|
|
199
|
+
id, due = @post.id, @post.publish_at
|
|
200
|
+
ActiveRecord.after_all_transactions_commit do
|
|
201
|
+
PublishScheduledPostJob.set(wait_until: due).perform_later(id)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
data/lib/open_blog.rb
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "rails"
|
|
3
|
+
require "commonmarker"
|
|
4
|
+
require "rouge"
|
|
5
|
+
require "mcp"
|
|
6
|
+
|
|
7
|
+
require_relative "open_blog/version"
|
|
8
|
+
require_relative "open_blog/instructions"
|
|
9
|
+
|
|
10
|
+
module OpenBlog
|
|
11
|
+
FAQ_SELECTORS = { section: "[data-open-blog-faq]", entry: "[data-open-blog-faq-entry]" }.freeze
|
|
12
|
+
|
|
13
|
+
autoload :Configuration, "open_blog/configuration"
|
|
14
|
+
autoload :ConfigurationError, "open_blog/configuration_error"
|
|
15
|
+
autoload :RevisionPayload, "open_blog/revision_payload"
|
|
16
|
+
autoload :PlainText, "open_blog/plain_text"
|
|
17
|
+
autoload :Result, "open_blog/result"
|
|
18
|
+
autoload :Error, "open_blog/errors"
|
|
19
|
+
autoload :PostIdentity, "open_blog/post_identity"
|
|
20
|
+
autoload :PostAttributes, "open_blog/post_attributes"
|
|
21
|
+
autoload :Operation, "open_blog/operation"
|
|
22
|
+
autoload :WritePost, "open_blog/write_post"
|
|
23
|
+
autoload :SaveDraft, "open_blog/save_draft"
|
|
24
|
+
autoload :Publish, "open_blog/publish"
|
|
25
|
+
autoload :Approve, "open_blog/approve"
|
|
26
|
+
autoload :RedirectTarget, "open_blog/redirect_target"
|
|
27
|
+
autoload :Remove, "open_blog/remove"
|
|
28
|
+
autoload :Unpublish, "open_blog/unpublish"
|
|
29
|
+
autoload :RecordRelease, "open_blog/record_release"
|
|
30
|
+
autoload :ContentGuard, "open_blog/content_guard"
|
|
31
|
+
autoload :RichTextGuard, "open_blog/rich_text_guard"
|
|
32
|
+
autoload :PolicyUrl, "open_blog/policy_url"
|
|
33
|
+
autoload :LabelPolicy, "open_blog/label_policy"
|
|
34
|
+
autoload :Findings, "open_blog/findings"
|
|
35
|
+
autoload :Adopt, "open_blog/adopt"
|
|
36
|
+
autoload :FaqExtraction, "open_blog/faq_extraction"
|
|
37
|
+
autoload :ImageFetch, "open_blog/image_fetch"
|
|
38
|
+
autoload :ImageImport, "open_blog/image_import"
|
|
39
|
+
autoload :ImageResolution, "open_blog/image_resolution"
|
|
40
|
+
autoload :Renderer, "open_blog/renderer"
|
|
41
|
+
autoload :SyntaxCss, "open_blog/syntax_css"
|
|
42
|
+
autoload :BuildCss, "open_blog/build_css"
|
|
43
|
+
autoload :ReaderPage, "open_blog/reader_page"
|
|
44
|
+
autoload :ReaderDates, "open_blog/reader_dates"
|
|
45
|
+
autoload :JsonFeed, "open_blog/json_feed"
|
|
46
|
+
autoload :MarkdownView, "open_blog/markdown_view"
|
|
47
|
+
autoload :ReaderQueries, "open_blog/reader_queries"
|
|
48
|
+
autoload :Search, "open_blog/search"
|
|
49
|
+
autoload :PageViews, "open_blog/page_views"
|
|
50
|
+
autoload :Pagination, "open_blog/pagination"
|
|
51
|
+
autoload :NotFound, "open_blog/not_found"
|
|
52
|
+
autoload :SitemapEntries, "open_blog/sitemap_entries"
|
|
53
|
+
autoload :SurfaceReport, "open_blog/surface_report"
|
|
54
|
+
autoload :Doctor, "open_blog/doctor"
|
|
55
|
+
autoload :Sample, "open_blog/sample"
|
|
56
|
+
autoload :Actor, "open_blog/actor"
|
|
57
|
+
autoload :Mcp, "open_blog/mcp"
|
|
58
|
+
autoload :ApiFields, "open_blog/api_fields"
|
|
59
|
+
autoload :Authentication, "open_blog/authentication"
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
def config
|
|
63
|
+
@config ||= Configuration.new
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def configure
|
|
67
|
+
yield config
|
|
68
|
+
config
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def policy_url(kind, config: self.config)
|
|
72
|
+
PolicyUrl.call(kind, config: config)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def sitemap_entries(base_url: config.public_base_url)
|
|
76
|
+
SitemapEntries.call(base_url: base_url)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def mount_path
|
|
80
|
+
application = Rails.application
|
|
81
|
+
return config.mount_path unless application&.initialized?
|
|
82
|
+
|
|
83
|
+
route = application.routes.routes.find do |candidate|
|
|
84
|
+
candidate.app.respond_to?(:app) && candidate.app.app == Engine
|
|
85
|
+
end
|
|
86
|
+
route ? route.path.spec.to_s : config.mount_path
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
require_relative "open_blog/engine"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
namespace :open_blog do
|
|
2
|
+
desc "Set up authentication during installation without replacing existing tokens"
|
|
3
|
+
task install_token: :environment do
|
|
4
|
+
if OpenBlog.config.authenticate
|
|
5
|
+
puts "API token: using host authentication."
|
|
6
|
+
elsif OpenBlog::ApiToken.exists?
|
|
7
|
+
puts "API token: existing token kept; use open_blog:token to issue another if needed."
|
|
8
|
+
else
|
|
9
|
+
_record, secret = OpenBlog::ApiToken.generate(name: "Blog publisher")
|
|
10
|
+
puts "API token: #{secret}"
|
|
11
|
+
puts "Save this secret now; it will not be shown again."
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Issue an API token and print its secret once"
|
|
16
|
+
task token: :environment do
|
|
17
|
+
name = ENV.fetch("NAME") { abort "Set NAME to identify this token's actor." }
|
|
18
|
+
scopes = ENV.key?("SCOPES") ? ENV.fetch("SCOPES").split(",").map(&:strip) : OpenBlog::ApiToken::SCOPES
|
|
19
|
+
expires_at = Time.iso8601(ENV.fetch("EXPIRES_AT")) if ENV["EXPIRES_AT"].present?
|
|
20
|
+
_record, secret = OpenBlog::ApiToken.generate(name: name, scopes: scopes, expires_at: expires_at)
|
|
21
|
+
puts secret
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
desc "Check the blog installation and report remaining setup"
|
|
25
|
+
task doctor: :environment do
|
|
26
|
+
checks = OpenBlog::Doctor.run
|
|
27
|
+
checks.each { |check| puts "#{check[:status]}: #{check[:name]} — #{check[:message]}" }
|
|
28
|
+
exit 1 if checks.any? { |check| check[:status].to_s == "error" }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "Create the sample article if it does not already exist"
|
|
32
|
+
task sample: :environment do
|
|
33
|
+
puts "Sample article: #{OpenBlog::Sample.call.path}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc "Generate scoped syntax colors for the configured light and dark theme"
|
|
37
|
+
task :syntax_css, [ :path ] => :environment do |_task, arguments|
|
|
38
|
+
puts "Wrote #{OpenBlog::SyntaxCss.write(path: arguments[:path])}"
|
|
39
|
+
end
|
|
40
|
+
desc "Build the packaged stylesheet and token-only theme override"
|
|
41
|
+
task build_css: :environment do
|
|
42
|
+
options = ENV["OUT"].present? ? { path: ENV["OUT"] } : {}
|
|
43
|
+
puts "Wrote #{OpenBlog::BuildCss.write(**options)}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
namespace :open_blog do
|
|
48
|
+
desc "Publish scheduled posts that are due"
|
|
49
|
+
task publish_due: :environment do
|
|
50
|
+
OpenBlog::Post.where(status: "scheduled").where("publish_at <= ?", Time.current).find_each do |post|
|
|
51
|
+
OpenBlog::PublishScheduledPostJob.perform_now(post.id)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
namespace :open_blog do
|
|
57
|
+
desc "Delete daily view totals older than the configured retention period"
|
|
58
|
+
task prune_page_views: :environment do
|
|
59
|
+
puts "Pruned #{OpenBlog::PageViews.prune!} daily view totals"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
namespace :open_blog do
|
|
64
|
+
desc "Inspect public pages and publishing records"
|
|
65
|
+
task report: :environment do
|
|
66
|
+
puts OpenBlog::SurfaceReport.run(scope: ENV.fetch("SCOPE", "site"), post: ENV["POST"],
|
|
67
|
+
page: ENV.fetch("PAGE", "1"), reach: ENV.fetch("REACH", "false")).to_text
|
|
68
|
+
rescue OpenBlog::Error => error
|
|
69
|
+
abort "#{error.code}: #{error.details.join(', ')}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: open-blog-adopt
|
|
3
|
+
description: Import existing articles into Open Blog with historical evidence, reviewed FAQ extraction, and dry runs.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Read the source articles without changing the source system. Preserve each exact slug, source identity, original body, and available historical records. Use `blog_extract_faq` to propose a separate FAQ list. Before applying a proposal, check that each answer is complete, the suggested cuts remove only FAQ content, the remaining body reads correctly, internal links still work, and the body does not duplicate the stored FAQ. Review ambiguous classifications with the user. Store source_body_sha256 when the submitted body differs from the original.
|
|
7
|
+
|
|
8
|
+
Ask whether the original system has an approval record for the imported article. Use imported_approval only with its reviewer, original time, evidence, and the person confirming that it covers this content. Otherwise, record a declaration only when the user supplies the reviewer, approval time, facts_checked answer, declaration date, and their own name. Ask for a declared first publication date when historical evidence is missing, and place that answer in declaration.declared_first_published_at. Do not send approval in an adoption request or invent historical evidence or declarations.
|
|
9
|
+
|
|
10
|
+
Use `blog_adopt_post` with dry_run true first. Inspect the proposed full article, notices, records, and redirects. A complete snapshot replaces omitted FAQ and tag lists with empty lists; send every field that must survive. Missing evidence leaves historical dates unknown. Do not assign today's date as an invented publication date.
|
|
11
|
+
|
|
12
|
+
After reviewing the dry-run result, import within the user's authorized migration scope. Check article totals, slugs, redirects, images, FAQ records, and notices. Reusing a source pair updates that import only until later publishing activity makes replacement unsafe; investigate an already_changed_in_gem refusal instead of forcing it.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: open-blog-install
|
|
3
|
+
description: Install and configure Open Blog in a Rails host, customize its reader theme, and check setup.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Install the gem in the requested Rails application, then run `bin/rails generate open_blog:install`. Inspect the host's JavaScript, Tailwind, image processor, mount path, and authentication setup before selecting options. Use `--skip-tailwind` when the host should keep its CSS configuration; use the generated theme override for branding.
|
|
7
|
+
|
|
8
|
+
Run `bin/rails open_blog:doctor` or `blog_doctor` and resolve relevant errors. Configure the actual publisher, author, public base URL, and policy links. Replace placeholders with user-supplied identities. Keep the initial API token secret; installation prints it once and repeat runs do not recover it. A host authentication hook takes precedence over tokens.
|
|
9
|
+
|
|
10
|
+
Check `tools/list` before using MCP tools. Finish with an actual reader request and authenticated `blog_list_posts` call. Do not publish unrelated content, change production routing, or deploy merely because installation was requested.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: open-blog-policy-pages
|
|
3
|
+
description: Prepare accurate editorial policy pages for an Open Blog publisher from the publisher’s own answers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use `blog_doctor` to inspect policy links. Read existing pages with `blog_get_site_page`, passing `kind`: `responsible_party`, `corrections`, `editorial`, or `ai_use`. A missing page returns `not_found`; start with the publisher’s answers below.
|
|
7
|
+
|
|
8
|
+
Ask for the accountable publisher and contact route, correction handling and reader reports, editorial selection and review practices, and how AI is used. Write only practices the user confirms. Reuse an existing host policy URL when that is the user's chosen source.
|
|
9
|
+
|
|
10
|
+
Show the final pages for approval before publishing. Use `blog_save_site_page` with `kind`, `title`, Markdown `body`, and `status: "draft"` while preparing text. After approval, save `status: "published"` with the actual `approved_by` name and ISO date `approved_on`; this requires both write and publish scopes. Do not fill approval fields from assumptions. Verify the published links and rendered text.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: open-blog-publish
|
|
3
|
+
description: Draft, review, publish, or schedule an Open Blog article using revision-bound approval.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Use `blog_search_posts` to check for existing coverage. Reuse or upload images with `blog_upload_image`; put their returned URLs in the body. Supply alternative text and keep FAQ answers as plain text.
|
|
7
|
+
|
|
8
|
+
Save with `blog_save_draft`, inspect the returned findings, and correct the draft before asking for approval. Use the returned preview URL or `blog_get_preview_link` so the user sees the version being reviewed. Keep its revision identifier with the answer.
|
|
9
|
+
|
|
10
|
+
Show the final text to the user before publication and ask once whether they approve that exact version and have checked its facts. Include the preview link for unpublished content. Record approval only from an actual answer: send the user's name, facts_checked with their stated true or false value, and the reviewed draft's revision_identifier. For an approved public edit without a saved draft identifier, send the complete reviewed content and inline approval with the user's name and facts_checked answer, omitting approval.revision_identifier so the operation binds it to the new revision. Never attach the old public identifier to changed text. Compare the returned content with the reviewed version and report any unexpected difference. If no answer was given, omit approval. Never invent a review or a fact-check. When text changes after approval, show the new version and ask again.
|
|
11
|
+
|
|
12
|
+
Set provenance to ai_assisted if AI authored or rewrote any content, or human_written only when a person wrote all of it.
|
|
13
|
+
|
|
14
|
+
Call `blog_publish_post` only within the user's publication request. Include the recorded approval when available; do not turn an unanswered fact-check question into true. A future `publish_at` schedules the post. Check the response state, notices, and findings; a schedule is not confirmation that the article is already public.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: open-blog-report
|
|
3
|
+
description: Inspect Open Blog diagnostics and available automated checks, then explain concrete findings and limits.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Start with `blog_doctor` for installation checks and `blog_check_post` for a particular article. Check `tools/list` before requesting broader reports. If `blog_get_surface_report` is available, request the relevant scope; opt into reach checks only when network checks fit the task.
|
|
7
|
+
|
|
8
|
+
Separate observed failures, warnings, skipped checks, and matters that require a person to assess. Automated checks describe observed behavior; they cannot certify editorial practices. Explain each actionable finding using the affected page or field and propose a proportionate correction.
|
|
9
|
+
|
|
10
|
+
Report only what the returned evidence supports. Do not fabricate results for unavailable tools, treat a successful HTTP response as proof of review, or silently rewrite and publish content while inspecting it.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: open-blog-update
|
|
3
|
+
description: Revise or correct a public Open Blog article while preserving its publishing history.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Read the current post and history with `blog_get_post` and `blog_get_post_records`. Choose substantive for a meaningful content revision, correction for an error that needs a reader-visible note, or maintenance for changes that should not advance the modification date.
|
|
7
|
+
|
|
8
|
+
A public article has one content state: an update is immediately public. Present the proposed final text before calling a write tool; there is no separate preview of edits to an already-public post. Fetch the current content again if another editor may have changed it.
|
|
9
|
+
|
|
10
|
+
Show the final text to the user before publication and ask once whether they approve that exact version and have checked its facts. Include the preview link for unpublished content. Record approval only from an actual answer: send the user's name, facts_checked with their stated true or false value, and the reviewed draft's revision_identifier. For an approved public edit without a saved draft identifier, send the complete reviewed content and inline approval with the user's name and facts_checked answer, omitting approval.revision_identifier so the operation binds it to the new revision. Never attach the old public identifier to changed text. Compare the returned content with the reviewed version and report any unexpected difference. If no answer was given, omit approval. Never invent a review or a fact-check. When text changes after approval, show the new version and ask again.
|
|
11
|
+
|
|
12
|
+
Set provenance to ai_assisted if AI authored or rewrote any content, or human_written only when a person wrote all of it.
|
|
13
|
+
|
|
14
|
+
Use `blog_update_post` with the selected change type, or `blog_correct_post` with a concrete correction note. Omitted fields remain unchanged; supplied FAQ and tag lists replace their lists. Read the returned findings and revision identifier. `blog_approve_revision` records a later review only for the current public identifier; never reuse an approval for text the reviewer did not see.
|
|
15
|
+
|
|
16
|
+
Include the complete reviewed title, body, FAQ list, and image fields in the update. An inline approval without an identifier binds to the resulting revision and works when the host requires approval. Compare the returned content with the reviewed version. A separate later approval is appropriate only when publication without approval is allowed and the user has reviewed that resulting revision.
|