Skip to content

Added publication social account fields#27396

Open
JohnONolan wants to merge 1 commit intoTryGhost:mainfrom
JohnONolan:codex/add-publication-social-fields
Open

Added publication social account fields#27396
JohnONolan wants to merge 1 commit intoTryGhost:mainfrom
JohnONolan:codex/add-publication-social-fields

Conversation

@JohnONolan
Copy link
Copy Markdown
Member

@JohnONolan JohnONolan commented Apr 14, 2026

This adds publication-level social settings for the same set of platforms supported on staff users, and wires them through admin settings, public settings exposure, and theme helpers.

What changed

  • added publication settings and migration entries for Threads, Bluesky, Mastodon, TikTok, YouTube, Instagram, and LinkedIn
  • refactored admin social account fields to use a shared platform config across publication and staff settings
  • exposed the new publication settings through public settings and {{social_url}}/@site theme access
  • expanded acceptance and unit coverage for settings, helper behavior, and schema integrity
image

Extended publication-level social settings to match the staff profile fields so sites can store, edit, and expose the same set of networks consistently across admin and themes.
@github-actions github-actions bot added the migration [pull request] Includes migration for review label Apr 14, 2026
@github-actions
Copy link
Copy Markdown
Contributor

It looks like this PR contains a migration 👀
Here's the checklist for reviewing migrations:

General requirements

  • ⚠️ Tested performance on staging database servers, as performance on local machines is not comparable to a production environment
  • Satisfies idempotency requirement (both up() and down())
  • Does not reference models
  • Filename is in the correct format (and correctly ordered)
  • Targets the next minor version
  • All code paths have appropriate log messages
  • Uses the correct utils
  • Contains a minimal changeset
  • Does not mix DDL/DML operations
  • Tested in MySQL and SQLite

Schema changes

  • Both schema change and related migration have been implemented
  • For index changes: has been performance tested for large tables
  • For new tables/columns: fields use the appropriate predefined field lengths
  • For new tables/columns: field names follow the appropriate conventions
  • Does not drop a non-alpha table outside of a major version

Data changes

  • Mass updates/inserts are batched appropriately
  • Does not loop over large tables/datasets
  • Defends against missing or invalid data
  • For settings updates: follows the appropriate guidelines

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 14, 2026

Walkthrough

This pull request extends social media platform support from Facebook and Twitter to include Threads, Bluesky, Mastodon, TikTok, YouTube, Instagram, and LinkedIn. The frontend social accounts component was refactored from hardcoded per-platform fields to a configuration-driven system. Backend changes include new database schema entries, a migration to register the settings, API endpoint updates to accept the new keys, and fixture updates. Test files were updated to cover validation and editing across all social platforms.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change—adding publication social account fields—and aligns with the comprehensive refactoring across settings, migrations, and theme helpers.
Description check ✅ Passed The description clearly explains what was added (publication social settings for new platforms), how it was implemented (shared platform config), and where it was integrated (admin, public settings, theme helpers).
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@JohnONolan JohnONolan assigned JohnONolan and ErisDS and unassigned JohnONolan Apr 14, 2026
@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

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 (3)
ghost/core/test/unit/shared/settings-cache.test.js (1)

155-170: Consider validating all newly added publication social keys in this test.

Right now this protects one key. A table-driven block for all new keys would better guard the public settings allowlist.

♻️ Refactor suggestion
-        cache.set('linkedin', {value: 'ghost-team'});
+        const socialValues = {
+            threads: 'ghost-threads',
+            bluesky: 'ghost.bsky.social',
+            mastodon: 'mastodon.social/@ghost',
+            tiktok: 'ghost-tiktok',
+            youtube: 'ghost-youtube',
+            instagram: 'ghost-instagram',
+            linkedin: 'ghost-team'
+        };
+        Object.entries(socialValues).forEach(([key, value]) => cache.set(key, {value}));

@@
-        values.linkedin = 'ghost-team';
+        Object.assign(values, socialValues);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/unit/shared/settings-cache.test.js` around lines 155 - 170,
The test currently only asserts the 'linkedin' public setting; update it to
validate all newly added publication social keys by iterating over the allowlist
(use the existing publicSettings symbol) instead of hardcoding one key. For each
social key, call cache.set(key, {value: <expected>}) or include it in the
initial values setup, ensure values and cache.getAll() contain that key
(symbols: cache.set, cache.getAll, publicSettings, values), and replace the
single 'linkedin' assertion with a table-driven loop that asserts presence and
correct value for every key in publicSettings.
ghost/core/test/unit/frontend/helpers/social-url.test.js (1)

87-95: Consider parameterizing fallback assertions with the existing platform table.

This avoids drift where new platforms are added to platforms but not fallback coverage.

♻️ Refactor suggestion
-    it('falls back to site data for publication social accounts', function () {
-        assert.equal(compile(`{{social_url type="facebook"}}`)
-            .with({}), 'https://www.facebook.com/testuser-fb');
-
-        assert.equal(compile(`{{social_url type="twitter"}}`)
-            .with({}), 'https://x.com/testuser-tw');
-
-        assert.equal(compile(`{{social_url type="linkedin"}}`)
-            .with({}), 'https://www.linkedin.com/in/testuser-li');
-    });
+    it('falls back to site data for publication social accounts', function () {
+        platforms.forEach((platform) => {
+            assert.equal(
+                compile(`{{social_url type="${platform.name}"}}`).with({}),
+                platform.expectedUrl
+            );
+        });
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ghost/core/test/unit/frontend/helpers/social-url.test.js` around lines 87 -
95, The test currently hardcodes three fallback assertions for
compile(`{{social_url type="..."}}`) which can drift when the platforms table
changes; update the 'falls back to site data for publication social accounts'
test to iterate over the existing platforms table (the same platforms data used
elsewhere in the test suite) and for each platform assert that
compile(`{{social_url type="${platform.name}"}}`).with({}) returns the expected
fallback URL derived from the platform's canonical URL pattern and the
publication's username (use the platform entry fields used by the helper);
reference the test helper compile and the social_url helper to build each
assertion so future platform additions are automatically covered.
apps/admin-x-settings/test/acceptance/general/social-accounts.test.ts (1)

44-46: Avoid the unchecked body cast in this assertion.

as {settings: Array<{key: string; value: string}>} bypasses the request-body shape check right where this test is supposed to verify the contract. Prefer a tiny runtime guard/helper for body.settings so malformed payloads fail for the right reason instead of being forced through a cast. Based on learnings: "In TypeScript files within the Ghost admin settings area ... avoid unsafe type assertions ... perform runtime type checks, and apply sensible defaults."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-settings/test/acceptance/general/social-accounts.test.ts` around
lines 44 - 46, The test currently uses an unsafe cast on
lastApiRequests.editSettings?.body to assume a {settings:
Array<{key:string;value:string}>} shape; add a small runtime guard helper (e.g.
isSettingsPayload(obj)) that checks obj is an object, obj.settings is an array,
and every item has string key and string value, then use that guard to extract
payloadSettings (or throw/assert with a clear message if the guard fails) and
feed payloadSettings into sortSettings for comparison with editedSocialSettings;
update the assertion to use lastApiRequests.editSettings?.body validated by
isSettingsPayload instead of the unchecked cast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/admin-x-settings/src/utils/social-urls/platform-config.ts`:
- Around line 137-143: normalizeSocialInput currently uses the raw result of
validate() as displayValue but some validators (e.g. twitter, youtube) return
handles rather than canonical URLs; update normalizeSocialInput to first compute
storedValue = normalizedUrl ?
SOCIAL_PLATFORM_CONFIG_BY_KEY[key].toStoredValue(normalizedUrl) : null and then
derive displayValue from that storedValue using
SOCIAL_PLATFORM_CONFIG_BY_KEY[key].toDisplayValue(storedValue) (or null when
storedValue is null), so displayValue is always the formatted/canonical
presentation consumers expect.

---

Nitpick comments:
In `@apps/admin-x-settings/test/acceptance/general/social-accounts.test.ts`:
- Around line 44-46: The test currently uses an unsafe cast on
lastApiRequests.editSettings?.body to assume a {settings:
Array<{key:string;value:string}>} shape; add a small runtime guard helper (e.g.
isSettingsPayload(obj)) that checks obj is an object, obj.settings is an array,
and every item has string key and string value, then use that guard to extract
payloadSettings (or throw/assert with a clear message if the guard fails) and
feed payloadSettings into sortSettings for comparison with editedSocialSettings;
update the assertion to use lastApiRequests.editSettings?.body validated by
isSettingsPayload instead of the unchecked cast.

In `@ghost/core/test/unit/frontend/helpers/social-url.test.js`:
- Around line 87-95: The test currently hardcodes three fallback assertions for
compile(`{{social_url type="..."}}`) which can drift when the platforms table
changes; update the 'falls back to site data for publication social accounts'
test to iterate over the existing platforms table (the same platforms data used
elsewhere in the test suite) and for each platform assert that
compile(`{{social_url type="${platform.name}"}}`).with({}) returns the expected
fallback URL derived from the platform's canonical URL pattern and the
publication's username (use the platform entry fields used by the helper);
reference the test helper compile and the social_url helper to build each
assertion so future platform additions are automatically covered.

In `@ghost/core/test/unit/shared/settings-cache.test.js`:
- Around line 155-170: The test currently only asserts the 'linkedin' public
setting; update it to validate all newly added publication social keys by
iterating over the allowlist (use the existing publicSettings symbol) instead of
hardcoding one key. For each social key, call cache.set(key, {value:
<expected>}) or include it in the initial values setup, ensure values and
cache.getAll() contain that key (symbols: cache.set, cache.getAll,
publicSettings, values), and replace the single 'linkedin' assertion with a
table-driven loop that asserts presence and correct value for every key in
publicSettings.
🪄 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: 5bafbe08-1bbc-441f-8a13-84e447e0b546

📥 Commits

Reviewing files that changed from the base of the PR and between 5b379e6 and f5cc706.

📒 Files selected for processing (18)
  • apps/admin-x-framework/src/test/responses/settings.json
  • apps/admin-x-settings/src/components/settings/general/general-settings.tsx
  • apps/admin-x-settings/src/components/settings/general/social-accounts.tsx
  • apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx
  • apps/admin-x-settings/src/components/settings/general/users/social-links-tab.tsx
  • apps/admin-x-settings/src/utils/social-urls/index.ts
  • apps/admin-x-settings/src/utils/social-urls/platform-config.ts
  • apps/admin-x-settings/test/acceptance/general/social-accounts.test.ts
  • ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js
  • ghost/core/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.js
  • ghost/core/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.js
  • ghost/core/core/server/data/migrations/versions/6.31/2026-04-14-20-12-28-add-publication-social-account-settings.js
  • ghost/core/core/server/data/schema/default-settings/default-settings.json
  • ghost/core/core/shared/settings-cache/public.js
  • ghost/core/test/unit/frontend/helpers/social-url.test.js
  • ghost/core/test/unit/server/data/schema/integrity.test.js
  • ghost/core/test/unit/shared/settings-cache.test.js
  • ghost/core/test/utils/fixtures/default-settings.json

Comment on lines +137 to +143
export const normalizeSocialInput = (key: SocialPlatformKey, value: string) => {
const normalizedUrl = SOCIAL_PLATFORM_CONFIG_BY_KEY[key].validate(value);

return {
displayValue: normalizedUrl,
storedValue: normalizedUrl ? SOCIAL_PLATFORM_CONFIG_BY_KEY[key].toStoredValue(normalizedUrl) : null
};
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.

⚠️ Potential issue | 🟠 Major

Build displayValue from the stored form, not directly from validate().

At least two validators return handles instead of canonical URLs: apps/admin-x-settings/src/utils/social-urls/twitter.ts:4-30 and apps/admin-x-settings/src/utils/social-urls/youtube.ts:4-66. With the current code, normalizeSocialInput('twitter', 'x.com/tw') produces displayValue: 'tw', so shared consumers such as the staff social links UI will lose the formatted URL after blur.

💡 Proposed fix
 export const normalizeSocialInput = (key: SocialPlatformKey, value: string) => {
-    const normalizedUrl = SOCIAL_PLATFORM_CONFIG_BY_KEY[key].validate(value);
+    const config = SOCIAL_PLATFORM_CONFIG_BY_KEY[key];
+    const normalizedValue = config.validate(value);
+    const storedValue = normalizedValue ? config.toStoredValue(normalizedValue) : null;
 
     return {
-        displayValue: normalizedUrl,
-        storedValue: normalizedUrl ? SOCIAL_PLATFORM_CONFIG_BY_KEY[key].toStoredValue(normalizedUrl) : null
+        displayValue: config.toDisplayValue(storedValue),
+        storedValue
     };
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/admin-x-settings/src/utils/social-urls/platform-config.ts` around lines
137 - 143, normalizeSocialInput currently uses the raw result of validate() as
displayValue but some validators (e.g. twitter, youtube) return handles rather
than canonical URLs; update normalizeSocialInput to first compute storedValue =
normalizedUrl ? SOCIAL_PLATFORM_CONFIG_BY_KEY[key].toStoredValue(normalizedUrl)
: null and then derive displayValue from that storedValue using
SOCIAL_PLATFORM_CONFIG_BY_KEY[key].toDisplayValue(storedValue) (or null when
storedValue is null), so displayValue is always the formatted/canonical
presentation consumers expect.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

migration [pull request] Includes migration for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants