@zyrab/domo-ssg 0.5.4 → 0.6.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zyrab/domo-ssg",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
4
4
  "description": "A Static Site Generator (SSG) for Domo-based projects.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -17,16 +17,21 @@
17
17
  "author": "Zyrab",
18
18
  "license": "MIT",
19
19
  "peerDependencies": {
20
- "@zyrab/domo": "^1.3.0",
21
- "@zyrab/domo-og": "0.2.0",
20
+ "@zyrab/domo": "^1.4.0",
21
+ "@zyrab/domo-og": "0.2.1",
22
22
  "@zyrab/domo-router": "^0.3.1"
23
23
  },
24
+ "devDependencies": {
25
+ "esbuild": "^0.20.0",
26
+ "@zyrab/domo": "^1.4.0"
27
+ },
24
28
  "publishConfig": {
25
29
  "access": "public"
26
30
  },
27
31
  "files": [
28
32
  "src/",
29
- "README"
33
+ "templates",
34
+ "README.md"
30
35
  ],
31
36
  "scripts": {
32
37
  "build": "node src/index.js",
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @file event_extraction.js
3
+ * @description Logic for extracting events, state, and references for SSG/SPA bundling.
4
+ */
5
+
6
+ import { createHash } from "crypto";
7
+
8
+ /**
9
+ * Normalizes a function for the client.
10
+ * Handlers will use 'state' and 'target' identifiers which are
11
+ * consistently minified by esbuild alongside the listener scope.
12
+ */
13
+ function transformHandler(handlerInfo) {
14
+ const { type, selector, handler, name } = handlerInfo;
15
+ let body = "";
16
+ let fnSource = handler.toString();
17
+
18
+ const isExternal = handlerInfo.path !== null;
19
+
20
+ if (isExternal) {
21
+ // If it's a named function from an external file, we just call it.
22
+ body = `${name}(e${type !== "direct" ? ", target" : ""});`;
23
+ } else {
24
+ // Extract body from anonymous/inline function
25
+ const match =
26
+ fnSource.match(/^(?:async\s+)?(?:\([^)]*\)|[a-zA-Z0-9_$]+)\s*=>\s*\{?([\s\S]*?)\}?$/) ||
27
+ fnSource.match(/^(?:async\s+)?function\s*[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
28
+ body = match ? match[1].trim() : fnSource;
29
+ }
30
+
31
+ // Wrap in blocks to isolate 'target'.
32
+ if (type === "closest") {
33
+ return `{\n const target = e.target.closest("${selector}");\n if (target) {\n ${body}\n }\n }`;
34
+ }
35
+
36
+ if (type === "match") {
37
+ return `{\n if (e.target.matches("${selector}")) {\n const target = e.target;\n ${body}\n }\n }`;
38
+ }
39
+
40
+ return body;
41
+ }
42
+ function transformRef(refInfo) {
43
+ const { handler, name } = refInfo;
44
+ let fnSource = handler.toString();
45
+
46
+ // Extract the body of the ref callback
47
+ const match =
48
+ fnSource.match(/^(?:async\s+)?(?:\([^)]*\)|[a-zA-Z0-9_$]+)\s*=>\s*\{?([\s\S]*?)\}?$/) ||
49
+ fnSource.match(/^(?:async\s+)?function\s*[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
50
+ const body = match ? match[1].trim() : fnSource;
51
+
52
+ return `{\n const el = document.getElementById("${refInfo.id}");\n if (el) {\n const callback = (el) => { ${body} };\n callback(el);\n }\n }`;
53
+ }
54
+
55
+ /**
56
+ * Generates the ESM content for a specific element's events.
57
+ */
58
+ export function generateElementScript(id, events, states, refs) {
59
+ const imports = new Map();
60
+ const listeners = [];
61
+ const refLogics = [];
62
+
63
+ events.forEach(({ event, handlers }) => {
64
+ const logicBlocks = handlers
65
+ .map((h) => {
66
+ if (h.path) {
67
+ if (!imports.has(h.path)) imports.set(h.path, new Set());
68
+ imports.get(h.path).add(h.name);
69
+ }
70
+ return transformHandler(h);
71
+ })
72
+ .join("\n ");
73
+
74
+ // We inject the state declaration as a plain string at the top of the listener.
75
+ // Because esbuild parses this whole block, it will minify the identifier 'state'
76
+ // to the same name used in the 'logicBlocks' (e.g., const n = ...; n.toggled = ...).
77
+ const stateInclusion =
78
+ states && Object.keys(states).length
79
+ ? Object.entries(states)
80
+ .map(([key, val]) => `let ${key} = ${JSON.stringify(val)};`)
81
+ .join("\n")
82
+ : "";
83
+
84
+ listeners.push(
85
+ `${stateInclusion}\n document.getElementById("${id}").addEventListener("${event}", async (e) => {${logicBlocks}\n });`,
86
+ );
87
+ });
88
+ refs.forEach((r) => {
89
+ refLogics.push(transformRef({ ...r, id }));
90
+ });
91
+
92
+ let importStr = "";
93
+
94
+ for (const [path, names] of imports) {
95
+ importStr += `import { ${[...names].join(", ")} } from "${path}";\n`;
96
+ }
97
+ const combinedLogic = [...refLogics, ...listeners].join("\n");
98
+
99
+ return `${importStr}\n(function() {\n${combinedLogic}\n})();`;
100
+ }
101
+
102
+ /**
103
+ * Hash helper for caching
104
+ */
105
+ export function getHash(content) {
106
+ return createHash("sha1").update(content).digest("hex").slice(0, 8);
107
+ }
@@ -1,177 +1,208 @@
1
- // src/event-utils.js
2
-
3
- import { writeFileSync, mkdirSync, existsSync } from "fs";
4
- import { join } from "path";
5
- import { createHash } from "crypto";
6
-
7
- export function generateScriptContent(events) {
8
- return events
9
- .map(({ id, event, handlers }) => {
10
- const varSet = new Set();
11
- const logicLines = [];
12
- const closureFunctions = [];
13
- let matchCounter = 0;
14
-
15
- for (const { type, selector, handler } of handlers) {
16
- const fnSource = handler.toString();
17
- const { name, body } = destructureFunction(fnSource);
18
- const vars = extractExposedVariables(body);
19
- vars.forEach((v) => varSet.add(v));
20
-
21
- if (type === "closest") {
22
- const matchVar = `match${++matchCounter}`;
23
- logicLines.push(`const ${matchVar} = e.target.closest("${selector}");`);
24
-
25
- if (name) {
26
- logicLines.push(`if (${matchVar}) ${name}(e, ${matchVar});`);
27
- closureFunctions.push(fnSource);
28
- } else {
29
- const adjustedBody = body.replace(/\btarget\b/g, matchVar);
30
- logicLines.push(`if (${matchVar}) {\n${indent(adjustedBody, 2)}\n}`);
31
- }
32
- } else if (type === "match") {
33
- const matchExpr = `e.target.matches("${selector}")`;
34
-
35
- if (name) {
36
- logicLines.push(`if (${matchExpr}) ${name}(e, e.target);`);
37
- closureFunctions.push(fnSource);
38
- } else {
39
- const adjustedBody = body.replace(/\btarget\b/g, "e.target");
40
- logicLines.push(`if (${matchExpr}) {\n${indent(adjustedBody, 2)}\n}`);
41
- }
42
- } else if (type === "direct") {
43
- logicLines.push(body);
44
- }
45
- }
46
-
47
- const varsStr = [...varSet].join("\n");
48
- const handlerBody = `function(e) {\n${indent(logicLines.join("\n"), 1)}\n}`;
49
- const closures = closureFunctions.length ? `\n\n${closureFunctions.join("\n\n")}` : "";
50
-
51
- return `${
52
- varsStr ? varsStr + "\n\n" : ""
53
- }document.getElementById("${id}").addEventListener("${event}", ${handlerBody});${closures}`;
54
- })
55
- .join("\n\n\n");
1
+ import { writeFileSync, mkdirSync, existsSync, rmSync, readFileSync } from "fs";
2
+ import { join, dirname, relative } from "path";
3
+ import { fileURLToPath, pathToFileURL } from "url";
4
+ import { build } from "esbuild";
5
+ import { generateElementScript, getHash } from "./event-extraction.js";
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const require = createRequire(import.meta.url);
9
+
10
+ const DOMO_CLIENT_PACKAGE = "@zyrab/domo/client";
11
+ let DOMO_CLIENT_SOURCE;
12
+
13
+ try {
14
+ // This works across different package managers
15
+ DOMO_CLIENT_SOURCE = require.resolve(DOMO_CLIENT_PACKAGE);
16
+ } catch (e) {
17
+ // Fallback for local dev if not linked
18
+ DOMO_CLIENT_SOURCE = join(__dirname, "../../domo/src/client/domo.client.js");
56
19
  }
57
-
58
- export function collectEvents(node, out = []) {
20
+ const cache = {
21
+ runtime: null,
22
+ events: new Map(), // hash -> file
23
+ islands: new Map(), // hash -> file
24
+ };
25
+
26
+ /**
27
+ * Plugin to convert imports of 'domo' into the global 'Domo' variable
28
+ */
29
+ const makeDomoExternalPlugin = {
30
+ name: "domo-external",
31
+ setup(build) {
32
+ // Intercept any import relating to domo
33
+ build.onResolve({ filter: /^domo$|^@zyrab\/domo/ }, (args) => {
34
+ return { path: args.path, external: true };
35
+ });
36
+ },
37
+ };
38
+
39
+ /**
40
+ * Traverses the Domo tree to find all elements with events or islands.
41
+ */
42
+ export function collectMetadata(node, out = { events: [], islands: [] }) {
59
43
  if (!node || typeof node !== "object") return out;
44
+
60
45
  const el = node.element;
61
- if (Array.isArray(el._events) && el._events.length > 0) {
62
- out.push(...el._events);
46
+
47
+ if ((el?._events?.length > 0 || el?._refs?.length > 0) && !el?._island) {
48
+ out.events.push({
49
+ id: el._attr["data-domo-id"] || el._attr["id"],
50
+ events: el._events || [],
51
+ states: el._state || {},
52
+ refs: el._refs || [],
53
+ });
63
54
  }
64
55
 
65
- if (Array.isArray(el._child)) {
56
+ if (el?._island) {
57
+ out.islands.push({
58
+ id: el._attr["data-domo-id"] || el._attr["id"],
59
+ path: el.__file,
60
+ });
61
+ }
62
+
63
+ if (Array.isArray(el?._child)) {
66
64
  for (const child of el._child) {
67
- collectEvents(child, out);
65
+ collectMetadata(child, out);
68
66
  }
69
67
  }
70
68
 
71
69
  return out;
72
70
  }
73
71
 
74
- function extractExposedVariables(source) {
75
- const lines = source.split("\n");
76
- const injected = [];
72
+ /**
73
+ * Bundle runtime (once)
74
+ */
75
+ async function bundleRuntime(outputDir) {
76
+ if (cache.runtime) return cache.runtime;
77
+
78
+ const file = "domo.runtime.js";
79
+ const out = join(outputDir, "js", file);
80
+
81
+ await build({
82
+ entryPoints: [DOMO_CLIENT_SOURCE],
83
+ bundle: true,
84
+ minify: false,
85
+ format: "iife",
86
+ globalName: "Domo",
87
+ outfile: out,
88
+ platform: "browser",
89
+ });
90
+
91
+ cache.runtime = file;
92
+ return file;
93
+ }
77
94
 
78
- for (const line of lines) {
79
- const trimmed = line.trim();
95
+ /**
96
+ * Bundle event logic
97
+ */
98
+ async function bundleEvents(metadata, jsDir, tempDir) {
99
+ if (metadata.events.length === 0) return null;
80
100
 
81
- if (trimmed === "") continue;
101
+ const raw = metadata.events
102
+ .map(({ id, events, states, refs }) => generateElementScript(id, events, states, refs))
103
+ .join("\n\n");
82
104
 
83
- if (trimmed.startsWith("// @ssg-let")) {
84
- const decl = trimmed.replace("// @ssg-let", "").replace(/;$/, "").trim();
85
- injected.push(`let ${decl};`);
86
- } else if (trimmed.startsWith("// @ssg-const")) {
87
- const decl = trimmed.replace("// @ssg-const", "").replace(/;$/, "").trim();
88
- injected.push(`const ${decl};`);
89
- } else if (!trimmed.startsWith("//")) {
90
- break; // stop at first non-comment code line
91
- }
92
- }
105
+ const hash = getHash(raw);
106
+ if (cache.events.has(hash)) return cache.events.get(hash);
93
107
 
94
- return injected;
95
- }
108
+ const file = `${hash}.events.js`;
109
+ const entry = join(tempDir, `${hash}.entry.js`);
96
110
 
97
- function destructureFunction(fnSource) {
98
- const funcMatch = fnSource.match(/^function\s*([a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{([\s\S]*)\}$/);
99
- if (funcMatch) {
100
- const [, name, body] = funcMatch;
101
- return { name: name.trim(), body: body.trim() };
102
- }
111
+ writeFileSync(entry, raw, "utf8");
103
112
 
104
- const arrowMatch = fnSource.match(/^\(?[a-zA-Z0-9_,\s]*\)?\s*=>\s*\{([\s\S]*)\}$/);
105
- if (arrowMatch) {
106
- return { name: "", body: arrowMatch[1].trim() };
107
- }
113
+ await build({
114
+ entryPoints: [entry],
115
+ bundle: true,
116
+ minify: false,
117
+ format: "iife",
118
+ outfile: join(jsDir, file),
119
+ platform: "browser",
120
+ });
108
121
 
109
- return { name: "", body: "" };
110
- }
122
+ rmSync(entry);
111
123
 
112
- function indent(str, level = 1) {
113
- const pad = " ".repeat(level);
114
- return str
115
- .split("\n")
116
- .map((line) => pad + line)
117
- .join("\n");
124
+ cache.events.set(hash, file);
125
+ return file;
118
126
  }
119
127
 
120
- function normalizeEventLogic(events) {
121
- const blocks = [];
122
-
123
- for (const { handlers } of events) {
124
- for (const { handler } of handlers) {
125
- const fnSource = handler.toString();
126
- const { body } = destructureFunction(fnSource);
127
- const injected = extractExposedVariables(body);
128
+ /**
129
+ * Bundle islands (deduped by CONTENT, not path)
130
+ */
131
+ async function bundleIslands(metadata, jsDir, tempDir) {
132
+ const results = [];
133
+ const islandsToBundle = metadata.islands.filter((i) => i.path);
134
+
135
+ await Promise.all(
136
+ islandsToBundle.map(async (island) => {
137
+ const { path: filePath, id } = island;
138
+ const content = readFileSync(filePath, "utf8");
139
+ const hash = getHash(content);
140
+
141
+ if (cache.islands.has(hash)) {
142
+ results.push({ path: filePath, file: cache.islands.get(hash) });
143
+ return;
144
+ }
128
145
 
129
- blocks.push({
130
- injected: injected.join("\n"),
131
- body: normalizeCode(body),
146
+ const file = `${hash}.island.js`;
147
+ const entryPath = join(tempDir, `${hash}.island.entry.js`);
148
+
149
+ // wrapper: hydrate island at the correct element
150
+ const wrapper = `
151
+ import Island from "${filePath.replace(/\\/g, "/")}";
152
+
153
+ (function() {
154
+ const el = document.querySelector('[data-domo-id="${id}"]');
155
+ if (el) {
156
+ const instance = Island();
157
+ if (instance && instance._isDomo) {
158
+ const built = instance.build();
159
+ el.replaceWith(built);
160
+ }
161
+ }
162
+ })();
163
+ `;
164
+
165
+ writeFileSync(entryPath, wrapper, "utf8");
166
+
167
+ await build({
168
+ entryPoints: [entryPath],
169
+ bundle: true,
170
+ minify: false,
171
+ format: "iife",
172
+ outfile: join(jsDir, file),
173
+ platform: "browser",
174
+ plugins: [makeDomoExternalPlugin],
132
175
  });
133
- }
134
- }
135
176
 
136
- // Sort to make sure logic is order-independent
137
- blocks.sort((a, b) => (a.body + a.injected).localeCompare(b.body + b.injected));
177
+ rmSync(entryPath);
178
+ cache.islands.set(hash, file);
179
+ results.push({ path: filePath, file });
180
+ }),
181
+ );
138
182
 
139
- return blocks.map(({ injected, body }) => `${injected}\n${body}`).join("\n");
183
+ return results;
140
184
  }
185
+ /**
186
+ * Main orchestrator
187
+ */
188
+ export async function writeJs(content, outputDir) {
189
+ const metadata = collectMetadata(content);
141
190
 
142
- function normalizeCode(code) {
143
- return code
144
- .split("\n")
145
- .map((line) => line.trim())
146
- .filter((line) => line && !line.startsWith("//"))
147
- .join("\n");
148
- }
191
+ const hasInteractivity = metadata.events.length > 0 || metadata.islands.length > 0;
149
192
 
150
- function hashContent(content) {
151
- return createHash("sha1").update(content).digest("hex").slice(0, 8); // short hash like '2fa4b1ab'
152
- }
193
+ if (!hasInteractivity) return null;
153
194
 
154
- const generatedCache = new Map(); // hash → filename
195
+ const jsDir = join(outputDir, "js");
196
+ const tempDir = join(outputDir, ".domo_temp");
155
197
 
156
- export function writeJs(constent, outputDir) {
157
- const events = collectEvents(constent);
158
- if (events.length <= 0) return;
198
+ if (!existsSync(jsDir)) mkdirSync(jsDir, { recursive: true });
199
+ if (!existsSync(tempDir)) mkdirSync(tempDir, { recursive: true });
159
200
 
160
- const jsDir = join(outputDir, "js");
161
- if (!existsSync(jsDir)) mkdirSync(jsDir);
162
-
163
- const normalized = normalizeEventLogic(events);
164
- const hash = hashContent(normalized);
165
-
166
- let fileName;
167
- if (generatedCache.has(hash)) {
168
- fileName = generatedCache.get(hash);
169
- } else {
170
- fileName = `${hash}.js`;
171
- const jsContent = generateScriptContent(events);
172
- writeFileSync(join(jsDir, fileName), jsContent, "utf8");
173
- generatedCache.set(hash, fileName);
174
- }
201
+ const [runtime, events, islands] = await Promise.all([
202
+ bundleRuntime(outputDir),
203
+ bundleEvents(metadata, jsDir, tempDir),
204
+ bundleIslands(metadata, jsDir, tempDir),
205
+ ]);
175
206
 
176
- return join(fileName);
207
+ return [runtime, events, ...islands.map((i) => i.file)].filter(Boolean);
177
208
  }
@@ -39,13 +39,12 @@ export async function handleRoute(params, renderLayout) {
39
39
  // Render the component content
40
40
  const content = await component(props);
41
41
 
42
- const embededScript = writeJs(content, outDir, path);
43
-
42
+ const bundlePath = await writeJs(content, outDir);
44
43
  const ogImage = await tryGenerateOgImage(meta, outDir, path);
45
44
 
46
45
  const fontPaths = normalizeAssets([fonts, assets.fonts]);
47
46
  const stylePaths = normalizeAssets([styles, assets.styles]);
48
- const scriptPaths = normalizeAssets([embededScript, scripts, assets.scripts]);
47
+ const scriptPaths = normalizeAssets([bundlePath, scripts, assets.scripts]);
49
48
 
50
49
  const html = await renderLayout(content, {
51
50
  scripts: scriptPaths,
@@ -0,0 +1,17 @@
1
+ // domo.config.js
2
+ export default {
3
+ outDir: "./dist",
4
+ routesFile: "./routes.js",
5
+ layout: "./layout.js",
6
+ author: "Domo SSG",
7
+ baseUrl: "https://www.domo.zyrab.dev",
8
+ lang: "en",
9
+ theme: "auto",
10
+ exclude: ["js", "css", "app-ads.txt", "assets", "data"],
11
+ assets: {
12
+ scripts: ["test.js", "global.js", { href: "theme-toggle.js", preload: true }],
13
+ styles: [{ href: "main.css", prefetch: false }, "hoora.css"],
14
+ fonts: [{ href: "wohaha.woff2", preload: true }, "yasWemadeIt.woff2"],
15
+ favicon: "path/to/fivicon.ico",
16
+ },
17
+ };
@@ -0,0 +1,110 @@
1
+ import Router from "../../../packages/domo-router/src/core.js";
2
+ import createHeader from "./header.js";
3
+ export async function renderLayout(content, data) {
4
+ const {
5
+ title,
6
+ description,
7
+ descriptionOG,
8
+ scripts,
9
+ styles,
10
+ fonts,
11
+ favicon,
12
+ baseUrl,
13
+ canonical,
14
+ lang,
15
+ author,
16
+ type,
17
+ ogImage,
18
+ theme,
19
+ } = data;
20
+
21
+ const canonicalUrl = baseUrl + (canonical || Router.path());
22
+
23
+ const scriptTags = scripts
24
+ .map((file) =>
25
+ file.preload
26
+ ? `<link rel="preload" as="script" href="/js/${file.href}">
27
+ <script defer src="/js/${file.href}"></script>`
28
+ : `<script defer src="/js/${file.href || file}"></script>`
29
+ )
30
+ .join("\n");
31
+
32
+ const styleTags = styles
33
+ .map((style) =>
34
+ style.preload
35
+ ? `<link rel="preload" href="/css/${style.href}" as="style" onload="this.rel='stylesheet'">`
36
+ : `<link rel="stylesheet" href="/css/${style.href || style}">`
37
+ )
38
+ .join("\n");
39
+
40
+ const fontTags = fonts
41
+ .map((font) =>
42
+ font.preload
43
+ ? `<link rel="preload" href="/assets/fonts/${font.href}" as="font" type="font/woff2" crossorigin="anonymous">`
44
+ : `<link rel="stylesheet" href="assets/fonts/${font.href || font}">`
45
+ )
46
+ .join("\n");
47
+
48
+ const themeColor =
49
+ theme === "auto" || theme === "toggle"
50
+ ? ` <meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
51
+ <meta name="theme-color" content="#000000" media="(prefers-color-scheme: dark)">
52
+ <meta name="color-scheme" content="light dark">`
53
+ : ` <meta name="theme-color" content="${theme === "dark" ? "#000000" : "#ffffff"}">
54
+ <meta name="color-scheme" content="${theme}">`;
55
+
56
+ return `<!DOCTYPE html>
57
+ <html lang="${lang}">
58
+ <head>
59
+ <meta charset="UTF-8">
60
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
61
+ <title>${title}</title>
62
+ <meta name="description" content="${description}">
63
+ <meta name="author" content="${author}">
64
+ <meta name="robots" content="index, follow">
65
+
66
+ <!-- Canonical -->
67
+ <link rel="canonical" href="${canonicalUrl}">
68
+
69
+ <!-- Social: OpenGraph -->
70
+ <meta property="og:title" content="${title}">
71
+ <meta property="og:description" content="${descriptionOG || description}">
72
+ <meta property="og:image" content="${ogImage}">
73
+ <meta property="og:url" content="${baseUrl}${Router.path()}">
74
+ <meta property="og:type" content="${type || "website"}">
75
+
76
+ <!-- Social: Twitter -->
77
+ <meta name="twitter:card" content="summary_large_image">
78
+ <meta name="twitter:title" content="${title}">
79
+ <meta name="twitter:description" content="${descriptionOG || description}">
80
+ <meta name="twitter:image" content="${ogImage}">
81
+
82
+ <!-- Favicon and Touch Icon -->
83
+ <link rel="icon" href="/assets/${favicon}" type="image/x-icon">
84
+ <link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">
85
+
86
+ <!-- Theme Colors -->
87
+ ${themeColor}
88
+
89
+ <!-- Performance -->
90
+ ${fontTags}
91
+ ${styleTags}
92
+
93
+ <!-- Privacy & Security -->
94
+ <meta name="referrer" content="strict-origin-when-cross-origin">
95
+ <meta http-equiv="Permissions-Policy" content="interest-cohort=()">
96
+ <meta http-equiv="Content-Security-Policy" content="script-src 'self'">
97
+
98
+
99
+ <!-- Scripts: preload or noraml injection -->
100
+ ${scriptTags}
101
+
102
+ </head>
103
+ <body>
104
+ ${createHeader()}
105
+ <main>
106
+ ${content.build()}
107
+ </main>
108
+ </body>
109
+ </html>`;
110
+ }
@@ -0,0 +1,45 @@
1
+ import Home from "./pages/home.js";
2
+ import About from "./pages/about.js";
3
+ import Contacts from "./pages/contacts.js";
4
+ import createProjects from "./pages/projects.js";
5
+ import createProjectPage from "./components/project-page.js";
6
+ import Error from "./pages/errot.js";
7
+ import { loadJson } from "./load-json.js";
8
+
9
+ export const routes = {
10
+ "/": {
11
+ component: Home,
12
+ meta: { title: "Home" },
13
+ },
14
+ "/about": {
15
+ component: About,
16
+ script: ["test.js", { href: "js-with-props.js", preload: true }],
17
+ style: ["test.css", { href: "css-with-props.css", preload: true }],
18
+ font: ["test.woff2", { href: "font-with-props.woff2", preload: true }],
19
+ meta: { title: "About" },
20
+ },
21
+ "/contacts": {
22
+ component: Contacts,
23
+ meta: {
24
+ title: "contacts",
25
+ description: "page description",
26
+ descriptionOG: "Learn more about our mission and values.",
27
+ canonical: "/test/canonical", //u will need this if u have highly duplicated pages. good for SEO,
28
+ ogImage: "/test/image.png",
29
+ type: "product", // this is for twitter cards to define what type of content is on page, default is webste
30
+ },
31
+ },
32
+ "/projects": {
33
+ component: createProjects,
34
+ meta: { title: "contacts" },
35
+ "/:id": {
36
+ routeParams: async (parentRouteName) => await loadJson(parentRouteName), // parentRouteName = projects, this is usfull in deeply nested dinmic routs
37
+ component: createProjectPage,
38
+ meta: { title: "test page" },
39
+ },
40
+ },
41
+ "*": {
42
+ component: Error,
43
+ meta: { title: "error" },
44
+ },
45
+ };