0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-03 23:00:14 -05:00
ghost/core/server/lib/mobiledoc.js
Simon Backx a051ab3b69
🎨 Reduced favicon requirements and added image formatting (#14918)
fixes https://github.com/TryGhost/Team/issues/1652
fixes https://github.com/TryGhost/Ghost/issues/13319

**Image formatting**
Added support for changing the format of images via the `handle-image-sizes` middleware (e.g. format SVG to png, jpeg, webp)

This change was required:
- Not all browsers support SVG favicons, so we need to convert them to PNGs
- We can't fit image resizing and formatting in the `serve-favicon` middleware: we need to store the resized image to avoid resizing on every request. This system was already present in the `handle-image-sizes` middleware.

To format an uploaded image:
- Original URL: https://localhost/blog/content/images/2022/05/giphy.gif
- To resize: https://localhost/blog/content/images/size/w256h256/2022/05/giphy.gif (already supported)
- To resize and format to webp: https://localhost/blog/content/images/size/w256h256/format/webp/2022/05/giphy.gif
- Animations are preserved when converting Gifs to Webp and in reverse, and also when only resizing (https://github.com/TryGhost/Ghost/issues/13319)

**Favicons**
- Custom favicons are no longer served via `/favicon.png` or `/favicon.ico` (only for default favicon), but use their full path
- Added support for uploading more image extensions in Ghost as a favicon: .jpg, .jpeg, .gif, .webp and .svg are now supported (already supported .png and .ico).
- File extensions other than jpg/jpeg, png, or ico will always get transformed to the image/png format to guarantee browser support (webp and svg images are not yet supported as favicons by all browsers).

For all image formats, other than .ico files:
- Allowed to upload images larger than 1000px in width and height, they will get cropped to 256x256px.
- Allowed uploading favicons that are not square. They will get cropped automatically.
- Allowed to upload larger files, up to 20MB (will get served at a lower file size after being resized)

For .svg files:
- The minimum size of 60x60px is no longer required.

For .ico files:
- The file size limit is increased to 200kb (coming from 100kb)
2022-05-27 16:36:53 +02:00

144 lines
4.8 KiB
JavaScript

const path = require('path');
const errors = require('@tryghost/errors');
const logging = require('@tryghost/logging');
const config = require('../../shared/config');
const storage = require('../adapters/storage');
let cardFactory;
let cards;
let mobiledocHtmlRenderer;
module.exports = {
get blankDocument() {
return {
version: '0.3.1',
ghostVersion: '4.0',
markups: [],
atoms: [],
cards: [],
sections: [
[1, 'p', [
[0, [], 0, '']
]]
]
};
},
get cards() {
if (!cards) {
const CardFactory = require('@tryghost/kg-card-factory');
const defaultCards = require('@tryghost/kg-default-cards');
cardFactory = new CardFactory({
siteUrl: config.get('url'),
imageOptimization: config.get('imageOptimization'),
canTransformImage(storagePath) {
const imageTransform = require('@tryghost/image-transform');
const {ext} = path.parse(storagePath);
// NOTE: the "saveRaw" check is smelly
return imageTransform.canTransformFiles()
&& imageTransform.shouldResizeFileExtension(ext)
&& typeof storage.getStorage('images').saveRaw === 'function';
}
});
cards = defaultCards.map((card) => {
return cardFactory.createCard(card);
});
}
return cards;
},
get atoms() {
return require('@tryghost/kg-default-atoms');
},
get mobiledocHtmlRenderer() {
if (!mobiledocHtmlRenderer) {
const MobiledocHtmlRenderer = require('@tryghost/kg-mobiledoc-html-renderer');
mobiledocHtmlRenderer = new MobiledocHtmlRenderer({
cards: this.cards,
atoms: this.atoms,
unknownCardHandler(args) {
logging.error(new errors.InternalServerError({
message: 'Mobiledoc card \'' + args.env.name + '\' not found.'
}));
}
});
}
return mobiledocHtmlRenderer;
},
get htmlToMobiledocConverter() {
try {
return require('@tryghost/html-to-mobiledoc').toMobiledoc;
} catch (err) {
return () => {
throw new errors.InternalServerError({
message: 'Unable to convert from source HTML to Mobiledoc',
context: 'The html-to-mobiledoc package was not installed',
help: 'Please review any errors from the install process by checking the Ghost logs',
code: 'HTML_TO_MOBILEDOC_INSTALLATION',
err: err
});
};
}
},
// used when force-rerendering post content to ensure that old image card
// payloads contain width/height values to be used when generating srcsets
populateImageSizes: async function (mobiledocJson) {
// do not require image-size until it's requested to avoid circular dependencies
// shared/url-utils > server/lib/mobiledoc > server/lib/image/image-size > server/adapters/storage/utils
const {imageSize} = require('./image');
async function getUnsplashSize(url) {
const parsedUrl = new URL(url);
parsedUrl.searchParams.delete('w');
parsedUrl.searchParams.delete('fit');
parsedUrl.searchParams.delete('crop');
parsedUrl.searchParams.delete('dpr');
return await imageSize.getImageSizeFromUrl(parsedUrl.href);
}
const mobiledoc = JSON.parse(mobiledocJson);
const sizePromises = mobiledoc.cards.map(async (card) => {
const [cardName, payload] = card;
const needsFilling = cardName === 'image' && payload && payload.src && (!payload.width || !payload.height);
if (!needsFilling) {
return;
}
const isUnsplash = payload.src.match(/images\.unsplash\.com/);
try {
const size = isUnsplash ? await getUnsplashSize(payload.src) : await imageSize.getOriginalImageSizeFromStorageUrl(payload.src);
if (size && size.width && size.height) {
payload.width = size.width;
payload.height = size.height;
}
} catch (e) {
// TODO: use debug instead?
logging.error(e);
}
});
await Promise.all(sizePromises);
return JSON.stringify(mobiledoc);
},
// allow config changes to be picked up - useful in tests
reload() {
cardFactory = null;
cards = null;
mobiledocHtmlRenderer = null;
}
};