ultimate-jekyll-manager 1.3.11 → 1.4.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/CLAUDE.md +2 -2
  3. package/README.md +4 -0
  4. package/dist/assets/js/core/auth.js +14 -0
  5. package/dist/assets/js/libs/auth.js +62 -2
  6. package/dist/assets/js/libs/form-manager.js +10 -0
  7. package/dist/assets/themes/classy/css/components/_forms.scss +25 -1
  8. package/dist/commands/setup.js +20 -10
  9. package/dist/defaults/dist/_alternatives/example-competitor.md +2 -2
  10. package/dist/defaults/dist/_includes/admin/sections/sidebar.json +1 -1
  11. package/dist/defaults/dist/_layouts/blueprint/admin/calendar/index.html +4 -4
  12. package/dist/defaults/dist/_layouts/blueprint/admin/firebase/index.html +2 -2
  13. package/dist/defaults/dist/_layouts/blueprint/admin/users/index.html +5 -5
  14. package/dist/defaults/dist/_layouts/blueprint/admin/users/new.html +3 -3
  15. package/dist/defaults/dist/_layouts/blueprint/legal/cookies.md +6 -6
  16. package/dist/defaults/dist/_layouts/blueprint/legal/terms.md +1 -1
  17. package/dist/defaults/dist/_layouts/blueprint/portal/email-preferences.html +2 -2
  18. package/dist/defaults/dist/_layouts/themes/classy/backend/pages/dashboard/index.html +5 -5
  19. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/404.html +2 -2
  20. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/about.html +3 -3
  21. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/account/index.html +22 -22
  22. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/alternative.html +11 -11
  23. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/alternatives/index.html +10 -10
  24. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/app.html +4 -4
  25. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/oauth2.html +3 -3
  26. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/reset.html +3 -3
  27. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signin.html +10 -9
  28. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/auth/signup.html +8 -8
  29. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/categories/category.html +2 -2
  30. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/categories/index.html +1 -1
  31. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/tags/index.html +1 -1
  32. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/blog/tags/tag.html +2 -2
  33. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/contact.html +13 -13
  34. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/download.html +13 -13
  35. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/extension/index.html +7 -7
  36. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/extension/installed.html +1 -1
  37. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/feedback.html +3 -3
  38. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/index.html +29 -29
  39. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/checkout.html +16 -16
  40. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/payment/confirmation.html +5 -5
  41. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/portal/email-preferences.html +4 -4
  42. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/pricing.html +40 -20
  43. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/status.html +2 -2
  44. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/team/index.html +10 -10
  45. package/dist/defaults/dist/_layouts/themes/classy/frontend/pages/team/member.html +1 -1
  46. package/dist/defaults/dist/pages/test/account/dashboard.html +1 -1
  47. package/dist/defaults/dist/pages/test/components/hero-demo-custom.html +2 -2
  48. package/dist/defaults/dist/pages/test/components/hero-demo-form.html +4 -4
  49. package/dist/defaults/dist/pages/test/components/hero-demo-input.html +3 -3
  50. package/dist/defaults/dist/pages/test/components/hero-demo-side.html +2 -2
  51. package/dist/defaults/dist/pages/test/components/hero-demo-video.html +2 -2
  52. package/dist/defaults/dist/pages/test/index.md +2 -2
  53. package/dist/defaults/dist/pages/test/libraries/appearance.html +6 -6
  54. package/dist/defaults/dist/pages/test/libraries/bootstrap.html +99 -99
  55. package/dist/defaults/dist/pages/test/libraries/cover.html +4 -4
  56. package/dist/defaults/dist/pages/test/libraries/error.html +5 -5
  57. package/dist/defaults/dist/pages/test/libraries/firestore.html +2 -2
  58. package/dist/defaults/dist/pages/test/libraries/form-manager.html +9 -9
  59. package/dist/defaults/dist/pages/test/libraries/lazy-loading.html +9 -9
  60. package/dist/defaults/dist/pages/test/redirect/external.md +2 -2
  61. package/dist/defaults/dist/pages/test/redirect/internal.md +2 -2
  62. package/dist/defaults/dist/pages/test/translation/index.md +3 -3
  63. package/dist/defaults/src/_includes/backend/sections/sidebar.json +3 -3
  64. package/dist/defaults/src/_includes/frontend/sections/footer.json +5 -5
  65. package/dist/defaults/src/_includes/frontend/sections/nav.json +1 -1
  66. package/dist/defaults/src/_includes/global/sections/account.json +2 -2
  67. package/dist/gulp/main.js +5 -0
  68. package/dist/gulp/tasks/imagemin.js +160 -79
  69. package/dist/utils/attach-log-file.js +78 -0
  70. package/docs/images.md +27 -0
  71. package/docs/local-development.md +11 -0
  72. package/package.json +1 -1
@@ -4,6 +4,7 @@ const logger = Manager.logger('imagemin');
4
4
  const { src, dest, watch, series } = require('gulp');
5
5
  const glob = require('glob').globSync;
6
6
  const responsive = require('gulp-responsive-modern');
7
+ const sharp = require('sharp');
7
8
  const path = require('path');
8
9
  const jetpack = require('fs-jetpack');
9
10
  const GitHubCache = require('./utils/github-cache');
@@ -15,6 +16,8 @@ const ujmConfig = Manager.getUJMConfig();
15
16
  // Settings
16
17
  const CACHE_DIR = '.temp/cache/imagemin';
17
18
  const CACHE_BRANCH = 'cache-uj-imagemin';
19
+ const MAX_SOURCE_DIMENSION = 4096;
20
+ const REWRITE_QUALITY = 80;
18
21
 
19
22
  // Variables
20
23
  let githubCache;
@@ -120,6 +123,15 @@ async function imagemin(complete) {
120
123
  githubCache.cleanDeletedFromMetadata(meta, files, rootPathProject);
121
124
  }
122
125
 
126
+ // Optionally rewrite oversized source images on disk (opt-in via UJ_IMAGEMIN_REWRITE_SOURCES=true).
127
+ // Caps longest dimension at MAX_SOURCE_DIMENSION so gulp-responsive-modern + sharp don't stall
128
+ // on huge inputs. Runs BEFORE determineFilesToProcess so cached-but-oversized images get
129
+ // rewritten too; the new on-disk content hashes differently than the stored meta hash, so
130
+ // determineFilesToProcess naturally picks the rewritten image up for re-optimization.
131
+ if (process.env.UJ_IMAGEMIN_REWRITE_SOURCES === 'true') {
132
+ await rewriteOversizedSources(files);
133
+ }
134
+
123
135
  // Determine what needs processing
124
136
  const { filesToProcess, validCachePaths } = await determineFilesToProcess(files, meta, githubCache, stats);
125
137
 
@@ -159,87 +171,101 @@ async function imagemin(complete) {
159
171
  // Progress counter
160
172
  let processedOutputs = 0;
161
173
 
162
- // Process images
163
- return src(filesToProcess, { base: 'src/assets/images' })
164
- .pipe(responsive({
165
- [`**/${RESPONSIVE_GLOB}`]: responsiveConfigs
166
- }, {
167
- quality: 80,
168
- progressive: true,
169
- withMetadata: false,
170
- withoutEnlargement: false,
171
- skipOnEnlargement: false,
172
- errorOnUnusedImage: false,
173
- passThroughUnused: true,
174
- }))
175
- .pipe(dest(output))
176
- .on('data', (file) => {
177
- // Progress tracking
178
- processedOutputs++;
179
- const relativePath = path.relative(path.join(rootPathProject, output), file.path);
180
- logger.log(`🖼️ ${processedOutputs}/${expectedOutputs}: ${relativePath}`);
181
-
182
- // Save to cache
183
- const cachePath = path.join(CACHE_DIR, 'images', relativePath);
184
- jetpack.copy(file.path, cachePath, { overwrite: true });
185
-
186
- // Track size after optimization
187
- const fileStats = jetpack.inspect(file.path);
188
- if (fileStats) {
189
- stats.sizeAfter += fileStats.size;
190
- }
191
- })
192
- .on('finish', async () => {
193
- // Calculate final statistics
194
- stats.savedBytes = stats.sizeBefore - stats.sizeAfter;
195
-
196
- // Calculate timing
197
- const endTime = Date.now();
198
- const elapsedMs = endTime - startTime;
199
-
200
- // Log statistics
201
- logImageStatistics(stats, startTime, endTime);
202
-
203
- // Save metadata and push cache
204
- if (githubCache && githubCache.hasCredentials()) {
205
- githubCache.saveMetadata(metaPath, meta);
206
-
207
- logger.log(`📊 Updating cache with ${stats.optimized} new optimizations and README stats...`);
208
-
209
- // Collect all cache files to push (metadata will be auto-included)
210
- const allCacheFiles = glob(path.join(CACHE_DIR, '**/*'), { nodir: true });
211
-
212
- // Push to GitHub with atomic replacement
213
- await githubCache.pushBranch(allCacheFiles, {
214
- validFiles: validCachePaths,
215
- stats: {
216
- timestamp: new Date().toISOString(),
217
- sourceCount: files.length,
218
- cachedCount: allCacheFiles.length - 1,
219
- processedNow: stats.optimized,
220
- fromCache: stats.fromCache,
221
- newlyProcessed: stats.optimized,
222
- timing: {
223
- startTime,
224
- endTime,
225
- elapsedMs
226
- },
227
- imageStats: {
228
- totalImages: stats.totalImages,
229
- optimized: stats.optimized,
230
- skipped: stats.fromCache,
231
- totalSizeBefore: stats.sizeBefore,
232
- totalSizeAfter: stats.sizeAfter,
233
- totalSaved: stats.savedBytes
234
- },
235
- details: `Optimized ${stats.optimized} images, ${stats.fromCache} from cache\n\n### Files from cache:\n${stats.cachedFiles.length > 0 ? stats.cachedFiles.map(f => `- ${f}`).join('\n') : 'None'}\n\n### Newly optimized files:\n${stats.optimizedFiles.length > 0 ? stats.optimizedFiles.map(f => `- ${f}`).join('\n') : 'None'}`
236
- }
237
- });
238
- }
174
+ // Process images.
175
+ //
176
+ // CRITICAL: this function is `async`, which means returning a stream from it yields a
177
+ // Promise<Stream> to gulp — gulp resolves the task immediately on the Promise rather than
178
+ // waiting for the stream's 'finish' event. Downstream tasks (jekyll, audit, etc.) then start
179
+ // before imagemin has actually written its outputs to disk, and the build "succeeds" while
180
+ // silently shipping a partial _site/. We must explicitly await stream completion + cache push
181
+ // before returning, so gulp sees the real completion.
182
+ //
183
+ // This await only ever runs in build mode — dev mode short-circuits via `!Manager.isBuildMode()`
184
+ // above (so `npm start` never blocks on this), letting BrowserSync reload as images land later.
185
+ await new Promise((resolve, reject) => {
186
+ src(filesToProcess, { base: 'src/assets/images' })
187
+ .pipe(responsive({
188
+ [`**/${RESPONSIVE_GLOB}`]: responsiveConfigs
189
+ }, {
190
+ quality: 80,
191
+ progressive: true,
192
+ withMetadata: false,
193
+ withoutEnlargement: false,
194
+ skipOnEnlargement: false,
195
+ errorOnUnusedImage: false,
196
+ passThroughUnused: true,
197
+ }))
198
+ .on('error', reject)
199
+ .pipe(dest(output))
200
+ .on('error', reject)
201
+ .on('data', (file) => {
202
+ // Progress tracking
203
+ processedOutputs++;
204
+ const relativePath = path.relative(path.join(rootPathProject, output), file.path);
205
+ logger.log(`🖼️ ${processedOutputs}/${expectedOutputs}: ${relativePath}`);
206
+
207
+ // Save to cache
208
+ const cachePath = path.join(CACHE_DIR, 'images', relativePath);
209
+ jetpack.copy(file.path, cachePath, { overwrite: true });
210
+
211
+ // Track size after optimization
212
+ const fileStats = jetpack.inspect(file.path);
213
+ if (fileStats) {
214
+ stats.sizeAfter += fileStats.size;
215
+ }
216
+ })
217
+ .on('finish', resolve);
218
+ });
219
+
220
+ // Calculate final statistics
221
+ stats.savedBytes = stats.sizeBefore - stats.sizeAfter;
239
222
 
240
- logger.log('✅ Finished!');
241
- return complete();
223
+ // Calculate timing
224
+ const endTime = Date.now();
225
+ const elapsedMs = endTime - startTime;
226
+
227
+ // Log statistics
228
+ logImageStatistics(stats, startTime, endTime);
229
+
230
+ // Save metadata and push cache
231
+ if (githubCache && githubCache.hasCredentials()) {
232
+ githubCache.saveMetadata(metaPath, meta);
233
+
234
+ logger.log(`📊 Updating cache with ${stats.optimized} new optimizations and README stats...`);
235
+
236
+ // Collect all cache files to push (metadata will be auto-included)
237
+ const allCacheFiles = glob(path.join(CACHE_DIR, '**/*'), { nodir: true });
238
+
239
+ // Push to GitHub with atomic replacement
240
+ await githubCache.pushBranch(allCacheFiles, {
241
+ validFiles: validCachePaths,
242
+ stats: {
243
+ timestamp: new Date().toISOString(),
244
+ sourceCount: files.length,
245
+ cachedCount: allCacheFiles.length - 1,
246
+ processedNow: stats.optimized,
247
+ fromCache: stats.fromCache,
248
+ newlyProcessed: stats.optimized,
249
+ timing: {
250
+ startTime,
251
+ endTime,
252
+ elapsedMs
253
+ },
254
+ imageStats: {
255
+ totalImages: stats.totalImages,
256
+ optimized: stats.optimized,
257
+ skipped: stats.fromCache,
258
+ totalSizeBefore: stats.sizeBefore,
259
+ totalSizeAfter: stats.sizeAfter,
260
+ totalSaved: stats.savedBytes
261
+ },
262
+ details: `Optimized ${stats.optimized} images, ${stats.fromCache} from cache\n\n### Files from cache:\n${stats.cachedFiles.length > 0 ? stats.cachedFiles.map(f => `- ${f}`).join('\n') : 'None'}\n\n### Newly optimized files:\n${stats.optimizedFiles.length > 0 ? stats.optimizedFiles.map(f => `- ${f}`).join('\n') : 'None'}`
263
+ }
242
264
  });
265
+ }
266
+
267
+ logger.log('✅ Finished!');
268
+ return complete();
243
269
  }
244
270
 
245
271
  // Watcher task
@@ -273,6 +299,61 @@ module.exports = series(
273
299
  // Helper Functions
274
300
  // ============================================================================
275
301
 
302
+ // Rewrite oversized source images in place, capping longest dimension at MAX_SOURCE_DIMENSION.
303
+ // Only affects files whose decoded longest side exceeds the cap. Cache invalidation is implicit:
304
+ // the new content hashes differently than the previously-cached entry, so determineFilesToProcess
305
+ // will pick affected files up for re-optimization on its own.
306
+ async function rewriteOversizedSources(files) {
307
+ const responsiveFiles = files.filter(f => RESPONSIVE_EXTENSIONS.has(path.extname(f).slice(1).toLowerCase()));
308
+ if (responsiveFiles.length === 0) {
309
+ return;
310
+ }
311
+
312
+ logger.log(`🔍 Checking ${responsiveFiles.length} source images for oversize (>${MAX_SOURCE_DIMENSION}px longest side)...`);
313
+
314
+ let rewritten = 0;
315
+ for (const file of responsiveFiles) {
316
+ const metadata = await sharp(file).metadata();
317
+ const longest = Math.max(metadata.width || 0, metadata.height || 0);
318
+
319
+ if (longest <= MAX_SOURCE_DIMENSION) {
320
+ continue;
321
+ }
322
+
323
+ const ext = path.extname(file).slice(1).toLowerCase();
324
+ const sizeBefore = jetpack.inspect(file)?.size || 0;
325
+
326
+ // Resize, encode to a buffer (sharp can't write back to its own input file directly), then overwrite.
327
+ const pipeline = sharp(file).resize({
328
+ width: MAX_SOURCE_DIMENSION,
329
+ height: MAX_SOURCE_DIMENSION,
330
+ fit: 'inside',
331
+ withoutEnlargement: true,
332
+ });
333
+
334
+ const buffer = ext === 'png'
335
+ ? await pipeline.png({ quality: REWRITE_QUALITY }).toBuffer()
336
+ : await pipeline.jpeg({ quality: REWRITE_QUALITY, progressive: true, mozjpeg: true }).toBuffer();
337
+
338
+ jetpack.write(file, buffer);
339
+ const sizeAfter = buffer.length;
340
+
341
+ // No cache bookkeeping needed: the rewritten file has new content, so the next
342
+ // determineFilesToProcess() call computes a different hash than the stored meta hash and
343
+ // naturally treats this image as needing reprocessing. Stored meta will be overwritten
344
+ // with the new hash when determineFilesToProcess() runs.
345
+
346
+ rewritten++;
347
+ logger.log(`✂️ Rewrote ${path.relative(rootPathProject, file)}: ${metadata.width}x${metadata.height} → max ${MAX_SOURCE_DIMENSION}px, ${formatBytes(sizeBefore)} → ${formatBytes(sizeAfter)}`);
348
+ }
349
+
350
+ if (rewritten === 0) {
351
+ logger.log(`✅ No oversized sources found (all within ${MAX_SOURCE_DIMENSION}px)`);
352
+ } else {
353
+ logger.log(`✂️ Rewrote ${rewritten} oversized source image(s)`);
354
+ }
355
+ }
356
+
276
357
  // Build responsive configurations from PICTURE_SIZES
277
358
  function getResponsiveConfigs() {
278
359
  const configs = [];
@@ -0,0 +1,78 @@
1
+ // attachLogFile(name) — duplicate process.stdout + process.stderr writes to logs/<name>.log
2
+ // in the consumer project root (process.cwd()).
3
+ //
4
+ // Mirrors EM's attach-log-file pattern so devs (and Claude) can `tail -f logs/dev.log` to see
5
+ // every line of output a UJM session produces — gulp tasks, jekyll child, webpack, SCSS, the
6
+ // works. ANSI color codes are stripped from the file output so it's grep-friendly; the
7
+ // console continues to receive the original colored output unchanged.
8
+ //
9
+ // Skipped entirely when Manager.isServer() returns true — CI/cloud runs don't need a logs/
10
+ // directory left behind in the workspace.
11
+ //
12
+ // Truncates fresh on each call (flags: 'w'), so a new `npm start` doesn't accumulate stale
13
+ // lines from the previous run.
14
+ //
15
+ // Idempotent: calling twice with the same path just returns the existing stream.
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const ANSI_PATTERN = /\x1B\[[0-9;]*[a-zA-Z]/g;
21
+
22
+ let activeStream = null;
23
+ let activePath = null;
24
+ let originalStdoutWrite = null;
25
+ let originalStderrWrite = null;
26
+
27
+ function attachLogFile(name) {
28
+ // Skip on CI/cloud — controlled by UJ_IS_SERVER env var (set by workflows).
29
+ const Manager = require('../build.js');
30
+ if (Manager.isServer()) return null;
31
+
32
+ if (!name) return null;
33
+
34
+ const abs = path.resolve(process.cwd(), 'logs', `${name}.log`);
35
+
36
+ if (activeStream && activePath === abs) return activeStream;
37
+ if (activeStream) detach();
38
+
39
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
40
+ const stream = fs.createWriteStream(abs, { flags: 'w' });
41
+
42
+ stream.write(`# ujm log — ${new Date().toISOString()} — pid=${process.pid}\n`);
43
+
44
+ originalStdoutWrite = process.stdout.write.bind(process.stdout);
45
+ originalStderrWrite = process.stderr.write.bind(process.stderr);
46
+
47
+ process.stdout.write = function (chunk, ...rest) {
48
+ try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
49
+ return originalStdoutWrite(chunk, ...rest);
50
+ };
51
+ process.stderr.write = function (chunk, ...rest) {
52
+ try { stream.write(stripAnsi(String(chunk))); } catch (e) { /* ignore */ }
53
+ return originalStderrWrite(chunk, ...rest);
54
+ };
55
+
56
+ activeStream = stream;
57
+ activePath = abs;
58
+
59
+ return stream;
60
+ }
61
+
62
+ function detach() {
63
+ if (originalStdoutWrite) process.stdout.write = originalStdoutWrite;
64
+ if (originalStderrWrite) process.stderr.write = originalStderrWrite;
65
+ if (activeStream) activeStream.end();
66
+ activeStream = null;
67
+ activePath = null;
68
+ originalStdoutWrite = null;
69
+ originalStderrWrite = null;
70
+ }
71
+
72
+ function stripAnsi(s) {
73
+ return String(s).replace(ANSI_PATTERN, '');
74
+ }
75
+
76
+ module.exports = attachLogFile;
77
+ module.exports.detach = detach;
78
+ module.exports.stripAnsi = stripAnsi;
package/docs/images.md CHANGED
@@ -40,3 +40,30 @@ When posts are created via BEM's `POST /admin/post` endpoint:
40
40
  2. Images are uploaded to `src/assets/images/blog/post-{id}/` on GitHub
41
41
  3. The body is rewritten to use `@post/{filename}` format
42
42
  4. Failed downloads are skipped (original external URL preserved)
43
+
44
+ ## Image Processing Pipeline (`imagemin` gulp task)
45
+
46
+ The `imagemin` task (`src/gulp/tasks/imagemin.js`) processes every file under `src/assets/images/**/*.{jpg,jpeg,png,gif,svg,webp}` into `dist/assets/images/`. For raster formats supported by `gulp-responsive-modern` (`jpg`, `jpeg`, `png`), each source produces **8 outputs**: original + `1024px`, `640px`, `320px` variants, each as the source format **and** WebP.
47
+
48
+ Outputs are content-addressed and cached on a dedicated branch (`cache-uj-imagemin`). Subsequent builds only reprocess images whose source hash changed.
49
+
50
+ ### Source size matters
51
+
52
+ The responsive pipeline streams every input through `sharp` to generate variants. Sources with very large dimensions (tens of thousands of pixels) decode into hundreds of MB of raw pixel data per worker. In practice this can stall the underlying stream so quietly that gulp can't see the failure — the build "succeeds" but the affected images never land in `_site/`.
53
+
54
+ **Recommendation:** Keep source images at sensible dimensions before they land in `src/assets/images/`. A 4096px longest-side cap at the upload step is plenty for blog hero images (the largest responsive variant is 1024px). If you ingest images via an automated pipeline (e.g. BEM's `admin/post` endpoint downloading external URLs), cap them there.
55
+
56
+ ### Cleanup for existing oversized sources: `UJ_IMAGEMIN_REWRITE_SOURCES`
57
+
58
+ If oversized images already exist in your repo and you can't easily re-upload them, run the imagemin task once with the rewrite flag set:
59
+
60
+ ```bash
61
+ UJ_IMAGEMIN_REWRITE_SOURCES=true npm run build
62
+ ```
63
+
64
+ When set, the imagemin task — **before** it pipes images into `gulp-responsive-modern` — scans every file scheduled for processing and rewrites in place any whose longest dimension exceeds `4096px`. Rewrites use `sharp` with `fit: 'inside'` (preserves aspect ratio, never enlarges), JPEG quality 80 / `mozjpeg` / progressive, and PNG quality 80. Cache hashes for affected files are updated so the new content is the new cache key.
65
+
66
+ **Notes:**
67
+ - The flag is **opt-in by design** — running it commits real diffs to your source images. Intended for one-off cleanup runs, not for regular builds or CI.
68
+ - Only files that the cache layer has decided need processing get checked. Already-cached images are untouched. To force-rewrite cached oversized images, also clear the cache (`npx mgr clean`) or delete the `cache-uj-imagemin` branch entries for the relevant files.
69
+ - 4096px is a hardcoded cap (`MAX_SOURCE_DIMENSION` in `src/gulp/tasks/imagemin.js`). Not currently configurable per project — open a PR if you need this.
@@ -2,6 +2,17 @@
2
2
 
3
3
  The local development server URL is stored in `.temp/_config_browsersync.yml` in the consuming project's root directory. Read this file to determine the correct URL for browsing and testing. By default, use `https://192.168.86.69:4000`.
4
4
 
5
+ ## Log Files
6
+
7
+ UJM tees every line of stdout/stderr from the gulp pipeline into a log file in the consumer project root, so you can `tail -f` it or `grep` through it after a run:
8
+
9
+ - `npm start` → `logs/dev.log`
10
+ - `npm run build` (i.e. `UJ_BUILD_MODE=true`) → `logs/build.log`
11
+
12
+ Both files **truncate fresh on each run** — the most recent session only. ANSI color codes are stripped from the file (so it's grep-friendly); the terminal continues to receive colored output unchanged. Captures everything that flows through stdout/stderr: `Manager.logger(...)` output, raw `console.log` calls, gulp task names, jekyll's child output, webpack output, the works.
13
+
14
+ **Skipped on CI/cloud.** When `UJ_IS_SERVER=true` (set by GitHub Actions workflows and other server contexts), the tee is bypassed entirely — no `logs/` directory is written. Implementation: [src/utils/attach-log-file.js](../src/utils/attach-log-file.js), attached at the top of [src/gulp/main.js](../src/gulp/main.js).
15
+
5
16
  ## Connecting to Local Firebase Emulators
6
17
 
7
18
  Set the `FIREBASE_EMULATOR_CONNECT` environment variable to `true` to connect the frontend to local Firebase services (Auth, Firestore, Functions, etc.):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultimate-jekyll-manager",
3
- "version": "1.3.11",
3
+ "version": "1.4.0",
4
4
  "description": "Ultimate Jekyll dependency manager",
5
5
  "main": "dist/index.js",
6
6
  "exports": {