mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Rework opengraph
This commit is contained in:
parent
72970bf480
commit
ba241f0431
8 changed files with 133 additions and 255 deletions
|
|
@ -251,9 +251,12 @@ class Conf {
|
||||||
return optionalBooleanSchema.parse(Deno.env.get('CRON_ENABLED')) ?? true;
|
return optionalBooleanSchema.parse(Deno.env.get('CRON_ENABLED')) ?? true;
|
||||||
}
|
}
|
||||||
/** Crawler User-Agent regex to render link previews to. */
|
/** Crawler User-Agent regex to render link previews to. */
|
||||||
static get crawlerRegex(): string {
|
static get crawlerRegex(): RegExp {
|
||||||
return Deno.env.get('CRAWLER_REGEX') ||
|
return new RegExp(
|
||||||
'googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp|mastodon|pleroma|Discordbot|AhrefsBot|SEMrushBot|MJ12bot|SeekportBot|Synapse|Matrix';
|
Deno.env.get('CRAWLER_REGEX') ||
|
||||||
|
'googlebot|bingbot|yandex|baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterestbot|slackbot|vkShare|W3C_Validator|whatsapp|mastodon|pleroma|Discordbot|AhrefsBot|SEMrushBot|MJ12bot|SeekportBot|Synapse|Matrix',
|
||||||
|
'i',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */
|
/** Path to the custom policy module. Must be an absolute path, https:, npm:, or jsr: URI. */
|
||||||
static get policy(): string {
|
static get policy(): string {
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: Abort
|
||||||
}
|
}
|
||||||
return filters;
|
return filters;
|
||||||
} catch {
|
} catch {
|
||||||
// do nothing
|
// fall through
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -164,7 +164,7 @@ async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: Abort
|
||||||
return [{ kinds: [0], authors: [pubkey] }];
|
return [{ kinds: [0], authors: [pubkey] }];
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// do nothing
|
// fall through
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -2,81 +2,19 @@ import { AppMiddleware } from '@/app.ts';
|
||||||
import { Conf } from '@/config.ts';
|
import { Conf } from '@/config.ts';
|
||||||
import { Stickynotes } from '@soapbox/stickynotes';
|
import { Stickynotes } from '@soapbox/stickynotes';
|
||||||
import { Storages } from '@/storages.ts';
|
import { Storages } from '@/storages.ts';
|
||||||
import {
|
import { getPathParams, MetadataEntities } from '@/utils/og-metadata.ts';
|
||||||
fetchProfile,
|
|
||||||
getHandle,
|
|
||||||
getPathParams,
|
|
||||||
getStatusInfo,
|
|
||||||
OpenGraphTemplateOpts,
|
|
||||||
PathParams,
|
|
||||||
} from '@/utils/og-metadata.ts';
|
|
||||||
import { getInstanceMetadata } from '@/utils/instance.ts';
|
import { getInstanceMetadata } from '@/utils/instance.ts';
|
||||||
import { metadataView } from '@/views/meta.ts';
|
import { lookupPubkey } from '@/utils/lookup.ts';
|
||||||
|
import { renderMetadata } from '@/views/meta.ts';
|
||||||
|
import { getAuthor, getEvent } from '@/queries.ts';
|
||||||
|
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
|
import { renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
|
|
||||||
const console = new Stickynotes('ditto:frontend');
|
const console = new Stickynotes('ditto:frontend');
|
||||||
|
|
||||||
/** Placeholder to find & replace with metadata. */
|
/** Placeholder to find & replace with metadata. */
|
||||||
const META_PLACEHOLDER = '<!--server-generated-meta-->' as const;
|
const META_PLACEHOLDER = '<!--server-generated-meta-->' as const;
|
||||||
|
|
||||||
async function buildTemplateOpts(params: PathParams, url: string): Promise<OpenGraphTemplateOpts> {
|
|
||||||
const store = await Storages.db();
|
|
||||||
const meta = await getInstanceMetadata(store);
|
|
||||||
|
|
||||||
const res: OpenGraphTemplateOpts = {
|
|
||||||
title: meta.name,
|
|
||||||
type: 'article',
|
|
||||||
description: meta.about,
|
|
||||||
url,
|
|
||||||
site: meta.name,
|
|
||||||
image: {
|
|
||||||
url: Conf.local('/favicon.ico'),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (params.statusId) {
|
|
||||||
const { description, image, title } = await getStatusInfo(params.statusId);
|
|
||||||
|
|
||||||
res.description = description;
|
|
||||||
res.title = title;
|
|
||||||
|
|
||||||
if (res.image) {
|
|
||||||
res.image = image;
|
|
||||||
}
|
|
||||||
} else if (params.acct) {
|
|
||||||
const key = /^[a-f0-9]{64}$/.test(params.acct) ? 'pubkey' : 'handle';
|
|
||||||
let handle = '';
|
|
||||||
try {
|
|
||||||
const profile = await fetchProfile({ [key]: params.acct });
|
|
||||||
handle = await getHandle(params.acct, profile);
|
|
||||||
|
|
||||||
res.description = profile.meta.about;
|
|
||||||
|
|
||||||
if (profile.meta.picture) {
|
|
||||||
res.image = {
|
|
||||||
url: profile.meta.picture,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
console.debug(`couldn't find kind 0 for ${params.acct}`);
|
|
||||||
// @ts-ignore we don't want getHandle trying to do a lookup here
|
|
||||||
// but we do want it to give us a nice pretty npub
|
|
||||||
handle = await getHandle(params.acct, {});
|
|
||||||
res.description = `@${handle}'s Nostr profile`;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.type = 'profile';
|
|
||||||
res.title = `View @${handle}'s profile on Ditto`;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('Error getting OpenGraph metadata information:');
|
|
||||||
console.debug(e);
|
|
||||||
console.trace();
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const frontendController: AppMiddleware = async (c, next) => {
|
export const frontendController: AppMiddleware = async (c, next) => {
|
||||||
try {
|
try {
|
||||||
const content = await Deno.readTextFile(new URL('../../public/index.html', import.meta.url));
|
const content = await Deno.readTextFile(new URL('../../public/index.html', import.meta.url));
|
||||||
|
|
@ -84,7 +22,7 @@ export const frontendController: AppMiddleware = async (c, next) => {
|
||||||
const ua = c.req.header('User-Agent');
|
const ua = c.req.header('User-Agent');
|
||||||
console.debug('ua', ua);
|
console.debug('ua', ua);
|
||||||
|
|
||||||
if (!new RegExp(Conf.crawlerRegex, 'i').test(ua ?? '')) {
|
if (!Conf.crawlerRegex.test(ua ?? '')) {
|
||||||
return c.html(content);
|
return c.html(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,7 +30,8 @@ export const frontendController: AppMiddleware = async (c, next) => {
|
||||||
const params = getPathParams(c.req.path);
|
const params = getPathParams(c.req.path);
|
||||||
if (params) {
|
if (params) {
|
||||||
try {
|
try {
|
||||||
const meta = metadataView(await buildTemplateOpts(params, Conf.local(c.req.path)));
|
const entities = await getEntities(params);
|
||||||
|
const meta = renderMetadata(c.req.url, entities);
|
||||||
return c.html(content.replace(META_PLACEHOLDER, meta));
|
return c.html(content.replace(META_PLACEHOLDER, meta));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(`Error building meta tags: ${e}`);
|
console.log(`Error building meta tags: ${e}`);
|
||||||
|
|
@ -106,3 +45,30 @@ export const frontendController: AppMiddleware = async (c, next) => {
|
||||||
await next();
|
await next();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function getEntities(params: { acct?: string; statusId?: string }): Promise<MetadataEntities> {
|
||||||
|
const store = await Storages.db();
|
||||||
|
|
||||||
|
const entities: MetadataEntities = {
|
||||||
|
instance: await getInstanceMetadata(store),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (params.statusId) {
|
||||||
|
const event = await getEvent(params.statusId, { kind: 1 });
|
||||||
|
if (event) {
|
||||||
|
entities.status = await renderStatus(event, {});
|
||||||
|
entities.account = entities.status?.account;
|
||||||
|
}
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.acct) {
|
||||||
|
const pubkey = await lookupPubkey(params.acct);
|
||||||
|
const event = pubkey ? await getAuthor(pubkey) : undefined;
|
||||||
|
if (event) {
|
||||||
|
entities.account = await renderAccount(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return entities;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,16 @@ export interface MastodonStatus {
|
||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
reblog: MastodonStatus | null;
|
reblog: MastodonStatus | null;
|
||||||
application: unknown;
|
application: unknown;
|
||||||
media_attachments: unknown[];
|
media_attachments: {
|
||||||
|
type: string;
|
||||||
|
preview_url?: string;
|
||||||
|
meta?: {
|
||||||
|
original?: {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}[];
|
||||||
mentions: unknown[];
|
mentions: unknown[];
|
||||||
tags: unknown[];
|
tags: unknown[];
|
||||||
emojis: unknown[];
|
emojis: unknown[];
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ export function extractIdentifier(value: string): string | undefined {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// do nothing
|
// fall through
|
||||||
}
|
}
|
||||||
|
|
||||||
value = value.replace(/^@/, '');
|
value = value.replace(/^@/, '');
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,20 @@
|
||||||
import { NostrEvent, NostrMetadata, NSchema as n } from '@nostrify/nostrify';
|
import { nip19 } from 'nostr-tools';
|
||||||
import { Stickynotes } from '@soapbox/stickynotes';
|
|
||||||
import { nip19, nip27 } from 'nostr-tools';
|
|
||||||
import { match } from 'path-to-regexp';
|
import { match } from 'path-to-regexp';
|
||||||
|
|
||||||
import { getAuthor, getEvent } from '@/queries.ts';
|
import { MastodonAccount } from '@/entities/MastodonAccount.ts';
|
||||||
import { lookupPubkey } from '@/utils/lookup.ts';
|
import { MastodonStatus } from '@/entities/MastodonStatus.ts';
|
||||||
import { parseAndVerifyNip05 } from '@/utils/nip05.ts';
|
import { InstanceMetadata } from '@/utils/instance.ts';
|
||||||
import { parseNip05 } from '@/utils.ts';
|
|
||||||
|
|
||||||
const console = new Stickynotes('ditto:frontend');
|
export interface MetadataEntities {
|
||||||
|
status?: MastodonStatus;
|
||||||
export interface OpenGraphTemplateOpts {
|
account?: MastodonAccount;
|
||||||
title: string;
|
instance: InstanceMetadata;
|
||||||
type: 'article' | 'profile' | 'website';
|
|
||||||
url: string;
|
|
||||||
image?: StatusInfo['image'];
|
|
||||||
description?: string;
|
|
||||||
site: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PathParams = Partial<Record<'statusId' | 'acct' | 'note' | 'nevent' | 'nprofile' | 'npub', string>>;
|
export interface MetadataPathParams {
|
||||||
|
statusId?: string;
|
||||||
interface StatusInfo {
|
acct?: string;
|
||||||
title: string;
|
bech32?: string;
|
||||||
description: string;
|
|
||||||
image?: {
|
|
||||||
url: string;
|
|
||||||
w?: number;
|
|
||||||
h?: number;
|
|
||||||
alt?: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** URL routes to serve metadata on. */
|
/** URL routes to serve metadata on. */
|
||||||
|
|
@ -42,139 +27,40 @@ const SSR_ROUTES = [
|
||||||
'/statuses/:statusId',
|
'/statuses/:statusId',
|
||||||
'/notice/:statusId',
|
'/notice/:statusId',
|
||||||
'/posts/:statusId',
|
'/posts/:statusId',
|
||||||
'/note:note',
|
'/:bech32',
|
||||||
'/nevent:nevent',
|
|
||||||
'/nprofile:nprofile',
|
|
||||||
'/npub:npub',
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const SSR_ROUTE_MATCHERS = SSR_ROUTES.map((route) => match(route, { decode: decodeURIComponent }));
|
const SSR_ROUTE_MATCHERS = SSR_ROUTES.map((route) => match(route));
|
||||||
|
|
||||||
export function getPathParams(path: string): PathParams | undefined {
|
export function getPathParams(path: string): MetadataPathParams | undefined {
|
||||||
for (const matcher of SSR_ROUTE_MATCHERS) {
|
for (const matcher of SSR_ROUTE_MATCHERS) {
|
||||||
const result = matcher(path);
|
const result = matcher(path);
|
||||||
if (!result) continue;
|
if (!result) continue;
|
||||||
const params = result.params as PathParams;
|
|
||||||
if (params.nevent) {
|
const params: MetadataPathParams = result.params;
|
||||||
const decoded = nip19.decode(`nevent${params.nevent}`).data as nip19.EventPointer;
|
|
||||||
params.statusId = decoded.id;
|
if (params.bech32) {
|
||||||
} else if (params.note) {
|
try {
|
||||||
params.statusId = nip19.decode(`note${params.note}`).data as string;
|
const decoded = nip19.decode(params.bech32);
|
||||||
|
switch (decoded.type) {
|
||||||
|
case 'nevent':
|
||||||
|
params.statusId = decoded.data.id;
|
||||||
|
break;
|
||||||
|
case 'note':
|
||||||
|
params.statusId = decoded.data;
|
||||||
|
break;
|
||||||
|
case 'nprofile':
|
||||||
|
params.acct = decoded.data.pubkey;
|
||||||
|
break;
|
||||||
|
case 'npub':
|
||||||
|
params.acct = decoded.data;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.nprofile) {
|
|
||||||
const decoded = nip19.decode(`nprofile${params.nprofile}`).data as nip19.ProfilePointer;
|
|
||||||
params.acct = decoded.pubkey;
|
|
||||||
} else if (params.npub) {
|
|
||||||
params.acct = nip19.decode(`npub${params.npub}`).data as string;
|
|
||||||
}
|
|
||||||
return params;
|
return params;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchProfile(
|
|
||||||
{ pubkey, handle }: Partial<Record<'pubkey' | 'handle', string>>,
|
|
||||||
): Promise<ProfileInfo> {
|
|
||||||
if (!handle && !pubkey) {
|
|
||||||
throw new Error('Tried to fetch kind 0 with no args');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handle) pubkey = await lookupPubkey(handle);
|
|
||||||
if (!pubkey) throw new Error('NIP-05 or bech32 specified and no pubkey found');
|
|
||||||
|
|
||||||
const author = await getAuthor(pubkey);
|
|
||||||
if (!author) throw new Error(`Author not found for pubkey ${pubkey}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
meta: n.json()
|
|
||||||
.pipe(n.metadata())
|
|
||||||
.parse(author.content),
|
|
||||||
event: author,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfileInfo = { meta: NostrMetadata; event: NostrEvent };
|
|
||||||
|
|
||||||
function truncate(s: string, len: number, ellipsis = '…') {
|
|
||||||
if (s.length <= len) return s;
|
|
||||||
return s.slice(0, len) + ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param id A nip-05 identifier, bech32 encoded npub/nprofile, or a pubkey
|
|
||||||
* @param acc A ProfileInfo object, if you've already fetched it then this is used to build a handle.
|
|
||||||
* @returns The handle
|
|
||||||
*/
|
|
||||||
export async function getHandle(id: string, acc?: ProfileInfo) {
|
|
||||||
let handle: string | undefined = '';
|
|
||||||
|
|
||||||
const pubkeyToHandle = async (pubkey: string) => {
|
|
||||||
const fallback = nip19.npubEncode(pubkey).slice(0, 8);
|
|
||||||
try {
|
|
||||||
const author = acc || await fetchProfile({ pubkey });
|
|
||||||
if (author?.meta?.nip05) return parseNip05(author.meta.nip05).handle;
|
|
||||||
else if (author?.meta?.name) return author.meta.name;
|
|
||||||
} catch (e) {
|
|
||||||
console.debug('Error fetching profile/parsing nip-05 in getHandle');
|
|
||||||
console.debug(e);
|
|
||||||
}
|
|
||||||
return fallback;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (/[a-f0-9]{64}/.test(id)) {
|
|
||||||
handle = await pubkeyToHandle(id);
|
|
||||||
} else if (n.bech32().safeParse(id).success) {
|
|
||||||
if (id.startsWith('npub')) {
|
|
||||||
handle = await pubkeyToHandle(nip19.decode(id as `npub1${string}`).data);
|
|
||||||
} else if (id.startsWith('nprofile')) {
|
|
||||||
const decoded = nip19.decode(id as `nprofile1${string}`).data.pubkey;
|
|
||||||
handle = await pubkeyToHandle(decoded);
|
|
||||||
} else {
|
|
||||||
throw new Error('non-nprofile or -npub bech32 passed to getHandle()');
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const pubkey = await lookupPubkey(id);
|
|
||||||
if (!pubkey) throw new Error('Invalid user identifier');
|
|
||||||
const parsed = await parseAndVerifyNip05(id, pubkey);
|
|
||||||
handle = parsed?.handle;
|
|
||||||
}
|
|
||||||
|
|
||||||
return handle || name || 'npub1xxx';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStatusInfo(id: string): Promise<StatusInfo> {
|
|
||||||
const event = await getEvent(id);
|
|
||||||
if (!event) throw new Error('Invalid post id supplied');
|
|
||||||
let title = 'View post on Ditto';
|
|
||||||
try {
|
|
||||||
const handle = await getHandle(event.pubkey);
|
|
||||||
title = `View @${handle}'s post on Ditto`;
|
|
||||||
} catch (_) {
|
|
||||||
console.log(`Error getting status info for ${id}: proceeding silently`);
|
|
||||||
}
|
|
||||||
const res: StatusInfo = {
|
|
||||||
title,
|
|
||||||
description: nip27.replaceAll(
|
|
||||||
event.content,
|
|
||||||
({ decoded, value }) => decoded.type === 'npub' ? value.slice(0, 8) : '',
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
const data: string[][] = event.tags
|
|
||||||
.find(([name]) => name === 'imeta')?.slice(1)
|
|
||||||
.map((entry: string) => entry.split(' ')) ?? [];
|
|
||||||
|
|
||||||
const url = data.find(([name]) => name === 'url')?.[1];
|
|
||||||
const dim = data.find(([name]) => name === 'dim')?.[1];
|
|
||||||
|
|
||||||
const [w, h] = dim?.split('x').map(Number) ?? [null, null];
|
|
||||||
|
|
||||||
if (url && w && h) {
|
|
||||||
res.image = { url, w, h };
|
|
||||||
res.description = res.description.replace(url.trim(), '');
|
|
||||||
}
|
|
||||||
|
|
||||||
// needs to be done last incase the image url was surrounded by newlines
|
|
||||||
res.description = truncate(res.description.trim(), 140);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export async function getRelays(store: NStore, pubkey: string): Promise<Set<stri
|
||||||
relays.add(url.toString() as `wss://${string}`);
|
relays.add(url.toString() as `wss://${string}`);
|
||||||
}
|
}
|
||||||
} catch (_e) {
|
} catch (_e) {
|
||||||
// do nothing
|
// fall through
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,54 @@
|
||||||
import { html } from '@/utils/html.ts';
|
import { html } from '@/utils/html.ts';
|
||||||
import { OpenGraphTemplateOpts } from '@/utils/og-metadata.ts';
|
import { MetadataEntities } from '@/utils/og-metadata.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a series of meta tags from supplied metadata for injection into the served HTML page.
|
* Builds a series of meta tags from supplied metadata for injection into the served HTML page.
|
||||||
* @param opts the metadata to use to fill the template.
|
* @param opts the metadata to use to fill the template.
|
||||||
* @returns the built OpenGraph metadata.
|
* @returns the built OpenGraph metadata.
|
||||||
*/
|
*/
|
||||||
export function metadataView({ title, type, url, image, description, site }: OpenGraphTemplateOpts): string {
|
export function renderMetadata(url: string, { account, status, instance }: MetadataEntities): string {
|
||||||
const res: string[] = [
|
const tags: string[] = [];
|
||||||
html` <meta content="${title}" property="og:title">`,
|
|
||||||
html` <meta content="${type}" property="og:type">`,
|
const title = account ? `${account.display_name} (@${account.acct})` : instance.name;
|
||||||
html` <meta content="${url}" property="og:url">`,
|
const attachment = status?.media_attachments?.find((a) => a.type === 'image');
|
||||||
html` <meta content="${site}" property="og:site_name">`,
|
const description = status?.content || account?.note || instance.tagline;
|
||||||
html` <meta name="twitter:card" content="summary">`,
|
const image = attachment?.preview_url || account?.avatar_static || instance.picture;
|
||||||
html` <meta name="twitter:title" content="${title}">`,
|
const siteName = instance?.name;
|
||||||
];
|
const width = attachment?.meta?.original?.width;
|
||||||
|
const height = attachment?.meta?.original?.height;
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
tags.push(html`<title>${title}</title>`);
|
||||||
|
tags.push(html`<meta property="og:title" content="${title}">`);
|
||||||
|
tags.push(html`<meta name="twitter:title" content="${title}">`);
|
||||||
|
}
|
||||||
|
|
||||||
if (description) {
|
if (description) {
|
||||||
res.push(html`<meta content="${description}" property="og:description">`);
|
tags.push(html`<meta name="description" content="${description}">`);
|
||||||
res.push(html`<meta content="${description}" property="twitter:description">`);
|
tags.push(html`<meta property="og:description" content="${description}">`);
|
||||||
|
tags.push(html`<meta name="twitter:description" content="${description}">`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image) {
|
if (image) {
|
||||||
res.push(html`<meta content="${image.url}" property="og:image">`);
|
tags.push(html`<meta property="og:image" content="${image}">`);
|
||||||
res.push(html`<meta name="twitter:image" content="${image.url}">`);
|
tags.push(html`<meta name="twitter:image" content="${image}">`);
|
||||||
|
|
||||||
if (image.w && image.h) {
|
|
||||||
res.push(html`<meta content="${image.w}" property="og:image:width">`);
|
|
||||||
res.push(html`<meta content="${image.h}" property="og:image:height">`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image.alt) {
|
if (typeof width === 'number' && typeof height === 'number') {
|
||||||
res.push(html`<meta content="${image.alt}" property="og:image:alt">`);
|
tags.push(html`<meta property="og:image:width" content="${width}">`);
|
||||||
res.push(html`<meta content="${image.alt}" property="twitter:image:alt">`);
|
tags.push(html`<meta property="og:image:height" content="${height}">`);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.join('');
|
if (siteName) {
|
||||||
|
tags.push(html`<meta property="og:site_name" content="${siteName}">`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra tags (always present if other tags exist).
|
||||||
|
if (tags.length > 0) {
|
||||||
|
tags.push(html`<meta property="og:url" content="${url}">`);
|
||||||
|
tags.push('<meta property="og:type" content="website">');
|
||||||
|
tags.push('<meta name="twitter:card" content="summary">');
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags.join('');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue