create-nextblock 0.0.2 → 0.0.4
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.
- package/bin/create-nextblock.js +5 -9
- package/package.json +1 -1
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +8 -8
- package/templates/nextblock-template/app/globals.css +80 -45
- package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +18 -18
- package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +41 -17
- package/templates/nextblock-template/docs/files-structure.md +5 -5
- package/templates/nextblock-template/eslint.config.mjs +18 -11
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +15 -14
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/next.config.js +66 -99
- package/templates/nextblock-template/postcss.config.js +1 -1
- package/templates/nextblock-template/{middleware.ts → proxy.ts} +230 -206
- package/templates/nextblock-template/tailwind.config.js +26 -0
- package/templates/nextblock-template/tools/empty-module.js +1 -0
- package/templates/nextblock-template/tailwind.config.ts +0 -19
package/bin/create-nextblock.js
CHANGED
|
@@ -131,7 +131,7 @@ async function handleCommand(projectDirectory, options) {
|
|
|
131
131
|
console.log(chalk.green('Global styles configured.'));
|
|
132
132
|
|
|
133
133
|
await sanitizeTailwindConfig(projectDir);
|
|
134
|
-
console.log(chalk.green('tailwind.config.
|
|
134
|
+
console.log(chalk.green('tailwind.config.js sanitized.'));
|
|
135
135
|
|
|
136
136
|
await normalizeTsconfig(projectDir);
|
|
137
137
|
console.log(chalk.green('tsconfig.json normalized.'));
|
|
@@ -598,10 +598,9 @@ async function ensureEditorStyles(projectDir) {
|
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
async function sanitizeTailwindConfig(projectDir) {
|
|
601
|
-
const tailwindConfigPath = resolve(projectDir, 'tailwind.config.
|
|
602
|
-
const content =
|
|
603
|
-
|
|
604
|
-
const config = {
|
|
601
|
+
const tailwindConfigPath = resolve(projectDir, 'tailwind.config.js');
|
|
602
|
+
const content = `/** @type {import('tailwindcss').Config} */
|
|
603
|
+
module.exports = {
|
|
605
604
|
darkMode: ['class'],
|
|
606
605
|
content: [
|
|
607
606
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
@@ -693,9 +692,7 @@ const config = {
|
|
|
693
692
|
},
|
|
694
693
|
},
|
|
695
694
|
plugins: [require('tailwindcss-animate')],
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
export default config;
|
|
695
|
+
};
|
|
699
696
|
`;
|
|
700
697
|
|
|
701
698
|
await fs.writeFile(tailwindConfigPath, content);
|
|
@@ -994,4 +991,3 @@ function buildNextConfigContent(editorUtilNames) {
|
|
|
994
991
|
|
|
995
992
|
return lines.join('\n');
|
|
996
993
|
}
|
|
997
|
-
|
package/package.json
CHANGED
|
@@ -129,13 +129,13 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
129
129
|
debouncedSave(updatedBlock);
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
const DynamicTextBlockEditor = dynamic(() => import('../editors/TextBlockEditor
|
|
133
|
-
const DynamicHeadingBlockEditor = dynamic(() => import('../editors/HeadingBlockEditor
|
|
134
|
-
const DynamicImageBlockEditor = dynamic(() => import('../editors/ImageBlockEditor
|
|
135
|
-
const DynamicButtonBlockEditor = dynamic(() => import('../editors/ButtonBlockEditor
|
|
136
|
-
const DynamicPostsGridBlockEditor = dynamic(() => import('../editors/PostsGridBlockEditor
|
|
137
|
-
const DynamicVideoEmbedBlockEditor = dynamic(() => import('../editors/VideoEmbedBlockEditor
|
|
138
|
-
const DynamicSectionBlockEditor = dynamic(() => import('../editors/SectionBlockEditor
|
|
132
|
+
const DynamicTextBlockEditor = dynamic(() => import('../editors/TextBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
133
|
+
const DynamicHeadingBlockEditor = dynamic(() => import('../editors/HeadingBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
134
|
+
const DynamicImageBlockEditor = dynamic(() => import('../editors/ImageBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
135
|
+
const DynamicButtonBlockEditor = dynamic(() => import('../editors/ButtonBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
136
|
+
const DynamicPostsGridBlockEditor = dynamic(() => import('../editors/PostsGridBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
137
|
+
const DynamicVideoEmbedBlockEditor = dynamic(() => import('../editors/VideoEmbedBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
138
|
+
const DynamicSectionBlockEditor = dynamic(() => import('../editors/SectionBlockEditor').then(mod => mod.default), { loading: () => <p>Loading editor...</p> });
|
|
139
139
|
|
|
140
140
|
useEffect(() => {
|
|
141
141
|
if (editingNestedBlockInfo) {
|
|
@@ -444,7 +444,7 @@ export default function BlockEditorArea({ parentId, parentType, initialBlocks, l
|
|
|
444
444
|
onContentChange={handleContentChange}
|
|
445
445
|
onDelete={async (blockIdToDelete) => {
|
|
446
446
|
startTransition(async () => {
|
|
447
|
-
const result = await import("../actions
|
|
447
|
+
const result = await import("../actions").then(({ deleteBlock }) =>
|
|
448
448
|
deleteBlock(
|
|
449
449
|
blockIdToDelete,
|
|
450
450
|
parentType === "page" ? parentId : null,
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
@config "../../../../tailwind.config.js";
|
|
2
|
+
@import "tailwindcss/preflight";
|
|
3
|
+
@import "tailwindcss/theme";
|
|
4
|
+
@import "tailwindcss/utilities";
|
|
5
|
+
/*─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
globals.css — complete rewrite
|
|
7
|
+
─────────────────────────────────────────────────────────────────────────────*/
|
|
9
8
|
/*─────────────────────────────────────────────────────────────────────────────
|
|
10
9
|
1. Critical CSS Resets (formerly in <style id="critical-css">)
|
|
11
10
|
─────────────────────────────────────────────────────────────────────────────*/
|
|
12
|
-
@layer base {
|
|
11
|
+
@layer base {
|
|
13
12
|
*, ::before, ::after {
|
|
14
13
|
box-sizing: border-box;
|
|
15
14
|
border-width: 0;
|
|
@@ -194,67 +193,103 @@
|
|
|
194
193
|
───────────────────────────────────────────────────────────────────────────*/
|
|
195
194
|
/* Apply your border color everywhere */
|
|
196
195
|
* {
|
|
197
|
-
|
|
196
|
+
border-color: hsl(var(--border));
|
|
198
197
|
}
|
|
199
198
|
|
|
200
199
|
/* Body background & text */
|
|
201
200
|
body {
|
|
202
|
-
|
|
201
|
+
background-color: hsl(var(--background));
|
|
202
|
+
color: hsl(var(--foreground));
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/* Semantic headings */
|
|
206
|
-
ul,
|
|
207
|
-
|
|
206
|
+
ul,
|
|
207
|
+
ol {
|
|
208
|
+
padding-left: 1.5rem;
|
|
209
|
+
margin-bottom: 1rem;
|
|
208
210
|
}
|
|
209
211
|
li {
|
|
210
|
-
|
|
212
|
+
margin-bottom: 0.25rem;
|
|
211
213
|
}
|
|
212
214
|
blockquote {
|
|
213
|
-
|
|
215
|
+
padding: 1rem;
|
|
216
|
+
font-style: italic;
|
|
217
|
+
border-left-width: 4px;
|
|
218
|
+
border-left-style: solid;
|
|
219
|
+
border-left-color: hsl(var(--border));
|
|
220
|
+
background-color: hsl(var(--muted));
|
|
221
|
+
color: hsl(var(--muted-foreground));
|
|
222
|
+
margin-bottom: 1rem;
|
|
214
223
|
}
|
|
215
224
|
code {
|
|
216
|
-
|
|
225
|
+
background-color: hsl(var(--muted));
|
|
226
|
+
color: hsl(var(--muted-foreground));
|
|
227
|
+
padding: 0.25rem 0.25rem;
|
|
228
|
+
padding-top: 0.125rem;
|
|
229
|
+
padding-bottom: 0.125rem;
|
|
230
|
+
border-radius: 0.125rem;
|
|
231
|
+
font-family:
|
|
232
|
+
ui-monospace,
|
|
233
|
+
SFMono-Regular,
|
|
234
|
+
Menlo,
|
|
235
|
+
Monaco,
|
|
236
|
+
Consolas,
|
|
237
|
+
'Liberation Mono',
|
|
238
|
+
'Courier New',
|
|
239
|
+
monospace;
|
|
240
|
+
font-size: 0.875rem;
|
|
241
|
+
line-height: 1.25rem;
|
|
217
242
|
}
|
|
218
243
|
pre {
|
|
219
|
-
|
|
244
|
+
background-color: hsl(var(--muted));
|
|
245
|
+
padding: 1rem;
|
|
246
|
+
border-radius: 0.375rem;
|
|
247
|
+
overflow-x: auto;
|
|
248
|
+
margin-bottom: 1rem;
|
|
220
249
|
}
|
|
221
250
|
|
|
222
251
|
table {
|
|
223
|
-
|
|
252
|
+
width: 100%;
|
|
253
|
+
border-collapse: collapse;
|
|
254
|
+
margin-bottom: 1rem;
|
|
224
255
|
}
|
|
225
256
|
thead {
|
|
226
|
-
|
|
257
|
+
background-color: hsl(var(--muted));
|
|
227
258
|
}
|
|
228
|
-
th,
|
|
229
|
-
|
|
259
|
+
th,
|
|
260
|
+
td {
|
|
261
|
+
border: 1px solid hsl(var(--border));
|
|
262
|
+
padding: 0.5rem;
|
|
263
|
+
text-align: left;
|
|
230
264
|
}
|
|
231
265
|
th {
|
|
232
|
-
|
|
266
|
+
font-weight: 600;
|
|
233
267
|
}
|
|
234
268
|
hr {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
margin-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
margin-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
margin-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
269
|
+
border-top: 1px solid hsl(var(--border));
|
|
270
|
+
margin: 2rem 0;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* Image alignment preserved from editor output */
|
|
275
|
+
@layer components {
|
|
276
|
+
img[data-align='left'] {
|
|
277
|
+
display: block;
|
|
278
|
+
margin-left: 0;
|
|
279
|
+
margin-right: auto;
|
|
280
|
+
}
|
|
281
|
+
img[data-align='right'] {
|
|
282
|
+
display: block;
|
|
283
|
+
margin-left: auto;
|
|
284
|
+
margin-right: 0;
|
|
285
|
+
}
|
|
286
|
+
img[data-align='center'] {
|
|
287
|
+
display: block;
|
|
288
|
+
margin-left: auto;
|
|
289
|
+
margin-right: auto;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
258
293
|
/*─────────────────────────────────────────────────────────────────────────────
|
|
259
294
|
3. Theme color variables
|
|
260
295
|
─────────────────────────────────────────────────────────────────────────────*/
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
import Link from "next/link";
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Link from "next/link";
|
|
3
3
|
export type ButtonBlockContent = {
|
|
4
4
|
text?: string;
|
|
5
5
|
url?: string;
|
|
6
6
|
variant?: 'default' | 'outline' | 'secondary' | 'ghost' | 'link';
|
|
7
|
-
size?: 'default' | 'sm' | 'lg';
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
interface ButtonBlockRendererProps {
|
|
11
|
-
content: ButtonBlockContent;
|
|
12
|
-
languageId: number; // This prop seems unused
|
|
13
|
-
}
|
|
14
|
-
|
|
7
|
+
size?: 'default' | 'sm' | 'lg';
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
interface ButtonBlockRendererProps {
|
|
11
|
+
content: ButtonBlockContent;
|
|
12
|
+
languageId: number; // This prop seems unused
|
|
13
|
+
}
|
|
14
|
+
|
|
15
15
|
const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
|
|
16
16
|
content,
|
|
17
17
|
// languageId, // Unused
|
|
18
18
|
}) => {
|
|
19
19
|
const baseClasses =
|
|
20
|
-
"inline-flex items-center justify-center rounded-md
|
|
20
|
+
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50";
|
|
21
21
|
const variantClasses: Record<string, string> = {
|
|
22
22
|
default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
23
23
|
outline:
|
|
@@ -36,12 +36,12 @@ const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
|
|
|
36
36
|
const isExternal =
|
|
37
37
|
content.url?.startsWith("http") ||
|
|
38
38
|
content.url?.startsWith("mailto:") ||
|
|
39
|
-
content.url?.startsWith("tel:");
|
|
40
|
-
const isAnchor = content.url?.startsWith("#");
|
|
41
|
-
|
|
42
|
-
const buttonText = content.text || "Button";
|
|
43
|
-
const buttonVariant = content.variant || "default";
|
|
44
|
-
const buttonSize = content.size || "default";
|
|
39
|
+
content.url?.startsWith("tel:");
|
|
40
|
+
const isAnchor = content.url?.startsWith("#");
|
|
41
|
+
|
|
42
|
+
const buttonText = content.text || "Button";
|
|
43
|
+
const buttonVariant = content.variant || "default";
|
|
44
|
+
const buttonSize = content.size || "default";
|
|
45
45
|
|
|
46
46
|
return (
|
|
47
47
|
<div className="my-6 text-center">
|
|
@@ -88,5 +88,5 @@ const ButtonBlockRenderer: React.FC<ButtonBlockRendererProps> = ({
|
|
|
88
88
|
</div>
|
|
89
89
|
);
|
|
90
90
|
};
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
export default ButtonBlockRenderer;
|
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
// components/blocks/renderers/HeroBlockRenderer.tsx
|
|
2
|
-
import React from "react";
|
|
2
|
+
import React from "react";
|
|
3
3
|
import type { SectionBlockContent, Gradient } from "../../../lib/blocks/blockRegistry";
|
|
4
4
|
import Image from 'next/image';
|
|
5
5
|
|
|
6
|
-
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
6
|
+
const R2_BASE_URL = process.env.NEXT_PUBLIC_R2_BASE_URL || "";
|
|
7
|
+
const HERO_BACKGROUND_DEFAULT_QUALITY = 60;
|
|
8
|
+
|
|
9
|
+
function resolveImageQuality(value: unknown, fallback: number): number {
|
|
10
|
+
if (value === undefined || value === null || value === '') {
|
|
11
|
+
return fallback;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const numeric =
|
|
15
|
+
typeof value === 'number'
|
|
16
|
+
? value
|
|
17
|
+
: Number.parseInt(String(value), 10);
|
|
18
|
+
|
|
19
|
+
if (!Number.isFinite(numeric)) {
|
|
20
|
+
return fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const rounded = Math.round(numeric);
|
|
24
|
+
if (rounded < 1 || rounded > 100) {
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return rounded;
|
|
29
|
+
}
|
|
7
30
|
|
|
8
31
|
interface SectionBlockRendererProps {
|
|
9
32
|
content: SectionBlockContent;
|
|
@@ -169,13 +192,14 @@ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
|
|
|
169
192
|
content,
|
|
170
193
|
languageId,
|
|
171
194
|
}) => {
|
|
172
|
-
const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
|
|
173
|
-
|
|
174
|
-
const backgroundImage = content.background.type === 'image' ? content.background.image : undefined;
|
|
175
|
-
|
|
176
|
-
if (backgroundImage) {
|
|
177
|
-
delete styles.backgroundImage;
|
|
178
|
-
}
|
|
195
|
+
const { styles, className: backgroundClassName } = generateBackgroundStyles(content.background);
|
|
196
|
+
|
|
197
|
+
const backgroundImage = content.background.type === 'image' ? content.background.image : undefined;
|
|
198
|
+
|
|
199
|
+
if (backgroundImage) {
|
|
200
|
+
delete styles.backgroundImage;
|
|
201
|
+
}
|
|
202
|
+
const heroImageQuality = resolveImageQuality(backgroundImage?.quality, HERO_BACKGROUND_DEFAULT_QUALITY);
|
|
179
203
|
|
|
180
204
|
// Build CSS classes
|
|
181
205
|
const containerClass = containerClasses[content.container_type] || containerClasses.container;
|
|
@@ -205,13 +229,13 @@ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
|
|
|
205
229
|
objectFit: backgroundImage.size || 'cover',
|
|
206
230
|
objectPosition: backgroundImage.position || 'center'
|
|
207
231
|
}}
|
|
208
|
-
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 100vw, 100vw"
|
|
209
|
-
priority={true}
|
|
210
|
-
fetchPriority="high"
|
|
211
|
-
quality={
|
|
212
|
-
{...imageProps}
|
|
213
|
-
/>
|
|
214
|
-
)}
|
|
232
|
+
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 100vw, 100vw"
|
|
233
|
+
priority={true}
|
|
234
|
+
fetchPriority="high"
|
|
235
|
+
quality={heroImageQuality}
|
|
236
|
+
{...imageProps}
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
215
239
|
{backgroundImage?.overlay?.gradient && (
|
|
216
240
|
<div
|
|
217
241
|
className="absolute inset-0"
|
|
@@ -237,4 +261,4 @@ const HeroBlockRenderer: React.FC<SectionBlockRendererProps> = ({
|
|
|
237
261
|
);
|
|
238
262
|
};
|
|
239
263
|
|
|
240
|
-
export default HeroBlockRenderer;
|
|
264
|
+
export default HeroBlockRenderer;
|
|
@@ -253,7 +253,7 @@
|
|
|
253
253
|
│ └── blocks
|
|
254
254
|
│ │ ├── README.md
|
|
255
255
|
│ │ └── blockRegistry.ts
|
|
256
|
-
│ ├──
|
|
256
|
+
│ ├── proxy.ts
|
|
257
257
|
│ ├── next-env.d.ts
|
|
258
258
|
│ ├── next.config.js
|
|
259
259
|
│ ├── postcss.config.js
|
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
│ ├── backfill-image-meta.ts
|
|
265
265
|
│ ├── backup.js
|
|
266
266
|
│ └── test-bundle-optimization.js
|
|
267
|
-
│ ├── tailwind.config.
|
|
267
|
+
│ ├── tailwind.config.js
|
|
268
268
|
│ └── tsconfig.json
|
|
269
269
|
├── components.json
|
|
270
270
|
├── docs
|
|
@@ -273,7 +273,7 @@
|
|
|
273
273
|
├── block-editor-analysis.md
|
|
274
274
|
├── inline-cta-widget-design.md
|
|
275
275
|
├── inline-widget-design.md
|
|
276
|
-
└── monorepo-
|
|
276
|
+
└── monorepo-architecture.md
|
|
277
277
|
├── eslint.config.mjs
|
|
278
278
|
├── libs
|
|
279
279
|
├── db
|
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
│ │ ├── lib
|
|
286
286
|
│ │ │ └── supabase
|
|
287
287
|
│ │ │ │ ├── client.ts
|
|
288
|
-
│ │ │ │ ├──
|
|
288
|
+
│ │ │ │ ├── proxy.ts
|
|
289
289
|
│ │ │ │ ├── server.ts
|
|
290
290
|
│ │ │ │ ├── ssg-client.ts
|
|
291
291
|
│ │ │ │ └── types.ts
|
|
@@ -422,5 +422,5 @@
|
|
|
422
422
|
├── package-lock.json
|
|
423
423
|
├── package.json
|
|
424
424
|
├── project.json
|
|
425
|
-
├── tailwind.config.
|
|
425
|
+
├── tailwind.config.js
|
|
426
426
|
└── tsconfig.base.json
|
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
import { FlatCompat } from '@eslint/eslintrc';
|
|
2
|
-
import { dirname } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import js from '@eslint/js';
|
|
5
|
-
import { fixupConfigRules } from '@eslint/compat';
|
|
6
1
|
import nx from '@nx/eslint-plugin';
|
|
7
2
|
import baseConfig from '../../eslint.config.mjs';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
import nextPlugin from '@next/eslint-plugin-next';
|
|
4
|
+
|
|
5
|
+
const nextRules = {
|
|
6
|
+
...nextPlugin.configs.recommended.rules,
|
|
7
|
+
...nextPlugin.configs['core-web-vitals'].rules,
|
|
8
|
+
};
|
|
12
9
|
|
|
13
10
|
const config = [
|
|
14
|
-
...fixupConfigRules(compat.extends('next')),
|
|
15
|
-
...fixupConfigRules(compat.extends('next/core-web-vitals')),
|
|
16
11
|
...baseConfig,
|
|
17
12
|
...nx.configs['flat/react-typescript'],
|
|
18
13
|
{
|
|
19
14
|
ignores: ['.next/**/*', '**/next-env.d.ts', 'apps/nextblock/next-env.d.ts'],
|
|
20
15
|
},
|
|
21
16
|
{
|
|
17
|
+
files: [
|
|
18
|
+
'**/*.ts',
|
|
19
|
+
'**/*.tsx',
|
|
20
|
+
'**/*.js',
|
|
21
|
+
'**/*.jsx',
|
|
22
|
+
'**/*.mjs',
|
|
23
|
+
'**/*.cjs',
|
|
24
|
+
],
|
|
25
|
+
plugins: {
|
|
26
|
+
'@next/next': nextPlugin,
|
|
27
|
+
},
|
|
22
28
|
rules: {
|
|
29
|
+
...nextRules,
|
|
23
30
|
'@next/next/no-html-link-for-pages': ['error', 'apps/nextblock/app'],
|
|
24
31
|
},
|
|
25
32
|
},
|
|
@@ -117,20 +117,21 @@ export interface SectionBlockContent {
|
|
|
117
117
|
solid_color?: string;
|
|
118
118
|
min_height?: string;
|
|
119
119
|
gradient?: Gradient;
|
|
120
|
-
image?: {
|
|
121
|
-
media_id: string;
|
|
122
|
-
object_key: string;
|
|
123
|
-
alt_text?: string;
|
|
124
|
-
width?: number;
|
|
125
|
-
height?: number;
|
|
126
|
-
blur_data_url?: string;
|
|
127
|
-
size: 'cover' | 'contain';
|
|
128
|
-
position: 'center' | 'top' | 'bottom' | 'left' | 'right';
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
120
|
+
image?: {
|
|
121
|
+
media_id: string;
|
|
122
|
+
object_key: string;
|
|
123
|
+
alt_text?: string;
|
|
124
|
+
width?: number;
|
|
125
|
+
height?: number;
|
|
126
|
+
blur_data_url?: string;
|
|
127
|
+
size: 'cover' | 'contain';
|
|
128
|
+
position: 'center' | 'top' | 'bottom' | 'left' | 'right';
|
|
129
|
+
quality?: number | null;
|
|
130
|
+
overlay?: {
|
|
131
|
+
type: 'gradient';
|
|
132
|
+
gradient: Gradient;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
134
135
|
};
|
|
135
136
|
/** Responsive column configuration */
|
|
136
137
|
responsive_columns: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
|
|
3
|
+
import "./.next/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
|
@@ -1,99 +1,66 @@
|
|
|
1
|
-
//@ts-check
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const nextConfig = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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);
|
|
1
|
+
//@ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {{ protocol?: 'http' | 'https'; hostname: string; port?: string; pathname?: string }} RemotePattern
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** @type {import('next').NextConfig} */
|
|
8
|
+
const nextConfig = {
|
|
9
|
+
env: {
|
|
10
|
+
NEXT_PUBLIC_SUPABASE_URL: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
|
11
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
|
12
|
+
},
|
|
13
|
+
images: {
|
|
14
|
+
formats: ['image/avif', 'image/webp'],
|
|
15
|
+
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384, 512],
|
|
16
|
+
deviceSizes: [320, 480, 640, 750, 828, 1080, 1200, 1440, 1920, 2048, 2560],
|
|
17
|
+
qualities: [60, 75],
|
|
18
|
+
minimumCacheTTL: 31_536_000,
|
|
19
|
+
dangerouslyAllowSVG: false,
|
|
20
|
+
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
|
21
|
+
remotePatterns: getRemotePatterns(),
|
|
22
|
+
},
|
|
23
|
+
compiler: {
|
|
24
|
+
removeConsole: process.env.NODE_ENV === 'production',
|
|
25
|
+
},
|
|
26
|
+
transpilePackages: [
|
|
27
|
+
'@nextblock-cms/utils',
|
|
28
|
+
'@nextblock-cms/ui',
|
|
29
|
+
'@nextblock-cms/editor',
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
module.exports = nextConfig;
|
|
34
|
+
|
|
35
|
+
function getRemotePatterns() {
|
|
36
|
+
/** @type {RemotePattern[]} */
|
|
37
|
+
const patterns = [
|
|
38
|
+
{
|
|
39
|
+
protocol: 'https',
|
|
40
|
+
hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev',
|
|
41
|
+
pathname: '/**',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
protocol: 'https',
|
|
45
|
+
hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com',
|
|
46
|
+
pathname: '/**',
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
if (process.env.NEXT_PUBLIC_URL) {
|
|
51
|
+
try {
|
|
52
|
+
const parsed = new URL(process.env.NEXT_PUBLIC_URL);
|
|
53
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
54
|
+
patterns.push({
|
|
55
|
+
protocol: parsed.protocol === 'https:' ? 'https' : 'http',
|
|
56
|
+
hostname: parsed.hostname,
|
|
57
|
+
pathname: '/**',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// ignore malformed value
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return patterns;
|
|
66
|
+
}
|
|
@@ -1,206 +1,230 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
'/cms': ['
|
|
15
|
-
'/cms/
|
|
16
|
-
'/cms/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
1
|
+
import { createServerClient, type CookieOptions } from '@supabase/ssr';
|
|
2
|
+
import { NextResponse, type NextRequest } from 'next/server';
|
|
3
|
+
import type { Database } from '@nextblock-cms/db';
|
|
4
|
+
|
|
5
|
+
type Profile = Database['public']['Tables']['profiles']['Row'];
|
|
6
|
+
type UserRole = Database['public']['Enums']['user_role'];
|
|
7
|
+
|
|
8
|
+
const LANGUAGE_COOKIE_KEY = 'NEXT_USER_LOCALE';
|
|
9
|
+
const DEFAULT_LOCALE = 'en';
|
|
10
|
+
const SUPPORTED_LOCALES = ['en', 'fr'];
|
|
11
|
+
|
|
12
|
+
const cmsRoutePermissions: Record<string, UserRole[]> = {
|
|
13
|
+
'/cms': ['WRITER', 'ADMIN'],
|
|
14
|
+
'/cms/admin': ['ADMIN'],
|
|
15
|
+
'/cms/users': ['ADMIN'],
|
|
16
|
+
'/cms/settings': ['ADMIN'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getRequiredRolesForPath(pathname: string): UserRole[] | null {
|
|
20
|
+
const sortedPaths = Object.keys(cmsRoutePermissions).sort(
|
|
21
|
+
(a, b) => b.length - a.length,
|
|
22
|
+
);
|
|
23
|
+
for (const specificPath of sortedPaths) {
|
|
24
|
+
if (
|
|
25
|
+
pathname === specificPath ||
|
|
26
|
+
pathname.startsWith(specificPath + (specificPath === '/' ? '' : '/'))
|
|
27
|
+
) {
|
|
28
|
+
return cmsRoutePermissions[specificPath];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default async function proxy(request: NextRequest) {
|
|
35
|
+
const requestHeaders = new Headers(request.headers);
|
|
36
|
+
const nonce = crypto.randomUUID();
|
|
37
|
+
requestHeaders.set('x-nonce', nonce);
|
|
38
|
+
|
|
39
|
+
let response = NextResponse.next({
|
|
40
|
+
request: {
|
|
41
|
+
headers: requestHeaders,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
46
|
+
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
47
|
+
|
|
48
|
+
if (!supabaseUrl || !supabaseAnonKey) {
|
|
49
|
+
throw new Error('Missing required Supabase environment variables');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
|
|
53
|
+
cookies: {
|
|
54
|
+
get(name: string) {
|
|
55
|
+
return request.cookies.get(name)?.value;
|
|
56
|
+
},
|
|
57
|
+
set(name: string, value: string, options: CookieOptions) {
|
|
58
|
+
request.cookies.set({ name, value, ...options });
|
|
59
|
+
response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
60
|
+
response.cookies.set({ name, value, ...options });
|
|
61
|
+
},
|
|
62
|
+
remove(name: string, options: CookieOptions) {
|
|
63
|
+
request.cookies.set({ name, value: '', ...options });
|
|
64
|
+
response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
65
|
+
response.cookies.set({ name, value: '', ...options });
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
await supabase.auth.getSession();
|
|
71
|
+
|
|
72
|
+
const cookieLocale = request.cookies.get(LANGUAGE_COOKIE_KEY)?.value;
|
|
73
|
+
let currentLocale = cookieLocale;
|
|
74
|
+
|
|
75
|
+
if (!currentLocale || !SUPPORTED_LOCALES.includes(currentLocale)) {
|
|
76
|
+
currentLocale = DEFAULT_LOCALE;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
requestHeaders.set('X-User-Locale', currentLocale);
|
|
80
|
+
|
|
81
|
+
const {
|
|
82
|
+
data: { user },
|
|
83
|
+
error: userError,
|
|
84
|
+
} = await supabase.auth.getUser();
|
|
85
|
+
const { pathname } = request.nextUrl;
|
|
86
|
+
|
|
87
|
+
if (pathname.startsWith('/cms')) {
|
|
88
|
+
if (userError || !user) {
|
|
89
|
+
return NextResponse.redirect(
|
|
90
|
+
new URL(`/sign-in?redirect=${pathname}`, request.url),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const requiredRoles = getRequiredRolesForPath(pathname);
|
|
95
|
+
|
|
96
|
+
if (requiredRoles && requiredRoles.length > 0) {
|
|
97
|
+
const {
|
|
98
|
+
data: profile,
|
|
99
|
+
error: profileError,
|
|
100
|
+
} = await supabase
|
|
101
|
+
.from('profiles')
|
|
102
|
+
.select('role')
|
|
103
|
+
.eq('id', user.id)
|
|
104
|
+
.single<Pick<Profile, 'role'>>();
|
|
105
|
+
|
|
106
|
+
if (profileError || !profile) {
|
|
107
|
+
console.error(
|
|
108
|
+
`Proxy: Profile error for user ${user.id} accessing ${pathname}. Error: ${profileError?.message}. Redirecting to unauthorized.`,
|
|
109
|
+
);
|
|
110
|
+
return NextResponse.redirect(
|
|
111
|
+
new URL('/unauthorized?error=profile_issue', request.url),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const userRole = profile.role as UserRole;
|
|
116
|
+
if (!requiredRoles.includes(userRole)) {
|
|
117
|
+
console.warn(
|
|
118
|
+
`Proxy: User ${user.id} (Role: ${userRole}) denied access to ${pathname}. Required: ${requiredRoles.join(' OR ')}. Redirecting to unauthorized.`,
|
|
119
|
+
);
|
|
120
|
+
return NextResponse.redirect(
|
|
121
|
+
new URL(
|
|
122
|
+
`/unauthorized?path=${pathname}&required=${requiredRoles.join(',')}`,
|
|
123
|
+
request.url,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (response.headers.get('location')) {
|
|
131
|
+
return response;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const finalResponse = NextResponse.next({
|
|
135
|
+
request: {
|
|
136
|
+
headers: requestHeaders,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
response.cookies.getAll().forEach((cookie) => {
|
|
141
|
+
finalResponse.cookies.set(cookie.name, cookie.value, cookie);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (request.cookies.get(LANGUAGE_COOKIE_KEY)?.value !== currentLocale) {
|
|
145
|
+
finalResponse.cookies.set(LANGUAGE_COOKIE_KEY, currentLocale, {
|
|
146
|
+
path: '/',
|
|
147
|
+
maxAge: 31_536_000,
|
|
148
|
+
sameSite: 'lax',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (
|
|
153
|
+
pathname === '/sign-in' ||
|
|
154
|
+
pathname === '/sign-up' ||
|
|
155
|
+
pathname === '/forgot-password'
|
|
156
|
+
) {
|
|
157
|
+
finalResponse.headers.set('X-Page-Type', 'auth');
|
|
158
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'critical');
|
|
159
|
+
} else if (pathname === '/') {
|
|
160
|
+
finalResponse.headers.set('X-Page-Type', 'home');
|
|
161
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'high');
|
|
162
|
+
} else if (pathname === '/blog') {
|
|
163
|
+
finalResponse.headers.set('X-Page-Type', 'blog-index');
|
|
164
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'high');
|
|
165
|
+
} else if (pathname.startsWith('/blog/')) {
|
|
166
|
+
finalResponse.headers.set('X-Page-Type', 'blog-post');
|
|
167
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'medium');
|
|
168
|
+
} else {
|
|
169
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
170
|
+
if (segments.length === 1 && !pathname.startsWith('/cms')) {
|
|
171
|
+
finalResponse.headers.set('X-Page-Type', 'dynamic-page');
|
|
172
|
+
finalResponse.headers.set('X-Prefetch-Priority', 'medium');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const acceptHeader = request.headers.get('accept');
|
|
177
|
+
if (acceptHeader && acceptHeader.includes('text/html') && !pathname.startsWith('/api/')) {
|
|
178
|
+
finalResponse.headers.set('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
179
|
+
finalResponse.headers.set('X-BFCache-Applied', 'true');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
finalResponse.headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains; preload');
|
|
183
|
+
finalResponse.headers.set('X-Frame-Options', 'SAMEORIGIN');
|
|
184
|
+
finalResponse.headers.set('X-Content-Type-Options', 'nosniff');
|
|
185
|
+
finalResponse.headers.set('Referrer-Policy', 'origin-when-cross-origin');
|
|
186
|
+
finalResponse.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
187
|
+
finalResponse.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
|
|
188
|
+
|
|
189
|
+
if (process.env.NODE_ENV === 'production') {
|
|
190
|
+
const nonceValue = requestHeaders.get('x-nonce');
|
|
191
|
+
if (nonceValue) {
|
|
192
|
+
const csp = [
|
|
193
|
+
"default-src 'self'",
|
|
194
|
+
`script-src 'self' blob: data: 'nonce-${nonceValue}'`,
|
|
195
|
+
"style-src 'self' 'unsafe-inline'",
|
|
196
|
+
"img-src 'self' data: blob: https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
|
|
197
|
+
"font-src 'self'",
|
|
198
|
+
"object-src 'none'",
|
|
199
|
+
"connect-src 'self' https://ppcppwsfnrptznvbxnsz.supabase.co wss://ppcppwsfnrptznvbxnsz.supabase.co https://nrh-next-cms.e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com https://pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev",
|
|
200
|
+
"frame-src 'self' blob: data: https://www.youtube.com",
|
|
201
|
+
"form-action 'self'",
|
|
202
|
+
"base-uri 'self'",
|
|
203
|
+
].join('; ');
|
|
204
|
+
|
|
205
|
+
finalResponse.headers.set('Content-Security-Policy', csp);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const responseForLogging = finalResponse.clone();
|
|
210
|
+
const cacheStatus = responseForLogging.headers.get('x-vercel-cache') || 'none';
|
|
211
|
+
|
|
212
|
+
if (!pathname.startsWith('/api/')) {
|
|
213
|
+
console.log(
|
|
214
|
+
JSON.stringify({
|
|
215
|
+
type: 'cache',
|
|
216
|
+
status: cacheStatus,
|
|
217
|
+
path: pathname,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return finalResponse;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const config = {
|
|
226
|
+
matcher: [
|
|
227
|
+
'/((?!_next/static|_next/image|favicon.ico|auth/.*|sign-in|sign-up|forgot-password|unauthorized|api/auth/.*|api/revalidate|api/revalidate-log).*)',
|
|
228
|
+
'/cms/:path*',
|
|
229
|
+
],
|
|
230
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const { existsSync } = require('fs');
|
|
2
|
+
const { join } = require('path');
|
|
3
|
+
|
|
4
|
+
/** @type {import('tailwindcss').Config} */
|
|
5
|
+
module.exports = {
|
|
6
|
+
presets: [require('../../tailwind.config.js')],
|
|
7
|
+
content: (() => {
|
|
8
|
+
const projectGlobs = [
|
|
9
|
+
join(
|
|
10
|
+
__dirname,
|
|
11
|
+
'{src,pages,components,app,lib}/**/*!(*.stories|*.spec).{ts,tsx,js,jsx,md,mdx,html}'
|
|
12
|
+
),
|
|
13
|
+
];
|
|
14
|
+
const libsDir = join(__dirname, '../../libs');
|
|
15
|
+
if (existsSync(libsDir)) {
|
|
16
|
+
projectGlobs.push(
|
|
17
|
+
join(libsDir, '**/*.{ts,tsx,js,jsx,md,mdx,html}')
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return projectGlobs;
|
|
21
|
+
})(),
|
|
22
|
+
theme: {
|
|
23
|
+
extend: {},
|
|
24
|
+
},
|
|
25
|
+
plugins: [],
|
|
26
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = {};
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
// File: apps/nextblock/tailwind.config.ts
|
|
2
|
-
const { createGlobPatternsForDependencies } = require('@nx/react/tailwind');
|
|
3
|
-
const { join } = require('path');
|
|
4
|
-
|
|
5
|
-
/** @type {import('tailwindcss').Config} */
|
|
6
|
-
module.exports = {
|
|
7
|
-
presets: [require('../../tailwind.config.ts')],
|
|
8
|
-
content: [
|
|
9
|
-
join(
|
|
10
|
-
__dirname,
|
|
11
|
-
'{src,pages,components,app}/**/*!(*.stories|*.spec).{ts,tsx,html}'
|
|
12
|
-
),
|
|
13
|
-
...createGlobPatternsForDependencies(__dirname),
|
|
14
|
-
],
|
|
15
|
-
theme: {
|
|
16
|
-
extend: {},
|
|
17
|
-
},
|
|
18
|
-
plugins: [],
|
|
19
|
-
};
|