@stacklist-app/brandkit 1.2.3 → 1.2.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/README.md CHANGED
@@ -73,10 +73,26 @@ export default {
73
73
 
74
74
  **Next.js / plain HTML**: run `brandkit build brand` and serve the resulting directory as a static route.
75
75
 
76
+ ### Serving from a base path
77
+
78
+ brandkit emits page-relative paths, which resolve only when the guide is served from the root or a **trailing-slash** directory URL (`/brand/`). If you serve it at `/brand` with **no** trailing slash — e.g. a Next.js rewrite that maps `/brand` to the built `/brand/index.html` — every relative path resolves against `/` instead, so `styles.css`, `engine.js`, and `config.json` all 404 and the guide renders an empty shell.
79
+
80
+ Set `basePath` in `config.json` to fix this:
81
+
82
+ ```json
83
+ {
84
+ "basePath": "/brand",
85
+ "brand": { "name": "Acme" }
86
+ }
87
+ ```
88
+
89
+ On `brandkit build`, the generated `index.html` then points its stylesheet, engine, and `brand.json` link at `${basePath}/…`, and the engine fetches `${basePath}/config.json` — so the guide loads fully whether or not the serving URL has a trailing slash. The value is normalized (leading slash added, trailing slash stripped). Omitting `basePath` keeps the original page-relative behavior, so existing setups are unaffected.
90
+
76
91
  ## Config
77
92
 
78
93
  `config.json` is the single source of truth. Top-level keys:
79
94
 
95
+ - `basePath` — absolute path the guide is served from (e.g. `/brand`). Omit (or leave empty) when serving at the root or from a trailing-slash directory URL — output stays page-relative and unchanged. See [Serving from a base path](#serving-from-a-base-path).
80
96
  - `brand` — name, tagline, description, version, date; optional `guideLabel` (renames the "Web Style Guide" header/footer label), `headerLogo` and `sidebarLogo` (logo image paths that replace the text wordmark in the header and left menu)
81
97
  - `fonts` — display + body with Google Fonts import; each font takes an optional `fallback` web stand-in for brands whose official typeface isn't web-available (the rendered font stack becomes `'family', 'fallback', sans-serif`, while labels keep the clean family name)
82
98
  - `theme` — CSS variable map (colors, gradients, font vars)
@@ -37,6 +37,7 @@
37
37
  }
38
38
  },
39
39
  "properties": {
40
+ "basePath": { "type": "string", "description": "Absolute base path the guide is served from (e.g. \"/brand\"). Empty = served at root / relative. Set this when serving from a non-trailing-slash URL (e.g. a framework rewrite)." },
40
41
  "brand": {
41
42
  "type": "object",
42
43
  "required": ["name"],
package/dist/engine.js CHANGED
@@ -6,7 +6,13 @@
6
6
  /* ================================================================
7
7
  0. Load config
8
8
  ================================================================ */
9
- var res = await fetch('./config.json');
9
+ // Base path the guide is served from. The build injects
10
+ // window.__BRANDKIT_BASE__ when config.basePath is set (e.g. "/brand"), so a
11
+ // guide served from a non-trailing-slash URL still resolves its assets.
12
+ // Default '.' keeps the original page-relative behavior (dev + root serving).
13
+ var BASE = (typeof window !== 'undefined' && window.__BRANDKIT_BASE__)
14
+ ? (String(window.__BRANDKIT_BASE__).replace(/\/+$/, '') || '.') : '.';
15
+ var res = await fetch(BASE + '/config.json');
10
16
  var config = await res.json();
11
17
  init(config);
12
18
 
@@ -139,7 +145,10 @@
139
145
  // Clear any userinfo so basic-auth creds in the page URL don't leak into
140
146
  // the copied prompt (a relative path already drops the page query/hash).
141
147
  function exportUrl(file) {
142
- var u = new URL(file, location.href);
148
+ // When served from a base path, resolve against it so the prompt
149
+ // carries the real export URLs (page URL may have no trailing slash).
150
+ var ref = (BASE && BASE !== '.') ? BASE + '/' + file : file;
151
+ var u = new URL(ref, location.href);
143
152
  u.username = ''; u.password = '';
144
153
  return u.href;
145
154
  }
package/lib/template.js CHANGED
@@ -42,6 +42,32 @@ function generateRootCSS(config) {
42
42
  return lines.join('\n');
43
43
  }
44
44
 
45
+ /**
46
+ * Escape a string for safe interpolation inside a double-quoted HTML attribute.
47
+ * A legitimate URL path never contains these chars, so real basePaths pass
48
+ * through unchanged; a hostile value can't break out of the attribute.
49
+ */
50
+ function escapeAttr(s) {
51
+ return String(s).replace(/&/g, '&').replace(/"/g, '"')
52
+ .replace(/</g, '&lt;').replace(/>/g, '&gt;');
53
+ }
54
+
55
+ /**
56
+ * Normalize config.basePath into an absolute, no-trailing-slash prefix.
57
+ * "" / undefined / "/" → "" (served at root, page-relative — unchanged).
58
+ * "brand" or "/brand/" → "/brand".
59
+ */
60
+ function normalizeBasePath(bp) {
61
+ if (!bp || typeof bp !== 'string') return '';
62
+ var s = bp.trim();
63
+ if (!s) return '';
64
+ // Exactly one leading slash: collapse "//brand" (a protocol-relative URL —
65
+ // browsers read it as a cross-origin host) and "brand" alike to "/brand".
66
+ s = '/' + s.replace(/^\/+/, '');
67
+ s = s.replace(/\/+$/, '');
68
+ return s;
69
+ }
70
+
45
71
  /**
46
72
  * Generate a Google Fonts <link> tag from config.fonts.
47
73
  */
@@ -64,6 +90,11 @@ function generateFontsLink(config) {
64
90
  * Writes index.html and styles.css into the target directory.
65
91
  */
66
92
  function build(distDir, targetDir, config) {
93
+ // Absolute base path the guide is served from (e.g. "/brand"). Empty = root /
94
+ // page-relative, in which case nothing below is rewritten (byte-identical to
95
+ // the pre-basePath output). The "/" + file form (no trailing slash on basePath)
96
+ // is what lets a framework rewrite like /brand → /brand/index.html resolve.
97
+ var basePath = normalizeBasePath(config.basePath);
67
98
  // Build styles.css: append the config :root AFTER the stylesheet's default
68
99
  // tokens so brand overrides win the cascade (styles.css ships a baseline
69
100
  // :root; an earlier block would be overridden by it).
@@ -81,10 +112,34 @@ function build(distDir, targetDir, config) {
81
112
  var fontsLink = generateFontsLink(config);
82
113
  var brandName = (config.brand && config.brand.displayName) || 'Brand';
83
114
 
84
- // Inject fonts link + a discovery <link> for the structured brand data
85
- var headInject = (fontsLink ? fontsLink + '\n' : '') +
86
- '<link rel="alternate" type="application/json" href="brand.json" title="Brand data">\n';
87
- htmlTemplate = htmlTemplate.replace('</head>', headInject + '</head>');
115
+ // Inject fonts link + a discovery <link> for the structured brand data.
116
+ // When a basePath is set, prefix the brandkit-generated refs (stylesheet,
117
+ // engine, brand.json) so they resolve from a non-trailing-slash URL; also
118
+ // expose the base to the runtime engine via window.__BRANDKIT_BASE__.
119
+ // The Google Fonts link and author-supplied logo paths are left untouched.
120
+ // basePath is author-controlled config, so it's hardened before it lands in
121
+ // markup, each context with its own escaping:
122
+ // - inline <script>: JSON.stringify + "<" → <, so a "</script>" in the
123
+ // value can't break out of the script element (JS-string context).
124
+ // - href/src attributes: HTML-attribute-escaped (basePathAttr), so a stray
125
+ // quote can't break out of the attribute and inject a handler.
126
+ // - all .replace() calls below use function replacers, so a "$" in the path
127
+ // isn't interpreted as a $-replacement pattern.
128
+ var basePathLiteral = JSON.stringify(basePath).replace(/</g, '\\u003C');
129
+ var basePathAttr = escapeAttr(basePath);
130
+ var brandJsonHref = basePath ? basePathAttr + '/brand.json' : 'brand.json';
131
+ var headInject = (basePath ? '<script>window.__BRANDKIT_BASE__ = ' + basePathLiteral + ';</script>\n' : '') +
132
+ (fontsLink ? fontsLink + '\n' : '') +
133
+ '<link rel="alternate" type="application/json" href="' + brandJsonHref + '" title="Brand data">\n';
134
+ htmlTemplate = htmlTemplate.replace('</head>', function () { return headInject + '</head>'; });
135
+
136
+ if (basePath) {
137
+ // Tolerant of quote style / whitespace around "=" so a future reformat of
138
+ // the template doesn't silently no-op and ship broken (root-relative) refs.
139
+ htmlTemplate = htmlTemplate
140
+ .replace(/href\s*=\s*["']styles\.css["']/, function () { return 'href="' + basePathAttr + '/styles.css"'; })
141
+ .replace(/src\s*=\s*["']engine\.js["']/, function () { return 'src="' + basePathAttr + '/engine.js"'; });
142
+ }
88
143
 
89
144
  // Set title
90
145
  htmlTemplate = htmlTemplate.replace(
@@ -116,5 +171,6 @@ function build(distDir, targetDir, config) {
116
171
  module.exports = {
117
172
  generateRootCSS: generateRootCSS,
118
173
  generateFontsLink: generateFontsLink,
174
+ normalizeBasePath: normalizeBasePath,
119
175
  build: build
120
176
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stacklist-app/brandkit",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Config-driven brand guide that bolts onto any website. Zero dependencies.",
5
5
  "main": "lib/resolve.js",
6
6
  "bin": {