Update React Editor integration and clean up template
This commit is contained in:
@@ -1,6 +1,3 @@
|
||||
// Server-only config for the Frontend Cloud proxy routes.
|
||||
// The API key lives here (read from a non-public env var) so it is never
|
||||
// shipped to the browser bundle.
|
||||
|
||||
export const CLOUD_BASE = "https://cloud.frontend.co";
|
||||
|
||||
|
||||
+1
-11
@@ -1,7 +1,6 @@
|
||||
import type { Metadata } from 'next';
|
||||
import { site } from '@/config/site';
|
||||
|
||||
/** The subset of root props (see config/root.tsx) that describes a page. */
|
||||
interface EditorPage {
|
||||
root?: {
|
||||
props?: {
|
||||
@@ -16,25 +15,16 @@ interface MetadataOverrides {
|
||||
title?: string;
|
||||
description?: string;
|
||||
image?: string;
|
||||
/** Root-relative path, used for the canonical and og:url. */
|
||||
path?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a page.json's root props into Next metadata. The editor exposes
|
||||
* title/description/ogImage on the root, so editing a page in the editor is
|
||||
* what changes its tags; `overrides` lets dynamic routes layer fetched product,
|
||||
* collection or policy data on top.
|
||||
*/
|
||||
export function pageMetadata(
|
||||
page: EditorPage,
|
||||
overrides: MetadataOverrides = {}
|
||||
): Metadata {
|
||||
const props = page.root?.props ?? {};
|
||||
|
||||
// Editor-authored titles already carry the brand ("About — Shop"), so they
|
||||
// opt out of the root template with `absolute`. A fetched override is a bare
|
||||
// product or collection name and flows through the template instead.
|
||||
// Editor titles already include the brand; fetched titles use the template.
|
||||
const title = overrides.title ?? props.title ?? site.storeName;
|
||||
const socialTitle = overrides.title ? `${title} — ${site.storeName}` : title;
|
||||
const description =
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
/**
|
||||
* Every editor-driven route in the app.
|
||||
*
|
||||
* `key` is what the editor's page picker shows and what `onPublish` hands back;
|
||||
* `dir` is the folder under `app/` holding that route's `page.json`. Keeping
|
||||
* the list here means the publish action can validate a route key against it
|
||||
* rather than trusting a path from the browser.
|
||||
*/
|
||||
export interface PageRoute {
|
||||
key: string;
|
||||
label: string;
|
||||
/** Path under `app/`, relative and without a leading slash. */
|
||||
dir: string;
|
||||
}
|
||||
|
||||
export const PAGE_ROUTES: PageRoute[] = [
|
||||
{ key: '/', label: 'Home', dir: '' },
|
||||
{ key: '/about', label: 'About', dir: 'about' },
|
||||
{ key: '/collections', label: 'Collections', dir: 'collections' },
|
||||
{
|
||||
key: '/collections/[handle]',
|
||||
label: 'Collection detail',
|
||||
dir: 'collections/[handle]',
|
||||
},
|
||||
{
|
||||
key: '/products/[handle]',
|
||||
label: 'Product detail',
|
||||
dir: 'products/[handle]',
|
||||
},
|
||||
{ key: '/search', label: 'Search', dir: 'search' },
|
||||
{ key: '/policies/[handle]', label: 'Policy', dir: 'policies/[handle]' },
|
||||
];
|
||||
|
||||
export const ROUTE_KEYS = PAGE_ROUTES.map((route) => route.key);
|
||||
|
||||
export function findPageRoute(key: string): PageRoute | undefined {
|
||||
return PAGE_ROUTES.find((route) => route.key === key);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL of the editor for a route: every public route has an `/editor` child, so
|
||||
* `/products/warrior-club-hoodie` is edited at
|
||||
* `/products/warrior-club-hoodie/editor`.
|
||||
*
|
||||
* Dynamic segments are filled from `params` when the caller knows them. The
|
||||
* page picker doesn't — switching to `/products/[handle]` there lands on the
|
||||
* literal `[handle]`, where the blocks render their empty state until a product
|
||||
* is picked. Opening the editor from a real product page is the path that
|
||||
* gives them concrete params.
|
||||
*/
|
||||
export function editorHref(
|
||||
routeKey: string,
|
||||
params: Record<string, string | undefined> = {}
|
||||
): string {
|
||||
const base =
|
||||
routeKey === '/'
|
||||
? ''
|
||||
: routeKey.replace(
|
||||
/\[(\w+)\]/g,
|
||||
(segment, name: string) => params[name] ?? segment
|
||||
);
|
||||
|
||||
return `${base}/editor`;
|
||||
}
|
||||
+10
-27
@@ -2,50 +2,35 @@
|
||||
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { findPageRoute } from '@/lib/pages';
|
||||
|
||||
/**
|
||||
* Writes a published page straight to its `page.json` on disk.
|
||||
*
|
||||
* A server action rather than a route handler: the editor calls it like a
|
||||
* function, and there is no HTTP endpoint sitting in front of the filesystem.
|
||||
*
|
||||
* The path is built from the registry in `lib/pages.ts`, never from the caller,
|
||||
* so an unknown or crafted route key is rejected outright rather than escaping
|
||||
* the `app/` directory.
|
||||
*/
|
||||
|
||||
// Props of blocks marked `global: true` (header, footer) live in one file that
|
||||
// every page.json references, so editing them once updates every route.
|
||||
const GLOBALS_FILE = path.join(process.cwd(), 'app.globals.json');
|
||||
|
||||
export interface PublishResult {
|
||||
/** Path of the written file, relative to the project root. */
|
||||
file?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function publishPage(
|
||||
routeKey: string,
|
||||
pagePath: string,
|
||||
page: unknown
|
||||
): Promise<PublishResult> {
|
||||
const route = findPageRoute(routeKey);
|
||||
if (!route) return { error: `Unknown route: ${routeKey}` };
|
||||
|
||||
if (!page || typeof page !== 'object') {
|
||||
return { error: 'Expected a page object.' };
|
||||
}
|
||||
|
||||
const appDir = path.join(process.cwd(), 'app');
|
||||
const file = path.join(appDir, route.dir, 'page.json');
|
||||
const file = path.resolve(process.cwd(), pagePath);
|
||||
const relativeToApp = path.relative(appDir, file);
|
||||
|
||||
// Belt-and-braces against a registry entry with a traversing `dir`.
|
||||
if (file !== path.join(appDir, 'page.json') && !file.startsWith(appDir + path.sep)) {
|
||||
return { error: `Unknown route: ${routeKey}` };
|
||||
if (
|
||||
!relativeToApp ||
|
||||
relativeToApp.startsWith(`..${path.sep}`) ||
|
||||
path.isAbsolute(relativeToApp) ||
|
||||
path.basename(file) !== 'page.json'
|
||||
) {
|
||||
return { error: 'Invalid page file.' };
|
||||
}
|
||||
|
||||
// Globals belong to the whole site, not this route, so they go to their own
|
||||
// file and are stripped from the page before it is written.
|
||||
const { globals, ...pageData } = page as Record<string, unknown>;
|
||||
|
||||
try {
|
||||
@@ -61,8 +46,6 @@ export async function publishPage(
|
||||
|
||||
return { file: path.relative(process.cwd(), file) };
|
||||
} catch (err) {
|
||||
// Read-only filesystems (most serverless hosts) land here. Say so plainly
|
||||
// rather than reporting a save that did not happen.
|
||||
console.error(`Failed to write ${file}:`, err);
|
||||
return {
|
||||
error:
|
||||
|
||||
Reference in New Issue
Block a user