Skip to content

feat(socialMeta): settings.socialMeta.description override (#7599 follow-up)#7691

Merged
JohnMcLear merged 1 commit intodevelopfrom
fix-7599-description-override
May 7, 2026
Merged

feat(socialMeta): settings.socialMeta.description override (#7599 follow-up)#7691
JohnMcLear merged 1 commit intodevelopfrom
fix-7599-description-override

Conversation

@JohnMcLear
Copy link
Copy Markdown
Member

@JohnMcLear JohnMcLear commented May 7, 2026

Summary

Follow-up to #7635 / issue #7599 addressing @stffen's comment:

  • The OG description had no obvious settings.json knob.
  • Most link-preview crawlers (WhatsApp, Signal, Slack, Telegram, Facebook) don't send Accept-Language, so they always hit the English fallback in the i18n catalog regardless of how many locales translate pad.social.description.

Adds an explicit socialMeta.description setting that wins over the i18n catalog when set as a non-empty string. The catalog stays the default — translatable strings still belong in locale files, per the prior Qodo review on PR #7635 — and operators who want fully localised descriptions still use customLocaleStrings. The new setting is the lever for the crawler-no-Accept-Language case on non-English instances.

Behaviour

socialMeta.description Result
null / missing / empty / whitespace Existing behaviour: i18n catalog → Accept-Language → English fallback
Non-empty string Used verbatim for og:description and twitter:description, regardless of negotiated language
  • Empty/whitespace is treated as unset (would otherwise silently blank previews — a footgun).
  • Override is HTML-escaped via the same path as every other value.
  • og:locale is unaffected; it continues to reflect the negotiated render language.

Files

  • src/node/utils/Settings.ts — new socialMeta: { description: string | null } block, default null, with operator-facing JSDoc.
  • src/node/utils/socialMeta.tsresolveDescriptionWithOverride() checks settings.socialMeta?.description and falls back to the existing i18n resolver.
  • settings.json.template / settings.json.docker — documented next to publicURL; the docker template wires it to ${SOCIAL_META_DESCRIPTION:null}. customLocaleStrings example now spells out the pad.social.description key explicitly so operators find both routes.
  • src/tests/backend/specs/socialMeta-unit.ts — 5 new specs (override wins / null fallback / blank-treated-as-unset / HTML escape / missing block).
  • src/tests/backend/specs/socialMeta.ts — 4 new integration specs covering the same matrix end-to-end through Express.
  • docs/superpowers/specs/2026-04-30-issue-7599-open-graph-metadata-design.md — addendum describing the follow-up.

Test plan

  • pnpm --filter ep_etherpad-lite run test for socialMeta-unit.ts — 31 passing (26 existing + 5 new)
  • pnpm --filter ep_etherpad-lite run test for socialMeta.ts integration — 15 passing (11 existing + 4 new)
  • pnpm ts-check clean for src/node/utils/socialMeta.ts, src/node/utils/Settings.ts, src/tests/backend/specs/socialMeta*.ts (pre-existing failures in plugin_packages/ are unrelated)
  • Verify in a real instance: set socialMeta.description in settings.json, share the URL into Slack/Signal/WhatsApp, confirm the unfurl shows the override

🤖 Generated with Claude Code

…low-up)

Issue #7599 follow-up from @stffen: the OG description has no obvious
settings.json knob and the i18n catalog default is English, but most
preview crawlers (WhatsApp, Signal, Slack, Telegram, Facebook) don't
send Accept-Language and so always hit the English fallback regardless
of how many locale files translate `pad.social.description`.

Keep the i18n catalog as the default source — translatable strings
belong in locale files, per the original Qodo review on PR #7635 — but
add an explicit `socialMeta.description` setting that wins when set as
a non-empty string, regardless of negotiated language. This is the
lever that fixes the crawler case for non-English instances without
re-introducing per-language config in settings.json (operators who
want that still use customLocaleStrings).

- Empty/whitespace overrides are treated as unset (would otherwise
  silently blank the preview).
- Override is HTML-escaped via the same path as every other value.
- og:locale stays language-negotiated; only the description is forced.
- Documented next to publicURL in settings.json.template and
  settings.json.docker (env var SOCIAL_META_DESCRIPTION). The
  customLocaleStrings example now spells out pad.social.description so
  operators discover both routes.

5 new unit specs + 4 new integration specs cover override-wins,
null/missing fallback, blank-treated-as-unset, and HTML-escaping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@qodo-code-review
Copy link
Copy Markdown

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@JohnMcLear JohnMcLear mentioned this pull request May 7, 2026
@qodo-free-for-open-source-projects
Copy link
Copy Markdown

Review Summary by Qodo

Add socialMeta.description setting to override OG preview text

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add settings.socialMeta.description override for Open Graph metadata
• Override wins over i18n catalog regardless of negotiated language
• Solves crawler issue where most preview tools ignore Accept-Language
• Empty/whitespace overrides safely fall back to i18n catalog
• Comprehensive unit and integration tests validate all scenarios
Diagram
flowchart LR
  A["Operator Setting<br/>socialMeta.description"] -->|"Non-empty string"| B["Use Override<br/>Verbatim"]
  A -->|"null/empty/whitespace"| C["Fall Back to<br/>i18n Catalog"]
  B --> D["og:description<br/>twitter:description"]
  C --> E["Accept-Language<br/>Negotiation"]
  E --> D
  D --> F["HTML-Escaped<br/>Meta Tags"]
Loading

Grey Divider

File Changes

1. src/node/utils/Settings.ts ⚙️ Configuration changes +21/-0

Add socialMeta.description type and configuration

• Add socialMeta block with description: string | null field to SettingsType
• Set default value to null to preserve existing i18n behavior
• Add comprehensive JSDoc explaining the override mechanism and use case
• Document that crawlers typically lack Accept-Language headers

src/node/utils/Settings.ts


2. src/node/utils/socialMeta.ts ✨ Enhancement +26/-3

Implement description override resolution logic

• Add socialMeta field to SocialMetaSettings type
• Implement resolveDescriptionWithOverride() function that checks operator setting
• Treat empty/whitespace strings as unset to prevent silently blanking previews
• Update JSDoc to document both override routes (flat setting vs customLocaleStrings)
• Integrate override into renderSocialMeta() call chain

src/node/utils/socialMeta.ts


3. src/tests/backend/specs/socialMeta-unit.ts 🧪 Tests +83/-0

Add unit tests for description override scenarios

• Add 5 new unit test specs for override behavior
• Test override wins regardless of negotiated language
• Test null override falls back to i18n catalog
• Test empty/whitespace treated as unset (safety check)
• Test HTML escaping of operator-controlled override text
• Test missing socialMeta block handled gracefully

src/tests/backend/specs/socialMeta-unit.ts


View more (4)
4. src/tests/backend/specs/socialMeta.ts 🧪 Tests +45/-0

Add integration tests for override through Express

• Add backup/restore of settings.socialMeta in beforeEach/afterEach
• Initialize socialMeta to {description: null} for clean test state
• Add 4 new integration test specs covering end-to-end override behavior
• Test override applied through Express request/response cycle
• Test override beats Accept-Language negotiation
• Test blank override safety (does not silence preview)
• Test HTML escaping in rendered output

src/tests/backend/specs/socialMeta.ts


5. settings.json.template 📝 Documentation +33/-1

Document socialMeta.description in template configuration

• Add socialMeta block with description: null default
• Add detailed JSDoc explaining override use case and crawler behavior
• Document that override applies regardless of negotiated language
• Clarify that null uses i18n catalog with Accept-Language negotiation
• Expand customLocaleStrings example to show pad.social.description key
• Add guidance to prefer socialMeta.description for single-language override

settings.json.template


6. settings.json.docker ⚙️ Configuration changes +13/-0

Add socialMeta.description to Docker configuration

• Add socialMeta block with description wired to SOCIAL_META_DESCRIPTION env var
• Add JSDoc explaining override behavior and crawler use case
• Document that null falls back to i18n catalog
• Provide environment variable pattern for Docker deployments

settings.json.docker


7. docs/superpowers/specs/2026-04-30-issue-7599-open-graph-metadata-design.md 📝 Documentation +35/-0

Document follow-up override feature in design spec

• Add follow-up section documenting the override feature
• Explain the gap: English default and crawler Accept-Language issue
• Document resolution: flat override setting that wins over i18n catalog
• Clarify behavior matrix: null vs non-empty vs empty/whitespace
• Note that og:locale remains language-negotiated
• Reference settings.json.template and customLocaleStrings documentation

docs/superpowers/specs/2026-04-30-issue-7599-open-graph-metadata-design.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown

qodo-free-for-open-source-projects Bot commented May 7, 2026

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0)

Context used

Grey Divider


Remediation recommended

1. Numeric env override ignored 🐞 Bug ≡ Correctness
Description
resolveDescriptionWithOverride() only applies the override when it is a string, but the env-var
substitution logic coerces numeric-looking strings to numbers, so SOCIAL_META_DESCRIPTION="123"
becomes 123 and the override is silently skipped. This can make the new docker-wired override appear
broken for numeric-only values without any warning/logging.
Code

src/node/utils/socialMeta.ts[R168-174]

+const resolveDescriptionWithOverride = (
+  override: string | null | undefined,
+  locales: {[lang: string]: {[key: string]: string}} | undefined,
+  renderLang: string,
+): string => {
+  if (typeof override === 'string' && override.trim() !== '') return override;
+  return resolveDescription(locales, renderLang);
Evidence
The Docker template encourages using SOCIAL_META_DESCRIPTION, but Settings’ env-var replacement
coerces numeric-looking strings to numbers (via coerceValue). Because
resolveDescriptionWithOverride() requires typeof override === 'string', numeric-only env var values
will never be used and will fall back to the i18n catalog instead.

src/node/utils/socialMeta.ts[164-175]
settings.json.docker[128-139]
src/node/utils/Settings.ts[1017-1025]
src/node/utils/Settings.ts[874-896]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`settings.socialMeta.description` is intended to be a string override. When configured via environment variables (as in `settings.json.docker`), the settings loader coerces numeric-only strings to numbers. The new override resolver only accepts strings, so numeric-only values are silently ignored and fall back to i18n.

### Issue Context
- Docker template wires `socialMeta.description` to `${SOCIAL_META_DESCRIPTION:null}`.
- Env-var substitution uses `coerceValue()`, which turns numeric-looking strings into numbers.
- `resolveDescriptionWithOverride()` currently checks `typeof override === 'string'`.

### Fix Focus Areas
- src/node/utils/socialMeta.ts[164-175]

### Suggested fix
Update `resolveDescriptionWithOverride()` to also accept numeric overrides by coercing them to string (e.g., allow `typeof override === 'number'` and return `String(override)` when non-empty after trim). Optionally, log a warning if `override` is neither `string` nor `null/undefined` to surface misconfiguration.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@JohnMcLear JohnMcLear merged commit 90aafb1 into develop May 7, 2026
45 checks passed
@JohnMcLear JohnMcLear deleted the fix-7599-description-override branch May 7, 2026 10:12
@JohnMcLear
Copy link
Copy Markdown
Member Author

Action follow-up to Qodo's bug-find: numeric/boolean env-var coercion fix in PR #7692.

JohnMcLear added a commit that referenced this pull request May 7, 2026
…7692)

* fix(socialMeta): coerced numeric/boolean override silently dropped

Qodo flagged on PR #7691: Settings.coerceValue() turns numeric-looking env
vars into numbers and "true"/"false" into booleans, so e.g.
SOCIAL_META_DESCRIPTION="2026" arrives at the resolver as the number 2026.
The previous resolver gated on `typeof override === 'string'`, so it
silently fell back to the i18n catalog with no warning — the operator's
docker config would appear broken.

Accept string|number|boolean and stringify before the empty-check; null /
undefined / unsupported types still fall through to the catalog. Two new
unit specs cover the numeric and boolean coercion paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(socialMeta): widen description type to match coerced runtime

Action Qodo bug-find: SettingsType.socialMeta.description and
SocialMetaSettings.description still claimed `string | null`, but
Settings.coerceValue() can produce number|boolean from env-var-driven
config — the previous resolver fix was correct at runtime but the type
mismatch forced `as unknown as string` casts in the new tests.

Widen both declared types to `string | number | boolean | null` to match
runtime reality, drop the typeof string|number|boolean guards in the
resolver (the union now narrows automatically) and remove the test casts.
Behaviour is unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant