postnhost 0.0.0 → 0.1.1

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 (500) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/{LICENSE.txt → LICENSE} +6 -6
  4. data/README.md +343 -15
  5. data/Rakefile +66 -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 +347 -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 +236 -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 +55 -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 +100 -0
  476. data/lib/generators/postnhost/tailwindcss/templates/host.tailwind.css +4 -0
  477. data/lib/generators/postnhost/user/user_generator.rb +36 -0
  478. data/lib/generators/postnhost/views/views_generator.rb +140 -0
  479. data/lib/postnhost/configuration.rb +64 -0
  480. data/lib/postnhost/css_build.rb +68 -0
  481. data/lib/postnhost/css_scope.rb +77 -0
  482. data/lib/postnhost/engine.rb +57 -0
  483. data/lib/postnhost/sample_data.rb +865 -0
  484. data/lib/postnhost/settings/i18n_overrides.rb +137 -0
  485. data/lib/postnhost/settings/i18n_patch.rb +18 -0
  486. data/lib/postnhost/tailwind_compiler.rb +75 -0
  487. data/lib/postnhost/tailwind_input.rb +32 -0
  488. data/lib/postnhost/version.rb +1 -3
  489. data/lib/postnhost.rb +26 -3
  490. data/lib/tasks/postnhost_snapshots.rake +77 -0
  491. data/lib/tasks/postnhost_tasks.rake +71 -0
  492. metadata +679 -21
  493. data/.github/workflows/main.yml +0 -27
  494. data/.gitignore +0 -11
  495. data/.rspec +0 -3
  496. data/.rubocop.yml +0 -8
  497. data/Gemfile +0 -13
  498. data/bin/console +0 -11
  499. data/bin/setup +0 -8
  500. 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,55 @@
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 the 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 your app:"
36
+ say " #{DEFAULT_MOUNT_PATH}/onboarding"
37
+ say " Or create a CMS user interactively: bin/rails g postnhost:user"
38
+ say ""
39
+ say " 3. Review config/initializers/postnhost.rb for optional defaults, production uploads, and AI translations."
40
+ say ""
41
+ say " 4. (Optional) Customize public views:"
42
+ say " bin/rails g postnhost:views --views-scope=minimal"
43
+ say " or"
44
+ say " bin/rails g postnhost:views --views-scope=full"
45
+ say ""
46
+ say " 5. (Optional) Enable host Tailwind support for custom PostnHost classes:"
47
+ say " bin/rails g postnhost:tailwindcss:install"
48
+ say ""
49
+ say " 6. (Optional) Add a new public locale (copy English strings into your app):"
50
+ say " bin/rails g postnhost:locale sv"
51
+ say ""
52
+ end
53
+ end
54
+ end
55
+ 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 the 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. bin/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,100 @@
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 support for PostnHost view overrides."
11
+
12
+ def create_stylesheet_directory
13
+ empty_directory "app/assets/stylesheets/postnhost"
14
+ end
15
+
16
+ def create_host_stylesheet
17
+ path = Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css")
18
+ return if path.exist?
19
+
20
+ copy_file "host.tailwind.css", path
21
+ end
22
+
23
+ def update_package_json
24
+ path = Rails.root.join("package.json")
25
+ return say_package_json_instructions unless path.exist?
26
+
27
+ @package_json_present = true
28
+
29
+ json_data = JSON.parse(File.read(path))
30
+ json_data["scripts"] ||= {}
31
+ json_data["devDependencies"] ||= {}
32
+
33
+ json_data["scripts"]["postnhost:tailwindcss"] = "bundle exec rails postnhost:tailwindcss:build"
34
+ json_data["scripts"]["postnhost:tailwindcss:watch"] = "bundle exec rails postnhost:tailwindcss:watch"
35
+
36
+ ensure_dependency(json_data, "tailwindcss", "^4.1.12")
37
+ ensure_dependency(json_data, "@tailwindcss/cli", "^4.1.12")
38
+ ensure_dependency(json_data, "@tailwindcss/typography", "^0.5.16")
39
+
40
+ File.write(path, "#{JSON.pretty_generate(json_data)}\n")
41
+
42
+ say "Updated package.json with PostnHost Tailwind scripts and dependencies.", :green
43
+ end
44
+
45
+ def wire_procfile_dev
46
+ path = Rails.root.join("Procfile.dev")
47
+ entry = "postnhost_css: bundle exec rails postnhost:tailwindcss:watch"
48
+
49
+ if path.exist?
50
+ if File.read(path).include?("postnhost:tailwindcss:watch")
51
+ say "Procfile.dev already includes the PostnHost watcher.", :green
52
+ else
53
+ append_to_file path, "\n#{entry}\n"
54
+ say "Added the PostnHost watcher to Procfile.dev.", :green
55
+ end
56
+ else
57
+ create_file "Procfile.dev", "#{entry}\n"
58
+ say "Created Procfile.dev with the PostnHost watcher.", :green
59
+ end
60
+ end
61
+
62
+ def print_next_steps
63
+ say ""
64
+ say "PostnHost host Tailwind support installed.", :green
65
+ say "Next steps:"
66
+ step = 1
67
+
68
+ if @package_json_present
69
+ say " #{step}. Install dependencies with the JavaScript package manager used by this app."
70
+ step += 1
71
+ end
72
+
73
+ if Rails.root.join("bin/dev").exist?
74
+ say " #{step}. Run bin/dev; the PostnHost watcher is wired in Procfile.dev."
75
+ else
76
+ say " #{step}. Run bin/rails postnhost:tailwindcss:watch alongside your Rails server."
77
+ end
78
+
79
+ say " #{step + 1}. Use normal Tailwind classes in copied PostnHost views."
80
+ say " #{step + 2}. Keep the default layout tags; the combined build overrides postnhost/application automatically."
81
+ say ""
82
+ end
83
+
84
+ private
85
+
86
+ def ensure_dependency(json_data, package_name, version)
87
+ return if json_data["dependencies"]&.key?(package_name) || json_data["devDependencies"].key?(package_name)
88
+
89
+ json_data["devDependencies"][package_name] = version
90
+ end
91
+
92
+ def say_package_json_instructions
93
+ say "package.json not found. Add a Tailwind CSS 4 compiler to the host Gemfile:", :yellow
94
+ say ' gem "tailwindcss-ruby", "~> 4.0"'
95
+ say "Then run bundle install."
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,4 @@
1
+ @source "../../../views/postnhost/**/*.erb";
2
+ @source "../../../views/layouts/postnhost/**/*.erb";
3
+ @source "../../../helpers/**/*.rb";
4
+ @source "../../../javascript/**/*.js";
@@ -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,140 @@
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 views_scope == "full" ? "All public templates and layouts were copied." : "The minimal public view set was copied.", :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 Tailwind support:", :yellow
127
+ say " bin/rails g postnhost:tailwindcss:install"
128
+ say ""
129
+ end
130
+
131
+ def host_tailwind_enabled?
132
+ Rails.root.join("app/assets/stylesheets/postnhost/host.tailwind.css").exist?
133
+ end
134
+
135
+ def source_paths
136
+ [engine_views_path.to_s]
137
+ end
138
+ end
139
+ end
140
+ 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,68 @@
1
+ require "English"
2
+ require "fileutils"
3
+
4
+ require_relative "css_scope"
5
+
6
+ module Postnhost
7
+ class CssBuild
8
+ POLL_INTERVAL = 0.1
9
+
10
+ class << self
11
+ def run(command:, unscoped_path:, output_path:, watch: false)
12
+ FileUtils.rm_f(unscoped_path)
13
+
14
+ if watch
15
+ watch(command:, unscoped_path:, output_path:)
16
+ else
17
+ system(*command, exception: true)
18
+ CssScope.scope_file(unscoped_path, output_path)
19
+ end
20
+ ensure
21
+ FileUtils.rm_f(unscoped_path)
22
+ end
23
+
24
+ private
25
+
26
+ def watch(command:, unscoped_path:, output_path:)
27
+ process_id = Process.spawn(*command)
28
+ applied_signature = nil
29
+ pending_signature = nil
30
+
31
+ loop do
32
+ process_status = Process.waitpid(process_id, Process::WNOHANG)
33
+ signature = file_signature(unscoped_path)
34
+
35
+ if signature && signature == pending_signature && signature != applied_signature
36
+ CssScope.scope_file(unscoped_path, output_path)
37
+ applied_signature = signature
38
+ end
39
+
40
+ pending_signature = signature
41
+ break if process_status
42
+
43
+ sleep POLL_INTERVAL
44
+ end
45
+
46
+ raise "Tailwind CSS watcher exited unsuccessfully" unless $CHILD_STATUS.success?
47
+ ensure
48
+ terminate(process_id)
49
+ end
50
+
51
+ def file_signature(path)
52
+ return unless File.exist?(path)
53
+
54
+ stat = File.stat(path)
55
+ [stat.mtime.to_f, stat.size]
56
+ end
57
+
58
+ def terminate(process_id)
59
+ return unless process_id
60
+
61
+ Process.kill("TERM", process_id)
62
+ Process.wait(process_id)
63
+ rescue Errno::ESRCH, Errno::ECHILD
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end