Load page.json by import, publish via fs, drop cookie auth
The editor no longer round-trips through /api/pages. Each route's editor/page.tsx imports its own `../page.json` and hands it to PageEditor as a prop, so the editor opens with the page already in hand — no fetch, no loading state, no undo history seeded from a placeholder. Globals still come from app.globals.json. Publishing moves from `PUT /api/pages` to a `publishPage` server action that writes the route's page.json with node:fs directly. The target path is still built from the lib/pages.ts registry rather than from the caller, so an unknown route key is rejected instead of escaping app/. Removes the customer account auth entirely: the httpOnly cookie session, the /api/account/* handlers, the customer service and GraphQL documents, the account-* blocks, and the /account/* routes. The template has no auth, so nothing reads a cookie now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01STGDvL4X7FhHHnxdRE2ayo
This commit is contained in:
co-authored by
Claude Opus 5
parent
63ecc5e284
commit
f11a764426
@@ -1,7 +1,9 @@
|
||||
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.
|
||||
// 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" />;
|
||||
return <PageEditor routeKey="/about" page={pageData} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
|
||||
// Editor for /account/activate/[id]/[token]. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/account/activate/[id]/[token]" />;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"root": {
|
||||
"props": {
|
||||
"title": "Activate your account",
|
||||
"description": "Finish setting up your account.",
|
||||
"ogImage": ""
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "header"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "account-activate",
|
||||
"props": {
|
||||
"id": "account-activate",
|
||||
"flow": "activate",
|
||||
"title": "Activate your account",
|
||||
"description": "Choose a password to finish setting up your account.",
|
||||
"submitLabel": "Activate account",
|
||||
"successMessage": "",
|
||||
"links": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "footer",
|
||||
"props": {
|
||||
"id": "footer"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "store-assistant",
|
||||
"props": {
|
||||
"id": "store-assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import PageRender from '@/components/page-render';
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
import page from './page.json';
|
||||
|
||||
export const metadata = pageMetadata(page);
|
||||
|
||||
export default function Page() {
|
||||
return <PageRender page={page} />;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
|
||||
// Editor for /account. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/account" />;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
|
||||
// Editor for /account/login. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/account/login" />;
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
{
|
||||
"root": {
|
||||
"props": {
|
||||
"title": "Sign in",
|
||||
"description": "Sign in to your Shop account.",
|
||||
"ogImage": ""
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "header"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "account-login",
|
||||
"props": {
|
||||
"id": "account-login",
|
||||
"flow": "login",
|
||||
"title": "Sign in",
|
||||
"description": "",
|
||||
"submitLabel": "Sign in",
|
||||
"successMessage": "",
|
||||
"links": [
|
||||
{
|
||||
"label": "Create an account",
|
||||
"url": "/account/register"
|
||||
},
|
||||
{
|
||||
"label": "Forgot your password?",
|
||||
"url": "/account/recover"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "footer",
|
||||
"props": {
|
||||
"id": "footer"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "store-assistant",
|
||||
"props": {
|
||||
"id": "store-assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import PageRender from '@/components/page-render';
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
import page from './page.json';
|
||||
|
||||
export const metadata = pageMetadata(page, { path: '/account/login' });
|
||||
|
||||
export default function Page() {
|
||||
return <PageRender page={page} />;
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
{
|
||||
"root": {
|
||||
"props": {
|
||||
"title": "Order history",
|
||||
"description": "Your recent orders.",
|
||||
"ogImage": ""
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "header"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "account-orders",
|
||||
"props": {
|
||||
"id": "account-orders",
|
||||
"title": "Order history",
|
||||
"signedOutMessage": "Sign in to see your orders.",
|
||||
"emptyMessage": "You haven't placed any orders yet.",
|
||||
"limit": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "footer",
|
||||
"props": {
|
||||
"id": "footer"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "store-assistant",
|
||||
"props": {
|
||||
"id": "store-assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { redirect } from 'next/navigation';
|
||||
import PageRender from '@/components/page-render';
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
import { getSessionToken } from '@/services/shopify/session';
|
||||
import page from './page.json';
|
||||
|
||||
export const metadata = pageMetadata(page, { path: '/account' });
|
||||
|
||||
export default async function Page() {
|
||||
// The order-history block re-reads the session through /api/account/orders,
|
||||
// but bouncing signed-out visitors here avoids rendering the page at all.
|
||||
const token = await getSessionToken();
|
||||
if (!token) redirect('/account/login');
|
||||
|
||||
return <PageRender page={page} />;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
|
||||
// Editor for /account/recover. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/account/recover" />;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"root": {
|
||||
"props": {
|
||||
"title": "Reset password",
|
||||
"description": "Request a password reset link.",
|
||||
"ogImage": ""
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "header"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "account-recover",
|
||||
"props": {
|
||||
"id": "account-recover",
|
||||
"flow": "recover",
|
||||
"title": "Reset password",
|
||||
"description": "Enter your email and we'll send you a link to set a new password.",
|
||||
"submitLabel": "Send reset link",
|
||||
"successMessage": "If that email has an account, a reset link is on its way.",
|
||||
"links": [
|
||||
{
|
||||
"label": "Back to sign in",
|
||||
"url": "/account/login"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "footer",
|
||||
"props": {
|
||||
"id": "footer"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "store-assistant",
|
||||
"props": {
|
||||
"id": "store-assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import PageRender from '@/components/page-render';
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
import page from './page.json';
|
||||
|
||||
export const metadata = pageMetadata(page, { path: '/account/recover' });
|
||||
|
||||
export default function Page() {
|
||||
return <PageRender page={page} />;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
|
||||
// Editor for /account/register. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/account/register" />;
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
{
|
||||
"root": {
|
||||
"props": {
|
||||
"title": "Create account",
|
||||
"description": "Create a Shop account.",
|
||||
"ogImage": ""
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "header"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "account-register",
|
||||
"props": {
|
||||
"id": "account-register",
|
||||
"flow": "register",
|
||||
"title": "Create account",
|
||||
"description": "",
|
||||
"submitLabel": "Create account",
|
||||
"successMessage": "",
|
||||
"links": [
|
||||
{
|
||||
"label": "Already have an account? Sign in",
|
||||
"url": "/account/login"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "footer",
|
||||
"props": {
|
||||
"id": "footer"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "store-assistant",
|
||||
"props": {
|
||||
"id": "store-assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import PageRender from '@/components/page-render';
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
import page from './page.json';
|
||||
|
||||
export const metadata = pageMetadata(page, { path: '/account/register' });
|
||||
|
||||
export default function Page() {
|
||||
return <PageRender page={page} />;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import PageEditor from '@/components/page-editor';
|
||||
|
||||
// Editor for /account/reset/[id]/[token]. Sitting under the route it edits means the preview
|
||||
// resolves the same params the public page gets.
|
||||
export default function EditorPage() {
|
||||
return <PageEditor routeKey="/account/reset/[id]/[token]" />;
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
{
|
||||
"root": {
|
||||
"props": {
|
||||
"title": "Set a new password",
|
||||
"description": "Choose a new password.",
|
||||
"ogImage": ""
|
||||
}
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "header",
|
||||
"props": {
|
||||
"id": "header"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "account-reset",
|
||||
"props": {
|
||||
"id": "account-reset",
|
||||
"flow": "reset",
|
||||
"title": "Set a new password",
|
||||
"description": "",
|
||||
"submitLabel": "Save password",
|
||||
"successMessage": "",
|
||||
"links": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "footer",
|
||||
"props": {
|
||||
"id": "footer"
|
||||
},
|
||||
"synced": true
|
||||
},
|
||||
{
|
||||
"type": "store-assistant",
|
||||
"props": {
|
||||
"id": "store-assistant"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import PageRender from '@/components/page-render';
|
||||
import { pageMetadata } from '@/lib/page-metadata';
|
||||
import page from './page.json';
|
||||
|
||||
export const metadata = pageMetadata(page);
|
||||
|
||||
export default function Page() {
|
||||
return <PageRender page={page} />;
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
import {
|
||||
activateAccount,
|
||||
toCustomerGid,
|
||||
customerErrorMessage,
|
||||
} from '@/services/shopify/customer';
|
||||
import { setSessionToken } from '@/services/shopify/session';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { id, activationToken, password } = await req.json();
|
||||
|
||||
if (!id || !activationToken || !password) {
|
||||
return Response.json(
|
||||
{ error: 'This activation link is incomplete.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { token, errors } = await activateAccount(
|
||||
toCustomerGid(id),
|
||||
activationToken,
|
||||
password
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return Response.json({ error: customerErrorMessage(errors) }, { status: 400 });
|
||||
}
|
||||
|
||||
await setSessionToken(token.accessToken, token.expiresAt);
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { login, customerErrorMessage } from '@/services/shopify/customer';
|
||||
import { setSessionToken } from '@/services/shopify/session';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { email, password } = await req.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return Response.json(
|
||||
{ error: 'Enter your email and password.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { token, errors } = await login(email, password);
|
||||
|
||||
if (!token) {
|
||||
// Shopify distinguishes wrong-password from unknown-email; collapse both so
|
||||
// the form can't be used to enumerate accounts.
|
||||
return Response.json(
|
||||
{
|
||||
error: errors.length
|
||||
? 'Incorrect email or password.'
|
||||
: customerErrorMessage(errors),
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
await setSessionToken(token.accessToken, token.expiresAt);
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { logout } from '@/services/shopify/customer';
|
||||
import { getSessionToken, clearSessionToken } from '@/services/shopify/session';
|
||||
|
||||
export async function POST() {
|
||||
const token = await getSessionToken();
|
||||
if (token) await logout(token);
|
||||
|
||||
await clearSessionToken();
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { getSessionToken } from '@/services/shopify/session';
|
||||
import { getCustomer } from '@/services/shopify/customer';
|
||||
|
||||
// Minimal session probe for the header menu — never returns the access token.
|
||||
export async function GET() {
|
||||
const token = await getSessionToken();
|
||||
if (!token) return Response.json({ customer: null });
|
||||
|
||||
const customer = await getCustomer(token, 0);
|
||||
if (!customer) return Response.json({ customer: null });
|
||||
|
||||
return Response.json({
|
||||
customer: {
|
||||
displayName: customer.displayName,
|
||||
email: customer.email,
|
||||
firstName: customer.firstName,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { getSessionToken } from '@/services/shopify/session';
|
||||
import { getCustomer } from '@/services/shopify/customer';
|
||||
|
||||
/**
|
||||
* Full customer record including orders, for the client-rendered order-history
|
||||
* block. `/api/account/me` stays the lightweight session probe the header uses
|
||||
* — it asks for zero orders — so the two don't fight over payload size.
|
||||
*
|
||||
* The access token never leaves the server: it is read from the session cookie
|
||||
* here and only the resolved customer is returned.
|
||||
*/
|
||||
export async function GET(request: Request) {
|
||||
const token = await getSessionToken();
|
||||
if (!token) return Response.json({ customer: null }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
const parsed = Number(searchParams.get('orders'));
|
||||
const orderCount = Number.isFinite(parsed)
|
||||
? Math.min(Math.max(Math.trunc(parsed), 1), 50)
|
||||
: 20;
|
||||
|
||||
const customer = await getCustomer(token, orderCount);
|
||||
if (!customer) return Response.json({ customer: null }, { status: 401 });
|
||||
|
||||
return Response.json({ customer });
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { recoverPassword } from '@/services/shopify/customer';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { email } = await req.json();
|
||||
|
||||
if (email) await recoverPassword(email);
|
||||
|
||||
// Always the same response, so the form can't reveal who has an account.
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import {
|
||||
createCustomer,
|
||||
login,
|
||||
customerErrorMessage,
|
||||
} from '@/services/shopify/customer';
|
||||
import { setSessionToken } from '@/services/shopify/session';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { email, password, firstName, lastName } = await req.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return Response.json(
|
||||
{ error: 'Enter your email and password.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { errors } = await createCustomer({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
return Response.json({ error: customerErrorMessage(errors) }, { status: 400 });
|
||||
}
|
||||
|
||||
// Sign the new customer straight in. Accounts needing email confirmation
|
||||
// won't return a token yet, which is not an error.
|
||||
const { token } = await login(email, password);
|
||||
|
||||
if (token) {
|
||||
await setSessionToken(token.accessToken, token.expiresAt);
|
||||
return Response.json({ ok: true, signedIn: true });
|
||||
}
|
||||
|
||||
return Response.json({ ok: true, signedIn: false });
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import {
|
||||
resetPassword,
|
||||
toCustomerGid,
|
||||
customerErrorMessage,
|
||||
} from '@/services/shopify/customer';
|
||||
import { setSessionToken } from '@/services/shopify/session';
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { id, resetToken, password } = await req.json();
|
||||
|
||||
if (!id || !resetToken || !password) {
|
||||
return Response.json({ error: 'This reset link is incomplete.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { token, errors } = await resetPassword(
|
||||
toCustomerGid(id),
|
||||
resetToken,
|
||||
password
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
return Response.json({ error: customerErrorMessage(errors) }, { status: 400 });
|
||||
}
|
||||
|
||||
await setSessionToken(token.accessToken, token.expiresAt);
|
||||
return Response.json({ ok: true });
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { findPageRoute } from '@/lib/pages';
|
||||
|
||||
// Touches the filesystem, so it must never be statically optimised.
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* Resolves a route key to its `page.json` on disk.
|
||||
*
|
||||
* The path is built from the registry in `lib/pages.ts`, never from the request
|
||||
* body, so an unknown or crafted route key is rejected outright rather than
|
||||
* escaping the `app/` directory. The realpath check is belt-and-braces for the
|
||||
* same thing.
|
||||
*/
|
||||
function resolvePageFile(routeKey: string): string | null {
|
||||
const route = findPageRoute(routeKey);
|
||||
if (!route) return null;
|
||||
|
||||
const appDir = path.join(process.cwd(), 'app');
|
||||
const file = path.join(appDir, route.dir, 'page.json');
|
||||
|
||||
return file.startsWith(appDir + path.sep) ? file : null;
|
||||
}
|
||||
|
||||
// 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');
|
||||
|
||||
async function readJson(file: string): Promise<Record<string, any> | null> {
|
||||
try {
|
||||
return JSON.parse(await readFile(file, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const routeKey = new URL(request.url).searchParams.get('route') ?? '/';
|
||||
const file = resolvePageFile(routeKey);
|
||||
|
||||
if (!file) {
|
||||
return Response.json({ error: `Unknown route: ${routeKey}` }, { status: 404 });
|
||||
}
|
||||
|
||||
const page = await readJson(file);
|
||||
// A route with no page.json yet is a new page, not an error.
|
||||
if (!page) return Response.json({ page: null });
|
||||
|
||||
const globals = await readJson(GLOBALS_FILE);
|
||||
return Response.json({ page: { ...page, globals: globals ?? {} } });
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
let body: { route?: string; page?: unknown };
|
||||
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return Response.json({ error: 'Expected a JSON body.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const routeKey = body.route ?? '';
|
||||
const file = resolvePageFile(routeKey);
|
||||
|
||||
if (!file) {
|
||||
return Response.json({ error: `Unknown route: ${routeKey}` }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!body.page || typeof body.page !== 'object') {
|
||||
return Response.json({ error: 'Expected a page object.' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 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, ...page } = body.page as Record<string, unknown>;
|
||||
|
||||
try {
|
||||
await writeFile(file, `${JSON.stringify(page, null, 2)}\n`, 'utf8');
|
||||
|
||||
if (globals && typeof globals === 'object') {
|
||||
await writeFile(
|
||||
GLOBALS_FILE,
|
||||
`${JSON.stringify(globals, null, 2)}\n`,
|
||||
'utf8'
|
||||
);
|
||||
}
|
||||
|
||||
return Response.json({ 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 Response.json(
|
||||
{
|
||||
error:
|
||||
'Could not write page.json. The filesystem is read-only — run the editor locally to save.',
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
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.
|
||||
// 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]" />;
|
||||
return <PageEditor routeKey="/collections/[handle]" page={pageData} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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.
|
||||
// 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" />;
|
||||
return <PageEditor routeKey="/collections" page={pageData} />;
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
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.
|
||||
// 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="/" />;
|
||||
return <PageEditor routeKey="/" page={pageData} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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.
|
||||
// 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]" />;
|
||||
return <PageEditor routeKey="/policies/[handle]" page={pageData} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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.
|
||||
// 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]" />;
|
||||
return <PageEditor routeKey="/products/[handle]" page={pageData} />;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
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.
|
||||
// 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" />;
|
||||
return <PageEditor routeKey="/search" page={pageData} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user