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,30 @@
1
+ module Postnhost
2
+ module Translation
3
+ class OpenaiLlmClient
4
+ DEFAULT_MODEL = "gpt-5.6-luna"
5
+ TIMEOUT = 1240
6
+
7
+ def initialize(client: nil)
8
+ @client = client
9
+ end
10
+
11
+ def text_response(prompt, model: Postnhost.config.openai_gpt_model || DEFAULT_MODEL)
12
+ response = client.responses.create(
13
+ model: model,
14
+ input: prompt
15
+ )
16
+
17
+ response.output_text.strip
18
+ end
19
+
20
+ private
21
+
22
+ def client
23
+ @client ||= OpenAI::Client.new(
24
+ api_key: Postnhost.config.openai_api_key,
25
+ timeout: TIMEOUT
26
+ )
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,55 @@
1
+ module Postnhost
2
+ module Translation
3
+ class PageVariantTranslator < BaseService
4
+ def initialize(page_id:, language_id:, llm_client: OpenaiLlmClient.new)
5
+ @page_id = page_id
6
+ @language_id = language_id
7
+ @llm_client = llm_client
8
+ end
9
+
10
+ def call
11
+ load_records
12
+ return failure("Translation source or language not found") unless page && language
13
+ return failure("Published translation source not found") unless source
14
+
15
+ load_variant
16
+ return failure("Translation variant not found") unless variant
17
+
18
+ attributes = ContentTranslator.call(source:, language:, llm_client:)
19
+ return finish_with_failure("Translation failed") if attributes[:title].blank? || attributes[:content].blank?
20
+
21
+ variant.update!(attributes.merge(generating: false))
22
+ success(variant)
23
+ rescue StandardError => e
24
+ mark_generation_finished
25
+ failure(e.message)
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :page_id, :language_id, :llm_client, :page, :language, :variant
31
+
32
+ def load_records
33
+ @page = Postnhost::Page.find_by(id: page_id)
34
+ @language = Postnhost::Language.find_by(id: language_id)
35
+ end
36
+
37
+ def load_variant
38
+ @variant = Postnhost::PageVariant.find_by(page:, language:)
39
+ end
40
+
41
+ def source
42
+ @source ||= page.page_snapshot
43
+ end
44
+
45
+ def finish_with_failure(message)
46
+ mark_generation_finished
47
+ failure(message)
48
+ end
49
+
50
+ def mark_generation_finished
51
+ variant&.update!(generating: false)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ class BaseUploader < CarrierWave::Uploader::Base
2
+ include CarrierWave::Vips
3
+
4
+ def store_dir
5
+ data = "#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
6
+ hash = Digest::SHA256.hexdigest(data)
7
+ "uploads/#{model.class.to_s.underscore}/#{hash[0..20]}"
8
+ end
9
+
10
+ def size_range
11
+ 0..(25.megabytes)
12
+ end
13
+
14
+ def filename
15
+ generated_filename
16
+ end
17
+
18
+ private
19
+
20
+ def generated_filename
21
+ data = "#{model.id}-#{mounted_as}-#{Time.current.to_i}"
22
+ hash = Digest::SHA256.hexdigest(data)
23
+ extension = "webp"
24
+ @generated_filename ||= "#{hash[0..12]}.#{extension}"
25
+ end
26
+ end
@@ -0,0 +1,31 @@
1
+ class CoverImageUploader < BaseUploader
2
+ QUALITY = 90
3
+
4
+ self.remove_previously_stored_files_after_update = false
5
+
6
+ process resize_to_fill: [1200, 630]
7
+ process convert: "webp"
8
+ process :quality
9
+
10
+ version :thumbnail do
11
+ process resize_to_fill: [780, 410]
12
+ process convert: "webp"
13
+ process :quality
14
+ end
15
+
16
+ version :medium do
17
+ process resize_to_fill: [1024, 537]
18
+ process convert: "webp"
19
+ process :quality
20
+ end
21
+
22
+ def quality
23
+ vips! do |v|
24
+ v.saver(Q: QUALITY)
25
+ end
26
+ end
27
+
28
+ def extension_allowlist
29
+ %w[png jpg jpeg gif webp heic]
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ module Postnhost
2
+ class AvatarUploader < ::BaseUploader
3
+ process resize_to_limit: [200, 200]
4
+ process convert: "webp"
5
+ process :quality
6
+
7
+ def quality
8
+ vips! do |v|
9
+ v.saver(Q: 90)
10
+ end
11
+ end
12
+
13
+ def extension_allowlist
14
+ %w[png jpg jpeg gif webp heic]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,38 @@
1
+ module Postnhost
2
+ class InlineArticleImageUploader < ::BaseUploader
3
+ MAX_WIDTH = 1200
4
+ QUALITY = 90
5
+
6
+ process resize_to_limit: [MAX_WIDTH, nil]
7
+ process convert: "webp"
8
+ process :quality
9
+
10
+ def store_dir
11
+ "uploads/articles/#{model.user_id}/#{model.id}/inline_images"
12
+ end
13
+
14
+ def size_range
15
+ 0..(20.megabytes)
16
+ end
17
+
18
+ def extension_allowlist
19
+ %w[png jpg jpeg gif webp heic]
20
+ end
21
+
22
+ def quality
23
+ vips! do |v|
24
+ v.saver(Q: QUALITY)
25
+ end
26
+ end
27
+
28
+ def filename
29
+ generated_filename
30
+ end
31
+
32
+ private
33
+
34
+ def generated_filename
35
+ @generated_filename ||= "#{SecureRandom.hex(12)}.webp"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,38 @@
1
+ module Postnhost
2
+ class InlinePageImageUploader < ::BaseUploader
3
+ MAX_WIDTH = 1200
4
+ QUALITY = 90
5
+
6
+ process resize_to_limit: [MAX_WIDTH, nil]
7
+ process convert: "webp"
8
+ process :quality
9
+
10
+ def store_dir
11
+ "uploads/pages/#{model.user_id}/#{model.id}/inline_images"
12
+ end
13
+
14
+ def size_range
15
+ 0..(20.megabytes)
16
+ end
17
+
18
+ def extension_allowlist
19
+ %w[png jpg jpeg gif webp heic]
20
+ end
21
+
22
+ def quality
23
+ vips! do |v|
24
+ v.saver(Q: QUALITY)
25
+ end
26
+ end
27
+
28
+ def filename
29
+ generated_filename
30
+ end
31
+
32
+ private
33
+
34
+ def generated_filename
35
+ @generated_filename ||= "#{SecureRandom.hex(12)}.webp"
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,18 @@
1
+ module Postnhost
2
+ class SettingAssetUploader < ::BaseUploader
3
+ QUALITY = 100
4
+
5
+ process convert: "webp"
6
+ process :quality
7
+
8
+ def quality
9
+ vips! do |v|
10
+ v.saver(Q: QUALITY)
11
+ end
12
+ end
13
+
14
+ def extension_allowlist
15
+ %w[png jpg jpeg gif webp heic]
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,6 @@
1
+ <!-- Favicon -->
2
+ <link rel="icon" href="/favicon.ico" sizes="32x32">
3
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
4
+ <link rel="apple-touch-icon" href="/apple-touch-icon.png">
5
+ <link rel="manifest" href="/site.webmanifest">
6
+ <meta name="theme-color" content="#ffffff">
@@ -0,0 +1,67 @@
1
+ <footer aria-labelledby="footer-heading" class="bg-white border-t border-gray-200">
2
+ <h2 id="footer-heading" class="sr-only"><%= t('postnhost.public.footer.heading') %></h2>
3
+
4
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
5
+ <%# Categories %>
6
+ <% if @use_auto_footer_navigation && @footer_categories.any? %>
7
+ <div class="text-center pb-6">
8
+ <p class="text-sm text-gray-500 mb-4"><%= t('postnhost.public.footer.explore_categories') %></p>
9
+ <div class="flex flex-wrap justify-center gap-3">
10
+ <% @footer_categories.each do |category| %>
11
+ <%= link_to localized_category_name(category, @current_language),
12
+ language_category_path(category.slug, @current_language),
13
+ class: "inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900 transition-color cursor-pointer" %>
14
+ <% end %>
15
+ </div>
16
+ </div>
17
+ <% elsif !@use_auto_footer_navigation && @footer_navigation_columns.any? %>
18
+ <div class="pb-8">
19
+ <div class="flex flex-col gap-y-6 sm:flex-row sm:flex-wrap sm:gap-x-10 sm:gap-y-8">
20
+ <% @footer_navigation_columns.each do |column| %>
21
+ <div class="min-w-32">
22
+ <p class="mb-3 text-sm font-semibold text-gray-700"><%= navigation_label(column) %></p>
23
+ <div class="space-y-2">
24
+ <% column.children.each do |child| %>
25
+ <% if child.target_kind != "external" && navigation_href(child).present? %>
26
+ <%= link_to navigation_label(child), navigation_href(child), **navigation_link_options(child), class: "block text-sm text-gray-600 hover:text-gray-900 transition-colors cursor-pointer" %>
27
+ <% end %>
28
+ <% end %>
29
+ </div>
30
+ </div>
31
+ <% end %>
32
+ </div>
33
+ </div>
34
+ <% end %>
35
+
36
+ <div class="pt-6 border-t border-gray-100 flex flex-col sm:flex-row items-center justify-between gap-3">
37
+ <p class="text-xs text-gray-400 text-center sm:text-left sm:flex-1">
38
+ &copy; <%= Date.current.year %> <%= t('postnhost.public.site.name_footer_copyright') %>
39
+ </p>
40
+
41
+ <% if @use_auto_footer_navigation && (@footer_published_pages.any? || @footer_static_pages.any?) %>
42
+ <div class="flex flex-wrap items-center justify-center gap-x-4 gap-y-2 sm:flex-1">
43
+ <% @footer_published_pages.each do |page| %>
44
+ <%= link_to (page.title.presence || page.slug.humanize),
45
+ language_static_page_path(page.slug, @current_language),
46
+ class: "text-xs text-gray-400 hover:text-gray-600 transition-colors cursor-pointer" %>
47
+ <% end %>
48
+
49
+ <% @footer_static_pages.each do |slug| %>
50
+ <%= link_to slug.humanize,
51
+ language_static_page_path(slug, @current_language),
52
+ class: "text-xs text-gray-400 hover:text-gray-600 transition-colors cursor-pointer" %>
53
+ <% end %>
54
+ </div>
55
+ <% else %>
56
+ <div class="hidden sm:block sm:flex-1"></div>
57
+ <% end %>
58
+
59
+ <% if @setting.show_powered_by? %>
60
+ <a href="https://postnhost.com" target="_blank" rel="noopener noreferrer" class="inline-flex items-center gap-1.5 text-xs text-gray-400 hover:text-gray-600 transition-colors sm:flex-1 sm:justify-end cursor-pointer">
61
+ Powered by
62
+ <%= image_tag 'postnhost/logo.webp', alt: 'PostnHost', class: 'h-4 w-auto opacity-60 hover:opacity-100 transition-opacity', width: 80, height: 16 %>
63
+ </a>
64
+ <% end %>
65
+ </div>
66
+ </div>
67
+ </footer>
@@ -0,0 +1,89 @@
1
+ <nav class="bg-white border-b border-gray-200">
2
+ <div class="mx-auto px-6 py-2 flex items-center justify-between relative">
3
+ <div class="flex items-center">
4
+ <%= link_to root_path, class: 'flex items-center group' do %>
5
+ <%= image_tag 'postnhost/logo.webp',
6
+ alt: 'postnhost',
7
+ width: 160,
8
+ height: 33,
9
+ class: 'h-8 w-auto group-hover:opacity-80 transition-opacity' %>
10
+ <% end %>
11
+ </div>
12
+
13
+ <!-- Flash Messages - Centered on desktop, hidden on mobile (shown below header) -->
14
+ <div class="hidden md:block absolute left-1/2 transform -translate-x-1/2 z-10">
15
+ <%= render 'postnhost/shared/flash_messages' %>
16
+ </div>
17
+
18
+ <div class="flex items-center space-x-4" data-controller="burger">
19
+ <% if local_assigns[:layout_name] == "postnhost/articles" %>
20
+ <% if controller_name == "pages" && action_name != "index" %>
21
+ <%= link_to pages_path, class: "flex items-center space-x-2 px-3 py-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200" do %>
22
+ <%= icon("arrow-left", css_class: "w-4 h-4") %>
23
+ <span class="hidden sm:inline">Back to Pages</span>
24
+ <% end %>
25
+ <% else %>
26
+ <%= link_to articles_path, class: "flex items-center space-x-2 px-3 py-2 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors duration-200" do %>
27
+ <%= icon("arrow-left", css_class: "w-4 h-4") %>
28
+ <span class="hidden sm:inline">Back to Articles</span>
29
+ <% end %>
30
+ <% end %>
31
+ <% end %>
32
+
33
+ <div class="relative mr-0" data-controller="dropdown">
34
+ <button
35
+ data-action="click->dropdown#toggle"
36
+ class="flex items-center gap-3 rounded-xl transition-colors duration-200 cursor-pointer"
37
+ >
38
+ <div class="w-9 h-9 rounded-xl bg-gray-100 flex items-center justify-center overflow-hidden border-2 border-gray-200">
39
+ <% if current_user&.avatar_file? %>
40
+ <%= image_tag current_user.avatar_file_url, class: "w-full h-full object-cover", alt: 'Author Image' %>
41
+ <% else %>
42
+ <span class="text-gray-700 font-semibold text-sm">
43
+ <%= author_initials(current_user) %>
44
+ </span>
45
+ <% end %>
46
+ </div>
47
+ <div class="hidden sm:block text-left">
48
+ <p class="text-sm font-medium text-gray-900">
49
+ <%= current_user.name %>
50
+ </p>
51
+ </div>
52
+ <span data-dropdown-target="chevron" class="hidden sm:block">
53
+ <%= icon("chevron-down-small", css_class: "h-4 w-4 text-gray-400 transition-transform duration-200") %>
54
+ </span>
55
+ </button>
56
+
57
+ <div data-dropdown-target="menu" class="absolute right-0 mt-3 w-48 bg-white rounded-2xl shadow-lg border border-gray-200 overflow-hidden z-20 hidden">
58
+ <div class="px-4 py-3 border-b border-gray-100">
59
+ <p class="text-sm font-medium text-gray-900"><%= current_user.name %></p>
60
+ <p class="text-xs text-gray-500 truncate"><%= current_user&.email %></p>
61
+ </div>
62
+
63
+ <div class="py-2">
64
+ <%= link_to edit_user_path(current_user), class: "flex items-center px-4 py-3 text-sm text-gray-700 hover:bg-gray-50 transition-colors duration-150" do %>
65
+ <%= icon("user", css_class: "mr-3 h-4 w-4 text-gray-400") %>
66
+ Account
67
+ <% end %>
68
+ </div>
69
+
70
+ <div class="border-t border-gray-100 py-2">
71
+ <%= link_to session_path, data: { turbo_method: :delete }, class: "flex items-center px-4 py-3 text-sm text-red-600 hover:bg-red-50 hover:text-red-700 transition-colors duration-150" do %>
72
+ <%= icon("logout", css_class: "mr-3 h-4 w-4") %>
73
+ Sign Out
74
+ <% end %>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <% unless local_assigns[:layout_name] == "postnhost/articles" %>
80
+ <button
81
+ class="block md:hidden p-2 pr-0 md:pr-2 rounded-xl hover:bg-gray-50 text-gray-400 hover:text-gray-600 transition-colors duration-200"
82
+ data-action="click->burger#toggleMenu"
83
+ >
84
+ <%= icon("menu", css_class: "w-5 h-5") %>
85
+ </button>
86
+ <% end %>
87
+ </div>
88
+ </div>
89
+ </nav>
@@ -0,0 +1,72 @@
1
+ <div id="menu" class="w-full md:w-64 bg-white p-4 h-full flex-shrink-0 border-r border-gray-200 flex flex-col justify-between overflow-y-auto hidden md:flex">
2
+ <div class="space-y-6">
3
+ <!-- Create Article Button -->
4
+ <%= link_to new_article_path, class: "w-full bg-gray-900 text-white px-4 py-2.5 rounded-xl hover:bg-gray-800 transition duration-200 text-sm font-medium inline-flex items-center justify-center space-x-2", data: { turbo_prefetch: false } do %>
5
+ <%= icon("plus", css_class: "w-4 h-4") %>
6
+ <span>Create Article</span>
7
+ <% end %>
8
+
9
+ <!-- Navigation -->
10
+ <div class="space-y-6">
11
+ <div>
12
+ <p class="px-3 mb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">Content</p>
13
+ <nav class="space-y-1">
14
+ <%= link_to articles_path, class: "group flex items-center space-x-3 text-gray-700 hover:text-gray-900 hover:bg-white font-medium px-3 py-2.5 rounded-xl transition-all duration-200 #{current_page?(articles_path) || controller_name == 'articles' || controller_name == 'article_variants' ? 'bg-white text-gray-900' : ''}" do %>
15
+ <div class="flex-shrink-0 #{current_page?(articles_path) || controller_name == 'articles' || controller_name == 'article_variants' ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-700'}">
16
+ <%= icon("document-text", css_class: "h-5 w-5") %>
17
+ </div>
18
+ <span class="text-sm font-medium">Articles</span>
19
+ <% end %>
20
+
21
+ <%= link_to categories_path, class: "group flex items-center space-x-3 text-gray-700 hover:text-gray-900 hover:bg-white font-medium px-3 py-2.5 rounded-xl transition-all duration-200 #{current_page?(categories_path) || controller_name == 'categories' ? 'bg-white text-gray-900' : ''}" do %>
22
+ <div class="flex-shrink-0 #{current_page?(categories_path) || controller_name == 'categories' ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-700'}">
23
+ <%= icon("folder", css_class: "h-5 w-5") %>
24
+ </div>
25
+ <span class="text-sm font-medium">Categories</span>
26
+ <% end %>
27
+
28
+ <%= link_to languages_path, class: "group flex items-center space-x-3 text-gray-700 hover:text-gray-900 hover:bg-white font-medium px-3 py-2.5 rounded-xl transition-all duration-200 #{current_page?(languages_path) || controller_name == 'languages' ? 'bg-white text-gray-900' : ''}" do %>
29
+ <div class="flex-shrink-0 #{current_page?(languages_path) || controller_name == 'languages' ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-700'}">
30
+ <%= icon("language", css_class: "h-5 w-5") %>
31
+ </div>
32
+ <span class="text-sm font-medium">Languages</span>
33
+ <% end %>
34
+
35
+ <%= link_to pages_path, class: "group flex items-center space-x-3 text-gray-700 hover:text-gray-900 hover:bg-white font-medium px-3 py-2.5 rounded-xl transition-all duration-200 #{current_page?(pages_path) || controller_name == 'pages' ? 'bg-white text-gray-900' : ''}" do %>
36
+ <div class="flex-shrink-0 #{current_page?(pages_path) || controller_name == 'pages' ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-700'}">
37
+ <%= icon("layers", css_class: "h-5 w-5") %>
38
+ </div>
39
+ <span class="text-sm font-medium">Pages</span>
40
+ <% end %>
41
+ </nav>
42
+ </div>
43
+
44
+ <div class="border-t border-gray-100 pt-4">
45
+ <p class="px-3 mb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">System</p>
46
+ <nav class="space-y-1">
47
+ <%= link_to users_path, class: "group flex items-center space-x-3 text-gray-700 hover:text-gray-900 hover:bg-white font-medium px-3 py-2.5 rounded-xl transition-all duration-200 #{current_page?(users_path) || controller_name == 'users' ? 'bg-white text-gray-900' : ''}" do %>
48
+ <div class="flex-shrink-0 #{current_page?(users_path) || controller_name == 'users' ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-700'}">
49
+ <%= icon("users", css_class: "h-5 w-5") %>
50
+ </div>
51
+ <span class="text-sm font-medium">Authors</span>
52
+ <% end %>
53
+
54
+ <%= link_to edit_settings_path, class: "group flex items-center space-x-3 text-gray-700 hover:text-gray-900 hover:bg-white font-medium px-3 py-2.5 rounded-xl transition-all duration-200 #{current_page?(edit_settings_path) || controller_name == 'settings' ? 'bg-white text-gray-900' : ''}" do %>
55
+ <div class="flex-shrink-0 #{current_page?(edit_settings_path) || controller_name == 'settings' ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-700'}">
56
+ <%= icon("settings", css_class: "h-5 w-5") %>
57
+ </div>
58
+ <span class="text-sm font-medium">Settings</span>
59
+ <% end %>
60
+
61
+ <%= link_to edit_template_path, class: "group flex items-center space-x-3 text-gray-700 hover:text-gray-900 hover:bg-white font-medium px-3 py-2.5 rounded-xl transition-all duration-200 #{current_page?(edit_template_path) || controller_name == 'templates' ? 'bg-white text-gray-900' : ''}" do %>
62
+ <div class="flex-shrink-0 #{current_page?(edit_template_path) || controller_name == 'templates' ? 'text-gray-900' : 'text-gray-500 group-hover:text-gray-700'}">
63
+ <%= icon("eye", css_class: "h-5 w-5") %>
64
+ </div>
65
+ <span class="text-sm font-medium">Template</span>
66
+ <% end %>
67
+
68
+ </nav>
69
+ </div>
70
+ </div>
71
+ </div>
72
+ </div>
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= content_for?(:lang) ? yield(:lang) : 'en' %>">
3
+ <head>
4
+ <title><%= content_for?(:title) ? yield(:title) : "postnhost" %></title>
5
+ <meta name="description" content="<%= content_for?(:meta_description) ? yield(:meta_description) : "postnhost" %>">
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <% if content_for?(:robots) %>
8
+ <meta name="robots" content="<%= yield(:robots) %>">
9
+ <% end %>
10
+ <%= csrf_meta_tags %>
11
+ <%= csp_meta_tag %>
12
+
13
+ <%= stylesheet_link_tag(*postnhost_stylesheet_assets, "data-turbo-track": "reload", media: "all") %>
14
+ <%= javascript_include_tag "postnhost/application", "data-turbo-track": "reload", type: "module" %>
15
+
16
+ <%= render "layouts/postnhost/favicon" %>
17
+
18
+ <%= yield :head %>
19
+ </head>
20
+ <body>
21
+ <%= yield %>
22
+ </body>
23
+ </html>
@@ -0,0 +1,34 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= content_for?(:lang) ? yield(:lang) : 'en' %>">
3
+ <head>
4
+ <title><%= content_for?(:title) ? yield(:title) : "postnhost" %></title>
5
+ <meta name="description" content="<%= content_for?(:meta_description) ? yield(:meta_description) : "postnhost dashboard - Manage your account and projects" %>">
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+
10
+ <%= stylesheet_link_tag(*postnhost_stylesheet_assets, 'data-turbo-track': 'reload') %>
11
+ <%= javascript_include_tag 'postnhost/application', 'data-turbo-track': 'reload', type: "module" %>
12
+ <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
13
+
14
+ <%= render 'layouts/postnhost/favicon' %>
15
+
16
+ <%= yield :head %>
17
+ </head>
18
+
19
+ <body class="min-h-screen bg-white">
20
+ <%= render 'layouts/postnhost/header', layout_name: 'postnhost/articles' %>
21
+
22
+ <!-- Flash Messages for mobile - shown below header -->
23
+ <div class="md:hidden px-6">
24
+ <%= render 'postnhost/shared/flash_messages' %>
25
+ </div>
26
+
27
+ <!-- Main content area -->
28
+ <main class="flex-grow w-full">
29
+ <div class="w-full">
30
+ <%= yield %>
31
+ </div>
32
+ </main>
33
+ </body>
34
+ </html>
@@ -0,0 +1,36 @@
1
+ <!DOCTYPE html>
2
+ <html lang="<%= content_for?(:lang) ? yield(:lang) : 'en' %>">
3
+ <head>
4
+ <title><%= content_for?(:title) ? yield(:title) : "postnhost" %></title>
5
+ <meta name="description" content="<%= content_for?(:meta_description) ? yield(:meta_description) : "postnhost dashboard - Manage your account and projects" %>">
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+
10
+ <%= stylesheet_link_tag(*postnhost_stylesheet_assets, 'data-turbo-track': 'reload') %>
11
+ <%= javascript_include_tag 'postnhost/application', 'data-turbo-track': 'reload', type: "module" %>
12
+
13
+ <%= turbo_refreshes_with method: :morph, scroll: :preserve %>
14
+
15
+ <%= render 'layouts/postnhost/favicon' %>
16
+
17
+ <%= yield :head %>
18
+ </head>
19
+
20
+ <body class="h-screen flex flex-col">
21
+ <%= render 'layouts/postnhost/header' %>
22
+
23
+ <div class="flex-grow flex overflow-hidden">
24
+ <%= render 'layouts/postnhost/sidebar' %>
25
+
26
+ <!-- Main content area -->
27
+ <div class="flex-grow overflow-y-auto">
28
+ <div class="min-h-screen bg-white">
29
+ <div class="max-w-6xl mx-auto py-6 px-6">
30
+ <%= yield %>
31
+ </div>
32
+ </div>
33
+ </div>
34
+ </div>
35
+ </body>
36
+ </html>