@undp/create-app 0.91.0 → 0.91.2
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/README.md +5 -3
- package/bin/generateFiles/generateIndexHtml.js +13 -5
- package/bin/generateFiles/generatePackageJson.js +1 -0
- package/bin/generateFiles/generateStaticWebAppConfig.js +15 -5
- package/bin/generateFiles/generateViteConfig.js +6 -0
- package/bin/promptUser.js +146 -127
- package/bin/templates/next/auth/README.md +17 -0
- package/bin/templates/next/basic/README.md +17 -0
- package/bin/templates/vite/basic/.github/ISSUE_TEMPLATE/launch-checklist.md +73 -0
- package/bin/templates/vite/basic/README.md +40 -5
- package/bin/templates/vite/query/.github/ISSUE_TEMPLATE/launch-checklist.md +73 -0
- package/bin/templates/vite/query/README.md +39 -5
- package/bin/templates/vite/query+router/.github/ISSUE_TEMPLATE/launch-checklist.md +73 -0
- package/bin/templates/vite/query+router/README.md +17 -0
- package/bin/templates/vite/router/.github/ISSUE_TEMPLATE/launch-checklist.md +73 -0
- package/bin/templates/vite/router/README.md +17 -0
- package/bin/templates/viteWithLanguage/basic/.github/ISSUE_TEMPLATE/launch-checklist.md +73 -0
- package/bin/templates/viteWithLanguage/basic/README.md +52 -5
- package/bin/templates/viteWithLanguage/query/.github/ISSUE_TEMPLATE/launch-checklist.md +73 -0
- package/bin/templates/viteWithLanguage/query/README.md +53 -5
- package/bin/templates/viteWithLanguage/query+router/.github/ISSUE_TEMPLATE/launch-checklist.md +72 -0
- package/bin/templates/viteWithLanguage/query+router/README.md +17 -0
- package/bin/templates/viteWithLanguage/router/.github/ISSUE_TEMPLATE/launch-checklist.md +73 -0
- package/bin/templates/viteWithLanguage/router/README.md +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,11 +27,13 @@ All setups include **Tailwind CSS** for styling and **Zustand** for state manage
|
|
|
27
27
|
|
|
28
28
|
| Framework | Variants | Description | Internationalization Available |
|
|
29
29
|
|------------|-----------|-------------|-------------|
|
|
30
|
-
| **Vite** | Basic | Minimal React + TypeScript setup | ✅ |
|
|
31
|
-
| **Vite** | + Router | Adds TanStack Router for routing | ✅ |
|
|
30
|
+
| **Vite** ⭐ Recommended | Basic | Minimal React + TypeScript setup | ✅ |
|
|
31
|
+
| **Vite** ⭐ Recommended | + Router | Adds TanStack Router for routing | ✅ |
|
|
32
32
|
| **Next.js** | Basic | SSR and file-based routing | ❌ |
|
|
33
33
|
| **Next.js** | + Auth | Includes authentication via Better Auth | ❌ |
|
|
34
34
|
|
|
35
|
+
> **Note:** Vite is the primary focus of this scaffolding. Its setups are the most mature and feature-complete, including i18n support. The Next.js setups are more experimental and may lag behind.
|
|
36
|
+
|
|
35
37
|
---
|
|
36
38
|
|
|
37
39
|
## 📦 Installation
|
|
@@ -54,4 +56,4 @@ cd my-undp-app && npm run dev
|
|
|
54
56
|
|
|
55
57
|
*Please note: If you have not installed the dependencies then you might want to run `npm install` before `npm run dev`.*
|
|
56
58
|
|
|
57
|
-
*Please note: The scaffolding tool installs Proxima Nova and Sonhe Breit fonts; you can only use these fonts if you have a license or are using the app as UNDP sub-domain.*
|
|
59
|
+
*Please note: The scaffolding tool installs Proxima Nova and Sonhe Breit fonts; you can only use these fonts if you have a license or are using the app as UNDP sub-domain.*
|
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
export function generateIndexHtml(config) {
|
|
2
|
-
|
|
2
|
+
return `<!doctype html>
|
|
3
3
|
<html lang="en">
|
|
4
4
|
<head>
|
|
5
5
|
<meta charset="UTF-8" />
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
|
7
|
+
|
|
7
8
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
8
9
|
<title>${config.projectName}</title>
|
|
10
|
+
<link rel="preload" href="/fonts/proximanova-regular-webfont.woff2" as="font" type="font/woff2" crossorigin />
|
|
11
|
+
<link rel="preload" href="/fonts/proximanova-semibold-webfont.woff2" as="font" type="font/woff2" crossorigin />
|
|
12
|
+
<link rel="preload" href="/fonts/soehne-breit-web-fett.woff2" as="font" type="font/woff2" crossorigin />
|
|
9
13
|
</head>
|
|
10
14
|
<body>
|
|
11
|
-
${
|
|
15
|
+
${
|
|
16
|
+
!config.addPostCSSScripts
|
|
17
|
+
? `<div id="root"></div>`
|
|
18
|
+
: `<div class="undp-container">
|
|
12
19
|
<div id="root"></div>
|
|
13
|
-
</div>`
|
|
20
|
+
</div>`
|
|
21
|
+
}
|
|
14
22
|
<script type="module" src="/src/main.tsx"></script>
|
|
15
23
|
</body>
|
|
16
24
|
</html>
|
|
17
|
-
|
|
18
|
-
}
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export function generateStaticWebAppConfig() {
|
|
2
|
-
|
|
2
|
+
return `{
|
|
3
3
|
"globalHeaders": {
|
|
4
4
|
"Access-Control-Allow-Origin": "*",
|
|
5
|
-
"Access-Control-Allow-Methods": "GET, OPTIONS"
|
|
5
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
6
|
+
'Strict-Transport-Security': 'max-age=31536000',
|
|
7
|
+
'X-Content-Type-Options': 'nosniff',
|
|
8
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
6
9
|
},
|
|
7
10
|
"routes": [
|
|
8
11
|
{
|
|
@@ -36,12 +39,19 @@ export function generateStaticWebAppConfig() {
|
|
|
36
39
|
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
37
40
|
"Cache-Control": "no-cache"
|
|
38
41
|
}
|
|
39
|
-
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"route": "/assets/*",
|
|
45
|
+
"headers": {
|
|
46
|
+
"Access-Control-Allow-Origin": "*",
|
|
47
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
48
|
+
"Cache-Control": "public, max-age=31536000, immutable"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
40
51
|
],
|
|
41
52
|
"navigationFallback": {
|
|
42
53
|
"rewrite": "/index.html",
|
|
43
54
|
"exclude": ["/assets/*", "/*.js", "/*.css", "/*.png", "/*.jpg", "/*.svg"]
|
|
44
55
|
}
|
|
45
|
-
}
|
|
56
|
+
}`;
|
|
46
57
|
}
|
|
47
|
-
|
|
@@ -4,6 +4,7 @@ import { defineConfig } from 'vite';
|
|
|
4
4
|
import react from '@vitejs/plugin-react';
|
|
5
5
|
// import { visualizer } from 'rollup-plugin-visualizer';
|
|
6
6
|
import postcssNested from 'postcss-nested';
|
|
7
|
+
import { viteStaticCopy } from 'vite-plugin-sitemap';
|
|
7
8
|
import tailwindcss from '@tailwindcss/postcss';${
|
|
8
9
|
config.addStaticWebAppConfig
|
|
9
10
|
? `
|
|
@@ -19,6 +20,11 @@ import { AtRule } from 'postcss';`
|
|
|
19
20
|
export default defineConfig({
|
|
20
21
|
plugins: [
|
|
21
22
|
react({}),
|
|
23
|
+
Sitemap({
|
|
24
|
+
hostname: 'https://example.com',
|
|
25
|
+
// includes: [],
|
|
26
|
+
exclude: ['/admin', '/private']
|
|
27
|
+
}),
|
|
22
28
|
// visualizer({ filename: 'stats.html', open: true }),${
|
|
23
29
|
config.addStaticWebAppConfig
|
|
24
30
|
? `
|
package/bin/promptUser.js
CHANGED
|
@@ -1,134 +1,153 @@
|
|
|
1
|
-
import
|
|
2
|
-
import inquirer from
|
|
3
|
-
import
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import inquirer from "inquirer";
|
|
3
|
+
import readline from "readline";
|
|
4
4
|
|
|
5
5
|
const rl = readline.createInterface({
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
input: process.stdin,
|
|
7
|
+
output: process.stdout,
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
const question = (query) =>
|
|
11
|
-
|
|
11
|
+
new Promise((resolve) => rl.question(query, resolve));
|
|
12
12
|
|
|
13
13
|
export async function promptUser(name) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
14
|
+
console.log(
|
|
15
|
+
chalk.bold.cyan(
|
|
16
|
+
"\n🚀 UNDP Frontend Starter Kit | Let's configure your application",
|
|
17
|
+
),
|
|
18
|
+
);
|
|
19
|
+
console.log(chalk.gray("─".repeat(60)) + "\n");
|
|
20
|
+
|
|
21
|
+
const projectName = name
|
|
22
|
+
? name
|
|
23
|
+
: (await question(chalk.yellow("📝 Enter project name: "))) ||
|
|
24
|
+
"my-undp-react-app";
|
|
25
|
+
const { framework } = await inquirer.prompt([
|
|
26
|
+
{
|
|
27
|
+
type: "list",
|
|
28
|
+
name: "framework",
|
|
29
|
+
loop: false,
|
|
30
|
+
message: chalk.yellow(
|
|
31
|
+
"📦 Choose a starter (all include Tailwind CSS + Zustand):\n",
|
|
32
|
+
),
|
|
33
|
+
choices: [
|
|
34
|
+
new inquirer.Separator(chalk.dim("── Vite (recommended) ──")),
|
|
35
|
+
{
|
|
36
|
+
name: `Vite · Basic ${chalk.dim("— Minimal SPA, great for embedding")}`,
|
|
37
|
+
value: "vite-basic",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: `Vite · Router ${chalk.dim("— Multi-page SPA with TanStack Router")}`,
|
|
41
|
+
value: "vite-router",
|
|
42
|
+
},
|
|
43
|
+
new inquirer.Separator(chalk.dim("── Next.js ──")),
|
|
44
|
+
{
|
|
45
|
+
name: `Next.js · Basic ${chalk.dim("— SSR with file-based routing")}`,
|
|
46
|
+
value: "next-basic",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: `Next.js · Auth ${chalk.dim("— SSR + authentication via Better Auth")}`,
|
|
50
|
+
value: "next-auth",
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
default: "vite-basic",
|
|
54
|
+
},
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
const { addQuery } = await inquirer.prompt([
|
|
58
|
+
{
|
|
59
|
+
type: "list",
|
|
60
|
+
name: "addQuery",
|
|
61
|
+
message: chalk.yellow("⚙️ Add @tanstack/query for data fetching?"),
|
|
62
|
+
choices: ["Yes", "No"],
|
|
63
|
+
default: "No",
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const query = addQuery === "Yes";
|
|
68
|
+
|
|
69
|
+
let language = false;
|
|
70
|
+
|
|
71
|
+
if (framework === "vite-basic" || framework === "vite-router") {
|
|
72
|
+
const { addLanguage } = await inquirer.prompt([
|
|
73
|
+
{
|
|
74
|
+
type: "list",
|
|
75
|
+
name: "addLanguage",
|
|
76
|
+
message: chalk.yellow("⚙️ Add multi language support (en, es, fr)?"),
|
|
77
|
+
choices: ["Yes", "No"],
|
|
78
|
+
default: "No",
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
language = addLanguage === "Yes";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const libraryChoices = [
|
|
86
|
+
{
|
|
87
|
+
name: "@undp/data-viz — UNDP data visualization components",
|
|
88
|
+
value: "@undp/data-viz",
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "lucide-react — Beautiful open-source icon set for React",
|
|
92
|
+
value: "lucide-react",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "Peer dependencies — Install required framework dependencies",
|
|
96
|
+
value: "peer",
|
|
97
|
+
},
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const { libraries } = await inquirer.prompt([
|
|
101
|
+
{
|
|
102
|
+
type: "checkbox",
|
|
103
|
+
name: "libraries",
|
|
104
|
+
message: chalk.yellow("📦 Select the libraries you want to install:"),
|
|
105
|
+
choices: libraryChoices,
|
|
106
|
+
default: ["@undp/data-viz", "lucide-react"],
|
|
107
|
+
},
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
let addPostCSSScripts = false;
|
|
111
|
+
|
|
112
|
+
if (framework === "vite-basic") {
|
|
113
|
+
const { postCSS } = await inquirer.prompt([
|
|
114
|
+
{
|
|
115
|
+
type: "list",
|
|
116
|
+
name: "postCSS",
|
|
117
|
+
message: chalk.yellow(
|
|
118
|
+
"⚙️ Add PostCSS script to wrap all classes in `.undp-container` (recommended only if embedding in another app)?",
|
|
119
|
+
),
|
|
120
|
+
choices: ["Yes", "No"],
|
|
121
|
+
default: "Yes",
|
|
122
|
+
},
|
|
123
|
+
]);
|
|
124
|
+
addPostCSSScripts = postCSS === "Yes";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { staticWebApp } =
|
|
128
|
+
framework !== "next-basic" && framework !== "next-auth"
|
|
129
|
+
? await inquirer.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: "list",
|
|
132
|
+
name: "staticWebApp",
|
|
133
|
+
message: chalk.yellow("⚙️ Add Azure Static Web App Config file?"),
|
|
134
|
+
choices: ["Yes", "No"],
|
|
135
|
+
default: "No",
|
|
136
|
+
},
|
|
137
|
+
])
|
|
138
|
+
: { staticWebApp: "No" };
|
|
139
|
+
|
|
140
|
+
const addStaticWebAppConfig = staticWebApp === "Yes";
|
|
141
|
+
|
|
142
|
+
rl.close();
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
projectName,
|
|
146
|
+
libraries,
|
|
147
|
+
addStaticWebAppConfig,
|
|
148
|
+
addPostCSSScripts,
|
|
149
|
+
framework,
|
|
150
|
+
query,
|
|
151
|
+
language,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -102,6 +102,23 @@ import { signIn } from '@/lib/auth-client';
|
|
|
102
102
|
|
|
103
103
|
More info: [Better Auth docs](https://www.better-auth.com/docs/introduction)
|
|
104
104
|
|
|
105
|
+
## ✅ Launch Checklist
|
|
106
|
+
|
|
107
|
+
Before deploying to production, complete the launch checklist. It covers metadata and SEO, analytics, QA, accessibility, performance, security headers and caching.
|
|
108
|
+
|
|
109
|
+
The checklist is set up as a GitHub issue template, so each launch gets its own tickable copy:
|
|
110
|
+
|
|
111
|
+
1. Go to **Issues → New issue** in this repository.
|
|
112
|
+
2. Select **Launch checklist**.
|
|
113
|
+
3. Assign yourself, and click **Create**.
|
|
114
|
+
4. Tick items off as you complete them. The issue list shows progress (e.g. "7 of 32 tasks").
|
|
115
|
+
|
|
116
|
+
Or open it directly: [Create launch checklist issue](../../issues/new?template=launch-checklist.md)
|
|
117
|
+
|
|
118
|
+
Don't close the issue until every item is checked, or marked N/A with a reason.
|
|
119
|
+
|
|
120
|
+
The template lives in `.github/ISSUE_TEMPLATE/launch-checklist.md`.
|
|
121
|
+
|
|
105
122
|
## 📬 Contact us
|
|
106
123
|
|
|
107
124
|
For questions or feedback, contact us at [data@undp.org](mailto:data@undp.org).
|
|
@@ -50,6 +50,23 @@ More info: [Biome Docs](https://biomejs.dev/guides/getting-started/)
|
|
|
50
50
|
|
|
51
51
|
This project uses [Tailwind CSS](https://tailwindcss.com/) for styling and and includes pre-configured design tokens from the UNDP Design System.
|
|
52
52
|
|
|
53
|
+
## ✅ Launch Checklist
|
|
54
|
+
|
|
55
|
+
Before deploying to production, complete the launch checklist. It covers metadata and SEO, analytics, QA, accessibility, performance, security headers and caching.
|
|
56
|
+
|
|
57
|
+
The checklist is set up as a GitHub issue template, so each launch gets its own tickable copy:
|
|
58
|
+
|
|
59
|
+
1. Go to **Issues → New issue** in this repository.
|
|
60
|
+
2. Select **Launch checklist**.
|
|
61
|
+
3. Assign yourself, and click **Create**.
|
|
62
|
+
4. Tick items off as you complete them. The issue list shows progress (e.g. "7 of 32 tasks").
|
|
63
|
+
|
|
64
|
+
Or open it directly: [Create launch checklist issue](../../issues/new?template=launch-checklist.md)
|
|
65
|
+
|
|
66
|
+
Don't close the issue until every item is checked, or marked N/A with a reason.
|
|
67
|
+
|
|
68
|
+
The template lives in `.github/ISSUE_TEMPLATE/launch-checklist.md`.
|
|
69
|
+
|
|
53
70
|
## 📬 Contact us
|
|
54
71
|
|
|
55
72
|
For questions or feedback, contact us at [data@undp.org](mailto:data@undp.org).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Launch checklist
|
|
3
|
+
about: Pre-launch checks to complete before going to production
|
|
4
|
+
title: "Launch checklist"
|
|
5
|
+
labels: launch
|
|
6
|
+
assignees: ''
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
<!-- Tick items as you go. Don't close this issue until every box is checked or explicitly marked N/A with a reason. -->
|
|
10
|
+
|
|
11
|
+
## Metadata & SEO
|
|
12
|
+
|
|
13
|
+
- [ ] Favicon added (`favicon.ico` + SVG).
|
|
14
|
+
- [ ] `<meta name="theme-color">` added.
|
|
15
|
+
- [ ] `<html lang="…">` is set correctly.
|
|
16
|
+
- [ ] Every route has a unique `<title>` and `<meta name="description">`.
|
|
17
|
+
- [ ] Every route has a canonical URL: `<link rel="canonical" href="…">`.
|
|
18
|
+
- [ ] Open Graph and Twitter tags are added (globally in `index.html` or per route). Use absolute URLs for `og:url` and `og:image`:
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<meta property="og:type" content="website" />
|
|
22
|
+
<meta property="og:site_name" content="SITE_NAME" />
|
|
23
|
+
<meta property="og:title" content="TITLE" />
|
|
24
|
+
<meta property="og:description" content="DESCRIPTION" />
|
|
25
|
+
<meta property="og:url" content="https://example.com/page" />
|
|
26
|
+
<meta property="og:image" content="https://example.com/og-image.png" />
|
|
27
|
+
<meta property="og:image:width" content="1200" />
|
|
28
|
+
<meta property="og:image:height" content="630" />
|
|
29
|
+
<meta property="og:image:alt" content="IMAGE_ALT_TEXT" />
|
|
30
|
+
<meta name="twitter:card" content="summary_large_image" />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- [ ] Social previews verified with a preview tool (e.g. opengraph.xyz, LinkedIn Post Inspector).
|
|
34
|
+
- [ ] `robots.txt` and `sitemap.xml` are in place.
|
|
35
|
+
- [ ] `<meta name="robots" content="noindex">` is added to routes that should not be indexed for search.
|
|
36
|
+
- [ ] Production is indexable (no leftover `noindex`), and staging is **not** indexable.
|
|
37
|
+
- [ ] `apple-touch-icon` (180×180) added - only needed for site which the users might "install".
|
|
38
|
+
- [ ] `site.webmanifest` added - only needed for site which the users might "install".
|
|
39
|
+
|
|
40
|
+
## Functionality & QA
|
|
41
|
+
|
|
42
|
+
- [ ] All routes load, including on direct visit or refresh (host has SPA fallback routing).
|
|
43
|
+
- [ ] A custom 404 page exists, and React error boundaries catch runtime errors.
|
|
44
|
+
- [ ] No errors or warnings in the browser console.
|
|
45
|
+
- [ ] All forms, links and external integrations work in production.
|
|
46
|
+
- [ ] Design QA is complete and signed off.
|
|
47
|
+
- [ ] The layout works on all viewport widths from 360px up. At minimum, check 360, 768, 1024 and 1440px.
|
|
48
|
+
- [ ] Tested in the latest Chrome, Firefox, Edge and Safari.
|
|
49
|
+
|
|
50
|
+
## Performance
|
|
51
|
+
|
|
52
|
+
- [ ] Initial JS on first page load is ≤ 175 KB gzipped.
|
|
53
|
+
- [ ] Main CSS file is ≤ 50 KB gzipped.
|
|
54
|
+
- [ ] Images are optimised (WebP/AVIF, correctly sized, ≤ 75 KB each, lazy-loaded below the fold).
|
|
55
|
+
- [ ] Fonts are subset and preloaded, with `font-display: swap`.
|
|
56
|
+
- [ ] Lighthouse scores (run in incognito against production):
|
|
57
|
+
|
|
58
|
+
| Category | Desktop | Mobile |
|
|
59
|
+
|----------------|---------|--------|
|
|
60
|
+
| Performance | ≥ 90 | ≥ 70 |
|
|
61
|
+
| Accessibility | ≥ 90 | ≥ 90 |
|
|
62
|
+
| Best Practices | ≥ 90 | ≥ 90 |
|
|
63
|
+
| SEO | ≥ 90 | ≥ 90 |
|
|
64
|
+
|
|
65
|
+
## Security & infrastructure
|
|
66
|
+
|
|
67
|
+
- [ ] `npm audit` shows no high or critical vulnerabilities.
|
|
68
|
+
- [ ] `npm audit --omit=dev` shows no vulnerabilities.
|
|
69
|
+
- [ ] No secrets or private API keys in the client bundle.
|
|
70
|
+
|
|
71
|
+
## Analytics & privacy
|
|
72
|
+
|
|
73
|
+
- [ ] Google Analytics and/or Microsoft Clarity are installed and events are confirmed arriving from production.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
This is a React + TypeScript + Vite project powered by Tailwind CSS, and the UNDP Design System, bootstrapped with [@undp/create-app](https://www.npmjs.com/package/@undp/create-app).
|
|
2
2
|
|
|
3
3
|
It includes:
|
|
4
|
+
|
|
4
5
|
* React 19.x with React compiler
|
|
5
6
|
* UNDP Design System
|
|
6
7
|
* TailwindCSS
|
|
@@ -54,7 +55,9 @@ This project uses [Tailwind CSS](https://tailwindcss.com/) for styling and and i
|
|
|
54
55
|
|
|
55
56
|
To embed the visualization on an external page, include:
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
### Option 1: Iframe
|
|
59
|
+
|
|
60
|
+
__HTML Placeholder__
|
|
58
61
|
|
|
59
62
|
```html
|
|
60
63
|
<iframe
|
|
@@ -65,16 +68,15 @@ To embed the visualization on an external page, include:
|
|
|
65
68
|
/>
|
|
66
69
|
```
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
__Required `<script>`__
|
|
69
72
|
|
|
70
|
-
```
|
|
73
|
+
```js
|
|
71
74
|
<script>
|
|
72
75
|
var iframe = document.getElementById('my-embed');
|
|
73
76
|
window.addEventListener('message', function (e) {
|
|
74
|
-
if (e.origin !== '
|
|
77
|
+
if (e.origin !== 'APP_URL') return;
|
|
75
78
|
if (e.source !== iframe.contentWindow) return;
|
|
76
79
|
if (e.data && e.data.type === 'iframe-height') {
|
|
77
|
-
console.log('new height', e.data.height);
|
|
78
80
|
iframe.style.height = e.data.height + 'px';
|
|
79
81
|
}
|
|
80
82
|
});
|
|
@@ -83,6 +85,39 @@ To embed the visualization on an external page, include:
|
|
|
83
85
|
|
|
84
86
|
_This script listens for height messages from the embedded web app and resizes the iframe to match its content, so the app displays without an inner scrollbar._
|
|
85
87
|
|
|
88
|
+
### Option 2: Embed in the page (Worst performance better UX)
|
|
89
|
+
|
|
90
|
+
__HTML Placeholder__
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<div id="root"></div>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
__Required `<script>` and `<link>` tags__
|
|
97
|
+
|
|
98
|
+
```html
|
|
99
|
+
<link rel="dns-prefetch" href="{{Visualization URL}}">
|
|
100
|
+
<script defer="defer" type="module" src="{{Visualization URL}}/index.js"></script>
|
|
101
|
+
<link rel="stylesheet" href="{{Visualization URL}}/style.css"></link>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## ✅ Launch Checklist
|
|
105
|
+
|
|
106
|
+
Before deploying to production, complete the launch checklist. It covers metadata and SEO, analytics, QA, accessibility, performance, security headers and caching.
|
|
107
|
+
|
|
108
|
+
The checklist is set up as a GitHub issue template, so each launch gets its own tickable copy:
|
|
109
|
+
|
|
110
|
+
1. Go to **Issues → New issue** in this repository.
|
|
111
|
+
2. Select **Launch checklist**.
|
|
112
|
+
3. Assign yourself, and click **Create**.
|
|
113
|
+
4. Tick items off as you complete them. The issue list shows progress (e.g. "7 of 32 tasks").
|
|
114
|
+
|
|
115
|
+
Or open it directly: [Create launch checklist issue](../../issues/new?template=launch-checklist.md)
|
|
116
|
+
|
|
117
|
+
Don't close the issue until every item is checked, or marked N/A with a reason.
|
|
118
|
+
|
|
119
|
+
The template lives in `.github/ISSUE_TEMPLATE/launch-checklist.md`.
|
|
120
|
+
|
|
86
121
|
## 📬 Contact us
|
|
87
122
|
|
|
88
123
|
For questions or feedback, contact us at [data@undp.org](mailto:data@undp.org).
|