Tighten social metadata generation
This commit is contained in:
parent
bbdc45eca3
commit
bc3cf9691f
5 changed files with 57 additions and 8 deletions
|
|
@ -5,6 +5,7 @@ import { googleFontHref, googleFontSubsetHref } from "../util/theme"
|
|||
import { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||
import { unescapeHTML } from "../util/escape"
|
||||
import { CustomOgImagesEmitterName } from "../plugins/emitters/ogImage"
|
||||
import { canonicalSocialUrl, normalizeSocialDescription } from "../util/social"
|
||||
export default (() => {
|
||||
const Head: QuartzComponent = ({
|
||||
cfg,
|
||||
|
|
@ -15,10 +16,11 @@ export default (() => {
|
|||
const titleSuffix = cfg.pageTitleSuffix ?? ""
|
||||
const title =
|
||||
(fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
|
||||
const description =
|
||||
const description = normalizeSocialDescription(
|
||||
fileData.frontmatter?.socialDescription ??
|
||||
fileData.frontmatter?.description ??
|
||||
unescapeHTML(fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description)
|
||||
fileData.frontmatter?.description ??
|
||||
unescapeHTML(fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description),
|
||||
)
|
||||
|
||||
const { css, js, additionalHead } = externalResources
|
||||
|
||||
|
|
@ -29,7 +31,7 @@ export default (() => {
|
|||
|
||||
// Url of current page
|
||||
const socialUrl =
|
||||
fileData.slug === "404" ? url.toString() : joinSegments(url.toString(), fileData.slug!)
|
||||
fileData.slug === "404" ? url.toString() : canonicalSocialUrl(cfg.baseUrl!, fileData.slug!)
|
||||
|
||||
const usesCustomOgImage = ctx.cfg.plugins.emitters.some(
|
||||
(e) => e.name === CustomOgImagesEmitterName,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { BuildCtx } from "../../util/ctx"
|
|||
import { QuartzPluginData } from "../vfile"
|
||||
import fs from "node:fs/promises"
|
||||
import { styleText } from "util"
|
||||
import { normalizeSocialDescription } from "../../util/social"
|
||||
|
||||
const defaultOptions: SocialImageOptions = {
|
||||
colorScheme: "lightMode",
|
||||
|
|
@ -76,10 +77,11 @@ async function processOgImage(
|
|||
const titleSuffix = cfg.pageTitleSuffix ?? ""
|
||||
const title =
|
||||
(fileData.frontmatter?.title ?? i18n(cfg.locale).propertyDefaults.title) + titleSuffix
|
||||
const description =
|
||||
const description = normalizeSocialDescription(
|
||||
fileData.frontmatter?.socialDescription ??
|
||||
fileData.frontmatter?.description ??
|
||||
unescapeHTML(fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description)
|
||||
fileData.frontmatter?.description ??
|
||||
unescapeHTML(fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description),
|
||||
)
|
||||
|
||||
const stream = await generateSocialImage(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -53,6 +53,17 @@ function isStandalonePreviewTagline(node: RootContent): boolean {
|
|||
return isElementNode(onlyChild) && ["em", "strong"].includes(onlyChild.tagName)
|
||||
}
|
||||
|
||||
function isStructuredMetadataParagraph(node: RootContent): boolean {
|
||||
if (!isElementNode(node) || node.tagName !== "p") {
|
||||
return false
|
||||
}
|
||||
|
||||
const text = normalizeWhitespace(toString(node)).toLowerCase()
|
||||
const labels = ["date:", "location:", "primary actors:", "authors:"]
|
||||
const labelMatches = labels.filter((label) => text.includes(label)).length
|
||||
return labelMatches >= 2
|
||||
}
|
||||
|
||||
export function extractDescriptionText(tree: HTMLRoot, pageTitle?: string): string {
|
||||
const children = [...tree.children]
|
||||
let startIdx = 0
|
||||
|
|
@ -77,6 +88,14 @@ export function extractDescriptionText(tree: HTMLRoot, pageTitle?: string): stri
|
|||
startIdx++
|
||||
}
|
||||
|
||||
if (startIdx < children.length && isStructuredMetadataParagraph(children[startIdx])) {
|
||||
startIdx++
|
||||
}
|
||||
|
||||
while (startIdx < children.length && !isMeaningfulNode(children[startIdx])) {
|
||||
startIdx++
|
||||
}
|
||||
|
||||
return escapeHTML(toString({ ...tree, children: children.slice(startIdx) }))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,11 @@ function getFirstMarkdownHeading(root: MdRoot): string | undefined {
|
|||
return toString(node).trim().length > 0
|
||||
})
|
||||
|
||||
if (!firstMeaningfulNode || firstMeaningfulNode.type !== "heading") {
|
||||
if (
|
||||
!firstMeaningfulNode ||
|
||||
firstMeaningfulNode.type !== "heading" ||
|
||||
firstMeaningfulNode.depth !== 1
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
|
|
|
|||
22
quartz/util/social.ts
Normal file
22
quartz/util/social.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { FullSlug, joinSegments, simplifySlug } from "./path"
|
||||
|
||||
export function canonicalSocialUrl(baseUrl: string, slug?: FullSlug): string {
|
||||
const rootUrl = `https://${baseUrl}`
|
||||
return slug ? joinSegments(rootUrl, simplifySlug(slug)) : rootUrl
|
||||
}
|
||||
|
||||
export function normalizeSocialDescription(description: string, maxLength = 220): string {
|
||||
const normalized = description.replace(/\s+/g, " ").trim()
|
||||
if (normalized.length <= maxLength) {
|
||||
return normalized
|
||||
}
|
||||
|
||||
const softCut = normalized.slice(0, maxLength + 1)
|
||||
const boundary = softCut.lastIndexOf(" ")
|
||||
const truncated =
|
||||
boundary > Math.floor(maxLength * 0.6)
|
||||
? softCut.slice(0, boundary)
|
||||
: normalized.slice(0, maxLength)
|
||||
|
||||
return truncated.replace(/[.,;:\s]+$/g, "") + "..."
|
||||
}
|
||||
Loading…
Reference in a new issue