2
1
Fork 0
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:
Simon Backx 2023-09-05 12:46:27 +02:00 committed by GitHub
parent 0d6e979077
commit 8b1ca62025
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 149 additions and 9 deletions

View 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/'
});

View file

@ -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 = (

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
};

View file

@ -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,

View file

@ -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;
}
});
}
}

View file

@ -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