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 +20 -0
- package/DOCS.md +1 -1
- package/ROADMAP.md +6 -0
- package/dist/core/server/fields/utils/imageStyles.js +5 -4
- package/dist/core/server/media/styles/sharp/generateImageStyle.js +27 -0
- package/dist/db-postgres/index.js +4 -4
- package/dist/db-postgres/schema/imageStyle.d.ts +17 -0
- package/dist/db-postgres/schema/imageStyle.js +3 -2
- package/dist/types/fields.d.ts +1 -0
- package/dist/updates/0.14.2/index.d.ts +2 -0
- package/dist/updates/0.14.2/index.js +18 -0
- package/dist/updates/index.js +2 -1
- package/package.json +1 -1
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
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 {
|
|
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
|
|
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
|
|
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
|
|
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)
|
package/dist/types/fields.d.ts
CHANGED
|
@@ -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
|
+
};
|
package/dist/updates/index.js
CHANGED
|
@@ -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
|
-
|
|
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) => {
|