{"record":{"id":"c0629af7dbe7e80b","repo":"TryGhost/Ghost","slug":"your-username-is-not-a-valid-bluesky-username","errorCode":null,"errorMessage":"Your Username is not a valid Bluesky Username","messagePattern":"Your Username is not a valid Bluesky Username","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"apps/admin/src/settings/app/utils/social-urls/bluesky.ts","lineNumber":27,"sourceCode":"    // domain handle: requires a dot, max 191 chars\n    // (the lookahead does the length check because + is unbounded)\n    /^(?=.{1,191}$)[a-zA-Z0-9._-]+\\.[a-zA-Z0-9._-]+$/\n];\n\nconst bluesky = createPlatformValidator({\n    domains: ['bsky.app'],\n    www: false,\n    pathTypes: [\n        // bsky.app/profile/@username is a common paste (users type Bluesky\n        // handles with a leading @ out of habit); the @ is decorative here,\n        // not a marker for a competing path type, so it's still stripped\n        {urlPrefix: 'profile/', storagePrefix: '', tolerateLeadingAt: true, rule: {patterns: BLUESKY_USERNAME_PATTERNS}}\n    ],\n    // DIDs are case-insensitive identifiers, canonically lowercase\n    transformUsername: username => (/^did:plc:/i.test(username) ? username.toLowerCase() : username),\n    errors: {\n        invalidUrl: 'The URL must be in a format like https://bsky.app/profile/yourUsername',\n        invalidUsername: 'Your Username is not a valid Bluesky Username'\n    }\n});\n\nexport const validateBlueskyUrl = bluesky.validate;\nexport const blueskyHandleToUrl = bluesky.handleToUrl;\nexport const blueskyUrlToHandle = bluesky.urlToHandle;\n","sourceCodeStart":9,"sourceCodeEnd":34,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/admin/src/settings/app/utils/social-urls/bluesky.ts#L9-L34","documentation":"Thrown by checkUsername inside createPlatformValidator when the Bluesky username extracted from a URL or handle fails to match any of the three accepted identifier shapes: a DID (did:plc: + 24 Base32 chars), a short handle (1-15 chars of [a-zA-Z0-9._]), or a domain handle (dot-separated, up to 191 chars). The message is the platform definition's errors.invalidUsername string. It signals that the input parsed as a Bluesky profile URL but the identifier portion is malformed.","triggerScenarios":"Calling validateBlueskyUrl, blueskyHandleToUrl, or blueskyUrlToHandle with input whose path segment after 'profile/' is not a valid DID, short handle, or domain handle. Examples: 'https://bsky.app/profile/user name' (space), 'https://bsky.app/profile/-bad' (leading dash in a non-domain handle), 'https://bsky.app/profile/' (empty, caught earlier as invalidUrl), 'https://bsky.app/profile/a.b.c.d.e.f.g' where the segment exceeds 15 chars and has no dot so it matches no pattern, or a handle like 'did:plc:XYZ' where XYZ contains invalid Base32 characters.","commonSituations":"User pastes a Bluesky URL with a trailing query string or fragment that leaks into the username (caught by the ?# check first), copies a profile URL for a suspended/renamed account, or types a handle with characters Bluesky forbids. Developers migrating from a system that stored handles with a leading '@' will usually pass because tolerateLeadingAt strips it, but handles stored with other decorations (slashes, spaces) will fail.","solutions":["Inspect the extracted username: it must match /^did:plc:[a-z2-7]{24}$/, /^[a-zA-Z0-9._]{1,15}$/, or /^(?=.{1,191}$)[a-zA-Z0-9._-]+\\.[a-zA-Z0-9._-]+$/.","If the input is a URL, ensure only the single path segment after profile/ is the identifier — remove trailing /post/... or /lists/... paths before validating, or rely on the pipeline which takes only the first segment.","If storing DIDs, lowercase them; the transformUsername only lowercases did:plc: prefixed values so a mixed-case short handle is accepted but a DID with uppercase after the prefix must already be lowercase or it will be lowercased automatically.","Strip any leading '@' before calling handleToUrl if you bypass validate, though tolerateLeadingAt already handles bsky.app/profile/@user pastes."],"exampleFix":"// before (fails — space in handle)\nvalidateBlueskyUrl('https://bsky.app/profile/my handle')\n// after\nvalidateBlueskyUrl('https://bsky.app/profile/myhandle')","handlingStrategy":"validation","validationCode":"const BLUESKY_PATTERNS = [\n  /^did:plc:[a-z2-7]{24}$/,\n  /^[a-zA-Z0-9._]{1,15}$/,\n  /^(?=.{1,191}$)[a-zA-Z0-9._-]+\\.[a-zA-Z0-9._-]+$/\n];\nfunction isValidBlueskyHandle(handle: string): boolean {\n  return BLUESKY_PATTERNS.some(p => p.test(handle));\n}\n// run before validateBlueskyUrl on the extracted segment","typeGuard":"function isBlueskyHandle(value: unknown): value is string {\n  return typeof value === 'string' && [\n    /^did:plc:[a-z2-7]{24}$/,\n    /^[a-zA-Z0-9._]{1,15}$/,\n    /^(?=.{1,191}$)[a-zA-Z0-9._-]+\\.[a-zA-Z0-9._-]+$/\n  ].some(p => p.test(value));\n}","tryCatchPattern":"try {\n  const url = validateBlueskyUrl(input);\n} catch (e) {\n  if (e instanceof Error && /not a valid Bluesky Username/.test(e.message)) {\n    // show field-level validation error\n  }\n  throw e;\n}","preventionTips":["Validate the handle format before calling validateBlueskyUrl using the same three patterns.","Pre-strip query strings and fragments from pasted URLs before validation.","Use blueskyUrlToHandle (which returns null instead of throwing) when you only need to extract, not canonicalize."],"tags":["validation","social-urls","bluesky","input-validation"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}