Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions ghost/core/core/frontend/helpers/reading_time.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@
const {checks} = require('../services/data');
const {SafeString} = require('../services/handlebars');

const {readingTime: calculateReadingTime} = require('@tryghost/helpers');
const {readingTime: calculateAndFormatReadingTime} = require('@tryghost/helpers');

module.exports = function reading_time(options) {// eslint-disable-line camelcase
options = options || {};
options.hash = options.hash || {};
const possiblyPost = this;

// only calculate reading time for posts
if (!checks.isPost(this)) {
if (!checks.isPost(possiblyPost)) {
return null;
}

let readingTime = calculateReadingTime(this, options.hash);
const readingTime = calculateAndFormatReadingTime(possiblyPost, options.hash);

return new SafeString(readingTime);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
const readingMinutes = require('@tryghost/helpers').utils.readingMinutes;

/**
*
* @param {Object} options - frame options
Expand All @@ -13,6 +11,7 @@ module.exports.forPost = (options, model, attrs) => {
const columnsIncludesPlaintext = options.columns?.includes('plaintext');
const columnsIncludesReadingTime = options.columns?.includes('reading_time');
const formatsIncludesPlaintext = options.formats?.includes('plaintext');
const noColumnsRequested = !Object.prototype.hasOwnProperty.call(options, 'columns');

// 1. Gets excerpt from post's plaintext. If custom_excerpt exists, it overrides the excerpt but the key remains excerpt.
if (columnsIncludesExcerpt) {
Expand Down Expand Up @@ -43,7 +42,7 @@ module.exports.forPost = (options, model, attrs) => {
}

// 3. Displays excerpt if no columns was requested - specifically needed for the Admin Posts API
if (!Object.prototype.hasOwnProperty.call(options, 'columns')) {
if (noColumnsRequested) {
let customExcerpt = model.get('custom_excerpt');

if (customExcerpt !== null) {
Expand All @@ -59,14 +58,9 @@ module.exports.forPost = (options, model, attrs) => {
}

// 4. Add `reading_time` if no columns were requested, or if `reading_time` was requested via `columns`
if (!Object.prototype.hasOwnProperty.call(options, 'columns') || columnsIncludesReadingTime) {
if (attrs.html) {
let additionalImages = 0;

if (attrs.feature_image) {
additionalImages += 1;
}
attrs.reading_time = readingMinutes(attrs.html, additionalImages);
if (noColumnsRequested || columnsIncludesReadingTime) {
if (attrs.reading_time === null) {
delete attrs.reading_time;
}
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const {createAddColumnMigration} = require('../../utils');

module.exports = createAddColumnMigration('posts', 'reading_time', {
type: 'integer',
nullable: true,
unsigned: true
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const logging = require('@tryghost/logging');
const {createTransactionalMigration} = require('../../utils');
const {calculatePostReadingTime} = require('../../../../../shared/post-reading-times');

module.exports = createTransactionalMigration(
async function up(knex) {
const posts = await knex.select('id', 'html', 'feature_image').from('posts').whereNotNull('html');

logging.info(`Adding reading_time field value to ${posts.length} posts.`);

// eslint-disable-next-line no-restricted-syntax
for (const post of posts) {
const readingTime = calculatePostReadingTime(post.html, post.feature_image);
await knex('posts').update('reading_time', readingTime).where('id', post.id);
}
},
async function down() {}
);
3 changes: 2 additions & 1 deletion ghost/core/core/server/data/schema/schema.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* String Column Sizes Information
* (From: https://github.com/TryGhost/Ghost/pull/7932)
* New/Updated column maxlengths should meet these guidlines
*
*
* Small strings = length 50
* Medium strings = length 191
* Large strings = length 2000 (use soft limits via validation for 191-2000)
Expand Down Expand Up @@ -96,6 +96,7 @@ module.exports = {
canonical_url: {type: 'text', maxlength: 2000, nullable: true},
newsletter_id: {type: 'string', maxlength: 24, nullable: true, references: 'newsletters.id'},
show_title_and_feature_image: {type: 'boolean', nullable: false, defaultTo: true},
reading_time: {type: 'integer', nullable: true, unsigned: true},
'@@INDEXES@@': [
['type','status','updated_at']
],
Expand Down
10 changes: 10 additions & 0 deletions ghost/core/core/server/models/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const {Newsletter} = require('./newsletter');
const {BadRequestError} = require('@tryghost/errors');
const {mobiledocToLexical} = require('@tryghost/kg-converters');
const {setIsRoles} = require('./role-utils');
const {calculatePostReadingTime} = require('../../shared/post-reading-times');

const messages = {
isAlreadyPublished: 'Your post is already published, please reload your page.',
Expand Down Expand Up @@ -750,6 +751,15 @@ Post = ghostBookshelf.Model.extend({
}
}

// Calculate and store reading_time whenever html or feature_image changes
if (this.hasChanged('html') || this.hasChanged('feature_image')) {
if (this.get('html')) {
this.set('reading_time', calculatePostReadingTime(this.get('html'), this.get('feature_image')));
} else {
this.set('reading_time', null);
}
}

// disabling sanitization until we can implement a better version
if (!options.importing) {
title = this.get('title') || tpl(messages.untitled);
Expand Down
16 changes: 16 additions & 0 deletions ghost/core/core/shared/post-reading-times.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const {readingMinutes} = require('@tryghost/helpers').utils;

/**
* Calculate reading time for a post
* @param {string} html - Post HTML content
* @param {string|null} featureImage - Feature image URL (or null)
* @returns {number} Reading time in minutes
*/
function calculatePostReadingTime(html, featureImage) {
const additionalImages = featureImage ? 1 : 0;
return readingMinutes(html, additionalImages);
}

module.exports = {
calculatePostReadingTime
};
Original file line number Diff line number Diff line change
Expand Up @@ -22699,7 +22699,7 @@ exports[`Activity Feed API Can filter events by post id 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "17979",
"content-length": "18030",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
Expand Down Expand Up @@ -24300,7 +24300,7 @@ exports[`Activity Feed API Returns signup events in activity feed 2: [headers] 1
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "21855",
"content-length": "21991",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ exports[`Members API - member attribution Returns sign up attributions of all ty
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "8685",
"content-length": "8719",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
Expand Down
2 changes: 1 addition & 1 deletion ghost/core/test/unit/server/data/schema/integrity.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const validateRouteSettings = require('../../../../../core/server/services/route
*/
describe('DB version integrity', function () {
// Only these variables should need updating
const currentSchemaHash = '3071f336e59e2ff87f1336caf5dfaed6';
const currentSchemaHash = '97d31db45c4817ed5e7760cf5d60de90';
const currentFixturesHash = '0877727032b8beddbaedc086a8acf1a2';
const currentSettingsHash = 'bb8be7d83407f2b4fa2ad68c19610579';
const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01';
Expand Down
Loading