desy-html 16.0.3 → 16.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/AGENTS.md CHANGED
@@ -155,7 +155,7 @@ desy-html/
155
155
 
156
156
  To customize for different organizations:
157
157
  1. Edit `branding/branding.config.js` or create `branding/branding.config.{name}.js`
158
- 2. Add favicon files to `branding/logos/` (favicon.svg, favicon.ico, apple-touch-icon.png)
158
+ 2. Add favicon files to `branding/logos/` (favicon.svg, favicon.ico, apple-touch-icon.png). The plugin automatically copies them to `/dist/images/` during build.
159
159
  3. Replace logos in `branding/logos/`
160
160
  4. Run `BRANDING_CONFIG={name} npm run build`
161
161
 
@@ -176,6 +176,32 @@ To customize for different organizations:
176
176
  2. Export function that accepts `aria` namespace
177
177
  3. Register in `src/js/desy-html.js`
178
178
 
179
+ ## Post-Implementation Documentation Review
180
+
181
+ After making code changes, review if documentation needs updates:
182
+
183
+ ### Checklist
184
+
185
+ - [ ] **AGENTS.md** - If adding new commands, patterns, or workflows
186
+ - [ ] **README.md** - If changing project setup or usage
187
+ - [ ] **TESTING_PLAN.md** - If adding new test scenarios
188
+ - [ ] **branding/BRANDING.md** - If changing branding configuration
189
+ - [ ] **Other .md files** - Check for relevant changes
190
+
191
+ ### When to Update
192
+
193
+ | Change Type | Files to Review |
194
+ | ----------- | --------------- |
195
+ | New feature | AGENTS.md, README.md, possibly TESTING_PLAN.md |
196
+ | Config change | BRANDING.md, AGENTS.md |
197
+ | Bug fix | TESTING_PLAN.md if adding test scenarios |
198
+ | Documentation only | Only the .md files being changed |
199
+
200
+ **Tip**: After any technical change, run a quick grep to find relevant .md files:
201
+ ```bash
202
+ grep -r "keyword" --include="*.md"
203
+ ```
204
+
179
205
  ## License
180
206
 
181
207
  This project uses EUPL-1.2 license. See EUPL-1.2.txt for details.
package/TESTING_PLAN.md CHANGED
@@ -57,7 +57,10 @@ This document defines the complete testing plan to execute before publishing the
57
57
  - Organization logos (aragon-*.svg)
58
58
  - EU logos (*.svg in subfolder or root)
59
59
  - Background images (header-background*.svg)
60
- - [ ] Verify favicon files are copied to dist (favicon.ico, apple-touch-icon.png)
60
+ - [ ] Verify favicon files are copied to dist/images/:
61
+ - `dist/images/favicon.svg`
62
+ - `dist/images/favicon.ico`
63
+ - `dist/images/apple-touch-icon.png`
61
64
 
62
65
  ### 2.3. Production Paths Verification
63
66
 
@@ -80,7 +83,7 @@ Using the browser agent, test the following pages:
80
83
  - [ ] Verify organization logo displays (not broken, no 404)
81
84
  - [ ] Verify primary colors are Aragón's (#00607a)
82
85
  - [ ] Verify typography is correct (Open Sans)
83
- - [ ] Verify favicon displays in browser tab (not broken, no 404)
86
+ - [ ] Verify favicon displays in browser tab (all 3: svg, ico, apple-touch-icon in /images/)
84
87
 
85
88
  #### Header
86
89
 
@@ -148,7 +148,10 @@ Favicon configuration for browser tabs and mobile devices.
148
148
  | `favicon.ico` | string | Yes | 32×32 | Legacy favicon in ICO format (fallback for older browsers) |
149
149
  | `favicon.appleTouch` | string | Yes | 180×180 | Apple Touch Icon for iOS home screen |
150
150
 
151
- > **Note:** For optimal compatibility, use the following sizes: 32×32 for browser tabs (both SVG and ICO), and 180×180 for Apple Touch Icon. The plugin automatically copies these files to `/dist` during build.
151
+ > **Note:** For optimal compatibility, use the following sizes: 32×32 for browser tabs (both SVG and ICO), and 180×180 for Apple Touch Icon. The vite-branding-plugin automatically copies favicon files to `/dist/images/` during build and rewrites the HTML paths:
152
+ > - `favicon.svg` → `/dist/images/favicon.svg`
153
+ > - `favicon.ico` → `/dist/images/favicon.ico`
154
+ > - `apple-touch-icon.png` → `/dist/images/apple-touch-icon.png`
152
155
 
153
156
  ## CSS Variables
154
157
 
@@ -62,4 +62,11 @@ export default {
62
62
  headerBackground: '/branding/images/header-background.svg',
63
63
  headerBackgroundLg: '/branding/images/header-background-lg.svg',
64
64
  },
65
+
66
+ // Favicon Configuration
67
+ favicon: {
68
+ svg: '/branding/logos/yourorganization-favicon.svg',
69
+ ico: '/branding/logos/yourorganization-favicon.ico',
70
+ appleTouch: '/branding/logos/yourorganization-apple-touch-icon.png'
71
+ },
65
72
  };
@@ -0,0 +1,13 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" aria-label="YourLogo" role="img">
2
+ <defs>
3
+ <clipPath id="clip-hex">
4
+ <path d="M16 1.5L29 9V23L16 30.5L3 23V9L16 1.5Z"/>
5
+ </clipPath>
6
+ </defs>
7
+ <!-- Hexagono exterior -->
8
+ <path d="M16 1.5L29 9V23L16 30.5L3 23V9L16 1.5Z" fill="#00607a" stroke="#00475c" stroke-width="1"/>
9
+ <!-- Cuadrado interior -->
10
+ <rect x="10" y="10" width="12" height="12" rx="1" fill="#ffffff"/>
11
+ <!-- Círculo centro -->
12
+ <circle cx="16" cy="16" r="3" fill="#00607a"/>
13
+ </svg>
@@ -125,28 +125,94 @@ export function brandingPlugin(config) {
125
125
 
126
126
  closeBundle() {
127
127
  const distDir = path.resolve(process.cwd(), 'dist');
128
+ const imagesDir = path.resolve(process.cwd(), 'dist/images');
128
129
  const favicon = config?.favicon;
129
130
 
131
+ // Early exit if no favicon configuration
130
132
  if (!favicon) return;
131
133
 
134
+ // Helper function to recursively find HTML files
135
+ const findHtmlFiles = (dir, files = []) => {
136
+ if (!fs.existsSync(dir)) return files;
137
+ const items = fs.readdirSync(dir, { withFileTypes: true });
138
+ for (const item of items) {
139
+ const fullPath = path.join(dir, item.name);
140
+ if (item.isDirectory()) {
141
+ findHtmlFiles(fullPath, files);
142
+ } else if (item.name.endsWith('.html')) {
143
+ files.push(fullPath);
144
+ }
145
+ }
146
+ return files;
147
+ };
148
+
132
149
  const faviconFiles = [
133
150
  { key: 'svg', fileName: 'favicon.svg' },
134
151
  { key: 'ico', fileName: 'favicon.ico' },
135
152
  { key: 'appleTouch', fileName: 'apple-touch-icon.png' }
136
153
  ];
137
154
 
155
+ // All go to imagesDir
156
+ const destDir = imagesDir;
157
+
138
158
  for (const { key, fileName } of faviconFiles) {
139
- if (favicon[key]) {
140
- const src = path.resolve(process.cwd(), 'branding/logos', fileName);
141
- if (fs.existsSync(src)) {
142
- try {
143
- fs.copyFileSync(src, path.join(distDir, fileName));
144
- console.log(`✓ Favicon ${fileName} copied to dist`);
145
- } catch (err) {
146
- console.warn(`Warning: Could not copy ${fileName}:`, err.message);
159
+ const faviconPath = favicon[key];
160
+ if (!faviconPath) continue;
161
+
162
+ // Extraer el nombre del archivo de la ruta configurada
163
+ const fileNameFromPath = path.basename(faviconPath);
164
+ const src = path.resolve(process.cwd(), faviconPath.slice(1)); // Quitar el / inicial
165
+
166
+ if (fs.existsSync(src)) {
167
+ try {
168
+ // Ensure destination directory exists
169
+ if (!fs.existsSync(destDir)) {
170
+ fs.mkdirSync(destDir, { recursive: true });
171
+ }
172
+ fs.copyFileSync(src, path.join(destDir, fileNameFromPath));
173
+ console.log(`✓ Favicon ${fileNameFromPath}: branding/logos/${fileNameFromPath} → dist/images/`);
174
+ } catch (err) {
175
+ console.warn(`Warning: Could not copy ${fileNameFromPath}:`, err.message);
176
+ }
177
+ }
178
+ }
179
+
180
+ // Rewrite HTML files to update favicon href paths
181
+ // Note: This is now handled in vite.config.js after HTML generation (rewriteFaviconPaths)
182
+ // Kept here for reference but not executed (HTML files don't exist yet at this point)
183
+ try {
184
+ const htmlFiles = findHtmlFiles(distDir);
185
+
186
+ for (const htmlFile of htmlFiles) {
187
+ let content = fs.readFileSync(htmlFile, 'utf8');
188
+ let modified = content;
189
+
190
+ // Rewrite favicon hrefs from /branding/logos/ to ./images/
191
+ // Only rewrite if the file was actually copied (exists in imagesDir)
192
+ for (const { key } of faviconFiles) {
193
+ const faviconPath = favicon[key];
194
+ if (!faviconPath) continue;
195
+
196
+ const fileNameFromPath = path.basename(faviconPath);
197
+ const sourcePath = faviconPath; // e.g., "/branding/logos/favicon.svg"
198
+ const destPath = `./images/${fileNameFromPath}`;
199
+
200
+ // Only rewrite if the destination file exists
201
+ const destFilePath = path.join(destDir, fileNameFromPath);
202
+ if (fs.existsSync(destFilePath)) {
203
+ // Simple string replacement - sourcePath is like "/branding/logos/favicon.svg"
204
+ const oldHref = `href="${sourcePath}"`;
205
+ const newHref = `href="${destPath}"`;
206
+ modified = modified.replaceAll(oldHref, newHref);
147
207
  }
148
208
  }
209
+
210
+ if (modified !== content) {
211
+ fs.writeFileSync(htmlFile, modified);
212
+ }
149
213
  }
214
+ } catch (err) {
215
+ // Silently ignore - favicon rewriting is handled in vite.config.js
150
216
  }
151
217
  }
152
218
  };
@@ -5,7 +5,6 @@
5
5
  <meta name="viewport" content="width=1=device-width, initial-scale=1.0">
6
6
  <title>{{ title if title else "desy-html docs" }}</title>
7
7
  {% if description %}<meta name="description" content="{{ description }}">{% endif %}
8
- <link rel="icon" type="image/x-icon" href="https://aplicaciones.aragon.es/favicon.ico">
9
8
  {% if branding and branding.typography and branding.typography.fontUrl %}
10
9
  {% if 'fonts.googleapis.com' in branding.typography.fontUrl %}
11
10
  <link rel="preconnect" href="https://fonts.googleapis.com">
package/docs/index.html CHANGED
@@ -147,8 +147,14 @@ cd desy-html</code></pre>
147
147
 
148
148
  <h2>Changelog (English)</h2>
149
149
  <p>What's new in the latest version of desy-html</p>
150
+ <h3>v.16.0.4</h3>
151
+ <ul class="text-sm">
152
+ <li>Fixed env vars in Windows to allow branding configuration.</li>
153
+ <li>Minor fixes.</li>
154
+ </ul>
150
155
  <h3>v.16.0.3</h3>
151
156
  <ul class="text-sm">
157
+ <li>Added favicon to branding configuration and vite plugin.</li>
152
158
  <li>Fixed breakpoints variants in text classes.</li>
153
159
  <li>Added missing params in header components.</li>
154
160
  </ul>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "desy-html",
3
- "version": "16.0.3",
3
+ "version": "16.0.4",
4
4
  "description": "desy-html contains the code you need to start building a user interface for Gobierno de Aragón government webapps.",
5
5
  "type": "module",
6
6
  "author": {
@@ -30,27 +30,30 @@
30
30
  },
31
31
  "scripts": {
32
32
  "dev": "vite",
33
- "dev:yourorganization": "BRANDING_CONFIG=yourorganization vite",
33
+ "dev:yourorganization": "cross-env BRANDING_CONFIG=yourorganization npm run dev",
34
34
  "build": "vite build",
35
- "preview": "vite preview"
35
+ "build:yourorganization": "cross-env BRANDING_CONFIG=yourorganization npm run build",
36
+ "preview": "vite preview",
37
+ "preview:yourorganization": "cross-env BRANDING_CONFIG=yourorganization npm run preview"
36
38
  },
37
39
  "dependencies": {
38
40
  "@floating-ui/dom": "^1.6.13",
41
+ "@tailwindcss/forms": "^0.5.10",
42
+ "@tailwindcss/typography": "^0.5.18",
39
43
  "@tailwindcss/vite": "^4.1.17",
40
44
  "autoprefixer": "^10.4.21",
41
45
  "cally": "^0.8.0",
42
46
  "chokidar": "^3.6.0",
43
47
  "hex-rgb": "^5.0.0",
44
48
  "js-yaml": "^4.1.0",
45
- "tailwindcss": "^4.1.17",
46
- "@tailwindcss/forms": "^0.5.10",
47
- "@tailwindcss/typography": "^0.5.18"
49
+ "tailwindcss": "^4.1.17"
48
50
  },
49
51
  "devDependencies": {
52
+ "cross-env": "^10.1.0",
50
53
  "glob": "^11.0.1",
51
54
  "highlight.js": "^11.11.1",
52
- "nunjucks": "^3.2.4",
53
55
  "js-beautify": "^1.14.11",
56
+ "nunjucks": "^3.2.4",
54
57
  "outdent": "^0.8.0",
55
58
  "sharp": "^0.34.3",
56
59
  "vite": "^7.1.6"
@@ -0,0 +1,9 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" aria-label="Demo Icon" role="img">
2
+ <!-- Face circle -->
3
+ <circle cx="16" cy="16" r="14" fill="none" stroke="currentColor" stroke-width="2"/>
4
+ <!-- Eyes -->
5
+ <circle cx="10" cy="12" r="2" fill="currentColor"/>
6
+ <circle cx="22" cy="12" r="2" fill="currentColor"/>
7
+ <!-- Smile -->
8
+ <path d="M10 20 Q16 26 22 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
9
+ </svg>
@@ -58,7 +58,7 @@ params:
58
58
  type: object
59
59
  required: false
60
60
  description: Options for the subnav at right.
61
- params:
61
+ params:
62
62
  - name: text
63
63
  type: string
64
64
  required: true
@@ -140,7 +140,7 @@ params:
140
140
  type: object
141
141
  required: false
142
142
  description: Options for the dropdown at right.
143
- params:
143
+ params:
144
144
  - name: text
145
145
  type: string
146
146
  required: true
@@ -222,7 +222,7 @@ params:
222
222
  type: object
223
223
  required: false
224
224
  description: Options for the navigation main menu.
225
- params:
225
+ params:
226
226
  - name: classes
227
227
  type: string
228
228
  required: false
@@ -264,7 +264,7 @@ params:
264
264
  type: object
265
265
  required: false
266
266
  description: Options for the offcanvas menu.
267
- params:
267
+ params:
268
268
  - name: text
269
269
  type: string
270
270
  required: true
@@ -11,10 +11,12 @@ params:
11
11
  type: object
12
12
  required: false
13
13
  description: This is an area over the title
14
+ params:
14
15
  - name: logo
15
16
  type: object
16
17
  required: false
17
18
  description: options for the logo element
19
+ params:
18
20
  - name: url
19
21
  type: string
20
22
  required: true
@@ -143,7 +145,7 @@ params:
143
145
  type: object
144
146
  required: false
145
147
  description: Options for the dropdown at right.
146
- params:
148
+ params:
147
149
  - name: text
148
150
  type: string
149
151
  required: true
@@ -225,7 +227,7 @@ params:
225
227
  type: object
226
228
  required: false
227
229
  description: Options for the navigation main menu.
228
- params:
230
+ params:
229
231
  - name: classes
230
232
  type: string
231
233
  required: false
@@ -266,7 +268,7 @@ params:
266
268
  - name: sub
267
269
  type: object
268
270
  required: false
269
- description: This is an area over the title
271
+ description: This is an area under the title
270
272
  - name: logo
271
273
  type: object
272
274
  required: false
@@ -294,7 +296,7 @@ params:
294
296
  - name: backgroundFullColor
295
297
  type: string
296
298
  required: true
297
- description: The css color used in the background image that fills all the super area.
299
+ description: The css color used in the background image that fills all the sub area.
298
300
  - name: backgroundFullUrl
299
301
  type: string
300
302
  required: false
@@ -315,7 +317,7 @@ params:
315
317
  type: object
316
318
  required: false
317
319
  description: Options for the offcanvas menu.
318
- params:
320
+ params:
319
321
  - name: labelledId
320
322
  type: string
321
323
  required: false
package/vite.config.js CHANGED
@@ -18,6 +18,28 @@ const brandingConfig = await loadBrandingConfig(
18
18
  'branding'
19
19
  );
20
20
 
21
+ // Helper para servir archivos con el MIME type correcto
22
+ function serveFileWithMimeType(filePath, res) {
23
+ if (!fs.existsSync(filePath)) return false;
24
+
25
+ const ext = path.extname(filePath).toLowerCase();
26
+ const mimeTypes = {
27
+ '.svg': 'image/svg+xml',
28
+ '.png': 'image/png',
29
+ '.jpg': 'image/jpeg',
30
+ '.jpeg': 'image/jpeg',
31
+ '.webp': 'image/webp',
32
+ '.ico': 'image/x-icon',
33
+ '.js': 'application/javascript',
34
+ '.css': 'text/css',
35
+ '.html': 'text/html'
36
+ };
37
+
38
+ res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
39
+ res.end(fs.readFileSync(filePath));
40
+ return true;
41
+ }
42
+
21
43
  // Helper functions
22
44
  function kebabCaseToPascalCase(value) {
23
45
  return value
@@ -588,32 +610,76 @@ async function rewriteHtmlImagePaths() {
588
610
 
589
611
  const htmlFiles = await glob('**/*.html', { cwd: distDir, nodir: true });
590
612
 
613
+ // Configuración centralizada de rewrites
614
+ const rewriteRules = [
615
+ // Imágenes en /images/
616
+ { pattern: /src="(\/images\/)/g, replacement: 'src="./images/' },
617
+ { pattern: /srcset="(\/images\/)/g, replacement: 'srcset="./images/' },
618
+ // Imágenes en /branding/logos/
619
+ { pattern: /src="(\/branding\/logos\/)/g, replacement: 'src="./images/' },
620
+ { pattern: /srcset="(\/branding\/logos\/)/g, replacement: 'srcset="./images/' },
621
+ // Imágenes en /branding/images/
622
+ { pattern: /src="(\/branding\/images\/)/g, replacement: 'src="./images/' },
623
+ { pattern: /srcset="(\/branding\/images\/)/g, replacement: 'srcset="./images/' },
624
+ ];
625
+
591
626
  for (const htmlFile of htmlFiles) {
592
627
  const filePath = path.join(distDir, htmlFile);
593
628
  let content = await fs.promises.readFile(filePath, 'utf8');
594
629
 
595
- // Replace src="/images/ with src="./images/
596
- let modified = content.replace(/src="(\/images\/)/g, 'src="./images/');
597
-
598
- // Replace srcset="/images/ with srcset="./images/
599
- modified = modified.replace(/srcset="(\/images\/)/g, 'srcset="./images/');
630
+ // Aplicar todas las reglas
631
+ let modified = content;
632
+ for (const rule of rewriteRules) {
633
+ modified = modified.replace(rule.pattern, rule.replacement);
634
+ }
600
635
 
601
- // Replace src="/branding/logos/ with src="./images/
602
- modified = modified.replace(/src="(\/branding\/logos\/)/g, 'src="./images/');
636
+ if (modified !== content) {
637
+ await fs.promises.writeFile(filePath, modified);
638
+ }
639
+ }
640
+ }
603
641
 
604
- // Replace srcset="/branding/logos/ with srcset="./images/
605
- modified = modified.replace(/srcset="(\/branding\/logos\/)/g, 'srcset="./images/');
642
+ // Rewrite favicon hrefs from /branding/logos/ to ./images/ (after HTML files are generated)
643
+ async function rewriteFaviconPaths() {
644
+ const distDir = path.resolve(process.cwd(), 'dist');
645
+ const imagesDir = path.resolve(process.cwd(), 'dist/images');
646
+ if (!fs.existsSync(distDir)) return;
606
647
 
607
- // Replace src="/branding/images/ with src="./images/
608
- modified = modified.replace(/src="(\/branding\/images\/)/g, 'src="./images/');
648
+ const htmlFiles = await glob('**/*.html', { cwd: distDir, nodir: true });
649
+
650
+ // Favicon files to rewrite
651
+ const faviconFiles = [
652
+ 'favicon.svg',
653
+ 'favicon.ico',
654
+ 'apple-touch-icon.png'
655
+ ];
609
656
 
610
- // Replace srcset="/branding/images/ with srcset="./images/
611
- modified = modified.replace(/srcset="(\/branding\/images\/)/g, 'srcset="./images/');
657
+ for (const htmlFile of htmlFiles) {
658
+ const filePath = path.join(distDir, htmlFile);
659
+ let content = await fs.promises.readFile(filePath, 'utf8');
660
+ let modified = content;
661
+
662
+ for (const faviconFile of faviconFiles) {
663
+ const sourcePath = `/branding/logos/${faviconFile}`;
664
+ const destPath = `./images/${faviconFile}`;
665
+
666
+ // Only rewrite if the destination file exists in imagesDir
667
+ const destFilePath = path.join(imagesDir, faviconFile);
668
+ if (fs.existsSync(destFilePath)) {
669
+ const oldHref = `href="${sourcePath}"`;
670
+ const newHref = `href="${destPath}"`;
671
+ modified = modified.replaceAll(oldHref, newHref);
672
+ }
673
+ }
612
674
 
613
675
  if (modified !== content) {
614
676
  await fs.promises.writeFile(filePath, modified);
615
677
  }
616
678
  }
679
+
680
+ if (htmlFiles.length > 0) {
681
+ console.log(`✓ Rewrote favicon paths in ${htmlFiles.length} HTML file(s)`);
682
+ }
617
683
  }
618
684
 
619
685
  // Custom Nunjucks plugin
@@ -643,18 +709,7 @@ function customNunjucksPlugin() {
643
709
  middlewares.use(async (req, res, next) => {
644
710
  if (req.url.startsWith('/branding/')) {
645
711
  const filePath = path.join(process.cwd(), req.url);
646
- if (fs.existsSync(filePath)) {
647
- const ext = path.extname(filePath).toLowerCase();
648
- const mimeTypes = {
649
- '.svg': 'image/svg+xml',
650
- '.png': 'image/png',
651
- '.jpg': 'image/jpeg',
652
- '.jpeg': 'image/jpeg',
653
- '.webp': 'image/webp'
654
- };
655
- res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
656
- return res.end(fs.readFileSync(filePath));
657
- }
712
+ if (serveFileWithMimeType(filePath, res)) return;
658
713
  }
659
714
  next();
660
715
  });
@@ -700,16 +755,7 @@ function customNunjucksPlugin() {
700
755
  }
701
756
 
702
757
  if (fs.existsSync(filePath)) {
703
- const ext = path.extname(filePath).toLowerCase();
704
- const mimeTypes = {
705
- '.svg': 'image/svg+xml',
706
- '.png': 'image/png',
707
- '.jpg': 'image/jpeg',
708
- '.jpeg': 'image/jpeg',
709
- '.webp': 'image/webp'
710
- };
711
- res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
712
- return res.end(fs.readFileSync(filePath));
758
+ if (serveFileWithMimeType(filePath, res)) return;
713
759
  }
714
760
  }
715
761
  next();
@@ -722,18 +768,7 @@ function customNunjucksPlugin() {
722
768
  const brandingLogosPath = path.join(process.cwd(), 'branding/logos');
723
769
  const filePath = findFileRecursive(brandingLogosPath, fileName);
724
770
 
725
- if (filePath && fs.existsSync(filePath)) {
726
- const ext = path.extname(filePath).toLowerCase();
727
- const mimeTypes = {
728
- '.svg': 'image/svg+xml',
729
- '.png': 'image/png',
730
- '.jpg': 'image/jpeg',
731
- '.jpeg': 'image/jpeg',
732
- '.webp': 'image/webp'
733
- };
734
- res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
735
- return res.end(fs.readFileSync(filePath));
736
- }
771
+ if (serveFileWithMimeType(filePath, res)) return;
737
772
  }
738
773
  next();
739
774
  });
@@ -745,18 +780,7 @@ function customNunjucksPlugin() {
745
780
  const brandingImagesPath = path.join(process.cwd(), 'branding/images');
746
781
  const filePath = findFileRecursive(brandingImagesPath, fileName);
747
782
 
748
- if (filePath && fs.existsSync(filePath)) {
749
- const ext = path.extname(filePath).toLowerCase();
750
- const mimeTypes = {
751
- '.svg': 'image/svg+xml',
752
- '.png': 'image/png',
753
- '.jpg': 'image/jpeg',
754
- '.jpeg': 'image/jpeg',
755
- '.webp': 'image/webp'
756
- };
757
- res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
758
- return res.end(fs.readFileSync(filePath));
759
- }
783
+ if (serveFileWithMimeType(filePath, res)) return;
760
784
  }
761
785
  next();
762
786
  });
@@ -878,6 +902,9 @@ function customNunjucksPlugin() {
878
902
  // Step 4b: Correct HTML image paths for production (after HTML files are generated)
879
903
  await rewriteHtmlImagePaths();
880
904
 
905
+ // Step 4c: Rewrite favicon paths from /branding/logos/ to ./images/
906
+ await rewriteFaviconPaths();
907
+
881
908
  // Step 5: Final validation to find and fix common errors
882
909
  await validateBuild();
883
910
 
@@ -1 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="169" height="30.7" viewBox="0 0 169 30.7"><g fill="#ffffff" fill-rule="evenodd"><path d="M13.3 23.8a18.279 18.279 0 0 1-5.1.9 7.762 7.762 0 0 1-5.9-2.4A8.869 8.869 0 0 1 0 16a9.324 9.324 0 0 1 2-6.1 6.45 6.45 0 0 1 5.2-2.4 5.477 5.477 0 0 1 4.6 2.1c1.1 1.4 1.6 3.4 1.6 6v.9H3c.4 3.9 2.3 5.9 5.7 5.9a13.2 13.2 0 0 0 4.6-1ZM3.2 14.3h7.3c0-3.1-1.1-4.6-3.4-4.6-2.4 0-3.7 1.6-3.9 4.6Z"/><path data-name="Trazado 1" d="M28 24.3v-3.1a5.863 5.863 0 0 1-5.5 3.5 5.605 5.605 0 0 1-4.7-2.2 9.008 9.008 0 0 1-1.7-5.9A10.548 10.548 0 0 1 18 10a6.076 6.076 0 0 1 5.1-2.5 5.925 5.925 0 0 1 4.8 2.2V.4h3v23.9Zm0-12.5a6.482 6.482 0 0 0-4.4-2c-2.9 0-4.3 2.2-4.3 6.6 0 3.8 1.3 5.8 3.8 5.8 1.7 0 3.3-.9 4.9-2.7v-7.7Z"/><path d="M46.1 24.3v-3.1c-1.6 2.3-3.5 3.5-5.8 3.5a4.441 4.441 0 0 1-3.4-1.4 5.353 5.353 0 0 1-1.3-3.7V7.9h3v10.8a5.385 5.385 0 0 0 .5 2.6 2.067 2.067 0 0 0 1.8.8q2.7 0 5.1-3.6V7.9h3v16.4Z"/><path data-name="Trazado 2" d="M60.4 24.7a6.828 6.828 0 0 1-5.5-2.5 8.723 8.723 0 0 1-2.2-6.2 8.494 8.494 0 0 1 2.2-6.3 7.781 7.781 0 0 1 6-2.2 23.692 23.692 0 0 1 4.3.5v2.5a15.149 15.149 0 0 0-4.1-.7 4.8 4.8 0 0 0-3.7 1.7 7.209 7.209 0 0 0-1.4 4.6 7.314 7.314 0 0 0 1.4 4.5 4.655 4.655 0 0 0 3.7 1.7 9.245 9.245 0 0 0 4.2-1v2.6a19.135 19.135 0 0 1-4.9.8Z"/><path data-name="Trazado 3" d="M76.7 22.5a7.087 7.087 0 0 1-4.8 2.2 4.6 4.6 0 0 1-3.5-1.3 4.665 4.665 0 0 1-1.4-3.5 4.617 4.617 0 0 1 2.2-4.2 12.192 12.192 0 0 1 6.3-1.5h1.2v-1.5c0-1.7-1-2.6-3-2.6a10.966 10.966 0 0 0-5.3 1.5V8.5a16.05 16.05 0 0 1 6-1.2c4.3 0 6.5 1.7 6.5 5.2v7.4c0 1.3.4 2 1.3 2a1.486 1.486 0 0 0 .6-.1l.1 2.5a8.082 8.082 0 0 1-2.5.4c-1.8 0-3-.7-3.5-2.2h-.2Zm0-2.4v-3.4h-1.1c-2.9 0-4.3.9-4.3 2.7a2.452 2.452 0 0 0 .6 1.6 1.8 1.8 0 0 0 1.6.6 4.932 4.932 0 0 0 3.2-1.5Z"/><path data-name="Trazado 4" d="M85.7 24.3V7.7h4.5v3.1c1.2-2.3 2.9-3.5 5.3-3.5a1.949 1.949 0 0 1 .8.1v4a5.663 5.663 0 0 0-1.8-.3 5.137 5.137 0 0 0-4.4 2.7v10.5Z"/><path data-name="Trazado 5" d="M106.9 22.5a7.087 7.087 0 0 1-4.8 2.2 4.6 4.6 0 0 1-3.5-1.3 4.665 4.665 0 0 1-1.4-3.5 4.617 4.617 0 0 1 2.2-4.2 12.192 12.192 0 0 1 6.3-1.5h1.2v-1.5c0-1.7-1-2.6-3-2.6a10.966 10.966 0 0 0-5.3 1.5V8.5a16.05 16.05 0 0 1 6-1.2c4.3 0 6.5 1.7 6.5 5.2v7.4c0 1.3.4 2 1.3 2a1.486 1.486 0 0 0 .6-.1l.1 2.5a8.082 8.082 0 0 1-2.5.4c-1.8 0-3-.7-3.5-2.2h-.2Zm0-2.4v-3.4h-1.1c-2.9 0-4.3.9-4.3 2.7a2.452 2.452 0 0 0 .6 1.6 1.8 1.8 0 0 0 1.6.6 4.932 4.932 0 0 0 3.2-1.5Z"/><path data-name="Trazado 6" d="m116 29.5.4-3.3a13.3 13.3 0 0 0 5.3 1.3 4.99 4.99 0 0 0 3.6-1.1 4.78 4.78 0 0 0 1.1-3.4v-2.3a5.962 5.962 0 0 1-10.1 1.4 9.3 9.3 0 0 1-1.7-6 9.8 9.8 0 0 1 2-6.4 6.316 6.316 0 0 1 5.2-2.4 6.559 6.559 0 0 1 4.7 2.1l.5-1.7h4v12.7a23.154 23.154 0 0 1-.5 5.5 5.938 5.938 0 0 1-1.8 2.9 9.3 9.3 0 0 1-6.3 1.9 26.488 26.488 0 0 1-6.4-1.2Zm10.4-11.4V12a4.628 4.628 0 0 0-3.4-1.8 3.174 3.174 0 0 0-2.7 1.5 7.121 7.121 0 0 0-1 4.1c0 3.2 1 4.9 3.1 4.9 1.4 0 2.8-.9 4-2.6Z"/><path data-name="Trazado 7" d="M142.3 24.7a8.135 8.135 0 0 1-6.2-2.4 8.869 8.869 0 0 1-2.3-6.3 8.951 8.951 0 0 1 2.3-6.4c1.5-1.6 3.6-2.3 6.3-2.3a8.541 8.541 0 0 1 6.3 2.3 8.7 8.7 0 0 1 2.3 6.3 8.787 8.787 0 0 1-2.3 6.4 8.6 8.6 0 0 1-6.4 2.4Zm0-2.8c2.6 0 3.9-2 3.9-5.9a7.83 7.83 0 0 0-1-4.3 3.25 3.25 0 0 0-5.6 0 7.83 7.83 0 0 0-1 4.3 7.83 7.83 0 0 0 1 4.3 3.152 3.152 0 0 0 2.7 1.6Z"/><path data-name="Trazado 8" d="m139.5 4.9 3.6-4.9h4.1l-4.8 4.9Z"/><path data-name="Trazado 9" d="M154 24.3V7.7h4.5v3.1c1.5-2.3 3.4-3.5 5.7-3.5a4.513 4.513 0 0 1 3.5 1.4 5.167 5.167 0 0 1 1.3 3.8v11.8h-4.5V13.7c0-1.9-.6-2.8-1.9-2.8-1.4 0-2.8 1-4.1 3v10.4Z"/></g></svg>