weg-shared-layout 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/components/weg-footer/weg-footer.d.ts +32 -47
- package/dist/types/components.d.ts +5 -79
- package/dist/weg-shared-layout/index-C8BdwtPR.js +4625 -0
- package/dist/weg-shared-layout/index-C8BdwtPR.js.map +1 -0
- package/dist/weg-shared-layout/index.esm.js +14 -1
- package/dist/weg-shared-layout/index.esm.js.map +1 -0
- package/dist/weg-shared-layout/my-component.entry.js +34 -0
- package/dist/weg-shared-layout/my-component.entry.js.map +1 -0
- package/dist/{cjs/index.cjs.js → weg-shared-layout/utils-DhW431pq.js} +4 -3
- package/dist/weg-shared-layout/utils-DhW431pq.js.map +1 -0
- package/dist/{esm/my-component_2.entry.js → weg-shared-layout/weg-footer.entry.js} +62 -138
- package/dist/weg-shared-layout/weg-footer.entry.js.map +1 -0
- package/dist/weg-shared-layout/weg-shared-layout.esm.js +50 -1
- package/dist/weg-shared-layout/weg-shared-layout.esm.js.map +1 -0
- package/package.json +1 -1
- package/readme.md +242 -70
- package/dist/cjs/app-globals-V2Kpy_OQ.js +0 -5
- package/dist/cjs/index-CmiaQ_Dj.js +0 -1612
- package/dist/cjs/loader.cjs.js +0 -13
- package/dist/cjs/my-component_2.cjs.entry.js +0 -249
- package/dist/cjs/weg-shared-layout.cjs.js +0 -25
- package/dist/collection/collection-manifest.json +0 -14
- package/dist/collection/components/my-component/my-component.cmp.test.js +0 -27
- package/dist/collection/components/my-component/my-component.css +0 -3
- package/dist/collection/components/my-component/my-component.js +0 -95
- package/dist/collection/components/weg-footer/icons/instagram.svg +0 -5
- package/dist/collection/components/weg-footer/icons/linkedin.svg +0 -3
- package/dist/collection/components/weg-footer/icons/tiktok.svg +0 -3
- package/dist/collection/components/weg-footer/icons/youtube.svg +0 -4
- package/dist/collection/components/weg-footer/weg-footer.css +0 -236
- package/dist/collection/components/weg-footer/weg-footer.js +0 -412
- package/dist/collection/index.js +0 -10
- package/dist/collection/utils/utils.js +0 -3
- package/dist/collection/utils/utils.unit.test.js +0 -16
- package/dist/components/index.js +0 -1
- package/dist/components/my-component.js +0 -1
- package/dist/components/p-BTQYW5OR.js +0 -1
- package/dist/components/weg-footer.js +0 -1
- package/dist/esm/app-globals-DQuL1Twl.js +0 -3
- package/dist/esm/index-QiJxC4Ow.js +0 -1606
- package/dist/esm/index.js +0 -5
- package/dist/esm/loader.js +0 -11
- package/dist/esm/weg-shared-layout.js +0 -21
- package/dist/index.cjs.js +0 -1
- package/dist/index.js +0 -1
- package/dist/weg-shared-layout/p-67d9c345.entry.js +0 -1
- package/dist/weg-shared-layout/p-DQuL1Twl.js +0 -1
- package/dist/weg-shared-layout/p-QiJxC4Ow.js +0 -2
package/readme.md
CHANGED
|
@@ -10,52 +10,206 @@ Install:
|
|
|
10
10
|
npm i weg-shared-layout
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
`<weg-footer>` is a **dumb / presentational** Web Component. It does **not** fetch data itself. Your host app calls the layout API, then passes the resulting object into the component via the `data` prop.
|
|
16
|
+
|
|
17
|
+
This keeps the component framework-agnostic, lets you share the same response with `<weg-header>` (coming soon), and means HTTP concerns like auth, caching, retries, SSR, and error handling live in your app — where they belong.
|
|
18
|
+
|
|
19
|
+
## Layout API
|
|
20
|
+
|
|
21
|
+
Call this endpoint from your host app to retrieve the data:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
GET https://weg-payload-test.vercel.app/api/layout
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Response shape:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"header": {},
|
|
32
|
+
"footer": {
|
|
33
|
+
"social": [
|
|
34
|
+
{ "platform": "LinkedIn", "href": "https://www.linkedin.com/" },
|
|
35
|
+
{ "platform": "Instagram", "href": "https://www.instagram.com/" },
|
|
36
|
+
{ "platform": "TikTok", "href": "https://www.tiktok.com/" },
|
|
37
|
+
{ "platform": "YouTube", "href": "https://www.youtube.com/" }
|
|
38
|
+
],
|
|
39
|
+
"standardLinks": [
|
|
40
|
+
{ "label": "About Us", "href": "/about" },
|
|
41
|
+
{ "label": "Privacy Policy", "href": "/privacy" },
|
|
42
|
+
{ "label": "Terms of Use", "href": "/terms" },
|
|
43
|
+
{ "label": "Cookie Policy", "href": "/cookies" },
|
|
44
|
+
{ "label": "Accessibility Statement", "href": "/accessibility" }
|
|
45
|
+
],
|
|
46
|
+
"credits": "Warwick Employment Group is a department of the Campus and Commercial Services Group at the University of Warwick.",
|
|
47
|
+
"copyright": "Copyright © Warwick Employment Group."
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Only `social.platform` values of `LinkedIn`, `Instagram`, `TikTok`, and `YouTube` render an icon. Items with missing/invalid fields are silently dropped.
|
|
53
|
+
|
|
13
54
|
## Using in Angular
|
|
14
55
|
|
|
15
|
-
|
|
56
|
+
This guide assumes a modern Angular project (v17+, **standalone components**) — the default for `ng new`. There's an `NgModule` section further down for older apps.
|
|
57
|
+
|
|
58
|
+
### 1. Install the package
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npm i weg-shared-layout
|
|
62
|
+
# or: pnpm add weg-shared-layout
|
|
63
|
+
# or: yarn add weg-shared-layout
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 2. Register the custom elements (once, at startup)
|
|
67
|
+
|
|
68
|
+
Stencil ships a loader that calls `customElements.define()` for every component in the package. Call it **once** before Angular bootstraps your app, otherwise the browser sees `<weg-footer>` as an unknown element and renders nothing.
|
|
69
|
+
|
|
70
|
+
Edit `src/main.ts`:
|
|
16
71
|
|
|
17
72
|
```ts
|
|
73
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
18
74
|
import { defineCustomElements } from 'weg-shared-layout/loader';
|
|
19
75
|
|
|
76
|
+
import { appConfig } from './app/app.config';
|
|
77
|
+
import { App } from './app/app';
|
|
78
|
+
|
|
79
|
+
// MUST run before bootstrapApplication so the browser recognises <weg-footer>
|
|
80
|
+
// by the time Angular's renderer touches the DOM.
|
|
20
81
|
defineCustomElements();
|
|
82
|
+
|
|
83
|
+
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> If you don't see your footer in the page, 9 times out of 10 it's because this step was skipped. Verify by typing `customElements.get('weg-footer')` in the browser DevTools console — it should return a class, not `undefined`.
|
|
87
|
+
|
|
88
|
+
### 3. Provide `HttpClient`
|
|
89
|
+
|
|
90
|
+
You need `HttpClient` to call the layout API. In `src/app/app.config.ts`:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
|
|
94
|
+
import { provideHttpClient } from '@angular/common/http';
|
|
95
|
+
import { provideRouter } from '@angular/router';
|
|
96
|
+
|
|
97
|
+
import { routes } from './app.routes';
|
|
98
|
+
|
|
99
|
+
export const appConfig: ApplicationConfig = {
|
|
100
|
+
providers: [
|
|
101
|
+
provideBrowserGlobalErrorListeners(),
|
|
102
|
+
provideRouter(routes),
|
|
103
|
+
provideHttpClient(),
|
|
104
|
+
],
|
|
105
|
+
};
|
|
21
106
|
```
|
|
22
107
|
|
|
23
|
-
|
|
108
|
+
### 4. Tell Angular to allow unknown elements
|
|
109
|
+
|
|
110
|
+
Without this, Angular's template compiler throws:
|
|
111
|
+
|
|
112
|
+
> `'weg-footer' is not a known element: ... If 'weg-footer' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message.`
|
|
113
|
+
|
|
114
|
+
In **standalone components** (default in Angular 17+), add `schemas: [CUSTOM_ELEMENTS_SCHEMA]` to the `@Component` decorator of **every component that uses `<weg-footer>` in its template**. Easiest is to add it to your root `App` component:
|
|
24
115
|
|
|
25
116
|
```ts
|
|
26
|
-
|
|
117
|
+
// src/app/app.ts
|
|
118
|
+
import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, OnInit, signal } from '@angular/core';
|
|
119
|
+
import { HttpClient } from '@angular/common/http';
|
|
120
|
+
import { RouterOutlet } from '@angular/router';
|
|
27
121
|
|
|
28
|
-
|
|
29
|
-
|
|
122
|
+
const LAYOUT_API = 'https://weg-payload-test.vercel.app/api/layout';
|
|
123
|
+
|
|
124
|
+
@Component({
|
|
125
|
+
selector: 'app-root',
|
|
126
|
+
imports: [RouterOutlet],
|
|
30
127
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
128
|
+
templateUrl: './app.html',
|
|
129
|
+
styleUrl: './app.css',
|
|
31
130
|
})
|
|
32
|
-
export class
|
|
131
|
+
export class App implements OnInit {
|
|
132
|
+
protected readonly layoutData = signal<unknown>(null);
|
|
133
|
+
|
|
134
|
+
private readonly http = inject(HttpClient);
|
|
135
|
+
|
|
136
|
+
ngOnInit(): void {
|
|
137
|
+
this.http.get(LAYOUT_API).subscribe({
|
|
138
|
+
next: (data) => this.layoutData.set(data),
|
|
139
|
+
error: (err) => console.error('Failed to load layout data', err),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
33
143
|
```
|
|
34
144
|
|
|
35
|
-
|
|
145
|
+
> `CUSTOM_ELEMENTS_SCHEMA` is **per-component** in standalone Angular. If you use `<weg-footer>` inside a child component's template, that child must declare `schemas: [CUSTOM_ELEMENTS_SCHEMA]` too. It does **not** cascade through `<router-outlet />`.
|
|
146
|
+
|
|
147
|
+
### 5. Pass the data into `<weg-footer>` via the `[data]` binding
|
|
148
|
+
|
|
149
|
+
In `src/app/app.html`:
|
|
36
150
|
|
|
37
151
|
```html
|
|
38
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
company-number="12345678"
|
|
42
|
-
social-links-src="/assets/footer-social-links.json"
|
|
43
|
-
standard-links-src="/assets/footer-standard-links.json"
|
|
44
|
-
></weg-footer>
|
|
152
|
+
<router-outlet />
|
|
153
|
+
|
|
154
|
+
<weg-footer [data]="layoutData()"></weg-footer>
|
|
45
155
|
```
|
|
46
156
|
|
|
47
|
-
|
|
157
|
+
That's the whole integration. `<weg-footer>` watches its `data` prop and re-renders when the signal value changes (e.g. when the HTTP response arrives), so an initial `null` is fine — the footer will simply render empty until the data lands.
|
|
48
158
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
159
|
+
#### Why `[data]` and not `data-src` or `[attr.data]`
|
|
160
|
+
|
|
161
|
+
- `[data]="..."` is Angular's **property binding** — it sets the JS property `data` on the underlying DOM element. Stencil exposes `@Prop()` values as JS properties, so this passes the object straight through. **This is what you want.**
|
|
162
|
+
- `[attr.data]="..."` would set an HTML attribute, which is always a string — Angular would call `JSON.stringify(layoutData)` for you, the component would have to parse it back, and you'd lose type fidelity. Avoid unless you have a reason.
|
|
163
|
+
- Plain `data="..."` only works for a static string literal and would have the same parsing cost — fine for testing, not for real data.
|
|
164
|
+
|
|
165
|
+
### 6. Verify it works
|
|
166
|
+
|
|
167
|
+
After running `ng serve`, open the page and check:
|
|
168
|
+
|
|
169
|
+
1. The dark footer bar renders at the bottom of the page.
|
|
170
|
+
2. DevTools → Network tab shows a `200 OK` to `https://weg-payload-test.vercel.app/api/layout`.
|
|
171
|
+
3. DevTools → Elements → click `<weg-footer>` → in the Console, type `$0.data` — you should see the JSON object you fetched.
|
|
172
|
+
4. Expand `<weg-footer>` in the Elements panel — you should see a `#shadow-root (open)` containing the actual `<footer>` markup.
|
|
173
|
+
|
|
174
|
+
### Troubleshooting
|
|
175
|
+
|
|
176
|
+
| Symptom | Cause / fix |
|
|
177
|
+
| --- | --- |
|
|
178
|
+
| `'weg-footer' is not a known element` build error | Add `schemas: [CUSTOM_ELEMENTS_SCHEMA]` to the `@Component` decorator of the component whose template uses `<weg-footer>`. |
|
|
179
|
+
| Element renders as plain inline text / empty box | `defineCustomElements()` was never called. Add it to `main.ts` **before** `bootstrapApplication`. |
|
|
180
|
+
| Footer is in the DOM but blank | The HTTP request failed or returned the wrong shape. Check DevTools → Network → confirm the response matches the [API shape](#layout-api). Also inspect `$0.data` in the console to confirm the binding made it onto the element. |
|
|
181
|
+
| `NullInjectorError: No provider for HttpClient` | You forgot `provideHttpClient()` in `app.config.ts`. |
|
|
182
|
+
| CORS error in the console | The API host must allow your origin. Either fix CORS on the backend, or proxy through Angular's dev proxy (`proxy.conf.json`) so the call becomes same-origin. |
|
|
183
|
+
| Footer doesn't update after data changes | Make sure you're using `[data]="layoutData()"` with a signal (or `[data]="layoutData$ | async"` with an Observable) so Angular pushes new values into the element. |
|
|
184
|
+
| Works locally, fails in SSR (`document is not defined`) | `defineCustomElements()` touches `window` / `customElements`. Wrap in `if (typeof window !== 'undefined') { ... }` or use `isPlatformBrowser(platformId)` in `main.ts`. |
|
|
185
|
+
| TypeScript: `Property 'weg-footer' does not exist on type 'HTMLElementTagNameMap'` | See the typings section below. |
|
|
186
|
+
|
|
187
|
+
### TypeScript typings (Angular)
|
|
188
|
+
|
|
189
|
+
If your editor / TS service complains about the unknown element or the `[data]` binding, add this to `src/global.d.ts` (creating the file if needed) and make sure it's included by `tsconfig.json`:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
/// <reference types="weg-shared-layout/dist/types/components" />
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Legacy: using with `NgModule`
|
|
196
|
+
|
|
197
|
+
If your app is still using `NgModule` (pre-Angular 17 default), add `CUSTOM_ELEMENTS_SCHEMA` once at the module level instead of per component:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
// src/app/app.module.ts
|
|
201
|
+
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
|
202
|
+
import { HttpClientModule } from '@angular/common/http';
|
|
203
|
+
|
|
204
|
+
@NgModule({
|
|
205
|
+
imports: [HttpClientModule /* ... */],
|
|
206
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
207
|
+
})
|
|
208
|
+
export class AppModule {}
|
|
57
209
|
```
|
|
58
210
|
|
|
211
|
+
`defineCustomElements()` in `main.ts` is still required either way. Inject `HttpClient` in your component the same way as the standalone example above.
|
|
212
|
+
|
|
59
213
|
## Using in React
|
|
60
214
|
|
|
61
215
|
Install:
|
|
@@ -80,70 +234,66 @@ Or:
|
|
|
80
234
|
import 'weg-shared-layout';
|
|
81
235
|
```
|
|
82
236
|
|
|
83
|
-
|
|
237
|
+
Then fetch the data in your component and pass it in via a ref (because the `data` prop is an object, not a string attribute):
|
|
84
238
|
|
|
85
|
-
|
|
239
|
+
```tsx
|
|
240
|
+
import { useEffect, useRef, useState } from 'react';
|
|
241
|
+
import 'weg-shared-layout/weg-footer';
|
|
86
242
|
|
|
87
|
-
|
|
243
|
+
const LAYOUT_API = 'https://weg-payload-test.vercel.app/api/layout';
|
|
88
244
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
245
|
+
export function SiteFooter() {
|
|
246
|
+
const ref = useRef<HTMLElement>(null);
|
|
247
|
+
const [layout, setLayout] = useState<unknown>(null);
|
|
248
|
+
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
fetch(LAYOUT_API).then((r) => r.json()).then(setLayout);
|
|
251
|
+
}, []);
|
|
92
252
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
variant="standard"
|
|
99
|
-
company-name="WEG"
|
|
100
|
-
company-number="12345678"
|
|
101
|
-
social-links-src="/assets/footer-social-links.json"
|
|
102
|
-
standard-links-src="/assets/footer-standard-links.json"
|
|
103
|
-
/>
|
|
104
|
-
);
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (ref.current) (ref.current as any).data = layout;
|
|
255
|
+
}, [layout]);
|
|
256
|
+
|
|
257
|
+
return <weg-footer ref={ref} />;
|
|
105
258
|
}
|
|
106
259
|
```
|
|
107
260
|
|
|
108
|
-
|
|
261
|
+
> React (pre-19) sets unknown JSX attributes as HTML attributes, which would stringify your object. Assigning via a ref guarantees you set the JS property. React 19+ handles this correctly without the ref dance.
|
|
109
262
|
|
|
110
|
-
|
|
111
|
-
import 'weg-shared-layout/weg-footer';
|
|
263
|
+
### Next.js (App Router) note
|
|
112
264
|
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<weg-footer
|
|
116
|
-
variant="standard"
|
|
117
|
-
company-name="WEG"
|
|
118
|
-
company-number="12345678"
|
|
119
|
-
social-links-src="/assets/footer-social-links.json"
|
|
120
|
-
standard-links-src="/assets/footer-standard-links.json"
|
|
121
|
-
/>
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
```
|
|
265
|
+
Stencil custom elements must be registered **in the browser**. In Next.js `app/` (App Router), don't import/register Stencil components from a Server Component.
|
|
125
266
|
|
|
126
|
-
|
|
267
|
+
Instead, register them in a Client Component, for example:
|
|
127
268
|
|
|
128
269
|
```tsx
|
|
270
|
+
// app/components/SiteFooter.tsx
|
|
271
|
+
"use client";
|
|
272
|
+
|
|
273
|
+
import { useEffect, useRef, useState } from 'react';
|
|
129
274
|
import 'weg-shared-layout/weg-footer';
|
|
130
275
|
|
|
131
|
-
export function
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
276
|
+
export function SiteFooter() {
|
|
277
|
+
const ref = useRef<HTMLElement>(null);
|
|
278
|
+
const [layout, setLayout] = useState<unknown>(null);
|
|
279
|
+
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
fetch('/api/layout').then((r) => r.json()).then(setLayout);
|
|
282
|
+
}, []);
|
|
283
|
+
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (ref.current) (ref.current as any).data = layout;
|
|
286
|
+
}, [layout]);
|
|
287
|
+
|
|
288
|
+
return <weg-footer ref={ref} />;
|
|
141
289
|
}
|
|
142
290
|
```
|
|
143
291
|
|
|
292
|
+
(You can also fetch in a Server Component and pass `layout` down as a prop to the Client Component — but the actual `data` assignment still has to happen in the browser.)
|
|
293
|
+
|
|
144
294
|
### React TypeScript typings
|
|
145
295
|
|
|
146
|
-
Stencil generates `components.d.ts
|
|
296
|
+
Stencil generates `components.d.ts`, but React doesn't pick up custom element typings automatically. Add a `global.d.ts` that references the generated types:
|
|
147
297
|
|
|
148
298
|
```ts
|
|
149
299
|
/// <reference types="weg-shared-layout/dist/types/components" />
|
|
@@ -151,6 +301,28 @@ Stencil generates `components.d.ts` in this repo, but React does not automatical
|
|
|
151
301
|
|
|
152
302
|
If your app still complains about JSX intrinsic elements, you can also augment `JSX.IntrinsicElements` in that same file (varies by React/TS setup).
|
|
153
303
|
|
|
154
|
-
|
|
304
|
+
## Plain HTML / vanilla JS
|
|
305
|
+
|
|
306
|
+
For a quick smoke test outside of a framework, fetch the JSON and assign it to the element's `data` property:
|
|
307
|
+
|
|
308
|
+
```html
|
|
309
|
+
<weg-footer id="footer"></weg-footer>
|
|
310
|
+
|
|
311
|
+
<script type="module">
|
|
312
|
+
const res = await fetch('https://weg-payload-test.vercel.app/api/layout');
|
|
313
|
+
document.getElementById('footer').data = await res.json();
|
|
314
|
+
</script>
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
You can also pass the JSON as a string attribute — useful for SSR or static HTML where you don't want a runtime fetch:
|
|
318
|
+
|
|
319
|
+
```html
|
|
320
|
+
<weg-footer data='{"footer":{"social":[],"standardLinks":[],"credits":"","copyright":"Copyright © WEG."}}'></weg-footer>
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
## CORS
|
|
324
|
+
|
|
325
|
+
Because the host app calls the API directly, **the API must respond with `Access-Control-Allow-Origin` headers** that include your app's origin. If you hit a CORS error in the browser console, either:
|
|
155
326
|
|
|
156
|
-
|
|
327
|
+
- Fix CORS on the API host, **or**
|
|
328
|
+
- Proxy the call through your own host. Angular: configure `proxy.conf.json` so `/api/layout` is rewritten to the production URL. Next.js: add a route handler that re-fetches and returns the JSON.
|