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,11 @@
1
+ class CreateSiteScripts < ActiveRecord::Migration[7.2]
2
+ def change
3
+ create_table :postnhost_site_scripts do |t|
4
+ t.references :setting, null: false, foreign_key: { to_table: :postnhost_settings }
5
+ t.string :placement, null: false
6
+ t.text :script
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,56 @@
1
+ module Postnhost
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ DEFAULT_MOUNT_PATH = "/blog"
5
+
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Creates Postnhost initializer and mounts the engine"
9
+
10
+ def create_initializer
11
+ template "postnhost.rb", "config/initializers/postnhost.rb"
12
+ end
13
+
14
+ def create_carrierwave_initializer
15
+ return if File.exist?("config/initializers/carrierwave.rb")
16
+
17
+ template "carrierwave.rb", "config/initializers/carrierwave.rb"
18
+ end
19
+
20
+ def mount_engine
21
+ route "mount Postnhost::Engine, at: \"#{DEFAULT_MOUNT_PATH}\""
22
+ end
23
+
24
+ def show_post_install
25
+ say ""
26
+ say "=" * 60
27
+ say "Postnhost has been installed!", :green
28
+ say "=" * 60
29
+ say ""
30
+ say "Next steps:"
31
+ say " 1. Run migrations:"
32
+ say " bin/rails postnhost:install:migrations"
33
+ say " bin/rails db:migrate"
34
+ say ""
35
+ say " 2. Open first-time setup in the browser:"
36
+ say " http://localhost:3000#{DEFAULT_MOUNT_PATH}/onboarding"
37
+ say " Or create a CMS user interactively: bin/rails g postnhost:user"
38
+ say ""
39
+ say " 3. Update config/initializers/postnhost.rb with your site settings."
40
+ say ""
41
+ say " 4. (Optional) Customize public views:"
42
+ say " rails g postnhost:views --views-scope=minimal"
43
+ say " # or"
44
+ say " rails g postnhost:views --views-scope=full"
45
+ say ""
46
+ say " 5. (Optional) Enable host Tailwind pipeline for custom Postnhost classes:"
47
+ say " rails g postnhost:tailwindcss:install"
48
+ say " bundle exec rails postnhost:tailwindcss:watch"
49
+ say ""
50
+ say " 6. (Optional) Add a new public locale (copy English strings into your app):"
51
+ say " rails g postnhost:locale sv"
52
+ say ""
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,17 @@
1
+ CarrierWave.configure do |config|
2
+ if Rails.env.production?
3
+ config.storage = :aws
4
+ config.aws_bucket = Postnhost.config.aws_bucket
5
+ config.aws_acl = "public-read"
6
+
7
+ config.aws_credentials = {
8
+ access_key_id: Postnhost.config.aws_access_key_id,
9
+ secret_access_key: Postnhost.config.aws_secret_access_key,
10
+ region: Postnhost.config.aws_region,
11
+ endpoint: Postnhost.config.aws_endpoint_url_s3,
12
+ stub_responses: Rails.env.test?
13
+ }
14
+ else
15
+ config.storage = :file
16
+ end
17
+ end
@@ -0,0 +1,36 @@
1
+ Postnhost.configure do |config|
2
+ # ==> Site defaults
3
+ # Used when the matching Dashboard → Settings → Site field is blank.
4
+ # config.site_url = "https://example.com"
5
+ # config.public_page_size = 12
6
+
7
+ # ==> Integrations
8
+ # Default admin timezone used for scheduling and timestamp display
9
+ # config.default_timezone = 'UTC'
10
+
11
+ # OpenAI API key for AI translations.
12
+ # Explicit config value takes priority over credentials fallback.
13
+ # config.openai_api_key = "sk-..."
14
+ # config.openai_gpt_model = "gpt-5.6-luna"
15
+
16
+ # ==> AWS/S3 (CarrierWave uploads)
17
+ # Explicit config values take priority over credentials fallback:
18
+ # config.aws_access_key_id = "AKIA..."
19
+ # config.aws_secret_access_key = "..."
20
+ # config.aws_region = "us-east-1"
21
+ # config.aws_bucket = "my-bucket"
22
+ # config.aws_endpoint_url_s3 = "https://fly.storage.tigris.dev"
23
+ #
24
+ # Encrypted credentials (alternative to explicit settings):
25
+ # postnhost:
26
+ # openai_access_token: sk-...
27
+ # openai_gpt_model: gpt-5.6-luna
28
+ # aws_access_key_id: AKIA...
29
+ # aws_secret_access_key: ...
30
+ # aws_region: us-east-1
31
+ # aws_bucket_name: my-bucket
32
+ # aws_endpoint_url_s3: https://fly.storage.tigris.dev
33
+ #
34
+ # Litestream (SQLite backups) is not configured here — use host app
35
+ # config/initializers/litestream.rb + credentials (see engine README).
36
+ end
@@ -0,0 +1,36 @@
1
+ require "yaml"
2
+
3
+ module Postnhost
4
+ module Generators
5
+ class LocaleGenerator < Rails::Generators::Base
6
+ argument :locale, type: :string, desc: "Locale code to match Language#html_lang (e.g. sv, pt-BR)"
7
+
8
+ desc "Copies Postnhost English locale (en.yml) to config/locales/<locale>.yml in your app for translation."
9
+
10
+ def copy_postnhost_locale
11
+ code = locale.to_s.strip
12
+ raise Thor::Error, "Locale cannot be blank." if code.blank?
13
+ raise Thor::Error, "Invalid locale #{code.inspect} (use letters, numbers, hyphen, underscore, dot)." unless code.match?(/\A[a-zA-Z0-9_.-]+\z/)
14
+
15
+ destination = File.join("config/locales", "#{code}.yml")
16
+ if File.exist?(destination)
17
+ say_status :skip, "#{destination} already exists", :yellow
18
+ return
19
+ end
20
+
21
+ source_path = Postnhost::Engine.root.join("config/locales/en.yml")
22
+ en_tree = YAML.load_file(source_path)["en"]
23
+ raise Thor::Error, "Could not find `en` root key in #{source_path}" unless en_tree
24
+
25
+ header = <<~TXT
26
+ # Postnhost public UI translations for "#{code}".
27
+ # Generated by: bin/rails g postnhost:locale #{code}
28
+ # Translate values under postnhost.public, then add "#{code}" to config.i18n.available_locales if needed.
29
+
30
+ TXT
31
+
32
+ create_file destination, header + YAML.dump({ code => en_tree })
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,47 @@
1
+ module Postnhost
2
+ module Generators
3
+ class StaticPagesGenerator < Rails::Generators::Base
4
+ argument :slugs, type: :array, default: [], banner: "slug [slug ...]"
5
+
6
+ desc "Creates static page templates in app/views/postnhost/static_pages"
7
+
8
+ def create_static_pages
9
+ if slugs.empty?
10
+ say("Please provide at least one page slug, e.g. rails g postnhost:static_pages terms privacy", :red)
11
+ return
12
+ end
13
+
14
+ slugs.each do |raw_slug|
15
+ slug = raw_slug.to_s.strip
16
+ unless valid_slug?(slug)
17
+ say("Skipping invalid slug '#{raw_slug}'. Use lowercase letters, numbers, underscores, or hyphens.", :yellow)
18
+ next
19
+ end
20
+
21
+ title = slug.tr("_-", " ").split.map(&:capitalize).join(" ")
22
+ create_file "app/views/postnhost/static_pages/#{slug}.html.erb", static_page_template(slug, title)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def valid_slug?(slug)
29
+ slug.match?(/\A[a-z0-9_-]+\z/)
30
+ end
31
+
32
+ def static_page_template(slug, title)
33
+ <<~ERB
34
+ <% content_for :title, "#{title}" %>
35
+ <% content_for :meta_description, "#{title} page" %>
36
+
37
+ <div class="max-w-4xl mx-auto px-4 py-12">
38
+ <h1 class="text-3xl font-bold text-gray-900 mb-4">#{title}</h1>
39
+ <p class="text-gray-700">
40
+ Replace this template content in <code>app/views/postnhost/static_pages/#{slug}.html.erb</code>.
41
+ </p>
42
+ </div>
43
+ ERB
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,97 @@
1
+ require "json"
2
+
3
+ module Postnhost
4
+ module Generators
5
+ module Tailwindcss
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path("templates", __dir__)
8
+
9
+ namespace "postnhost:tailwindcss:install"
10
+ desc "Install host Tailwind pipeline for Postnhost view overrides."
11
+
12
+ def create_stylesheet_directory
13
+ empty_directory "app/assets/stylesheets/postnhost"
14
+ end
15
+
16
+ def create_tailwind_config
17
+ path = Rails.root.join("postnhost.tailwind.config.js")
18
+ return if path.exist?
19
+
20
+ copy_file "tailwind.config.js", path
21
+ end
22
+
23
+ def create_host_stylesheet
24
+ path = Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css")
25
+ return if path.exist?
26
+
27
+ copy_file "host.tailwind.css", path
28
+ end
29
+
30
+ def update_package_json
31
+ path = Rails.root.join("package.json")
32
+ return say_package_json_instructions unless path.exist?
33
+
34
+ json_data = JSON.parse(File.read(path))
35
+ json_data["scripts"] ||= {}
36
+ json_data["devDependencies"] ||= {}
37
+
38
+ json_data["scripts"]["postnhost:tailwindcss"] = tailwind_build_command
39
+ json_data["scripts"]["postnhost:tailwindcss:watch"] = "#{tailwind_build_command} --watch"
40
+
41
+ ensure_dependency(json_data, "tailwindcss", "^4.1.12")
42
+ ensure_dependency(json_data, "@tailwindcss/cli", "^4.1.12")
43
+ ensure_dependency(json_data, "@tailwindcss/typography", "^0.5.16")
44
+
45
+ File.write(path, "#{JSON.pretty_generate(json_data)}\n")
46
+
47
+ say "Updated package.json script:", :green
48
+ say " yarn postnhost:tailwindcss:watch"
49
+ end
50
+
51
+ def wire_procfile_dev
52
+ path = Rails.root.join("Procfile.dev")
53
+ entry = "postnhost_css: bundle exec rails postnhost:tailwindcss:watch"
54
+
55
+ if path.exist?
56
+ if File.read(path).include?("postnhost:tailwindcss:watch")
57
+ say "Procfile.dev already includes Postnhost watcher.", :green
58
+ else
59
+ append_to_file path, "\n#{entry}\n"
60
+ say "Added Postnhost watcher to Procfile.dev.", :green
61
+ end
62
+ else
63
+ create_file "Procfile.dev", "#{entry}\n"
64
+ say "Created Procfile.dev with Postnhost watcher.", :green
65
+ end
66
+ end
67
+
68
+ def print_next_steps
69
+ say ""
70
+ say "Postnhost host-tailwind mode installed.", :green
71
+ say "Next steps:"
72
+ say " 1. Run bin/dev (Postnhost watcher is wired in Procfile.dev)."
73
+ say " 2. Keep default layout tags; Postnhost auto-loads postnhost/host when config is present."
74
+ say ""
75
+ end
76
+
77
+ private
78
+
79
+ def tailwind_build_command
80
+ "./node_modules/.bin/tailwindcss -i ./app/assets/stylesheets/postnhost/host.tailwind.css -o ./app/assets/builds/postnhost/host.css -c ./postnhost.tailwind.config.js --minify"
81
+ end
82
+
83
+ def ensure_dependency(json_data, package_name, version)
84
+ return if json_data["dependencies"]&.key?(package_name) || json_data["devDependencies"].key?(package_name)
85
+
86
+ json_data["devDependencies"][package_name] = version
87
+ end
88
+
89
+ def say_package_json_instructions
90
+ say "package.json not found. Add these scripts manually:", :yellow
91
+ say " \"postnhost:tailwindcss\": \"#{tailwind_build_command}\""
92
+ say " \"postnhost:tailwindcss:watch\": \"#{tailwind_build_command} --watch\""
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,7 @@
1
+ @import "tailwindcss";
2
+ @plugin "@tailwindcss/typography";
3
+
4
+ @source "../../../views/postnhost/**/*.erb";
5
+ @source "../../../views/layouts/postnhost/**/*.erb";
6
+ @source "../../../helpers/**/*.rb";
7
+ @source "../../../javascript/**/*.js";
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ content: [
3
+ "./app/views/postnhost/**/*.erb",
4
+ "./app/views/layouts/postnhost/**/*.erb",
5
+ "./app/helpers/**/*.rb",
6
+ "./app/javascript/**/*.js"
7
+ ]
8
+ }
@@ -0,0 +1,36 @@
1
+ module Postnhost
2
+ module Generators
3
+ class UserGenerator < Rails::Generators::Base
4
+ desc "Interactively creates a Postnhost CMS user"
5
+
6
+ def create_user
7
+ attributes = prompt_for_user_attributes
8
+ raise Thor::Error, "Passwords do not match." unless matching_passwords?(attributes)
9
+
10
+ user = Postnhost::User.new(attributes)
11
+ raise Thor::Error, user.errors.full_messages.to_sentence unless user.save
12
+
13
+ say("Created CMS user #{user.email}.", :green)
14
+ end
15
+
16
+ private
17
+
18
+ def prompt_for_user_attributes
19
+ {
20
+ name: ask("Name:"),
21
+ email: ask("Email:"),
22
+ password: ask_for_secret("Password:"),
23
+ password_confirmation: ask_for_secret("Confirm password:")
24
+ }
25
+ end
26
+
27
+ def ask_for_secret(prompt)
28
+ ask(prompt, echo: false).tap { say "" }
29
+ end
30
+
31
+ def matching_passwords?(attributes)
32
+ attributes[:password] == attributes[:password_confirmation]
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,142 @@
1
+ module Postnhost
2
+ module Generators
3
+ class ViewsGenerator < Rails::Generators::Base
4
+ desc "Copies public Postnhost views to your application for customization"
5
+ class_option :views_scope,
6
+ type: :string,
7
+ required: true,
8
+ enum: %w[minimal full],
9
+ desc: "Choose copied view scope: minimal (clean style-focused views) or full (all views and helper partials)."
10
+
11
+ def copy_views
12
+ copy_public_views
13
+ end
14
+
15
+ private
16
+
17
+ def copy_public_views
18
+ say "Copying public views...", :green
19
+ copy_public_views_by_scope
20
+ copy_template_public_views
21
+ copy_public_template_layouts
22
+ copy_shared_content_for_partials
23
+ copy_full_scope_shared_public_partials
24
+ copy_file "layouts/postnhost/_footer.html.erb", "app/views/layouts/postnhost/_footer.html.erb"
25
+ copy_file "layouts/postnhost/_favicon.html.erb", "app/views/layouts/postnhost/_favicon.html.erb"
26
+ copy_file "layouts/postnhost/public_header/_logo.html.erb", "app/views/layouts/postnhost/public_header/_logo.html.erb"
27
+ copy_file "layouts/postnhost/public_header/_language_switcher.html.erb", "app/views/layouts/postnhost/public_header/_language_switcher.html.erb"
28
+ copy_file "layouts/postnhost/public_header/_dashboard_sign_in.html.erb", "app/views/layouts/postnhost/public_header/_dashboard_sign_in.html.erb"
29
+ say ""
30
+ warn_if_host_tailwind_not_installed
31
+ say "Public views copied (#{views_scope})! Full scope includes every public template and layout under app/views/postnhost/public/templates/ and app/views/layouts/postnhost/public/templates/.", :green
32
+ end
33
+
34
+ def engine_views_path
35
+ Postnhost::Engine.root.join("app/views")
36
+ end
37
+
38
+ def copy_public_views_by_scope
39
+ return copy_default_template_public_views if full_scope?
40
+
41
+ copy_minimal_public_views
42
+ end
43
+
44
+ def copy_full_scope_shared_public_partials
45
+ return unless full_scope?
46
+
47
+ copy_file "postnhost/shared/_seo_meta_tags.html.erb", "app/views/postnhost/shared/_seo_meta_tags.html.erb"
48
+ copy_file "postnhost/shared/_structured_data.html.erb", "app/views/postnhost/shared/_structured_data.html.erb"
49
+ end
50
+
51
+ def copy_minimal_public_views
52
+ default_template_public_view_mappings.each do |source_path, destination_path|
53
+ copy_file source_path, File.join("app/views", destination_path)
54
+ end
55
+ end
56
+
57
+ def copy_default_template_public_views
58
+ default_template_public_view_mappings.each do |source_path, destination_path|
59
+ copy_file source_path, File.join("app/views", destination_path)
60
+ end
61
+ end
62
+
63
+ def copy_template_public_views
64
+ return unless full_scope?
65
+
66
+ template_public_view_files.each do |relative_path|
67
+ copy_file relative_path, File.join("app/views", relative_path)
68
+ end
69
+ end
70
+
71
+ def copy_public_template_layouts
72
+ return unless full_scope?
73
+
74
+ public_template_layout_files.each do |relative_path|
75
+ copy_file relative_path, File.join("app/views", relative_path)
76
+ end
77
+ end
78
+
79
+ def copy_shared_content_for_partials
80
+ return unless full_scope?
81
+
82
+ shared_content_for_files.each do |relative_path|
83
+ copy_file relative_path, File.join("app/views", relative_path)
84
+ end
85
+ end
86
+
87
+ def default_template_public_view_mappings
88
+ Dir.glob(engine_views_path.join("postnhost/public/templates/default/**/*.erb")).map do |path|
89
+ source_path = Pathname.new(path).relative_path_from(engine_views_path).to_s
90
+ destination_path = source_path
91
+ [source_path, destination_path]
92
+ end
93
+ end
94
+
95
+ def shared_content_for_files
96
+ Dir.glob(engine_views_path.join("postnhost/public/**/content_for/**/*.erb")).map do |path|
97
+ Pathname.new(path).relative_path_from(engine_views_path).to_s
98
+ end
99
+ end
100
+
101
+ def template_public_view_files
102
+ template_files = Dir.glob(engine_views_path.join("postnhost/public/templates/**/*.erb")).map do |path|
103
+ Pathname.new(path).relative_path_from(engine_views_path).to_s
104
+ end
105
+
106
+ template_files.reject { |path| path.start_with?("postnhost/public/templates/default/") }
107
+ end
108
+
109
+ def public_template_layout_files
110
+ Dir.glob(engine_views_path.join("layouts/postnhost/public/templates/*.erb")).map do |path|
111
+ Pathname.new(path).relative_path_from(engine_views_path).to_s
112
+ end
113
+ end
114
+
115
+ def full_scope?
116
+ views_scope == "full"
117
+ end
118
+
119
+ def views_scope
120
+ options[:views_scope]
121
+ end
122
+
123
+ def warn_if_host_tailwind_not_installed
124
+ return if host_tailwind_enabled?
125
+
126
+ say "Tip: if you add new Tailwind classes in copied views, install host pipeline:", :yellow
127
+ say " rails g postnhost:tailwindcss:install"
128
+ say " bundle exec rails postnhost:tailwindcss:watch"
129
+ say ""
130
+ end
131
+
132
+ def host_tailwind_enabled?
133
+ Rails.root.join("postnhost.tailwind.config.js").exist? &&
134
+ Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css").exist?
135
+ end
136
+
137
+ def source_paths
138
+ [engine_views_path.to_s]
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,64 @@
1
+ require "uri"
2
+
3
+ module Postnhost
4
+ class Configuration
5
+ PUBLIC_PAGE_SIZE_RANGE = (1..100)
6
+
7
+ # Integrations
8
+ attr_accessor :openai_api_key, :openai_gpt_model, :default_timezone
9
+
10
+ # Site defaults
11
+ attr_reader :site_url, :public_page_size
12
+
13
+ # AWS/S3 Storage
14
+ attr_accessor :aws_access_key_id, :aws_secret_access_key,
15
+ :aws_region, :aws_bucket, :aws_endpoint_url_s3
16
+
17
+ def initialize
18
+ # Integrations
19
+ @default_timezone = rails_default_timezone
20
+ @site_url = nil
21
+ @public_page_size = 12
22
+ @openai_api_key = credentials_dig(:postnhost, :openai_access_token)
23
+ @openai_gpt_model = credentials_dig(:postnhost, :openai_gpt_model)
24
+ # AWS/S3 (from Rails credentials)
25
+ @aws_access_key_id = credentials_dig(:postnhost, :aws_access_key_id)
26
+ @aws_secret_access_key = credentials_dig(:postnhost, :aws_secret_access_key)
27
+ @aws_region = credentials_dig(:postnhost, :aws_region)
28
+ @aws_bucket = credentials_dig(:postnhost, :aws_bucket_name)
29
+ @aws_endpoint_url_s3 = credentials_dig(:postnhost, :aws_endpoint_url_s3)
30
+ end
31
+
32
+ def site_url=(value)
33
+ if value.blank?
34
+ @site_url = nil
35
+ return
36
+ end
37
+
38
+ normalized_url = value.to_s.strip.chomp("/")
39
+ uri = URI.parse(normalized_url)
40
+ raise ArgumentError, "site_url must be an HTTP(S) origin without a path, query, or fragment" unless uri.is_a?(URI::HTTP) && uri.host.present? && uri.userinfo.nil? && uri.path.blank? && uri.query.nil? && uri.fragment.nil?
41
+
42
+ @site_url = normalized_url
43
+ rescue URI::InvalidURIError
44
+ raise ArgumentError, "site_url must be a valid URL"
45
+ end
46
+
47
+ def public_page_size=(value)
48
+ page_size = Integer(value, exception: false)
49
+ raise ArgumentError, "public_page_size must be an integer between 1 and 100" unless page_size && PUBLIC_PAGE_SIZE_RANGE.cover?(page_size)
50
+
51
+ @public_page_size = page_size
52
+ end
53
+
54
+ private
55
+
56
+ def credentials_dig(*keys)
57
+ Rails.application.credentials.dig(*keys)
58
+ end
59
+
60
+ def rails_default_timezone
61
+ Rails.application.config.time_zone.presence || "UTC"
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,57 @@
1
+ module Postnhost
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Postnhost
4
+
5
+ # Initialize configuration before other initializers
6
+ initializer "postnhost.configuration", before: :load_config_initializers do
7
+ Postnhost.configure
8
+ end
9
+
10
+ initializer "postnhost.yaml_column_permitted_classes" do |app|
11
+ permitted_classes = app.config.active_record.yaml_column_permitted_classes ||= []
12
+ [
13
+ ActiveSupport::TimeWithZone,
14
+ ActiveSupport::TimeZone,
15
+ Time,
16
+ Date,
17
+ Symbol
18
+ ].each do |klass|
19
+ permitted_classes << klass unless permitted_classes.include?(klass)
20
+ end
21
+ end
22
+
23
+ initializer "postnhost.i18n_patch" do
24
+ I18n.backend.singleton_class.prepend(Postnhost::Settings::I18nPatch) unless I18n.backend.singleton_class.ancestors.include?(Postnhost::Settings::I18nPatch)
25
+ end
26
+
27
+ # Asset pipeline configuration (Sprockets + Propshaft compatible)
28
+ initializer "postnhost.assets" do |app|
29
+ if app.config.respond_to?(:assets)
30
+ # For Propshaft: add builds directory so postnhost/application.css is found
31
+ # The path structure is: builds/postnhost/application.css
32
+ # When requesting "postnhost/application", Propshaft finds it in builds/
33
+ app.config.assets.paths << Engine.root.join("app", "assets", "builds").to_s
34
+ app.config.assets.paths << Engine.root.join("app", "assets", "images").to_s
35
+
36
+ # For Sprockets: programmatically add all assets to precompile
37
+ if defined?(::Sprockets)
38
+ asset_paths = [
39
+ %w[app assets builds postnhost],
40
+ %w[app assets images postnhost]
41
+ ]
42
+
43
+ paths_to_precompile = asset_paths.flat_map do |path|
44
+ Dir[Engine.root.join(*path, "**", "*")].filter_map do |file|
45
+ next unless File.file?(file)
46
+
47
+ Pathname.new(file).relative_path_from(Engine.root.join(*path)).to_s
48
+ end
49
+ end
50
+
51
+ app.config.assets.precompile += paths_to_precompile
52
+ app.config.assets.precompile += %w[postnhost_manifest.js]
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end