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,138 @@
1
+ module Postnhost
2
+ class ArticleVariantsController < ApplicationController
3
+ PAGE_SIZE = 15
4
+
5
+ before_action :authenticate_user!
6
+ before_action :load_article
7
+ before_action :load_article_variant, only: %i[edit update publish unpublish destroy]
8
+ before_action :ensure_variant_editable, only: %i[edit update publish unpublish]
9
+
10
+ layout :resolve_layout
11
+
12
+ def index
13
+ variants = @article.article_variants.includes(:language, :article_variant_snapshot).order(:created_at)
14
+ @pagy, @variants = pagy(:offset, variants, limit: PAGE_SIZE)
15
+ @available_languages = Postnhost::Language.where.not(id: @article.article_variants.select(:language_id))
16
+ @available_languages = @available_languages.where.not(id: @article.language_id) if @article.language_id.present?
17
+ end
18
+
19
+ def new
20
+ @article_variant = @article.article_variants.new
21
+ @article_variant.language = Postnhost::Language.find_by(id: params[:language_id]) if params[:language_id]
22
+ @article_variant.title = @article.title
23
+ @article_variant.title_tag = @article.title_tag
24
+ @article_variant.og_title = @article.og_title
25
+ @article_variant.schema_headline = @article.schema_headline
26
+ @article_variant.meta_description = @article.meta_description
27
+ @article_variant.custom_excerpt = @article.custom_excerpt
28
+ @article_variant.use_excerpt_as_meta_description = @article.use_excerpt_as_meta_description
29
+ @article_variant.content = @article.content
30
+ @article_variant.save!
31
+
32
+ redirect_to edit_article_variant_path(@article, @article_variant)
33
+ end
34
+
35
+ def edit; end
36
+
37
+ def update
38
+ if @article_variant.update(variant_params)
39
+ respond_to do |format|
40
+ format.html do
41
+ redirect_to edit_article_variant_path(@article, @article_variant)
42
+ end
43
+ format.json { head :no_content }
44
+ format.turbo_stream
45
+ end
46
+ else
47
+ render :edit, status: :unprocessable_content
48
+ end
49
+ end
50
+
51
+ def publish
52
+ result = Postnhost::Publishing::ArticleVariants::Publish.call(article_variant: @article_variant)
53
+ @article_variant.reload
54
+ error = result.errors.to_sentence unless result.success?
55
+
56
+ respond_to do |format|
57
+ format.html do
58
+ redirect_to edit_article_variant_path(@article, @article_variant), **(error.present? ? { alert: error } : {})
59
+ end
60
+ format.turbo_stream do
61
+ flash.now[:alert] = error if error.present?
62
+ render :publish, status: result.status
63
+ end
64
+ end
65
+ end
66
+
67
+ def unpublish
68
+ result = Postnhost::Publishing::ArticleVariants::Unpublish.call(article_variant: @article_variant)
69
+ @article_variant.reload
70
+ error = result.errors.to_sentence unless result.success?
71
+
72
+ respond_to do |format|
73
+ format.html do
74
+ redirect_to edit_article_variant_path(@article, @article_variant), **(error.present? ? { alert: error } : {})
75
+ end
76
+ format.turbo_stream do
77
+ flash.now[:alert] = error if error.present?
78
+ render :unpublish, status: result.status
79
+ end
80
+ end
81
+ end
82
+
83
+ def destroy
84
+ @article_variant.destroy
85
+ redirect_to article_variants_path(@article)
86
+ end
87
+
88
+ def bulk_publish
89
+ result = Postnhost::Publishing::ArticleVariants::PublishMany.call(article: @article, ids: params[:ids])
90
+ error = result.errors.to_sentence unless result.success?
91
+ error = "#{result.value[:failures].count} translation(s) failed." if result.success? && result.value[:failures].any?
92
+
93
+ redirect_to article_variants_path(@article), **(error.present? ? { alert: error } : {})
94
+ end
95
+
96
+ def bulk_unpublish
97
+ result = Postnhost::Publishing::ArticleVariants::UnpublishMany.call(article: @article, ids: params[:ids])
98
+ error = result.errors.to_sentence unless result.success?
99
+ error = "#{result.value[:failures].count} translation(s) failed." if result.success? && result.value[:failures].any?
100
+
101
+ redirect_to article_variants_path(@article), **(error.present? ? { alert: error } : {})
102
+ end
103
+
104
+ def bulk_destroy
105
+ variants = @article.article_variants.where(id: params[:ids]).where.not(generating: true)
106
+ count = variants.count
107
+ variants.destroy_all
108
+
109
+ redirect_to article_variants_path(@article), notice: "#{count} translation(s) deleted successfully."
110
+ end
111
+
112
+ private
113
+
114
+ def load_article
115
+ @article = Postnhost::Article.find(params[:article_id])
116
+ end
117
+
118
+ def load_article_variant
119
+ @article_variant = @article.article_variants.find(params[:id])
120
+ end
121
+
122
+ def ensure_variant_editable
123
+ return unless @article_variant.generating?
124
+
125
+ redirect_to article_variants_path(@article),
126
+ alert: "This translation is still in progress. You can edit it when it finishes."
127
+ end
128
+
129
+ def variant_params
130
+ params.require(:article_variant).permit(:title, :title_tag, :og_title, :schema_headline, :meta_description,
131
+ :custom_excerpt, :use_excerpt_as_meta_description, :content, :language_id)
132
+ end
133
+
134
+ def resolve_layout
135
+ action_name == "index" ? "postnhost/product" : "postnhost/articles"
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,35 @@
1
+ module Postnhost
2
+ class Articles::CoverImagesController < ApplicationController
3
+ before_action :authenticate_user!
4
+ before_action :load_article, only: %i[create destroy]
5
+
6
+ def create
7
+ file = params[:file]
8
+
9
+ if file.present?
10
+ @article.cover_image = file
11
+
12
+ if @article.save
13
+ render json: { url: @article.cover_image.url }, status: :ok
14
+ else
15
+ render json: { error: @article.errors.full_messages.join(", ") }, status: :unprocessable_content
16
+ end
17
+ else
18
+ render json: { error: "No file provided" }, status: :bad_request
19
+ end
20
+ end
21
+
22
+ def destroy
23
+ # HACK: to bypass carrierwave image removal from bucket to keep the version history
24
+ @article.update_column(:cover_image, nil)
25
+
26
+ head :no_content
27
+ end
28
+
29
+ private
30
+
31
+ def load_article
32
+ @article = Postnhost::Article.find(params[:article_id])
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ module Postnhost
2
+ class Articles::ImagesController < ApplicationController
3
+ before_action :authenticate_user!
4
+ before_action :load_article
5
+
6
+ def create
7
+ file = params[:file]
8
+
9
+ if file.present?
10
+ result = @article.upload_inline_image(file)
11
+
12
+ if result[:success]
13
+ render json: { url: result[:url] }, status: :ok
14
+ else
15
+ render json: { error: result[:error] }, status: :unprocessable_content
16
+ end
17
+ else
18
+ render json: { error: "No file provided" }, status: :bad_request
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def load_article
25
+ @article = Postnhost::Article.find(params[:article_id])
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,26 @@
1
+ module Postnhost
2
+ class Articles::TranslationsController < ApplicationController
3
+ before_action :authenticate_user!
4
+ before_action :load_article
5
+
6
+ def new; end
7
+
8
+ def create
9
+ language_ids = params[:language_ids] || []
10
+
11
+ language_ids.each do |language_id|
12
+ Postnhost::Translation::ArticleVariantJob.perform_later(@article.id, language_id.to_i)
13
+
14
+ @article.article_variants.create!(language: Postnhost::Language.find_by(id: language_id.to_i), generating: true)
15
+ end
16
+
17
+ redirect_to article_variants_path(@article)
18
+ end
19
+
20
+ private
21
+
22
+ def load_article
23
+ @article = Postnhost::Article.find(params[:article_id])
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,130 @@
1
+ module Postnhost
2
+ class ArticlesController < ApplicationController
3
+ PAGE_SIZE = 15
4
+
5
+ before_action :authenticate_user!
6
+ before_action :load_article, except: %i[index new]
7
+
8
+ layout :resolve_layout
9
+
10
+ def index
11
+ @status = Postnhost::Article.normalize_status(params[:status])
12
+ @sort = Postnhost::Article.normalize_sort(params[:sort])
13
+
14
+ articles = current_user.articles
15
+ .includes(:language, :article_snapshot)
16
+ .with_status(@status)
17
+ .sorted_by(@sort)
18
+
19
+ @pagy, @articles = pagy(:offset, articles, limit: PAGE_SIZE)
20
+ end
21
+
22
+ def new
23
+ @article = current_user.articles.new
24
+ @article.language = Postnhost::Language.blog_default
25
+ @article.save!
26
+ redirect_to edit_article_path(@article)
27
+ end
28
+
29
+ def edit
30
+ @categories = Postnhost::Category.all
31
+ @users = Postnhost::User.order(:name, :email)
32
+ @languages = Postnhost::Language.order(default: :desc, name: :asc)
33
+ @articles = current_user.articles.where.not(id: @article.id).order(:title)
34
+ end
35
+
36
+ def update
37
+ if @article.update(article_params)
38
+ @article.sync_publication_schedule!
39
+ respond_to do |format|
40
+ format.html { redirect_to articles_path }
41
+ format.json { head :no_content }
42
+ format.turbo_stream
43
+ end
44
+ else
45
+ @categories = Postnhost::Category.all
46
+ @users = Postnhost::User.order(:name, :email)
47
+ @languages = Postnhost::Language.order(default: :desc, name: :asc)
48
+ @articles = current_user.articles.where.not(id: @article.id).order(:title)
49
+ render :edit, status: :unprocessable_content
50
+ end
51
+ end
52
+
53
+ def publish
54
+ result = Postnhost::Publishing::Articles::Publish.call(article: @article)
55
+ @article.reload
56
+ respond_to do |format|
57
+ format.html do
58
+ redirect_to edit_article_path(@article),
59
+ **publication_flash(result, success: "Article has been published.")
60
+ end
61
+ format.turbo_stream do
62
+ flash.now[result.success? ? :notice : :alert] = result.success? ? "Article has been published." : result.errors.to_sentence
63
+ render :publish, status: result.status
64
+ end
65
+ end
66
+ end
67
+
68
+ def unpublish
69
+ result = Postnhost::Publishing::Articles::Unpublish.call(article: @article)
70
+ @article.reload
71
+ respond_to do |format|
72
+ format.html do
73
+ redirect_to edit_article_path(@article),
74
+ **publication_flash(result, success: "Article has been unpublished.")
75
+ end
76
+ format.turbo_stream do
77
+ flash.now[result.success? ? :notice : :alert] = result.success? ? "Article has been unpublished." : result.errors.to_sentence
78
+ render :unpublish, status: result.status
79
+ end
80
+ end
81
+ end
82
+
83
+ def rollback
84
+ version = @article.versions.find_by(id: params[:version_id])
85
+ result = version && Postnhost::Publishing::Versions::RestoreDraft.call(record: @article, version:)
86
+ if result&.success?
87
+ redirect_to edit_article_path(@article), notice: "Article has been rolled back to the selected version."
88
+ else
89
+ redirect_to edit_article_path(@article), alert: result&.errors&.to_sentence || "Version was not found."
90
+ end
91
+ end
92
+
93
+ def versions
94
+ publication_version_id = @article.article_snapshot&.paper_trail_version_id
95
+ @versions = @article.versions.includes(:item).sort_by do |version|
96
+ [version.id == publication_version_id ? 0 : 1, -version.created_at.to_i]
97
+ end
98
+ end
99
+
100
+ def version_preview
101
+ @version = @article.versions.find(params[:version_id])
102
+ @versioned_article = @version.reify
103
+ end
104
+
105
+ def destroy
106
+ @article.destroy
107
+ redirect_to articles_url
108
+ end
109
+
110
+ private
111
+
112
+ def load_article
113
+ @article = Postnhost::Article.find(params[:id])
114
+ end
115
+
116
+ def article_params
117
+ params.require(:article).permit(:title, :title_tag, :og_title, :schema_headline, :schema_article_type, :slug, :meta_description,
118
+ :custom_excerpt, :use_excerpt_as_meta_description, :content, :language_id, :cover_image_alt,
119
+ :scheduled_at, :top_pick, author_ids: [], category_ids: [], suggested_article_ids: [])
120
+ end
121
+
122
+ def resolve_layout
123
+ %w[index versions].include?(action_name) ? "postnhost/product" : "postnhost/articles"
124
+ end
125
+
126
+ def publication_flash(result, success:)
127
+ result.success? ? { notice: success } : { alert: result.errors.to_sentence }
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,24 @@
1
+ module Postnhost
2
+ class Categories::TranslationsController < ApplicationController
3
+ before_action :authenticate_user!
4
+ before_action :load_category
5
+
6
+ def create
7
+ language_ids = params[:language_ids] || []
8
+
9
+ language_ids.each do |language_id|
10
+ Postnhost::Translation::CategoryVariantJob.perform_later(@category.id, language_id.to_i)
11
+
12
+ @category.category_variants.create!(language: Postnhost::Language.find_by(id: language_id.to_i), generating: true)
13
+ end
14
+
15
+ redirect_to category_variants_path(@category)
16
+ end
17
+
18
+ private
19
+
20
+ def load_category
21
+ @category = Postnhost::Category.find(params[:category_id])
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,59 @@
1
+ module Postnhost
2
+ class CategoriesController < ApplicationController
3
+ PAGE_SIZE = 15
4
+
5
+ before_action :authenticate_user!
6
+ before_action :load_category, only: %i[show edit update destroy]
7
+
8
+ layout "postnhost/product"
9
+
10
+ def index
11
+ @categories = Postnhost::Category.order(updated_at: :desc)
12
+ end
13
+
14
+ def show
15
+ articles = @category.articles.includes(:article_snapshot, :language).order(created_at: :desc)
16
+ @pagy, @articles = pagy(:offset, articles, limit: PAGE_SIZE)
17
+ end
18
+
19
+ def new
20
+ @category = Postnhost::Category.new
21
+ end
22
+
23
+ def edit; end
24
+
25
+ def create
26
+ @category = Postnhost::Category.new
27
+ @category.assign_attributes(category_params)
28
+
29
+ if @category.save
30
+ redirect_to categories_url
31
+ else
32
+ render :new, status: :unprocessable_content
33
+ end
34
+ end
35
+
36
+ def update
37
+ if @category.update(category_params)
38
+ redirect_to @category, notice: "Category was successfully updated."
39
+ else
40
+ render :edit, status: :unprocessable_content
41
+ end
42
+ end
43
+
44
+ def destroy
45
+ @category.destroy
46
+ redirect_to categories_url, notice: "Category was successfully deleted."
47
+ end
48
+
49
+ private
50
+
51
+ def load_category
52
+ @category = Postnhost::Category.find(params[:id])
53
+ end
54
+
55
+ def category_params
56
+ params.require(:category).permit(:name, :slug, :meta_description)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,69 @@
1
+ module Postnhost
2
+ class CategoryVariantsController < ApplicationController
3
+ before_action :authenticate_user!
4
+ before_action :load_category
5
+ before_action :load_category_variant, only: %i[edit update destroy]
6
+ before_action :ensure_variant_editable, only: %i[edit update]
7
+
8
+ layout "postnhost/product"
9
+
10
+ def index
11
+ @variants = @category.category_variants.includes(:language).order(:created_at)
12
+ @available_languages = Postnhost::Language.where.not(id: @category.category_variants.select(:language_id))
13
+
14
+ default_language = Postnhost::Language.blog_default
15
+ @available_languages = @available_languages.where.not(id: default_language.id) if default_language.present?
16
+ end
17
+
18
+ def new
19
+ @category_variant = @category.category_variants.new
20
+ @category_variant.language = Postnhost::Language.find_by(id: params[:language_id]) if params[:language_id]
21
+ end
22
+
23
+ def edit; end
24
+
25
+ def create
26
+ @category_variant = @category.category_variants.new(variant_params)
27
+
28
+ if @category_variant.save
29
+ redirect_to category_variants_path(@category), notice: "Translation was successfully created."
30
+ else
31
+ render :new, status: :unprocessable_content
32
+ end
33
+ end
34
+
35
+ def update
36
+ if @category_variant.update(variant_params)
37
+ redirect_to category_variants_path(@category), notice: "Translation was successfully updated."
38
+ else
39
+ render :edit, status: :unprocessable_content
40
+ end
41
+ end
42
+
43
+ def destroy
44
+ @category_variant.destroy
45
+ redirect_to category_variants_path(@category), notice: "Translation was successfully deleted."
46
+ end
47
+
48
+ private
49
+
50
+ def load_category
51
+ @category = Postnhost::Category.find(params[:category_id])
52
+ end
53
+
54
+ def load_category_variant
55
+ @category_variant = @category.category_variants.find(params[:id])
56
+ end
57
+
58
+ def ensure_variant_editable
59
+ return unless @category_variant.generating?
60
+
61
+ redirect_to category_variants_path(@category),
62
+ alert: "This translation is still in progress. You can edit it when it finishes."
63
+ end
64
+
65
+ def variant_params
66
+ params.require(:category_variant).permit(:name, :meta_description, :language_id)
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,66 @@
1
+ module Postnhost
2
+ class LanguagesController < ApplicationController
3
+ PAGE_SIZE = 15
4
+
5
+ before_action :authenticate_user!
6
+ before_action :load_language, only: %i[show edit update destroy]
7
+ layout "postnhost/product"
8
+
9
+ def index
10
+ @languages = Postnhost::Language.order(updated_at: :desc)
11
+ end
12
+
13
+ def show
14
+ articles = @language.articles.includes(:article_snapshot, :categories).order(created_at: :desc)
15
+ article_variants = @language.article_variants.includes(:article).order(created_at: :desc)
16
+
17
+ @articles_pagy, @articles = pagy(:offset, articles, limit: PAGE_SIZE, page_key: "articles_page")
18
+ @article_variants_pagy, @article_variants = pagy(
19
+ :offset,
20
+ article_variants,
21
+ limit: PAGE_SIZE,
22
+ page_key: "article_variants_page"
23
+ )
24
+ end
25
+
26
+ def new
27
+ @language = Postnhost::Language.new
28
+ end
29
+
30
+ def edit; end
31
+
32
+ def create
33
+ @language = Postnhost::Language.new
34
+ @language.assign_attributes(language_params)
35
+
36
+ if @language.save
37
+ redirect_to @language
38
+ else
39
+ render :new, status: :unprocessable_content
40
+ end
41
+ end
42
+
43
+ def update
44
+ if @language.update(language_params)
45
+ redirect_to languages_url, notice: "Language was successfully updated."
46
+ else
47
+ render :edit, status: :unprocessable_content
48
+ end
49
+ end
50
+
51
+ def destroy
52
+ @language.destroy
53
+ redirect_to languages_url, notice: "Language was successfully deleted."
54
+ end
55
+
56
+ private
57
+
58
+ def load_language
59
+ @language = Postnhost::Language.find(params[:id])
60
+ end
61
+
62
+ def language_params
63
+ params.require(:language).permit(:name, :html_lang, :default)
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,70 @@
1
+ module Postnhost
2
+ class OnboardingController < ApplicationController
3
+ include Postnhost::Onboarding::AccessControl
4
+ include Postnhost::Onboarding::StepAssignment
5
+
6
+ layout "postnhost/application"
7
+
8
+ before_action :ensure_onboarding_allowed!
9
+ before_action :synchronize_onboarding_session
10
+
11
+ def show
12
+ @post_install_checklist = post_install_checklist_mode?
13
+ return if @post_install_checklist
14
+
15
+ @step = current_step
16
+ load_step_assignments
17
+ end
18
+
19
+ def update
20
+ handle_step_result(Postnhost::Onboarding::StepProcessor.call(params:, session:))
21
+ end
22
+
23
+ private
24
+
25
+ def handle_step_result(result)
26
+ payload = result.value || {}
27
+
28
+ case result.status
29
+ when :redirect_onboarding then redirect_to_onboarding(payload[:alert])
30
+ when :redirect_new_session then redirect_to postnhost.new_session_path, alert: payload[:alert]
31
+ when :render_show then render_step(payload:, result:)
32
+ when :finish_setup then finish_setup(payload.fetch(:user))
33
+ when :redirect_articles then redirect_to postnhost.articles_path
34
+ else redirect_to_onboarding("Invalid onboarding response.")
35
+ end
36
+ end
37
+
38
+ def redirect_to_onboarding(alert = nil)
39
+ if alert.present?
40
+ redirect_to postnhost.onboarding_path, alert:
41
+ else
42
+ redirect_to postnhost.onboarding_path
43
+ end
44
+ end
45
+
46
+ def render_step(payload:, result:)
47
+ assign_step_payload(payload)
48
+ flash.now[:alert] = payload[:alert] || result.errors.first if payload[:alert].present? || result.errors.first.present?
49
+ render :show, status: :unprocessable_content
50
+ end
51
+
52
+ def assign_step_payload(payload)
53
+ @step = payload[:step] if payload.key?(:step)
54
+ @user = payload[:user] if payload.key?(:user)
55
+ @language_options = payload[:language_options] if payload.key?(:language_options)
56
+ @selected_language_locales = payload[:selected_language_locales] if payload.key?(:selected_language_locales)
57
+ @selected_default_locale = payload[:selected_default_locale] if payload.key?(:selected_default_locale)
58
+ @locale = payload[:locale] if payload.key?(:locale)
59
+ @site_url = payload[:site_url] if payload.key?(:site_url)
60
+ @site_indexing = payload[:site_indexing] if payload.key?(:site_indexing)
61
+ @site_copy = payload[:site_copy] if payload.key?(:site_copy)
62
+ end
63
+
64
+ def finish_setup(user)
65
+ sign_in(user)
66
+ session[:onboarding_post_install_checklist] = true
67
+ redirect_to postnhost.onboarding_path(post_install: "1")
68
+ end
69
+ end
70
+ end