Added publication social account fields#27396
Added publication social account fields#27396JohnONolan wants to merge 1 commit intoTryGhost:mainfrom
Conversation
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.
|
It looks like this PR contains a migration 👀 General requirements
Schema changes
Data changes
|
WalkthroughThis 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)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
There was a problem hiding this comment.
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
platformsbut 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 forbody.settingsso 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
📒 Files selected for processing (18)
apps/admin-x-framework/src/test/responses/settings.jsonapps/admin-x-settings/src/components/settings/general/general-settings.tsxapps/admin-x-settings/src/components/settings/general/social-accounts.tsxapps/admin-x-settings/src/components/settings/general/user-detail-modal.tsxapps/admin-x-settings/src/components/settings/general/users/social-links-tab.tsxapps/admin-x-settings/src/utils/social-urls/index.tsapps/admin-x-settings/src/utils/social-urls/platform-config.tsapps/admin-x-settings/test/acceptance/general/social-accounts.test.tsghost/core/core/server/api/endpoints/utils/serializers/input/settings.jsghost/core/core/server/api/endpoints/utils/serializers/input/utils/settings-key-group-mapper.jsghost/core/core/server/api/endpoints/utils/serializers/input/utils/settings-key-type-mapper.jsghost/core/core/server/data/migrations/versions/6.31/2026-04-14-20-12-28-add-publication-social-account-settings.jsghost/core/core/server/data/schema/default-settings/default-settings.jsonghost/core/core/shared/settings-cache/public.jsghost/core/test/unit/frontend/helpers/social-url.test.jsghost/core/test/unit/server/data/schema/integrity.test.jsghost/core/test/unit/shared/settings-cache.test.jsghost/core/test/utils/fixtures/default-settings.json
| 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 | ||
| }; |
There was a problem hiding this comment.
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.



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
{{social_url}}/@sitetheme access