Skip to content

Commit 03c7366

Browse files
authored
feat: create projects with 404 page on all plans (#5309)
Ref #2782 Here I did a few things - allow `/*` path on free plan - allow changing status code on free plan - create "Not Found" page for all new projects https://github.com/user-attachments/assets/1f36882f-3c09-4814-a1ee-d6bdfd468467
1 parent 19a98fa commit 03c7366

File tree

7 files changed

+167
-62
lines changed

7 files changed

+167
-62
lines changed

apps/builder/app/builder/features/pages/page-settings.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,13 +366,11 @@ const StatusField = ({
366366
onChange: (value: undefined | string) => void;
367367
}) => {
368368
const id = useId();
369-
const { allowDynamicData } = useStore($userPlanFeatures);
370369
const { variableValues, scope, aliases } = useStore($pageRootScope);
371370
return (
372371
<Grid gap={1}>
373372
<Flex align="center" gap={1}>
374373
<Label htmlFor={id}>Status Code </Label>
375-
{allowDynamicData === false && <ProBadge>PRO</ProBadge>}
376374
<Tooltip
377375
content={
378376
<Text>
@@ -801,7 +799,7 @@ const FormFields = ({
801799
{allowDynamicData === false && (
802800
<PanelBanner>
803801
<Text>
804-
Dynamic routing, redirect and status code are a part of the CMS
802+
Dynamic routing and redirect are a part of the CMS
805803
functionality.
806804
</Text>
807805
<Flex align="center" gap={1}>

apps/builder/app/builder/features/topbar/publish.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -263,12 +263,10 @@ const $usedProFeatures = computed(
263263
pageId: page.id,
264264
instanceSelector: [page.rootInstanceId],
265265
};
266-
if (isPathnamePattern(page.path)) {
266+
// allow catch all for 404 pages on free plan
267+
if (isPathnamePattern(page.path) && page.path !== "/*") {
267268
features.set("Dynamic path", { awareness, view: "pageSettings" });
268269
}
269-
if (page.meta.status && page.meta.status !== `200`) {
270-
features.set("Page status code", { awareness, view: "pageSettings" });
271-
}
272270
if (page.meta.redirect && page.meta.redirect !== `""`) {
273271
features.set("Redirect", { awareness, view: "pageSettings" });
274272
}

packages/project-build/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@webstudio-is/authorization-token": "workspace:*",
2525
"@webstudio-is/postrest": "workspace:*",
2626
"@webstudio-is/sdk": "workspace:*",
27+
"@webstudio-is/template": "workspace:*",
2728
"@webstudio-is/trpc-interface": "workspace:*",
2829
"nanoid": "^5.1.5",
2930
"zod": "^3.24.2"

packages/project-build/src/db/build.ts

Lines changed: 25 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,31 @@
11
/* eslint no-console: ["error", { allow: ["time", "timeEnd"] }] */
22

3-
import { nanoid } from "nanoid";
43
import type { Database } from "@webstudio-is/postrest/index.server";
54
import {
65
AuthorizationError,
76
authorizeProject,
87
type AppContext,
98
} from "@webstudio-is/trpc-interface/index.server";
109
import { db as authDb } from "@webstudio-is/authorization-token/index.server";
11-
import {
12-
type Deployment,
13-
type Resource,
14-
type StyleSource,
15-
type Prop,
16-
type DataSource,
17-
type Instance,
18-
type Breakpoint,
19-
type StyleSourceSelection,
20-
type StyleDecl,
10+
import type {
11+
Deployment,
12+
Resource,
13+
StyleSource,
14+
Prop,
15+
DataSource,
16+
Instance,
17+
Breakpoint,
18+
StyleSourceSelection,
19+
StyleDecl,
2120
Pages,
22-
initialBreakpoints,
23-
elementComponent,
2421
} from "@webstudio-is/sdk";
2522
import type { Build, CompactBuild } from "../types";
2623
import { parseDeployment } from "./deployment";
27-
import { serializePages } from "./pages";
28-
import { createDefaultPages } from "../shared/pages-utils";
2924
import type { MarketplaceProduct } from "../shared//marketplace";
3025
import { breakCyclesMutable } from "../shared/graph-utils";
26+
import { createPages } from "../template";
27+
import { serializeStyles } from "./styles";
28+
import { serializeStyleSourceSelections } from "./style-source-selections";
3129

3230
const parseCompactData = <Item>(serialized: string) =>
3331
JSON.parse(serialized) as Item[];
@@ -222,35 +220,6 @@ export const loadApprovedProdBuildByProjectId = async (
222220
return parseCompactBuild(build.data[0]);
223221
};
224222

225-
const createNewPageInstances = (): Build["instances"] => {
226-
const instanceId = nanoid();
227-
return [
228-
[
229-
instanceId,
230-
{
231-
type: "instance",
232-
id: instanceId,
233-
component: elementComponent,
234-
tag: "body",
235-
children: [],
236-
},
237-
],
238-
];
239-
};
240-
241-
const createInitialBreakpoints = (): [Breakpoint["id"], Breakpoint][] => {
242-
return initialBreakpoints.map((breakpoint) => {
243-
const id = nanoid();
244-
return [
245-
id,
246-
{
247-
...breakpoint,
248-
id,
249-
},
250-
];
251-
});
252-
};
253-
254223
/*
255224
* We create "dev" build in two cases:
256225
* 1. when we create a new project
@@ -263,16 +232,21 @@ export const createBuild = async (
263232
},
264233
context: AppContext
265234
): Promise<void> => {
266-
const newInstances = createNewPageInstances();
267-
const [rootInstanceId] = newInstances[0];
268-
const defaultPages = createDefaultPages({ rootInstanceId });
269-
235+
const data = createPages();
270236
const newBuild = await context.postgrest.client.from("Build").insert({
271237
id: crypto.randomUUID(),
272238
projectId: props.projectId,
273-
pages: serializePages(defaultPages),
274-
breakpoints: serializeData<Breakpoint>(new Map(createInitialBreakpoints())),
275-
instances: serializeData<Instance>(new Map(newInstances)),
239+
pages: serializeConfig<Pages>(data.pages),
240+
breakpoints: serializeData<Breakpoint>(data.breakpoints),
241+
styles: serializeStyles(data.styles),
242+
styleSources: serializeData<StyleSource>(data.styleSources),
243+
styleSourceSelections: serializeStyleSourceSelections(
244+
data.styleSourceSelections
245+
),
246+
props: serializeData<Prop>(data.props),
247+
dataSources: serializeData<DataSource>(data.dataSources),
248+
resources: serializeData<Resource>(data.resources),
249+
instances: serializeData<Instance>(data.instances),
276250
});
277251
if (newBuild.error) {
278252
throw newBuild.error;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { nanoid } from "nanoid";
2+
import {
3+
initialBreakpoints,
4+
type Pages,
5+
type WebstudioData,
6+
} from "@webstudio-is/sdk";
7+
import { coreTemplates } from "@webstudio-is/sdk/core-templates";
8+
import { css, renderData, ws } from "@webstudio-is/template";
9+
import { createRootFolder } from "./shared/pages-utils";
10+
11+
export const createPages = (): WebstudioData => {
12+
const breakpoints = initialBreakpoints.map((breakpoint) => ({
13+
...breakpoint,
14+
id: nanoid(),
15+
}));
16+
const homePageId = nanoid();
17+
const homeBodyId = nanoid();
18+
const notFoundPageId = nanoid();
19+
const notFoundBodyId = nanoid();
20+
21+
const data = renderData(
22+
<>
23+
{/* home page body */}
24+
<ws.element ws:tag="body" ws:id={homeBodyId}></ws.element>
25+
{/* not found page body */}
26+
<ws.element
27+
ws:tag="body"
28+
ws:id={notFoundBodyId}
29+
ws:style={css`
30+
display: flex;
31+
justify-content: center;
32+
align-items: center;
33+
background-color: #fff;
34+
`}
35+
>
36+
<ws.element ws:tag="div">
37+
<ws.element
38+
ws:tag="div"
39+
ws:style={css`
40+
position: relative;
41+
text-align: center;
42+
font-weight: 900;
43+
font-size: 8rem;
44+
line-height: 1;
45+
letter-spacing: -0.05em;
46+
`}
47+
>
48+
<ws.element ws:tag="div">404</ws.element>
49+
<ws.element
50+
ws:tag="div"
51+
ws:style={css`
52+
position: absolute;
53+
inset: 0 -0.125rem 0 0.125rem;
54+
opacity: 0.3;
55+
`}
56+
>
57+
404
58+
</ws.element>
59+
<ws.element
60+
ws:tag="div"
61+
ws:style={css`
62+
position: absolute;
63+
inset: 0 0.125rem 0 -0.125rem;
64+
opacity: 0.3;
65+
`}
66+
>
67+
404
68+
</ws.element>
69+
<ws.element
70+
ws:tag="div"
71+
ws:style={css`
72+
position: absolute;
73+
top: 50%;
74+
left: 0;
75+
width: 100%;
76+
background-color: #fff;
77+
height: 0.375rem;
78+
`}
79+
></ws.element>
80+
</ws.element>
81+
<ws.element
82+
ws:tag="p"
83+
ws:style={css`
84+
margin-top: 1.5rem;
85+
font-weight: 700;
86+
font-size: 1.5rem;
87+
line-height: 2rem;
88+
letter-spacing: 0.05em;
89+
`}
90+
>
91+
PAGE NOT FOUND
92+
</ws.element>
93+
</ws.element>
94+
{coreTemplates.builtWithWebstudio.template}
95+
</ws.element>
96+
</>,
97+
nanoid,
98+
breakpoints
99+
);
100+
101+
const pages: Pages = {
102+
homePage: {
103+
id: homePageId,
104+
name: "Home",
105+
path: "",
106+
title: `"Home"`,
107+
meta: {},
108+
rootInstanceId: homeBodyId,
109+
},
110+
pages: [
111+
{
112+
id: notFoundPageId,
113+
name: "404",
114+
path: "/*",
115+
title: `"Page not found"`,
116+
meta: {
117+
status: `404`,
118+
excludePageFromSearch: "false",
119+
},
120+
rootInstanceId: notFoundBodyId,
121+
},
122+
],
123+
folders: [createRootFolder([homePageId, notFoundPageId])],
124+
};
125+
126+
return { ...data, pages };
127+
};
128+
129+
createPages();

packages/template/src/jsx.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,12 @@ const getElementChildren = (element: JSX.Element): JSX.Element[] => {
122122

123123
export const renderTemplate = (
124124
root: JSX.Element,
125-
generateId?: () => string
125+
generateId?: () => string,
126+
initialBreakpoints: Breakpoint[] = []
126127
): WebstudioFragment => {
127128
const instances: Instance[] = [];
128129
const props: Prop[] = [];
129-
const breakpoints: Breakpoint[] = [];
130+
const breakpoints = Array.from(initialBreakpoints);
130131
const styleSources: StyleSource[] = [];
131132
const styleSourceSelections: StyleSourceSelection[] = [];
132133
const styles: StyleDecl[] = [];
@@ -417,7 +418,8 @@ export const renderTemplate = (
417418

418419
export const renderData = (
419420
root: JSX.Element,
420-
generateId?: () => string
421+
generateId?: () => string,
422+
initialBreakpoints: Breakpoint[] = []
421423
): Omit<WebstudioData, "pages"> => {
422424
const {
423425
instances,
@@ -429,7 +431,7 @@ export const renderData = (
429431
dataSources,
430432
resources,
431433
assets,
432-
} = renderTemplate(root, generateId);
434+
} = renderTemplate(root, generateId, initialBreakpoints);
433435
return {
434436
instances: new Map(instances.map((item) => [item.id, item])),
435437
props: new Map(props.map((item) => [item.id, item])),

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)