Advanced

Custom Sidebar Widgets

Put a map, a chart, a booking form, or a countdown in the right sidebar. Build it once from a Ghost page, or give a single post its own.

Enova's right sidebar ships with a fixed set of cards: featured posts, recent posts, newsletter signup, and so on. Custom widgets are the escape hatch. They let you put anything at all in that column (an interactive map, a chart, a booking widget, a countdown, an affiliate panel), styled to match the rest of the sidebar automatically.

There are two ways to add one, and you can use both together:

Built fromBest for
A Ghost pageThe same widget on many posts. Written in the editor, no code needed.
A post's Code injectionA widget unique to one post: a map of this hike, a chart for this article.

A widget everyone sees

Any published page tagged #sidebar-widget becomes a card in the sidebar. The page title becomes the card's heading, and the page body becomes its content.

Create a page. In Ghost Admin, add a new page and give it any title you like. That title is the heading readers see on the card, so make it short: Our Shop, This Week, Support Us.

Tag it #sidebar-widget. Open Page settings and add the tag. The # keeps it internal, so it never shows up on your tag archives.

Write the body. Anything you can put in the Ghost editor works: text, images, buttons, embeds, and HTML cards. Keep it compact: the column is about 280 pixels wide.

Publish. The card appears in the sidebar of your posts and pages straight away.

Widget page in Ghost

A page tagged #sidebar-widget in the Ghost editor

Widget in the sidebar

The same page rendered as a card in the right sidebar

Leave it out of your navigation

A widget page is still a normal Ghost page, reachable at its own URL. Give it a slug you don't mind being public, and don't add it to your navigation menu.

Choosing where it appears

By default a widget page shows on posts and pages. Add one more tag to change that:

Tag on the widget pageWhere the card appears
(no scope tag)Posts and pages
#widget-posts-onlyPosts only
#widget-pages-onlyPages only
#widget-site-wideEverywhere, including the homepage, tag pages, and author pages

Each widget page carries its own scope, so a map can be posts-only while a shop promo runs site-wide.

Several widgets, and their order

You can have up to three widget pages showing at once. Any beyond three are ignored, so keep the set small and deliberate.

They stack in this order:

  1. Pages you have featured (the star in Page settings) come first. This is your pin-to-top.
  2. Everything else follows by publish date, oldest first.

Publishing a new widget adds it to the bottom without shuffling what's already there. To move one up or down, change its publish date. To pin one to the top, feature it.

Hiding widgets on one post

Tag a post or page #hide-sidebar-widgets and none of the page-driven widgets appear on it. Widgets that post defines for itself still do.


A widget for one post

To give a single post its own widget, put a marker in that post's Code injection. This keeps the code out of your article body, so it's never sent out with your email newsletter.

Open the post, then Post settings → Code injection.

Paste your widget into the Post footer field, wrapped in the marker below.

Update the post. The card appears in the sidebar.

<template data-enova-widget data-widget-title="Where this happened">
  <!-- anything you like goes here -->
</template>

data-widget-title is the card's heading. Leave it out and the card has no heading, which suits a widget that's purely visual.

data-widget-height is optional and sets the card's placeholder height while the embed loads, so the sidebar doesn't move when it arrives. Use the embed's real height with a unit: data-widget-height="300px" for the map above. Plain lengths only (px, rem, em); anything else is ignored and the card falls back to a standard placeholder.

You can add more than one marker. Each becomes its own card, in the order you wrote them, below any page-driven widgets.

A worked example: a map of this post's location

In Google Maps, find the place, choose Share → Embed a map, and copy the HTML. Wrap it in the marker and paste the result into Post settings → Code injection → Post footer:

<template data-enova-widget data-widget-title="Where this happened">
    <iframe
    src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d49789.69204448611!2d-9.20093522194235!3d38.744072219617834!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0xd19331a61e4f33b%3A0x400ebbde49036d0!2sLisbon%2C%20Portugal!5e0!3m2!1sen!2sau!4v1785664955945!5m2!1sen!2sau"
    width="400"
    height="300"
    style="border:0;"
    title="Map of Lisbon, Portugal"
    allowfullscreen=""
    loading="lazy"
    referrerpolicy="strict-origin-when-cross-origin"></iframe>
</template>

That's the entire widget. No account, no API key, and no JavaScript of your own.

Leave the width="400" exactly as Google gives it to you. Enova resizes embeds to fit the column, so the number doesn't matter. The height is yours to choose: 300 is a good starting point, and 200 keeps the card compact.

One line worth adding to whatever embed code you paste: a title on the <iframe> (like the "Map of Lisbon, Portugal" above). It's what a screen reader announces for the frame, and accessibility checkers flag embeds without one.

Marker in Code injection

The template marker pasted into a post's Code injection footer

Map in the sidebar

The embedded map rendered as a sidebar card

Paste embed code exactly as you were given it

The same wrapper works for any embed you're handed: a YouTube video, a Spotify player, an Airtable form, a booking calendar. Snippets that come with their own <script> tags, such as a Mapbox map or a charting library, work the same way. Write the snippet as you would for any web page: if it works pasted into an ordinary page, it works here.

When the widget loads

Custom widgets are built for heavy content, so they're loaded carefully:

  • A per-post widget loads only when the reader scrolls near it. On phones and small laptops, where the sidebar is a panel the reader opens, it doesn't load at all until they open that panel. A map costs nothing to a reader who never looks at it.
  • A page-driven widget is part of the page from the start, so its text is visible to search engines. If that widget is heavy (a map, a chart, a live feed), tag its page #widget-lazy and it will wait for the reader too.

What `#widget-lazy` trades away

A lazy widget isn't in the page's initial HTML, so its content isn't indexed by search engines and doesn't appear for readers with JavaScript turned off (the card is hidden entirely rather than showing an empty box). Use it for embeds and interactive tools, not for text you want found.

Sizing and styling

The sidebar column is roughly 280 pixels wide inside the card padding, and narrower still inside the mobile panel. Enova handles the common problems for you:

  • Embeds and <iframe> elements are resized to fit the card, whatever width the snippet asks for.
  • Images, video, and canvas elements never overflow.
  • Text, headings, lists, and links pick up the same type styles as the other sidebar cards.

Height is yours to set. An <iframe> keeps its height attribute, so leave Google's 300 or change it to taste. Anything that draws itself, like a map or chart, needs a height on the element it's given (style="height:260px" is a good starting point), because a container with no height is invisible.

To style a widget further, use the selector hooks in the hook reference: [data-component="sidebar-widget"][data-variant="custom"] for the card, and [data-part="body"] for its content.

Turning widgets off

What you wantHow
Remove one widget everywhereUnpublish its page, or remove the #sidebar-widget tag
Hide page-driven widgets on one postTag that post #hide-sidebar-widgets
Remove a post's own widgetDelete the <template> from that post's Code injection

Good to know

  • Widget pages are ordinary pages. They keep their own URL and appear in your sitemap, the same as the house-ad, sponsors, and donate pages. Keep them out of your navigation.
  • The card heading comes from the page title, so a page called Untitled gets an odd-looking card. Per-post widgets have no heading at all unless you set data-widget-title.
  • Nothing renders when there's nothing to show. An unpublished page, an empty body, or a post with no marker produces no card and no empty box.
  • Widgets stack below the donate card and above the featured posts, so a post-specific widget sits high in the column where readers see it.
  • Start headings inside a widget body at heading 3 or lower. The card's own heading is a level-3 heading, so a level-2 in the body puts the page's heading outline out of order, which accessibility checkers flag.
  • Give heavy content its height up front — the height attribute on an iframe, or an inline height on a map's container. A widget that loads late and then grows pushes the cards below it down mid-read; a declared height means the space is simply there waiting.
  • If you need code elsewhere on the page to run once a widget appears, listen for the enova:widget-mounted event. It fires on the card and bubbles, with the card in event.detail.card.

Troubleshooting

The card isn't appearing at all

Check that the page is published (not draft or scheduled), that its tag is exactly #sidebar-widget, and that the body isn't empty. Then check the scope: a page tagged #widget-pages-only will never show on a post. Empty or unpublished pages render nothing by design.

One of my widget pages is missing

Only three show at once. Unpublish one you no longer need, or feature the page you want to promote so it sorts to the top of the three.

My per-post widget isn't showing

It must live in Post settings → Code injection, not in the article body, and be wrapped in <template data-enova-widget>…</template>. Also check the right sidebar is switched on for posts in Design → Site wide.

My embed is the wrong height, or has no height at all

Height is yours to set; only width is handled for you. An <iframe> takes its height from the height attribute, and anything that draws itself needs a height on the element it's given. With no height it collapses to nothing.

I see an empty box where the embed should be

The frame is there but the content never arrived. Open your browser's developer console on that post and read the errors. For an <iframe>, the usual cause is that the other site refuses to be embedded. You'll see a message mentioning X-Frame-Options or frame-ancestors, and the only fix is to use that service's official embed code. For a widget with its own scripts, look instead for a rejected API key, or a data file that's missing or blocked. Files you uploaded to Ghost live under /content/files/.

My script runs before the library it needs

Scripts inside a widget run top to bottom, and a <script src="…"> finishes loading before the next line runs, so the usual order works. If you load a library yourself with document.createElement('script'), use that script's onload to start your setup.