TryGhost/Ghost · warning

The URL must be in a format like @username@instance.tld or h

Error message

The URL must be in a format like @username@instance.tld or https://instance.tld/@username or https://website.com/@username@instance.tld

What it means

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.

Source

Thrown at apps/admin/src/settings/app/utils/social-urls/mastodon.ts:19

import validator from 'validator';

// Validates and normalizes Mastodon URLs
export function validateMastodonUrl(newUrl: string) {
    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';
    if (!newUrl) {
        return '';
    }

    let normalizedUrl = newUrl;

    // Remove https:// or http:// if present
    normalizedUrl = normalizedUrl.replace(/^https?:\/\//, '');

    // Check if it's in @username@instance format
    if (normalizedUrl.match(/^@[^@]+@[^/]+$/)) {
        const [username, instance] = normalizedUrl.split('@').slice(1);
        if (!validator.isFQDN(instance)) {
            throw new Error(errMessage);
        }
        return `https://${instance}/@${username}`;
    }

    // Check if it's in instance/@username format
    if (normalizedUrl.match(/^[^/]+\.[^/]+\/@[^/]+(@[^/]+)?$/)) {
        const [instance, rest] = normalizedUrl.split('/@');
        if (!validator.isFQDN(instance)) {
            throw new Error(errMessage);
        }

        // If there's a second @, validate that part too
        if (rest.includes('@')) {
            const [, userInstance] = rest.split('@');
            if (!validator.isFQDN(userInstance)) {
                throw new Error(errMessage);
            }
        }

View on GitHub (pinned to 47d8b0e2ad)

Solutions

  1. Match one of the documented formats: '@username@instance.tld', 'https://instance.tld/@username', or 'https://website.com/@username@instance.tld'.
  2. Ensure the instance portion is a valid fully-qualified domain (validator.isFQDN): real TLD, no underscores, public DNS resolvable shape.
  3. Strip trailing slashes, query strings, and fragments before validating — the regexes are strict about the whole string matching.
  4. If building integrations, prefer the non-throwing mastodonUrlToHandle to test a URL first (returns null instead of throwing).

Example fix

// before — throws on malformed federated handle
const normalized = validateMastodonUrl(input);

// after — non-throwing pre-check, then validate
import {mastodonUrlToHandle} from './mastodon';
const trimmed = input.trim();
if (!trimmed) {
    setNormalized('');
} else if (mastodonUrlToHandle(trimmed) === null && !/^@[^@]+@[^/]+$/.test(trimmed.replace(/^https?:\/\//, ''))) {
    setFieldError('mastodon', 'Use @username@instance.tld or https://instance.tld/@username');
} else {
    try {
        setNormalized(validateMastodonUrl(trimmed));
    } catch (e) {
        setFieldError('mastodon', e instanceof Error ? e.message : 'Invalid Mastodon URL');
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Non-throwing pre-check using the URL→handle extractor
import {mastodonUrlToHandle} from './mastodon';
function looksLikeMastodon(input: string): boolean {
    if (!input) return true; // empty is allowed (returns '')
    const stripped = input.trim().replace(/^https?:\/\//, '');
    if (/^@[^@]+@[^/]+$/.test(stripped)) return true; // @user@instance handle form
    return mastodonUrlToHandle(input) !== null;     // instance/@user URL form
}

Type guard

null

Try / catch

try {
    const normalized = validateMastodonUrl(input.trim());
} catch (e) {
    setFieldError('mastodon', e instanceof Error ? e.message : 'Invalid Mastodon URL');
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of TryGhost/Ghost@47d8b0e2ad (2026-08-13). Data as JSON: /api/errors/8ef8ea266db0ea57. Report an issue: GitHub.