Skip to content

Commit

Permalink
✨ Favicon URI (#7700)
Browse files Browse the repository at this point in the history
closes #7688

- Use `/favicon.ico` and `/favicon.png` in blog app. Depending on type of storage (custom upload = local file storage), serves either from storage adapter with `read()` method or reads the bytes via `fs`.
- Redirects requests for `favicon.ico` to `favicon.png` if custom `png` icon is uploaded and vice versa.
- Redirect requests for `favicon.png` to `favicon.ico` if default icon is used (in `core/shared`).
- Changes the `{{asset}}` helper for favicon to not serve from theme assets anymore. It will either be served the custom blog-icon or the default one.
- The `{{@blog.icon}}` helper renders the url of the **uploaded** blog icon. It won't render the default icon.
  • Loading branch information
aileen authored and kevinansfield committed Jan 26, 2017
1 parent 584bd15 commit d2f2888
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 66 deletions.
4 changes: 1 addition & 3 deletions core/server/api/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,7 @@ updateConfigCache = function () {
config.set('theme:timezone', (settingsCache.activeTimezone && settingsCache.activeTimezone.value) || config.get('theme').timezone);
config.set('theme:url', globalUtils.url.urlFor('home', true));
config.set('theme:amp', (settingsCache.amp && settingsCache.amp.value === 'true'));
config.set('theme:icon', (settingsCache.icon && settingsCache.icon.value) ?
{type: 'upload', url: (settingsCache.icon && settingsCache.icon.value)} :
{type: 'default', url: config.get('theme:icon')});
config.set('theme:icon', settingsCache.icon && settingsCache.icon.value);

_.each(labsValue, function (value, key) {
config.set('labs:' + key, value);
Expand Down
5 changes: 3 additions & 2 deletions core/server/blog/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ var debug = require('debug')('ghost:blog'),
prettyURLs = require('../middleware/pretty-urls'),
serveSharedFile = require('../middleware/serve-shared-file'),
staticTheme = require('../middleware/static-theme'),
themeHandler = require('../middleware/theme-handler');
themeHandler = require('../middleware/theme-handler'),
serveFavicon = require('../middleware/serve-favicon');

module.exports = function setupBlogApp() {
debug('Blog setup start');
Expand All @@ -41,7 +42,7 @@ module.exports = function setupBlogApp() {
// Static content/assets
// @TODO make sure all of these have a local 404 error handler
// Favicon
blogApp.use(serveSharedFile('favicon.ico', 'image/x-icon', utils.ONE_DAY_S));
blogApp.use(serveFavicon());
// Ghost-Url
blogApp.use(serveSharedFile('shared/ghost-url.js', 'application/javascript', utils.ONE_HOUR_S));
blogApp.use(serveSharedFile('shared/ghost-url.min.js', 'application/javascript', utils.ONE_HOUR_S));
Expand Down
6 changes: 1 addition & 5 deletions core/server/config/overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,7 @@
"publishAPostBySchedulerToleranceInMinutes": 2
},
"theme": {
"timezone": "Etc/UTC",
"icon": {
"type": "default",
"url": "core/shared/favicon.ico"
}
"timezone": "Etc/UTC"
},
"maintenance": {
"enabled": false
Expand Down
17 changes: 13 additions & 4 deletions core/server/data/meta/asset_url.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,22 @@ function getAssetUrl(path, isAdmin, minify) {

output += utils.url.urlJoin(utils.url.getSubdir(), '/');

if (!path.match(/^favicon\.ico$/) && !path.match(/^shared/) && !path.match(/^asset/)) {
if (!path.match(/^favicon\.(ico|png)$/) && !path.match(/^shared/) && !path.match(/^asset/)) {
if (isAdmin) {
output = utils.url.urlJoin(output, 'ghost/');
}

output = utils.url.urlJoin(output, 'assets/');
}

// Serve either uploaded favicon or default
// for favicon, we don't care anymore about the `/` leading slash, as don't support theme favicons
if (path.match(/\/?favicon\.(ico|png)$/)) {
if (isAdmin) {
output = utils.url.urlJoin(utils.url.getSubdir(), '/favicon.ico');
} else {
output = config.get('theme:icon') ? utils.url.urlJoin(utils.url.getSubdir(), utils.url.urlFor('image', {image: config.get('theme:icon')})) : utils.url.urlJoin(utils.url.getSubdir(), '/favicon.ico');
}
}
// Get rid of any leading slash on the path
path = path.replace(/^\//, '');

Expand All @@ -22,9 +30,10 @@ function getAssetUrl(path, isAdmin, minify) {
path = path.replace(/\.([^\.]*)$/, '.min.$1');
}

output += path;
if (!path.match(/^favicon\.(ico|png)$/)) {
// we don't want to concat the path with our favicon url
output += path;

if (!path.match(/^favicon\.ico$/)) {
if (!config.get('assetHash')) {
config.set('assetHash', utils.generateAssetHash());
}
Expand Down
7 changes: 6 additions & 1 deletion core/server/helpers/ghost_head.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var getMetaData = require('../data/meta'),
config = require('../config'),
Promise = require('bluebird'),
labs = require('../utils/labs'),
utils = require('../utils'),
api = require('../api');

function getClient() {
Expand Down Expand Up @@ -86,14 +87,18 @@ function ghost_head(options) {
fetch = {
metaData: getMetaData(this, options.data.root),
client: getClient()
};
},
// CASE: blog icon is not set in config, we serve the default
iconType = !config.get('theme:icon') ? 'x-icon' : config.get('theme:icon').match(/\/favicon\.ico$/i) ? 'x-icon' : 'png',
favicon = !config.get('theme:icon') ? '/favicon.ico' : utils.url.urlFor('image', {image: config.get('theme:icon')}, true);

return Promise.props(fetch).then(function (response) {
client = response.client;
metaData = response.metaData;

if (context) {
// head is our main array that holds our meta data
head.push('<link rel="shortcut icon" href="' + favicon + '" type="' + iconType + '" />');
head.push('<link rel="canonical" href="' +
escapeExpression(metaData.canonicalUrl) + '" />');
head.push('<meta name="referrer" content="' + referrerPolicy + '" />');
Expand Down
88 changes: 88 additions & 0 deletions core/server/middleware/serve-favicon.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
var fs = require('fs'),
path = require('path'),
storage = require('../storage'),
config = require('../config'),
utils = require('../utils'),
crypto = require('crypto'),
buildContentResponse,
content;

buildContentResponse = function buildContentResponse(ext, buf) {
content = {
headers: {
'Content-Type': 'image/' + ext,
'Content-Length': buf.length,
ETag: '"' + crypto.createHash('md5').update(buf, 'utf8').digest('hex') + '"',
'Cache-Control': 'public, max-age=' + utils.ONE_DAY_S
},
body: buf
};

return content;
};

// ### serveFavicon Middleware
// Handles requests to favicon.png and favicon.ico
function serveFavicon() {
var iconType,
filePath;

return function serveFavicon(req, res, next) {
if (req.path.match(/^\/favicon\.(ico|png)/i)) {
// CASE: favicon is default
// confusing: if you upload an icon, it's same logic as storing images
// we store as /content/images, because this is the url path images get requested via the browser
// we are using an express route to skip /content/images and the result is a image path
// based on config.getContentPath('images') + req.path
// in this case we don't use path rewrite, that's why we have to make it manually
filePath = config.get('theme:icon').replace(/\/content\/images\//, '');

var originalExtension = path.extname(filePath).toLowerCase(),
requestedExtension = path.extname(req.path).toLowerCase();

// CASE: custom favicon exists, load it from local file storage
if (config.get('theme:icon')) {
// depends on the uploaded icon extension
if (originalExtension !== requestedExtension) {
return res.redirect(302, '/favicon' + originalExtension);
}

storage.getStorage().read({path: filePath}).then(function readFile(buf, err) {
if (err) {
return next(err);
}

iconType = config.get('theme:icon').match(/\/favicon\.ico$/i) ? 'x-icon' : 'png';

content = buildContentResponse(iconType, buf);

res.writeHead(200, content.headers);
res.end(content.body);
});
} else {
filePath = 'core/shared/favicon.ico';
originalExtension = path.extname(filePath).toLowerCase();

// CASE: always redirect to .ico for default icon
if (originalExtension !== requestedExtension) {
return res.redirect(302, '/favicon.ico');
}

fs.readFile(filePath, function readFile(err, buf) {
if (err) {
return next(err);
}

content = buildContentResponse('x-icon', buf);

res.writeHead(200, content.headers);
res.end(content.body);
});
}
} else {
next();
}
};
}

module.exports = serveFavicon;
47 changes: 2 additions & 45 deletions core/test/unit/config/index_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ describe('Config', function () {
logo: 'casper',
cover: 'casper',
timezone: 'Etc/UTC',
icon: {
type: 'default',
url: 'core/shared/favicon.ico'
}
icon: 'core/shared/favicon.ico'
}
});
});
Expand All @@ -61,10 +58,7 @@ describe('Config', function () {
themeConfig.should.have.property('logo', 'casper');
themeConfig.should.have.property('cover', 'casper');
themeConfig.should.have.property('timezone', 'Etc/UTC');
themeConfig.should.have.property('icon', {
type: 'default',
url: 'core/shared/favicon.ico'
});
themeConfig.should.have.property('icon', 'core/shared/favicon.ico');
});
});

Expand Down Expand Up @@ -93,43 +87,6 @@ describe('Config', function () {
});
});

describe('Favicon default', function () {
it('should use uploaded blog icon', function () {
var themeConfig = config.get('theme');

// Check values are as we expect
themeConfig.should.have.property('icon', {
type: 'default',
url: 'core/shared/favicon.ico'
});

configUtils.set({
theme: {
icon: {
type: 'upload',
url: 'content/images/favicon.ico'
}
}
});

config.get('theme').should.have.property('icon', {
type: 'upload',
url: 'content/images/favicon.ico'
});
});

it('should set theme object with default favicon', function () {
var themeConfig = configUtils.defaultConfig;

// Check values are as we expect
themeConfig.should.have.property('theme');
themeConfig.theme.should.have.property('icon', {
type: 'default',
url: 'core/shared/favicon.ico'
});
});
});

describe('Index', function () {
it('should have exactly the right keys', function () {
var pathConfig = config.get('paths');
Expand Down
1 change: 0 additions & 1 deletion core/test/unit/metadata/image-dimensions_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ describe('getImageDimensions', function () {
getImageDimensions.__set__('getCachedImageSizeFromUrl', sizeOfStub);

getImageDimensions(metaData).then(function (result) {
console.log('result:', result);
should.exist(result);
sizeOfStub.calledWith(metaData.coverImage.url).should.be.true();
sizeOfStub.calledWith(metaData.authorImage.url).should.be.true();
Expand Down
Loading

0 comments on commit d2f2888

Please sign in to comment.