mirror of
https://github.com/TryGhost/Ghost.git
synced 2023-12-13 21:00:40 +01:00
Added "Recommended you" section to settings (#17973)
fixes https://github.com/TryGhost/Product/issues/3808 refs https://github.com/TryGhost/Product/issues/3791
This commit is contained in:
parent
0d6e979077
commit
8b1ca62025
8 changed files with 149 additions and 9 deletions
27
apps/admin-x-settings/src/api/mentions.ts
Normal file
27
apps/admin-x-settings/src/api/mentions.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import {Meta, createQuery} from '../utils/apiRequests';
|
||||
|
||||
export type Mention = {
|
||||
id: string;
|
||||
source: string;
|
||||
source_title: string|null;
|
||||
source_site_title: string|null;
|
||||
source_excerpt: string|null;
|
||||
source_author: string|null;
|
||||
source_featured_image: string|null;
|
||||
source_favicon: string|null;
|
||||
target: string;
|
||||
verified: boolean;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export interface MentionsResponseType {
|
||||
meta?: Meta
|
||||
mentions: Mention[];
|
||||
}
|
||||
|
||||
const dataType = 'MentionsResponseType';
|
||||
|
||||
export const useBrowseMentions = createQuery<MentionsResponseType>({
|
||||
dataType,
|
||||
path: '/mentions/'
|
||||
});
|
|
@ -1,4 +1,5 @@
|
|||
import Button from '../../../admin-x-ds/global/Button';
|
||||
import IncomingRecommendations from './recommendations/IncomingRecommendations';
|
||||
import Link from '../../../admin-x-ds/global/Link';
|
||||
import React, {useState} from 'react';
|
||||
import RecommendationList from './recommendations/RecommendationList';
|
||||
|
@ -35,13 +36,12 @@ const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
id: 'your-recommendations',
|
||||
title: 'Your recommendations',
|
||||
contents: (<RecommendationList recommendations={recommendations ?? []} />)
|
||||
},
|
||||
{
|
||||
id: 'recommending-you',
|
||||
title: 'Recommending you',
|
||||
contents: (<IncomingRecommendations />)
|
||||
}
|
||||
// TODO: Show "Recommending you" tab once we hook it up
|
||||
// {
|
||||
// id: 'recommending-you',
|
||||
// title: 'Recommending you',
|
||||
// contents: (<RecommendationList recommendations={[]} />)
|
||||
// }
|
||||
];
|
||||
|
||||
const groupDescription = (
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import NoValueLabel from '../../../../admin-x-ds/global/NoValueLabel';
|
||||
import React from 'react';
|
||||
import Table from '../../../../admin-x-ds/global/Table';
|
||||
import TableCell from '../../../../admin-x-ds/global/TableCell';
|
||||
import TableRow from '../../../../admin-x-ds/global/TableRow';
|
||||
import {Mention} from '../../../../api/mentions';
|
||||
|
||||
interface IncomingRecommendationListProps {
|
||||
mentions: Mention[]
|
||||
}
|
||||
|
||||
const IncomingRecommendationItem: React.FC<{mention: Mention}> = ({mention}) => {
|
||||
const cleanedSource = mention.source.replace('/.well-known/recommendations.json', '');
|
||||
|
||||
const showDetails = () => {
|
||||
// Open url
|
||||
window.open(cleanedSource, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<TableRow hideActions>
|
||||
<TableCell onClick={showDetails}>
|
||||
<div className='group flex items-center gap-3 hover:cursor-pointer'>
|
||||
<div className={`flex grow flex-col`}>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
{mention.source_favicon && <img alt={mention.source_title || mention.source_site_title || cleanedSource} className="h-5 w-5 rounded-sm" src={mention.source_favicon} />}
|
||||
<span className='line-clamp-1'>{mention.source_title || mention.source_site_title || cleanedSource}</span>
|
||||
</div>
|
||||
<span className='line-clamp-1 text-xs leading-snug text-grey-700'>{mention.source_excerpt || cleanedSource}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
const IncomingRecommendationList: React.FC<IncomingRecommendationListProps> = ({mentions}) => {
|
||||
if (mentions.length) {
|
||||
return <Table>
|
||||
{mentions.map(mention => <IncomingRecommendationItem key={mention.id} mention={mention} />)}
|
||||
</Table>;
|
||||
} else {
|
||||
return <NoValueLabel icon='thumbs-up'>
|
||||
No sites are recommending you yet.
|
||||
</NoValueLabel>;
|
||||
}
|
||||
};
|
||||
|
||||
export default IncomingRecommendationList;
|
|
@ -0,0 +1,15 @@
|
|||
import IncomingRecommendationList from './IncomingRecommendationList';
|
||||
import {useBrowseMentions} from '../../../../api/mentions';
|
||||
|
||||
const IncomingRecommendations: React.FC = () => {
|
||||
const {data: {mentions} = {}} = useBrowseMentions({
|
||||
searchParams: {
|
||||
filter: `source:~$'/.well-known/recommendations.json'+verified:true`,
|
||||
order: 'created_at desc'
|
||||
}
|
||||
});
|
||||
|
||||
return (<IncomingRecommendationList mentions={mentions ?? []} />);
|
||||
};
|
||||
|
||||
export default IncomingRecommendations;
|
|
@ -1,12 +1,41 @@
|
|||
const oembedService = require('../oembed');
|
||||
|
||||
module.exports = class WebmentionMetadata {
|
||||
/**
|
||||
* Helpers that change the URL for which metadata for a given external resource is fetched. Return undefined to now handle the URL.
|
||||
* @type {((url: URL) => URL|undefined)[]}
|
||||
*/
|
||||
#mappers = [];
|
||||
|
||||
/**
|
||||
* @param {(url: URL) => URL|undefined} mapper
|
||||
*/
|
||||
addMapper(mapper) {
|
||||
this.#mappers.push(mapper);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {URL} url
|
||||
*/
|
||||
#getMappedUrl(url) {
|
||||
for (const mapper of this.#mappers) {
|
||||
const mappedUrl = mapper(url);
|
||||
if (mappedUrl) {
|
||||
return this.#getMappedUrl(mappedUrl);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {URL} url
|
||||
* @returns {Promise<import('@tryghost/webmentions/lib/MentionsAPI').WebmentionMetadata>}
|
||||
*/
|
||||
async fetch(url) {
|
||||
const data = await oembedService.fetchOembedDataFromUrl(url.href, 'mention');
|
||||
const mappedUrl = this.#getMappedUrl(url);
|
||||
const data = await oembedService.fetchOembedDataFromUrl(mappedUrl.href, 'mention');
|
||||
|
||||
const result = {
|
||||
siteTitle: data.metadata.publisher,
|
||||
title: data.metadata.title,
|
||||
|
@ -17,6 +46,14 @@ module.exports = class WebmentionMetadata {
|
|||
body: data.body,
|
||||
contentType: data.contentType
|
||||
};
|
||||
|
||||
if (mappedUrl.href !== url.href) {
|
||||
// Still need to fetch body and contentType separately now
|
||||
// For verification
|
||||
const {body, contentType} = await oembedService.fetchPageHtml(url);
|
||||
result.body = body;
|
||||
result.contentType = contentType;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ module.exports = {
|
|||
/** @type {import('@tryghost/webmentions/lib/MentionsAPI')} */
|
||||
api: null,
|
||||
controller: new MentionController(),
|
||||
metadata: new WebmentionMetadata(),
|
||||
/** @type {import('@tryghost/webmentions/lib/MentionSendingService')} */
|
||||
sendingService: null,
|
||||
didInit: false,
|
||||
|
@ -40,7 +41,7 @@ module.exports = {
|
|||
MentionModel: models.Mention,
|
||||
DomainEvents
|
||||
});
|
||||
const webmentionMetadata = new WebmentionMetadata();
|
||||
const webmentionMetadata = this.metadata;
|
||||
const discoveryService = new MentionDiscoveryService({externalRequest});
|
||||
const resourceService = new ResourceService({
|
||||
urlUtils,
|
||||
|
|
|
@ -51,6 +51,17 @@ class RecommendationServiceWrapper {
|
|||
|
||||
// eslint-disable-next-line no-console
|
||||
this.service.init().catch(console.error);
|
||||
|
||||
// Add mapper to WebmentionMetadata
|
||||
mentions.metadata.addMapper((url) => {
|
||||
const p = '/.well-known/recommendations.json';
|
||||
if (url.pathname.endsWith(p)) {
|
||||
// Strip p
|
||||
const newUrl = new URL(url.toString());
|
||||
newUrl.pathname = newUrl.pathname.slice(0, -p.length);
|
||||
return newUrl;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ export class RecommendationService {
|
|||
|
||||
async addRecommendation(addRecommendation: AddRecommendation) {
|
||||
const recommendation = Recommendation.create(addRecommendation);
|
||||
this.repository.save(recommendation);
|
||||
await this.repository.save(recommendation);
|
||||
await this.updateWellknown();
|
||||
|
||||
// Only send an update for the mentioned URL
|
||||
|
|
Loading…
Reference in a new issue