14 changed files with 655 additions and 196 deletions
@ -0,0 +1,93 @@ |
|||||||
|
Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display" |
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1. |
||||||
|
This license is copied below, and is also available with a FAQ at: |
||||||
|
http://scripts.sil.org/OFL |
||||||
|
|
||||||
|
|
||||||
|
----------------------------------------------------------- |
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 |
||||||
|
----------------------------------------------------------- |
||||||
|
|
||||||
|
PREAMBLE |
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide |
||||||
|
development of collaborative font projects, to support the font creation |
||||||
|
efforts of academic and linguistic communities, and to provide a free and |
||||||
|
open framework in which fonts may be shared and improved in partnership |
||||||
|
with others. |
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and |
||||||
|
redistributed freely as long as they are not sold by themselves. The |
||||||
|
fonts, including any derivative works, can be bundled, embedded, |
||||||
|
redistributed and/or sold with any software provided that any reserved |
||||||
|
names are not used by derivative works. The fonts and derivatives, |
||||||
|
however, cannot be released under any other type of license. The |
||||||
|
requirement for fonts to remain under this license does not apply |
||||||
|
to any document created using the fonts or their derivatives. |
||||||
|
|
||||||
|
DEFINITIONS |
||||||
|
"Font Software" refers to the set of files released by the Copyright |
||||||
|
Holder(s) under this license and clearly marked as such. This may |
||||||
|
include source files, build scripts and documentation. |
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the |
||||||
|
copyright statement(s). |
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as |
||||||
|
distributed by the Copyright Holder(s). |
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting, |
||||||
|
or substituting -- in part or in whole -- any of the components of the |
||||||
|
Original Version, by changing formats or by porting the Font Software to a |
||||||
|
new environment. |
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical |
||||||
|
writer or other person who contributed to the Font Software. |
||||||
|
|
||||||
|
PERMISSION & CONDITIONS |
||||||
|
Permission is hereby granted, free of charge, to any person obtaining |
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify, |
||||||
|
redistribute, and sell modified and unmodified copies of the Font |
||||||
|
Software, subject to the following conditions: |
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components, |
||||||
|
in Original or Modified Versions, may be sold by itself. |
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled, |
||||||
|
redistributed and/or sold with any software, provided that each copy |
||||||
|
contains the above copyright notice and this license. These can be |
||||||
|
included either as stand-alone text files, human-readable headers or |
||||||
|
in the appropriate machine-readable metadata fields within text or |
||||||
|
binary files as long as those fields can be easily viewed by the user. |
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font |
||||||
|
Name(s) unless explicit written permission is granted by the corresponding |
||||||
|
Copyright Holder. This restriction only applies to the primary font name as |
||||||
|
presented to the users. |
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font |
||||||
|
Software shall not be used to promote, endorse or advertise any |
||||||
|
Modified Version, except to acknowledge the contribution(s) of the |
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written |
||||||
|
permission. |
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole, |
||||||
|
must be distributed entirely under this license, and must not be |
||||||
|
distributed under any other license. The requirement for fonts to |
||||||
|
remain under this license does not apply to any document created |
||||||
|
using the Font Software. |
||||||
|
|
||||||
|
TERMINATION |
||||||
|
This license becomes null and void if any of the above conditions are |
||||||
|
not met. |
||||||
|
|
||||||
|
DISCLAIMER |
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF |
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT |
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE |
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL |
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM |
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE. |
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 794 KiB After Width: | Height: | Size: 82 KiB |
@ -0,0 +1,68 @@ |
|||||||
|
/** |
||||||
|
* Rasterize public/og-image.svg → public/og-image.png with a true Playfair Display wordmark. |
||||||
|
* ImageMagick/Inkscape copy the SVG to /tmp, so @font-face + file URLs often never load; |
||||||
|
* we outline "Imwald" with opentype.js so the PNG is font-independent. |
||||||
|
* |
||||||
|
* Wordmark fill + weight are read from the `#og-imwald` <text> in the SVG so PNG matches |
||||||
|
* the green-tinged off-white you see in the browser (rasterizers often look harsher than live text). |
||||||
|
*/ |
||||||
|
import { execFileSync } from 'node:child_process' |
||||||
|
import { readFileSync, writeFileSync, unlinkSync } from 'node:fs' |
||||||
|
import { dirname, join } from 'node:path' |
||||||
|
import { fileURLToPath } from 'node:url' |
||||||
|
import opentype from 'opentype.js' |
||||||
|
|
||||||
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..') |
||||||
|
const svgPath = join(root, 'public/og-image.svg') |
||||||
|
const fontPath = join(root, 'public/fonts/PlayfairDisplay-wght.ttf') |
||||||
|
const outPng = join(root, 'public/og-image.png') |
||||||
|
const tmpSvg = join(root, 'public/.og-image.raster.svg') |
||||||
|
|
||||||
|
function parseOgImwaldFromSvg(svg) { |
||||||
|
const defaults = { fill: '#d4ebe0', weight: 700, letterSpacing: 0.018 } |
||||||
|
const idPos = svg.indexOf('id="og-imwald"') |
||||||
|
if (idPos < 0) return defaults |
||||||
|
const textOpen = svg.lastIndexOf('<text', idPos) |
||||||
|
const textClose = svg.indexOf('</text>', idPos) |
||||||
|
if (textOpen < 0 || textClose < 0) return defaults |
||||||
|
const t = svg.slice(textOpen, textClose + '</text>'.length) |
||||||
|
const fill = t.match(/\bfill="([^"]+)"/)?.[1] ?? defaults.fill |
||||||
|
const weight = parseInt(t.match(/font-weight="(\d+)"/)?.[1] ?? String(defaults.weight), 10) |
||||||
|
return { |
||||||
|
fill, |
||||||
|
weight: Number.isFinite(weight) ? weight : defaults.weight, |
||||||
|
letterSpacing: defaults.letterSpacing |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
let svg = readFileSync(svgPath, 'utf8') |
||||||
|
const { fill: imwaldFill, weight: imwaldWght, letterSpacing } = parseOgImwaldFromSvg(svg) |
||||||
|
|
||||||
|
const font = opentype.loadSync(fontPath) |
||||||
|
const pathObj = font.getPath('Imwald', 72, 300, 108, { |
||||||
|
variation: { wght: imwaldWght }, |
||||||
|
letterSpacing |
||||||
|
}) |
||||||
|
let pathTag = pathObj.toSVG(2) |
||||||
|
if (!pathTag.includes('fill=')) { |
||||||
|
pathTag = pathTag.replace('<path ', `<path fill="${imwaldFill}" `) |
||||||
|
} |
||||||
|
|
||||||
|
svg = svg.replace(/<text[^>]*id="og-imwald"[^>]*>[\s\S]*?<\/text>/, pathTag) |
||||||
|
|
||||||
|
writeFileSync(tmpSvg, svg, 'utf8') |
||||||
|
|
||||||
|
try { |
||||||
|
execFileSync('convert', ['-background', 'none', '-density', '150', tmpSvg, outPng], { |
||||||
|
stdio: 'inherit', |
||||||
|
cwd: root |
||||||
|
}) |
||||||
|
} finally { |
||||||
|
try { |
||||||
|
unlinkSync(tmpSvg) |
||||||
|
} catch { |
||||||
|
/* ignore */ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
console.info('[og:image] wrote', outPng) |
||||||
@ -0,0 +1,271 @@ |
|||||||
|
/** Shared Open Graph / Twitter / document title helpers (client-side). */ |
||||||
|
|
||||||
|
export const SITE_NAME = 'Imwald' |
||||||
|
|
||||||
|
export const SITE_TAGLINE = |
||||||
|
'A user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery.' |
||||||
|
|
||||||
|
export function getSiteOrigin(): string { |
||||||
|
if (typeof window === 'undefined') return 'https://jumble.imwald.eu' |
||||||
|
return window.location.origin |
||||||
|
} |
||||||
|
|
||||||
|
export function defaultOgImageAbsoluteUrl(): string { |
||||||
|
return `${getSiteOrigin()}/og-image.png` |
||||||
|
} |
||||||
|
|
||||||
|
export function avatarProxyUrl(pubkey: string): string { |
||||||
|
return `${getSiteOrigin()}/api/avatar/${pubkey}` |
||||||
|
} |
||||||
|
|
||||||
|
export function updateMetaTag(property: string, content: string): void { |
||||||
|
if (typeof document === 'undefined') return |
||||||
|
const prop = |
||||||
|
property.startsWith('og:') || property.startsWith('article:') || property.startsWith('profile:') |
||||||
|
? property |
||||||
|
: property.replace(/^property="|"$/, '') |
||||||
|
|
||||||
|
const isTwitterTag = prop.startsWith('twitter:') |
||||||
|
const selector = isTwitterTag ? `meta[name="${prop}"]` : `meta[property="${prop}"]` |
||||||
|
|
||||||
|
let meta = document.querySelector(selector) |
||||||
|
if (!meta) { |
||||||
|
meta = document.createElement('meta') |
||||||
|
if (isTwitterTag) { |
||||||
|
meta.setAttribute('name', prop) |
||||||
|
} else { |
||||||
|
meta.setAttribute('property', prop) |
||||||
|
} |
||||||
|
document.head.appendChild(meta) |
||||||
|
} |
||||||
|
meta.setAttribute('content', content) |
||||||
|
} |
||||||
|
|
||||||
|
export function removeMetaByProperty(property: string): void { |
||||||
|
if (typeof document === 'undefined') return |
||||||
|
document.querySelectorAll(`meta[property="${property}"]`).forEach((m) => m.remove()) |
||||||
|
} |
||||||
|
|
||||||
|
export function applyDefaultSiteSocialMeta(): void { |
||||||
|
if (typeof window === 'undefined') return |
||||||
|
const href = window.location.href |
||||||
|
const truncatedUrl = href.length > 150 ? href.substring(0, 147) + '...' : href |
||||||
|
const desc = `${truncatedUrl} — ${SITE_TAGLINE}` |
||||||
|
const img = defaultOgImageAbsoluteUrl() |
||||||
|
|
||||||
|
updateMetaTag('og:title', SITE_NAME) |
||||||
|
updateMetaTag('og:description', desc) |
||||||
|
updateMetaTag('og:image', img) |
||||||
|
updateMetaTag('og:type', 'website') |
||||||
|
updateMetaTag('og:url', href) |
||||||
|
updateMetaTag('og:site_name', SITE_NAME) |
||||||
|
|
||||||
|
updateMetaTag('twitter:card', 'summary_large_image') |
||||||
|
updateMetaTag('twitter:title', SITE_NAME) |
||||||
|
updateMetaTag('twitter:description', desc) |
||||||
|
updateMetaTag('twitter:image', img) |
||||||
|
} |
||||||
|
|
||||||
|
const PRIMARY_PAGE_LABEL: Record<string, string> = { |
||||||
|
explore: 'Explore', |
||||||
|
feed: 'Feed', |
||||||
|
me: 'Me', |
||||||
|
profile: 'Profile', |
||||||
|
relay: 'Relay', |
||||||
|
search: 'Search', |
||||||
|
'follows-latest': 'Latest follows', |
||||||
|
rss: 'RSS', |
||||||
|
settings: 'Settings', |
||||||
|
spells: 'Spells' |
||||||
|
} |
||||||
|
|
||||||
|
function relayHostnameFromPath(pathname: string): string | null { |
||||||
|
const m = |
||||||
|
pathname.match(/\/(?:home|explore)\/relays\/(.+)$/i) || pathname.match(/^\/relays\/(.+)$/i) |
||||||
|
if (!m?.[1]) return null |
||||||
|
try { |
||||||
|
const decoded = decodeURIComponent(m[1].split('/')[0]) |
||||||
|
const asHttp = decoded.startsWith('wss://') |
||||||
|
? 'https://' + decoded.slice(6) |
||||||
|
: decoded.startsWith('ws://') |
||||||
|
? 'http://' + decoded.slice(5) |
||||||
|
: decoded |
||||||
|
const u = new URL(asHttp.includes('://') ? asHttp : `https://${asHttp}`) |
||||||
|
return u.hostname || decoded |
||||||
|
} catch { |
||||||
|
return m[1].slice(0, 80) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export type TRouteSocialCopy = { pageTitle: string; ogTitle: string; description: string } |
||||||
|
|
||||||
|
/** Note detail URLs set OG tags in NotePage. */ |
||||||
|
export function isNoteDetailPathname(pathname: string): boolean { |
||||||
|
const path = pathname.split('?')[0].split('#')[0] |
||||||
|
return ( |
||||||
|
/\/notes\/[^/?#]+/.test(path) || |
||||||
|
/\/(?:discussions|search|profile|home|feed|spells|explore|rss|follows-latest)\/notes\/[^/?#]+/.test( |
||||||
|
path |
||||||
|
) |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
/** Profile detail (/users/:id) sets OG in ProfilePage. */ |
||||||
|
export function isProfileDetailPathname(pathname: string): boolean { |
||||||
|
const path = pathname.split('?')[0].split('#')[0].replace(/\/$/, '') || '/' |
||||||
|
return /^\/users\/[^/]+$/.test(path) |
||||||
|
} |
||||||
|
|
||||||
|
/** Labels for static Imwald URLs (in-app link previews + route-level OG when no note/profile). */ |
||||||
|
export function resolveImwaldRouteSocialCopy( |
||||||
|
pathname: string, |
||||||
|
currentPrimaryPage: string |
||||||
|
): TRouteSocialCopy { |
||||||
|
const path = pathname.split('?')[0].split('#')[0].replace(/\/$/, '') || '/' |
||||||
|
const href = typeof window !== 'undefined' ? window.location.href : '' |
||||||
|
const relayHost = relayHostnameFromPath(path) |
||||||
|
|
||||||
|
let pageTitle = SITE_NAME |
||||||
|
let ogTitle = SITE_NAME |
||||||
|
let description = href ? `${SITE_TAGLINE} ${href}` : SITE_TAGLINE |
||||||
|
|
||||||
|
if (path.startsWith('/settings')) { |
||||||
|
if (path.includes('/general')) { |
||||||
|
pageTitle = `General · ${SITE_NAME}` |
||||||
|
ogTitle = `General settings · ${SITE_NAME}` |
||||||
|
} else if (path.includes('/relays')) { |
||||||
|
pageTitle = `Relays · ${SITE_NAME}` |
||||||
|
ogTitle = `Relay & storage settings · ${SITE_NAME}` |
||||||
|
} else if (path.includes('/cache')) { |
||||||
|
pageTitle = `Cache · ${SITE_NAME}` |
||||||
|
ogTitle = `Cache & offline storage · ${SITE_NAME}` |
||||||
|
} else if (path.includes('/wallet')) { |
||||||
|
pageTitle = `Wallet · ${SITE_NAME}` |
||||||
|
ogTitle = `Wallet settings · ${SITE_NAME}` |
||||||
|
} else if (path.includes('/posts')) { |
||||||
|
pageTitle = `Posts · ${SITE_NAME}` |
||||||
|
ogTitle = `Post settings · ${SITE_NAME}` |
||||||
|
} else if (path.includes('/translation')) { |
||||||
|
pageTitle = `Translation · ${SITE_NAME}` |
||||||
|
ogTitle = `Translation settings · ${SITE_NAME}` |
||||||
|
} else if (path.includes('/rss-feeds')) { |
||||||
|
pageTitle = `RSS feeds · ${SITE_NAME}` |
||||||
|
ogTitle = `RSS feed settings · ${SITE_NAME}` |
||||||
|
} else if (path.includes('/follow-sets')) { |
||||||
|
pageTitle = `Follow sets · ${SITE_NAME}` |
||||||
|
ogTitle = `Follow sets · ${SITE_NAME}` |
||||||
|
} else if (path.includes('/personal-lists')) { |
||||||
|
pageTitle = `Lists · ${SITE_NAME}` |
||||||
|
ogTitle = `Personal lists · ${SITE_NAME}` |
||||||
|
} else { |
||||||
|
pageTitle = `Settings · ${SITE_NAME}` |
||||||
|
ogTitle = `Settings · ${SITE_NAME}` |
||||||
|
} |
||||||
|
description = `${ogTitle}. ${SITE_TAGLINE}` |
||||||
|
} else if (path === '/search' || path.startsWith('/search/')) { |
||||||
|
pageTitle = `Search · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `Search notes and people on Nostr with ${SITE_NAME}.` |
||||||
|
} else if (relayHost) { |
||||||
|
const host = relayHost |
||||||
|
pageTitle = `${host} · ${SITE_NAME}` |
||||||
|
ogTitle = `Relay ${host} · ${SITE_NAME}` |
||||||
|
description = `Relay ${host} on ${SITE_NAME}. ${SITE_TAGLINE}` |
||||||
|
} else if (path.startsWith('/rss-item') || path.includes('/rss-item/')) { |
||||||
|
pageTitle = `Article · ${SITE_NAME}` |
||||||
|
ogTitle = `RSS article · ${SITE_NAME}` |
||||||
|
description = `Read an RSS-sourced article in ${SITE_NAME}.` |
||||||
|
} else if (path === '/users') { |
||||||
|
pageTitle = `People · ${SITE_NAME}` |
||||||
|
ogTitle = `People on ${SITE_NAME}` |
||||||
|
description = `Browse Nostr profiles in ${SITE_NAME}.` |
||||||
|
} else if (path === '/bookmarks') { |
||||||
|
pageTitle = `Bookmarks · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `Your bookmarked notes on ${SITE_NAME}.` |
||||||
|
} else if (path === '/mutes') { |
||||||
|
pageTitle = `Muted users · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `Muted users in ${SITE_NAME}.` |
||||||
|
} else if (path === '/pins') { |
||||||
|
pageTitle = `Pinned notes · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `Pinned notes in ${SITE_NAME}.` |
||||||
|
} else if (path === '/interests') { |
||||||
|
pageTitle = `Interests · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `Interest lists in ${SITE_NAME}.` |
||||||
|
} else if (path === '/profile-editor') { |
||||||
|
pageTitle = `Edit profile · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `Edit your Nostr profile in ${SITE_NAME}.` |
||||||
|
} else if (path === '/follow-packs') { |
||||||
|
pageTitle = `Follow packs · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `Follow packs on ${SITE_NAME}.` |
||||||
|
} else if (path === '/notes') { |
||||||
|
pageTitle = `Notes · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `Notes in ${SITE_NAME}.` |
||||||
|
} else if (path.match(/^\/users\/[^/]+\/following$/)) { |
||||||
|
pageTitle = `Following · ${SITE_NAME}` |
||||||
|
ogTitle = `Following list · ${SITE_NAME}` |
||||||
|
description = `Following list on ${SITE_NAME}.` |
||||||
|
} else if (path.match(/^\/users\/[^/]+\/relays$/)) { |
||||||
|
pageTitle = `Relays · ${SITE_NAME}` |
||||||
|
ogTitle = `User relays · ${SITE_NAME}` |
||||||
|
description = `Relay list on ${SITE_NAME}.` |
||||||
|
} else if (path === '/' || path === '/home') { |
||||||
|
pageTitle = `Home · ${SITE_NAME}` |
||||||
|
ogTitle = `Home · ${SITE_NAME}` |
||||||
|
description = `${SITE_TAGLINE} ${href}` |
||||||
|
} else { |
||||||
|
const seg = path.split('/').filter(Boolean)[0] |
||||||
|
if (seg && PRIMARY_PAGE_LABEL[seg]) { |
||||||
|
const label = PRIMARY_PAGE_LABEL[seg] |
||||||
|
pageTitle = `${label} · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `${label} in ${SITE_NAME}. ${SITE_TAGLINE}` |
||||||
|
} else if (currentPrimaryPage && PRIMARY_PAGE_LABEL[currentPrimaryPage]) { |
||||||
|
const label = PRIMARY_PAGE_LABEL[currentPrimaryPage] |
||||||
|
pageTitle = `${label} · ${SITE_NAME}` |
||||||
|
ogTitle = pageTitle |
||||||
|
description = `${label} in ${SITE_NAME}. ${SITE_TAGLINE}` |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return { pageTitle, ogTitle, description } |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Browser tab + social tags for routes that do not set their own (e.g. settings, lists). |
||||||
|
* Note and profile detail pages set richer tags in their components. |
||||||
|
*/ |
||||||
|
export function applyRouteDocumentMeta(pathname: string, currentPrimaryPage: string): void { |
||||||
|
if (typeof window === 'undefined') return |
||||||
|
|
||||||
|
const { pageTitle, ogTitle, description } = resolveImwaldRouteSocialCopy(pathname, currentPrimaryPage) |
||||||
|
const href = window.location.href |
||||||
|
const img = defaultOgImageAbsoluteUrl() |
||||||
|
|
||||||
|
document.title = pageTitle |
||||||
|
|
||||||
|
updateMetaTag('og:title', ogTitle) |
||||||
|
updateMetaTag('og:description', description.length > 300 ? description.slice(0, 297) + '...' : description) |
||||||
|
updateMetaTag('og:image', img) |
||||||
|
updateMetaTag('og:type', 'website') |
||||||
|
updateMetaTag('og:url', href) |
||||||
|
updateMetaTag('og:site_name', SITE_NAME) |
||||||
|
|
||||||
|
updateMetaTag('twitter:card', 'summary_large_image') |
||||||
|
updateMetaTag('twitter:title', ogTitle) |
||||||
|
updateMetaTag( |
||||||
|
'twitter:description', |
||||||
|
description.length > 200 ? description.slice(0, 197) + '...' : description |
||||||
|
) |
||||||
|
updateMetaTag('twitter:image', img) |
||||||
|
|
||||||
|
removeMetaByProperty('article:tag') |
||||||
|
removeMetaByProperty('article:author') |
||||||
|
document.querySelector('meta[property="article:author:url"]')?.remove() |
||||||
|
} |
||||||
Loading…
Reference in new issue