postnhost 0.0.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (497) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/{LICENSE.txt → LICENSE} +6 -6
  4. data/README.md +341 -15
  5. data/Rakefile +38 -6
  6. data/app/assets/builds/postnhost/application.css +2 -0
  7. data/app/assets/builds/postnhost/application.js +50190 -0
  8. data/app/assets/config/postnhost_manifest.js +2 -0
  9. data/app/assets/images/postnhost/demo.webp +0 -0
  10. data/app/assets/images/postnhost/icons/alert-circle.svg +3 -0
  11. data/app/assets/images/postnhost/icons/arrow-left-long.svg +3 -0
  12. data/app/assets/images/postnhost/icons/arrow-left.svg +3 -0
  13. data/app/assets/images/postnhost/icons/arrow-right-end-on-rectangle.svg +3 -0
  14. data/app/assets/images/postnhost/icons/arrow-right-long.svg +3 -0
  15. data/app/assets/images/postnhost/icons/arrow-right.svg +3 -0
  16. data/app/assets/images/postnhost/icons/bars.svg +3 -0
  17. data/app/assets/images/postnhost/icons/bluesky.svg +3 -0
  18. data/app/assets/images/postnhost/icons/check-circle.svg +3 -0
  19. data/app/assets/images/postnhost/icons/chevron-down-small.svg +3 -0
  20. data/app/assets/images/postnhost/icons/chevron-down.svg +3 -0
  21. data/app/assets/images/postnhost/icons/chevron-left.svg +3 -0
  22. data/app/assets/images/postnhost/icons/chevron-right.svg +3 -0
  23. data/app/assets/images/postnhost/icons/clock.svg +3 -0
  24. data/app/assets/images/postnhost/icons/close.svg +3 -0
  25. data/app/assets/images/postnhost/icons/code-bracket.svg +3 -0
  26. data/app/assets/images/postnhost/icons/document-text.svg +3 -0
  27. data/app/assets/images/postnhost/icons/drag-handle.svg +8 -0
  28. data/app/assets/images/postnhost/icons/edit.svg +3 -0
  29. data/app/assets/images/postnhost/icons/external-link.svg +3 -0
  30. data/app/assets/images/postnhost/icons/eye.svg +4 -0
  31. data/app/assets/images/postnhost/icons/facebook.svg +3 -0
  32. data/app/assets/images/postnhost/icons/folder.svg +3 -0
  33. data/app/assets/images/postnhost/icons/image.svg +3 -0
  34. data/app/assets/images/postnhost/icons/info-circle.svg +3 -0
  35. data/app/assets/images/postnhost/icons/instagram.svg +3 -0
  36. data/app/assets/images/postnhost/icons/language.svg +3 -0
  37. data/app/assets/images/postnhost/icons/layers.svg +3 -0
  38. data/app/assets/images/postnhost/icons/lightning.svg +3 -0
  39. data/app/assets/images/postnhost/icons/linkedin.svg +3 -0
  40. data/app/assets/images/postnhost/icons/logout.svg +3 -0
  41. data/app/assets/images/postnhost/icons/magnifying-glass.svg +3 -0
  42. data/app/assets/images/postnhost/icons/mastodon.svg +3 -0
  43. data/app/assets/images/postnhost/icons/menu.svg +3 -0
  44. data/app/assets/images/postnhost/icons/plus.svg +3 -0
  45. data/app/assets/images/postnhost/icons/schedule.svg +3 -0
  46. data/app/assets/images/postnhost/icons/settings.svg +4 -0
  47. data/app/assets/images/postnhost/icons/sparkles.svg +3 -0
  48. data/app/assets/images/postnhost/icons/spinner.svg +4 -0
  49. data/app/assets/images/postnhost/icons/threads.svg +3 -0
  50. data/app/assets/images/postnhost/icons/tiktok.svg +3 -0
  51. data/app/assets/images/postnhost/icons/tiptap/align-center.svg +3 -0
  52. data/app/assets/images/postnhost/icons/tiptap/align-left.svg +3 -0
  53. data/app/assets/images/postnhost/icons/tiptap/align-right.svg +3 -0
  54. data/app/assets/images/postnhost/icons/tiptap/apply-link.svg +3 -0
  55. data/app/assets/images/postnhost/icons/tiptap/blockquote.svg +3 -0
  56. data/app/assets/images/postnhost/icons/tiptap/bold.svg +3 -0
  57. data/app/assets/images/postnhost/icons/tiptap/bullet-list.svg +3 -0
  58. data/app/assets/images/postnhost/icons/tiptap/code-block.svg +3 -0
  59. data/app/assets/images/postnhost/icons/tiptap/code.svg +3 -0
  60. data/app/assets/images/postnhost/icons/tiptap/heading-2.svg +3 -0
  61. data/app/assets/images/postnhost/icons/tiptap/heading-3.svg +3 -0
  62. data/app/assets/images/postnhost/icons/tiptap/image-add.svg +3 -0
  63. data/app/assets/images/postnhost/icons/tiptap/italic.svg +3 -0
  64. data/app/assets/images/postnhost/icons/tiptap/link.svg +3 -0
  65. data/app/assets/images/postnhost/icons/tiptap/ordered-list.svg +3 -0
  66. data/app/assets/images/postnhost/icons/tiptap/redo.svg +3 -0
  67. data/app/assets/images/postnhost/icons/tiptap/remove-link.svg +3 -0
  68. data/app/assets/images/postnhost/icons/tiptap/strike.svg +3 -0
  69. data/app/assets/images/postnhost/icons/tiptap/table.svg +3 -0
  70. data/app/assets/images/postnhost/icons/tiptap/underline.svg +3 -0
  71. data/app/assets/images/postnhost/icons/tiptap/undo.svg +3 -0
  72. data/app/assets/images/postnhost/icons/tiptap/youtube.svg +4 -0
  73. data/app/assets/images/postnhost/icons/translate.svg +3 -0
  74. data/app/assets/images/postnhost/icons/trash.svg +3 -0
  75. data/app/assets/images/postnhost/icons/user.svg +4 -0
  76. data/app/assets/images/postnhost/icons/users.svg +3 -0
  77. data/app/assets/images/postnhost/icons/warning.svg +3 -0
  78. data/app/assets/images/postnhost/icons/x.svg +3 -0
  79. data/app/assets/images/postnhost/icons/youtube.svg +3 -0
  80. data/app/assets/images/postnhost/logo.webp +0 -0
  81. data/app/assets/images/postnhost/og-image.webp +0 -0
  82. data/app/assets/images/postnhost/sample-docs/08-how-to-control-suggested-articles-and-top-picks-01-discovery-controls.webp +0 -0
  83. data/app/assets/images/postnhost/sample-docs/09-all-about-authors-in-postnhost-01-author-profile.webp +0 -0
  84. data/app/assets/images/postnhost/sample-docs/09-all-about-authors-in-postnhost-02-multiple-authors.webp +0 -0
  85. data/app/assets/images/postnhost/sample-docs/11-how-to-schedule-an-article-for-publication-01-timezone.webp +0 -0
  86. data/app/assets/images/postnhost/sample-docs/11-how-to-schedule-an-article-for-publication-02-scheduled-article.webp +0 -0
  87. data/app/assets/images/postnhost/sample-docs/12-how-article-version-history-and-restores-work-01-version-history.webp +0 -0
  88. data/app/assets/images/postnhost/sample-docs/13-how-to-add-and-manage-languages-01-language-management.webp +0 -0
  89. data/app/assets/images/postnhost/sample-docs/14-how-to-translate-articles-pages-and-categories-01-translation-options.webp +0 -0
  90. data/app/assets/images/postnhost/sample-docs/14-how-to-translate-articles-pages-and-categories-02-ai-language-selection.webp +0 -0
  91. data/app/assets/images/postnhost/sample-docs/16-how-to-add-navigation-links-01-navigation-builder.webp +0 -0
  92. data/app/assets/images/postnhost/sample-docs/16-how-to-add-navigation-links-02-external-link-settings.webp +0 -0
  93. data/app/assets/images/postnhost/sample-docs/17-how-to-add-a-logo-and-default-og-image-01-assets-settings.webp +0 -0
  94. data/app/assets/images/postnhost/sample-docs/19-how-to-customize-public-text-for-each-language-01-i18n-overrides.webp +0 -0
  95. data/app/assets/images/postnhost/sample-docs/20-how-to-configure-structured-data-and-sitemap-seo-01-site-schema.webp +0 -0
  96. data/app/assets/images/postnhost/sample-docs/20-how-to-configure-structured-data-and-sitemap-seo-02-author-schema-defaults.webp +0 -0
  97. data/app/assets/images/postnhost/sample-docs/20-how-to-configure-structured-data-and-sitemap-seo-03-author-schema-advanced-fields.webp +0 -0
  98. data/app/assets/images/postnhost/sample-docs/covers/08-suggested-articles-and-top-picks.webp +0 -0
  99. data/app/assets/images/postnhost/sample-docs/covers/09-authors.webp +0 -0
  100. data/app/assets/images/postnhost/sample-docs/covers/11-scheduling.webp +0 -0
  101. data/app/assets/images/postnhost/sample-docs/covers/12-version-history.webp +0 -0
  102. data/app/assets/images/postnhost/sample-docs/covers/13-languages.webp +0 -0
  103. data/app/assets/images/postnhost/sample-docs/covers/14-translations.webp +0 -0
  104. data/app/assets/images/postnhost/sample-docs/covers/16-navigation-links.webp +0 -0
  105. data/app/assets/images/postnhost/sample-docs/covers/17-logo-and-default-og-image.webp +0 -0
  106. data/app/assets/images/postnhost/sample-docs/covers/19-public-text-for-each-language.webp +0 -0
  107. data/app/assets/images/postnhost/sample-docs/covers/20-structured-data-and-sitemap-seo.webp +0 -0
  108. data/app/assets/stylesheets/application.tailwind.css +343 -0
  109. data/app/assets/stylesheets/postnhost/application.css +15 -0
  110. data/app/assets/xsl/sitemap.xsl +64 -0
  111. data/app/controllers/concerns/postnhost/authentication.rb +59 -0
  112. data/app/controllers/concerns/postnhost/canonical_first_page_redirect.rb +14 -0
  113. data/app/controllers/concerns/postnhost/language_switcher.rb +42 -0
  114. data/app/controllers/concerns/postnhost/onboarding/access_control.rb +57 -0
  115. data/app/controllers/concerns/postnhost/onboarding/step_assignment.rb +55 -0
  116. data/app/controllers/concerns/postnhost/public_footer_data.rb +103 -0
  117. data/app/controllers/concerns/postnhost/public_http_caching.rb +35 -0
  118. data/app/controllers/concerns/postnhost/public_page_size.rb +17 -0
  119. data/app/controllers/concerns/postnhost/public_template_resolvable.rb +44 -0
  120. data/app/controllers/concerns/postnhost/settings/i18n_manageable.rb +61 -0
  121. data/app/controllers/concerns/postnhost/settings/navigation_manageable.rb +118 -0
  122. data/app/controllers/concerns/postnhost/settings/schema_manageable.rb +140 -0
  123. data/app/controllers/concerns/postnhost/users/schema_manageable.rb +119 -0
  124. data/app/controllers/postnhost/application_controller.rb +37 -0
  125. data/app/controllers/postnhost/article_variants_controller.rb +138 -0
  126. data/app/controllers/postnhost/articles/cover_images_controller.rb +35 -0
  127. data/app/controllers/postnhost/articles/images_controller.rb +28 -0
  128. data/app/controllers/postnhost/articles/translations_controller.rb +26 -0
  129. data/app/controllers/postnhost/articles_controller.rb +130 -0
  130. data/app/controllers/postnhost/categories/translations_controller.rb +24 -0
  131. data/app/controllers/postnhost/categories_controller.rb +59 -0
  132. data/app/controllers/postnhost/category_variants_controller.rb +69 -0
  133. data/app/controllers/postnhost/languages_controller.rb +66 -0
  134. data/app/controllers/postnhost/onboarding_controller.rb +70 -0
  135. data/app/controllers/postnhost/page_variants_controller.rb +136 -0
  136. data/app/controllers/postnhost/pages/images_controller.rb +28 -0
  137. data/app/controllers/postnhost/pages/translations_controller.rb +26 -0
  138. data/app/controllers/postnhost/pages_controller.rb +95 -0
  139. data/app/controllers/postnhost/public/articles_controller.rb +135 -0
  140. data/app/controllers/postnhost/public/authors_controller.rb +57 -0
  141. data/app/controllers/postnhost/public/categories_controller.rb +61 -0
  142. data/app/controllers/postnhost/public/static_pages_controller.rb +77 -0
  143. data/app/controllers/postnhost/public_controller.rb +32 -0
  144. data/app/controllers/postnhost/sessions_controller.rb +37 -0
  145. data/app/controllers/postnhost/settings/site_scripts_controller.rb +61 -0
  146. data/app/controllers/postnhost/settings_controller.rb +63 -0
  147. data/app/controllers/postnhost/sitemap_controller.rb +17 -0
  148. data/app/controllers/postnhost/templates_controller.rb +26 -0
  149. data/app/controllers/postnhost/users_controller.rb +112 -0
  150. data/app/helpers/postnhost/application_helper.rb +247 -0
  151. data/app/helpers/postnhost/article_helper.rb +41 -0
  152. data/app/helpers/postnhost/category_helper.rb +52 -0
  153. data/app/helpers/postnhost/common_css_helper.rb +97 -0
  154. data/app/helpers/postnhost/navigation_helper.rb +148 -0
  155. data/app/helpers/postnhost/pagy_helper.rb +103 -0
  156. data/app/helpers/postnhost/public_pages_helper.rb +287 -0
  157. data/app/helpers/postnhost/schema_helper.rb +102 -0
  158. data/app/helpers/postnhost/seo_helper.rb +428 -0
  159. data/app/helpers/postnhost/settings_helper.rb +79 -0
  160. data/app/javascript/application.js +3 -0
  161. data/app/javascript/controllers/ai_translation_controller.js +42 -0
  162. data/app/javascript/controllers/application.js +9 -0
  163. data/app/javascript/controllers/author_manager_controller.js +96 -0
  164. data/app/javascript/controllers/auto_save_controller.js +244 -0
  165. data/app/javascript/controllers/bulk_selection_controller.js +74 -0
  166. data/app/javascript/controllers/burger_controller.js +20 -0
  167. data/app/javascript/controllers/category_manager_controller.js +99 -0
  168. data/app/javascript/controllers/collapsible_sidebar_controller.js +87 -0
  169. data/app/javascript/controllers/cover_image_upload_controller.js +164 -0
  170. data/app/javascript/controllers/dropdown_controller.js +38 -0
  171. data/app/javascript/controllers/editor_mobile_sidebars_controller.js +36 -0
  172. data/app/javascript/controllers/file_auto_submit_controller.js +23 -0
  173. data/app/javascript/controllers/flash_message_controller.js +40 -0
  174. data/app/javascript/controllers/index.js +71 -0
  175. data/app/javascript/controllers/navigation_builder_controller.js +463 -0
  176. data/app/javascript/controllers/public_mobile_nav_controller.js +26 -0
  177. data/app/javascript/controllers/publish_guard_controller.js +47 -0
  178. data/app/javascript/controllers/reading_progress_controller.js +38 -0
  179. data/app/javascript/controllers/schedule_manager_controller.js +87 -0
  180. data/app/javascript/controllers/suggestion_manager_controller.js +99 -0
  181. data/app/javascript/controllers/template_preview_controller.js +34 -0
  182. data/app/javascript/controllers/tiptap_editor_controller.js +812 -0
  183. data/app/javascript/controllers/title_field_controller.js +17 -0
  184. data/app/javascript/helpers/auto_save_state.js +47 -0
  185. data/app/javascript/helpers/code_highlighting.js +48 -0
  186. data/app/javascript/helpers/dom_helpers.js +109 -0
  187. data/app/javascript/helpers/style_helpers.js +168 -0
  188. data/app/jobs/postnhost/application_job.rb +9 -0
  189. data/app/jobs/postnhost/scheduled_article_job.rb +13 -0
  190. data/app/jobs/postnhost/translation/article_variant_job.rb +17 -0
  191. data/app/jobs/postnhost/translation/category_variant_job.rb +17 -0
  192. data/app/jobs/postnhost/translation/page_variant_job.rb +17 -0
  193. data/app/models/concerns/postnhost/article/image_uploadable.rb +11 -0
  194. data/app/models/concerns/postnhost/excerpt_optimizable.rb +57 -0
  195. data/app/models/concerns/postnhost/inline_image_uploadable.rb +40 -0
  196. data/app/models/concerns/postnhost/page/image_uploadable.rb +11 -0
  197. data/app/models/concerns/postnhost/primary_language_guard.rb +18 -0
  198. data/app/models/concerns/postnhost/public_revision_touch.rb +15 -0
  199. data/app/models/concerns/postnhost/schema_localizable.rb +28 -0
  200. data/app/models/postnhost/application_record.rb +5 -0
  201. data/app/models/postnhost/article.rb +142 -0
  202. data/app/models/postnhost/article_author.rb +20 -0
  203. data/app/models/postnhost/article_category.rb +8 -0
  204. data/app/models/postnhost/article_suggestion.rb +19 -0
  205. data/app/models/postnhost/article_variant.rb +28 -0
  206. data/app/models/postnhost/category.rb +34 -0
  207. data/app/models/postnhost/category_variant.rb +24 -0
  208. data/app/models/postnhost/language.rb +38 -0
  209. data/app/models/postnhost/navigation.rb +76 -0
  210. data/app/models/postnhost/navigation_item.rb +100 -0
  211. data/app/models/postnhost/page.rb +63 -0
  212. data/app/models/postnhost/page_variant.rb +26 -0
  213. data/app/models/postnhost/public_site_revision.rb +24 -0
  214. data/app/models/postnhost/published_cover_image.rb +28 -0
  215. data/app/models/postnhost/setting.rb +116 -0
  216. data/app/models/postnhost/site_script.rb +11 -0
  217. data/app/models/postnhost/snapshot/article.rb +142 -0
  218. data/app/models/postnhost/snapshot/article_author.rb +11 -0
  219. data/app/models/postnhost/snapshot/article_category.rb +10 -0
  220. data/app/models/postnhost/snapshot/article_suggestion.rb +11 -0
  221. data/app/models/postnhost/snapshot/article_variant.rb +60 -0
  222. data/app/models/postnhost/snapshot/page.rb +33 -0
  223. data/app/models/postnhost/snapshot/page_variant.rb +41 -0
  224. data/app/models/postnhost/template.rb +20 -0
  225. data/app/models/postnhost/user.rb +63 -0
  226. data/app/models/postnhost/version.rb +7 -0
  227. data/app/services/postnhost/base_service.rb +17 -0
  228. data/app/services/postnhost/constraints/public_locale_constraint.rb +9 -0
  229. data/app/services/postnhost/constraints/public_route_constraint.rb +19 -0
  230. data/app/services/postnhost/onboarding/step_processor.rb +27 -0
  231. data/app/services/postnhost/onboarding/steps/base.rb +52 -0
  232. data/app/services/postnhost/onboarding/steps/choose_languages.rb +66 -0
  233. data/app/services/postnhost/onboarding/steps/complete_post_install_checklist.rb +12 -0
  234. data/app/services/postnhost/onboarding/steps/create_admin_account.rb +28 -0
  235. data/app/services/postnhost/onboarding/steps/finish_setup.rb +27 -0
  236. data/app/services/postnhost/onboarding/steps/set_site_metadata.rb +70 -0
  237. data/app/services/postnhost/public_navigation_targets.rb +67 -0
  238. data/app/services/postnhost/public_request_context.rb +101 -0
  239. data/app/services/postnhost/publishing/article_variants/publish.rb +26 -0
  240. data/app/services/postnhost/publishing/article_variants/publish_many.rb +23 -0
  241. data/app/services/postnhost/publishing/article_variants/snapshot_writer.rb +44 -0
  242. data/app/services/postnhost/publishing/article_variants/unpublish.rb +27 -0
  243. data/app/services/postnhost/publishing/article_variants/unpublish_many.rb +23 -0
  244. data/app/services/postnhost/publishing/articles/publish.rb +25 -0
  245. data/app/services/postnhost/publishing/articles/publish_scheduled.rb +42 -0
  246. data/app/services/postnhost/publishing/articles/snapshot_writer.rb +84 -0
  247. data/app/services/postnhost/publishing/articles/unpublish.rb +26 -0
  248. data/app/services/postnhost/publishing/bulk_transition.rb +63 -0
  249. data/app/services/postnhost/publishing/error.rb +12 -0
  250. data/app/services/postnhost/publishing/page_variants/publish.rb +26 -0
  251. data/app/services/postnhost/publishing/page_variants/publish_many.rb +23 -0
  252. data/app/services/postnhost/publishing/page_variants/snapshot_writer.rb +41 -0
  253. data/app/services/postnhost/publishing/page_variants/unpublish.rb +27 -0
  254. data/app/services/postnhost/publishing/page_variants/unpublish_many.rb +23 -0
  255. data/app/services/postnhost/publishing/pages/publish.rb +25 -0
  256. data/app/services/postnhost/publishing/pages/snapshot_writer.rb +38 -0
  257. data/app/services/postnhost/publishing/pages/unpublish.rb +26 -0
  258. data/app/services/postnhost/publishing/revision.rb +37 -0
  259. data/app/services/postnhost/publishing/route_validator.rb +41 -0
  260. data/app/services/postnhost/publishing/versions/restore_draft.rb +64 -0
  261. data/app/services/postnhost/service_result.rb +7 -0
  262. data/app/services/postnhost/sitemap_builder.rb +395 -0
  263. data/app/services/postnhost/translation/article_variant_translator.rb +102 -0
  264. data/app/services/postnhost/translation/category_variant_translator.rb +92 -0
  265. data/app/services/postnhost/translation/content_translator.rb +119 -0
  266. data/app/services/postnhost/translation/openai_llm_client.rb +30 -0
  267. data/app/services/postnhost/translation/page_variant_translator.rb +55 -0
  268. data/app/uploaders/base_uploader.rb +26 -0
  269. data/app/uploaders/cover_image_uploader.rb +31 -0
  270. data/app/uploaders/postnhost/avatar_uploader.rb +17 -0
  271. data/app/uploaders/postnhost/inline_article_image_uploader.rb +38 -0
  272. data/app/uploaders/postnhost/inline_page_image_uploader.rb +38 -0
  273. data/app/uploaders/postnhost/setting_asset_uploader.rb +18 -0
  274. data/app/views/layouts/postnhost/_favicon.html.erb +6 -0
  275. data/app/views/layouts/postnhost/_footer.html.erb +67 -0
  276. data/app/views/layouts/postnhost/_header.html.erb +89 -0
  277. data/app/views/layouts/postnhost/_sidebar.html.erb +72 -0
  278. data/app/views/layouts/postnhost/application.html.erb +23 -0
  279. data/app/views/layouts/postnhost/articles.html.erb +34 -0
  280. data/app/views/layouts/postnhost/product.html.erb +36 -0
  281. data/app/views/layouts/postnhost/public/templates/default.html.erb +110 -0
  282. data/app/views/layouts/postnhost/public/templates/swiss-editorial.html.erb +213 -0
  283. data/app/views/layouts/postnhost/public/templates/workspace-journal.html.erb +231 -0
  284. data/app/views/layouts/postnhost/public_header/_dashboard_sign_in.html.erb +14 -0
  285. data/app/views/layouts/postnhost/public_header/_language_switcher.html.erb +43 -0
  286. data/app/views/layouts/postnhost/public_header/_logo.html.erb +9 -0
  287. data/app/views/postnhost/article_variants/_variants_list.html.erb +82 -0
  288. data/app/views/postnhost/article_variants/edit.html.erb +48 -0
  289. data/app/views/postnhost/article_variants/index.html.erb +128 -0
  290. data/app/views/postnhost/article_variants/publish.turbo_stream.erb +3 -0
  291. data/app/views/postnhost/article_variants/unpublish.turbo_stream.erb +3 -0
  292. data/app/views/postnhost/article_variants/update.turbo_stream.erb +3 -0
  293. data/app/views/postnhost/articles/_author_manager.html.erb +38 -0
  294. data/app/views/postnhost/articles/_category_manager.html.erb +42 -0
  295. data/app/views/postnhost/articles/_cover_image_upload.html.erb +82 -0
  296. data/app/views/postnhost/articles/_suggestion_manager.html.erb +43 -0
  297. data/app/views/postnhost/articles/_version_snapshot.html.erb +10 -0
  298. data/app/views/postnhost/articles/edit.html.erb +51 -0
  299. data/app/views/postnhost/articles/index.html.erb +132 -0
  300. data/app/views/postnhost/articles/publish.turbo_stream.erb +3 -0
  301. data/app/views/postnhost/articles/unpublish.turbo_stream.erb +3 -0
  302. data/app/views/postnhost/articles/update.turbo_stream.erb +3 -0
  303. data/app/views/postnhost/articles/version_preview.html.erb +17 -0
  304. data/app/views/postnhost/articles/versions.html.erb +68 -0
  305. data/app/views/postnhost/categories/edit.html.erb +77 -0
  306. data/app/views/postnhost/categories/index.html.erb +80 -0
  307. data/app/views/postnhost/categories/new.html.erb +50 -0
  308. data/app/views/postnhost/categories/show.html.erb +99 -0
  309. data/app/views/postnhost/category_variants/_category_variants_list.html.erb +67 -0
  310. data/app/views/postnhost/category_variants/_form.html.erb +29 -0
  311. data/app/views/postnhost/category_variants/edit.html.erb +21 -0
  312. data/app/views/postnhost/category_variants/index.html.erb +82 -0
  313. data/app/views/postnhost/category_variants/new.html.erb +21 -0
  314. data/app/views/postnhost/languages/edit.html.erb +45 -0
  315. data/app/views/postnhost/languages/index.html.erb +86 -0
  316. data/app/views/postnhost/languages/new.html.erb +45 -0
  317. data/app/views/postnhost/languages/show.html.erb +156 -0
  318. data/app/views/postnhost/onboarding/_post_install_checklist.html.erb +40 -0
  319. data/app/views/postnhost/onboarding/_step_1.html.erb +42 -0
  320. data/app/views/postnhost/onboarding/_step_2.html.erb +38 -0
  321. data/app/views/postnhost/onboarding/_step_3.html.erb +62 -0
  322. data/app/views/postnhost/onboarding/_step_4.html.erb +26 -0
  323. data/app/views/postnhost/onboarding/show.html.erb +51 -0
  324. data/app/views/postnhost/page_variants/_variants_list.html.erb +82 -0
  325. data/app/views/postnhost/page_variants/edit.html.erb +50 -0
  326. data/app/views/postnhost/page_variants/index.html.erb +128 -0
  327. data/app/views/postnhost/page_variants/publish.turbo_stream.erb +3 -0
  328. data/app/views/postnhost/page_variants/unpublish.turbo_stream.erb +3 -0
  329. data/app/views/postnhost/page_variants/update.turbo_stream.erb +3 -0
  330. data/app/views/postnhost/pages/edit.html.erb +50 -0
  331. data/app/views/postnhost/pages/index.html.erb +110 -0
  332. data/app/views/postnhost/pages/publish.turbo_stream.erb +3 -0
  333. data/app/views/postnhost/pages/unpublish.turbo_stream.erb +3 -0
  334. data/app/views/postnhost/public/articles/content_for/_index.html.erb +30 -0
  335. data/app/views/postnhost/public/articles/content_for/_preview.html.erb +9 -0
  336. data/app/views/postnhost/public/articles/content_for/_show.html.erb +24 -0
  337. data/app/views/postnhost/public/authors/content_for/_show.html.erb +24 -0
  338. data/app/views/postnhost/public/categories/content_for/_index.html.erb +29 -0
  339. data/app/views/postnhost/public/static_pages/content_for/_show.html.erb +19 -0
  340. data/app/views/postnhost/public/templates/default/articles/_article_card.html.erb +37 -0
  341. data/app/views/postnhost/public/templates/default/articles/_search_form.html.erb +12 -0
  342. data/app/views/postnhost/public/templates/default/articles/_suggestions.html.erb +44 -0
  343. data/app/views/postnhost/public/templates/default/articles/_top_picks.html.erb +42 -0
  344. data/app/views/postnhost/public/templates/default/articles/index.html.erb +53 -0
  345. data/app/views/postnhost/public/templates/default/articles/preview.html.erb +102 -0
  346. data/app/views/postnhost/public/templates/default/articles/show.html.erb +111 -0
  347. data/app/views/postnhost/public/templates/default/authors/_article_card.html.erb +28 -0
  348. data/app/views/postnhost/public/templates/default/authors/_profile_block.html.erb +29 -0
  349. data/app/views/postnhost/public/templates/default/authors/show.html.erb +36 -0
  350. data/app/views/postnhost/public/templates/default/categories/_article_card.html.erb +34 -0
  351. data/app/views/postnhost/public/templates/default/categories/index.html.erb +48 -0
  352. data/app/views/postnhost/public/templates/default/shared/_article_authors.html.erb +24 -0
  353. data/app/views/postnhost/public/templates/default/shared/_author_avatar.html.erb +9 -0
  354. data/app/views/postnhost/public/templates/default/shared/_hero_cover_image.html.erb +11 -0
  355. data/app/views/postnhost/public/templates/default/shared/_pagination.html.erb +35 -0
  356. data/app/views/postnhost/public/templates/default/shared/_thumbnail_cover_image.html.erb +9 -0
  357. data/app/views/postnhost/public/templates/default/static_pages/show.html.erb +22 -0
  358. data/app/views/postnhost/public/templates/swiss-editorial/articles/_article_card.html.erb +28 -0
  359. data/app/views/postnhost/public/templates/swiss-editorial/articles/_search_form.html.erb +19 -0
  360. data/app/views/postnhost/public/templates/swiss-editorial/articles/_suggestions.html.erb +42 -0
  361. data/app/views/postnhost/public/templates/swiss-editorial/articles/_top_picks.html.erb +38 -0
  362. data/app/views/postnhost/public/templates/swiss-editorial/articles/index.html.erb +133 -0
  363. data/app/views/postnhost/public/templates/swiss-editorial/articles/preview.html.erb +42 -0
  364. data/app/views/postnhost/public/templates/swiss-editorial/articles/show.html.erb +100 -0
  365. data/app/views/postnhost/public/templates/swiss-editorial/authors/_article_card.html.erb +27 -0
  366. data/app/views/postnhost/public/templates/swiss-editorial/authors/_profile_block.html.erb +32 -0
  367. data/app/views/postnhost/public/templates/swiss-editorial/authors/show.html.erb +31 -0
  368. data/app/views/postnhost/public/templates/swiss-editorial/categories/_article_card.html.erb +28 -0
  369. data/app/views/postnhost/public/templates/swiss-editorial/categories/index.html.erb +88 -0
  370. data/app/views/postnhost/public/templates/swiss-editorial/shared/_article_authors.html.erb +26 -0
  371. data/app/views/postnhost/public/templates/swiss-editorial/shared/_author_avatar.html.erb +9 -0
  372. data/app/views/postnhost/public/templates/swiss-editorial/shared/_hero_cover_image.html.erb +11 -0
  373. data/app/views/postnhost/public/templates/swiss-editorial/shared/_pagination.html.erb +35 -0
  374. data/app/views/postnhost/public/templates/swiss-editorial/shared/_thumbnail_cover_image.html.erb +9 -0
  375. data/app/views/postnhost/public/templates/swiss-editorial/static_pages/show.html.erb +17 -0
  376. data/app/views/postnhost/public/templates/workspace-journal/articles/_article_card.html.erb +44 -0
  377. data/app/views/postnhost/public/templates/workspace-journal/articles/_search_form.html.erb +19 -0
  378. data/app/views/postnhost/public/templates/workspace-journal/articles/_suggestions.html.erb +49 -0
  379. data/app/views/postnhost/public/templates/workspace-journal/articles/_top_picks.html.erb +34 -0
  380. data/app/views/postnhost/public/templates/workspace-journal/articles/index.html.erb +69 -0
  381. data/app/views/postnhost/public/templates/workspace-journal/articles/preview.html.erb +113 -0
  382. data/app/views/postnhost/public/templates/workspace-journal/articles/show.html.erb +120 -0
  383. data/app/views/postnhost/public/templates/workspace-journal/authors/_article_card.html.erb +1 -0
  384. data/app/views/postnhost/public/templates/workspace-journal/authors/_profile_block.html.erb +32 -0
  385. data/app/views/postnhost/public/templates/workspace-journal/authors/show.html.erb +33 -0
  386. data/app/views/postnhost/public/templates/workspace-journal/categories/_article_card.html.erb +1 -0
  387. data/app/views/postnhost/public/templates/workspace-journal/categories/index.html.erb +58 -0
  388. data/app/views/postnhost/public/templates/workspace-journal/shared/_article_authors.html.erb +27 -0
  389. data/app/views/postnhost/public/templates/workspace-journal/shared/_article_card.html.erb +44 -0
  390. data/app/views/postnhost/public/templates/workspace-journal/shared/_author_avatar.html.erb +9 -0
  391. data/app/views/postnhost/public/templates/workspace-journal/shared/_category_navigation.html.erb +10 -0
  392. data/app/views/postnhost/public/templates/workspace-journal/shared/_hero_cover_image.html.erb +11 -0
  393. data/app/views/postnhost/public/templates/workspace-journal/shared/_pagination.html.erb +35 -0
  394. data/app/views/postnhost/public/templates/workspace-journal/shared/_thumbnail_cover_image.html.erb +9 -0
  395. data/app/views/postnhost/public/templates/workspace-journal/static_pages/show.html.erb +17 -0
  396. data/app/views/postnhost/sessions/new.html.erb +59 -0
  397. data/app/views/postnhost/settings/edit.html.erb +68 -0
  398. data/app/views/postnhost/settings/sections/_assets.html.erb +40 -0
  399. data/app/views/postnhost/settings/sections/_feature_flags.html.erb +43 -0
  400. data/app/views/postnhost/settings/sections/_i18n.html.erb +57 -0
  401. data/app/views/postnhost/settings/sections/_navigation.html.erb +98 -0
  402. data/app/views/postnhost/settings/sections/_schema.html.erb +253 -0
  403. data/app/views/postnhost/settings/sections/_scripts.html.erb +79 -0
  404. data/app/views/postnhost/settings/sections/_site.html.erb +41 -0
  405. data/app/views/postnhost/settings/sections/_timezone.html.erb +19 -0
  406. data/app/views/postnhost/shared/_ai_translation_modal.html.erb +85 -0
  407. data/app/views/postnhost/shared/_flash_messages.html.erb +27 -0
  408. data/app/views/postnhost/shared/_form_errors.html.erb +22 -0
  409. data/app/views/postnhost/shared/_seo_meta_tags.html.erb +70 -0
  410. data/app/views/postnhost/shared/_structured_data.html.erb +5 -0
  411. data/app/views/postnhost/shared/articles/_action_buttons.html.erb +87 -0
  412. data/app/views/postnhost/shared/articles/_cover_image_preview.html.erb +30 -0
  413. data/app/views/postnhost/shared/articles/_editor_column.html.erb +15 -0
  414. data/app/views/postnhost/shared/articles/_editor_column_preview.html.erb +33 -0
  415. data/app/views/postnhost/shared/articles/_error_messages.html.erb +23 -0
  416. data/app/views/postnhost/shared/articles/_meta_sidebar.html.erb +322 -0
  417. data/app/views/postnhost/shared/articles/_meta_sidebar_preview.html.erb +29 -0
  418. data/app/views/postnhost/shared/articles/_post_actions.html.erb +105 -0
  419. data/app/views/postnhost/shared/articles/_schedule_actions.html.erb +51 -0
  420. data/app/views/postnhost/shared/articles/_tiptap_editor.html.erb +225 -0
  421. data/app/views/postnhost/shared/articles/_title_field.html.erb +9 -0
  422. data/app/views/postnhost/shared/articles/_version_action_buttons.html.erb +35 -0
  423. data/app/views/postnhost/shared/elements/_button.html.erb +20 -0
  424. data/app/views/postnhost/shared/elements/_button_link.html.erb +24 -0
  425. data/app/views/postnhost/shared/elements/_empty_state.html.erb +22 -0
  426. data/app/views/postnhost/shared/elements/_error_messages.html.erb +28 -0
  427. data/app/views/postnhost/shared/elements/_form_card.html.erb +10 -0
  428. data/app/views/postnhost/shared/elements/_form_page_header.html.erb +21 -0
  429. data/app/views/postnhost/shared/elements/_icon_button.html.erb +17 -0
  430. data/app/views/postnhost/shared/elements/_page_header.html.erb +40 -0
  431. data/app/views/postnhost/shared/elements/_table_container.html.erb +23 -0
  432. data/app/views/postnhost/shared/pages/_action_buttons.html.erb +84 -0
  433. data/app/views/postnhost/shared/pages/_meta_sidebar.html.erb +132 -0
  434. data/app/views/postnhost/shared/pages/_post_actions.html.erb +69 -0
  435. data/app/views/postnhost/templates/edit.html.erb +83 -0
  436. data/app/views/postnhost/users/_avatar.html.erb +9 -0
  437. data/app/views/postnhost/users/_form.html.erb +160 -0
  438. data/app/views/postnhost/users/edit.html.erb +26 -0
  439. data/app/views/postnhost/users/index.html.erb +91 -0
  440. data/app/views/postnhost/users/new.html.erb +9 -0
  441. data/app/views/postnhost/users/schema_edit.html.erb +175 -0
  442. data/config/locales/de.yml +55 -0
  443. data/config/locales/en.yml +88 -0
  444. data/config/locales/es.yml +58 -0
  445. data/config/locales/fr.yml +55 -0
  446. data/config/locales/ja.yml +55 -0
  447. data/config/locales/ko.yml +55 -0
  448. data/config/locales/pl.yml +55 -0
  449. data/config/locales/pt.yml +55 -0
  450. data/config/locales/ru.yml +55 -0
  451. data/config/routes.rb +112 -0
  452. data/db/migrate/20250822213820_create_users.rb +29 -0
  453. data/db/migrate/20250822214046_create_categories.rb +15 -0
  454. data/db/migrate/20250822214106_create_languages.rb +16 -0
  455. data/db/migrate/20250822214148_create_articles.rb +34 -0
  456. data/db/migrate/20250822214217_create_article_variants.rb +24 -0
  457. data/db/migrate/20250929091137_create_article_categories.rb +12 -0
  458. data/db/migrate/20251013204200_create_versions.rb +40 -0
  459. data/db/migrate/20260106074532_create_article_suggestions.rb +13 -0
  460. data/db/migrate/20260108110506_create_category_variants.rb +15 -0
  461. data/db/migrate/20260308130000_create_settings.rb +23 -0
  462. data/db/migrate/20260323113000_create_pages.rb +19 -0
  463. data/db/migrate/20260414100000_create_article_authors.rb +14 -0
  464. data/db/migrate/20260512100000_create_templates.rb +9 -0
  465. data/db/migrate/20260520110001_create_navigations.rb +9 -0
  466. data/db/migrate/20260520110002_create_navigation_items.rb +22 -0
  467. data/db/migrate/20260528000002_create_page_variants.rb +20 -0
  468. data/db/migrate/20260709090000_create_snapshots.rb +154 -0
  469. data/db/migrate/20260726130000_create_site_scripts.rb +11 -0
  470. data/lib/generators/postnhost/install/install_generator.rb +56 -0
  471. data/lib/generators/postnhost/install/templates/carrierwave.rb +17 -0
  472. data/lib/generators/postnhost/install/templates/postnhost.rb +36 -0
  473. data/lib/generators/postnhost/locale/locale_generator.rb +36 -0
  474. data/lib/generators/postnhost/static_pages/static_pages_generator.rb +47 -0
  475. data/lib/generators/postnhost/tailwindcss/install_generator.rb +97 -0
  476. data/lib/generators/postnhost/tailwindcss/templates/host.tailwind.css +7 -0
  477. data/lib/generators/postnhost/tailwindcss/templates/tailwind.config.js +8 -0
  478. data/lib/generators/postnhost/user/user_generator.rb +36 -0
  479. data/lib/generators/postnhost/views/views_generator.rb +142 -0
  480. data/lib/postnhost/configuration.rb +64 -0
  481. data/lib/postnhost/engine.rb +57 -0
  482. data/lib/postnhost/sample_data.rb +865 -0
  483. data/lib/postnhost/settings/i18n_overrides.rb +137 -0
  484. data/lib/postnhost/settings/i18n_patch.rb +18 -0
  485. data/lib/postnhost/version.rb +1 -3
  486. data/lib/postnhost.rb +26 -3
  487. data/lib/tasks/postnhost_snapshots.rake +77 -0
  488. data/lib/tasks/postnhost_tasks.rake +96 -0
  489. metadata +662 -21
  490. data/.github/workflows/main.yml +0 -27
  491. data/.gitignore +0 -11
  492. data/.rspec +0 -3
  493. data/.rubocop.yml +0 -8
  494. data/Gemfile +0 -13
  495. data/bin/console +0 -11
  496. data/bin/setup +0 -8
  497. data/postnhost.gemspec +0 -31
@@ -0,0 +1,103 @@
1
+ module Postnhost
2
+ module PublicFooterData
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
7
+ def set_footer_data
8
+ @setting = current_setting
9
+ @use_auto_header_navigation = @setting.use_auto_header_navigation
10
+ @use_auto_footer_navigation = @setting.use_auto_footer_navigation
11
+ initialize_navigation_data
12
+
13
+ set_auto_navigation_data if @use_auto_header_navigation || @use_auto_footer_navigation
14
+ set_custom_navigation_data unless @use_auto_header_navigation && @use_auto_footer_navigation
15
+ end
16
+
17
+ def initialize_navigation_data
18
+ @sidebar_categories = []
19
+ @header_categories = []
20
+ @footer_categories = []
21
+ @footer_published_pages = []
22
+ @footer_static_pages = []
23
+ @header_navigation_items = []
24
+ @footer_navigation_columns = []
25
+ end
26
+
27
+ def set_auto_navigation_data
28
+ data = Rails.cache.fetch(public_navigation_cache_key("auto")) do
29
+ {
30
+ categories: auto_navigation_categories.to_a,
31
+ published_pages: published_footer_pages.to_a
32
+ }
33
+ end
34
+
35
+ if @use_auto_header_navigation
36
+ @sidebar_categories = data.fetch(:categories)
37
+ @header_categories = data.fetch(:categories)
38
+ end
39
+
40
+ return unless @use_auto_footer_navigation
41
+
42
+ @footer_categories = data.fetch(:categories)
43
+ @footer_published_pages = data.fetch(:published_pages)
44
+ @footer_static_pages = static_footer_pages - @footer_published_pages.map(&:slug)
45
+ end
46
+
47
+ def set_custom_navigation_data
48
+ items = Rails.cache.fetch(public_navigation_cache_key("custom")) do
49
+ @setting.current_navigation.navigation_items.roots.with_children.to_a
50
+ end
51
+ @default_language = public_request_context.default_language
52
+ targets = Postnhost::PublicNavigationTargets.call(
53
+ items: items.flat_map { |item| [item, *item.children] },
54
+ language: @current_language
55
+ ).value
56
+ @navigation_target_cache = targets.records
57
+ @navigation_variant_cache = targets.variants
58
+
59
+ @header_navigation_items = items.select { |item| item.container_kind == "header" && item.parent_id.nil? }.sort_by(&:position) unless @use_auto_header_navigation
60
+ return if @use_auto_footer_navigation
61
+
62
+ @footer_navigation_columns = items.select { |item| item.container_kind == "footer" && item.parent_id.nil? }.sort_by(&:position)
63
+ end
64
+
65
+ def public_navigation_cache_key(kind)
66
+ ["postnhost", "public_navigation", kind, public_site_revision, @current_language]
67
+ end
68
+
69
+ def auto_navigation_categories
70
+ Postnhost::Category.with_category_variants_for_language(@current_language)
71
+ .with_published_articles
72
+ .alphabetical
73
+ end
74
+
75
+ def static_footer_pages
76
+ static_page_template_paths
77
+ .map { |template_path| File.basename(template_path, ".html.erb") }
78
+ .uniq
79
+ .sort
80
+ end
81
+
82
+ def published_footer_pages
83
+ return default_footer_pages if @current_language.default?
84
+
85
+ localized_footer_pages
86
+ end
87
+
88
+ def default_footer_pages
89
+ Postnhost::Snapshot::Page.footer_links
90
+ end
91
+
92
+ def localized_footer_pages
93
+ Postnhost::Snapshot::PageVariant.footer_links_for(@current_language)
94
+ end
95
+
96
+ def static_page_template_paths
97
+ [
98
+ Rails.root.join("app/views/postnhost/static_pages/*.html.erb"),
99
+ Postnhost::Engine.root.join("app/views/postnhost/static_pages/*.html.erb")
100
+ ].flat_map { |pattern| Dir[pattern.to_s] }
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,35 @@
1
+ module Postnhost
2
+ module PublicHttpCaching
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :public_site_revision
7
+ end
8
+
9
+ private
10
+
11
+ def apply_public_http_cache(*dependencies, template:)
12
+ return if user_signed_in?
13
+
14
+ fresh_when(
15
+ etag: [public_layout_digest, public_site_revision, @current_language&.id, controller_path, action_name,
16
+ request.script_name, current_setting.site_configuration_cache_key, *dependencies].compact,
17
+ public: true,
18
+ template:
19
+ )
20
+ expires_in 0.seconds, public: true, must_revalidate: true
21
+ end
22
+
23
+ def public_site_revision
24
+ public_request_context.public_site_revision
25
+ end
26
+
27
+ def public_layout_digest
28
+ ActionView::Digestor.digest(
29
+ name: "layouts/#{resolved_public_layout_template}",
30
+ format: nil,
31
+ finder: lookup_context
32
+ )
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,17 @@
1
+ module Postnhost
2
+ module PublicPageSize
3
+ extend ActiveSupport::Concern
4
+
5
+ PAGINATION_SLOTS = 3
6
+
7
+ private
8
+
9
+ def pagination_slots
10
+ PAGINATION_SLOTS
11
+ end
12
+
13
+ def public_page_size
14
+ current_setting.effective_public_page_size
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,44 @@
1
+ module Postnhost
2
+ module PublicTemplateResolvable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ helper_method :current_public_template_name
7
+ end
8
+
9
+ def default_url_options
10
+ options = @public_default_url_options ||= super.merge(current_setting.site_url_options)
11
+ return options unless preview_public_template_name
12
+
13
+ options.merge(preview_template: preview_public_template_name)
14
+ end
15
+
16
+ private
17
+
18
+ def current_public_template_name
19
+ @current_public_template_name ||= preview_public_template_name || public_request_context.template_name
20
+ end
21
+
22
+ def preview_public_template_name
23
+ return @preview_public_template_name if defined?(@preview_public_template_name)
24
+ return @preview_public_template_name = nil unless user_signed_in? && params[:preview_template].present?
25
+
26
+ @preview_public_template_name = params[:preview_template].tap do |template_name|
27
+ raise ActiveRecord::RecordNotFound unless Postnhost::Template::NAMES.include?(template_name)
28
+ end
29
+ end
30
+
31
+ def resolved_public_layout_template
32
+ "postnhost/public/templates/#{current_public_template_name}"
33
+ end
34
+
35
+ def render_public_template(path, **options)
36
+ resolved_template = resolve_public_template(path)
37
+ render({ template: resolved_template }.merge(options))
38
+ end
39
+
40
+ def resolve_public_template(path)
41
+ "postnhost/public/templates/#{current_public_template_name}/#{path}"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,61 @@
1
+ module Postnhost
2
+ module Settings
3
+ module I18nManageable
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ def translations_params
9
+ params.fetch(:translations, ActionController::Parameters.new).permit!.to_h
10
+ end
11
+
12
+ def selected_locale_key
13
+ locale_keys = @locale_keys || locale_keys_for_i18n
14
+ locale = params[:locale].presence || params[:locale_key].presence
15
+ if locale.present?
16
+ locale_key = locale.to_s
17
+ return locale_key if locale_keys.include?(locale_key)
18
+ end
19
+
20
+ locale_keys.first
21
+ end
22
+
23
+ def update_i18n
24
+ locale = selected_locale_key
25
+ if locale.blank?
26
+ @setting.errors.add(:base, "Create at least one language before editing i18n settings.")
27
+ set_i18n_editor_state
28
+ render :edit, status: :unprocessable_content
29
+ return
30
+ end
31
+
32
+ merged_overrides = Postnhost::Settings::I18nOverrides.merge_locale_overrides(
33
+ @setting.locale_overrides, locale, translations_params
34
+ )
35
+
36
+ if @setting.update(locale_overrides: merged_overrides)
37
+ redirect_to edit_settings_path(section: "i18n", locale: locale),
38
+ notice: "I18n settings were successfully updated."
39
+ else
40
+ set_i18n_editor_state
41
+ @translation_entries = @translation_entries.merge(translations_params.transform_keys(&:to_s))
42
+ render :edit, status: :unprocessable_content
43
+ end
44
+ end
45
+
46
+ def set_i18n_editor_state
47
+ @locale_keys = locale_keys_for_i18n
48
+ @selected_locale_key = selected_locale_key
49
+ @translation_entries = Postnhost::Settings::I18nOverrides.editable_translations_for_locale(
50
+ @selected_locale_key,
51
+ @setting.locale_overrides
52
+ )
53
+ end
54
+
55
+ def locale_keys_for_i18n
56
+ allowed_locales = Postnhost::Language.pluck(:html_lang).compact
57
+ Postnhost::Settings::I18nOverrides.locale_keys(@setting.locale_overrides, allowed_locales: allowed_locales)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,118 @@
1
+ module Postnhost
2
+ module Settings
3
+ module NavigationManageable
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ def set_navigation_editor_state
9
+ @navigation_locale_keys = Postnhost::Language.order(:name).pluck(:html_lang)
10
+ @selected_navigation_locale_key = selected_navigation_locale_key
11
+ @default_navigation_locale_key = Postnhost::Language.blog_default&.html_lang || I18n.default_locale.to_s
12
+ @navigation = @setting.current_navigation
13
+ @navigation_tree = navigation_tree_for_editor
14
+ @navigation_target_options = navigation_target_options
15
+ end
16
+
17
+ def selected_navigation_locale_key
18
+ locale_keys = @navigation_locale_keys || Postnhost::Language.order(:name).pluck(:html_lang)
19
+ locale = params[:locale].presence || params[:locale_key].presence
20
+ locale = locale.to_s if locale.present?
21
+ return locale if locale.present? && locale_keys.include?(locale)
22
+
23
+ locale_keys.first
24
+ end
25
+
26
+ def update_navigation
27
+ set_navigation_editor_state
28
+ @setting.assign_attributes(
29
+ use_auto_header_navigation: navigation_setting_enabled?(:use_auto_header_navigation),
30
+ use_auto_footer_navigation: navigation_setting_enabled?(:use_auto_footer_navigation)
31
+ )
32
+
33
+ tree = parse_navigation_tree
34
+ if tree.nil?
35
+ @setting.errors.add(:base, "Navigation payload is invalid")
36
+ render :edit, status: :unprocessable_content
37
+ return
38
+ end
39
+
40
+ Postnhost::Setting.transaction do
41
+ @setting.save!
42
+ @navigation.replace_tree!(tree:, locale_key: @selected_navigation_locale_key)
43
+ end
44
+
45
+ redirect_to edit_settings_path(section: "navigation", locale: @selected_navigation_locale_key),
46
+ notice: "Navigation settings were successfully updated."
47
+ end
48
+
49
+ def navigation_setting_enabled?(key)
50
+ ActiveModel::Type::Boolean.new.cast(params.dig(:setting, key))
51
+ end
52
+
53
+ def parse_navigation_tree
54
+ raw_tree = params.dig(:navigation, :tree)
55
+ return nil if raw_tree.blank?
56
+
57
+ parsed = JSON.parse(raw_tree)
58
+ return unless parsed.is_a?(Hash)
59
+
60
+ header = parsed["header"]
61
+ footer = parsed["footer"]
62
+ return unless header.is_a?(Array) && footer.is_a?(Array)
63
+
64
+ {
65
+ "header" => header,
66
+ "footer" => footer
67
+ }
68
+ rescue JSON::ParserError
69
+ nil
70
+ end
71
+
72
+ def navigation_tree_for_editor
73
+ items = @navigation.navigation_items.includes(:children).to_a
74
+
75
+ {
76
+ "header" => navigation_items_tree(items.select { |item| item.container_kind == "header" && item.parent_id.nil? }),
77
+ "footer" => navigation_items_tree(items.select { |item| item.container_kind == "footer" && item.parent_id.nil? })
78
+ }
79
+ end
80
+
81
+ def navigation_items_tree(items)
82
+ items.sort_by(&:position).map do |item|
83
+ {
84
+ "id" => item.id,
85
+ "kind" => item.kind,
86
+ "target_kind" => item.target_kind,
87
+ "target_id" => item.target_id,
88
+ "target_slug" => item.target_slug,
89
+ "url" => item.url,
90
+ "nofollow" => item.nofollow,
91
+ "label" => item.label_for(locale_key: @selected_navigation_locale_key, default_locale_key: @default_navigation_locale_key),
92
+ "has_locale_override" => item.label_translations.to_h.key?(@selected_navigation_locale_key),
93
+ "children" => navigation_items_tree(item.children)
94
+ }
95
+ end
96
+ end
97
+
98
+ def navigation_target_options
99
+ {
100
+ "article" => Postnhost::Snapshot::Article.order(:title).pluck(:title, :article_id),
101
+ "page" => Postnhost::Snapshot::Page.order(:title).pluck(:title, :page_id),
102
+ "category" => Postnhost::Category.order(:name).pluck(:name, :id),
103
+ "static_page" => static_page_slugs.map { |slug| [slug.humanize, slug] }
104
+ }
105
+ end
106
+
107
+ def static_page_slugs
108
+ [
109
+ Rails.root.join("app/views/postnhost/static_pages/*.html.erb"),
110
+ Postnhost::Engine.root.join("app/views/postnhost/static_pages/*.html.erb")
111
+ ].flat_map { |pattern| Dir[pattern.to_s] }
112
+ .map { |path| File.basename(path, ".html.erb") }
113
+ .uniq
114
+ .sort
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,140 @@
1
+ module Postnhost
2
+ module Settings
3
+ module SchemaManageable
4
+ extend ActiveSupport::Concern
5
+ include Postnhost::SchemaHelper
6
+
7
+ SCHEMA_SETTING_FIELDS = %w[
8
+ organization_type
9
+ founding_date
10
+ founding_location
11
+ contact_email
12
+ contact_type
13
+ address
14
+ address_city
15
+ address_country
16
+ default_article_type
17
+ coverage_starts_at
18
+ coverage_ends_at
19
+ ].freeze
20
+
21
+ SCHEMA_ARRAY_FIELDS = %w[same_as].freeze
22
+
23
+ private
24
+
25
+ def set_schema_editor_state
26
+ @schema_locale_keys = schema_locale_keys
27
+ @schema_default_locale_key = @setting.schema_default_locale_key
28
+ @schema_selected_locale_key = selected_schema_locale_key
29
+ @schema_setting_values = @setting.schema_settings_hash
30
+ @schema_translation_entries = schema_translation_entries_for(@schema_selected_locale_key)
31
+ end
32
+
33
+ def selected_schema_locale_key
34
+ @schema_locale_keys ||= schema_locale_keys
35
+ locale = params[:locale].presence || params[:locale_key].presence
36
+ return locale if locale.present? && @schema_locale_keys.include?(locale.to_s)
37
+
38
+ @schema_default_locale_key || @setting.schema_default_locale_key
39
+ end
40
+
41
+ def update_schema
42
+ @schema_locale_keys = schema_locale_keys
43
+ @schema_default_locale_key = @setting.schema_default_locale_key
44
+ locale_key = selected_schema_locale_key
45
+ merged_schema_settings = @setting.schema_settings_hash.merge(schema_settings_params)
46
+ merged_schema_overrides = merge_schema_locale_overrides(
47
+ @setting.schema_locale_overrides_hash,
48
+ locale_key,
49
+ schema_translation_params
50
+ )
51
+
52
+ if @setting.update(schema_settings: merged_schema_settings, schema_locale_overrides: merged_schema_overrides)
53
+ redirect_to edit_settings_path(section: "schema", locale: locale_key),
54
+ notice: "Schema settings were successfully updated."
55
+ else
56
+ set_schema_editor_state
57
+ @schema_setting_values = merged_schema_settings
58
+ @schema_translation_entries = @schema_translation_entries.merge(schema_translation_params.stringify_keys)
59
+ render :edit, status: :unprocessable_content
60
+ end
61
+ end
62
+
63
+ def schema_locale_keys
64
+ keys = Postnhost::Language.pluck(:html_lang).compact.uniq
65
+ return [I18n.default_locale.to_s] if keys.empty?
66
+
67
+ return keys unless keys.include?("en")
68
+
69
+ ["en"] + keys.reject { |locale| locale == "en" }
70
+ end
71
+
72
+ def schema_translation_entries_for(locale)
73
+ SETTINGS_TEXT_OVERRIDE_FIELDS.index_with do |field|
74
+ @setting.schema_text_for(locale:, key: field, fallback: schema_text_fallback(field))
75
+ end
76
+ end
77
+
78
+ def schema_translation_params
79
+ translations = params.fetch(:schema_translations, ActionController::Parameters.new).permit!
80
+ SETTINGS_TEXT_OVERRIDE_FIELDS.index_with do |field|
81
+ translations[field]
82
+ end
83
+ end
84
+
85
+ def schema_settings_params
86
+ settings_params = params.fetch(:schema_settings, ActionController::Parameters.new).permit!
87
+ merged = SCHEMA_SETTING_FIELDS.index_with { |field| settings_params[field] }
88
+
89
+ SCHEMA_ARRAY_FIELDS.each do |field|
90
+ merged[field] = schema_multiline_to_array(settings_params[field])
91
+ end
92
+
93
+ merged["policies"] = schema_policy_fields.keys.index_with do |key|
94
+ settings_params.dig("policies", key)
95
+ end
96
+
97
+ merged.transform_values do |value|
98
+ value.is_a?(String) ? value.strip : value
99
+ end
100
+ end
101
+
102
+ def merge_schema_locale_overrides(current_overrides, locale_key, edited_translations)
103
+ locale = locale_key.to_s
104
+ normalized = current_overrides.deep_dup
105
+ normalized[locale] ||= {}
106
+
107
+ edited_translations.each do |field, value|
108
+ text = value.to_s.strip
109
+ if text.blank?
110
+ normalized[locale].delete(field)
111
+ next
112
+ end
113
+
114
+ if locale != @schema_default_locale_key && text == default_locale_schema_value(field)
115
+ normalized[locale].delete(field)
116
+ next
117
+ end
118
+
119
+ normalized[locale][field] = text
120
+ end
121
+
122
+ normalized.delete(locale) if normalized[locale].blank?
123
+ normalized
124
+ end
125
+
126
+ def default_locale_schema_value(field)
127
+ @setting.schema_text_for(locale: @schema_default_locale_key, key: field, fallback: schema_text_fallback(field))
128
+ end
129
+
130
+ def schema_text_fallback(field)
131
+ case field.to_s
132
+ when "website_name", "organization_name"
133
+ I18n.t("postnhost.public.site.schema_site_name")
134
+ when "website_description", "organization_description"
135
+ I18n.t("postnhost.public.site.blog_meta_description")
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,119 @@
1
+ module Postnhost
2
+ module Users
3
+ module SchemaManageable
4
+ extend ActiveSupport::Concern
5
+ include Postnhost::SchemaHelper
6
+
7
+ USER_SCHEMA_PROFILE_FIELDS = %w[
8
+ same_as
9
+ image_url
10
+ knows_language
11
+ alumni_type
12
+ ].freeze
13
+
14
+ USER_SCHEMA_TEXT_FIELDS = %w[
15
+ name
16
+ job_title
17
+ bio
18
+ knows_about
19
+ alumni_of
20
+ awards
21
+ ].freeze
22
+
23
+ private
24
+
25
+ def set_user_schema_editor_state
26
+ @schema_locale_keys = user_schema_locale_keys
27
+ @schema_default_locale_key = user_schema_default_locale_key
28
+ @schema_selected_locale_key = selected_user_schema_locale_key
29
+ @schema_profile_values = @user.schema_profile_hash
30
+ @schema_translation_entries = user_schema_translation_entries_for(@schema_selected_locale_key)
31
+ end
32
+
33
+ def selected_user_schema_locale_key
34
+ @schema_locale_keys ||= user_schema_locale_keys
35
+ locale = params[:locale].presence || params[:locale_key].presence
36
+ return locale if locale.present? && @schema_locale_keys.include?(locale.to_s)
37
+
38
+ user_schema_default_locale_key
39
+ end
40
+
41
+ def user_schema_default_locale_key
42
+ Postnhost::Language.find_by(default: true)&.html_lang.presence || I18n.default_locale.to_s
43
+ end
44
+
45
+ def user_schema_locale_keys
46
+ keys = Postnhost::Language.pluck(:html_lang).compact.uniq
47
+ return [user_schema_default_locale_key] if keys.empty?
48
+
49
+ return keys unless keys.include?("en")
50
+
51
+ ["en"] + keys.reject { |locale| locale == "en" }
52
+ end
53
+
54
+ def user_schema_profile_params
55
+ settings_params = params.fetch(:schema_profile, ActionController::Parameters.new).permit!
56
+ USER_SCHEMA_PROFILE_FIELDS.index_with do |field|
57
+ if %w[same_as knows_language].include?(field)
58
+ schema_multiline_to_array(settings_params[field])
59
+ else
60
+ settings_params[field].to_s.strip
61
+ end
62
+ end
63
+ end
64
+
65
+ def user_schema_translation_params
66
+ translations = params.fetch(:schema_translations, ActionController::Parameters.new).permit!
67
+ USER_SCHEMA_TEXT_FIELDS.index_with do |field|
68
+ translations[field].to_s.strip
69
+ end
70
+ end
71
+
72
+ def user_schema_translation_entries_for(locale)
73
+ USER_SCHEMA_TEXT_FIELDS.index_with do |field|
74
+ @user.schema_text_for(locale:, key: field, fallback: user_schema_fallback(field))
75
+ end
76
+ end
77
+
78
+ def merge_user_schema_locale_overrides(current_overrides, locale_key, edited_translations)
79
+ locale = locale_key.to_s
80
+ normalized = current_overrides.deep_dup
81
+ normalized[locale] ||= {}
82
+
83
+ edited_translations.each do |field, value|
84
+ if value.blank?
85
+ normalized[locale].delete(field)
86
+ next
87
+ end
88
+
89
+ if locale != user_schema_default_locale_key && value == user_default_schema_value(field)
90
+ normalized[locale].delete(field)
91
+ next
92
+ end
93
+
94
+ normalized[locale][field] = value
95
+ end
96
+
97
+ normalized.delete(locale) if normalized[locale].blank?
98
+ normalized
99
+ end
100
+
101
+ def user_default_schema_value(field)
102
+ @user.schema_text_for(locale: user_schema_default_locale_key, key: field, fallback: user_schema_fallback(field))
103
+ end
104
+
105
+ def user_schema_fallback(field)
106
+ case field.to_s
107
+ when "name"
108
+ @user.name
109
+ when "job_title"
110
+ @user.position
111
+ when "bio"
112
+ @user.bio
113
+ else
114
+ ""
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,37 @@
1
+ module Postnhost
2
+ class ApplicationController < ::ApplicationController
3
+ include Postnhost::Authentication
4
+ include Pagy::Method
5
+
6
+ # Base locale before public LanguageSwitcher; avoids leaking I18n.locale into CMS/tests.
7
+ prepend_before_action :set_cms_locale
8
+
9
+ around_action :use_postnhost_timezone
10
+
11
+ helper_method :current_setting
12
+
13
+ helper Postnhost::ApplicationHelper
14
+ helper Postnhost::ArticleHelper
15
+ helper Postnhost::CategoryHelper
16
+ helper Postnhost::CommonCssHelper
17
+ helper Postnhost::NavigationHelper
18
+ helper Postnhost::PagyHelper
19
+ helper Postnhost::PublicPagesHelper
20
+ helper Postnhost::SeoHelper
21
+ helper Postnhost::SettingsHelper
22
+
23
+ private
24
+
25
+ def current_setting
26
+ @current_setting ||= Postnhost::Setting.current
27
+ end
28
+
29
+ def set_cms_locale
30
+ I18n.locale = I18n.default_locale
31
+ end
32
+
33
+ def use_postnhost_timezone(&)
34
+ Time.use_zone(current_setting.effective_timezone_name, &)
35
+ end
36
+ end
37
+ end