972c25edc7
refs https://github.com/TryGhost/Team/issues/1899 - Added `addEmailAttributionToUrl` method to MemberAttributionService. This adds both the source attribution (`rel=newsletter`) and member attribution (`?attribution_id=123&attribution_type=post`) to a URL. - The URLHistory can now contain a new sort of items: `{type: 'post', id: 'post-id', time: 123}`. - Updated frontend script to read `?attribution_id=123&attribution_type=post` from the URL and add it to the URLHistory + clear it from the URL. - Wired up some external dependencies to LinkReplacementService and added some dummy code. - Increased test coverage of attribution service - Moved all logic that removes the subdirectory from a URL to the UrlTranslator instead of the AttributionBuilder - The UrlTranslator now parses a URLHistoryItem to an object that can be used to build an Attribution instance - Excluded sites with different domain from member id and attribution tracking
164 lines
4.3 KiB
JavaScript
164 lines
4.3 KiB
JavaScript
/**
|
|
* @typedef {Object} UrlService
|
|
* @prop {(resourceId: string, options) => Object} getResource
|
|
* @prop {(resourceId: string, options) => string} getUrlByResourceId
|
|
*
|
|
*/
|
|
|
|
/**
|
|
* Translate a url into, (id+type), or a resource, and vice versa
|
|
*/
|
|
class UrlTranslator {
|
|
/**
|
|
*
|
|
* @param {Object} deps
|
|
* @param {UrlService} deps.urlService
|
|
* @param {Object} deps.urlUtils
|
|
* @param {Object} deps.models
|
|
* @param {Object} deps.models.Post
|
|
* @param {Object} deps.models.Tag
|
|
* @param {Object} deps.models.User
|
|
*/
|
|
constructor({urlService, urlUtils, models}) {
|
|
this.urlService = urlService;
|
|
this.urlUtils = urlUtils;
|
|
this.models = models;
|
|
}
|
|
|
|
/**
|
|
* Convert root relative URLs to subdirectory relative URLs
|
|
*/
|
|
stripSubdirectoryFromPath(path) {
|
|
// Bit weird, but only way to do it with the urlUtils atm
|
|
|
|
// First convert path to an absolute path
|
|
const absolute = this.urlUtils.relativeToAbsolute(path);
|
|
|
|
// Then convert it to a relative path, but without subdirectory
|
|
return this.urlUtils.absoluteToRelative(absolute, {withoutSubdirectory: true});
|
|
}
|
|
|
|
/**
|
|
* Convert root relative URLs to subdirectory relative URLs
|
|
*/
|
|
relativeToAbsolute(path) {
|
|
return this.urlUtils.relativeToAbsolute(path);
|
|
}
|
|
|
|
/**
|
|
* Gives an ordinary URL a name, e.g. / is 'homepage'
|
|
*/
|
|
getUrlTitle(url) {
|
|
return url === '/' ? 'homepage' : url;
|
|
}
|
|
|
|
/**
|
|
* Get the resource type and ID from a URLHistory item that was added by the frontend attribution script
|
|
* @param {import('./history').UrlHistoryItem} item
|
|
* @returns {Promise<{type: string, id: string | null, url: string}|null>} Returns null if the item is invalid
|
|
*/
|
|
async getResourceDetails(item) {
|
|
if (item.type) {
|
|
const resource = await this.getResourceById(item.id, item.type);
|
|
if (resource) {
|
|
return {
|
|
type: item.type,
|
|
id: item.id,
|
|
url: this.getUrlByResourceId(item.id, {absolute: false})
|
|
};
|
|
}
|
|
|
|
// Invalid id: ignore
|
|
return null;
|
|
}
|
|
|
|
if (!item.path) {
|
|
return null;
|
|
}
|
|
|
|
const path = this.stripSubdirectoryFromPath(item.path);
|
|
return {
|
|
type: 'url',
|
|
id: null,
|
|
...this.getTypeAndIdFromPath(path),
|
|
url: path
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the resource type and ID from a path that was visited on the site
|
|
* @param {string} path (excluding subdirectory)
|
|
*/
|
|
getTypeAndIdFromPath(path) {
|
|
const resource = this.urlService.getResource(path);
|
|
if (!resource) {
|
|
return;
|
|
}
|
|
|
|
if (resource.config.type === 'posts') {
|
|
return {
|
|
type: 'post',
|
|
id: resource.data.id
|
|
};
|
|
}
|
|
|
|
if (resource.config.type === 'pages') {
|
|
return {
|
|
type: 'page',
|
|
id: resource.data.id
|
|
};
|
|
}
|
|
|
|
if (resource.config.type === 'tags') {
|
|
return {
|
|
type: 'tag',
|
|
id: resource.data.id
|
|
};
|
|
}
|
|
|
|
if (resource.config.type === 'authors') {
|
|
return {
|
|
type: 'author',
|
|
id: resource.data.id
|
|
};
|
|
}
|
|
}
|
|
|
|
getUrlByResourceId(id, options = {absolute: true}) {
|
|
return this.urlService.getUrlByResourceId(id, options);
|
|
}
|
|
|
|
async getResourceById(id, type) {
|
|
switch (type) {
|
|
case 'post':
|
|
case 'page': {
|
|
const post = await this.models.Post.findOne({id}, {require: false});
|
|
if (!post) {
|
|
return null;
|
|
}
|
|
|
|
return post;
|
|
}
|
|
case 'author': {
|
|
const user = await this.models.User.findOne({id}, {require: false});
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
return user;
|
|
}
|
|
case 'tag': {
|
|
const tag = await this.models.Tag.findOne({id}, {require: false});
|
|
if (!tag) {
|
|
return null;
|
|
}
|
|
|
|
return tag;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
module.exports = UrlTranslator;
|