Update React Editor integration and clean up template

This commit is contained in:
Rami Bitar
2026-08-15 13:47:33 -04:00
parent 0a7ea0bf6b
commit 0fb6cef226
82 changed files with 113 additions and 913 deletions
+1 -4
View File
@@ -1,9 +1,6 @@
import PageEditor from '@/components/page-editor';
import pageData from '../page.json';
// Editor for /about. Sitting under the route it edits means the preview
// resolves the same params the public page gets, and `../page.json` is the
// very file publishing writes back to.
export default function EditorPage() {
return <PageEditor routeKey="/about" page={pageData} />;
return <PageEditor pagePath="app/about/page.json" page={pageData} />;
}
+1 -9
View File
@@ -9,10 +9,8 @@ import {
getCollectionProductsPage,
isDefaultTitleOption,
isDefaultTitleSelection,
} from '@/services/shopify/catalog';
} from '@/services/shopify/shop';
// Streaming needs the Node runtime here because the Storefront helpers run
// server-side on each tool call.
export const maxDuration = 30;
const MODEL = process.env.OPENROUTER_MODEL ?? 'openai/gpt-5.6-luna-pro';
@@ -36,7 +34,6 @@ Guidelines:
- You cannot place orders, change carts, process payments, or look up customer
or order data. Say so and point the shopper to the relevant page instead.`;
// Trim the Storefront payloads to what the model actually needs to answer.
const summariseProduct = (product: {
id: string;
title: string;
@@ -88,12 +85,9 @@ export async function POST(req: Request) {
const openrouter = createOpenRouter({ apiKey });
const result = streamText({
// `reasoning` asks OpenRouter to stream the model's thinking; the UI
// renders it via the Reasoning component.
model: openrouter(MODEL, { reasoning: { enabled: true, effort: 'medium' } }),
system: SYSTEM_PROMPT,
messages: await convertToModelMessages(messages),
// Let the model call a tool, read the result, then answer.
stopWhen: stepCountIs(5),
tools: {
searchCatalogue: tool({
@@ -201,7 +195,5 @@ export async function POST(req: Request) {
},
});
// sendReasoning forwards reasoning parts to the client; without it the
// stream carries text and tool calls only.
return result.toUIMessageStreamResponse({ sendReasoning: true });
}
+6 -4
View File
@@ -1,9 +1,11 @@
import PageEditor from '@/components/page-editor';
import pageData from '../page.json';
// Editor for /collections/[handle]. Sitting under the route it edits means the preview
// resolves the same params the public page gets, and `../page.json` is the
// very file publishing writes back to.
export default function EditorPage() {
return <PageEditor routeKey="/collections/[handle]" page={pageData} />;
return (
<PageEditor
pagePath="app/collections/[handle]/page.json"
page={pageData}
/>
);
}
+2 -5
View File
@@ -1,7 +1,7 @@
import type { Metadata } from 'next';
import PageRender from '@/components/page-render';
import { pageMetadata } from '@/lib/page-metadata';
import { getCollectionProductsPage } from '@/services/shopify/catalog';
import { getCollectionProductsPage } from '@/services/shopify/shop';
import { truncate } from '@/lib/utils';
import page from './page.json';
@@ -12,14 +12,11 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { handle } = await params;
// No collection-only query exists, so ask for the smallest page of products
// and use just the collection node off it.
// Shopify exposes collection metadata through the products connection.
const { collection } = await getCollectionProductsPage(handle, {
first: 1,
}).catch(() => ({ collection: null }));
// The blocks fetch client-side and render their own empty state, so a failed
// lookup falls back to the page.json tags rather than a 500.
if (!collection) return pageMetadata(page);
return pageMetadata(page, {
+1 -4
View File
@@ -1,9 +1,6 @@
import PageEditor from '@/components/page-editor';
import pageData from '../page.json';
// Editor for /collections. Sitting under the route it edits means the preview
// resolves the same params the public page gets, and `../page.json` is the
// very file publishing writes back to.
export default function EditorPage() {
return <PageEditor routeKey="/collections" page={pageData} />;
return <PageEditor pagePath="app/collections/page.json" page={pageData} />;
}
+1 -4
View File
@@ -1,9 +1,6 @@
import PageEditor from '@/components/page-editor';
import pageData from '../page.json';
// Editor for /. Sitting under the route it edits means the preview
// resolves the same params the public page gets, and `../page.json` is the
// very file publishing writes back to.
export default function EditorPage() {
return <PageEditor routeKey="/" page={pageData} />;
return <PageEditor pagePath="app/page.json" page={pageData} />;
}
+2 -27
View File
@@ -3,53 +3,41 @@
@custom-variant dark (&:is(.dark *));
@theme {
/* Refined neutral modern palette */
--color-background: hsl(0 0% 100%);
--color-foreground: hsl(240 10% 3.9%);
/* Card */
--color-card: hsl(0 0% 100%);
--color-card-foreground: hsl(240 10% 3.9%);
/* Popover */
--color-popover: hsl(0 0% 100%);
--color-popover-foreground: hsl(240 10% 3.9%);
/* Primary - Deep neutral slate for professional look */
--color-primary: hsl(240 5.9% 10%);
--color-primary-foreground: hsl(0 0% 98%);
/* Secondary */
--color-secondary: hsl(240 4.8% 95.9%);
--color-secondary-foreground: hsl(240 5.9% 10%);
/* Muted */
--color-muted: hsl(240 4.8% 95.9%);
--color-muted-foreground: hsl(240 3.8% 46.1%);
/* Accent - Subtle warm gray */
--color-accent: hsl(30 6.7% 95%);
--color-accent-foreground: hsl(240 5.9% 10%);
/* Destructive */
--color-destructive: hsl(0 84.2% 60.2%);
--color-destructive-foreground: hsl(0 0% 98%);
/* Shop (accelerated checkout) purple */
--color-shop: #5a31f4;
/* Border */
--color-border: hsl(240 5.9% 90%);
--color-input: hsl(240 5.9% 90%);
--color-ring: hsl(240 5.9% 10%);
/* Radius - Modern larger corners */
--radius-sm: 0.5rem;
--radius-md: 0.75rem;
--radius-lg: 1rem;
--radius-xl: 1.25rem;
/* Typography — Geist Sans / Geist Mono */
--font-sans: var(--font-geist-sans), system-ui, sans-serif;
--font-mono: var(--font-geist-mono), ui-monospace, SFMono-Regular, monospace;
--font-heading: var(--font-geist-sans), system-ui, sans-serif;
@@ -57,10 +45,7 @@
--font-poppins: var(--font-geist-sans), system-ui, sans-serif;
}
/* Base styles for light, modern Shopify storefront */
/* Tailwind v4 defaults an unqualified `border` to currentColor. shadcn
components (e.g. Button's outline variant) rely on this base layer to pick up
the theme's border colour instead of the text colour. */
/* Tailwind v4 otherwise defaults unqualified borders to currentColor. */
@layer base {
* {
border-color: var(--color-border);
@@ -75,7 +60,6 @@ body {
-moz-osx-font-smoothing: grayscale;
}
/* Modern card design for products and collections */
.card-modern {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--color-border);
@@ -87,7 +71,6 @@ body {
border-color: hsl(240 5.9% 85%);
}
/* Heading styles — regular weight, slightly tightened like the Geist reference */
h1,
h2,
.font-heading {
@@ -95,7 +78,6 @@ h2,
letter-spacing: -0.02em;
}
/* Swipeable rows (mobile galleries) without a visible scrollbar */
.no-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
@@ -105,7 +87,6 @@ h2,
display: none;
}
/* Diagonal strike marking an unavailable product option (size pill, swatch) */
.option-unavailable {
background-image: linear-gradient(
to top right,
@@ -116,7 +97,6 @@ h2,
);
}
/* Product description (HTML returned by the Storefront API) */
.product-description p {
margin-bottom: 1rem;
}
@@ -125,7 +105,6 @@ h2,
margin-bottom: 0;
}
/* Shop policy body (HTML returned by the Storefront API) */
.policy-body p,
.policy-body ul,
.policy-body ol {
@@ -164,7 +143,6 @@ h2,
margin-bottom: 0;
}
/* Enhanced button styles */
button,
.btn-modern {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
@@ -177,12 +155,10 @@ button,
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
/* Hero section styles */
.hero-bg {
background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f5 100%);
}
/* Product gallery improvements */
.product-gallery-main {
transition: box-shadow 0.3s ease;
}
@@ -191,7 +167,6 @@ button,
box-shadow: 0 0 0 1px hsl(240 5.9% 80%) inset;
}
/* Clean price display */
.price-display {
font-feature-settings: 'tnum';
}
@@ -227,4 +202,4 @@ button,
--color-3: oklch(69.6% 0.165 251);
--color-4: oklch(80.2% 0.134 225);
--color-5: oklch(90.7% 0.231 133);
}
}
-5
View File
@@ -14,8 +14,6 @@ const geistMono = Geist_Mono({
variable: '--font-geist-mono',
});
// Per-page tags come from each page.json's root props via pageMetadata(); this
// is the fallback for routes that don't set their own.
export const metadata: Metadata = {
metadataBase: new URL(site.url),
title: {
@@ -44,9 +42,6 @@ export default function RootLayout({
}) {
return (
<html lang="en" className={`${geist.variable} ${geistMono.variable}`}>
{/* Header, footer and the store assistant are editable blocks now, so
each page.json carries its own chrome rather than the layout doing
it. Keeps what the editor shows identical to what the route ships. */}
<body className="font-body antialiased bg-background text-foreground m-0 p-0 flex min-h-screen flex-col">
<div className="flex-1">{children}</div>
</body>
+3 -4
View File
@@ -1,9 +1,8 @@
import PageEditor from '@/components/page-editor';
import pageData from '../page.json';
// Editor for /policies/[handle]. Sitting under the route it edits means the preview
// resolves the same params the public page gets, and `../page.json` is the
// very file publishing writes back to.
export default function EditorPage() {
return <PageEditor routeKey="/policies/[handle]" page={pageData} />;
return (
<PageEditor pagePath="app/policies/[handle]/page.json" page={pageData} />
);
}
+3 -4
View File
@@ -1,9 +1,8 @@
import PageEditor from '@/components/page-editor';
import pageData from '../page.json';
// Editor for /products/[handle]. Sitting under the route it edits means the preview
// resolves the same params the public page gets, and `../page.json` is the
// very file publishing writes back to.
export default function EditorPage() {
return <PageEditor routeKey="/products/[handle]" page={pageData} />;
return (
<PageEditor pagePath="app/products/[handle]/page.json" page={pageData} />
);
}
+1 -3
View File
@@ -1,7 +1,7 @@
import type { Metadata } from 'next';
import PageRender from '@/components/page-render';
import { pageMetadata } from '@/lib/page-metadata';
import { getProduct } from '@/services/shopify/catalog';
import { getProduct } from '@/services/shopify/shop';
import { truncate } from '@/lib/utils';
import page from './page.json';
@@ -12,8 +12,6 @@ export async function generateMetadata({
}): Promise<Metadata> {
const { handle } = await params;
// The blocks fetch client-side and render their own empty state, so a failed
// lookup falls back to the page.json tags rather than a 500.
const product = await getProduct(handle).catch(() => null);
if (!product) return pageMetadata(page);
+1 -4
View File
@@ -1,9 +1,6 @@
import PageEditor from '@/components/page-editor';
import pageData from '../page.json';
// Editor for /search. Sitting under the route it edits means the preview
// resolves the same params the public page gets, and `../page.json` is the
// very file publishing writes back to.
export default function EditorPage() {
return <PageEditor routeKey="/search" page={pageData} />;
return <PageEditor pagePath="app/search/page.json" page={pageData} />;
}
-2
View File
@@ -1,12 +1,10 @@
import type { Metadata } from 'next';
import PageRender from '@/components/page-render';
import { pageMetadata } from '@/lib/page-metadata';
// Alias of /collections — same page.json, so editing that route updates both.
import page from '../../collections/page.json';
export const metadata: Metadata = {
...pageMetadata(page),
// Point crawlers at the canonical listing.
alternates: { canonical: '/collections' },
};