diff --git a/public/app.js b/public/app.js
index 78f5a36..64e3080 100644
--- a/public/app.js
+++ b/public/app.js
@@ -632,6 +632,8 @@ async function handleScrapeSubmit(e) {
isInstagram: errorData.isInstagram,
isTikTok: errorData.isTikTok,
isFacebook: errorData.isFacebook,
+ isTruncated: errorData.isTruncated,
+ partialCaption: errorData.partialCaption || '',
author: errorData.author || '',
coverImage: errorData.coverImage || '',
errorMessage: errorData.error
@@ -661,7 +663,7 @@ async function handleScrapeSubmit(e) {
// Open Social Caption Extractor Modal (Instagram / TikTok / Facebook)
function openSocialCaptionModal(options = {}) {
- const { url = '', isInstagram = false, isTikTok = false, isFacebook = false, author = '', coverImage = '' } = options;
+ const { url = '', isInstagram = false, isTikTok = false, isFacebook = false, isTruncated = false, partialCaption = '', author = '', coverImage = '' } = options;
const titleEl = document.getElementById('social-modal-title');
const iconEl = document.getElementById('social-modal-icon');
@@ -704,7 +706,11 @@ function openSocialCaptionModal(options = {}) {
iconEl.style.color = '#1877F2';
}
if (hintEl) {
- hintEl.textContent = 'Facebook restricts automated extraction of Reel captions and comments. Simply copy the caption text or pinned recipe comment from Facebook and paste it below—ForkFolio will parse the recipe and format the ingredients automatically!';
+ if (isTruncated && partialCaption) {
+ hintEl.innerHTML = '⚠️ Truncated Caption Detected
Facebook shortened this caption with an ellipsis. We\'ve pre-filled what was retrieved below—please paste the missing steps or copy the full text from Facebook!';
+ } else {
+ hintEl.textContent = 'Facebook restricts automated extraction of Reel captions and comments. Simply copy the caption text or pinned recipe comment from Facebook and paste it below—ForkFolio will parse the recipe and format the ingredients automatically!';
+ }
}
} else {
if (titleEl) titleEl.textContent = 'Social Media Recipe Import';
@@ -720,7 +726,7 @@ function openSocialCaptionModal(options = {}) {
if (urlInput) urlInput.value = url || '';
if (authorInput) authorInput.value = author || '';
if (coverInput) coverInput.value = coverImage || '';
- if (captionText) captionText.value = '';
+ if (captionText) captionText.value = (isTruncated && partialCaption) ? partialCaption : '';
if (previewBox) {
if (url || author || coverImage) {
@@ -747,7 +753,12 @@ function openSocialCaptionModal(options = {}) {
openModal('social-caption-modal');
lucide.createIcons();
if (captionText) {
- setTimeout(() => captionText.focus(), 150);
+ setTimeout(() => {
+ captionText.focus();
+ if (isTruncated && partialCaption) {
+ captionText.selectionStart = captionText.selectionEnd = captionText.value.length;
+ }
+ }, 150);
}
}
diff --git a/server.js b/server.js
index ea0d2b4..0fda361 100644
--- a/server.js
+++ b/server.js
@@ -1453,6 +1453,68 @@ function cleanFacebookCaption(raw) {
return cleanSocialCaption(text);
}
+// Helper: Detect if a social caption is truncated by platform character limits or ellipsis
+function isCaptionTruncated(text) {
+ if (!text || typeof text !== 'string') return false;
+ const trimmed = text.trim();
+ // 1. Explicit ellipsis or "See more" markers
+ if (/(?:\.{3,}|…)\s*(?:See\s+more)?$/i.test(trimmed)) return true;
+ if (/\bSee\s+more$/i.test(trimmed)) return true;
+ // 2. Typical metadata character limit truncation (~180-320 chars without ending punctuation)
+ if (trimmed.length >= 180 && trimmed.length <= 320) {
+ const endsWithPunctuation = /[.!?\n\r"']$/.test(trimmed);
+ if (!endsWithPunctuation) return true;
+ }
+ return false;
+}
+
+// Helper: Extract full untruncated Facebook message from embedded Relay / GraphQL / script tags
+function extractFullFacebookMessageFromHtml(html) {
+ if (!html || typeof html !== 'string') return '';
+ let longestText = '';
+
+ const isBoilerplate = (str) => /log in|watch popular videos|sign up for facebook|connect with friends/i.test(str);
+
+ // 1. Search for JSON Relay message text blobs: "message":{"text":"..."} or "savable_description":{"text":"..."}
+ const messageRegex = /"(?:message|savable_description)"\s*:\s*\{\s*"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
+ let match;
+ while ((match = messageRegex.exec(html)) !== null) {
+ try {
+ const decoded = JSON.parse(`"${match[1]}"`);
+ if (decoded && decoded.length > longestText.length && !isBoilerplate(decoded)) {
+ longestText = decoded;
+ }
+ } catch (e) {
+ // Ignore JSON parse errors on malformed sub-strings
+ }
+ }
+
+ // 2. Search for creation_story or story messages
+ const storyRegex = /"creation_story"\s*:\s*\{[\s\S]*?"message"\s*:\s*\{\s*"text"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
+ while ((match = storyRegex.exec(html)) !== null) {
+ try {
+ const decoded = JSON.parse(`"${match[1]}"`);
+ if (decoded && decoded.length > longestText.length && !isBoilerplate(decoded)) {
+ longestText = decoded;
+ }
+ } catch (e) {}
+ }
+
+ // 3. Check JSON-LD articleBody or description
+ const ldJsonRegex = /
+