create-nextblock 0.12.16 → 0.13.2
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/package.json +1 -1
- package/templates/nextblock-template/app/actions/interactions.ts +27 -4
- package/templates/nextblock-template/app/api/ai/global-agent/route.ts +287 -48
- package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +238 -209
- package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +103 -8
- package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +31 -2
- package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +37 -15
- package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +26 -15
- package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +123 -46
- package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +8 -1
- package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +62 -22
- package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +40 -2
- package/templates/nextblock-template/app/cms/interactions/EmailRecipientsInput.tsx +189 -0
- package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +138 -71
- package/templates/nextblock-template/app/cms/media/import-external-image.ts +289 -0
- package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +13 -10
- package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +14 -3
- package/templates/nextblock-template/app/cms/pages/actions.ts +59 -6
- package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +21 -11
- package/templates/nextblock-template/app/cms/posts/actions.ts +45 -0
- package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +11 -9
- package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +463 -227
- package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +220 -1
- package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +11 -0
- package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +20 -1
- package/templates/nextblock-template/app/cms/users/actions.ts +69 -0
- package/templates/nextblock-template/app/cms/users/components/CreateUserForm.tsx +217 -0
- package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +4 -1
- package/templates/nextblock-template/app/cms/users/new/page.tsx +44 -0
- package/templates/nextblock-template/app/cms/users/page.tsx +12 -3
- package/templates/nextblock-template/app/lib/homepage.ts +36 -0
- package/templates/nextblock-template/app/lib/sitemap-utils.ts +13 -6
- package/templates/nextblock-template/app/page.tsx +55 -12
- package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +56 -0
- package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +60 -30
- package/templates/nextblock-template/components/blocks/renderers/StockPhotoCredit.tsx +167 -0
- package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +94 -6
- package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +7 -1
- package/templates/nextblock-template/lib/blocks/blockRegistry.ts +29 -3
- package/templates/nextblock-template/lib/search/server.ts +11 -1
- package/templates/nextblock-template/lib/setup/migrations-bundle.ts +82 -72
- package/templates/nextblock-template/next-env.d.ts +1 -1
- package/templates/nextblock-template/package.json +1 -1
- package/templates/nextblock-template/proxy.ts +5 -0
- package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
|
@@ -64,7 +64,7 @@ Known incomplete or future work:
|
|
|
64
64
|
- Footer link updates currently replace footer links for the selected locale. Footer append mode is not yet implemented.
|
|
65
65
|
- Documentation search is keyword/scored search over `posts` and `pages`, not a vector embedding RAG system yet.
|
|
66
66
|
- The sandbox should eventually seed a visible product/package item for Cortex AI, similar to ecommerce. The preferred image asset is `apps/nextblock/public/images/cortex-ai-square.webp`.
|
|
67
|
-
- Block insertion is
|
|
67
|
+
- Block insertion, creation (`create_cms_page/post/product`), deletion (`delete_cms_item`), multi-step plans (`execute_cms_action_plan`), direct typed DB CRUD, external URL ingestion (`fetch_url_content`), and whole-page rewrites staged into Live Draft Mode (`rewrite_page_draft`) are all implemented (this "future work" note is stale — see the tool inventory in `createCortexGlobalAgentTools` and the "External URL Ingestion and Live-Draft Page Rewrites" section below). Per-block mutations still write directly to live `blocks` via service role (no draft/snapshot); only `rewrite_page_draft` goes through `content_drafts`.
|
|
68
68
|
|
|
69
69
|
## Important Files
|
|
70
70
|
|
|
@@ -909,10 +909,15 @@ Model orchestration:
|
|
|
909
909
|
|
|
910
910
|
- Uses `streamText`.
|
|
911
911
|
- Uses `buildCortexAiRoutingPolicy`.
|
|
912
|
-
- Uses `stepCountIs(6)
|
|
912
|
+
- Uses `stepCountIs(8)` (raised from 6 to allow read -> plan -> build/confirm multi-tool sequences such as rewriting a full page).
|
|
913
913
|
- Temperature is `0.1`.
|
|
914
|
-
- Max output tokens is `2000
|
|
915
|
-
- Per-
|
|
914
|
+
- Max output tokens is `4000` (raised from 2000; this is a per-step cap that also counts reasoning/tool-argument tokens, so a low value could starve the post-tool summary step and produce empty text).
|
|
915
|
+
- Per-attempt timeout is **idle-based** (`GLOBAL_AGENT_MODEL_IDLE_TIMEOUT_MS = 60000`): the attempt aborts only after 60s with no stream activity, and the timer resets on every stream part. A slow-but-progressing generation is not killed mid-answer.
|
|
916
|
+
|
|
917
|
+
Read-only tool summaries:
|
|
918
|
+
|
|
919
|
+
- After a successful `read_current_cms_item` or `search_documentation`, if the model emits no follow-up text, the route now returns a **deterministic, truthful summary built from the tool output** (`summarizeReadCurrentCmsItemOutput` / `summarizeSearchDocumentationOutput`) instead of the old canned "the model was interrupted before it could finish a summary" line. Read tools have no side effects, so the answer never depends on the model narrating them.
|
|
920
|
+
- `looksLikeRawToolCallLeak` only flags **structural** markers (`<toolcall>`/`<tool_call>`/`<function_call>` wrappers, or a JSON object carrying both `"name"`/`"tool"` and `"arguments"`). It no longer discards legitimate prose that merely quotes a tool name or the bare word "arguments".
|
|
916
921
|
|
|
917
922
|
System prompt:
|
|
918
923
|
|
|
@@ -983,6 +988,89 @@ This was added after a real issue where:
|
|
|
983
988
|
|
|
984
989
|
The current implementation treats the DB tool result as the source of truth once a mutation succeeds.
|
|
985
990
|
|
|
991
|
+
## Section Design Intelligence
|
|
992
|
+
|
|
993
|
+
Section blocks are the layout primitive for multi-section pages (heroes, landing/marketing pages). The strict `section` schema requires every layout field, so cheap models used to either fail validation or emit bland sections. Two mechanisms now make section authoring reliable:
|
|
994
|
+
|
|
995
|
+
1. **Server-side section normalizer** (`normalizeSectionContent` in `libs/cortex/src/lib/ai-global-agent-tools.ts`). Runs on every create/insert of a `section` block (via `normalizeBlockContentForType`). It:
|
|
996
|
+
- Fills all required layout fields with sensible defaults: `container_type` `container`, `column_gap` `lg`, `padding` `{top:'xl',bottom:'xl'}`, `vertical_alignment` `center` for heroes / `start` otherwise.
|
|
997
|
+
- Keeps the grid in sync: `responsive_columns.desktop` is derived from the number of columns actually provided in `column_blocks` (clamped 1-4), so the grid never has empty trailing tracks or overflowing cells.
|
|
998
|
+
- Completes background intent: a bare `{type:'gradient'}` gets real color stops; a `theme` background without a theme defaults to `muted`; an `image` background without a real `media_id` is downgraded to `none` (the AI cannot invent media).
|
|
999
|
+
- Deep-normalizes and validates each nested column block (also fixes a prior bug where nested blocks were only shallow-validated on CREATE).
|
|
1000
|
+
- Tolerates a model that flattens columns into a single list (`[blockA, blockB]`) by treating them as one column.
|
|
1001
|
+
|
|
1002
|
+
Net effect: a model can emit a section with just `column_blocks` plus intent (`is_hero`, an optional `background`) and the server produces a valid, well-styled section.
|
|
1003
|
+
|
|
1004
|
+
2. **Design recipe in the global-agent system prompt** (`route.ts`, the `PAGE DESIGN` bullets). Tells the model to compose pages from `section` blocks, supply one column per desired grid track, make the first section a hero, alternate `none`/`theme:'muted'`/`theme:'primary'` backgrounds for rhythm, use discrete heading blocks (not `<h2>` inside text HTML), and prefer gradient/theme backgrounds unless a real `media_id` exists. A single `text` block's `html_content` still accepts fully custom HTML/CSS for bespoke sections.
|
|
1005
|
+
|
|
1006
|
+
## External URL Ingestion and Live-Draft Page Rewrites
|
|
1007
|
+
|
|
1008
|
+
Two tools power the "rewrite my home page based on `<url>`" use case. Both live in `libs/cortex/src/lib/ai-global-agent-tools.ts` and are registered in `createCortexGlobalAgentTools`.
|
|
1009
|
+
|
|
1010
|
+
### fetch_url_content (read-only)
|
|
1011
|
+
|
|
1012
|
+
- Input: `{ url: string (http/https), maxChars?: number (500-20000, default 8000) }`.
|
|
1013
|
+
- Fetches an external page and returns `{ title, description, headings[], text, finalUrl, truncated }` (scripts/styles/svg stripped, HTML reduced to readable text).
|
|
1014
|
+
- Safety: rejects non-http(s) URLs and blocks local/loopback/private/link-local hosts and cloud metadata endpoints (`isBlockedFetchHost`), re-checks the host after redirects, enforces a 12s timeout and a ~2MB read cap, and only processes `text/html`/`text/plain` responses.
|
|
1015
|
+
- No confirmation, no DB access. The agent calls it FIRST when a prompt references an external site, then writes new sections from the returned material.
|
|
1016
|
+
|
|
1017
|
+
### rewrite_page_draft (mutating, staged into Live Draft Mode)
|
|
1018
|
+
|
|
1019
|
+
- Input: `cmsTarget (contentType/entityId/slug/title)` + `blocks: CreateCmsBlock[] (1-20)` + optional `meta` overrides (title/slug/status/meta_title/meta_description).
|
|
1020
|
+
- Replaces ALL blocks of a page/post with the supplied set, but writes them into a `content_drafts` row (via `context.supabase` service role) instead of the live `blocks` table. It seeds `meta` from the current published item (so metadata is preserved) and carries `base_version` from the item version.
|
|
1021
|
+
- Nothing goes live: the user previews the draft (`/api/draft/start?path=/<slug>`), then Publishes from the edit screen. Publishing runs the existing draft-publish path, which applies the blocks live AND calls `createPageRevision`/`createPostRevision` — so the rewrite is previewable and reversible.
|
|
1022
|
+
- Blocks are normalized through the same `normalizeCreateBlocks` pipeline as `create_cms_page` (section defaults, column-count sync, nested validation).
|
|
1023
|
+
- Two-step confirmation like other mutating tools; the confirmation payload hash excludes non-deterministic nested `temp_id`s so the confirm phrase is stable.
|
|
1024
|
+
- Result: `{ mutationExecuted, contentType, entityId, slug, blockCount, editPath, draftPreviewPath, isDraft: true }`. The chat treats it as mutating (`MUTATING_TOOL_NAMES`) and navigates to `editPath`, where the "Unpublished Draft → Publish/Discard" toolbar (`DraftStatusActions`) appears.
|
|
1025
|
+
|
|
1026
|
+
Typical flow for "rewrite my home page with 5 sections based on `<url>`": `fetch_url_content(url)` → design a hero + 4 sections following the PAGE DESIGN recipe → `rewrite_page_draft(home, blocks)` → user previews and publishes.
|
|
1027
|
+
|
|
1028
|
+
## Stock Photos and External Images
|
|
1029
|
+
|
|
1030
|
+
Cortex can insert real photos into pages at zero inference cost, and external image URLs are supported natively across the block system.
|
|
1031
|
+
|
|
1032
|
+
### search_stock_photos (read-only)
|
|
1033
|
+
|
|
1034
|
+
- In `libs/cortex/src/lib/ai-global-agent-tools.ts`; registered in `createCortexGlobalAgentTools`.
|
|
1035
|
+
- Input: `{ query: string, count?: 1-15 (default 6), orientation?: 'landscape'|'portrait'|'square' }`.
|
|
1036
|
+
- Key resolution: `resolveCortexAiStockPhotoProvider(supabase)` prefers an admin-stored, encrypted key in `site_settings` (`cortex_ai_pexels_api_key` / `cortex_ai_unsplash_access_key`, read via the service-role client), then falls back to the `PEXELS_API_KEY` / `UNSPLASH_ACCESS_KEY` env vars. Pexels wins when both exist. Returns a clear "not configured" message if neither is set. Both are free API keys.
|
|
1037
|
+
- The stored keys are protected by migration `00000000000012_cortex_ai_stock_photo_settings.sql`, which adds them to the `site_settings` sensitive-keys RLS group (admin-only read/write, never anon-readable), and encrypted with the same envelope as the OpenRouter BYOK key.
|
|
1038
|
+
- **The model is told up front whether stock photos are available.** The global-agent route resolves the provider and injects it into the system prompt: available → "use search_stock_photos"; not configured → "do NOT call search_stock_photos; use gradient/theme backgrounds." So a missing key never wastes a tool call, and the keys are never mandatory — Cortex builds pages either way.
|
|
1039
|
+
- Admin UI: `/cms/settings/cortex-ai` has a Stock Photos card (save/clear Pexels + Unsplash keys, step-by-step, and why) via `saveStockPhotoKeysAction` / `clearStockPhotoKeysAction`.
|
|
1040
|
+
- Rate-limit fallback: `resolveCortexAiStockPhotoProviders` returns ALL configured providers ordered Pexels→Unsplash; `executeSearchStockPhotos` tries them in order, falling through to the next on error/HTTP 429/empty results, and returns `attemptedProviders`.
|
|
1041
|
+
- Returns `{ photos: [{ url, thumbnailUrl, alt, width, height, photographer, photographerUrl, sourceUrl, downloadLocation, credit, provider }], provider, usageGuidance, attemptedProviders, success }`. The agent drops a photo `url` into an image block's `external_url` or a section background's `image.external_url`, and copies the photo's attribution fields into the image content's `attribution`.
|
|
1042
|
+
|
|
1043
|
+
### Provider compliance (Unsplash API Guidelines)
|
|
1044
|
+
|
|
1045
|
+
Unsplash has strict usage rules; Pexels' license is permissive (attribution optional, re-hosting allowed, no download trigger). Handled:
|
|
1046
|
+
|
|
1047
|
+
- **Hotlink**: external stock URLs render via a plain `<img>` from the provider host (never proxied). `importExternalImageToMedia` **refuses to re-host `*.unsplash.com` images** (Pexels re-host is allowed).
|
|
1048
|
+
- **Trigger downloads**: `maybeTriggerStockPhotoDownloads(blocks, supabase)` fires each Unsplash `download_location` (with the resolved Unsplash key) when a photo is committed to a page. Wired into the create (`insertContentBlocks`), `rewrite_page_draft`, `insert_content_block`, and `update_content_block` persist paths. Best-effort/fire-and-forget; depends on the agent copying `attribution.downloadLocation` from the search result.
|
|
1049
|
+
- **Attribution**: `ImageAttributionSchema` on the image block + section background image carries `{ provider, photographer, photographerUrl, sourceUrl, downloadLocation }`. The shared `StockPhotoCredit` component renders "Photo by {photographer} on {Provider}" with the photographer + provider linked and `utm_source`/`utm_medium` params on Unsplash links. The system prompt requires the agent to set `attribution` (and the image caption) from the search result.
|
|
1050
|
+
- **App name/branding**: dashboard-side (the operator's Unsplash app registration); NextBlock uses no Unsplash branding. The `utm_source` in `StockPhotoCredit.tsx` defaults to `nextblock` — change it to the registered app name if needed.
|
|
1051
|
+
|
|
1052
|
+
### External image URLs in blocks
|
|
1053
|
+
|
|
1054
|
+
- `ImageBlockSchema` (`external_url`) and the section `BackgroundSchema.image` (`external_url`, with `media_id`/`object_key` now optional) accept a direct https URL. The cortex fallback schemas mirror this.
|
|
1055
|
+
- Renderers: `ImageBlockRenderer` and `SectionBlockRenderer` render an external URL with a plain `<img>` (so any allowlisted host works without Next `remotePatterns`), and keep the optimized `next/image` path for stored R2 media. `normalizeSectionContent` accepts image backgrounds with an `external_url` (filling `size`/`position`) instead of downgrading them.
|
|
1056
|
+
- Security: the CSP `img-src` allows `https:` (images only — see `apps/nextblock/proxy.ts`), so trusted ADMIN/WRITER authors can embed any https image. script/style/connect stay strict.
|
|
1057
|
+
|
|
1058
|
+
### Persist to media library
|
|
1059
|
+
|
|
1060
|
+
- `importExternalImageToMedia` (`apps/nextblock/app/cms/media/import-external-image.ts`, ADMIN/WRITER) downloads an external image (SSRF-guarded, 15MB/15s caps), measures it with `sharp`, generates a blur placeholder, uploads to R2/Supabase Storage via the shared storage provider, and records it with `recordMediaUpload`. Returns `{ media_id, object_key, width, height, url, blur_data_url }`.
|
|
1061
|
+
- Editor UX: `ImageBlockEditor` and `BackgroundSelector` accept a pasted image URL and show a **Save to media library** action that swaps the external URL for a permanent optimized media reference (or the author can replace it with their own uploaded asset).
|
|
1062
|
+
|
|
1063
|
+
## Advanced Agent Settings
|
|
1064
|
+
|
|
1065
|
+
The global agent's model limits are admin-tunable from `/cms/settings/cortex-ai` (collapsible "Advanced settings"), stored as a non-secret JSON `site_settings` row `cortex_ai_agent_settings` and read by the route via `resolveCortexAiAgentSettings(supabase)` (defaults + clamping in `normalizeCortexAiAgentSettings`, `libs/cortex/src/lib/ai-config.ts`):
|
|
1066
|
+
|
|
1067
|
+
- `maxOutputTokens` — per-step output cap. **`null` = Unlimited** (the route omits the cap so the model uses its own full budget). Default 16000. This is the main lever when a large `rewrite_page_draft` gets truncated.
|
|
1068
|
+
- `maxSteps` — `stepCountIs(n)` tool-call rounds. Default 8.
|
|
1069
|
+
- `temperature` — default 0.1.
|
|
1070
|
+
- `responseTimeoutMs` — the per-attempt idle abort. Default 120000.
|
|
1071
|
+
|
|
1072
|
+
All values are clamped to safe bounds (`CORTEX_AI_AGENT_SETTINGS_BOUNDS`). Actions: `saveCortexAiAgentSettingsAction` / `resetCortexAiAgentSettingsAction`. The route applies them per attempt (omitting `maxOutputTokens` entirely when Unlimited).
|
|
1073
|
+
|
|
986
1074
|
## Dashboard Chat UI
|
|
987
1075
|
|
|
988
1076
|
File:
|
|
@@ -1121,8 +1209,8 @@ Notes:
|
|
|
1121
1209
|
|
|
1122
1210
|
Current protections:
|
|
1123
1211
|
|
|
1124
|
-
- Server-side
|
|
1125
|
-
- Client
|
|
1212
|
+
- Server-side **idle** timeout: 60 seconds with no stream activity (resets on each stream part).
|
|
1213
|
+
- Client **idle** timeout: 90 seconds with no stream activity (`IDLE_TIMEOUT_MS`, resets on each chunk), so a long multi-section build is not aborted at a fixed wall-clock deadline.
|
|
1126
1214
|
- Client stops reading on `finish`.
|
|
1127
1215
|
|
|
1128
1216
|
If it still happens:
|
|
@@ -84,7 +84,13 @@ Products support two kinds of visual editing:
|
|
|
84
84
|
|
|
85
85
|
---
|
|
86
86
|
|
|
87
|
-
## 5.
|
|
87
|
+
## 5. Cortex AI Integration
|
|
88
|
+
|
|
89
|
+
The Cortex AI global agent's `rewrite_page_draft` tool writes into `content_drafts` rather than the live `blocks` table, so an AI-generated whole-page rewrite (e.g. "rewrite my home page with 5 sections") lands as an unpublished draft. It seeds the draft `meta` from the current published item and carries `base_version` from the item's version. The user then previews via `/api/draft/start?path=/<slug>` and publishes with the normal `publishVisualEditingDraft` flow, which applies the blocks live and auto-creates a revision snapshot. See [08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md](./08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md#external-url-ingestion-and-live-draft-page-rewrites).
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 6. Local Verification
|
|
88
94
|
|
|
89
95
|
To test Live Draft Mode locally:
|
|
90
96
|
|
|
@@ -27,13 +27,37 @@ export const HeadingBlockSchema = z.object({
|
|
|
27
27
|
});
|
|
28
28
|
export type HeadingBlockContent = z.infer<typeof HeadingBlockSchema>;
|
|
29
29
|
|
|
30
|
+
// Stock-photo attribution (required for Unsplash: photographer + Unsplash must be
|
|
31
|
+
// credited and linked). Optional; set when an external_url is a stock photo. Field
|
|
32
|
+
// names match the search_stock_photos result so the agent can copy them verbatim.
|
|
33
|
+
// All fields are nullable + optional: the search_stock_photos result carries
|
|
34
|
+
// explicit nulls for missing values (e.g. Pexels has no downloadLocation, and
|
|
35
|
+
// utmSource is null until the Unsplash app name is set), and the agent copies the
|
|
36
|
+
// object verbatim — so the schema must accept null, not just undefined.
|
|
37
|
+
export const ImageAttributionSchema = z.object({
|
|
38
|
+
provider: z.string().nullable().optional().describe('e.g. "unsplash" or "pexels"'),
|
|
39
|
+
photographer: z.string().nullable().optional(),
|
|
40
|
+
photographerUrl: z.string().nullable().optional(),
|
|
41
|
+
sourceUrl: z.string().nullable().optional().describe('Link to the photo on the provider'),
|
|
42
|
+
downloadLocation: z.string().nullable().optional().describe('Unsplash download-trigger endpoint'),
|
|
43
|
+
utmSource: z.string().nullable().optional().describe('Unsplash app name for attribution utm_source'),
|
|
44
|
+
});
|
|
45
|
+
export type ImageAttribution = z.infer<typeof ImageAttributionSchema>;
|
|
46
|
+
|
|
30
47
|
export const ImageBlockSchema = z.object({
|
|
31
|
-
media_id: z.string().nullable().describe('UUID of the media item'),
|
|
48
|
+
media_id: z.string().nullable().optional().describe('UUID of the media item'),
|
|
32
49
|
object_key: z.string().nullable().optional().describe('The actual R2 object key'),
|
|
50
|
+
external_url: z
|
|
51
|
+
.string()
|
|
52
|
+
.nullable()
|
|
53
|
+
.optional()
|
|
54
|
+
.describe('Direct external image URL (e.g. a stock photo). Rendered as-is; media_id/object_key are not required when this is set.'),
|
|
55
|
+
attribution: ImageAttributionSchema.optional().describe('Stock-photo credit (required for Unsplash).'),
|
|
33
56
|
alt_text: z.string().optional().describe('Alternative text'),
|
|
34
57
|
caption: z.string().optional().describe('Optional caption'),
|
|
35
58
|
width: z.number().nullable().optional().describe('Image width'),
|
|
36
59
|
height: z.number().nullable().optional().describe('Image height'),
|
|
60
|
+
blur_data_url: z.string().nullable().optional().describe('Base64 blur placeholder'),
|
|
37
61
|
});
|
|
38
62
|
export type ImageBlockContent = z.infer<typeof ImageBlockSchema>;
|
|
39
63
|
|
|
@@ -77,8 +101,10 @@ const BackgroundSchema = z.object({
|
|
|
77
101
|
min_height: z.string().optional(),
|
|
78
102
|
gradient: GradientSchema.optional(),
|
|
79
103
|
image: z.object({
|
|
80
|
-
media_id: z.string(),
|
|
81
|
-
object_key: z.string(),
|
|
104
|
+
media_id: z.string().optional(),
|
|
105
|
+
object_key: z.string().optional(),
|
|
106
|
+
external_url: z.string().optional(),
|
|
107
|
+
attribution: ImageAttributionSchema.optional(),
|
|
82
108
|
alt_text: z.string().optional(),
|
|
83
109
|
width: z.number().optional(),
|
|
84
110
|
height: z.number().optional(),
|
|
@@ -2,6 +2,7 @@ import 'server-only';
|
|
|
2
2
|
|
|
3
3
|
import { getSsgSupabaseClient, verifyPackageOnline } from '@nextblock-cms/db/server';
|
|
4
4
|
import { resolveMediaUrl } from '../media/resolveMediaUrl';
|
|
5
|
+
import { getHomepageTranslationGroupId } from '../../app/lib/homepage';
|
|
5
6
|
import type {
|
|
6
7
|
GlobalSearchFilter,
|
|
7
8
|
GlobalSearchResponse,
|
|
@@ -340,6 +341,7 @@ async function fetchPages(languageId: number | null): Promise<SearchCandidate[]>
|
|
|
340
341
|
meta_description,
|
|
341
342
|
updated_at,
|
|
342
343
|
language_id,
|
|
344
|
+
translation_group_id,
|
|
343
345
|
languages!inner(code),
|
|
344
346
|
media:feature_image_id(object_key, blur_data_url, width, height),
|
|
345
347
|
blocks(content, block_type, order)
|
|
@@ -360,10 +362,18 @@ async function fetchPages(languageId: number | null): Promise<SearchCandidate[]>
|
|
|
360
362
|
return [];
|
|
361
363
|
}
|
|
362
364
|
|
|
365
|
+
// Every language variation of the homepage (its translation group, any slug)
|
|
366
|
+
// is served at "/", so link those results there rather than at "/{slug}".
|
|
367
|
+
const homepageGroupId = await getHomepageTranslationGroupId(supabase);
|
|
368
|
+
|
|
363
369
|
return data.map((page: any) => {
|
|
364
370
|
const bodyText = buildBodyFromBlocks(page.blocks);
|
|
365
371
|
const description = page.meta_description || null;
|
|
366
|
-
const
|
|
372
|
+
const isHomepage =
|
|
373
|
+
(homepageGroupId && page.translation_group_id === homepageGroupId) ||
|
|
374
|
+
page.slug === 'home' ||
|
|
375
|
+
page.slug === 'accueil';
|
|
376
|
+
const href = isHomepage ? '/' : `/${page.slug}`;
|
|
367
377
|
const media = getFirstRelation(page.media as { object_key?: string | null } | { object_key?: string | null }[] | null);
|
|
368
378
|
|
|
369
379
|
return {
|