Skip to content

Fixed YouTube bookmark metadata#29114

Merged
9larsons merged 7 commits into
TryGhost:mainfrom
cyphercodes:fix/youtube-bookmark-metadata
Jul 22, 2026
Merged

Fixed YouTube bookmark metadata#29114
9larsons merged 7 commits into
TryGhost:mainfrom
cyphercodes:fix/youtube-bookmark-metadata

Conversation

@cyphercodes

Copy link
Copy Markdown
Contributor

Fixes #24741

Why:
YouTube bookmark requests can receive generic page metadata (and no thumbnail) from the fetched page, even though YouTube's allowlisted oEmbed endpoint has the video title, author, provider, and thumbnail. Missing image URLs were also still sent through bookmark image processing, which produced Failed to parse URL from null logs.

What:

  • Builds explicit bookmark cards from allowlisted known-provider oEmbed metadata without exposing provider-supplied embed HTML.
  • Skips absent bookmark icon/thumbnail URLs before image processing while keeping the default icon fallback.
  • Adds unit coverage for YouTube bookmark metadata and missing image URLs.

Why Ghost users or developers need this:
YouTube bookmark cards now use video metadata instead of a generic YouTube card and avoid noisy null-image URL errors.

Tests:

  • pnpm test:single test/unit/server/services/oembed/oembed-service.test.js
  • pnpm exec eslint core/server/services/oembed/oembed-service.js test/unit/server/services/oembed/oembed-service.test.js
  • git diff --check

Please check your PR against these items:

  • I've read and followed the Contributor Guide
  • I've explained my change
  • I've written an automated test to prove my change works

@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This change adds allowlisted oEmbed enrichment for bookmark cards and combines it with fetched page metadata. Bookmark image processing now handles missing URLs safely, uses shared defaults, and conditionally processes icons and thumbnails. Failed page requests can return enrichment-only bookmark data when a title is available. Tests cover provider metadata, fallback paths, timeout behavior, missing images, and exclusion of provider embed HTML.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: fixing YouTube bookmark metadata handling.
Description check ✅ Passed The description is directly related to the implemented YouTube bookmark metadata and null image fixes.
Linked Issues check ✅ Passed The changes satisfy #24741 by using YouTube oEmbed metadata for bookmark cards and avoiding missing-image URL errors.
Out of Scope Changes check ✅ Passed The added bookmark enrichment and image-guarding logic remain within the issue scope and do not introduce unrelated changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
ghost/core/test/unit/server/services/oembed/oembed-service.test.js (1)

315-329: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider a test for the known-provider failure/fallback path.

Coverage is good for the success case and the missing-image case, but there's no test exercising fetchBookmarkDataFromKnownProvider when knownProvider throws (or returns no title) to confirm fetchOembedDataFromUrl correctly falls back to page scraping for a bookmark request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/test/unit/server/services/oembed/oembed-service.test.js` around
lines 315 - 329, Add a unit test for the bookmark fallback path in
oembed-service.test.js that exercises fetchOembedDataFromUrl when
fetchBookmarkDataFromKnownProvider fails or returns no title, so it falls back
to page scraping for bookmark requests. Use the existing oembedService and stub
fetchBookmarkDataFromKnownProvider to throw or return an incomplete result, then
assert the fallback result comes from page scraping and that bookmark metadata
is still populated correctly. Keep the test alongside the existing
fetchBookmarkData coverage and reference fetchOembedDataFromUrl and
fetchBookmarkDataFromKnownProvider to locate the behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ghost/core/core/server/services/oembed/oembed-service.js`:
- Around line 192-244: The thumbnail image handling in
fetchBookmarkDataFromKnownProvider and fetchBookmarkData is inconsistent with
the icon fallback and can leave raw provider URLs exposed when
processImageFromUrl fails. Update the thumbnail processing path so failures
clear or replace metadata.thumbnail with a safe fallback instead of keeping the
original URL, matching the DEFAULT_BOOKMARK_ICON behavior used for icons. While
touching fetchBookmarkData and fetchBookmarkDataFromKnownProvider, consider
extracting the repeated “process image with fallback” logic into a shared helper
to remove duplication and keep the thumbnail/icon behavior consistent.

---

Nitpick comments:
In `@ghost/core/test/unit/server/services/oembed/oembed-service.test.js`:
- Around line 315-329: Add a unit test for the bookmark fallback path in
oembed-service.test.js that exercises fetchOembedDataFromUrl when
fetchBookmarkDataFromKnownProvider fails or returns no title, so it falls back
to page scraping for bookmark requests. Use the existing oembedService and stub
fetchBookmarkDataFromKnownProvider to throw or return an incomplete result, then
assert the fallback result comes from page scraping and that bookmark metadata
is still populated correctly. Keep the test alongside the existing
fetchBookmarkData coverage and reference fetchOembedDataFromUrl and
fetchBookmarkDataFromKnownProvider to locate the behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 11974741-5297-4601-918a-7c0105cc664a

📥 Commits

Reviewing files that changed from the base of the PR and between 7e35129 and 715b168.

📒 Files selected for processing (2)
  • ghost/core/core/server/services/oembed/oembed-service.js
  • ghost/core/test/unit/server/services/oembed/oembed-service.test.js

Comment on lines +192 to +244
/**
* Build bookmark metadata from a known oEmbed provider without exposing the
* provider-supplied HTML that embed cards use.
*
* @param {string} url
* @returns {Promise<Object|undefined>}
*/
async fetchBookmarkDataFromKnownProvider(url) {
const {url: providerUrl, provider} = findUrlWithProvider(url);
if (!provider) {
return;
}

let oembed;
try {
oembed = await this.knownProvider(providerUrl);
} catch {
// Known-provider metadata is an enhancement for explicit bookmark
// cards. If it fails, fall back to scraping the page as before.
return;
}

if (!oembed.title) {
return;
}

const metadata = {
url,
title: oembed.title,
description: oembed.description || null,
author: oembed.author_name || null,
publisher: oembed.provider_name || null,
thumbnail: oembed.thumbnail_url || (oembed.type === 'photo' ? oembed.url : null),
icon: DEFAULT_BOOKMARK_ICON
};

if (metadata.thumbnail) {
await this.processImageFromUrl(metadata.thumbnail, 'thumbnail')
.then((processedImageUrl) => {
metadata.thumbnail = processedImageUrl;
}).catch((err) => {
logging.error(err);
});
}

return {
version: '1.0',
type: 'bookmark',
url,
metadata
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Thumbnail fallback inconsistency + duplicated image-processing pattern.

When processImageFromUrl fails for the thumbnail (Line 228-235 here, and the equivalent Line 469-476 in fetchBookmarkData), the .catch only logs the error and leaves metadata.thumbnail pointing at the raw, unprocessed provider/third-party URL. This is inconsistent with the icon handling, which explicitly falls back to DEFAULT_BOOKMARK_ICON on failure, and it partially undercuts this PR's stated goal of not exposing provider-supplied content directly — a bookmark card can end up hot-linking an untrusted external image URL instead of a locally-stored copy.

Additionally, the "process image, fall back on missing/failure" pattern is now duplicated three times (icon in fetchBookmarkData, thumbnail in fetchBookmarkData, thumbnail in fetchBookmarkDataFromKnownProvider). Consider extracting a small shared helper.

♻️ Suggested helper + fallback fix
+    async _processBookmarkImage(imageUrl, imageType, fallback = null) {
+        if (!imageUrl) {
+            return fallback;
+        }
+        try {
+            return await this.processImageFromUrl(imageUrl, imageType);
+        } catch (err) {
+            logging.error(err);
+            return fallback;
+        }
+    }
+
     async fetchBookmarkDataFromKnownProvider(url) {
         ...
-        if (metadata.thumbnail) {
-            await this.processImageFromUrl(metadata.thumbnail, 'thumbnail')
-                .then((processedImageUrl) => {
-                    metadata.thumbnail = processedImageUrl;
-                }).catch((err) => {
-                    logging.error(err);
-                });
-        }
+        metadata.thumbnail = await this._processBookmarkImage(metadata.thumbnail, 'thumbnail');

Based on the PR's own rationale of not exposing provider-supplied content raw, applying the same null-fallback used for icons to thumbnails would close this gap.

Also applies to: 228-235, 469-476

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/core/server/services/oembed/oembed-service.js` around lines 192 -
244, The thumbnail image handling in fetchBookmarkDataFromKnownProvider and
fetchBookmarkData is inconsistent with the icon fallback and can leave raw
provider URLs exposed when processImageFromUrl fails. Update the thumbnail
processing path so failures clear or replace metadata.thumbnail with a safe
fallback instead of keeping the original URL, matching the DEFAULT_BOOKMARK_ICON
behavior used for icons. While touching fetchBookmarkData and
fetchBookmarkDataFromKnownProvider, consider extracting the repeated “process
image with fallback” logic into a shared helper to remove duplication and keep
the thumbnail/icon behavior consistent.

cyphercodes and others added 2 commits July 7, 2026 15:32
Fixes TryGhost#24741

Why: YouTube pages can return generic localized metadata to server-side bookmark requests even when the allowlisted oEmbed endpoint has the video details.

What: Use known-provider oEmbed metadata for explicit bookmark cards and skip missing image URLs before processing them.
@9larsons
9larsons force-pushed the fix/youtube-bookmark-metadata branch from 715b168 to 201ce2f Compare July 7, 2026 20:34
@cursor

cursor Bot commented Jul 7, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

9larsons added 2 commits July 21, 2026 09:21
ref TryGhost#24741

Limited the oEmbed bookmark shortcut to YouTube so other providers keep their richer page metadata while YouTube retains its provider-specific fallback.
ref TryGhost#24741

Known providers can expose accurate structured media metadata even when their pages return generic content or block server-side requests, so bookmarks now enrich page metadata without discarding useful page fields.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ghost/core/core/server/services/oembed/oembed-service.js (1)

442-488: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Thumbnail fallback still leaves raw provider URL exposed on processing failure.

When processImageFromUrl fails for metadata.thumbnail (Lines 472-479), the .catch only logs and leaves metadata.thumbnail pointing at the raw, unprocessed provider URL — inconsistent with the icon path (Lines 460-471), which falls back to DEFAULT_BOOKMARK_ICON on failure. This was already flagged in a prior review and remains unresolved in this revision. The "process image, fall back on failure" pattern is also still duplicated between the icon and thumbnail branches here.

♻️ Suggested fix
-            if (metadata.thumbnail) {
-                await this.processImageFromUrl(metadata.thumbnail, 'thumbnail')
-                    .then((processedImageUrl) => {
-                        metadata.thumbnail = processedImageUrl;
-                    }).catch((err) => {
-                        logging.error(err);
-                    });
-            }
+            metadata.thumbnail = await this.processImageFromUrl(metadata.thumbnail, 'thumbnail')
+                .catch((err) => {
+                    logging.error(err);
+                    return null;
+                });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ghost/core/core/server/services/oembed/oembed-service.js` around lines 442 -
488, Update the thumbnail handling in buildBookmarkData so a processImageFromUrl
failure replaces metadata.thumbnail with DEFAULT_BOOKMARK_ICON instead of
retaining the raw provider URL. Reuse the existing icon/thumbnail
image-processing fallback pattern where practical, while preserving successful
processed URLs and existing logging behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@ghost/core/core/server/services/oembed/oembed-service.js`:
- Around line 442-488: Update the thumbnail handling in buildBookmarkData so a
processImageFromUrl failure replaces metadata.thumbnail with
DEFAULT_BOOKMARK_ICON instead of retaining the raw provider URL. Reuse the
existing icon/thumbnail image-processing fallback pattern where practical, while
preserving successful processed URLs and existing logging behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6ca2ea1d-6000-4313-acc2-b577cbdab90b

📥 Commits

Reviewing files that changed from the base of the PR and between 639f9d4 and 37d6a8b.

📒 Files selected for processing (2)
  • ghost/core/core/server/services/oembed/oembed-service.js
  • ghost/core/test/unit/server/services/oembed/oembed-service.test.js

- simplified the enrichment timeout to the got-style {request: ms} shape only;
  no caller passes a bare number, and the dual-shape handling widened the
  public JSDoc for a case that never occurs
- always create the abort signal so a zero/negative timeout can't leave the
  enrichment request unbounded while Promise.allSettled waits on it
- merged two near-identical YouTube bookmark tests into the TryGhost#24741 regression
  test, keeping the union of their assertions (description passthrough, icon
  fallback, thumbnail processing call, embed-HTML leak check)
@nx-cloud

nx-cloud Bot commented Jul 22, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix

Ensure the fix-ci command is configured to always run in your CI pipeline to get automatic fixes in future runs. For more information, please see https://nx.dev/ci/features/self-healing-ci


View your CI Pipeline Execution ↗ for commit a05e75d

Command Status Duration Result
nx run ghost:test:ci:integration ✅ Succeeded 2m 55s View ↗
nx run ghost:test:integration ✅ Succeeded 3m 16s View ↗
nx run ghost:test:legacy ✅ Succeeded 3m 6s View ↗
nx run ghost:test:e2e ✅ Succeeded 2m 40s View ↗
nx run-many -t test:unit -p ghost ✅ Succeeded 31s View ↗
nx run ghost-monorepo:lint:boundaries ✅ Succeeded <1s View ↗
nx run-many --target=build --projects=tag:publi... ✅ Succeeded 1s View ↗
nx run @tryghost/admin:build ✅ Succeeded 5s View ↗
nx run-many -t lint -p ghost,ghost-monorepo ✅ Succeeded 22s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-07-22 01:26:38 UTC

9larsons added 2 commits July 21, 2026 19:58
no ref

- the test's page used <link rel="thumbnail">, which metascraper never
  picks up, so the service always called processImageFromUrl(undefined);
  the test only passed because the stub returned a fake stored URL for
  any input, including undefined
- the missing-image guard added for TryGhost#24741 now skips absent thumbnails,
  exposing the artifact; use og:image so the test exercises the real
  scrape-and-store path
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.43478% with 34 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.56%. Comparing base (8de5eeb) to head (a05e75d).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...core/core/server/services/oembed/oembed-service.js 70.43% 32 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #29114      +/-   ##
==========================================
- Coverage   74.57%   74.56%   -0.01%     
==========================================
  Files        1609     1609              
  Lines      141363   141454      +91     
  Branches    17258    17269      +11     
==========================================
+ Hits       105423   105477      +54     
- Misses      34909    34913       +4     
- Partials     1031     1064      +33     
Flag Coverage Δ
e2e-tests 76.63% <70.43%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@9larsons
9larsons merged commit 9b0f0ce into TryGhost:main Jul 22, 2026
45 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bookmark for YouTube links not populated with video metadata

2 participants