create-nextblock 0.0.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 (206) hide show
  1. package/bin/create-nextblock.js +997 -0
  2. package/package.json +25 -0
  3. package/scripts/sync-template.js +284 -0
  4. package/templates/nextblock-template/.env.example +37 -0
  5. package/templates/nextblock-template/.swcrc +30 -0
  6. package/templates/nextblock-template/README.md +194 -0
  7. package/templates/nextblock-template/app/(auth-pages)/forgot-password/page.tsx +57 -0
  8. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -0
  9. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +28 -0
  10. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +67 -0
  11. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +70 -0
  12. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -0
  13. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +147 -0
  14. package/templates/nextblock-template/app/[slug]/page.tsx +145 -0
  15. package/templates/nextblock-template/app/[slug]/page.utils.ts +183 -0
  16. package/templates/nextblock-template/app/actions/email.ts +31 -0
  17. package/templates/nextblock-template/app/actions/formActions.ts +65 -0
  18. package/templates/nextblock-template/app/actions/languageActions.ts +130 -0
  19. package/templates/nextblock-template/app/actions/postActions.ts +80 -0
  20. package/templates/nextblock-template/app/actions.ts +146 -0
  21. package/templates/nextblock-template/app/api/process-image/route.ts +210 -0
  22. package/templates/nextblock-template/app/api/revalidate/route.ts +86 -0
  23. package/templates/nextblock-template/app/api/revalidate-log/route.ts +23 -0
  24. package/templates/nextblock-template/app/api/upload/presigned-url/route.ts +106 -0
  25. package/templates/nextblock-template/app/api/upload/proxy/route.ts +84 -0
  26. package/templates/nextblock-template/app/auth/callback/route.ts +58 -0
  27. package/templates/nextblock-template/app/blog/[slug]/PostClientContent.tsx +169 -0
  28. package/templates/nextblock-template/app/blog/[slug]/page.tsx +177 -0
  29. package/templates/nextblock-template/app/blog/[slug]/page.utils.ts +136 -0
  30. package/templates/nextblock-template/app/blog/page.tsx +77 -0
  31. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +321 -0
  32. package/templates/nextblock-template/app/cms/blocks/actions.ts +434 -0
  33. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +348 -0
  34. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +567 -0
  35. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +98 -0
  36. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +58 -0
  37. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +62 -0
  38. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +276 -0
  39. package/templates/nextblock-template/app/cms/blocks/components/DeleteBlockButtonClient.tsx +47 -0
  40. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +182 -0
  41. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +120 -0
  42. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +133 -0
  43. package/templates/nextblock-template/app/cms/blocks/components/SortableBlockItem.tsx +46 -0
  44. package/templates/nextblock-template/app/cms/blocks/editors/ButtonBlockEditor.tsx +85 -0
  45. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +182 -0
  46. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +111 -0
  47. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +150 -0
  48. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +79 -0
  49. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +337 -0
  50. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +81 -0
  51. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +64 -0
  52. package/templates/nextblock-template/app/cms/components/ConfirmationModal.tsx +51 -0
  53. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +145 -0
  54. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +203 -0
  55. package/templates/nextblock-template/app/cms/components/LanguageFilterSelect.tsx +69 -0
  56. package/templates/nextblock-template/app/cms/dashboard/page.tsx +247 -0
  57. package/templates/nextblock-template/app/cms/layout.tsx +10 -0
  58. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -0
  59. package/templates/nextblock-template/app/cms/media/[id]/edit/page.tsx +80 -0
  60. package/templates/nextblock-template/app/cms/media/actions.ts +577 -0
  61. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +53 -0
  62. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -0
  63. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -0
  64. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +157 -0
  65. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +275 -0
  66. package/templates/nextblock-template/app/cms/media/components/MediaImage.tsx +70 -0
  67. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +195 -0
  68. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +362 -0
  69. package/templates/nextblock-template/app/cms/media/page.tsx +120 -0
  70. package/templates/nextblock-template/app/cms/navigation/[id]/edit/page.tsx +101 -0
  71. package/templates/nextblock-template/app/cms/navigation/actions.ts +358 -0
  72. package/templates/nextblock-template/app/cms/navigation/components/DeleteNavItemButton.tsx +52 -0
  73. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +248 -0
  74. package/templates/nextblock-template/app/cms/navigation/components/NavigationLanguageSwitcher.tsx +132 -0
  75. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +701 -0
  76. package/templates/nextblock-template/app/cms/navigation/components/SortableNavItem.tsx +98 -0
  77. package/templates/nextblock-template/app/cms/navigation/new/page.tsx +26 -0
  78. package/templates/nextblock-template/app/cms/navigation/page.tsx +102 -0
  79. package/templates/nextblock-template/app/cms/navigation/utils.ts +51 -0
  80. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +121 -0
  81. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -0
  82. package/templates/nextblock-template/app/cms/pages/actions.ts +241 -0
  83. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +47 -0
  84. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +253 -0
  85. package/templates/nextblock-template/app/cms/pages/new/page.tsx +52 -0
  86. package/templates/nextblock-template/app/cms/pages/page.tsx +232 -0
  87. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +183 -0
  88. package/templates/nextblock-template/app/cms/posts/actions.ts +309 -0
  89. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +55 -0
  90. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +419 -0
  91. package/templates/nextblock-template/app/cms/posts/new/page.tsx +21 -0
  92. package/templates/nextblock-template/app/cms/posts/page.tsx +192 -0
  93. package/templates/nextblock-template/app/cms/revisions/JsonDiffView.tsx +86 -0
  94. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +201 -0
  95. package/templates/nextblock-template/app/cms/revisions/actions.ts +84 -0
  96. package/templates/nextblock-template/app/cms/revisions/service.ts +344 -0
  97. package/templates/nextblock-template/app/cms/revisions/utils.ts +127 -0
  98. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +68 -0
  99. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +78 -0
  100. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +32 -0
  101. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +117 -0
  102. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +216 -0
  103. package/templates/nextblock-template/app/cms/settings/languages/[id]/edit/page.tsx +77 -0
  104. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +261 -0
  105. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +76 -0
  106. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +167 -0
  107. package/templates/nextblock-template/app/cms/settings/languages/new/page.tsx +34 -0
  108. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +156 -0
  109. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +19 -0
  110. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +114 -0
  111. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +177 -0
  112. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +11 -0
  113. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +118 -0
  114. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -0
  115. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +91 -0
  116. package/templates/nextblock-template/app/cms/users/actions.ts +156 -0
  117. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +71 -0
  118. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +138 -0
  119. package/templates/nextblock-template/app/cms/users/page.tsx +183 -0
  120. package/templates/nextblock-template/app/favicon.ico +0 -0
  121. package/templates/nextblock-template/app/globals.css +401 -0
  122. package/templates/nextblock-template/app/layout.tsx +191 -0
  123. package/templates/nextblock-template/app/lib/sitemap-utils.ts +68 -0
  124. package/templates/nextblock-template/app/page.tsx +109 -0
  125. package/templates/nextblock-template/app/providers.tsx +43 -0
  126. package/templates/nextblock-template/app/robots.txt/route.ts +19 -0
  127. package/templates/nextblock-template/app/sitemap.xml/route.ts +63 -0
  128. package/templates/nextblock-template/app/unauthorized/page.tsx +27 -0
  129. package/templates/nextblock-template/backup/backup_2025-06-19.sql +8057 -0
  130. package/templates/nextblock-template/backup/backup_2025-06-20.sql +8159 -0
  131. package/templates/nextblock-template/backup/backup_2025-07-08.sql +8411 -0
  132. package/templates/nextblock-template/backup/backup_2025-07-09.sql +8442 -0
  133. package/templates/nextblock-template/backup/backup_2025-07-10.sql +8442 -0
  134. package/templates/nextblock-template/backup/backup_2025-10-01.sql +8803 -0
  135. package/templates/nextblock-template/backup/backup_2025-10-02.sql +9749 -0
  136. package/templates/nextblock-template/components/BlockRenderer.tsx +119 -0
  137. package/templates/nextblock-template/components/FooterNavigation.tsx +33 -0
  138. package/templates/nextblock-template/components/Header.tsx +42 -0
  139. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -0
  140. package/templates/nextblock-template/components/LanguageSwitcher.tsx +103 -0
  141. package/templates/nextblock-template/components/ResponsiveNav.tsx +372 -0
  142. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +17 -0
  143. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +93 -0
  144. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +180 -0
  145. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -0
  146. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +69 -0
  147. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +98 -0
  148. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +41 -0
  149. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +240 -0
  150. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +79 -0
  151. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +33 -0
  152. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +189 -0
  153. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +31 -0
  154. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +59 -0
  155. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +51 -0
  156. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +40 -0
  157. package/templates/nextblock-template/components/blocks/types.ts +8 -0
  158. package/templates/nextblock-template/components/env-var-warning.tsx +33 -0
  159. package/templates/nextblock-template/components/form-message.tsx +26 -0
  160. package/templates/nextblock-template/components/header-auth.tsx +71 -0
  161. package/templates/nextblock-template/components/submit-button.tsx +23 -0
  162. package/templates/nextblock-template/components/theme-switcher.tsx +78 -0
  163. package/templates/nextblock-template/context/AuthContext.tsx +138 -0
  164. package/templates/nextblock-template/context/CurrentContentContext.tsx +42 -0
  165. package/templates/nextblock-template/context/LanguageContext.tsx +206 -0
  166. package/templates/nextblock-template/docs/cms-application-overview.md +56 -0
  167. package/templates/nextblock-template/docs/cms-architecture-overview.md +73 -0
  168. package/templates/nextblock-template/docs/files-structure.md +426 -0
  169. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +174 -0
  170. package/templates/nextblock-template/eslint.config.mjs +28 -0
  171. package/templates/nextblock-template/index.d.ts +5 -0
  172. package/templates/nextblock-template/lib/blocks/README.md +670 -0
  173. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +1001 -0
  174. package/templates/nextblock-template/lib/ui/ColorPicker.ts +1 -0
  175. package/templates/nextblock-template/lib/ui/ConfirmationDialog.ts +1 -0
  176. package/templates/nextblock-template/lib/ui/CustomSelectWithInput.ts +1 -0
  177. package/templates/nextblock-template/lib/ui/Skeleton.ts +1 -0
  178. package/templates/nextblock-template/lib/ui/avatar.ts +1 -0
  179. package/templates/nextblock-template/lib/ui/badge.ts +1 -0
  180. package/templates/nextblock-template/lib/ui/button.ts +1 -0
  181. package/templates/nextblock-template/lib/ui/card.ts +1 -0
  182. package/templates/nextblock-template/lib/ui/checkbox.ts +1 -0
  183. package/templates/nextblock-template/lib/ui/dialog.ts +1 -0
  184. package/templates/nextblock-template/lib/ui/dropdown-menu.ts +1 -0
  185. package/templates/nextblock-template/lib/ui/input.ts +1 -0
  186. package/templates/nextblock-template/lib/ui/label.ts +1 -0
  187. package/templates/nextblock-template/lib/ui/popover.ts +1 -0
  188. package/templates/nextblock-template/lib/ui/progress.ts +1 -0
  189. package/templates/nextblock-template/lib/ui/select.ts +1 -0
  190. package/templates/nextblock-template/lib/ui/separator.ts +1 -0
  191. package/templates/nextblock-template/lib/ui/table.ts +1 -0
  192. package/templates/nextblock-template/lib/ui/textarea.ts +1 -0
  193. package/templates/nextblock-template/lib/ui/tooltip.ts +1 -0
  194. package/templates/nextblock-template/lib/ui/ui.ts +1 -0
  195. package/templates/nextblock-template/middleware.ts +206 -0
  196. package/templates/nextblock-template/next-env.d.ts +6 -0
  197. package/templates/nextblock-template/next.config.js +99 -0
  198. package/templates/nextblock-template/package.json +52 -0
  199. package/templates/nextblock-template/postcss.config.js +6 -0
  200. package/templates/nextblock-template/project.json +7 -0
  201. package/templates/nextblock-template/public/.gitkeep +0 -0
  202. package/templates/nextblock-template/scripts/backfill-image-meta.ts +149 -0
  203. package/templates/nextblock-template/scripts/backup.js +53 -0
  204. package/templates/nextblock-template/scripts/test-bundle-optimization.js +114 -0
  205. package/templates/nextblock-template/tailwind.config.ts +19 -0
  206. package/templates/nextblock-template/tsconfig.json +62 -0
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1 @@
1
+ export * from '@nextblock-cms/ui';
@@ -0,0 +1,206 @@
1
+ // middleware.ts
2
+ import { createServerClient, type CookieOptions } from '@supabase/ssr';
3
+ import { NextResponse, type NextRequest } from 'next/server';
4
+ import type { Database } from "@nextblock-cms/db";
5
+
6
+ type Profile = Database["public"]["Tables"]["profiles"]["Row"];
7
+ type UserRole = Database["public"]["Enums"]["user_role"];
8
+
9
+ const LANGUAGE_COOKIE_KEY = 'NEXT_USER_LOCALE';
10
+ const DEFAULT_LOCALE = 'en';
11
+ const SUPPORTED_LOCALES = ['en', 'fr']; // Keep this in sync with DB or make dynamic
12
+
13
+ const cmsRoutePermissions: Record<string, UserRole[]> = {
14
+ '/cms': ['WRITER', 'ADMIN'],
15
+ '/cms/admin': ['ADMIN'],
16
+ '/cms/users': ['ADMIN'],
17
+ '/cms/settings': ['ADMIN'],
18
+ };
19
+
20
+ function getRequiredRolesForPath(pathname: string): UserRole[] | null {
21
+ const sortedPaths = Object.keys(cmsRoutePermissions).sort((a, b) => b.length - a.length);
22
+ for (const specificPath of sortedPaths) {
23
+ if (pathname === specificPath || pathname.startsWith(specificPath + (specificPath === '/' ? '' : '/'))) {
24
+ return cmsRoutePermissions[specificPath];
25
+ }
26
+ }
27
+ return null;
28
+ }
29
+
30
+ export async function middleware(request: NextRequest) {
31
+ const requestHeaders = new Headers(request.headers);
32
+ const nonce = crypto.randomUUID();
33
+ requestHeaders.set('x-nonce', nonce);
34
+
35
+ let response = NextResponse.next({
36
+ request: {
37
+ headers: requestHeaders,
38
+ },
39
+ });
40
+
41
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
42
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
43
+
44
+ if (!supabaseUrl || !supabaseAnonKey) {
45
+ throw new Error('Missing required Supabase environment variables');
46
+ }
47
+
48
+ const supabase = createServerClient(
49
+ supabaseUrl,
50
+ supabaseAnonKey,
51
+ {
52
+ cookies: {
53
+ get(name: string) { return request.cookies.get(name)?.value; },
54
+ set(name: string, value: string, options: CookieOptions) {
55
+ request.cookies.set({ name, value, ...options });
56
+ response = NextResponse.next({ request: { headers: requestHeaders } });
57
+ response.cookies.set({ name, value, ...options });
58
+ },
59
+ remove(name: string, options: CookieOptions) {
60
+ request.cookies.set({ name, value: '', ...options });
61
+ response = NextResponse.next({ request: { headers: requestHeaders } });
62
+ response.cookies.set({ name, value: '', ...options });
63
+ },
64
+ },
65
+ }
66
+ );
67
+
68
+ await supabase.auth.getSession();
69
+
70
+ const cookieLocale = request.cookies.get(LANGUAGE_COOKIE_KEY)?.value;
71
+ let currentLocale = cookieLocale;
72
+
73
+ if (!currentLocale || !SUPPORTED_LOCALES.includes(currentLocale)) {
74
+ currentLocale = DEFAULT_LOCALE;
75
+ }
76
+
77
+ requestHeaders.set('X-User-Locale', currentLocale);
78
+
79
+ const { data: { user }, error: userError } = await supabase.auth.getUser(); // Use getUser for revalidation
80
+ const { pathname } = request.nextUrl;
81
+
82
+ // CMS route protection
83
+ if (pathname.startsWith('/cms')) { // Ensure this check is broad enough for all CMS paths
84
+ if (userError || !user) { // Check for error or no user
85
+ return NextResponse.redirect(new URL(`/sign-in?redirect=${pathname}`, request.url));
86
+ }
87
+
88
+ const requiredRoles = getRequiredRolesForPath(pathname);
89
+
90
+ if (requiredRoles && requiredRoles.length > 0) {
91
+ const { data: profile, error: profileError } = await supabase
92
+ .from('profiles')
93
+ .select('role')
94
+ .eq('id', user.id)
95
+ .single<Pick<Profile, 'role'>>();
96
+
97
+ if (profileError || !profile) {
98
+ console.error(`Middleware: Profile error for user ${user.id} accessing ${pathname}. Error: ${profileError?.message}. Redirecting to unauthorized.`);
99
+ return NextResponse.redirect(new URL('/unauthorized?error=profile_issue', request.url));
100
+ }
101
+
102
+ const userRole = profile.role as UserRole;
103
+ if (!requiredRoles.includes(userRole)) {
104
+ console.warn(`Middleware: User ${user.id} (Role: ${userRole}) denied access to ${pathname}. Required: ${requiredRoles.join(' OR ')}. Redirecting to unauthorized.`);
105
+ return NextResponse.redirect(new URL(`/unauthorized?path=${pathname}&required=${requiredRoles.join(',')}`, request.url));
106
+ }
107
+ }
108
+ }
109
+
110
+ if (response.headers.get('location')) {
111
+ return response;
112
+ }
113
+
114
+ const finalResponse = NextResponse.next({
115
+ request: {
116
+ headers: requestHeaders,
117
+ },
118
+ });
119
+
120
+ response.cookies.getAll().forEach(cookie => {
121
+ finalResponse.cookies.set(cookie.name, cookie.value, cookie);
122
+ });
123
+
124
+ if (request.cookies.get(LANGUAGE_COOKIE_KEY)?.value !== currentLocale) {
125
+ finalResponse.cookies.set(LANGUAGE_COOKIE_KEY, currentLocale, { path: '/', maxAge: 31536000, sameSite: 'lax' });
126
+ }
127
+
128
+ if (pathname === '/sign-in' || pathname === '/sign-up' || pathname === '/forgot-password') {
129
+ finalResponse.headers.set('X-Page-Type', 'auth');
130
+ finalResponse.headers.set('X-Prefetch-Priority', 'critical');
131
+ } else if (pathname === '/') {
132
+ finalResponse.headers.set('X-Page-Type', 'home');
133
+ finalResponse.headers.set('X-Prefetch-Priority', 'high');
134
+ } else if (pathname === '/blog') {
135
+ finalResponse.headers.set('X-Page-Type', 'blog-index');
136
+ finalResponse.headers.set('X-Prefetch-Priority', 'high');
137
+ } else if (pathname.startsWith('/blog/')) {
138
+ finalResponse.headers.set('X-Page-Type', 'blog-post');
139
+ finalResponse.headers.set('X-Prefetch-Priority', 'medium');
140
+ } else {
141
+ const segments = pathname.split('/').filter(Boolean);
142
+ if (segments.length === 1 && !pathname.startsWith('/cms')) {
143
+ finalResponse.headers.set('X-Page-Type', 'dynamic-page');
144
+ finalResponse.headers.set('X-Prefetch-Priority', 'medium');
145
+ }
146
+ }
147
+
148
+ const acceptHeader = request.headers.get('accept');
149
+ if (
150
+ acceptHeader &&
151
+ acceptHeader.includes('text/html') &&
152
+ !pathname.startsWith('/api/')
153
+ ) {
154
+ finalResponse.headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
155
+ finalResponse.headers.set('X-BFCache-Applied', 'true');
156
+ }
157
+
158
+ finalResponse.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
159
+ finalResponse.headers.set('X-Frame-Options', 'SAMEORIGIN');
160
+ finalResponse.headers.set('X-Content-Type-Options', 'nosniff');
161
+ finalResponse.headers.set('Referrer-Policy', 'origin-when-cross-origin');
162
+ finalResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
163
+ finalResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
164
+
165
+ // Only send nonce-based CSP in production. In development, Next.js
166
+ // Dev Overlay injects inline scripts without a nonce, which would be blocked.
167
+ if (process.env.NODE_ENV === 'production') {
168
+ const nonceValue = requestHeaders.get('x-nonce');
169
+ if (nonceValue) {
170
+ const csp = [
171
+ "default-src 'self'",
172
+ `script-src 'self' blob: data: 'nonce-${nonceValue}'`,
173
+ "style-src 'self' 'unsafe-inline'",
174
+ "img-src 'self' data: blob: https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
175
+ "font-src 'self'",
176
+ "object-src 'none'",
177
+ "connect-src 'self' https://ppcppwsfnrptznvbxnsz.supabase.co wss://ppcppwsfnrptznvbxnsz.supabase.co https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
178
+ "frame-src 'self' blob: data: https://www.youtube.com",
179
+ "form-action 'self'",
180
+ "base-uri 'self'",
181
+ ].join('; ');
182
+
183
+ finalResponse.headers.set('Content-Security-Policy', csp);
184
+ }
185
+ }
186
+
187
+ const responseForLogging = finalResponse.clone();
188
+ const cacheStatus = responseForLogging.headers.get('x-vercel-cache') || 'none';
189
+
190
+ if (!pathname.startsWith('/api/')) {
191
+ console.log(JSON.stringify({
192
+ type: 'cache',
193
+ status: cacheStatus,
194
+ path: pathname,
195
+ }));
196
+ }
197
+
198
+ return finalResponse;
199
+ }
200
+
201
+ export const config = {
202
+ matcher: [
203
+ '/((?!_next/static|_next/image|favicon.ico|auth/.*|sign-in|sign-up|forgot-password|unauthorized|api/auth/.*|api/revalidate|api/revalidate-log).*)',
204
+ '/cms/:path*',
205
+ ],
206
+ };
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ /// <reference path="./../../dist/apps/nextblock/.next/types/routes.d.ts" />
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,99 @@
1
+ //@ts-check
2
+
3
+ const { composePlugins, withNx } = require('@nx/next');
4
+
5
+ /**
6
+ * @type {import('@nx/next/plugins/with-nx').WithNxOptions}
7
+ **/
8
+ const nextConfig = {
9
+ // Use this to set Nx-specific options
10
+ // See: https://nx.dev/recipes/next/next-config-setup
11
+ nx: {svgr: false},
12
+ env: {
13
+ NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
14
+ NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
15
+ },
16
+ images: {
17
+ formats: ['image/avif', 'image/webp'],
18
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],
19
+ deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],
20
+ minimumCacheTTL: 31536000,
21
+ dangerouslyAllowSVG: false,
22
+ contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
23
+ remotePatterns: [
24
+ {
25
+ protocol: 'https',
26
+ hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev',
27
+ },
28
+ {
29
+ protocol: 'https',
30
+ hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com',
31
+ },
32
+ // Add other necessary hostnames, for example, from NEXT_PUBLIC_URL if it's different
33
+ // and used for images. This example assumes NEXT_PUBLIC_R2_BASE_URL's hostname is the one above.
34
+ // If NEXT_PUBLIC_URL is also an image source and has a different hostname:
35
+ ...(process.env.NEXT_PUBLIC_URL
36
+ ? [
37
+ {
38
+ protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),
39
+ hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,
40
+ },
41
+ ]
42
+ : []),
43
+ ],
44
+ },
45
+ experimental: {
46
+ optimizeCss: true,
47
+ cssChunking: 'strict',
48
+ },
49
+ transpilePackages: ['@nextblock-cms/utils', '@nextblock-cms/ui', '@nextblock-cms/editor'],
50
+ webpack: (config, { isServer }) => {
51
+ if (!isServer) {
52
+ config.module.rules.push({
53
+ test: /\.svg$/i,
54
+ issuer: /\.[jt]sx?$/,
55
+ use: ['@svgr/webpack'],
56
+ });
57
+ // Optimize TipTap bundle separation for client-side
58
+ config.optimization = {
59
+ ...config.optimization,
60
+ splitChunks: {
61
+ ...config.optimization.splitChunks,
62
+ cacheGroups: {
63
+ ...config.optimization.splitChunks?.cacheGroups,
64
+ // Create a separate chunk for TipTap and related dependencies
65
+ tiptap: {
66
+ test: /[\\/]node_modules[\\/](@tiptap|prosemirror)[\\/]/,
67
+ name: 'tiptap',
68
+ chunks: 'async', // Only include in async chunks (dynamic imports)
69
+ priority: 30,
70
+ reuseExistingChunk: true,
71
+ },
72
+ // Separate chunk for TipTap extensions and custom components
73
+ tiptapExtensions: {
74
+ test: /[\\/](tiptap-extensions|RichTextEditor|MenuBar|MediaLibraryModal)[\\/]/,
75
+ name: 'tiptap-extensions',
76
+ chunks: 'async',
77
+ priority: 25,
78
+ reuseExistingChunk: true,
79
+ },
80
+ },
81
+ },
82
+ };
83
+ }
84
+ return config;
85
+ },
86
+ turbopack: {
87
+ // Turbopack-specific options can be placed here if needed in the future
88
+ },
89
+ compiler: {
90
+ removeConsole: process.env.NODE_ENV === 'production',
91
+ }
92
+ };
93
+
94
+ const plugins = [
95
+ // Add more Next.js plugins to this list if needed.
96
+ withNx,
97
+ ];
98
+
99
+ module.exports = composePlugins(...plugins)(nextConfig);
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@nextblock-cms/template",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "next lint"
10
+ },
11
+ "dependencies": {
12
+ "@aws-sdk/client-s3": "^3.810.0",
13
+ "@dnd-kit/core": "^6.3.1",
14
+ "@dnd-kit/sortable": "^10.0.0",
15
+ "@dnd-kit/utilities": "^3.2.2",
16
+ "@supabase/ssr": "^0.6.1",
17
+ "@supabase/supabase-js": "^2.47.2",
18
+ "@tiptap/react": "^3.3.0",
19
+ "date-fns": "^3.6.0",
20
+ "dotenv": "^16.5.0",
21
+ "fast-json-patch": "^3.1.1",
22
+ "html-react-parser": "^5.2.6",
23
+ "js-cookie": "^3.0.5",
24
+ "lodash.debounce": "^4.0.8",
25
+ "lucide-react": "^0.534.0",
26
+ "next": "^15.5.4",
27
+ "nodemailer": "^7.0.4",
28
+ "plaiceholder": "^3.0.0",
29
+ "react": "19.0.0",
30
+ "react-dom": "19.0.0",
31
+ "react-hot-toast": "^2.4.1",
32
+ "sharp": "^0.34.2",
33
+ "uuid": "^10.0.0",
34
+ "zod": "^3.25.76",
35
+ "@nextblock-cms/ui": "workspace:*",
36
+ "@nextblock-cms/utils": "workspace:*",
37
+ "@nextblock-cms/db": "workspace:*",
38
+ "@nextblock-cms/editor": "workspace:*",
39
+ "@nextblock-cms/sdk": "workspace:*"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "22.10.2",
43
+ "@types/react": "^19.0.0",
44
+ "@types/react-dom": "19.0.2",
45
+ "autoprefixer": "^10.4.13",
46
+ "eslint": "^9.8.0",
47
+ "eslint-config-next": "^15.5.4",
48
+ "postcss": "^8.4.38",
49
+ "tailwindcss": "^3.4.3",
50
+ "typescript": "~5.8.2"
51
+ }
52
+ }
@@ -0,0 +1,6 @@
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "nextblock-template",
3
+ "projectType": "application",
4
+ "root": "apps/create-nextblock/templates/nextblock-template",
5
+ "sourceRoot": "apps/create-nextblock/templates/nextblock-template",
6
+ "targets": {}
7
+ }
File without changes
@@ -0,0 +1,149 @@
1
+ // scripts/backfill-image-meta.ts
2
+ import { createClient } from '@supabase/supabase-js';
3
+ import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
4
+ import sharp from 'sharp';
5
+ import { Readable } from 'stream';
6
+ import { getPlaiceholder } from 'plaiceholder';
7
+ import 'dotenv/config';
8
+ import * as dotenv from 'dotenv';
9
+
10
+ dotenv.config({ path: '.env.local' });
11
+
12
+ // Helper to convert stream to buffer
13
+ async function streamToBuffer(stream: Readable): Promise<Buffer> {
14
+ return new Promise((resolve, reject) => {
15
+ const chunks: Buffer[] = [];
16
+ stream.on('data', (chunk) => chunks.push(chunk as Buffer));
17
+ stream.on('error', reject);
18
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
19
+ });
20
+ }
21
+
22
+ const R2_BUCKET_NAME = process.env.R2_BUCKET_NAME;
23
+ const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL;
24
+ const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY;
25
+ const MAX_WIDTH = 2560;
26
+
27
+ async function backfillImageMeta() {
28
+ const {
29
+ R2_ACCOUNT_ID,
30
+ R2_ACCESS_KEY_ID,
31
+ R2_SECRET_ACCESS_KEY,
32
+ R2_BUCKET_NAME: Bucket,
33
+ } = process.env;
34
+
35
+ if (
36
+ !R2_ACCOUNT_ID ||
37
+ !R2_ACCESS_KEY_ID ||
38
+ !R2_SECRET_ACCESS_KEY ||
39
+ !Bucket
40
+ ) {
41
+ console.error('Cloudflare R2 environment variables are not fully set.');
42
+ process.exit(1);
43
+ }
44
+ if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) {
45
+ console.error('Supabase environment variables are not set.');
46
+ process.exit(1);
47
+ }
48
+
49
+ const s3Client = new S3Client({
50
+ region: 'auto',
51
+ endpoint: `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
52
+ credentials: {
53
+ accessKeyId: R2_ACCESS_KEY_ID,
54
+ secretAccessKey: R2_SECRET_ACCESS_KEY,
55
+ },
56
+ });
57
+
58
+ const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
59
+
60
+ // Fetch all media records
61
+ const { data: mediaItems, error } = await supabase.from('media').select('id, object_key, width');
62
+
63
+ if (error) {
64
+ console.error('Error fetching media items:', error);
65
+ return;
66
+ }
67
+
68
+ if (!mediaItems || mediaItems.length === 0) {
69
+ console.log('No images to backfill.');
70
+ return;
71
+ }
72
+
73
+ console.log(`Found ${mediaItems.length} images to process.`);
74
+
75
+ for (const item of mediaItems) {
76
+ try {
77
+ if (item.width && item.width > MAX_WIDTH) {
78
+ console.log(`Resizing image ${item.id} (${item.object_key}) from ${item.width}px wide.`);
79
+
80
+ // 1. Download the image from R2
81
+ const getObjectParams = {
82
+ Bucket: R2_BUCKET_NAME,
83
+ Key: item.object_key,
84
+ };
85
+ const getObjectResponse = await s3Client.send(new GetObjectCommand(getObjectParams));
86
+
87
+ if (!getObjectResponse.Body) {
88
+ throw new Error('Failed to retrieve image from R2: Empty body.');
89
+ }
90
+
91
+ const imageBuffer = await streamToBuffer(getObjectResponse.Body as Readable);
92
+
93
+ // 2. Resize the image
94
+ const resizedImageBuffer = await sharp(imageBuffer)
95
+ .resize({
96
+ width: MAX_WIDTH,
97
+ withoutEnlargement: true,
98
+ })
99
+ .toBuffer();
100
+
101
+ // 3. Upload the resized image back to R2, overwriting the original
102
+ const putObjectParams = {
103
+ Bucket: R2_BUCKET_NAME,
104
+ Key: item.object_key,
105
+ Body: resizedImageBuffer,
106
+ ContentType: getObjectResponse.ContentType,
107
+ };
108
+ await s3Client.send(new PutObjectCommand(putObjectParams));
109
+
110
+ // 4. Get new metadata from the resized image
111
+ const sharpInstance = sharp(resizedImageBuffer);
112
+ const metadata = await sharpInstance.metadata();
113
+ const { base64: blurDataURL } = await getPlaiceholder(resizedImageBuffer, { size: 10 });
114
+
115
+ const { width, height } = metadata;
116
+
117
+ if (!width || !height) {
118
+ console.warn(`Could not extract new width/height for ${item.object_key}. Skipping update.`);
119
+ continue;
120
+ }
121
+
122
+ // 5. Update the record in the media table
123
+ const { error: updateError } = await supabase
124
+ .from('media')
125
+ .update({
126
+ width,
127
+ height,
128
+ blur_data_url: blurDataURL,
129
+ updated_at: new Date().toISOString(),
130
+ })
131
+ .eq('id', item.id);
132
+
133
+ if (updateError) {
134
+ console.error(`Failed to update item ${item.id}:`, updateError);
135
+ } else {
136
+ console.log(`Successfully resized and updated item ${item.id}.`);
137
+ }
138
+ } else {
139
+ console.log(`Skipping image ${item.id} (${item.object_key}) - not oversized.`);
140
+ }
141
+ } catch (e: unknown) {
142
+ console.error(`An error occurred while processing item ${item.id}:`, e instanceof Error ? e.message : String(e));
143
+ }
144
+ }
145
+
146
+ console.log('Backfill complete.');
147
+ }
148
+
149
+ backfillImageMeta();
@@ -0,0 +1,53 @@
1
+ const { exec } = require('child_process');
2
+ const { URL } = require('url');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ require('dotenv').config({ path: '.env.local' });
6
+
7
+ // Backup directory
8
+ const backupDir = path.join(__dirname, '..', 'backup');
9
+
10
+ // Create backup directory if it doesn't exist
11
+ if (!fs.existsSync(backupDir)) {
12
+ fs.mkdirSync(backupDir);
13
+ }
14
+
15
+ // Backup file name
16
+ const backupFile = path.join(backupDir, `backup_${new Date().toISOString().slice(0, 10)}.sql`);
17
+
18
+ // Function to perform the backup
19
+ const backupDatabase = () => {
20
+ const connectionString = process.env.POSTGRES_URL;
21
+
22
+ if (!connectionString) {
23
+ console.error('Error: Missing POSTGRES_URL in .env.local file.');
24
+ return;
25
+ }
26
+
27
+ // Parse the URL and remove non-standard parameters for pg_dump
28
+ const url = new URL(connectionString);
29
+ url.searchParams.delete('supa'); // Remove supa parameter
30
+ const finalConnectionString = url.toString();
31
+
32
+ const command = `pg_dump "${finalConnectionString}" > "${backupFile}"`;
33
+
34
+ console.log('Starting database backup...');
35
+
36
+ exec(command, (error, stdout, stderr) => {
37
+ if (error) {
38
+ console.error(`Error executing backup: ${error.message}`);
39
+ if (stderr) {
40
+ console.error(`pg_dump error: ${stderr}`);
41
+ }
42
+ return;
43
+ }
44
+ if (stderr && stderr.toLowerCase().includes('error')) {
45
+ console.error(`Error: ${stderr}`);
46
+ return;
47
+ }
48
+ console.log(`Backup successful! File saved as ${backupFile}`);
49
+ });
50
+ };
51
+
52
+ // Run the backup
53
+ backupDatabase();