fix: og image generation
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
import { BOOK_NAMES, CC_LILAS } from "@/lib/constants";
|
||||
import { decodeTextFragment } from "@/lib/text-highlight";
|
||||
import { decodeTextFragment, extractMatchedText } from "@/lib/text-highlight";
|
||||
|
||||
const SITE_URL =
|
||||
process.env.NEXT_PUBLIC_SITE_URL || "https://prabhupadasays.org";
|
||||
@@ -305,13 +305,18 @@ export function ogForLettersByLocation(location: string, count: number) {
|
||||
/**
|
||||
* Extract a display quote from the `?hl=` search param.
|
||||
*
|
||||
* The `hl` param encodes `prefix,suffix,length`. We reconstruct a readable
|
||||
* snippet from the prefix + suffix: "first few words ... last few words"
|
||||
* When `paragraphs` are provided (from the database), the full matched text
|
||||
* is extracted by searching the content — giving the OG image the actual
|
||||
* selected quote, not just the prefix/suffix.
|
||||
*
|
||||
* When `paragraphs` are not provided, falls back to reconstructing a
|
||||
* readable snippet from prefix + suffix: "first few words ... last few words"
|
||||
*
|
||||
* Returns null if hl is missing or invalid.
|
||||
*/
|
||||
export function extractQuoteFromHl(
|
||||
searchParams: Record<string, string | string[] | undefined> | URLSearchParams,
|
||||
paragraphs?: string[],
|
||||
): string | null {
|
||||
const raw =
|
||||
searchParams instanceof URLSearchParams
|
||||
@@ -325,6 +330,23 @@ export function extractQuoteFromHl(
|
||||
const fragment = decodeTextFragment(raw);
|
||||
if (!fragment) return null;
|
||||
|
||||
// If paragraphs are available, extract the full matched text
|
||||
if (paragraphs && paragraphs.length > 0) {
|
||||
const fullMatch = extractMatchedText(fragment, paragraphs);
|
||||
if (fullMatch) {
|
||||
// Truncate at word boundary if too long for OG image
|
||||
if (fullMatch.length > 280) {
|
||||
const truncated = fullMatch.slice(0, 277);
|
||||
const lastSpace = truncated.lastIndexOf(" ");
|
||||
return (
|
||||
(lastSpace > 200 ? truncated.slice(0, lastSpace) : truncated) + "..."
|
||||
);
|
||||
}
|
||||
return fullMatch;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: reconstruct from prefix + suffix
|
||||
const { prefix, suffix } = fragment;
|
||||
if (!prefix) return null;
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Text Fragment Highlight — compact URL encoding for selected text
|
||||
//
|
||||
// These utilities work in both browser and server environments.
|
||||
// The DOM-based findAndHighlightText() is browser-only.
|
||||
// The string-based extractMatchedText() is server-safe (for OG image generation).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -52,6 +56,67 @@ export function decodeTextFragment(
|
||||
return { prefix, suffix, length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Server-side: extract the full matched text from an array of paragraph strings
|
||||
* using a decoded text fragment (prefix, suffix, length).
|
||||
*
|
||||
* This is the server equivalent of findAndHighlightText() — instead of
|
||||
* walking the DOM, it searches a flat array of HTML paragraph strings.
|
||||
* HTML tags are stripped before matching.
|
||||
*
|
||||
* Returns the matched text string, or null if not found.
|
||||
*/
|
||||
export function extractMatchedText(
|
||||
fragment: { prefix: string; suffix: string; length: number },
|
||||
paragraphs: string[],
|
||||
): string | null {
|
||||
// Strip HTML tags to get plain text
|
||||
const plainParagraphs = paragraphs.map((p) =>
|
||||
p.replace(/<[^>]*>/g, "").replace(/&[^;]+;/g, " "),
|
||||
);
|
||||
|
||||
// Build a single string with paragraph breaks
|
||||
const fullText = plainParagraphs.join(" ");
|
||||
const normalized = fullText.replace(/\s+/g, " ");
|
||||
|
||||
const normalizedPrefix = fragment.prefix.replace(/\s+/g, " ");
|
||||
let searchFrom = 0;
|
||||
let matchStart = -1;
|
||||
|
||||
while (searchFrom < normalized.length) {
|
||||
const idx = normalized.indexOf(normalizedPrefix, searchFrom);
|
||||
if (idx === -1) break;
|
||||
|
||||
if (fragment.suffix) {
|
||||
const normalizedSuffix = fragment.suffix.replace(/\s+/g, " ");
|
||||
const expectedEnd = idx + fragment.length;
|
||||
const suffixSearchStart = Math.max(
|
||||
idx,
|
||||
expectedEnd - normalizedSuffix.length - 20,
|
||||
);
|
||||
const suffixSearchEnd = Math.min(normalized.length, expectedEnd + 20);
|
||||
const searchWindow = normalized.slice(suffixSearchStart, suffixSearchEnd);
|
||||
if (searchWindow.includes(normalizedSuffix)) {
|
||||
matchStart = idx;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
matchStart = idx;
|
||||
break;
|
||||
}
|
||||
searchFrom = idx + 1;
|
||||
}
|
||||
|
||||
if (matchStart === -1) return null;
|
||||
|
||||
const matchEnd = Math.min(matchStart + fragment.length, normalized.length);
|
||||
const matched = normalized.slice(matchStart, matchEnd).trim();
|
||||
|
||||
if (matched.length === 0) return null;
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full URL with highlight fragment for the selected text.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user