{"record":{"id":"8ef8ea266db0ea57","repo":"TryGhost/Ghost","slug":"the-url-must-be-in-a-format-like-username-instanc","errorCode":null,"errorMessage":"The URL must be in a format like @username@instance.tld or https://instance.tld/@username or https://website.com/@username@instance.tld","messagePattern":"The URL must be in a format like @username@instance\\.tld or https://instance\\.tld/@username or https://website\\.com/@username@instance\\.tld","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"apps/admin/src/settings/app/utils/social-urls/mastodon.ts","lineNumber":19,"sourceCode":"import validator from 'validator';\n\n// Validates and normalizes Mastodon URLs\nexport function validateMastodonUrl(newUrl: string) {\n    const errMessage = 'The URL must be in a format like @username@instance.tld or https://instance.tld/@username or https://website.com/@username@instance.tld';\n    if (!newUrl) {\n        return '';\n    }\n\n    let normalizedUrl = newUrl;\n\n    // Remove https:// or http:// if present\n    normalizedUrl = normalizedUrl.replace(/^https?:\\/\\//, '');\n\n    // Check if it's in @username@instance format\n    if (normalizedUrl.match(/^@[^@]+@[^/]+$/)) {\n        const [username, instance] = normalizedUrl.split('@').slice(1);\n        if (!validator.isFQDN(instance)) {\n            throw new Error(errMessage);\n        }\n        return `https://${instance}/@${username}`;\n    }\n\n    // Check if it's in instance/@username format\n    if (normalizedUrl.match(/^[^/]+\\.[^/]+\\/@[^/]+(@[^/]+)?$/)) {\n        const [instance, rest] = normalizedUrl.split('/@');\n        if (!validator.isFQDN(instance)) {\n            throw new Error(errMessage);\n        }\n\n        // If there's a second @, validate that part too\n        if (rest.includes('@')) {\n            const [, userInstance] = rest.split('@');\n            if (!validator.isFQDN(userInstance)) {\n                throw new Error(errMessage);\n            }\n        }","sourceCodeStart":1,"sourceCodeEnd":37,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/apps/admin/src/settings/app/utils/social-urls/mastodon.ts#L1-L37","documentation":"Thrown by validateMastodonUrl when input can't be recognised as either the @username@instance handle form or the instance.tld/@username URL form, OR when it matches one of those forms but the instance portion fails validator.isFQDN. Mastodon is federated so there's no fixed domain — validation hinges on the syntactic shape plus a valid fully-qualified domain for the instance.","triggerScenarios":"Input that after stripping the protocol: (a) doesn't match ^@[^@]+@[^/]+$ nor ^[^/]+\\.[^/]+\\/@[^/]+(@[^/]+)?$ — e.g. 'mastodon.social', '@user', 'example.com/user' (missing @); (b) matches @username@instance but the instance isn't an FQDN (e.g. '@user@not_a_domain', '@user@localhost'); (c) matches instance/@username but the instance portion fails FQDN; (d) the second @ in a website.com/@username@instance form has an invalid instance.","commonSituations":"User pastes a bare username without the @; user enters only the instance; instance uses an IP/localhost/invalid TLD; user typed a Profile URL from a non-Mastodon service; copy-paste included trailing slashes or query strings that break the strict regex.","solutions":["Match one of the documented formats: '@username@instance.tld', 'https://instance.tld/@username', or 'https://website.com/@username@instance.tld'.","Ensure the instance portion is a valid fully-qualified domain (validator.isFQDN): real TLD, no underscores, public DNS resolvable shape.","Strip trailing slashes, query strings, and fragments before validating — the regexes are strict about the whole string matching.","If building integrations, prefer the non-throwing mastodonUrlToHandle to test a URL first (returns null instead of throwing)."],"exampleFix":"// before — throws on malformed federated handle\nconst normalized = validateMastodonUrl(input);\n\n// after — non-throwing pre-check, then validate\nimport {mastodonUrlToHandle} from './mastodon';\nconst trimmed = input.trim();\nif (!trimmed) {\n    setNormalized('');\n} else if (mastodonUrlToHandle(trimmed) === null && !/^@[^@]+@[^/]+$/.test(trimmed.replace(/^https?:\\/\\//, ''))) {\n    setFieldError('mastodon', 'Use @username@instance.tld or https://instance.tld/@username');\n} else {\n    try {\n        setNormalized(validateMastodonUrl(trimmed));\n    } catch (e) {\n        setFieldError('mastodon', e instanceof Error ? e.message : 'Invalid Mastodon URL');\n    }\n}","handlingStrategy":"try-catch","validationCode":"// Non-throwing pre-check using the URL→handle extractor\nimport {mastodonUrlToHandle} from './mastodon';\nfunction looksLikeMastodon(input: string): boolean {\n    if (!input) return true; // empty is allowed (returns '')\n    const stripped = input.trim().replace(/^https?:\\/\\//, '');\n    if (/^@[^@]+@[^/]+$/.test(stripped)) return true; // @user@instance handle form\n    return mastodonUrlToHandle(input) !== null;     // instance/@user URL form\n}","typeGuard":"null","tryCatchPattern":"try {\n    const normalized = validateMastodonUrl(input.trim());\n} catch (e) {\n    setFieldError('mastodon', e instanceof Error ? e.message : 'Invalid Mastodon URL');\n}","preventionTips":["Match the documented formats exactly: @username@instance.tld, https://instance.tld/@username, https://website.com/@username@instance.tld.","Ensure the instance portion is a valid FQDN (validator.isFQDN).","Strip trailing slashes and query strings before validating — the regexes match the whole string.","Use mastodonUrlToHandle for non-throwing URL checks."],"tags":["mastodon","social-urls","validation","federated"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}