mirror of
https://github.com/diced/zipline.git
synced 2026-06-12 19:01:18 -07:00
feat(view): media-only OpenGraph previews when embeds disabled (#1090)
Add embedMediaOnly view setting: emit og:image/og:video (and related tags) without custom title, description, or site name for Discord-style unfurls. Canonical og:url uses the view page URL. API and dashboard settings updated. Co-authored-by: dicedtomato <git@diced.sh>
This commit is contained in:
@@ -168,9 +168,13 @@ export async function render(
|
||||
const safeOriginalName = stripHtml(file.originalName || '');
|
||||
const safeType = stripHtml(file.type || '');
|
||||
|
||||
const meta = `
|
||||
${
|
||||
user?.view?.embedTitle && user.view.embed
|
||||
const viewEnabled = !!user.view?.enabled;
|
||||
const showRichOg = viewEnabled && !!user.view.embed;
|
||||
const showMediaOg = viewEnabled && (!!user.view.embed || !!user.view.embedMediaOnly);
|
||||
const pageUrl = `${host}${url.split('?')[0]}`;
|
||||
|
||||
const richMeta = [
|
||||
showRichOg && user?.view?.embedTitle
|
||||
? `<meta property="og:title" content="${stripHtml(
|
||||
parseString(user.view.embedTitle, {
|
||||
file: file as unknown as File,
|
||||
@@ -178,10 +182,8 @@ export async function render(
|
||||
...metrics,
|
||||
}) ?? '',
|
||||
)}" />`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
user?.view?.embedDescription && user.view.embed
|
||||
: '',
|
||||
showRichOg && user?.view?.embedDescription
|
||||
? `<meta property="og:description" content="${stripHtml(
|
||||
parseString(user.view.embedDescription, {
|
||||
file: file as unknown as File,
|
||||
@@ -189,10 +191,8 @@ export async function render(
|
||||
...metrics,
|
||||
}) ?? '',
|
||||
)}" />`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
user?.view?.embedSiteName && user.view.embed
|
||||
: '',
|
||||
showRichOg && user?.view?.embedSiteName
|
||||
? `<meta property="og:site_name" content="${stripHtml(
|
||||
parseString(user.view.embedSiteName, {
|
||||
file: file as unknown as File,
|
||||
@@ -200,10 +200,8 @@ export async function render(
|
||||
...metrics,
|
||||
}) ?? '',
|
||||
)}" />`
|
||||
: ''
|
||||
}
|
||||
${
|
||||
user?.view?.embedColor && user.view.embed
|
||||
: '',
|
||||
showRichOg && user?.view?.embedColor
|
||||
? `<meta property="theme-color" content="${stripHtml(
|
||||
parseString(user.view.embedColor, {
|
||||
file: file as unknown as File,
|
||||
@@ -211,67 +209,70 @@ export async function render(
|
||||
...metrics,
|
||||
}) ?? '',
|
||||
)}" />`
|
||||
: ''
|
||||
}
|
||||
: '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n ');
|
||||
|
||||
${
|
||||
file.type?.startsWith('image')
|
||||
const imageOg =
|
||||
showMediaOg && file.type?.startsWith('image')
|
||||
? `
|
||||
<meta property="og:type" content="image" />
|
||||
<meta property="og:image" itemProp="image" content="${host}/raw/${safeFilename}" />
|
||||
<meta property="og:url" content="${host}/raw/${safeFilename}" />
|
||||
<meta property="og:url" content="${pageUrl}" />
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:image" content="${host}/raw/${safeFilename}" />
|
||||
<meta property="twitter:title" content="${safeFilename}" />
|
||||
${showRichOg ? `<meta property="twitter:title" content="${safeFilename}" />` : ''}
|
||||
`
|
||||
: ''
|
||||
}
|
||||
: '';
|
||||
|
||||
${
|
||||
file.type?.startsWith('video')
|
||||
const videoOg =
|
||||
showMediaOg && file.type?.startsWith('video')
|
||||
? `
|
||||
${file.thumbnail ? `<meta property="og:image" content="${host}/raw/${file.thumbnail.path}" />` : ''}
|
||||
<meta property="og:type" content="video.other" />
|
||||
<meta property="og:url" content="${pageUrl}" />
|
||||
<meta property="og:video:url" content="${host}/raw/${safeFilename}" />
|
||||
<meta property="og:video:width" content="1920" />
|
||||
<meta property="og:video:height" content="1080" />
|
||||
`
|
||||
: ''
|
||||
}
|
||||
: '';
|
||||
|
||||
${
|
||||
file.type?.startsWith('audio')
|
||||
const audioOg =
|
||||
showMediaOg && file.type?.startsWith('audio')
|
||||
? `
|
||||
<meta name="twitter:card" content="player" />
|
||||
<meta name="twitter:player" content="${host}/raw/${safeFilename}" />
|
||||
<meta name="twitter:player:stream" content="${host}/raw/${safeFilename}" />
|
||||
<meta name="twitter:player:stream:content_type" content="${safeType}" />
|
||||
<meta name="twitter:title" content="${safeFilename}" />
|
||||
${showRichOg ? `<meta name="twitter:title" content="${safeFilename}" />` : ''}
|
||||
<meta name="twitter:player:width" content="720" />
|
||||
<meta name="twitter:player:height" content="480" />
|
||||
|
||||
<meta property="og:type" content="music.song" />
|
||||
<meta property="og:url" content="${host}/raw/${safeFilename}" />
|
||||
<meta property="og:url" content="${pageUrl}" />
|
||||
<meta property="og:audio" content="${host}/raw/${safeFilename}" />
|
||||
<meta property="og:audio:secure_url" content="${host}/raw/${safeFilename}" />
|
||||
<meta property="og:audio:type" content="${safeType}" />
|
||||
`
|
||||
: ''
|
||||
}
|
||||
: '';
|
||||
|
||||
${
|
||||
!file.type?.startsWith('video') && !file.type?.startsWith('image')
|
||||
const otherOg =
|
||||
showRichOg && !file.type?.startsWith('video') && !file.type?.startsWith('image')
|
||||
? `
|
||||
<meta property="og:url" content="${host}/raw/${safeFilename}" />
|
||||
<meta property="og:url" content="${pageUrl}" />
|
||||
`
|
||||
: ''
|
||||
}
|
||||
: '';
|
||||
|
||||
<title>${file.originalName ? safeOriginalName : safeFilename}</title>
|
||||
`;
|
||||
const docTitle = `<title>${file.originalName ? safeOriginalName : safeFilename}</title>`;
|
||||
|
||||
const includeHead = showRichOg || showMediaOg;
|
||||
const headMeta = includeHead
|
||||
? [richMeta, imageOg, videoOg, audioOg, otherOg, docTitle].filter(Boolean).join('\n')
|
||||
: '';
|
||||
|
||||
return {
|
||||
html,
|
||||
meta: `${user.view.embed ? meta : ''}\n${createZiplineSsr(data)}`,
|
||||
meta: `${headMeta ? `${headMeta}\n` : ''}${createZiplineSsr(data)}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ function Form({ user, setUser }: { user: User; setUser: (u: User) => void }) {
|
||||
enabled: user.view.enabled || false,
|
||||
content: user.view.content || '',
|
||||
embed: user.view.embed || false,
|
||||
embedMediaOnly: user.view.embedMediaOnly || false,
|
||||
embedTitle: user.view.embedTitle || '',
|
||||
embedDescription: user.view.embedDescription || '',
|
||||
embedSiteName: user.view.embedSiteName || '',
|
||||
@@ -75,6 +76,7 @@ function Form({ user, setUser }: { user: User; setUser: (u: User) => void }) {
|
||||
const valuesTrimmed = {
|
||||
enabled: values.enabled,
|
||||
embed: values.embed,
|
||||
embedMediaOnly: values.embed ? false : values.embedMediaOnly,
|
||||
content: values.content.trim() || null,
|
||||
embedTitle: values.embedTitle.trim() || null,
|
||||
embedDescription: values.embedDescription.trim() || null,
|
||||
@@ -186,6 +188,20 @@ function Form({ user, setUser }: { user: User; setUser: (u: User) => void }) {
|
||||
disabled={!form.values.enabled}
|
||||
my='xs'
|
||||
{...form.getInputProps('embed', { type: 'checkbox' })}
|
||||
onChange={(event) => {
|
||||
form.getInputProps('embed', { type: 'checkbox' }).onChange(event);
|
||||
if (event.currentTarget.checked) {
|
||||
form.setFieldValue('embedMediaOnly', false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<Switch
|
||||
label='Media-only link preview'
|
||||
description='When embeds are off, still add OpenGraph image/video tags so Discord and similar apps unfurl the media only (no custom title, description, or site name). The URL you paste stays in the message as plain text.'
|
||||
disabled={!form.values.enabled || form.values.embed}
|
||||
my='xs'
|
||||
{...form.getInputProps('embedMediaOnly', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing='sm'>
|
||||
|
||||
@@ -23,6 +23,7 @@ export const userViewSchema = z
|
||||
showFolder: z.boolean().nullish(),
|
||||
content: z.string().nullish(),
|
||||
embed: z.boolean().nullish(),
|
||||
embedMediaOnly: z.boolean().nullish(),
|
||||
embedTitle: z.string().nullish(),
|
||||
embedDescription: z.string().nullish(),
|
||||
embedColor: z.string().nullish(),
|
||||
|
||||
@@ -52,6 +52,7 @@ export default typedPlugin(
|
||||
.object({
|
||||
content: z.string().nullish(),
|
||||
embed: z.boolean().optional(),
|
||||
embedMediaOnly: z.boolean().optional(),
|
||||
embedTitle: z.string().nullish(),
|
||||
embedDescription: z.string().nullish(),
|
||||
embedColor: z.string().nullish(),
|
||||
@@ -101,6 +102,14 @@ export default typedPlugin(
|
||||
...(req.body.view.enabled !== undefined && { enabled: req.body.view.enabled || false }),
|
||||
...(req.body.view.content !== undefined && { content: req.body.view.content || null }),
|
||||
...(req.body.view.embed !== undefined && { embed: req.body.view.embed || false }),
|
||||
...(req.body.view.embedMediaOnly !== undefined && {
|
||||
embedMediaOnly: (() => {
|
||||
const embedOn = !!(req.body.view.embed !== undefined
|
||||
? req.body.view.embed
|
||||
: (req.user.view as { embed?: boolean }).embed);
|
||||
return embedOn ? false : req.body.view.embedMediaOnly || false;
|
||||
})(),
|
||||
}),
|
||||
...(req.body.view.embedTitle !== undefined && {
|
||||
embedTitle: req.body.view.embedTitle || null,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user