When I started publishing posts on Ghost, I would start sharing them on my Twitter feed.  Immediately I noticed that unless I specifically set an Image on the blog post or specifically set a Twitter Card Image, it would show up with a blank Twitter Card image.

And I'm not the only one...

See that blank "missing image" icon?

What bothered me is that I want to publish posts with as little effort as possible.  I want to be able to quickly write a post and have it automatically get shared to Twitter and not have to worry about setting specific settings/configs that would take time.

I went down a rabbit hole, because I assumed there was something in the Ghost Theme that I could tweak to make this happen.  Unfortunately, it turns out that the logic returning the meta fields in a page are generated dynamically in the source code of Ghost itself.

When the home page is loaded, it will check to see if a Twitter Image is set, and if not default to the Cover Image.

if (_.includes(context, 'home')) {
        const imgUrl = settingsCache.get('twitter_image') || settingsCache.get('cover_image');
        return (imgUrl && urlUtils.relativeToAbsolute(imgUrl)) || null;
    }

core/frontend/meta/twitter_image.js

Unfortunately, for me, the Post logic just checks for a Post specific Twitter Image or a Post specific Feature Image and returns those.  However to get the behavior I wanted, I just hacked it to use the same logic as the Home Page if the Twitter or Featured Image are not set.

if (_.includes(context, 'post') || _.includes(context, 'page') || _.includes(context, 'amp')) {
        if (contextObject.twitter_image) {
            return urlUtils.relativeToAbsolute(contextObject.twitter_image);
        } else if (contextObject.feature_image) {
            return urlUtils.relativeToAbsolute(contextObject.feature_image);
        }
        
        // Fall back to the Twitter or Cover Image
        const imgUrl = settingsCache.get('twitter_image') || settingsCache.get('cover_image');
        return (imgUrl && urlUtils.relativeToAbsolute(imgUrl)) || null;
    }

core/frontend/meta/twitter_image.js

Finally, you can do the same logic in the OG Meta tags for Facebook, LinkedIn, etc by editing the OG Image file with the same fallback.

/core/frontend/meta/og_image.js

I may end up submitting a PR to Ghost for this but my local hacked version seems to be working for now.  Try sharing on Twitter to see the default image working or use the following tool that I found handy to preview what it would look like:

"Social Share Preview" in Chrome.