The featured post template is the flagship template in the Kinja ecosystem- it provides editors with a suite of tools to present their long-form journalism in visually stunning fashion. On the Kinja tech team, we want these posts to shine as brightly as possible, while still providing opportunity for monetization. The solution we’ve landed on is to place ads in the body of these posts.
Editors traditionally have had no control over where the ads will appear. Ads are inserted algorithmically after posts are published, based on proprietary logic. This means the flow of an article may occasionally be interrupted by an ad div, most often if the article makes use of a floated inline element, like an image.
To solve this problem, we decided to give users the ability to move in-post ads.
This tool facilitates moving ads up and down in a post, and even deleting ads with the proper user permissions. This post chronicles some of the challenges we faced while building this feature, and explains our thinking behind the choices we made.
Challenges We Faced
- No way to store ad placements using the current post model. When we started development on this feature, we weren’t storing any information about ads in posts. Ads were inserted programmatically at render time, and our only recourse to change ad placement for one post was to update the logic for all posts.
- Maintain existing off-platform version of featured posts (AMP, RSS, et al). Advertisements are presented differently on those platforms, so we couldn’t tie our ad logic too closely to the post itself without making those platforms aware of how we deal with ads.
- Keep frontend and backend logic in sync. In order for the frontend to accurately save and reset the ad slot positions, the placement logic had to be simple enough to implement and maintain in both the frontend and backend code.
- Support legacy featured posts. There were 869 featured posts published on Kinja in 2017 alone, and the featured post template has existed for several years. We wanted out-of-the-box compatibility with legacy posts, with no migration effort required.
Backend Logic - How Will We Store Ad Placements?
We brainstormed two possible approaches to storing ad placements on the backend.
First Approach: Ad Blocknodes
We could store in-post ads as blocknodes in the body of each post. Kinja posts are stored as an array of blocknodes, each of which represents an atom of the post’s composition. A blocknode might be a paragraph, an image, or a slideshow; it’s the main building block of Kinja posts. We thought we could store ads as blocknodes and render them the same way we render other blocknodes. There ended up being a few notable drawbacks to this approach.
In order to store ads as blocknodes, they’d have to be manipulated in the Kinja editor, where blocknodes are created and ultimately ingested by the Core API service. This would mean rendering ads in the editor while authors were working on posts, which they could find disruptive. Our goal was to build a tool that allowed writers to loop back after they’re done with a draft and make any necessary changes to resolve ad placements as a last step before publishing. We hoped to achieve a set-it-and-forget-it user experience with feature, and ad blocknodes were more like watching a pot boil.
From a dogmatic engineering perspective, we thought ad blocknodes violated the separation of concerns principle. If we stored ads (presentation logic) in the post model with other blocknodes (raw post data) we’d be tied to this presentation in perpetuity. If, a year from now, Kinja decided to abandon in-post ads in favor of another ad scheme, we’d have to migrate the data from every featured post. Not to mention each platform we shared our posts with would have to be made aware of a new type of blocknode and have logic built-in to ignore them. This approach also made supporting legacy content more complicated, as backdated featured posts wouldn’t have ad blocknodes and would probably have to be migrated to the new format.
Second Approach: AdSettings Property
Since a Kinja article is ultimately just an array of blocknodes, instead of storing the ads themselves, we could to store the indexes at which we wanted ads to appear. If we wanted the first ad to appear after six blocknodes, we could just store the number six in an array. At render time, the controller could simply check for the presence of this array, and either place the ads at the given indexes or revert to the hard-coded ad placement logic.
This is the approach we ended up taking - we created an optional property on the post model called adSettings.
An article with three inpost ads after the sixth, twelfth, and eighteenth nodes would have an adSettings property of [6,12,18]. If an author moved the second ad down a node to move it out of the way of a floated element, the array would be updated to [6,13,18]. Easy.
An added benefit of storing ad placements instead of the ads themselves is that it made it very easy to delete an ad- you just remove a number from the array! Also, since our ad placement logic works with or without the presence of an adSettings field, legacy posts would be unaffected by our change, but could still benefit from it (in fact, we have seen significant usage of this feature on high-traffic, evergreen content)!
Once we knew how we were going to store ad placements on the backend, exposing a useful UI for authors was pretty simple. We just had to provide functions for moving and saving ads.
Saving Ad Placement
We made it so saving was as easy as clicking a save button, which invokes the saveAds function. This function simply iterates over the post body and stores every index where it finds an ad in an array. All we have to send to the backend is this array of indices, which the Core API service checks for length (only top-level users have the ability to delete ads, so normally the array’s length shouldn’t change) and then updates the record with the new adSettings field. For a UX flourish, we quickly refresh the page with a cachebusting url param, so users can see their changes right away.
We’re proud to have provided a way to monetize featured posts without sacrificing a premium reading experience. Usage is still evolving and we have gotten positive feedback from editors on the general look and feel of the feature. In the future, we hope to continue to implement features that bridge the gap between editorial concerns and the tech team!