diff --git a/craft/src/hooks/useWhpApi.ts b/craft/src/hooks/useWhpApi.ts index aebf05e..6492721 100644 --- a/craft/src/hooks/useWhpApi.ts +++ b/craft/src/hooks/useWhpApi.ts @@ -51,8 +51,11 @@ export function useWhpApi() { // Build the pages array with HTML for each page // For the active page, use the freshly exported HTML from the canvas; // for others, export from their stored craft state - const pagesPayload = pages.map((page) => { - const filename = (page.slug === 'index' ? 'index' : page.slug) + '.html'; + const pagesPayload = pages.map((page, i) => { + // The first page is ALWAYS the landing page → publishes to index.html + // regardless of the page name/slug. Apache serves '/' from index.html, + // and renaming the first page should not break the root URL. + const filename = i === 0 ? 'index.html' : page.slug + '.html'; let pageHtml = ''; if (page.id === activePageId) { @@ -77,10 +80,13 @@ export function useWhpApi() { // Build pages_craft_state array: for each page, store its craft state // For the currently active page, always use the fresh canvas state (currentCraftState) // since page.craftState may be stale (not updated until page switch) - const pagesGrapesjs = pages.map((page) => ({ + const pagesGrapesjs = pages.map((page, i) => ({ id: page.id, name: page.name, - slug: page.slug, + // Pin the landing page's slug to 'index' on the wire too, so that on + // reload the editor's clean-URL routing (.htaccess rewrite of /name → + // name.html) lines up with the file we just wrote (index.html). + slug: i === 0 ? 'index' : page.slug, craftState: page.id === activePageId ? currentCraftState : (page.craftState || null), })); diff --git a/craft/src/panels/left/PagesPanel.tsx b/craft/src/panels/left/PagesPanel.tsx index e9ecffe..c982b7f 100644 --- a/craft/src/panels/left/PagesPanel.tsx +++ b/craft/src/panels/left/PagesPanel.tsx @@ -145,7 +145,9 @@ export const PagesPanel: React.FC = () => { {/* Page list */} - {pages.map((page) => ( + {pages.map((page, pageIndex) => { + const isLanding = pageIndex === 0; + return (
{editingId === page.id ? ( /* Editing mode */ @@ -176,14 +178,25 @@ export const PagesPanel: React.FC = () => { if (e.key === 'Escape') setEditingId(null); }} /> - setEditSlug(e.target.value)} - placeholder="page-slug" - className="control-input" - style={{ fontSize: 11 }} - /> + {isLanding ? ( +
+ Landing page — URL locked to / +
+ ) : ( + setEditSlug(e.target.value)} + placeholder="page-slug" + className="control-input" + style={{ fontSize: 11 }} + /> + )}
{ marginTop: 2, }} > - /{page.slug} + {isLanding ? '/' : '/' + page.slug}
{ > ✎ - {pages.length > 1 && ( + {pages.length > 1 && !isLanding && (
)} - ))} + ); + })} {/* Add page section */} {isAdding ? ( diff --git a/craft/src/state/PageContext.tsx b/craft/src/state/PageContext.tsx index 34b7823..5e1ff46 100644 --- a/craft/src/state/PageContext.tsx +++ b/craft/src/state/PageContext.tsx @@ -276,9 +276,14 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) => const renamePage = useCallback((pageId: string, name: string, slug: string) => { setPages((prev) => - prev.map((p) => - p.id === pageId ? { ...p, name, slug: slug || slugify(name) } : p, - ), + prev.map((p, i) => { + if (p.id !== pageId) return p; + // First page is the landing page — its slug is locked to 'index' so + // the file always publishes to index.html regardless of the user-set + // name. The display name can change freely. + const nextSlug = i === 0 ? 'index' : (slug || slugify(name)); + return { ...p, name, slug: nextSlug }; + }), ); }, []); @@ -294,10 +299,13 @@ export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) => /** Allow external code (e.g., load from API) to restore pages with craft states */ const setPagesCraftState = useCallback((pagesData: { id: string; name: string; slug: string; craftState: string | null }[]) => { - setPages(pagesData.map((p) => ({ + setPages(pagesData.map((p, i) => ({ id: p.id, name: p.name, - slug: p.slug, + // Heal legacy projects whose first page was saved with slug='home' (or + // any other) before the landing-page rule existed. The first page is + // ALWAYS the landing page → slug 'index' → file index.html. + slug: i === 0 ? 'index' : p.slug, craftState: p.craftState, headCode: '', })));