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,19 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module Api
|
|
3
|
+
module V1
|
|
4
|
+
class ViewsController < BaseController
|
|
5
|
+
def show
|
|
6
|
+
require_scope!(:read)
|
|
7
|
+
input = input_fields!(*ApiFields::VIEWS)
|
|
8
|
+
render json: PageViews.for(find_post!, **input)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def top
|
|
12
|
+
require_scope!(:read)
|
|
13
|
+
input = input_fields!(*ApiFields::TOP_VIEWS)
|
|
14
|
+
render json: PageViews.top(**input, base_url: request.base_url)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class AuthorsController < ApplicationController
|
|
3
|
+
before_action :require_html
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
@author = Author.find_by!(slug: params[:slug])
|
|
7
|
+
pagination = paginate(ReaderQueries.posts.where(author_id: @author.id))
|
|
8
|
+
@posts = pagination.records
|
|
9
|
+
path = ReaderQueries.author_path(@author)
|
|
10
|
+
page_context(:author, record: @author, path: path, pagination: pagination,
|
|
11
|
+
breadcrumbs: [ { name: OpenBlog.config.blog_title, path: OpenBlog.mount_path }, { name: @author.name, path: path } ])
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class CategoriesController < ApplicationController
|
|
3
|
+
before_action :require_html
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
@category = Category.find_by!(slug: params[:slug])
|
|
7
|
+
pagination = paginate(ReaderQueries.posts.where(category_id: @category.id))
|
|
8
|
+
@posts = pagination.records
|
|
9
|
+
load_sidebar
|
|
10
|
+
path = ReaderQueries.category_path(@category)
|
|
11
|
+
page_context(:category, record: @category, path: path, pagination: pagination,
|
|
12
|
+
breadcrumbs: [ { name: OpenBlog.config.blog_title, path: OpenBlog.mount_path }, { name: @category.name, path: path } ])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class FeedsController < ApplicationController
|
|
3
|
+
def show
|
|
4
|
+
scope = ReaderQueries.posts
|
|
5
|
+
if params[:slug]
|
|
6
|
+
@category = Category.find_by!(slug: params[:slug])
|
|
7
|
+
scope = scope.where(category_id: @category.id)
|
|
8
|
+
end
|
|
9
|
+
@posts = scope.limit(OpenBlog.config.feed_size).to_a
|
|
10
|
+
@base_url = origin.chomp("/")
|
|
11
|
+
@feed_title = @category&.name || OpenBlog.config.blog_title
|
|
12
|
+
@feed_updated_at = @posts.map { |post| ReaderDates.feed_updated_at(post) }.max || Time.current
|
|
13
|
+
expires_in 1.hour, public: true
|
|
14
|
+
if request.format.json?
|
|
15
|
+
feed = JsonFeed.new(posts: @posts, base_url: @base_url, feed_path: request.path, title: @feed_title, category: @category)
|
|
16
|
+
render body: feed.to_json, content_type: "application/feed+json"
|
|
17
|
+
else
|
|
18
|
+
render "open_blog/feeds/atom", formats: [ :xml ], content_type: "application/atom+xml", layout: false
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module OpenBlog
|
|
4
|
+
class McpController < Api::V1::BaseController
|
|
5
|
+
prepend_before_action :check_transport!
|
|
6
|
+
|
|
7
|
+
def create
|
|
8
|
+
server = ::MCP::Server.new(name: "open_blog", tools: Mcp.tools,
|
|
9
|
+
server_context: { actor: actor, base_url: request.base_url })
|
|
10
|
+
result = server.handle_json(request.raw_post)
|
|
11
|
+
result.nil? ? head(:accepted) : render(body: result, content_type: "application/json")
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def check_transport!
|
|
17
|
+
prevent_caching!
|
|
18
|
+
raise Error::NotFound unless OpenBlog.config.mcp.enabled
|
|
19
|
+
unless request.post?
|
|
20
|
+
response.headers["Allow"] = "POST"
|
|
21
|
+
return head :method_not_allowed
|
|
22
|
+
end
|
|
23
|
+
return unless request.headers.key?("Origin")
|
|
24
|
+
origin = normalized_origin(request.headers["Origin"])
|
|
25
|
+
allowed = [ request.base_url, OpenBlog.config.public_base_url ].compact.map { |value| normalized_origin(value) }
|
|
26
|
+
raise Error::ScopeRequired.new(details: [ "origin" ]) unless origin && allowed.include?(origin)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def normalized_origin(value)
|
|
30
|
+
uri = URI.parse(value.to_s)
|
|
31
|
+
return unless uri.is_a?(URI::HTTP) && uri.host.present? && uri.userinfo.nil? &&
|
|
32
|
+
uri.query.nil? && uri.fragment.nil? && [ "", "/" ].include?(uri.path)
|
|
33
|
+
[ uri.scheme.downcase, uri.host.downcase, uri.port ]
|
|
34
|
+
rescue URI::InvalidURIError
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class MediaController < ApplicationController
|
|
3
|
+
include ActiveStorage::SetCurrent
|
|
4
|
+
rescue_from ActiveStorage::FileNotFoundError, with: :render_not_found
|
|
5
|
+
|
|
6
|
+
def show
|
|
7
|
+
image = Image.find_by!(sha256: params[:sha256])
|
|
8
|
+
raise NotFound unless image.file.attached?
|
|
9
|
+
if OpenBlog.config.image_delivery == :proxy
|
|
10
|
+
bytes = image.file.download
|
|
11
|
+
expires_in 365.days, public: true, immutable: true
|
|
12
|
+
send_data bytes, type: image.content_type, filename: image.filename, disposition: "inline"
|
|
13
|
+
else
|
|
14
|
+
lifetime = [ ActiveStorage.service_urls_expire_in.to_i, 365.days.to_i ].min
|
|
15
|
+
expires_in lifetime, public: true
|
|
16
|
+
redirect_to image.file.blob.url(expires_in: lifetime, disposition: "inline"), allow_other_host: true
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class PagesController < ApplicationController
|
|
3
|
+
before_action :require_html
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
@page = Page.published.find_by!(slug: params[:slug])
|
|
7
|
+
raise NotFound if OpenBlog.config.policy_urls[@page.kind.to_sym].present?
|
|
8
|
+
page_context(:page, record: @page, path: @page.path,
|
|
9
|
+
breadcrumbs: [ { name: OpenBlog.config.blog_title, path: OpenBlog.mount_path }, { name: @page.title, path: @page.path } ])
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class PostsController < ApplicationController
|
|
3
|
+
before_action :require_html, only: :index
|
|
4
|
+
before_action :require_post_format, only: :show
|
|
5
|
+
after_action :count_page_view, only: :show
|
|
6
|
+
|
|
7
|
+
def index
|
|
8
|
+
@featured = ReaderQueries.featured
|
|
9
|
+
scope = ReaderQueries.posts
|
|
10
|
+
scope = scope.where.not(id: @featured.id) if @featured
|
|
11
|
+
pagination = paginate(scope)
|
|
12
|
+
@posts = pagination.records
|
|
13
|
+
load_sidebar
|
|
14
|
+
page_context(:index, path: OpenBlog.mount_path, pagination: pagination,
|
|
15
|
+
breadcrumbs: [ { name: OpenBlog.config.blog_title, path: OpenBlog.mount_path } ])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show
|
|
19
|
+
@post = ReaderQueries.posts.find_by(slug: params[:slug])
|
|
20
|
+
unless @post
|
|
21
|
+
redirect = Redirect.find_by(old_path: "#{OpenBlog.mount_path.chomp('/')}/#{params[:slug]}")
|
|
22
|
+
raise NotFound unless redirect
|
|
23
|
+
return redirect_to(redirect.new_path, status: :moved_permanently, allow_other_host: true) if redirect.new_path
|
|
24
|
+
request.format = :html
|
|
25
|
+
@posts = ReaderQueries.posts.limit(6).to_a
|
|
26
|
+
page_context(:gone, path: request.path)
|
|
27
|
+
return render "open_blog/errors/not_found", status: :gone, formats: [ :html ], content_type: "text/html"
|
|
28
|
+
end
|
|
29
|
+
if request.format == Mime[:md]
|
|
30
|
+
canonical = @post.canonical_url.presence || @post.url(base: origin)
|
|
31
|
+
response.headers["X-Robots-Tag"] = "noindex"
|
|
32
|
+
response.headers["Link"] = "<#{canonical}>; rel=\"canonical\""
|
|
33
|
+
return render plain: MarkdownView.render(@post, base_url: origin), content_type: "text/markdown"
|
|
34
|
+
end
|
|
35
|
+
@related_posts = ReaderQueries.related(@post)
|
|
36
|
+
@related_cache_key = ReaderQueries.related_cache_key(@post, @related_posts)
|
|
37
|
+
crumbs = [ { name: OpenBlog.config.blog_title, path: OpenBlog.mount_path } ]
|
|
38
|
+
crumbs << { name: @post.category.name, path: ReaderQueries.category_path(@post.category) } if @post.category
|
|
39
|
+
crumbs << { name: @post.title, path: @post.path }
|
|
40
|
+
page_context(:post, record: @post, path: @post.path, breadcrumbs: crumbs)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def count_page_view
|
|
46
|
+
return unless @post && request.format.html? && [ 200, 304 ].include?(response.status)
|
|
47
|
+
PageViews.count!(@post, request)
|
|
48
|
+
rescue StandardError => error
|
|
49
|
+
Rails.logger.warn("OpenBlog page view count failed (#{error.class})")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def require_post_format
|
|
53
|
+
raise NotFound unless params[:format].blank? || %w[html md].include?(params[:format])
|
|
54
|
+
request.format = params[:format] == "md" ? :md : :html
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class PreviewsController < ActionController::Base
|
|
3
|
+
include ReaderController
|
|
4
|
+
before_action :preview_headers
|
|
5
|
+
before_action :require_html
|
|
6
|
+
|
|
7
|
+
def show
|
|
8
|
+
@post = Post.find_by_preview_token(params[:token])
|
|
9
|
+
raise NotFound unless @post && (@post.previewable? || @post.published?)
|
|
10
|
+
return redirect_to(@post.path, status: :found) if @post.published?
|
|
11
|
+
@related_posts = ReaderQueries.related(@post)
|
|
12
|
+
@related_cache_key = ReaderQueries.related_cache_key(@post, @related_posts)
|
|
13
|
+
page_context(:preview, record: @post, path: @post.path,
|
|
14
|
+
breadcrumbs: [ { name: OpenBlog.config.blog_title, path: OpenBlog.mount_path }, { name: @post.title, path: @post.path } ])
|
|
15
|
+
render "open_blog/posts/show"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def preview_headers
|
|
21
|
+
request.delete_header("HTTP_IF_NONE_MATCH")
|
|
22
|
+
request.delete_header("HTTP_IF_MODIFIED_SINCE")
|
|
23
|
+
response.headers["Cache-Control"] = "no-store"
|
|
24
|
+
response.headers["X-Robots-Tag"] = "noindex, nofollow"
|
|
25
|
+
response.headers["Referrer-Policy"] = "no-referrer"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def render_not_found
|
|
29
|
+
preview_headers
|
|
30
|
+
request.format = :html
|
|
31
|
+
page_context(:preview_expired, path: "#{OpenBlog.mount_path.chomp('/')}/preview")
|
|
32
|
+
render "open_blog/errors/preview_expired", status: :not_found, formats: [ :html ], content_type: "text/html"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class SearchController < ApplicationController
|
|
3
|
+
before_action :limit_search_requests
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
raise NotFound if params[:format].present? && !%w[html json].include?(params[:format])
|
|
7
|
+
response.headers["X-Robots-Tag"] = "noindex"
|
|
8
|
+
@query = params[:q].is_a?(String) ? params[:q].strip : ""
|
|
9
|
+
scope = Search.call(@query, scope: ReaderQueries.posts)
|
|
10
|
+
if params[:format] == "json"
|
|
11
|
+
render json: scope.limit(8).map { |post| { title: post.title, url: post.url(base: origin), description: post.description } }
|
|
12
|
+
else
|
|
13
|
+
request.format = :html
|
|
14
|
+
pagination = paginate(scope)
|
|
15
|
+
@posts = pagination.records
|
|
16
|
+
load_sidebar
|
|
17
|
+
path = "#{OpenBlog.mount_path.chomp('/')}/search?#{ { q: @query }.to_query }"
|
|
18
|
+
page_context(:search, path: path, pagination: pagination,
|
|
19
|
+
breadcrumbs: [ { name: OpenBlog.config.blog_title, path: OpenBlog.mount_path }, { name: I18n.t("open_blog.search.heading"), path: path } ])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def limit_search_requests
|
|
26
|
+
options = { to: OpenBlog.config.search_rate_limit.fetch(:to), within: OpenBlog.config.search_rate_limit.fetch(:within),
|
|
27
|
+
by: -> { request.remote_ip }, with: -> { head :too_many_requests }, store: OpenBlog.config.rate_limit_store, name: nil }
|
|
28
|
+
options[:scope] = "open_blog/search" if method(:rate_limiting).parameters.any? { |kind, name| kind == :keyreq && name == :scope }
|
|
29
|
+
rate_limiting(**options)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class SeriesController < ApplicationController
|
|
3
|
+
before_action :require_html
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
@series = Series.find_by!(slug: params[:slug])
|
|
7
|
+
pagination = paginate(ReaderQueries.series_posts(@series))
|
|
8
|
+
@posts = pagination.records
|
|
9
|
+
load_sidebar
|
|
10
|
+
path = ReaderQueries.series_path(@series)
|
|
11
|
+
page_context(:series, record: @series, path: path, pagination: pagination,
|
|
12
|
+
breadcrumbs: [ { name: OpenBlog.config.blog_title, path: OpenBlog.mount_path }, { name: @series.name, path: path } ])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class SitemapsController < ApplicationController
|
|
3
|
+
def show
|
|
4
|
+
raise NotFound unless OpenBlog.config.serve_sitemap
|
|
5
|
+
@entries = OpenBlog.sitemap_entries(base_url: origin)
|
|
6
|
+
expires_in 1.hour, public: true
|
|
7
|
+
render "open_blog/sitemaps/show", formats: [ :xml ], content_type: "application/xml", layout: false
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
class TagsController < ApplicationController
|
|
3
|
+
before_action :require_html
|
|
4
|
+
|
|
5
|
+
def show
|
|
6
|
+
@tag = Tag.find_by!(slug: params[:slug])
|
|
7
|
+
pagination = paginate(ReaderQueries.posts.where(id: Tagging.where(tag_id: @tag.id).select(:post_id)))
|
|
8
|
+
@posts = pagination.records
|
|
9
|
+
load_sidebar
|
|
10
|
+
path = ReaderQueries.tag_path(@tag)
|
|
11
|
+
page_context(:tag, record: @tag, path: path, pagination: pagination,
|
|
12
|
+
breadcrumbs: [ { name: OpenBlog.config.blog_title, path: OpenBlog.mount_path }, { name: @tag.name, path: path } ])
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module ContentHelper
|
|
3
|
+
include FaqHelper
|
|
4
|
+
|
|
5
|
+
def open_blog_post_content(_post, &block)
|
|
6
|
+
tag.article(class: "ob-post-content", data: { open_blog_content: true }, &block)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def open_blog_title(post)
|
|
10
|
+
tag.h1(post.title, class: "ob-post-title")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def open_blog_byline(post)
|
|
14
|
+
tag.div(class: "ob-byline") do
|
|
15
|
+
safe_join([ link_to(post.author_name, open_blog_list_path(:author, post.author), class: "ob-byline-author"), open_blog_dates(post) ], " ")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def open_blog_cover(post)
|
|
20
|
+
return "".html_safe unless post.cover_image
|
|
21
|
+
image = post.cover_image
|
|
22
|
+
tag.figure(class: "ob-cover") do
|
|
23
|
+
tag.img(src: image.path, alt: post.cover_alt.to_s, width: image.width, height: image.height)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def open_blog_card_image(post)
|
|
28
|
+
image = post.cover_image
|
|
29
|
+
return tag.div(class: "ob-card-placeholder", aria: { hidden: true }) unless image
|
|
30
|
+
source = image.file.attached? ? main_app.url_for(image.file.variant(resize_to_fill: [ 640, 360 ])) : image.path
|
|
31
|
+
tag.img(src: source, alt: post.cover_alt.to_s, width: 640, height: 360, loading: "lazy", class: "ob-card-image")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def open_blog_avatar(author, name: nil)
|
|
35
|
+
name ||= author.name
|
|
36
|
+
if author.avatar.attached?
|
|
37
|
+
tag.img(src: main_app.url_for(author.avatar.variant(resize_to_fill: [ 80, 80 ])), alt: name, width: 40, height: 40, class: "ob-avatar", loading: "lazy")
|
|
38
|
+
else
|
|
39
|
+
tag.span(name.to_s.chars.first(2).join.upcase, class: "ob-avatar ob-avatar--initials", aria: { hidden: true })
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def open_blog_body(post)
|
|
44
|
+
tag.div(Renderer.render(post, cache: @open_blog_page&.kind != :preview), class: "ob-prose", data: { open_blog_body: true })
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def open_blog_reading_time(post)
|
|
48
|
+
tag.span(open_blog_translate("reading_time", count: post.reading_time_minutes || 1), class: "ob-reading-time")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def open_blog_toc(post)
|
|
52
|
+
fragment = Nokogiri::HTML5.fragment(Renderer.render(post, cache: @open_blog_page&.kind != :preview))
|
|
53
|
+
headings = fragment.css("h2, h3")
|
|
54
|
+
has_faq = post.faq_list.any?
|
|
55
|
+
return "".html_safe if headings.length + (has_faq ? 1 : 0) < 3
|
|
56
|
+
items = headings.map do |heading|
|
|
57
|
+
text = heading.dup
|
|
58
|
+
text.css(".ob-heading-anchor").remove
|
|
59
|
+
tag.li(link_to(text.text, "##{heading['id']}"), class: "ob-toc-item ob-toc-item--#{heading.name}")
|
|
60
|
+
end
|
|
61
|
+
if has_faq
|
|
62
|
+
items << tag.li(link_to(open_blog_translate("faq.heading"), "##{open_blog_faq_section_id(post, body: fragment)}"), class: "ob-toc-item ob-toc-item--h2")
|
|
63
|
+
end
|
|
64
|
+
tag.nav(class: "ob-toc", aria: { label: open_blog_translate("navigation.toc") }, data: { controller: "open-blog--toc" }) do
|
|
65
|
+
safe_join([ tag.h2(open_blog_translate("navigation.toc")), tag.ol(safe_join(items)) ])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def open_blog_breadcrumbs(page = nil)
|
|
70
|
+
page = open_blog_page(page)
|
|
71
|
+
tag.nav(class: "ob-breadcrumbs", aria: { label: open_blog_translate("navigation.breadcrumbs") }) do
|
|
72
|
+
tag.ol(safe_join(page.breadcrumbs.each_with_index.map do |crumb, index|
|
|
73
|
+
last = index == page.breadcrumbs.length - 1
|
|
74
|
+
tag.li(last ? tag.span(crumb[:name], aria: { current: "page" }) : link_to(crumb[:name], crumb[:path]))
|
|
75
|
+
end))
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module DatesHelper
|
|
3
|
+
def open_blog_dates(post)
|
|
4
|
+
published = ReaderDates.published_at(post)
|
|
5
|
+
modified = ReaderDates.modified_at(post)
|
|
6
|
+
dates = []
|
|
7
|
+
dates << open_blog_date(published, :published) if published
|
|
8
|
+
dates << open_blog_date(modified, :updated) if modified && (!published || modified > published)
|
|
9
|
+
safe_join(dates, " ")
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def open_blog_date(time, label)
|
|
13
|
+
tag.span(class: "ob-date ob-date--#{label}") do
|
|
14
|
+
safe_join([ tag.span(open_blog_translate("dates.#{label}")), tag.time(I18n.l(time.to_date, format: :long, locale: OpenBlog.config.locale), datetime: time.iso8601) ], " ")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
module OpenBlog
|
|
4
|
+
module FaqHelper
|
|
5
|
+
def open_blog_faq(post)
|
|
6
|
+
entries = post.faq_list
|
|
7
|
+
return "".html_safe if entries.empty?
|
|
8
|
+
heading = tag.h2(open_blog_translate("faq.heading"), id: open_blog_faq_section_id(post))
|
|
9
|
+
items = entries.map do |entry|
|
|
10
|
+
question = tag.h3(ERB::Util.html_escape(String.new(entry[:question].to_s)))
|
|
11
|
+
paragraphs = entry[:answer].to_s.gsub(/\r\n?/, "\n").split(/\n[\t ]*\n+/).map do |paragraph|
|
|
12
|
+
tag.p(safe_join(paragraph.split("\n", -1).map { |line| open_blog_faq_links(line) }, safe_join([ tag.br, "\n" ])))
|
|
13
|
+
end
|
|
14
|
+
tag.div(safe_join([ question, *paragraphs ], "\n"), class: "ob-faq-entry", data: { open_blog_faq_entry: true })
|
|
15
|
+
end
|
|
16
|
+
tag.section(safe_join([ heading, *items ], "\n"), class: "ob-faq", data: { open_blog_faq: true })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def open_blog_faq_section_id(post, body: nil)
|
|
20
|
+
body ||= Nokogiri::HTML5.fragment(Renderer.render(post, cache: @open_blog_page&.kind != :preview))
|
|
21
|
+
used = body.css("[id]").map { |node| node["id"] }
|
|
22
|
+
identifier, suffix = "open-blog-faq", 0
|
|
23
|
+
while used.include?(identifier)
|
|
24
|
+
suffix += 1
|
|
25
|
+
identifier = "open-blog-faq-#{suffix}"
|
|
26
|
+
end
|
|
27
|
+
identifier
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def open_blog_faq_links(line)
|
|
33
|
+
line = String.new(line)
|
|
34
|
+
parts, offset = [], 0
|
|
35
|
+
line.to_enum(:scan, %r{https?://[^\s<>"']+}i).each do
|
|
36
|
+
match = Regexp.last_match
|
|
37
|
+
parts << line[offset...match.begin(0)]
|
|
38
|
+
candidate = match[0]
|
|
39
|
+
url = candidate.sub(/[.,;:!?]+\z/, "")
|
|
40
|
+
{ ")" => "(", "]" => "[", "}" => "{" }.each do |closing, opening|
|
|
41
|
+
url = url.chop while url.end_with?(closing) && url.count(closing) > url.count(opening)
|
|
42
|
+
end
|
|
43
|
+
url = url.sub(/[.,;:!?]+\z/, "")
|
|
44
|
+
parts << (open_blog_faq_http_url?(url) ? link_to(url, url, rel: "noopener") : url)
|
|
45
|
+
parts << candidate[url.length..]
|
|
46
|
+
offset = match.end(0)
|
|
47
|
+
end
|
|
48
|
+
parts << line[offset..]
|
|
49
|
+
safe_join(parts)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def open_blog_faq_http_url?(url)
|
|
53
|
+
parsed = URI.parse(url)
|
|
54
|
+
%w[http https].include?(parsed.scheme&.downcase) && parsed.host.present?
|
|
55
|
+
rescue URI::InvalidURIError
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module HeadHelper
|
|
3
|
+
include ThemeHelper
|
|
4
|
+
|
|
5
|
+
def open_blog_lang
|
|
6
|
+
OpenBlog.config.locale.to_s.tr("_", "-")
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def open_blog_head(resource = nil)
|
|
10
|
+
page = open_blog_page(resource)
|
|
11
|
+
title = open_blog_page_title(page)
|
|
12
|
+
description = open_blog_page_description(page)
|
|
13
|
+
canonical = open_blog_canonical_url(page)
|
|
14
|
+
tags = [ tag.title(title), tag.meta(name: "description", content: description), tag.link(rel: "canonical", href: canonical),
|
|
15
|
+
tag.meta(name: "robots", content: open_blog_robots(page)), tag.meta(name: "viewport", content: "width=device-width, initial-scale=1"),
|
|
16
|
+
tag.meta(property: "og:title", content: title), tag.meta(property: "og:description", content: description),
|
|
17
|
+
tag.meta(property: "og:type", content: %i[post preview].include?(page.kind) ? "article" : "website"), tag.meta(property: "og:url", content: canonical),
|
|
18
|
+
tag.meta(property: "og:site_name", content: OpenBlog.config.site_name), tag.meta(name: "twitter:card", content: "summary_large_image") ]
|
|
19
|
+
image = open_blog_social_image_url(page)
|
|
20
|
+
tags << tag.meta(property: "og:image", content: image) if image
|
|
21
|
+
if %i[post preview].include?(page.kind)
|
|
22
|
+
{ published_time: ReaderDates.published_at(page.record), modified_time: ReaderDates.modified_at(page.record) }.each do |name, time|
|
|
23
|
+
tags << tag.meta(property: "article:#{name}", content: time.iso8601) if time
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
feed_path = page.kind == :category ? "#{page.path}/feed.xml" : "#{open_blog_index_path.chomp('/')}/feed.xml"
|
|
27
|
+
tags << tag.link(rel: "alternate", type: "application/atom+xml", title: OpenBlog.config.blog_title, href: open_blog_absolute_url(feed_path, base: page.base_url))
|
|
28
|
+
tags << tag.link(rel: "alternate", type: "application/feed+json", title: OpenBlog.config.blog_title,
|
|
29
|
+
href: open_blog_absolute_url(feed_path.sub(/\.xml\z/, ".json"), base: page.base_url))
|
|
30
|
+
tags << open_blog_theme_colors
|
|
31
|
+
safe_join(tags, "\n")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def open_blog_page_title(page = open_blog_page)
|
|
35
|
+
title = case page.kind
|
|
36
|
+
when :post, :preview then page.record.search_title.presence || page.record.title
|
|
37
|
+
when :page then page.record.title
|
|
38
|
+
when :search then open_blog_translate("search.heading")
|
|
39
|
+
when :category, :author, :series then page.record.name
|
|
40
|
+
when :tag then open_blog_translate("titles.tag", name: page.record.name)
|
|
41
|
+
when :not_found, :gone, :preview_expired then open_blog_translate("titles.#{page.kind}")
|
|
42
|
+
else OpenBlog.config.blog_title
|
|
43
|
+
end
|
|
44
|
+
title = open_blog_translate("titles.page", title: title, page: page.page_number) if page.page_number > 1
|
|
45
|
+
"#{title}#{OpenBlog.config.title_suffix}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def open_blog_page_description(page = open_blog_page)
|
|
49
|
+
record = page.record
|
|
50
|
+
return record.search_description.presence || record.description.to_s if %i[post preview].include?(page.kind)
|
|
51
|
+
return record.title if page.kind == :page
|
|
52
|
+
return open_blog_translate("search.description") if page.kind == :search
|
|
53
|
+
paginated = page.page_number > 1
|
|
54
|
+
if !paginated
|
|
55
|
+
return OpenBlog.config.blog_tagline if page.kind == :index && OpenBlog.config.blog_tagline.present?
|
|
56
|
+
return record.description if %i[category tag series].include?(page.kind) && record.respond_to?(:description) && record.description.present?
|
|
57
|
+
return record.bio if page.kind == :author && record.bio.present?
|
|
58
|
+
end
|
|
59
|
+
kind = %i[index category tag author series].include?(page.kind) ? page.kind : :index
|
|
60
|
+
open_blog_translate("descriptions.#{kind}#{'_page' if paginated}", site_name: OpenBlog.config.site_name, name: record&.try(:name), page: page.page_number)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def open_blog_robots(page = open_blog_page)
|
|
64
|
+
return "noindex, nofollow" if %i[not_found gone preview preview_expired].include?(page.kind)
|
|
65
|
+
return "index, follow" if %i[index post page].include?(page.kind)
|
|
66
|
+
primary = OpenBlog.config.primary_list_type.to_s.singularize.to_sym
|
|
67
|
+
page.kind == primary ? "index, follow" : "noindex, follow"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def open_blog_social_image_url(page = open_blog_page)
|
|
71
|
+
image = %i[post preview].include?(page.kind) && (page.record.social_image || page.record.cover_image)
|
|
72
|
+
open_blog_absolute_url(image ? image.path : OpenBlog.config.default_social_image_url, base: page.base_url)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module IconsHelper
|
|
3
|
+
ICONS = %w[sun moon monitor share copy check link].freeze
|
|
4
|
+
|
|
5
|
+
def open_blog_icon(name)
|
|
6
|
+
raise ArgumentError, "Unknown icon: #{name}" unless ICONS.include?(name.to_s)
|
|
7
|
+
render partial: "open_blog/icons/#{name}", formats: [ :svg ]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module NoticesHelper
|
|
3
|
+
def open_blog_notices(post)
|
|
4
|
+
notices = []
|
|
5
|
+
label = LabelPolicy.for(post)
|
|
6
|
+
notices << tag.p(open_blog_translate("notices.#{label}"), class: "ob-notice ob-notice--ai") unless label == :none
|
|
7
|
+
if open_blog_connection_declaration(post)&.third_party_paid?
|
|
8
|
+
notices << tag.p(open_blog_translate("notices.paid"), class: "ob-notice ob-notice--paid")
|
|
9
|
+
end
|
|
10
|
+
safe_join(notices)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def open_blog_disclosure(post)
|
|
14
|
+
declaration = open_blog_connection_declaration(post)
|
|
15
|
+
return "".html_safe unless declaration
|
|
16
|
+
content = if declaration.connections.empty?
|
|
17
|
+
tag.p(open_blog_translate("notices.connections_none"))
|
|
18
|
+
else
|
|
19
|
+
tag.ul(safe_join(declaration.connections.map { |entry| tag.li("#{entry['party']}: #{entry['relation']}") }))
|
|
20
|
+
end
|
|
21
|
+
tag.aside(class: "ob-disclosure ob-notice", aria: { label: open_blog_translate("notices.disclosure") }) do
|
|
22
|
+
safe_join([ tag.h2(open_blog_translate("notices.disclosure")), content ])
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def open_blog_corrections(post)
|
|
27
|
+
rows = post.publications.where(entry_type: "correction").where("occurred_at <= ?", Time.current).order(:occurred_at, :id)
|
|
28
|
+
safe_join(rows.map do |entry|
|
|
29
|
+
tag.aside(class: "ob-correction ob-notice") do
|
|
30
|
+
safe_join([ tag.strong(open_blog_translate("notices.correction")),
|
|
31
|
+
tag.time(I18n.l(entry.occurred_at.to_date, format: :long, locale: OpenBlog.config.locale), datetime: entry.occurred_at.iso8601),
|
|
32
|
+
tag.p(entry.note) ], " ")
|
|
33
|
+
end
|
|
34
|
+
end)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def open_blog_responsible_party_link
|
|
38
|
+
url = OpenBlog.policy_url(:responsible_party)
|
|
39
|
+
return "".html_safe unless url.present? && Renderer::Sanitizer.safe_url?(url)
|
|
40
|
+
link_to(open_blog_translate("navigation.responsible_party"), url, class: "ob-responsible-party")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def open_blog_connection_declaration(post)
|
|
44
|
+
post.connection_declarations.order(id: :desc).first
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module OpenBlog
|
|
2
|
+
module PaginationHelper
|
|
3
|
+
def open_blog_paginate(pagination = nil)
|
|
4
|
+
page = open_blog_page
|
|
5
|
+
pagination ||= page.pagination
|
|
6
|
+
return "".html_safe unless pagination && pagination.total_pages > 1
|
|
7
|
+
links = (1..pagination.total_pages).map do |number|
|
|
8
|
+
if number == pagination.page
|
|
9
|
+
tag.span(number, class: "ob-pagination-current", aria: { current: "page" })
|
|
10
|
+
else
|
|
11
|
+
path = page.path_for(number)
|
|
12
|
+
link_to(number, path, class: "ob-pagination-link", aria: { label: open_blog_translate("navigation.page", page: number) })
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
tag.nav(safe_join(links, " "), class: "ob-pagination", aria: { label: open_blog_translate("navigation.pagination") })
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|