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,322 @@
1
+ <div class="order-2 md:order-1 hidden md:block w-full md:w-1/5 bg-white border border-gray-200 p-4 md:p-6 border-t-0 md:sticky md:top-0 md:self-start md:min-h-screen"
2
+ data-editor-mobile-sidebars-target="<%= local_assigns[:mobile_target] %>"
3
+ data-controller="collapsible-sidebar"
4
+ data-collapsible-sidebar-storage-key-value="<%= is_variant ? "article-variant-sidebar-#{resource.id}" : "article-sidebar-#{resource.id}" %>">
5
+ <div class="md:hidden flex items-center justify-between mb-4">
6
+ <h3 class="text-sm font-semibold text-gray-900">Details</h3>
7
+ <button type="button"
8
+ class="text-gray-500 hover:text-gray-700 cursor-pointer"
9
+ data-action="click->editor-mobile-sidebars#closeMeta">
10
+ <%= icon("close", css_class: "w-4 h-4") %>
11
+ </button>
12
+ </div>
13
+ <div class="space-y-6">
14
+ <div data-collapsible-sidebar-target="section" data-section-id="title-tag">
15
+ <button type="button"
16
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
17
+ data-role="section-toggle"
18
+ data-action="click->collapsible-sidebar#toggle"
19
+ data-collapsible-sidebar-id-param="title-tag"
20
+ aria-expanded="true"
21
+ aria-controls="sidebar-section-title-tag">
22
+ <span class="text-sm font-medium text-gray-900">Title Tag</span>
23
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 rotate-0">
24
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
25
+ </span>
26
+ </button>
27
+ <div data-role="section-content" id="sidebar-section-title-tag">
28
+ <%= form.text_field :title_tag, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm", placeholder: (is_variant ? "Falls back to variant title if empty." : "Fallbacks to article title if empty."), autocomplete: "off" %>
29
+ </div>
30
+ </div>
31
+
32
+ <div data-collapsible-sidebar-target="section" data-section-id="meta-description">
33
+ <button type="button"
34
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
35
+ data-role="section-toggle"
36
+ data-action="click->collapsible-sidebar#toggle"
37
+ data-collapsible-sidebar-id-param="meta-description"
38
+ aria-expanded="true"
39
+ aria-controls="sidebar-section-meta-description">
40
+ <span class="text-sm font-medium text-gray-900">Meta Description</span>
41
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 rotate-0">
42
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
43
+ </span>
44
+ </button>
45
+ <div data-role="section-content" id="sidebar-section-meta-description">
46
+ <%= form.text_area :meta_description, rows: 3, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm resize-none", placeholder: "Brief description for SEO...", autocomplete: "off" %>
47
+ <p class="mt-2 text-xs text-gray-500">Recommended: 150-160 characters.</p>
48
+ </div>
49
+ </div>
50
+
51
+ <% if is_variant %>
52
+ <div data-collapsible-sidebar-target="section" data-section-id="language">
53
+ <button type="button"
54
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
55
+ data-role="section-toggle"
56
+ data-action="click->collapsible-sidebar#toggle"
57
+ data-collapsible-sidebar-id-param="language"
58
+ aria-expanded="true"
59
+ aria-controls="sidebar-section-language">
60
+ <span class="text-sm font-medium text-gray-900">Language</span>
61
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 rotate-0">
62
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
63
+ </span>
64
+ </button>
65
+ <div data-role="section-content" id="sidebar-section-language">
66
+ <%= form.text_field :language, disabled: true, value: resource.language&.name, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm disabled:bg-gray-100" %>
67
+ </div>
68
+ </div>
69
+
70
+ <div data-collapsible-sidebar-target="section" data-section-id="og-title" data-default-collapsed="true">
71
+ <button type="button"
72
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
73
+ data-role="section-toggle"
74
+ data-action="click->collapsible-sidebar#toggle"
75
+ data-collapsible-sidebar-id-param="og-title"
76
+ aria-expanded="false"
77
+ aria-controls="sidebar-section-og-title">
78
+ <span class="text-sm font-medium text-gray-900">OG Title</span>
79
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 -rotate-90">
80
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
81
+ </span>
82
+ </button>
83
+ <div data-role="section-content" id="sidebar-section-og-title" class="hidden">
84
+ <%= form.text_field :og_title, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm", placeholder: "Fallbacks to Title Tag / title if empty", autocomplete: "off" %>
85
+ </div>
86
+ </div>
87
+
88
+ <div data-collapsible-sidebar-target="section" data-section-id="schema-headline" data-default-collapsed="true">
89
+ <button type="button"
90
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
91
+ data-role="section-toggle"
92
+ data-action="click->collapsible-sidebar#toggle"
93
+ data-collapsible-sidebar-id-param="schema-headline"
94
+ aria-expanded="false"
95
+ aria-controls="sidebar-section-schema-headline">
96
+ <span class="text-sm font-medium text-gray-900">headline (Schema)</span>
97
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 -rotate-90">
98
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
99
+ </span>
100
+ </button>
101
+ <div data-role="section-content" id="sidebar-section-schema-headline" class="hidden">
102
+ <%= form.text_field :schema_headline, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm", placeholder: "Fallbacks to Title Tag / title if empty", autocomplete: "off" %>
103
+ </div>
104
+ </div>
105
+
106
+ <div data-collapsible-sidebar-target="section" data-section-id="excerpt" data-default-collapsed="true">
107
+ <button type="button"
108
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
109
+ data-role="section-toggle"
110
+ data-action="click->collapsible-sidebar#toggle"
111
+ data-collapsible-sidebar-id-param="excerpt"
112
+ aria-expanded="false"
113
+ aria-controls="sidebar-section-excerpt">
114
+ <span class="text-sm font-medium text-gray-900">Custom Excerpt</span>
115
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 -rotate-90">
116
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
117
+ </span>
118
+ </button>
119
+ <div data-role="section-content" id="sidebar-section-excerpt" class="hidden">
120
+ <%= form.text_area :custom_excerpt, rows: 4, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm resize-none", placeholder: "Optional custom excerpt for cards and snippets...", autocomplete: "off" %>
121
+ <p class="mt-2 text-xs text-gray-500">Maximum <%= Postnhost::Article.custom_excerpt_max_chars %> characters. If empty, cards fall back to the first sentences from content.</p>
122
+ <label class="mt-3 flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
123
+ <%= form.check_box :use_excerpt_as_meta_description, class: "h-4 w-4 rounded border-gray-300 text-gray-900 focus:ring-gray-900 cursor-pointer" %>
124
+ <span class="cursor-pointer">Use excerpt as meta description</span>
125
+ </label>
126
+ </div>
127
+ </div>
128
+ <% else %>
129
+ <div data-collapsible-sidebar-target="section" data-section-id="authors">
130
+ <button type="button"
131
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
132
+ data-role="section-toggle"
133
+ data-action="click->collapsible-sidebar#toggle"
134
+ data-collapsible-sidebar-id-param="authors"
135
+ aria-expanded="true"
136
+ aria-controls="sidebar-section-authors">
137
+ <span class="text-sm font-medium text-gray-900">Authors</span>
138
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 rotate-0">
139
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
140
+ </span>
141
+ </button>
142
+ <div data-role="section-content" id="sidebar-section-authors">
143
+ <%= render "postnhost/articles/author_manager", form: form, article: resource, users: users %>
144
+ </div>
145
+ </div>
146
+
147
+ <div data-collapsible-sidebar-target="section" data-section-id="categories">
148
+ <button type="button"
149
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
150
+ data-role="section-toggle"
151
+ data-action="click->collapsible-sidebar#toggle"
152
+ data-collapsible-sidebar-id-param="categories"
153
+ aria-expanded="true"
154
+ aria-controls="sidebar-section-categories">
155
+ <span class="text-sm font-medium text-gray-900">Categories</span>
156
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 rotate-0">
157
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
158
+ </span>
159
+ </button>
160
+ <div data-role="section-content" id="sidebar-section-categories">
161
+ <%= render 'postnhost/articles/category_manager', form: form, article: resource, categories: categories %>
162
+ </div>
163
+ </div>
164
+
165
+ <div data-collapsible-sidebar-target="section" data-section-id="og-title" data-default-collapsed="true">
166
+ <button type="button"
167
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
168
+ data-role="section-toggle"
169
+ data-action="click->collapsible-sidebar#toggle"
170
+ data-collapsible-sidebar-id-param="og-title"
171
+ aria-expanded="false"
172
+ aria-controls="sidebar-section-og-title">
173
+ <span class="text-sm font-medium text-gray-900">OG Title</span>
174
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 -rotate-90">
175
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
176
+ </span>
177
+ </button>
178
+ <div data-role="section-content" id="sidebar-section-og-title" class="hidden">
179
+ <%= form.text_field :og_title, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm", placeholder: "Fallbacks to Title Tag / title if empty", autocomplete: "off" %>
180
+ </div>
181
+ </div>
182
+
183
+ <div data-collapsible-sidebar-target="section" data-section-id="excerpt" data-default-collapsed="true">
184
+ <button type="button"
185
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
186
+ data-role="section-toggle"
187
+ data-action="click->collapsible-sidebar#toggle"
188
+ data-collapsible-sidebar-id-param="excerpt"
189
+ aria-expanded="false"
190
+ aria-controls="sidebar-section-excerpt">
191
+ <span class="text-sm font-medium text-gray-900">Custom Excerpt</span>
192
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 -rotate-90">
193
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
194
+ </span>
195
+ </button>
196
+ <div data-role="section-content" id="sidebar-section-excerpt" class="hidden">
197
+ <%= form.text_area :custom_excerpt, rows: 4, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm resize-none", placeholder: "Optional custom excerpt for cards and snippets...", autocomplete: "off" %>
198
+ <p class="mt-2 text-xs text-gray-500">Maximum <%= Postnhost::Article.custom_excerpt_max_chars %> characters. If empty, cards fall back to the first sentences from content.</p>
199
+ <label class="mt-3 flex items-center gap-2 text-sm text-gray-700 cursor-pointer">
200
+ <%= form.check_box :use_excerpt_as_meta_description, class: "h-4 w-4 rounded border-gray-300 text-gray-900 focus:ring-gray-900 cursor-pointer" %>
201
+ <span class="cursor-pointer">Use excerpt as meta description</span>
202
+ </label>
203
+ </div>
204
+ </div>
205
+
206
+ <div data-collapsible-sidebar-target="section" data-section-id="suggested-articles" data-default-collapsed="true">
207
+ <button type="button"
208
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
209
+ data-role="section-toggle"
210
+ data-action="click->collapsible-sidebar#toggle"
211
+ data-collapsible-sidebar-id-param="suggested-articles"
212
+ aria-expanded="false"
213
+ aria-controls="sidebar-section-suggested-articles">
214
+ <span class="text-sm font-medium text-gray-900">Suggested Articles</span>
215
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 -rotate-90">
216
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
217
+ </span>
218
+ </button>
219
+ <div data-role="section-content" id="sidebar-section-suggested-articles" class="hidden">
220
+ <p class="mb-2 text-xs text-gray-500">If none are specified, 3 suggestions are auto selected.</p>
221
+ <%= render 'postnhost/articles/suggestion_manager', form: form, article: resource, articles: articles %>
222
+ </div>
223
+ </div>
224
+
225
+ <div data-collapsible-sidebar-target="section" data-section-id="url-slug">
226
+ <button type="button"
227
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
228
+ data-role="section-toggle"
229
+ data-action="click->collapsible-sidebar#toggle"
230
+ data-collapsible-sidebar-id-param="url-slug"
231
+ aria-expanded="true"
232
+ aria-controls="sidebar-section-url-slug">
233
+ <span class="text-sm font-medium text-gray-900">URL Slug</span>
234
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 rotate-0">
235
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
236
+ </span>
237
+ </button>
238
+ <div data-role="section-content" id="sidebar-section-url-slug">
239
+ <%= form.text_field :slug, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm", placeholder: "article-url-slug", autocomplete: "off" %>
240
+ <p class="mt-2 text-xs text-gray-500">Slug changes will be reflected immediately.</p>
241
+ </div>
242
+ </div>
243
+
244
+ <div data-collapsible-sidebar-target="section" data-section-id="top-picks" data-default-collapsed="true">
245
+ <button type="button"
246
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
247
+ data-role="section-toggle"
248
+ data-action="click->collapsible-sidebar#toggle"
249
+ data-collapsible-sidebar-id-param="top-picks"
250
+ aria-expanded="false"
251
+ aria-controls="sidebar-section-top-picks">
252
+ <span class="text-sm font-medium text-gray-900">Top Picks Block</span>
253
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 -rotate-90">
254
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
255
+ </span>
256
+ </button>
257
+ <div data-role="section-content" id="sidebar-section-top-picks" class="hidden">
258
+ <label class="flex items-start gap-2 text-sm text-gray-700 cursor-pointer">
259
+ <%= form.check_box :top_pick, class: "mt-0.5 h-4 w-4 rounded border-gray-300 text-gray-900 focus:ring-gray-900 cursor-pointer" %>
260
+ <span class="cursor-pointer">Show this article in the public Top Picks block.</span>
261
+ </label>
262
+ </div>
263
+ </div>
264
+
265
+ <div data-collapsible-sidebar-target="section" data-section-id="schema" data-default-collapsed="true">
266
+ <button type="button"
267
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
268
+ data-role="section-toggle"
269
+ data-action="click->collapsible-sidebar#toggle"
270
+ data-collapsible-sidebar-id-param="schema"
271
+ aria-expanded="false"
272
+ aria-controls="sidebar-section-schema">
273
+ <span class="text-sm font-medium text-gray-900">Schema</span>
274
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 -rotate-90">
275
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
276
+ </span>
277
+ </button>
278
+ <div data-role="section-content" id="sidebar-section-schema" class="hidden space-y-4">
279
+ <div>
280
+ <label class="mb-1 block text-xs font-medium text-gray-700">headline (Schema)</label>
281
+ <%= form.text_field :schema_headline, class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm", placeholder: "Fallbacks to Title Tag / title if empty", autocomplete: "off" %>
282
+ </div>
283
+ <div>
284
+ <label class="mb-1 block text-xs font-medium text-gray-700">articleType (Schema)</label>
285
+ <%= form.select :schema_article_type,
286
+ options_for_select(schema_article_type_options, resource.schema_article_type),
287
+ { include_blank: "Use site default" },
288
+ class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm bg-white" %>
289
+ <p class="mt-2 text-xs text-gray-500">Leave blank to use the site-level default article schema type.</p>
290
+ </div>
291
+ </div>
292
+ </div>
293
+ <% end %>
294
+
295
+ <div class="pt-4 border-t border-gray-100" data-collapsible-sidebar-target="section" data-section-id="timestamps">
296
+ <button type="button"
297
+ class="w-full flex items-center justify-between text-left mb-2 cursor-pointer"
298
+ data-role="section-toggle"
299
+ data-action="click->collapsible-sidebar#toggle"
300
+ data-collapsible-sidebar-id-param="timestamps"
301
+ aria-expanded="true"
302
+ aria-controls="sidebar-section-timestamps">
303
+ <span class="text-sm font-medium text-gray-900">Timestamps</span>
304
+ <span data-role="section-icon" class="inline-flex text-gray-400 transition-transform duration-200 rotate-0">
305
+ <%= icon("chevron-down", css_class: "w-4 h-4") %>
306
+ </span>
307
+ </button>
308
+ <div data-role="section-content" id="sidebar-section-timestamps">
309
+ <div class="text-sm text-gray-600 space-y-1">
310
+ <p><strong>Created:</strong> <%= admin_datetime(resource.created_at) %></p>
311
+ <p><strong>Updated:</strong> <%= admin_datetime(resource.updated_at) %></p>
312
+ <% if !is_variant && resource.article_snapshot.present? %>
313
+ <p><strong>Published:</strong> <%= admin_datetime(resource.article_snapshot.published_at) %></p>
314
+ <% end %>
315
+ <% if !is_variant && resource.publication_error.present? %>
316
+ <p class="text-red-700"><strong>Publication error:</strong> <%= resource.publication_error %></p>
317
+ <% end %>
318
+ </div>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </div>
@@ -0,0 +1,29 @@
1
+ <div class="w-1/5 bg-white border border-gray-200 p-6 hidden md:block border-t-0 sticky top-0 self-start min-h-screen">
2
+ <div class="space-y-6">
3
+ <div>
4
+ <label class="block text-sm font-medium text-gray-900 mb-2">Title Tag</label>
5
+ <div class="w-full px-3 py-2 border border-gray-200 rounded-xl bg-gray-50 text-sm text-gray-700">
6
+ <%= resource.title_tag.presence || resource.title %>
7
+ </div>
8
+ <p class="mt-2 text-xs text-gray-500">Fallbacks to article title if empty.</p>
9
+ </div>
10
+
11
+ <div>
12
+ <label class="block text-sm font-medium text-gray-900 mb-2">Meta Description</label>
13
+ <div class="w-full px-3 py-2 border border-gray-200 rounded-xl bg-gray-50 text-sm text-gray-700 min-h-[72px]">
14
+ <%= resource.meta_description %>
15
+ </div>
16
+ <p class="mt-2 text-xs text-gray-500">Recommended: 150-160 characters</p>
17
+ </div>
18
+
19
+ <div class="pt-4 border-t border-gray-100">
20
+ <div class="text-sm text-gray-600 space-y-1">
21
+ <p><strong>Created:</strong> <%= admin_datetime(resource.created_at) %></p>
22
+ <p><strong>Updated:</strong> <%= admin_datetime(resource.updated_at) %></p>
23
+ <% if resource.article_snapshot.present? %>
24
+ <p><strong>Published:</strong> <%= admin_datetime(resource.article_snapshot.published_at) %></p>
25
+ <% end %>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
@@ -0,0 +1,105 @@
1
+ <%= turbo_frame_tag (resource_type == "variant" ? "article_variant_actions" : "article_actions") do %>
2
+ <div class="space-y-3">
3
+ <% if resource_type == "article" %>
4
+ <% if resource.published? %>
5
+ <%= link_to public_article_path(resource.slug), target: "_blank", class: editor_button_classes do %>
6
+ <div class="flex items-center justify-center space-x-2">
7
+ <span>Open</span>
8
+ <%= icon("external-link", css_class: "w-4 h-4") %>
9
+ </div>
10
+ <% end %>
11
+ <% end %>
12
+
13
+ <% if !resource.published? || resource.unpublished_changes? %>
14
+ <%= link_to preview_article_path(resource), target: "_blank", class: editor_button_classes do %>
15
+ <div class="flex items-center justify-center space-x-2">
16
+ <span>Preview</span>
17
+ <%= icon("external-link", css_class: "w-4 h-4") %>
18
+ </div>
19
+ <% end %>
20
+ <% end %>
21
+ <% elsif resource_type == "variant" && resource.published? && !resource.generating? %>
22
+ <%= link_to localized_public_article_path(resource.language.html_lang, resource.article.slug), target: "_blank", class: editor_button_classes do %>
23
+ <div class="flex items-center justify-center space-x-2">
24
+ <span>Open</span>
25
+ <%= icon("external-link", css_class: "w-4 h-4") %>
26
+ </div>
27
+ <% end %>
28
+ <% end %>
29
+
30
+ <% unless resource_type == "variant" && resource.generating? %>
31
+ <% if resource.published? %>
32
+ <% if resource.unpublished_changes? %>
33
+ <%= link_to (resource_type == "variant" ? publish_article_variant_path(article, resource) : publish_article_path(resource)),
34
+ method: :patch,
35
+ class: editor_button_classes(variant: :primary),
36
+ data: { turbo_method: :patch, publish_guard_target: "publishButton", action: "click->publish-guard#prevent" } do %>
37
+ Publish New Version
38
+ <% end %>
39
+ <% end %>
40
+
41
+ <%= link_to (resource_type == "variant" ? unpublish_article_variant_path(article, resource) : unpublish_article_path(resource)),
42
+ method: :patch,
43
+ class: "#{editor_button_classes} #{'mb-4' if resource_type == 'variant'}",
44
+ data: { turbo_method: :patch } do %>
45
+ Unpublish
46
+ <% end %>
47
+ <% else %>
48
+ <%= link_to (resource_type == "variant" ? publish_article_variant_path(article, resource) : publish_article_path(resource)),
49
+ method: :patch,
50
+ class: "#{editor_button_classes(variant: :primary)} #{'mb-4' if resource_type == 'variant'}",
51
+ data: { turbo_method: :patch, publish_guard_target: "publishButton", action: "click->publish-guard#prevent" } do %>
52
+ Publish Now
53
+ <% end %>
54
+
55
+ <% if resource_type == "article" %>
56
+ <%= render "postnhost/shared/articles/schedule_actions", resource: resource, form: (local_assigns[:form] if local_assigns[:form].present?) %>
57
+ <% end %>
58
+ <% end %>
59
+ <% end %>
60
+ </div>
61
+
62
+ <div class="mt-5 pt-3 pb-3 border-t border-gray-100">
63
+ <h4 class="text-sm font-semibold text-gray-900 mb-4">Current Status</h4>
64
+ <div class="space-y-3">
65
+ <% if resource_type == "article" && resource.scheduled_at.present? && !resource.published? %>
66
+ <div class="flex items-start">
67
+ <div class="w-2 h-2 bg-blue-400 rounded-full mt-2 mr-3 flex-shrink-0"></div>
68
+ <p class="text-sm text-gray-600">Scheduled to publish on <%= admin_datetime(resource.scheduled_at) %></p>
69
+ </div>
70
+ <% end %>
71
+
72
+ <% if resource.published? %>
73
+ <div class="flex items-start">
74
+ <div class="w-2 h-2 bg-green-400 rounded-full mt-2 mr-3 flex-shrink-0"></div>
75
+ <p class="text-sm text-gray-600">This <%= resource_type %> is published and visible to everyone</p>
76
+ </div>
77
+
78
+ <% if resource.unpublished_changes? %>
79
+ <div class="flex items-start">
80
+ <div class="w-2 h-2 bg-yellow-400 rounded-full mt-2 mr-3 flex-shrink-0"></div>
81
+ <p class="text-sm text-gray-600">You have unpublished changes. Click "Publish Now" to make them live.</p>
82
+ </div>
83
+ <% end %>
84
+ <% else %>
85
+ <div class="flex items-start">
86
+ <div class="w-2 h-2 bg-gray-400 rounded-full mt-2 mr-3 flex-shrink-0"></div>
87
+ <p class="text-sm text-gray-600">This <%= resource_type %> is a draft and only visible to you</p>
88
+ </div>
89
+ <% end %>
90
+ </div>
91
+ </div>
92
+
93
+ <% if resource_type == 'article' && resource.versions.any? %>
94
+ <div class="mt-5 pt-3 border-t border-gray-100">
95
+ <h4 class="text-sm font-semibold text-gray-900 mb-3">Version History</h4>
96
+ <div class="space-y-2">
97
+ <p class="text-sm text-gray-600"><%= resource.versions.count %> <%= resource.published? ? 'published' : 'previously published' %> version<%= resource.versions.count == 1 ? '' : 's' %></p>
98
+ <%= link_to versions_article_path(resource), class: "#{editor_button_classes} flex items-center justify-center", data: { turbo: false, turbo_prefetch: false } do %>
99
+ <%= icon("clock", css_class: "w-4 h-4 mr-2") %>
100
+ <span>View History</span>
101
+ <% end %>
102
+ </div>
103
+ </div>
104
+ <% end %>
105
+ <% end %>
@@ -0,0 +1,51 @@
1
+ <div class="space-y-3" data-controller="schedule-manager">
2
+ <button
3
+ type="button"
4
+ data-schedule-manager-target="addButton"
5
+ data-action="click->schedule-manager#showSelector"
6
+ class="<%= editor_button_classes %>">
7
+ <div class="flex items-center justify-center space-x-2">
8
+ <%= icon("schedule", css_class: "w-4 h-4") %>
9
+ <span>Schedule</span>
10
+ </div>
11
+ </button>
12
+
13
+ <% if resource.scheduled_at.present? %>
14
+ <div data-schedule-manager-target="scheduledSummary" class="bg-blue-50 border border-blue-100 rounded-xl px-3 py-2 text-sm text-blue-900">
15
+ <div class="flex items-start justify-between">
16
+ <div class="flex items-start space-x-2">
17
+ <%= icon("schedule", css_class: "w-4 h-4 mt-0.5 text-blue-700") %>
18
+ <div>
19
+ <p class="font-medium">Scheduled publish</p>
20
+ <p class="text-blue-700 text-xs mt-1"><%= admin_datetime(resource.scheduled_at) %></p>
21
+ </div>
22
+ </div>
23
+ </div>
24
+ <div class="mt-2 flex items-center justify-center space-x-4">
25
+ <button type="button" data-action="click->schedule-manager#showSelector" class="text-xs text-blue-700 hover:text-blue-900 cursor-pointer">
26
+ Update
27
+ </button>
28
+ <button type="button" data-action="click->schedule-manager#clearSchedule" class="text-xs text-blue-700 hover:text-blue-900 cursor-pointer">
29
+ Cancel
30
+ </button>
31
+ </div>
32
+ </div>
33
+ <% end %>
34
+
35
+ <div data-schedule-manager-target="selector" class="hidden">
36
+ <% if local_assigns[:form].present? %>
37
+ <%= form.datetime_local_field :scheduled_at,
38
+ min: admin_datetime_local_value(Time.current),
39
+ data: { schedule_manager_target: "input", action: "change->schedule-manager#markChanged blur->schedule-manager#markChanged" },
40
+ class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm bg-white" %>
41
+ <% else %>
42
+ <%= datetime_local_field_tag "article[scheduled_at]",
43
+ admin_datetime_local_value(resource.scheduled_at),
44
+ id: "article_scheduled_at",
45
+ min: admin_datetime_local_value(Time.current),
46
+ data: { schedule_manager_target: "input", action: "change->schedule-manager#markChanged blur->schedule-manager#markChanged" },
47
+ class: "w-full px-3 py-2 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-gray-900 focus:border-transparent text-sm bg-white" %>
48
+ <% end %>
49
+ <button type="button" data-action="click->schedule-manager#hideSelector" class="mt-2 text-xs text-gray-500 hover:text-gray-700 cursor-pointer">Cancel</button>
50
+ </div>
51
+ </div>