create-snapify-app 0.0.0-stage → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (614) hide show
  1. package/LICENSE.md +24 -0
  2. package/README.md +55 -2
  3. package/bin/create-snapify-app.mjs +174 -0
  4. package/lib/args.mjs +59 -0
  5. package/lib/copy.mjs +44 -0
  6. package/lib/env.mjs +84 -0
  7. package/lib/prompt.mjs +63 -0
  8. package/package.json +35 -4
  9. package/template/.env.example +79 -0
  10. package/template/.github/workflows/ci.yml +23 -0
  11. package/template/.github/workflows/deploy.yml +26 -0
  12. package/template/.github/workflows/ping.yml +8 -0
  13. package/template/.github/workflows/release.yml +67 -0
  14. package/template/.snapify-install.json +5 -0
  15. package/template/CHANGELOG.md +28 -0
  16. package/template/LICENSE +14 -0
  17. package/template/README.md +287 -0
  18. package/template/administrator.html +23 -0
  19. package/template/apps/client/src/App.tsx +765 -0
  20. package/template/apps/client/src/auth.css +400 -0
  21. package/template/apps/client/src/lib/api.ts +22 -0
  22. package/template/apps/client/src/main.tsx +20 -0
  23. package/template/apps/community/src/App.tsx +592 -0
  24. package/template/apps/community/src/main.tsx +20 -0
  25. package/template/apps/community/src/styles.css +24 -0
  26. package/template/apps/developers/src/App.tsx +643 -0
  27. package/template/apps/developers/src/main.tsx +17 -0
  28. package/template/apps/developers/src/pages.tsx +1587 -0
  29. package/template/apps/developers/src/styles.css +98 -0
  30. package/template/apps/developers/src/ui.tsx +179 -0
  31. package/template/apps/landing/src/App.tsx +598 -0
  32. package/template/apps/landing/src/LegalPages.tsx +158 -0
  33. package/template/apps/landing/src/OrderPage.tsx +381 -0
  34. package/template/apps/landing/src/SeoPages.tsx +923 -0
  35. package/template/apps/landing/src/copy.ts +195 -0
  36. package/template/apps/landing/src/main.tsx +20 -0
  37. package/template/apps/landing/src/styles.css +803 -0
  38. package/template/apps/landing/src/types.ts +19 -0
  39. package/template/apps/marketplace/src/App.tsx +1229 -0
  40. package/template/apps/marketplace/src/developer/DeveloperApp.tsx +1266 -0
  41. package/template/apps/marketplace/src/main.tsx +20 -0
  42. package/template/apps/marketplace/src/styles.css +1001 -0
  43. package/template/apps/shared/AuthContext.tsx +65 -0
  44. package/template/apps/shared/Logo.tsx +33 -0
  45. package/template/apps/shared/api.ts +47 -0
  46. package/template/apps/shared/ui.tsx +128 -0
  47. package/template/apps/superadmin/src/App.tsx +1264 -0
  48. package/template/apps/superadmin/src/lib/api.ts +20 -0
  49. package/template/apps/superadmin/src/lib/hooks.ts +20 -0
  50. package/template/apps/superadmin/src/main.tsx +16 -0
  51. package/template/apps/superadmin/src/pages/ClientsPage.tsx +185 -0
  52. package/template/apps/superadmin/src/pages/InvoicesPage.tsx +157 -0
  53. package/template/apps/superadmin/src/pages/NewsletterPage.tsx +212 -0
  54. package/template/apps/superadmin/src/pages/OrdersPage.tsx +195 -0
  55. package/template/apps/superadmin/src/pages/PaymentsPage.tsx +150 -0
  56. package/template/apps/superadmin/src/pages/PayoutsPage.tsx +133 -0
  57. package/template/apps/superadmin/src/pages/PublishersPage.tsx +151 -0
  58. package/template/apps/superadmin/src/pages/SeoControlPage.tsx +692 -0
  59. package/template/apps/superadmin/src/pages/SettingsPage.tsx +199 -0
  60. package/template/apps/superadmin/src/pages/SystemPage.tsx +109 -0
  61. package/template/apps/superadmin/src/pages/TicketsPage.tsx +201 -0
  62. package/template/apps/superadmin/src/pages/UsersPage.tsx +167 -0
  63. package/template/client.html +21 -0
  64. package/template/community/hamdiayari/akhbar/README.md +23 -0
  65. package/template/community/hamdiayari/akhbar/screenshot.jpg +0 -0
  66. package/template/community/hamdiayari/akhbar/snapify.theme.json +76 -0
  67. package/template/community/hamdiayari/akhbar/src/Brand.tsx +26 -0
  68. package/template/community/hamdiayari/akhbar/src/Footer.tsx +182 -0
  69. package/template/community/hamdiayari/akhbar/src/Header.tsx +191 -0
  70. package/template/community/hamdiayari/akhbar/src/Layout.tsx +49 -0
  71. package/template/community/hamdiayari/akhbar/src/Social.tsx +33 -0
  72. package/template/community/hamdiayari/akhbar/src/Ticker.tsx +86 -0
  73. package/template/community/hamdiayari/akhbar/src/Views.tsx +285 -0
  74. package/template/community/hamdiayari/akhbar/src/blocks/CollectionBlocks.tsx +672 -0
  75. package/template/community/hamdiayari/akhbar/src/blocks/ContentBlocks.tsx +305 -0
  76. package/template/community/hamdiayari/akhbar/src/blocks/ConversionBlocks.tsx +569 -0
  77. package/template/community/hamdiayari/akhbar/src/blocks/HeaderBlocks.tsx +287 -0
  78. package/template/community/hamdiayari/akhbar/src/index.tsx +51 -0
  79. package/template/community/hamdiayari/akhbar/src/kit.tsx +357 -0
  80. package/template/community/hamdiayari/akhbar/src/site.ts +96 -0
  81. package/template/community/hamdiayari/akhbar/src/styles.css +566 -0
  82. package/template/community/hamdiayari/akhbar-actualites/README.md +28 -0
  83. package/template/community/hamdiayari/akhbar-actualites/screenshot.jpg +0 -0
  84. package/template/community/hamdiayari/akhbar-actualites/snapify.module.json +107 -0
  85. package/template/community/hamdiayari/akhbar-actualites/src/admin/AudiencePage.tsx +185 -0
  86. package/template/community/hamdiayari/akhbar-actualites/src/admin/index.tsx +11 -0
  87. package/template/community/hamdiayari/akhbar-actualites/src/public/Archive.tsx +142 -0
  88. package/template/community/hamdiayari/akhbar-actualites/src/public/cards.tsx +152 -0
  89. package/template/community/hamdiayari/akhbar-actualites/src/public/index.tsx +71 -0
  90. package/template/community/hamdiayari/akhbar-actualites/src/public/posts.ts +103 -0
  91. package/template/community/hamdiayari/akhbar-actualites/src/public/styles.css +568 -0
  92. package/template/community/hamdiayari/akhbar-actualites/src/public/widgets.tsx +226 -0
  93. package/template/community/hamdiayari/akhbar-actualites/src/server.ts +119 -0
  94. package/template/community/hamdiayari/akhbar-actualites/src/types.ts +25 -0
  95. package/template/community/hamdiayari/akhbar-urgent/README.md +36 -0
  96. package/template/community/hamdiayari/akhbar-urgent/screenshot.jpg +0 -0
  97. package/template/community/hamdiayari/akhbar-urgent/snapify.module.json +64 -0
  98. package/template/community/hamdiayari/akhbar-urgent/src/admin/FlashesPage.tsx +229 -0
  99. package/template/community/hamdiayari/akhbar-urgent/src/admin/index.tsx +11 -0
  100. package/template/community/hamdiayari/akhbar-urgent/src/public/Ticker.tsx +81 -0
  101. package/template/community/hamdiayari/akhbar-urgent/src/public/index.tsx +63 -0
  102. package/template/community/hamdiayari/akhbar-urgent/src/public/styles.css +142 -0
  103. package/template/community/hamdiayari/akhbar-urgent/src/server.ts +101 -0
  104. package/template/community/hamdiayari/akhbar-urgent/src/types.ts +35 -0
  105. package/template/community.html +21 -0
  106. package/template/developers.html +24 -0
  107. package/template/docker-compose.db.yml +24 -0
  108. package/template/docs/BACKOFFICE_SNAPIFY.md +130 -0
  109. package/template/docs/CI_CD.md +66 -0
  110. package/template/docs/MODULES.md +346 -0
  111. package/template/docs/PLATFORM_DEPLOY.md +54 -0
  112. package/template/docs/logo.svg +10 -0
  113. package/template/drizzle/0000_init.sql +147 -0
  114. package/template/drizzle/0001_page_project.sql +5 -0
  115. package/template/drizzle/0002_modules.sql +23 -0
  116. package/template/drizzle/0003_themes.sql +4 -0
  117. package/template/drizzle/0004_security.sql +82 -0
  118. package/template/drizzle/0005_module_records.sql +16 -0
  119. package/template/drizzle/0006_avatars.sql +4 -0
  120. package/template/drizzle/0007_platform.sql +264 -0
  121. package/template/drizzle/0008_mp_validation.sql +3 -0
  122. package/template/drizzle/0009_saas_ops.sql +77 -0
  123. package/template/drizzle/0010_cms_chrome.sql +13 -0
  124. package/template/drizzle/0011_client_portal.sql +26 -0
  125. package/template/drizzle/meta/0000_snapshot.json +998 -0
  126. package/template/drizzle/meta/0001_snapshot.json +1004 -0
  127. package/template/drizzle/meta/_journal.json +90 -0
  128. package/template/drizzle.config.ts +16 -0
  129. package/template/ecosystem.snapify.cjs +24 -0
  130. package/template/for.html +1 -0
  131. package/template/index.html +29 -0
  132. package/template/landing.html +37 -0
  133. package/template/marketplace/catalog.json +373 -0
  134. package/template/marketplace/gallery/hamdiayari.preveris/01-home.jpg +0 -0
  135. package/template/marketplace/gallery/hamdiayari.recaptcha/01-reglages.jpg +0 -0
  136. package/template/marketplace/gallery/hamdiayari.recaptcha/02-logo.jpg +0 -0
  137. package/template/marketplace/gallery/novalabs.ai-articles/01-generer.jpg +0 -0
  138. package/template/marketplace/gallery/novalabs.ai-articles/02-fournisseur-ia.jpg +0 -0
  139. package/template/marketplace/gallery/novalabs.calendar/01-agenda-site.jpg +0 -0
  140. package/template/marketplace/gallery/novalabs.calendar/02-evenements-admin.jpg +0 -0
  141. package/template/marketplace/gallery/novalabs.calendar/03-reglages.jpg +0 -0
  142. package/template/marketplace/screenshots/hamdiayari.preveris-1.0.6.jpg +0 -0
  143. package/template/marketplace/screenshots/hamdiayari.recaptcha-1.0.0.jpg +0 -0
  144. package/template/marketplace/screenshots/hamdiayari.wp-seo-migrator-1.1.1.jpg +0 -0
  145. package/template/marketplace/screenshots/novalabs.ai-articles-1.0.1.jpg +0 -0
  146. package/template/marketplace/screenshots/novalabs.ai-chat-1.0.1.jpg +0 -0
  147. package/template/marketplace/screenshots/novalabs.calendar-1.0.1.jpg +0 -0
  148. package/template/marketplace/screenshots/novalabs.cart-1.0.0.jpg +0 -0
  149. package/template/marketplace/screenshots/novalabs.checkout-1.0.0.jpg +0 -0
  150. package/template/marketplace/screenshots/novalabs.customers-1.1.0.jpg +0 -0
  151. package/template/marketplace/screenshots/novalabs.dogga-1.0.0.jpg +0 -0
  152. package/template/marketplace/screenshots/novalabs.mahdia-1.0.0.jpg +0 -0
  153. package/template/marketplace/screenshots/novalabs.shop-1.0.0.jpg +0 -0
  154. package/template/marketplace/screenshots/novalabs.sidibou-1.0.0.jpg +0 -0
  155. package/template/marketplace/screenshots/novalabs.whatsapp-1.0.1.jpg +0 -0
  156. package/template/marketplace.html +21 -0
  157. package/template/metadata.json +7 -0
  158. package/template/modules/ai-articles/screenshot.jpg +0 -0
  159. package/template/modules/ai-articles/snapify.module.json +74 -0
  160. package/template/modules/ai-articles/src/admin/ArticlesPage.tsx +162 -0
  161. package/template/modules/ai-articles/src/admin/GeneratePage.tsx +118 -0
  162. package/template/modules/ai-articles/src/admin/index.tsx +12 -0
  163. package/template/modules/ai-articles/src/public/index.tsx +119 -0
  164. package/template/modules/ai-articles/src/server.ts +287 -0
  165. package/template/modules/ai-articles/src/types.ts +30 -0
  166. package/template/modules/ai-chat/screenshot.jpg +0 -0
  167. package/template/modules/ai-chat/snapify.module.json +217 -0
  168. package/template/modules/ai-chat/src/public.tsx +198 -0
  169. package/template/modules/ai-chat/src/server.ts +142 -0
  170. package/template/modules/ai-theme-studio/README.md +15 -0
  171. package/template/modules/ai-theme-studio/screenshot.jpg +0 -0
  172. package/template/modules/ai-theme-studio/snapify.module.json +175 -0
  173. package/template/modules/ai-theme-studio/src/admin/HistoryPage.tsx +91 -0
  174. package/template/modules/ai-theme-studio/src/admin/StudioPage.tsx +414 -0
  175. package/template/modules/ai-theme-studio/src/admin/index.tsx +11 -0
  176. package/template/modules/ai-theme-studio/src/lib/images.ts +240 -0
  177. package/template/modules/ai-theme-studio/src/lib/llm.ts +123 -0
  178. package/template/modules/ai-theme-studio/src/lib/normalize.ts +714 -0
  179. package/template/modules/ai-theme-studio/src/lib/prompts.ts +198 -0
  180. package/template/modules/ai-theme-studio/src/server.ts +137 -0
  181. package/template/modules/ai-theme-studio/src/types.ts +112 -0
  182. package/template/modules/calendar/screenshot.jpg +0 -0
  183. package/template/modules/calendar/snapify.module.json +45 -0
  184. package/template/modules/calendar/src/admin/EventsPage.tsx +217 -0
  185. package/template/modules/calendar/src/admin/index.tsx +11 -0
  186. package/template/modules/calendar/src/public/index.tsx +231 -0
  187. package/template/modules/calendar/src/server.ts +143 -0
  188. package/template/modules/calendar/src/types.ts +25 -0
  189. package/template/modules/cart/screenshot.jpg +0 -0
  190. package/template/modules/cart/snapify.module.json +90 -0
  191. package/template/modules/cart/src/admin/index.tsx +265 -0
  192. package/template/modules/cart/src/public/CartPage.tsx +198 -0
  193. package/template/modules/cart/src/public/index.tsx +261 -0
  194. package/template/modules/cart/src/public/useQuote.ts +67 -0
  195. package/template/modules/cart/src/server.ts +148 -0
  196. package/template/modules/cart/src/types.ts +31 -0
  197. package/template/modules/checkout/screenshot.jpg +0 -0
  198. package/template/modules/checkout/snapify.module.json +264 -0
  199. package/template/modules/checkout/src/admin/OrderDetailPage.tsx +304 -0
  200. package/template/modules/checkout/src/admin/badges.tsx +28 -0
  201. package/template/modules/checkout/src/admin/index.tsx +236 -0
  202. package/template/modules/checkout/src/public/CheckoutPage.tsx +461 -0
  203. package/template/modules/checkout/src/public/OrderPages.tsx +153 -0
  204. package/template/modules/checkout/src/public/index.tsx +23 -0
  205. package/template/modules/checkout/src/server.ts +383 -0
  206. package/template/modules/checkout/src/types.ts +70 -0
  207. package/template/modules/customers/screenshot.jpg +0 -0
  208. package/template/modules/customers/snapify.module.json +115 -0
  209. package/template/modules/customers/src/admin/CustomerDetailPage.tsx +274 -0
  210. package/template/modules/customers/src/admin/index.tsx +194 -0
  211. package/template/modules/customers/src/public/AccountPages.tsx +470 -0
  212. package/template/modules/customers/src/public/AddressForm.tsx +115 -0
  213. package/template/modules/customers/src/public/AuthPages.tsx +349 -0
  214. package/template/modules/customers/src/public/Avatar.tsx +29 -0
  215. package/template/modules/customers/src/public/ProfileSection.tsx +291 -0
  216. package/template/modules/customers/src/public/index.tsx +137 -0
  217. package/template/modules/customers/src/public/session.ts +51 -0
  218. package/template/modules/customers/src/server.ts +421 -0
  219. package/template/modules/customers/src/types.ts +53 -0
  220. package/template/modules/recaptcha/logo.jpg +0 -0
  221. package/template/modules/recaptcha/screenshot.jpg +0 -0
  222. package/template/modules/recaptcha/snapify.module.json +99 -0
  223. package/template/modules/recaptcha/src/public/index.tsx +94 -0
  224. package/template/modules/recaptcha/src/server.ts +65 -0
  225. package/template/modules/recaptcha/src/types.ts +18 -0
  226. package/template/modules/shop/screenshot.jpg +0 -0
  227. package/template/modules/shop/snapify.module.json +340 -0
  228. package/template/modules/shop/src/admin/CategoriesPage.tsx +224 -0
  229. package/template/modules/shop/src/admin/ProductEditor.tsx +313 -0
  230. package/template/modules/shop/src/admin/ProductsPage.tsx +307 -0
  231. package/template/modules/shop/src/admin/index.tsx +14 -0
  232. package/template/modules/shop/src/public/Catalog.tsx +244 -0
  233. package/template/modules/shop/src/public/NotFound.tsx +22 -0
  234. package/template/modules/shop/src/public/ProductPage.tsx +195 -0
  235. package/template/modules/shop/src/public/index.tsx +134 -0
  236. package/template/modules/shop/src/public/useFetch.ts +26 -0
  237. package/template/modules/shop/src/server.ts +493 -0
  238. package/template/modules/shop/src/shared.ts +41 -0
  239. package/template/modules/shop/src/types.ts +60 -0
  240. package/template/modules/whatsapp/screenshot.jpg +0 -0
  241. package/template/modules/whatsapp/snapify.module.json +121 -0
  242. package/template/modules/whatsapp/src/public.tsx +119 -0
  243. package/template/modules/wp-seo-migrator/ARCHITECTURE.md +113 -0
  244. package/template/modules/wp-seo-migrator/README.md +21 -0
  245. package/template/modules/wp-seo-migrator/fixtures/aioseo-sample.csv +3 -0
  246. package/template/modules/wp-seo-migrator/fixtures/sample-exporter.json +39 -0
  247. package/template/modules/wp-seo-migrator/fixtures/sample-page.html +12 -0
  248. package/template/modules/wp-seo-migrator/fixtures/sample.htaccess +3 -0
  249. package/template/modules/wp-seo-migrator/screenshot.jpg +0 -0
  250. package/template/modules/wp-seo-migrator/snapify.module.json +87 -0
  251. package/template/modules/wp-seo-migrator/src/admin/HistoryPage.tsx +64 -0
  252. package/template/modules/wp-seo-migrator/src/admin/WizardPage.tsx +788 -0
  253. package/template/modules/wp-seo-migrator/src/admin/i18n.ts +156 -0
  254. package/template/modules/wp-seo-migrator/src/admin/index.tsx +11 -0
  255. package/template/modules/wp-seo-migrator/src/connectors/CUSTOM_CONNECTOR.md +31 -0
  256. package/template/modules/wp-seo-migrator/src/connectors/csvSeoFactory.ts +106 -0
  257. package/template/modules/wp-seo-migrator/src/connectors/exporter.ts +96 -0
  258. package/template/modules/wp-seo-migrator/src/connectors/gscCsv.ts +53 -0
  259. package/template/modules/wp-seo-migrator/src/connectors/htaccess.ts +65 -0
  260. package/template/modules/wp-seo-migrator/src/connectors/htmlCrawler.ts +90 -0
  261. package/template/modules/wp-seo-migrator/src/connectors/index.ts +113 -0
  262. package/template/modules/wp-seo-migrator/src/connectors/pluginCsv.ts +77 -0
  263. package/template/modules/wp-seo-migrator/src/connectors/rankMathCsv.ts +77 -0
  264. package/template/modules/wp-seo-migrator/src/connectors/redirection.ts +62 -0
  265. package/template/modules/wp-seo-migrator/src/connectors/sitemap.ts +44 -0
  266. package/template/modules/wp-seo-migrator/src/connectors/utils.ts +111 -0
  267. package/template/modules/wp-seo-migrator/src/connectors/wxr.ts +103 -0
  268. package/template/modules/wp-seo-migrator/src/connectors/yoastCsv.ts +56 -0
  269. package/template/modules/wp-seo-migrator/src/pipeline/analyze.ts +158 -0
  270. package/template/modules/wp-seo-migrator/src/pipeline/htmlParse.ts +58 -0
  271. package/template/modules/wp-seo-migrator/src/pipeline/remoteAuto.ts +328 -0
  272. package/template/modules/wp-seo-migrator/src/server.ts +492 -0
  273. package/template/modules/wp-seo-migrator/src/types.ts +151 -0
  274. package/template/modules/wp-seo-migrator/tests/connectors-extra.test.ts +39 -0
  275. package/template/modules/wp-seo-migrator/tests/merge.test.ts +25 -0
  276. package/template/package-lock.json +6016 -0
  277. package/template/package.json +74 -0
  278. package/template/public/images/photos/caravane-chameaux-desert.webp +0 -0
  279. package/template/public/images/photos/hammamet-medina-plage.jpg +0 -0
  280. package/template/public/images/photos/maison-campagne-collines.webp +0 -0
  281. package/template/public/images/photos/maisons-troglodytes-desert.jpg +0 -0
  282. package/template/public/images/photos/panorama-ville.jpg +0 -0
  283. package/template/public/images/photos/preveris-accessibilite-erp.jpg +0 -0
  284. package/template/public/images/photos/preveris-collectivites.jpg +0 -0
  285. package/template/public/images/photos/preveris-etudes-plans.jpg +0 -0
  286. package/template/public/images/photos/preveris-hero-erp.jpg +0 -0
  287. package/template/public/images/photos/preveris-ssi-incendie.jpg +0 -0
  288. package/template/public/images/photos/preveris-suivi-hotel.jpg +0 -0
  289. package/template/public/images/photos/ruelle-bleue-medina.jpg +0 -0
  290. package/template/public/images/photos/sidi-bou-said-port.webp +0 -0
  291. package/template/public/images/photos/terrasse-blanche-mer-voilier.jpg +0 -0
  292. package/template/public/landing/hero-ai-cms.jpg +0 -0
  293. package/template/public/landing/shot-dogga.jpg +0 -0
  294. package/template/public/landing/shot-mahdia.jpg +0 -0
  295. package/template/public/landing/shot-sidibou.jpg +0 -0
  296. package/template/public/llms.txt +35 -0
  297. package/template/public/logo.svg +10 -0
  298. package/template/scripts/app-pack.ts +155 -0
  299. package/template/scripts/app-publish.ts +123 -0
  300. package/template/scripts/apply-ecommerce-home.ts +196 -0
  301. package/template/scripts/auto-deploy-poll.sh +13 -0
  302. package/template/scripts/db-backup.sh +16 -0
  303. package/template/scripts/db-restore.sh +11 -0
  304. package/template/scripts/deploy.sh +34 -0
  305. package/template/scripts/github-webhook-deploy.sh +7 -0
  306. package/template/scripts/install-new-modules.ts +96 -0
  307. package/template/scripts/refresh-demo-baseline.ts +9 -0
  308. package/template/scripts/reinstall-marketplace-packs.ts +51 -0
  309. package/template/scripts/restore-classic-home.ts +13 -0
  310. package/template/scripts/sdk-pack.ts +289 -0
  311. package/template/scripts/sdk-publish.ts +75 -0
  312. package/template/scripts/seed-demo-products.ts +120 -0
  313. package/template/scripts/seed-theme-content-packs.ts +42 -0
  314. package/template/scripts/snapify-module.ts +207 -0
  315. package/template/scripts/test-theme-switch.ts +24 -0
  316. package/template/sdk/admin.css +18 -0
  317. package/template/sdk/admin.ts +95 -0
  318. package/template/sdk/build.ts +249 -0
  319. package/template/sdk/commerce.ts +512 -0
  320. package/template/sdk/content.ts +76 -0
  321. package/template/sdk/icons.ts +101 -0
  322. package/template/sdk/image.ts +60 -0
  323. package/template/sdk/index.ts +500 -0
  324. package/template/sdk/package.ts +298 -0
  325. package/template/sdk/recaptcha.tsx +122 -0
  326. package/template/sdk/settings.ts +84 -0
  327. package/template/sdk/theme.css +29 -0
  328. package/template/sdk/theme.ts +100 -0
  329. package/template/sdk/ui/commerce.tsx +385 -0
  330. package/template/sdk/ui/orders.tsx +136 -0
  331. package/template/sdk-kit/CHANGELOG.md +17 -0
  332. package/template/sdk-kit/LICENSE.md +47 -0
  333. package/template/sdk-kit/README.md +81 -0
  334. package/template/sdk-kit/cli.ts +339 -0
  335. package/template/sdk-kit/create-snapify/README.md +28 -0
  336. package/template/sdk-kit/create-snapify/create-snapify.mjs +11 -0
  337. package/template/sdk-kit/create-snapify/package.template.json +16 -0
  338. package/template/sdk-kit/package.template.json +95 -0
  339. package/template/sdk-kit/templates/common/README.md.tpl +27 -0
  340. package/template/sdk-kit/templates/common/_gitignore +3 -0
  341. package/template/sdk-kit/templates/common/package.json.tpl +19 -0
  342. package/template/sdk-kit/templates/common/src/snapify-env.d.ts +2 -0
  343. package/template/sdk-kit/templates/common/tsconfig.json +15 -0
  344. package/template/sdk-kit/templates/module/snapify.module.json.tpl +33 -0
  345. package/template/sdk-kit/templates/module/src/admin.tsx +53 -0
  346. package/template/sdk-kit/templates/module/src/public.tsx +49 -0
  347. package/template/sdk-kit/templates/module/src/server.ts +30 -0
  348. package/template/sdk-kit/templates/module/src/types.ts +19 -0
  349. package/template/sdk-kit/templates/theme/snapify.theme.json.tpl +22 -0
  350. package/template/sdk-kit/templates/theme/src/Footer.tsx +40 -0
  351. package/template/sdk-kit/templates/theme/src/Header.tsx +47 -0
  352. package/template/sdk-kit/templates/theme/src/Views.tsx +36 -0
  353. package/template/sdk-kit/templates/theme/src/blocks.tsx +93 -0
  354. package/template/sdk-kit/templates/theme/src/index.tsx +30 -0
  355. package/template/sdk-kit/templates/theme/src/site.tsx +59 -0
  356. package/template/sdk-kit/templates/theme/src/styles.css +71 -0
  357. package/template/server/client/routes.ts +252 -0
  358. package/template/server/cms/analyticsStore.ts +180 -0
  359. package/template/server/cms/auth.ts +155 -0
  360. package/template/server/cms/blockCatalog.ts +342 -0
  361. package/template/server/cms/blocks.ts +521 -0
  362. package/template/server/cms/contentMigration.ts +1119 -0
  363. package/template/server/cms/defaults.ts +303 -0
  364. package/template/server/cms/permissions.ts +154 -0
  365. package/template/server/cms/restoreClassicHome.ts +183 -0
  366. package/template/server/cms/revisions.ts +99 -0
  367. package/template/server/cms/scheduler.ts +78 -0
  368. package/template/server/cms/seed.ts +72 -0
  369. package/template/server/cms/seoAnalyzer.ts +213 -0
  370. package/template/server/cms/store.ts +122 -0
  371. package/template/server/cms/themeContent.ts +1386 -0
  372. package/template/server/cms/themePacks/sidibou-branding.json +1 -0
  373. package/template/server/cms/themePacks/sidibou-home.json +1 -0
  374. package/template/server/cms/themePacks/sidibou-menus.json +1 -0
  375. package/template/server/cms/types.ts +432 -0
  376. package/template/server/community/routes.ts +410 -0
  377. package/template/server/db/client.ts +43 -0
  378. package/template/server/db/repository.ts +389 -0
  379. package/template/server/db/schema.ts +726 -0
  380. package/template/server/demo/cmsReset.ts +196 -0
  381. package/template/server/developers/sdk.ts +85 -0
  382. package/template/server/geoip.ts +15 -0
  383. package/template/server/http.ts +53 -0
  384. package/template/server/mail.ts +216 -0
  385. package/template/server/marketplace/catalog.ts +74 -0
  386. package/template/server/marketplace/packageValidate.ts +475 -0
  387. package/template/server/marketplace/payments.ts +193 -0
  388. package/template/server/marketplace/routes.ts +850 -0
  389. package/template/server/marketplace/signing.ts +116 -0
  390. package/template/server/marketplace/syncOfficial.ts +282 -0
  391. package/template/server/media/images.ts +63 -0
  392. package/template/server/middleware/adminAuth.ts +134 -0
  393. package/template/server/modules/legacy.ts +44 -0
  394. package/template/server/modules/manager.ts +1122 -0
  395. package/template/server/modules/marketplace.ts +178 -0
  396. package/template/server/modules/outbound.ts +178 -0
  397. package/template/server/modules/purchases.ts +149 -0
  398. package/template/server/modules/records.ts +168 -0
  399. package/template/server/modules/themeHtml.ts +32 -0
  400. package/template/server/modules/trust.ts +16 -0
  401. package/template/server/modules/types.ts +147 -0
  402. package/template/server/platform/auth.ts +223 -0
  403. package/template/server/platform/clientWelcome.ts +168 -0
  404. package/template/server/platform/clients.ts +139 -0
  405. package/template/server/platform/dailyImprove.ts +356 -0
  406. package/template/server/platform/flouci.ts +126 -0
  407. package/template/server/platform/host.ts +69 -0
  408. package/template/server/platform/hostingProviders.ts +143 -0
  409. package/template/server/platform/licenseOrders.ts +291 -0
  410. package/template/server/platform/money.ts +42 -0
  411. package/template/server/platform/passwords.ts +41 -0
  412. package/template/server/platform/region.ts +86 -0
  413. package/template/server/platform/routes.ts +225 -0
  414. package/template/server/platform/seed.ts +82 -0
  415. package/template/server/platform/settings.ts +72 -0
  416. package/template/server/routes/adminAccount.ts +403 -0
  417. package/template/server/routes/adminCms.ts +808 -0
  418. package/template/server/routes/adminSystem.ts +243 -0
  419. package/template/server/routes/adminUsers.ts +449 -0
  420. package/template/server/routes/googleOAuth.ts +327 -0
  421. package/template/server/routes/modules.ts +323 -0
  422. package/template/server/routes/publicCms.ts +313 -0
  423. package/template/server/routes/seoPublic.ts +158 -0
  424. package/template/server/security/accounts.ts +121 -0
  425. package/template/server/security/audit.ts +143 -0
  426. package/template/server/security/authTokens.ts +74 -0
  427. package/template/server/security/ip.ts +68 -0
  428. package/template/server/security/rateLimit.ts +85 -0
  429. package/template/server/security/retention.ts +46 -0
  430. package/template/server/security/secrets.ts +44 -0
  431. package/template/server/security/sessions.ts +140 -0
  432. package/template/server/security/totp.ts +101 -0
  433. package/template/server/superadmin/analytics.ts +109 -0
  434. package/template/server/superadmin/auth.ts +92 -0
  435. package/template/server/superadmin/backlinksCatalog.ts +210 -0
  436. package/template/server/superadmin/frSeoContent.ts +165 -0
  437. package/template/server/superadmin/landingSeo.ts +675 -0
  438. package/template/server/superadmin/ops.ts +238 -0
  439. package/template/server/superadmin/routes.ts +1095 -0
  440. package/template/server/superadmin/tnSeoContent.ts +130 -0
  441. package/template/server.ts +522 -0
  442. package/template/src/App.tsx +263 -0
  443. package/template/src/admin/AdminApp.tsx +162 -0
  444. package/template/src/admin/account/AuthLayout.tsx +57 -0
  445. package/template/src/admin/account/SecurityForms.tsx +276 -0
  446. package/template/src/admin/admin.css +127 -0
  447. package/template/src/admin/context/AuthContext.tsx +147 -0
  448. package/template/src/admin/context/SiteContext.tsx +50 -0
  449. package/template/src/admin/editor/BlockLibrary.tsx +173 -0
  450. package/template/src/admin/editor/BlocksPanel.tsx +396 -0
  451. package/template/src/admin/editor/EditorBits.tsx +106 -0
  452. package/template/src/admin/editor/PreviewFrame.tsx +145 -0
  453. package/template/src/admin/editor/RichTextEditor.tsx +276 -0
  454. package/template/src/admin/editor/SeoPanel.tsx +349 -0
  455. package/template/src/admin/editor/StyleEditor.tsx +114 -0
  456. package/template/src/admin/editor/blockSchemas.ts +699 -0
  457. package/template/src/admin/editor/fields.tsx +486 -0
  458. package/template/src/admin/layout/AdminShell.tsx +418 -0
  459. package/template/src/admin/layout/nav.ts +95 -0
  460. package/template/src/admin/lib/api.ts +408 -0
  461. package/template/src/admin/lib/audit.ts +141 -0
  462. package/template/src/admin/lib/format.ts +84 -0
  463. package/template/src/admin/lib/hooks.ts +125 -0
  464. package/template/src/admin/main.tsx +16 -0
  465. package/template/src/admin/modules/InstallDialog.tsx +160 -0
  466. package/template/src/admin/modules/SettingsForm.tsx +145 -0
  467. package/template/src/admin/modules/kit.tsx +127 -0
  468. package/template/src/admin/modules/labels.tsx +226 -0
  469. package/template/src/admin/pages/AccountSetupPage.tsx +62 -0
  470. package/template/src/admin/pages/AnalyticsPage.tsx +206 -0
  471. package/template/src/admin/pages/BrandingPage.tsx +154 -0
  472. package/template/src/admin/pages/DashboardPage.tsx +239 -0
  473. package/template/src/admin/pages/EngagementPage.tsx +168 -0
  474. package/template/src/admin/pages/FormsPage.tsx +435 -0
  475. package/template/src/admin/pages/GeneralPage.tsx +120 -0
  476. package/template/src/admin/pages/LoginPage.tsx +249 -0
  477. package/template/src/admin/pages/MaintenancePage.tsx +125 -0
  478. package/template/src/admin/pages/MarketplacePage.tsx +426 -0
  479. package/template/src/admin/pages/MediaPage.tsx +251 -0
  480. package/template/src/admin/pages/MenusPage.tsx +446 -0
  481. package/template/src/admin/pages/ModuleAdminPage.tsx +113 -0
  482. package/template/src/admin/pages/ModuleDetailPage.tsx +293 -0
  483. package/template/src/admin/pages/ModulesPage.tsx +135 -0
  484. package/template/src/admin/pages/NewsletterPage.tsx +193 -0
  485. package/template/src/admin/pages/PageEditorPage.tsx +322 -0
  486. package/template/src/admin/pages/PagesListPage.tsx +277 -0
  487. package/template/src/admin/pages/PostEditorPage.tsx +292 -0
  488. package/template/src/admin/pages/PostsListPage.tsx +187 -0
  489. package/template/src/admin/pages/ProfilePage.tsx +489 -0
  490. package/template/src/admin/pages/ResetPasswordPage.tsx +110 -0
  491. package/template/src/admin/pages/SeoPage.tsx +670 -0
  492. package/template/src/admin/pages/SettingsPage.tsx +189 -0
  493. package/template/src/admin/pages/ThemeCustomizePage.tsx +170 -0
  494. package/template/src/admin/pages/ThemesPage.tsx +292 -0
  495. package/template/src/admin/pages/UsersPage.tsx +57 -0
  496. package/template/src/admin/pages/WidgetsPage.tsx +200 -0
  497. package/template/src/admin/seo/GoogleSetup.tsx +529 -0
  498. package/template/src/admin/settings/EmailTab.tsx +293 -0
  499. package/template/src/admin/settings/OtherTabs.tsx +270 -0
  500. package/template/src/admin/settings/SecurityTab.tsx +172 -0
  501. package/template/src/admin/settings/controls.tsx +133 -0
  502. package/template/src/admin/ui/Pagination.tsx +170 -0
  503. package/template/src/admin/ui/SaveBar.tsx +77 -0
  504. package/template/src/admin/ui/TagInput.tsx +54 -0
  505. package/template/src/admin/ui/charts.tsx +108 -0
  506. package/template/src/admin/ui/media.tsx +265 -0
  507. package/template/src/admin/ui/overlays.tsx +239 -0
  508. package/template/src/admin/ui/primitives.tsx +544 -0
  509. package/template/src/admin/users/AuditTab.tsx +104 -0
  510. package/template/src/admin/users/RolesTab.tsx +368 -0
  511. package/template/src/admin/users/UsersTab.tsx +723 -0
  512. package/template/src/admin/users/shared.tsx +278 -0
  513. package/template/src/cms/icons.ts +7 -0
  514. package/template/src/components/CookieBanner.tsx +166 -0
  515. package/template/src/components/MaintenanceGate.tsx +66 -0
  516. package/template/src/components/SiteAnalytics.tsx +64 -0
  517. package/template/src/components/SnapifyLogo.tsx +44 -0
  518. package/template/src/components/cms/BlockRenderer.tsx +154 -0
  519. package/template/src/components/cms/WidgetArea.tsx +193 -0
  520. package/template/src/context/SiteConfigContext.tsx +130 -0
  521. package/template/src/demoBase.ts +23 -0
  522. package/template/src/index.css +90 -0
  523. package/template/src/main.tsx +16 -0
  524. package/template/src/modules/ModuleHost.tsx +197 -0
  525. package/template/src/preview.ts +63 -0
  526. package/template/src/runtime.ts +38 -0
  527. package/template/src/themes/ThemeHost.tsx +168 -0
  528. package/template/src/themes/fallback.tsx +71 -0
  529. package/template/src/types.ts +94 -0
  530. package/template/src/views/BlogPostView.tsx +68 -0
  531. package/template/src/views/CmsPageView.tsx +34 -0
  532. package/template/start-pm2.sh +3 -0
  533. package/template/superadmin.html +20 -0
  534. package/template/themes/ai-canvas/screenshot.jpg +0 -0
  535. package/template/themes/ai-canvas/snapify.theme.json +96 -0
  536. package/template/themes/ai-canvas/src/CeeSimulator.tsx +247 -0
  537. package/template/themes/ai-canvas/src/Footer.tsx +190 -0
  538. package/template/themes/ai-canvas/src/Header.tsx +324 -0
  539. package/template/themes/ai-canvas/src/Layout.tsx +56 -0
  540. package/template/themes/ai-canvas/src/NotFound.tsx +58 -0
  541. package/template/themes/ai-canvas/src/Post.tsx +26 -0
  542. package/template/themes/ai-canvas/src/SnapifyLogo.tsx +44 -0
  543. package/template/themes/ai-canvas/src/blocks/CollectionBlocks.tsx +711 -0
  544. package/template/themes/ai-canvas/src/blocks/ContentBlocks.tsx +318 -0
  545. package/template/themes/ai-canvas/src/blocks/ConversionBlocks.tsx +559 -0
  546. package/template/themes/ai-canvas/src/blocks/HeaderBlocks.tsx +341 -0
  547. package/template/themes/ai-canvas/src/index.tsx +54 -0
  548. package/template/themes/ai-canvas/src/kit.tsx +398 -0
  549. package/template/themes/ai-canvas/src/site.ts +29 -0
  550. package/template/themes/ai-canvas/src/styles.css +317 -0
  551. package/template/themes/dogga/screenshot.jpg +0 -0
  552. package/template/themes/dogga/snapify.theme.json +55 -0
  553. package/template/themes/dogga/src/Brand.tsx +41 -0
  554. package/template/themes/dogga/src/Footer.tsx +202 -0
  555. package/template/themes/dogga/src/Header.tsx +239 -0
  556. package/template/themes/dogga/src/Layout.tsx +43 -0
  557. package/template/themes/dogga/src/Views.tsx +71 -0
  558. package/template/themes/dogga/src/blocks/CollectionBlocks.tsx +674 -0
  559. package/template/themes/dogga/src/blocks/ContentBlocks.tsx +307 -0
  560. package/template/themes/dogga/src/blocks/ConversionBlocks.tsx +571 -0
  561. package/template/themes/dogga/src/blocks/HeaderBlocks.tsx +289 -0
  562. package/template/themes/dogga/src/index.tsx +53 -0
  563. package/template/themes/dogga/src/kit.tsx +362 -0
  564. package/template/themes/dogga/src/site.ts +56 -0
  565. package/template/themes/dogga/src/styles.css +396 -0
  566. package/template/themes/mahdia/screenshot.jpg +0 -0
  567. package/template/themes/mahdia/snapify.theme.json +65 -0
  568. package/template/themes/mahdia/src/Brand.tsx +39 -0
  569. package/template/themes/mahdia/src/Footer.tsx +210 -0
  570. package/template/themes/mahdia/src/Header.tsx +291 -0
  571. package/template/themes/mahdia/src/Layout.tsx +44 -0
  572. package/template/themes/mahdia/src/Views.tsx +79 -0
  573. package/template/themes/mahdia/src/blocks/CollectionBlocks.tsx +674 -0
  574. package/template/themes/mahdia/src/blocks/ContentBlocks.tsx +307 -0
  575. package/template/themes/mahdia/src/blocks/ConversionBlocks.tsx +571 -0
  576. package/template/themes/mahdia/src/blocks/HeaderBlocks.tsx +289 -0
  577. package/template/themes/mahdia/src/index.tsx +53 -0
  578. package/template/themes/mahdia/src/kit.tsx +362 -0
  579. package/template/themes/mahdia/src/site.ts +75 -0
  580. package/template/themes/mahdia/src/styles.css +446 -0
  581. package/template/themes/preveris/screenshot.jpg +0 -0
  582. package/template/themes/preveris/snapify.theme.json +126 -0
  583. package/template/themes/preveris/src/Brand.tsx +44 -0
  584. package/template/themes/preveris/src/Footer.tsx +202 -0
  585. package/template/themes/preveris/src/Header.tsx +239 -0
  586. package/template/themes/preveris/src/Layout.tsx +42 -0
  587. package/template/themes/preveris/src/Views.tsx +71 -0
  588. package/template/themes/preveris/src/blocks/CollectionBlocks.tsx +678 -0
  589. package/template/themes/preveris/src/blocks/ContentBlocks.tsx +307 -0
  590. package/template/themes/preveris/src/blocks/ConversionBlocks.tsx +571 -0
  591. package/template/themes/preveris/src/blocks/HeaderBlocks.tsx +299 -0
  592. package/template/themes/preveris/src/index.tsx +53 -0
  593. package/template/themes/preveris/src/kit.tsx +365 -0
  594. package/template/themes/preveris/src/site.ts +79 -0
  595. package/template/themes/preveris/src/styles.css +446 -0
  596. package/template/themes/sidibou/screenshot.jpg +0 -0
  597. package/template/themes/sidibou/snapify.theme.json +21 -0
  598. package/template/themes/sidibou/src/CeeSimulator.tsx +247 -0
  599. package/template/themes/sidibou/src/Footer.tsx +203 -0
  600. package/template/themes/sidibou/src/Header.tsx +324 -0
  601. package/template/themes/sidibou/src/Layout.tsx +53 -0
  602. package/template/themes/sidibou/src/NotFound.tsx +58 -0
  603. package/template/themes/sidibou/src/Post.tsx +26 -0
  604. package/template/themes/sidibou/src/SnapifyLogo.tsx +44 -0
  605. package/template/themes/sidibou/src/blocks/CollectionBlocks.tsx +711 -0
  606. package/template/themes/sidibou/src/blocks/ContentBlocks.tsx +318 -0
  607. package/template/themes/sidibou/src/blocks/ConversionBlocks.tsx +559 -0
  608. package/template/themes/sidibou/src/blocks/HeaderBlocks.tsx +341 -0
  609. package/template/themes/sidibou/src/index.tsx +54 -0
  610. package/template/themes/sidibou/src/kit.tsx +398 -0
  611. package/template/themes/sidibou/src/site.ts +28 -0
  612. package/template/themes/sidibou/src/styles.css +317 -0
  613. package/template/tsconfig.json +33 -0
  614. package/template/vite.config.ts +42 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Contenu SEO Tunisie — pages locales & positionnement vs agences (Medianet, Novatis…).
3
+ * Utilisé pour meta PATH_SEO + HTML crawler (sans JS).
4
+ */
5
+ export type TnSeoPage = {
6
+ path: string;
7
+ titleTn: string;
8
+ titleFr: string;
9
+ descTn: string;
10
+ descFr: string;
11
+ priority: string;
12
+ /** HTML minimal pour crawlers (H1 + paragraphes). */
13
+ crawlerHtml: string;
14
+ };
15
+
16
+ function wrap(h1: string, blocks: string[]): string {
17
+ return `<article id="snapify-seo-crawler" lang="fr"><h1>${h1}</h1>${blocks.join('')}</article>`;
18
+ }
19
+
20
+ export const TN_SEO_PAGES: TnSeoPage[] = [
21
+ {
22
+ path: '/creation-site-web-tunisie',
23
+ titleTn: 'Création site web Tunisie — tarif clair 699 DT/an | Snapify',
24
+ titleFr: 'Création site web Tunisie — Snapify CMS',
25
+ descTn:
26
+ 'Création de site web en Tunisie sans devis flou : vitrine ou e-commerce, IA, Flouci, migration WordPress. Alternative aux agences (Medianet, Novatis…) à 699 DT/an.',
27
+ descFr: 'Créer un site web en Tunisie avec Snapify. Aussi disponible en France à 99 €/an.',
28
+ priority: '1.0',
29
+ crawlerHtml: wrap('Création site web Tunisie — Snapify CMS 699 DT/an', [
30
+ '<p>Créez un site vitrine ou une boutique en ligne en Tunisie avec Snapify : CMS Node sans PHP, génération IA, édition visuelle, paiement Flouci. Installation sous 48 h sur votre hébergement.</p>',
31
+ '<p>Face aux devis d’agence web (souvent plusieurs milliers de dinars chez Medianet, Novatis et autres), Snapify propose une licence annuelle à 699 DT : vous gardez le contrôle, sans refaire payer un projet à chaque évolution.</p>',
32
+ '<ul><li>Tunis, Sfax, Sousse et toute la Tunisie</li><li>Migration WordPress / Prestashop</li><li>Démo en ligne avant achat</li></ul>',
33
+ '<p><a href="/commander">Commander l’installation</a> · <a href="/creation-site-web-tunis">Création site Tunis</a> · <a href="/agence-web-tunisie">Snapify vs agence web</a></p>',
34
+ ]),
35
+ },
36
+ {
37
+ path: '/creation-site-web-tunis',
38
+ titleTn: 'Création site web Tunis — créateur de site | Snapify 699 DT/an',
39
+ titleFr: 'Création site web Tunis — Snapify',
40
+ descTn:
41
+ 'Création de site internet à Tunis (Lac, Centre-ville, Ariana…). Site vitrine ou e-commerce, IA, Flouci. 699 DT/an, installé en 48 h — moins cher qu’une agence classique.',
42
+ descFr: 'Créer un site web à Tunis avec Snapify CMS.',
43
+ priority: '0.95',
44
+ crawlerHtml: wrap('Création site web à Tunis — Snapify', [
45
+ '<p>Vous cherchez une création de site web à Tunis ? Snapify installe un CMS moderne (sans PHP) sur votre hébergement : pages, boutique, SEO, Flouci. Licence 699 DT/an.</p>',
46
+ '<p>Idéal pour TPE/PME du Grand Tunis (Berges du Lac, Montplaisir, La Marsa, Ariana) qui veulent un site pro sans projet à 5 000–8 000 DT chez une agence.</p>',
47
+ '<p><a href="/commander">Commander</a> · <a href="/creation-site-web-tunisie">Toute la Tunisie</a> · <a href="/boutique-en-ligne-tunisie">Boutique en ligne</a></p>',
48
+ ]),
49
+ },
50
+ {
51
+ path: '/creation-site-web-sfax',
52
+ titleTn: 'Création site web Sfax — site vitrine & e-commerce | Snapify',
53
+ titleFr: 'Création site web Sfax — Snapify',
54
+ descTn:
55
+ 'Création de site web à Sfax : vitrine, boutique, IA et Flouci. CMS tunisien Snapify à 699 DT/an, installation 48 h. Alternative locale aux gros devis d’agence.',
56
+ descFr: 'Créer un site web à Sfax avec Snapify.',
57
+ priority: '0.9',
58
+ crawlerHtml: wrap('Création site web à Sfax — Snapify', [
59
+ '<p>Entreprises sfaxiennes : lancez votre site vitrine ou e-commerce avec Snapify. Édition visuelle, modules signés, paiement Flouci. 699 DT/an.</p>',
60
+ '<p>Installation à distance — vous gardez votre domaine et votre hébergeur à Sfax ou ailleurs en Tunisie.</p>',
61
+ '<p><a href="/commander">Commander</a> · <a href="/creation-site-web-tunis">Tunis</a> · <a href="/creation-site-web-sousse">Sousse</a></p>',
62
+ ]),
63
+ },
64
+ {
65
+ path: '/creation-site-web-sousse',
66
+ titleTn: 'Création site web Sousse — tourisme, commerce, services | Snapify',
67
+ titleFr: 'Création site web Sousse — Snapify',
68
+ descTn:
69
+ 'Création de site web à Sousse et Sahel : hôtels, commerces, services. CMS Snapify 699 DT/an, IA, Flouci, mise en ligne 48 h.',
70
+ descFr: 'Créer un site web à Sousse avec Snapify.',
71
+ priority: '0.9',
72
+ crawlerHtml: wrap('Création site web à Sousse — Snapify', [
73
+ '<p>Site vitrine ou boutique pour votre activité à Sousse, Hammamet, Monastir : Snapify vous donne un CMS éditable + IA, sans dépendre d’un devis d’agence à plusieurs milliers de dinars.</p>',
74
+ '<p><a href="/commander">Commander 699 DT/an</a> · <a href="/creation-site-web-tunisie">Création site Tunisie</a></p>',
75
+ ]),
76
+ },
77
+ {
78
+ path: '/boutique-en-ligne-tunisie',
79
+ titleTn: 'Boutique en ligne Tunisie — e-commerce Flouci | Snapify 699 DT/an',
80
+ titleFr: 'Boutique en ligne Tunisie — Snapify',
81
+ descTn:
82
+ 'Créer une boutique en ligne en Tunisie : catalogue, panier, commandes, Flouci. CMS e-commerce Snapify à 699 DT/an — sans WooCommerce ni plugins fragiles.',
83
+ descFr: 'E-commerce Tunisie avec Snapify CMS.',
84
+ priority: '0.95',
85
+ crawlerHtml: wrap('Boutique en ligne Tunisie — Snapify', [
86
+ '<p>Lancez votre e-commerce en Tunisie : produits, panier, espace client, paiement Flouci. Stack Node, modules signés, 699 DT/an.</p>',
87
+ '<p>Alternative aux boutiques WordPress/WooCommerce et aux projets e-commerce d’agence (souvent 4 000 DT et plus).</p>',
88
+ '<p><a href="/commander">Créer ma boutique</a> · <a href="/creation-site-web-tunisie">Création site web Tunisie</a></p>',
89
+ ]),
90
+ },
91
+ {
92
+ path: '/agence-web-tunisie',
93
+ titleTn: 'Agence web Tunisie : Snapify vs Medianet, Novatis & co | 699 DT/an',
94
+ titleFr: 'Agence web Tunisie — Snapify CMS',
95
+ descTn:
96
+ 'Comparer agence web Tunisie (Medianet, Novatis…) et Snapify : devis 3–8 k DT vs licence 699 DT/an, CMS IA, Flouci, migration. Idéal TPE/PME.',
97
+ descFr: 'Snapify : alternative CMS aux agences web en Tunisie.',
98
+ priority: '0.95',
99
+ crawlerHtml: wrap('Agence web Tunisie : choisir entre devis d’agence et CMS Snapify', [
100
+ '<p>Les agences web tunisiennes (Medianet, Novatis, WEBI et d’autres) livrent souvent des sites sur mesure entre 3 000 et 8 000 DT pour une vitrine, davantage pour l’e-commerce. C’est pertinent pour des projets complexes ou une direction artistique très custom.</p>',
101
+ '<p>Snapify vise un autre besoin : créer et faire évoluer votre site vous-même (ou avec votre freelance) grâce à un CMS Node + IA, pour <strong>699 DT / an</strong>, installé en 48 h sur votre hébergement.</p>',
102
+ '<table><thead><tr><th>Critère</th><th>Agence classique</th><th>Snapify</th></tr></thead><tbody>',
103
+ '<tr><td>Budget typique</td><td>3 000–8 000+ DT (projet)</td><td>699 DT / an</td></tr>',
104
+ '<tr><td>Évolutions</td><td>Nouveaux devis</td><td>Édition autonome + IA</td></tr>',
105
+ '<tr><td>E-commerce</td><td>Sur devis</td><td>Boutique + Flouci inclus</td></tr>',
106
+ '<tr><td>Stack</td><td>Souvent WordPress / sur-mesure</td><td>CMS Node, modules signés</td></tr>',
107
+ '</tbody></table>',
108
+ '<p>Pas d’opposition frontale : si vous avez besoin d’une équipe créative full-service, une agence reste adaptée. Si vous voulez un site professionnel maîtrisé et abordable, testez Snapify.</p>',
109
+ '<p><a href="/commander">Commander Snapify</a> · <a href="/demo/">Voir la démo</a> · <a href="/creation-site-web-tunis">Site web Tunis</a></p>',
110
+ ]),
111
+ },
112
+ {
113
+ path: '/site-vitrine-tunisie',
114
+ titleTn: 'Site vitrine Tunisie — création rapide | Snapify 699 DT/an',
115
+ titleFr: 'Site vitrine Tunisie — Snapify',
116
+ descTn:
117
+ 'Créer un site vitrine en Tunisie : pages, SEO, formulaires, édition visuelle. Snapify 699 DT/an — plus rapide et prévisible qu’un devis d’agence.',
118
+ descFr: 'Site vitrine Tunisie avec Snapify.',
119
+ priority: '0.9',
120
+ crawlerHtml: wrap('Site vitrine Tunisie — Snapify', [
121
+ '<p>Présentez votre entreprise avec un site vitrine moderne : blocs, SEO, contact, responsive. Licence Snapify 699 DT/an, mise en service 48 h.</p>',
122
+ '<p><a href="/commander">Commander</a> · <a href="/boutique-en-ligne-tunisie">Passer en boutique</a></p>',
123
+ ]),
124
+ },
125
+ ];
126
+
127
+ export function tnSeoByPath(path: string): TnSeoPage | undefined {
128
+ const key = path.replace(/\/$/, '') || '/';
129
+ return TN_SEO_PAGES.find((p) => p.path === key);
130
+ }
@@ -0,0 +1,522 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ import 'dotenv/config';
8
+ import express from 'express';
9
+ import path from 'path';
10
+ import fs from 'fs/promises';
11
+ import { fileURLToPath } from 'url';
12
+ import { createServer as createViteServer } from 'vite';
13
+ import { createAdminCmsRouter } from './server/routes/adminCms';
14
+ import { createPublicCmsRouter, injectMaintenance } from './server/routes/publicCms';
15
+ import { createGoogleOAuthRouter } from './server/routes/googleOAuth';
16
+ import { createSeoPublicRouter, injectSeo, redirectMiddleware } from './server/routes/seoPublic';
17
+ import { injectTheme } from './server/modules/themeHtml';
18
+ import { loadSnapshot } from './server/cms/store';
19
+ import { runRetention } from './server/security/retention';
20
+ import { adminIpGuard } from './server/middleware/adminAuth';
21
+ import { auditMiddleware } from './server/security/audit';
22
+ import { rateLimit } from './server/security/rateLimit';
23
+ import { clientIp } from './server/security/ip';
24
+ import { createAdminAccountRouter } from './server/routes/adminAccount';
25
+ import { createAdminUsersRouter } from './server/routes/adminUsers';
26
+ import { createAdminSystemRouter } from './server/routes/adminSystem';
27
+ import { closeDb, runMigrations } from './server/db/client';
28
+ import { apiErrorHandler } from './server/http';
29
+ import { initModules } from './server/modules/manager';
30
+ import { createModulesAdminRouter, createModulesPublicRouter } from './server/routes/modules';
31
+ import { createCommunityRouter } from './server/community/routes';
32
+ import { createMarketplaceRouter } from './server/marketplace/routes';
33
+ import { createClientPortalRouter } from './server/client/routes';
34
+ import { createSdkDownloadsRouter } from './server/developers/sdk';
35
+ import { createFlouciWebhookRouter, createLandingRouter, createPlatformAuthRouter } from './server/platform/routes';
36
+ import { isLegacyClientHost, legacyClientRedirectUrl, resolveAppHost } from './server/platform/host';
37
+ import { resolveGeoFromIp } from './server/geoip';
38
+ import { geoRegionRedirect } from './server/platform/region';
39
+ import { seedPlatform } from './server/platform/seed';
40
+ import { startDemoResetScheduler } from './server/demo/cmsReset';
41
+ import { startDailyImproveScheduler } from './server/platform/dailyImprove';
42
+ import { db } from './server/db/client';
43
+ import { mpProducts } from './server/db/schema';
44
+ import { eq as eqDb } from 'drizzle-orm';
45
+ import {
46
+ createSuperadminRouter,
47
+ landingRobotsTxt,
48
+ landingSitemapIndexXml,
49
+ landingSitemapXml,
50
+ } from './server/superadmin/routes';
51
+ import {
52
+ buildMarketSeoPack,
53
+ crawlerSeoHtml,
54
+ getLandingSeo,
55
+ injectGa4,
56
+ marketFromHost,
57
+ renderLandingHeadTags,
58
+ } from './server/superadmin/landingSeo';
59
+
60
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
61
+
62
+ function requestHostHeader(req: { headers: Record<string, unknown> }): string {
63
+ const xf = String(req.headers['x-forwarded-host'] || '')
64
+ .split(',')[0]
65
+ .trim();
66
+ return (xf || String(req.headers.host || 'snapify.tn')).split(':')[0].toLowerCase();
67
+ }
68
+
69
+ /** Googlebot & crawlers : pays = marché du host (.fr → FR), pas la géoloc IP du datacenter. */
70
+ function isSearchCrawler(ua: string): boolean {
71
+ return /googlebot|bingbot|yandex|duckduckbot|slurp|baiduspider|facebookexternalhit|twitterbot|linkedinbot|applebot|semrush|ahrefs|dotbot|mj12bot/i.test(
72
+ ua || ''
73
+ );
74
+ }
75
+
76
+ function countryForRequest(req: { headers: Record<string, unknown> }, host: string, geoCountry: string): string {
77
+ if (isSearchCrawler(String(req.headers['user-agent'] || ''))) {
78
+ return marketFromHost(host) === 'fr' ? 'FR' : 'TN';
79
+ }
80
+ return geoCountry || (marketFromHost(host) === 'fr' ? 'FR' : 'TN');
81
+ }
82
+
83
+ function sendXml(res: express.Response, body: string) {
84
+ res.setHeader('Content-Type', 'application/xml; charset=utf-8');
85
+ res.setHeader('Cache-Control', 'public, max-age=3600');
86
+ res.removeHeader('X-Robots-Tag');
87
+ res.status(200).send(body);
88
+ }
89
+
90
+ function injectCountryScript(html: string, country: string): string {
91
+ const safe = country.replace(/[^A-Za-z]/g, '').slice(0, 2).toUpperCase();
92
+ if (html.includes('__SNAPIFY_COUNTRY__')) return html;
93
+ const script = `<script>window.__SNAPIFY_COUNTRY__=${JSON.stringify(safe)};</script>`;
94
+ return html.includes('</head>') ? html.replace('</head>', `${script}</head>`) : script + html;
95
+ }
96
+
97
+ /** Hors Tunisie : titre et prix en euros dès le HTML, avant l’hydratation + SEO serveur. */
98
+ async function withLandingMarket(html: string, country: string, host: string, pathName = '/'): Promise<string> {
99
+ let out = injectCountryScript(html, country);
100
+
101
+ // SEO serveur (Google voit title/description/hreflang/JSON-LD sans JS)
102
+ try {
103
+ const market = marketFromHost(host);
104
+ const seo = await getLandingSeo();
105
+ const pack = buildMarketSeoPack(seo, market, pathName);
106
+ const tags = renderLandingHeadTags(pack, seo);
107
+ // Remplace title + metas de base si présents, sinon injecte avant </head>
108
+ out = out.replace(/<title>[^<]*<\/title>/i, '');
109
+ out = out.replace(/<meta\s+name="description"[^>]*>/gi, '');
110
+ out = out.replace(/<meta\s+property="og:[^"]+"[^>]*>/gi, '');
111
+ out = out.replace(/<link\s+rel="canonical"[^>]*>/gi, '');
112
+ out = out.replace('</head>', ` ${tags}\n </head>`);
113
+ // Contenu indexable sans JS (pages TN / FR vs Wix & villes)
114
+ const crawler = crawlerSeoHtml(pathName.replace(/\/$/, '') || '/', market);
115
+ if (crawler) {
116
+ out = out.replace(/<article id="snapify-seo-crawler"[\s\S]*?<\/article>/i, '');
117
+ if (out.includes('<div id="root"></div>')) {
118
+ out = out.replace('<div id="root"></div>', `${crawler}\n <div id="root"></div>`);
119
+ } else if (out.includes('<div id="root">')) {
120
+ out = out.replace(/<div id="root">/, `${crawler}\n <div id="root">`);
121
+ }
122
+ }
123
+ } catch (e) {
124
+ console.warn('[landing-seo]', e);
125
+ }
126
+
127
+ // Remplacements prix / wording uniquement sur le domaine .fr (pas selon l’IP).
128
+ if (marketFromHost(host) === 'tn') return out;
129
+ return out
130
+ .replaceAll('CMS tunisien pour sites & e-commerce', 'CMS pour sites & e-commerce')
131
+ .replaceAll('CMS moderne sans PHP | IA, migration, e-commerce', 'CMS moderne | IA, migration, e-commerce')
132
+ .replaceAll('CMS tunisien', 'CMS')
133
+ .replaceAll('699 DT/an', '99 €/an')
134
+ .replaceAll('699 DT', '99 €');
135
+ }
136
+
137
+ async function startServer() {
138
+ const app = express();
139
+ const PORT = Number(process.env.PORT) || 3000;
140
+
141
+ app.set('trust proxy', 1);
142
+ app.disable('x-powered-by');
143
+ await runMigrations();
144
+ await seedPlatform();
145
+ await loadSnapshot();
146
+ await initModules();
147
+
148
+ // « //hamdisup90 » ou « /administrator//pages » : redirige vers le chemin propre au lieu d’afficher la landing.
149
+ // Les « \ » sont normalisés aussi : « /\evil.com » serait suivi par les navigateurs comme une autre origine.
150
+ app.use((req, res, next) => {
151
+ if (req.method !== 'GET' && req.method !== 'HEAD') return next();
152
+ const [pathname, ...query] = req.originalUrl.split('?');
153
+ if (!/\/\/|\\/.test(pathname)) return next();
154
+ const clean = pathname.replace(/\\/g, '/').replace(/\/{2,}/g, '/');
155
+ res.redirect(301, clean + (query.length ? `?${query.join('?')}` : ''));
156
+ });
157
+
158
+ // Plus de sous-domaine client.* : redirection vers /espace-client sur snapify.tn / snapify.fr
159
+ app.use((req, res, next) => {
160
+ if (req.method !== 'GET' && req.method !== 'HEAD') return next();
161
+ const host = requestHostHeader(req);
162
+ if (!isLegacyClientHost(host)) return next();
163
+ const dest = legacyClientRedirectUrl(host, req.originalUrl || '/');
164
+ res.setHeader('Cache-Control', 'no-store');
165
+ res.redirect(301, dest);
166
+ });
167
+
168
+ app.use(async (req, res, next) => {
169
+ res.setHeader('X-Content-Type-Options', 'nosniff');
170
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
171
+ if (req.path.startsWith('/administrator') || req.path.startsWith('/api/admin')) {
172
+ res.setHeader('X-Frame-Options', 'DENY');
173
+ res.setHeader('X-Robots-Tag', 'noindex, nofollow');
174
+ }
175
+ try {
176
+ if (req.secure && (await loadSnapshot()).system.security.hsts) {
177
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
178
+ }
179
+ next();
180
+ } catch (e) {
181
+ next(e);
182
+ }
183
+ });
184
+ // Base64 : +37 % par rapport à la taille du fichier autorisée dans les paramètres « Médias ».
185
+ app.use('/api/admin/media/upload', async (req, res, next) => {
186
+ try {
187
+ const { maxSizeMb } = (await loadSnapshot()).system.uploads;
188
+ express.json({ limit: `${Math.ceil(maxSizeMb * 1.37) + 1}mb` })(req, res, next);
189
+ } catch (e) {
190
+ next(e);
191
+ }
192
+ });
193
+ app.use('/api/admin/modules/upload', express.json({ limit: '30mb' }));
194
+ app.use('/api/marketplace/developer', express.json({ limit: '30mb' }));
195
+ // Webhook Flouci : conserver le corps brut pour la vérification HMAC
196
+ app.use(
197
+ '/api/platform/flouci/webhook',
198
+ express.json({
199
+ limit: '1mb',
200
+ verify: (req, _res, buf) => {
201
+ (req as express.Request & { rawBody?: string }).rawBody = buf.toString('utf8');
202
+ },
203
+ })
204
+ );
205
+ app.use(express.json({ limit: '1mb' }));
206
+ // Tunisie → *.snapify.tn ; hors Tunisie → *.snapify.fr
207
+ app.use(geoRegionRedirect);
208
+ app.use((req, res, next) => {
209
+ // Redirections CMS uniquement sur le host principal
210
+ if (resolveAppHost(req) === 'cms') return redirectMiddleware(req, res, next);
211
+ next();
212
+ });
213
+
214
+ app.use('/uploads', express.static(path.resolve(__dirname, 'public/uploads'), { maxAge: '30d', immutable: true }));
215
+ app.use(express.static(path.resolve(__dirname, 'public'), { index: false }));
216
+
217
+ const publicQuota = rateLimit(
218
+ 'public',
219
+ async () => ({ max: (await loadSnapshot()).system.limits.publicApiPerMinute, windowMs: 60_000 }),
220
+ clientIp
221
+ );
222
+ app.use('/api/public', publicQuota);
223
+ app.use('/api/modules', publicQuota);
224
+ app.use('/api/community', publicQuota);
225
+ app.use('/api/marketplace', publicQuota);
226
+ app.use('/api/platform', publicQuota);
227
+ app.use('/api/superadmin', publicQuota);
228
+ app.use('/api/admin', adminIpGuard, auditMiddleware);
229
+
230
+ // Plateforme (community + marketplace) — disponibles sur tous les hosts
231
+ app.use('/api/platform/auth', createPlatformAuthRouter());
232
+ app.use('/api/platform/flouci', createFlouciWebhookRouter());
233
+ app.use('/api/platform', createLandingRouter());
234
+ app.use('/api/superadmin', createSuperadminRouter());
235
+ app.use('/api/community', createCommunityRouter());
236
+ const marketplaceRouter = createMarketplaceRouter();
237
+ app.use('/api/marketplace', marketplaceRouter);
238
+ app.use('/api/client', publicQuota);
239
+ app.use('/api/client', createClientPortalRouter());
240
+ app.use('/api/developers', publicQuota);
241
+ app.use(createSdkDownloadsRouter());
242
+
243
+ // SEO marketing snapify.tn / snapify.fr (prioritaire sur le sitemap CMS)
244
+ // Pièges GSC « Sitemap is HTML » : /sitemap et /sitemap.xml.gz → XML
245
+ app.get(['/sitemap', '/sitemap.xml.gz'], (req, res) => {
246
+ res.redirect(301, '/sitemap.xml');
247
+ });
248
+ app.get('/robots.txt', async (req, res, next) => {
249
+ if (resolveAppHost(req) !== 'cms') return next();
250
+ res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(await landingRobotsTxt(marketFromHost(requestHostHeader(req))));
251
+ });
252
+ // IndexNow key file: https://host/{key}.txt
253
+ app.get('/:key.txt', async (req, res, next) => {
254
+ if (resolveAppHost(req) !== 'cms') return next();
255
+ try {
256
+ const seo = await getLandingSeo();
257
+ const key = (seo.indexNowKey || '').trim();
258
+ if (!key || req.params.key !== key) return next();
259
+ res.type('text/plain').send(key);
260
+ } catch {
261
+ next();
262
+ }
263
+ });
264
+ app.get('/sitemap_index.xml', async (req, res, next) => {
265
+ if (resolveAppHost(req) !== 'cms') return next();
266
+ sendXml(res, await landingSitemapIndexXml(marketFromHost(requestHostHeader(req))));
267
+ });
268
+ app.get('/sitemap.xml', async (req, res, next) => {
269
+ if (resolveAppHost(req) !== 'cms') return next();
270
+ sendXml(res, await landingSitemapXml(marketFromHost(requestHostHeader(req))));
271
+ });
272
+ app.get('/sitemap-landing.xml', async (req, res) => {
273
+ // Alias rétrocompat → même contenu que sitemap.xml
274
+ sendXml(res, await landingSitemapXml(marketFromHost(requestHostHeader(req))));
275
+ });
276
+ app.get('/llms.txt', (_req, res) => {
277
+ res.type('text/plain').sendFile(path.resolve(__dirname, 'public/llms.txt'), (err) => {
278
+ if (err) res.status(404).type('text/plain').send('Not found');
279
+ });
280
+ });
281
+
282
+ // SEO marketplace / developers (hôtes dédiés — visible pour les crawlers & les devs)
283
+ app.get('/robots.txt', async (req, res, next) => {
284
+ const appHost = resolveAppHost(req);
285
+ if (appHost !== 'marketplace' && appHost !== 'developers' && appHost !== 'community') return next();
286
+ const host = requestHostHeader(req);
287
+ const proto = 'https';
288
+ const base = `${proto}://${host}`;
289
+ const lines = [
290
+ 'User-agent: *',
291
+ 'Allow: /',
292
+ 'Allow: /product/',
293
+ 'Allow: /docs',
294
+ 'Disallow: /api/',
295
+ 'Disallow: /developer/dashboard',
296
+ 'Disallow: /administrator',
297
+ '',
298
+ `Sitemap: ${base}/sitemap.xml`,
299
+ '',
300
+ ];
301
+ res.type('text/plain').set('Cache-Control', 'public, max-age=3600').send(lines.join('\n'));
302
+ });
303
+ app.get('/sitemap.xml', async (req, res, next) => {
304
+ const appHost = resolveAppHost(req);
305
+ if (appHost !== 'marketplace' && appHost !== 'developers' && appHost !== 'community') return next();
306
+ const host = requestHostHeader(req);
307
+ const base = `https://${host}`;
308
+ const today = new Date().toISOString().slice(0, 10);
309
+ const urls: { loc: string; priority: string }[] = [{ loc: `${base}/`, priority: '1.0' }];
310
+ if (appHost === 'marketplace') {
311
+ urls.push(
312
+ { loc: `${base}/catalog`, priority: '0.85' },
313
+ { loc: `${base}/developer`, priority: '0.8' }
314
+ );
315
+ try {
316
+ const products = await db.select().from(mpProducts).where(eqDb(mpProducts.status, 'published'));
317
+ for (const p of products.slice(0, 500)) {
318
+ urls.push({ loc: `${base}/product/${p.slug}`, priority: '0.7' });
319
+ }
320
+ } catch {
321
+ /* ignore */
322
+ }
323
+ }
324
+ if (appHost === 'developers') {
325
+ urls.push(
326
+ { loc: `${base}/docs/introduction`, priority: '0.9' },
327
+ { loc: `${base}/docs`, priority: '0.8' }
328
+ );
329
+ }
330
+ if (appHost === 'community') {
331
+ urls.push({ loc: `${base}/`, priority: '1.0' });
332
+ }
333
+ sendXml(
334
+ res,
335
+ `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${urls
336
+ .map(
337
+ (u) =>
338
+ ` <url><loc>${u.loc}</loc><lastmod>${today}</lastmod><changefreq>daily</changefreq><priority>${u.priority}</priority></url>`
339
+ )
340
+ .join('\n')}\n</urlset>`
341
+ );
342
+ });
343
+ // Alias catalogue CMS (MARKETPLACE_URL → …/api/catalog.json ou …/api/marketplace/catalog.json)
344
+ app.use('/api/catalog.json', (req, res, next) => {
345
+ req.url = '/catalog.json';
346
+ marketplaceRouter(req, res, next);
347
+ });
348
+
349
+ app.use(createModulesPublicRouter());
350
+ app.use('/api/public', createPublicCmsRouter());
351
+ app.use('/api/admin/google', createGoogleOAuthRouter());
352
+ app.use('/api/admin', createAdminAccountRouter());
353
+ app.use('/api/admin', createAdminUsersRouter());
354
+ app.use('/api/admin', createAdminSystemRouter());
355
+ app.use('/api/admin', createModulesAdminRouter());
356
+ app.use('/api/admin', createAdminCmsRouter());
357
+ app.use(createSeoPublicRouter());
358
+
359
+ app.use('/api', (_req, res) => res.status(404).json({ error: 'Route API inconnue' }));
360
+ app.use('/api', apiErrorHandler);
361
+
362
+ const isAdminPath = (p: string) => p === '/administrator' || p.startsWith('/administrator/');
363
+ const isSuperadminPath = (p: string) => p === '/hamdisup90' || p.startsWith('/hamdisup90/');
364
+ const isClientPath = (p: string) => p === '/espace-client' || p.startsWith('/espace-client/');
365
+ const isDemoPath = (p: string) => p === '/demo' || p.startsWith('/demo/');
366
+
367
+ const htmlForHost = (host: ReturnType<typeof resolveAppHost>, admin: boolean, p: string) => {
368
+ if (host === 'community') return 'community.html';
369
+ if (host === 'marketplace') return 'marketplace.html';
370
+ if (host === 'developers') return 'developers.html';
371
+ if (admin) return 'administrator.html';
372
+ if (host === 'cms' && isSuperadminPath(p)) return 'superadmin.html';
373
+ if (host === 'cms' && isClientPath(p)) return 'client.html';
374
+ if (isDemoPath(p)) return 'index.html';
375
+ return 'landing.html';
376
+ };
377
+
378
+ if (process.env.NODE_ENV === 'production') {
379
+ const distDir = path.resolve(__dirname, 'dist');
380
+ const htmlCache = new Map<string, { mtime: number; html: string }>();
381
+ const loadHtml = async (file: string) => {
382
+ const full = path.join(distDir, file);
383
+ try {
384
+ const { mtimeMs } = await fs.stat(full);
385
+ const cur = htmlCache.get(file);
386
+ if (!cur || mtimeMs !== cur.mtime) {
387
+ htmlCache.set(file, { mtime: mtimeMs, html: await fs.readFile(full, 'utf-8') });
388
+ }
389
+ } catch (e) {
390
+ if (!htmlCache.get(file)?.html) throw e;
391
+ }
392
+ return htmlCache.get(file)!.html;
393
+ };
394
+ await loadHtml('index.html').catch(() => undefined);
395
+ await loadHtml('landing.html').catch(() => undefined);
396
+ await loadHtml('superadmin.html').catch(() => undefined);
397
+ app.use('/assets', express.static(path.join(distDir, 'assets'), { maxAge: '1y', immutable: true }));
398
+ app.use('/assets', (_req, res) => res.status(404).end());
399
+ app.use(express.static(distDir, { index: false }));
400
+ app.get('*', async (req, res, next) => {
401
+ try {
402
+ const host = resolveAppHost(req);
403
+ if (host !== 'cms') {
404
+ res.setHeader('Cache-Control', 'no-cache');
405
+ const reqHost = requestHostHeader(req);
406
+ const country = countryForRequest(req, reqHost, resolveGeoFromIp(clientIp(req)).country || '');
407
+ let html = injectCountryScript(await loadHtml(htmlForHost(host, false, req.path)), country);
408
+ try {
409
+ const { ga4Id } = await getLandingSeo();
410
+ html = injectGa4(html, ga4Id);
411
+ } catch {
412
+ /* ignore */
413
+ }
414
+ return res.type('html').send(html);
415
+ }
416
+ if (isAdminPath(req.path)) {
417
+ res.setHeader('Cache-Control', 'no-store');
418
+ return res.sendFile(path.join(distDir, 'administrator.html'));
419
+ }
420
+ if (isSuperadminPath(req.path)) {
421
+ res.setHeader('Cache-Control', 'no-store');
422
+ return res.type('html').send(await loadHtml('superadmin.html'));
423
+ }
424
+ if (isClientPath(req.path)) {
425
+ res.setHeader('Cache-Control', 'no-store');
426
+ const reqHost = requestHostHeader(req);
427
+ const country = countryForRequest(req, reqHost, resolveGeoFromIp(clientIp(req)).country || '');
428
+ return res.type('html').send(injectCountryScript(await loadHtml('client.html'), country));
429
+ }
430
+ res.setHeader('Cache-Control', 'no-cache');
431
+ if (!isDemoPath(req.path)) {
432
+ const reqHost = requestHostHeader(req);
433
+ const country = countryForRequest(req, reqHost, resolveGeoFromIp(clientIp(req)).country || '');
434
+ return res
435
+ .type('html')
436
+ .send(await withLandingMarket(await loadHtml('landing.html'), country, reqHost, req.path || '/'));
437
+ }
438
+ // SEO/theme sur le chemin CMS interne (sans /demo)
439
+ const cmsPath = req.path === '/demo' || req.path === '/demo/' ? '/' : req.path.slice('/demo'.length) || '/';
440
+ const page = await injectMaintenance(req, await injectTheme(await injectSeo(await loadHtml('index.html'), cmsPath)));
441
+ if (page.notice) res.status(503).setHeader('Retry-After', '600');
442
+ res.type('html').send(page.html);
443
+ } catch (e) {
444
+ next(e);
445
+ }
446
+ });
447
+ } else {
448
+ const vite = await createViteServer({
449
+ server: { middlewareMode: true },
450
+ appType: 'custom',
451
+ });
452
+ app.use(vite.middlewares);
453
+ app.get('*', async (req, res, next) => {
454
+ try {
455
+ const host = resolveAppHost(req);
456
+ const admin = host === 'cms' && isAdminPath(req.path);
457
+ const file = htmlForHost(host, admin, req.path);
458
+ const raw = await fs.readFile(path.resolve(__dirname, file), 'utf-8');
459
+ let html = await vite.transformIndexHtml(req.originalUrl, raw);
460
+ const reqHost = requestHostHeader(req);
461
+ const geoCountry = countryForRequest(req, reqHost, resolveGeoFromIp(clientIp(req)).country || '');
462
+ if (file === 'landing.html') {
463
+ html = await withLandingMarket(html, geoCountry, reqHost, req.path || '/');
464
+ } else {
465
+ if (host === 'marketplace' || host === 'developers' || host === 'community' || file === 'client.html') {
466
+ html = injectCountryScript(html, geoCountry);
467
+ }
468
+ if (host !== 'cms' || (!admin && !isSuperadminPath(req.path) && !isDemoPath(req.path))) {
469
+ try {
470
+ html = injectGa4(html, (await getLandingSeo()).ga4Id);
471
+ } catch {
472
+ /* ignore */
473
+ }
474
+ }
475
+ }
476
+ if (host !== 'cms' || admin || isSuperadminPath(req.path) || !isDemoPath(req.path)) {
477
+ return res.status(200).type('html').send(html);
478
+ }
479
+ const cmsPath = req.path === '/demo' || req.path === '/demo/' ? '/' : req.path.slice('/demo'.length) || '/';
480
+ const page = await injectMaintenance(req, await injectTheme(await injectSeo(html, cmsPath)));
481
+ if (page.notice) res.setHeader('Retry-After', '600');
482
+ res.status(page.notice ? 503 : 200).type('html').send(page.html);
483
+ } catch (e) {
484
+ next(e);
485
+ }
486
+ });
487
+ }
488
+
489
+ void startDemoResetScheduler().catch((e) => console.error('[demo] scheduler', e));
490
+ startDailyImproveScheduler();
491
+
492
+ const server = app.listen(PORT, '0.0.0.0', () => {
493
+ console.log(`Serveur Snapify démarré sur http://0.0.0.0:${PORT}`);
494
+ });
495
+
496
+ const prune = () =>
497
+ runRetention()
498
+ .then((r) => {
499
+ const total = Object.values(r).reduce((a, b) => a + b, 0);
500
+ if (total) console.log('[rétention] purge quotidienne', r);
501
+ })
502
+ .catch((e) => console.error('[rétention] purge impossible', e));
503
+ void prune();
504
+ const pruneTimer = setInterval(prune, 24 * 3600 * 1000);
505
+ pruneTimer.unref();
506
+
507
+ const shutdown = (signal: string) => {
508
+ console.log(`[server] ${signal} reçu, arrêt en cours`);
509
+ clearInterval(pruneTimer);
510
+ server.close(() => {
511
+ closeDb().finally(() => process.exit(0));
512
+ });
513
+ setTimeout(() => process.exit(1), 10_000).unref();
514
+ };
515
+ process.once('SIGINT', () => shutdown('SIGINT'));
516
+ process.once('SIGTERM', () => shutdown('SIGTERM'));
517
+ }
518
+
519
+ startServer().catch((e) => {
520
+ console.error('[server] démarrage impossible', e);
521
+ process.exit(1);
522
+ });