includio-cms 0.14.1 → 0.14.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/CHANGELOG.md CHANGED
@@ -3,6 +3,26 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.14.2 — 2026-03-27
7
+
8
+ Image styles: aspectRatio support, lazy generation, skip defaults when custom styles defined
9
+
10
+ ### Added
11
+ - aspectRatio option for image styles — crop to ratio without fixed dimensions
12
+ - Lazy generation of custom field styles on first read
13
+ - Skip default styles when field defines custom styles (prevents <source> conflicts in <picture>)
14
+
15
+ ### Migration
16
+
17
+ ```sql
18
+ ALTER TABLE image_styles ADD COLUMN IF NOT EXISTS aspect_ratio REAL;
19
+
20
+ -- Drop old unique index and recreate with aspect_ratio
21
+ DROP INDEX IF EXISTS image_styles_unique_key;
22
+ CREATE UNIQUE INDEX image_styles_unique_key
23
+ ON image_styles (media_file_id, name, COALESCE(width, 0), COALESCE(height, 0), COALESCE(quality, 0), COALESCE(aspect_ratio, 0));
24
+ ```
25
+
6
26
  ## 0.14.1 — 2026-03-27
7
27
 
8
28
  CMS context provider, Svelte 5 compat, docs overhaul
package/DOCS.md CHANGED
@@ -1,4 +1,4 @@
1
- # Includio CMS Documentation (v0.14.1)
1
+ # Includio CMS Documentation (v0.14.2)
2
2
 
3
3
  > This file is auto-generated from the docs site. For the latest version, update the package.
4
4
 
package/ROADMAP.md CHANGED
@@ -263,6 +263,12 @@
263
263
  - [x] `[chore]` `[P1]` Documentation overhaul — new pages, README rewrite, DOCS.md compilation script <!-- files: scripts/compile-docs.ts, DOCS.md -->
264
264
  - [x] `[chore]` `[P2]` Remove obsolete docs/ Obsidian config & stale docs
265
265
 
266
+ ## 0.14.2 — Image styles: aspectRatio, skip defaults, lazy generation
267
+
268
+ - [x] `[feature]` `[P1]` aspectRatio option for image styles — crop to ratio without fixed dimensions <!-- files: src/lib/types/fields.ts, src/lib/core/server/media/styles/sharp/generateImageStyle.ts, src/lib/db-postgres/schema/imageStyle.ts -->
269
+ - [x] `[feature]` `[P1]` Skip default styles when field defines custom styles (prevents `<source>` conflicts in `<picture>`) <!-- files: src/lib/core/server/fields/utils/imageStyles.ts -->
270
+ - [x] `[feature]` `[P2]` Lazy generation of custom field styles on first read <!-- files: src/lib/core/server/fields/utils/imageStyles.ts -->
271
+
266
272
  ## 0.15.0 — SEO module
267
273
 
268
274
  - [ ] `[feature]` `[P1]` SERP preview + character limits for title/description <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
@@ -1,4 +1,4 @@
1
- import { getImageStyleIfExists } from '../../media/styles/operations/getImageStyle.js';
1
+ import { getImageStyle } from '../../media/styles/operations/getImageStyle.js';
2
2
  import { getCMS } from '../../../cms.js';
3
3
  import { generateBlurDataUrl } from '../../media/utils/generateBlurDataUrl.js';
4
4
  export const defaultStyles = [
@@ -61,13 +61,14 @@ async function ensureBlurDataUrl(val) {
61
61
  }
62
62
  }
63
63
  export async function getImageStyles(field, val) {
64
- const rawStyles = [...(isProcessableImage(val) ? defaultStyles : []), ...(field.styles || [])];
64
+ const hasCustomStyles = field.styles && field.styles.length > 0;
65
+ const rawStyles = [...(isProcessableImage(val) && !hasCustomStyles ? defaultStyles : []), ...(field.styles || [])];
65
66
  const originalFormat = getOriginalFormat(val);
66
67
  const stylesArr = expandStyleFormats(rawStyles, originalFormat);
67
68
  const [styles, blurDataUrl] = await Promise.all([
68
69
  Promise.all(stylesArr.map(async (style) => {
69
70
  try {
70
- const styleDbData = await getImageStyleIfExists(val.id, style);
71
+ const styleDbData = await getImageStyle(val.id, style);
71
72
  if (!styleDbData)
72
73
  return null;
73
74
  const result = {
@@ -88,7 +89,7 @@ export async function getImageStyles(field, val) {
88
89
  srcset: undefined,
89
90
  sizes: undefined
90
91
  };
91
- const variantData = await getImageStyleIfExists(val.id, variantStyle);
92
+ const variantData = await getImageStyle(val.id, variantStyle);
92
93
  if (!variantData)
93
94
  return null;
94
95
  return `${variantData.url} ${w}w`;
@@ -31,6 +31,33 @@ export async function generateImageStyleFromBuffer(buf, mediaFile, style) {
31
31
  const width = style.width ?? imgWidth ?? mediaFile.width ?? undefined;
32
32
  const height = style.height ?? undefined;
33
33
  if (
34
+ // Aspect ratio crop
35
+ style.crop &&
36
+ style.aspectRatio &&
37
+ imgWidth &&
38
+ imgHeight) {
39
+ const targetAspect = style.aspectRatio;
40
+ const imgAspect = imgWidth / imgHeight;
41
+ let cropW, cropH;
42
+ if (imgAspect > targetAspect) {
43
+ cropH = imgHeight;
44
+ cropW = Math.round(imgHeight * targetAspect);
45
+ }
46
+ else {
47
+ cropW = imgWidth;
48
+ cropH = Math.round(imgWidth / targetAspect);
49
+ }
50
+ const fX = mediaFile.focalX ?? 0.5;
51
+ const fY = mediaFile.focalY ?? 0.5;
52
+ const region = calculateFocalCropRegion(imgWidth, imgHeight, fX, fY, cropW, cropH);
53
+ sharpInstance = sharpInstance.extract(region);
54
+ if (style.width || style.height) {
55
+ const resizeW = style.width ?? undefined;
56
+ const resizeH = style.height ?? (resizeW ? Math.round(resizeW / style.aspectRatio) : undefined);
57
+ sharpInstance = sharpInstance.resize(resizeW, resizeH);
58
+ }
59
+ }
60
+ else if (
34
61
  // Focal point crop
35
62
  style.crop &&
36
63
  width &&
@@ -584,7 +584,7 @@ export function pg(config) {
584
584
  const [imageStyle] = await db
585
585
  .select()
586
586
  .from(schema.imageStylesTable)
587
- .where(and(eq(schema.imageStylesTable.mediaFileId, mediaFileId), eq(schema.imageStylesTable.name, style.name), style.width ? eq(schema.imageStylesTable.width, style.width) : undefined, style.height ? eq(schema.imageStylesTable.height, style.height) : undefined, style.quality ? eq(schema.imageStylesTable.quality, style.quality) : undefined))
587
+ .where(and(eq(schema.imageStylesTable.mediaFileId, mediaFileId), eq(schema.imageStylesTable.name, style.name), style.width ? eq(schema.imageStylesTable.width, style.width) : undefined, style.height ? eq(schema.imageStylesTable.height, style.height) : undefined, style.quality ? eq(schema.imageStylesTable.quality, style.quality) : undefined, style.aspectRatio ? eq(schema.imageStylesTable.aspectRatio, style.aspectRatio) : undefined))
588
588
  .limit(1);
589
589
  if (!imageStyle)
590
590
  return null;
@@ -597,9 +597,9 @@ export function pg(config) {
597
597
  createImageStyle: async (mediaFileId, file, style) => {
598
598
  const mimeType = file.mimeType ?? `image/${style.format}`;
599
599
  const rows = await db.execute(sql `
600
- INSERT INTO image_styles (id, media_file_id, name, url, width, height, crop, quality, media, mime_type)
601
- VALUES (gen_random_uuid(), ${mediaFileId}, ${style.name}, ${file.url}, ${style.width ?? null}, ${style.height ?? null}, ${style.crop ?? false}, ${style.quality ?? null}, ${style.media ?? null}, ${mimeType})
602
- ON CONFLICT (media_file_id, name, COALESCE(width, 0), COALESCE(height, 0), COALESCE(quality, 0))
600
+ INSERT INTO image_styles (id, media_file_id, name, url, width, height, crop, quality, media, mime_type, aspect_ratio)
601
+ VALUES (gen_random_uuid(), ${mediaFileId}, ${style.name}, ${file.url}, ${style.width ?? null}, ${style.height ?? null}, ${style.crop ?? false}, ${style.quality ?? null}, ${style.media ?? null}, ${mimeType}, ${style.aspectRatio ?? null})
602
+ ON CONFLICT (media_file_id, name, COALESCE(width, 0), COALESCE(height, 0), COALESCE(quality, 0), COALESCE(aspect_ratio, 0))
603
603
  DO UPDATE SET url = EXCLUDED.url, mime_type = EXCLUDED.mime_type
604
604
  RETURNING url, mime_type, media
605
605
  `);
@@ -172,6 +172,23 @@ export declare const imageStylesTable: import("drizzle-orm/pg-core/table", { wit
172
172
  identity: undefined;
173
173
  generated: undefined;
174
174
  }, {}, {}>;
175
+ aspectRatio: import("drizzle-orm/pg-core", { with: { "resolution-mode": "require" } }).PgColumn<{
176
+ name: "aspect_ratio";
177
+ tableName: "image_styles";
178
+ dataType: "number";
179
+ columnType: "PgReal";
180
+ data: number;
181
+ driverParam: string | number;
182
+ notNull: false;
183
+ hasDefault: false;
184
+ isPrimaryKey: false;
185
+ isAutoincrement: false;
186
+ hasRuntimeDefault: false;
187
+ enumValues: undefined;
188
+ baseColumn: never;
189
+ identity: undefined;
190
+ generated: undefined;
191
+ }, {}, {}>;
175
192
  };
176
193
  dialect: "pg";
177
194
  }>;
@@ -1,4 +1,4 @@
1
- import { boolean, integer, pgTable, text, uuid } from 'drizzle-orm/pg-core';
1
+ import { boolean, integer, pgTable, real, text, uuid } from 'drizzle-orm/pg-core';
2
2
  export const imageStylesTable = pgTable('image_styles', {
3
3
  id: uuid('id').primaryKey().defaultRandom(),
4
4
  mediaFileId: uuid('media_file_id').notNull(),
@@ -9,7 +9,8 @@ export const imageStylesTable = pgTable('image_styles', {
9
9
  crop: boolean('crop').notNull().default(false),
10
10
  quality: integer('quality'),
11
11
  media: text('media'),
12
- mimeType: text('mime_type').notNull()
12
+ mimeType: text('mime_type').notNull(),
13
+ aspectRatio: real('aspect_ratio')
13
14
  });
14
15
  // NOTE: unique index on (media_file_id, name, COALESCE(width,0), COALESCE(height,0), COALESCE(quality,0))
15
16
  // is managed via SQL migration in src/lib/updates/0.5.8/index.ts (expression-based, not supported by Drizzle ORM)
@@ -68,6 +68,7 @@ export interface ImageFieldStyle {
68
68
  format?: keyof FormatEnum;
69
69
  media?: string;
70
70
  crop?: boolean;
71
+ aspectRatio?: number;
71
72
  quality?: number;
72
73
  srcset?: number[];
73
74
  sizes?: string;
@@ -0,0 +1,2 @@
1
+ import type { CmsUpdate } from '../index.js';
2
+ export declare const update: CmsUpdate;
@@ -0,0 +1,18 @@
1
+ export const update = {
2
+ version: '0.14.2',
3
+ date: '2026-03-27',
4
+ description: 'Image styles: aspectRatio support, lazy generation, skip defaults when custom styles defined',
5
+ features: [
6
+ 'aspectRatio option for image styles — crop to ratio without fixed dimensions',
7
+ 'Lazy generation of custom field styles on first read',
8
+ 'Skip default styles when field defines custom styles (prevents <source> conflicts in <picture>)'
9
+ ],
10
+ fixes: [],
11
+ breakingChanges: [],
12
+ sql: `ALTER TABLE image_styles ADD COLUMN IF NOT EXISTS aspect_ratio REAL;
13
+
14
+ -- Drop old unique index and recreate with aspect_ratio
15
+ DROP INDEX IF EXISTS image_styles_unique_key;
16
+ CREATE UNIQUE INDEX image_styles_unique_key
17
+ ON image_styles (media_file_id, name, COALESCE(width, 0), COALESCE(height, 0), COALESCE(quality, 0), COALESCE(aspect_ratio, 0));`
18
+ };
@@ -39,7 +39,8 @@ import { update as update0133 } from './0.13.3/index.js';
39
39
  import { update as update0134 } from './0.13.4/index.js';
40
40
  import { update as update0140 } from './0.14.0/index.js';
41
41
  import { update as update0141 } from './0.14.1/index.js';
42
- export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130, update0131, update0132, update0133, update0134, update0140, update0141];
42
+ import { update as update0142 } from './0.14.2/index.js';
43
+ export const updates = [update0065, update0066, update0067, update0068, update0069, update010, update011, update012, update013, update014, update015, update020, update022, update050, update051, update052, update053, update054, update055, update056, update057, update058, update060, update061, update062, update070, update071, update072, update073, update080, update090, update0100, update0110, update0120, update0130, update0131, update0132, update0133, update0134, update0140, update0141, update0142];
43
44
  export const getUpdatesFrom = (fromVersion) => {
44
45
  const fromParts = fromVersion.split('.').map(Number);
45
46
  return updates.filter((update) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "includio-cms",
3
- "version": "0.14.1",
3
+ "version": "0.14.2",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && npm run prepack",