Skip to content

Commit 0e11434

Browse files
author
Herve Tribouilloy
committed
feat(banner): switch widget config to external JSON contract
- load config via data-contract attribute - async bootstrap using loadContract - refactor useWidgetConfig hook - enable CORS for /cdn/ endpoint
1 parent 45fa991 commit 0e11434

10 files changed

Lines changed: 146 additions & 80 deletions

File tree

docker/nginx_vite/default.conf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,10 @@ server {
2323
add_header Cache-Control "public, max-age=31536000";
2424
try_files $uri =404;
2525
}
26+
27+
location /cdn/ {
28+
add_header Access-Control-Allow-Origin * always;
29+
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
30+
add_header Access-Control-Allow-Headers "Content-Type" always;
31+
}
2632
}

vite_project/index.html

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,7 @@
44
<div class="banner">
55
<div data-load="on-scroll:100"
66
data-src="https://widget.banner.co.uk:8442/widget-banner.iife.js">
7-
<banner-widget>
8-
<script type="application/json" data-config>
9-
{
10-
"data": {
11-
"slides": [
12-
{
13-
"image": {
14-
"src": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/banner-roofing-solutions.jpg",
15-
"srcset": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/banner-roofing-solutions.jpg 479w, https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/banner-roofing-solutions-small.jpg 359w",
16-
"alt": "Roofing Solutions for more energy in your house"
17-
}
18-
},
19-
{
20-
"image": {
21-
"src": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/roof-solar.jpg",
22-
"srcset": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/roof-solar.jpg 479w, https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/roof-solar-small.jpg 359w",
23-
"alt": "Tim Fudge owner of Southern Solar Limited"
24-
}
25-
},
26-
{
27-
"image": {
28-
"src": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/house-solar.jpg",
29-
"srcset": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/house-solar.jpg 479w, https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/house-solar-small.jpg 359w",
30-
"alt": "A Dorset local specialist in Solar Energy Solutions"
31-
}
32-
}
33-
]
34-
},
35-
"settings": {
36-
"mode": {
37-
"desktop": "static",
38-
"tablet": "slider",
39-
"mobile": "slider"
40-
},
41-
"height": "300px"
42-
}
43-
}
44-
</script>
45-
</banner-widget>
7+
<banner-widget data-contract="/cdn/uk.json" />
468
</div>
479
</div>
4810

vite_project/package-lock.json

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

vite_project/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "widget-banner",
33
"private": true,
4-
"version": "0.1.3",
4+
"version": "0.2.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

vite_project/public/cdn/fr.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"data": {
3+
"slides": [
4+
{
5+
"image": {
6+
"src": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/banner-roofing-solutions.jpg",
7+
"srcset": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/banner-roofing-solutions.jpg 479w, https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/banner-roofing-solutions-small.jpg 359w",
8+
"alt": "Roofing Solutions for more energy in your house"
9+
}
10+
},
11+
{
12+
"image": {
13+
"src": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/roof-solar.jpg",
14+
"srcset": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/roof-solar.jpg 479w, https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/roof-solar-small.jpg 359w",
15+
"alt": "Tim Fudge owner of Southern Solar Limited"
16+
}
17+
},
18+
{
19+
"image": {
20+
"src": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/house-solar.jpg",
21+
"srcset": "https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/house-solar.jpg 479w, https://southernsolar.com/wp-content/themes/southernsolar/images/home-banner/house-solar-small.jpg 359w",
22+
"alt": "A Dorset local specialist in Solar Energy Solutions"
23+
}
24+
}
25+
]
26+
},
27+
"settings": {
28+
"mode": {
29+
"desktop": "static",
30+
"tablet": "slider",
31+
"mobile": "slider"
32+
},
33+
"height": "300px"
34+
}
35+
}

vite_project/public/cdn/uk.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"data": {
3+
"slides": [
4+
{
5+
"image": {
6+
"src": "https://www.reactedge.net/wp-content/themes/digitalrisedorset/images/banner/hero-orange.webp",
7+
"srcset": "https://www.reactedge.net/wp-content/themes/digitalrisedorset/images/banner/hero-orange.webp 479w, https://www.reactedge.net/wp-content/themes/digitalrisedorset/images/banner/hero-orange-365.webp 359w",
8+
"alt": "Vitalising your system without rebuilding"
9+
}
10+
},
11+
{
12+
"image": {
13+
"src": "https://www.reactedge.net/wp-content/themes/digitalrisedorset/images/banner/dev-talk.webp",
14+
"srcset": "https://www.reactedge.net/wp-content/themes/digitalrisedorset/images/banner/dev-talk.webp 479w, https://www.reactedge.net/wp-content/themes/digitalrisedorset/images/banner/dev-talk-365.webp 359w",
15+
"alt": "Successful Dev Talk in Dorset with dev.talk bournemouth group"
16+
}
17+
}
18+
],
19+
"settings": {
20+
"mode": {
21+
"desktop": "static",
22+
"tablet": "slider",
23+
"mobile": "slider"
24+
},
25+
"height": "400px"
26+
}
27+
}
28+
}

vite_project/src/BannerConfig.ts

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,19 @@
1-
import {type BannerSettingConfig, type BannerSlide, defaultBannerConfig} from "./components/Types.ts";
1+
import {type BannerSettingConfig, type BannerSlide} from "./components/Types.ts";
2+
import {loadContract} from "./widget-runtime/lib/contractLoader.ts";
3+
import {activity} from "./activity";
24

35
export interface BannerWidgetConfig {
4-
/**
5-
* Structured banner payload.
6-
* Shape is banner-owned and opaque to the platform.
7-
*/
86
readonly slides: BannerSlide[]
97

108
readonly settings: BannerSettingConfig;
119
}
1210

13-
export function readWidgetConfig(
11+
export async function readWidgetConfig(
1412
hostElement: HTMLElement
15-
): BannerWidgetConfig {
16-
const configScript = hostElement.querySelector<HTMLScriptElement>(
17-
'script[type="application/json"][data-config]'
18-
);
13+
): Promise<BannerWidgetConfig | null> {
14+
const contract = await loadContract(hostElement);
1915

20-
if (!configScript) {
21-
throw new Error("USP widget requires a <script data-config> block.");
22-
}
16+
activity('bootstrap', 'Config resolved', contract.data);
2317

24-
try {
25-
const parsed = JSON.parse(configScript.textContent || "{}");
26-
27-
return Object.freeze({
28-
slides: parsed.data.slides ?? [],
29-
settings: parsed.settings ?? defaultBannerConfig,
30-
});
31-
} catch {
32-
return {
33-
slides: [],
34-
settings: defaultBannerConfig
35-
};
36-
}
37-
}
18+
return Object.freeze(contract.data);
19+
}

vite_project/src/components/BannerWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ type Props = {
77
}
88

99
export const BannerWidget = ({host}: Props) => {
10-
const config = useWidgetConfig(host);
10+
const {config} = useWidgetConfig(host);
1111

1212
if (!config) return null;
1313

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,54 @@
1-
import {useMemo} from "react";
21
import {type BannerWidgetConfig, readWidgetConfig} from "../BannerConfig.ts";
32
import {activity} from "../activity";
3+
import {useEffect, useState} from "react";
44

55
export function useWidgetConfig(
66
host: HTMLElement
7-
): BannerWidgetConfig | null {
8-
return useMemo(() => {
9-
const baseConfig = readWidgetConfig(host);
10-
if (!baseConfig) {
11-
activity('bootstrap', 'Widget is not correctly configured', null, 'error');
12-
return null;
7+
): {
8+
config: BannerWidgetConfig | null;
9+
error: Error | null;
10+
loading: boolean;
11+
} {
12+
13+
const [config, setConfig] = useState<BannerWidgetConfig | null>(null);
14+
const [error, setError] = useState<Error | null>(null);
15+
const [loading, setLoading] = useState(true);
16+
17+
useEffect(() => {
18+
let cancelled = false;
19+
20+
async function bootstrap() {
21+
try {
22+
setLoading(true);
23+
const resolved = await readWidgetConfig(host);
24+
25+
if (!cancelled) {
26+
setConfig(resolved);
27+
setError(null);
28+
}
29+
} catch (err) {
30+
activity('bootstrap', 'Config error', {
31+
error: (err as Error).message
32+
});
33+
34+
if (!cancelled) {
35+
setError(err as Error);
36+
setConfig(null);
37+
}
38+
} finally {
39+
if (!cancelled) setLoading(false);
40+
}
1341
}
1442

15-
activity('bootstrap', 'Widget config', baseConfig);
43+
bootstrap();
44+
45+
return () => {
46+
cancelled = true;
47+
};
1648

17-
return baseConfig
1849
}, [host]);
19-
}
2050

51+
return { config, error, loading };
52+
}
2153

2254

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {activity} from "../../activity";
2+
3+
export async function loadContract(hostElement: HTMLElement) {
4+
const contractUrl = hostElement.getAttribute("data-contract");
5+
6+
if (!contractUrl) {
7+
throw new Error("Missing data-contract attribute");
8+
}
9+
10+
const response = await fetch(contractUrl);
11+
12+
if (!response.ok) {
13+
activity('bootstrap', 'Config error', {res: response});
14+
15+
throw new Error(`Failed to load contract: ${response.status}`);
16+
}
17+
18+
const json = await response.json();
19+
20+
return json;
21+
}

0 commit comments

Comments
 (0)