How to add customer HTML after "post_1"

I’d like to add a customer banner (for affiliate purposes) inside of my Posts. Specifically, I’d like to add this block of (example) HTML right after <article id="post_1...".

<div id="custom-ad">
    <a href="example.com">
        <img src="https://picsum.photos/id/74/750/90">
    </a>
</div>

Therefore, looking something like this:

I’ve had limited success with CSS :after. And so was wondering if this is something that could be done from inside the </HEAD> using a <script> like this example.

EDIT: Having played around with it a little more, it seems cleaner to insert the banner at the end of <div class="topic-map"> instead.:

<div class="topic-map">
    <section class="map map-collapsed">...</section>
    CUSTOM HTML HERE
</div>
1 Like

Posts are widgets which means what you’re trying to do will involve a bit more work that just adding html.

Discourse themes have the ability to decorate widgets so you can leverage on that.

Decorating a widget is explained in the link above so let’s focus on what you’re trying to do - add markup after the first post in every topic.

Start by adding the markup to all posts. So something like this

<script type="text/discourse-plugin" version="0.8">
api.decorateWidget("post:after", helper => {
  return helper.h("div", "test text");
});
</script>

to the header section of your theme. That should be enough to add “test text” below every post.

Let’s breakdown the script above

api.decorateWidget("post:after", helper => {

calls the decorateWidget method, the target widget being post and the target location being after. So after a post widget.

helper is a built in helper that gives you access to a bunch of stuff that I will explain later

return helper.h("div", "test text")

This is the desired additional markup you want to add. You might notice that there’s no html in there and that’s because discourse widgets emit virtual nodes and not raw html.

Explaining what virtual nodes are or how the syntax works is outside of the scope for this topic, so I’ll skip that. I’ve added a note to write a howto for authoring virtual nodes but here are a couple of example for now

helper.h("div", "test text")

renders

<div>test text</div>

and

return helper.h("div#custom-ad", [
  helper.h(
    "a.custom-ad-link",
    { href: "example.com" },
    helper.h("img", { src: "https://picsum.photos/id/74/750/90" })
  )
]);

will render

<div id="custom-ad">
  <a href="example.com" class="custom-ad-link">
    <img src="https://picsum.photos/id/74/750/90">
  </a>
</div>

In a nutshell, a node looks like this

helper.h(selector, {properties}, children)

I’ll explain this more in the virtual node howto.

So, now you have the nodes ready, you would just need to add the whole script to the header section of your theme, so something like this

<script type="text/discourse-plugin" version="0.8">
  api.decorateWidget("post:after", helper => {
    return helper.h("div#custom-ad", [
      helper.h(
        "a.custom-ad-link",
        { href: "example.com" },
        helper.h("img", { src: "https://picsum.photos/id/74/750/90" })
      )
    ]);
  });
</script>

However, there’s still a problem here, the ad will be inserted below every post in the stream, which is not ideal.

This is where the helper comes in handy, the post attributes are passed to the helper, so you can do a quick

console.log(helper)

You’ll be able to see all the post attributes available for you to work with.

those are just examples, there’s more in there.

Luckily, the firstPost attribute is available for us here, so all you have left is to feed that into a conditional that will only render the ad markup if it’s indeed the first post, otherwise nothing happens. So something like this:

<script type="text/discourse-plugin" version="0.8">
api.decorateWidget("post:after", helper => {
  const firstPost = helper.attrs.firstPost;
  const h = helper.h;
  if (firstPost) {
    return h("div#custom-ad", [
      h(
        "a.custom-ad-link",
        { href: "example.com" },
        h("img", { src: "https://picsum.photos/id/74/750/90" })
      )
    ]);
  }
});
</script>

and that will insert your ad only after the first post. One additional thing to do here is to add a height to the image, otherwise it will cause jitter as it loads. So like I briefly touched on above, the height attribute for the image tag is a property, so you’re need to add it next to the src

With all of that put together, here’s the final code for what you’re trying to achieve

<script type="text/discourse-plugin" version="0.8">
api.decorateWidget("post:after", helper => {
  const firstPost = helper.attrs.firstPost;
  const h = helper.h;
  if (firstPost) {
    return h("div#custom-ad", [
      h(
        "a.custom-ad-link",
        { href: "example.com" },
        h("img", { src: "https://picsum.photos/id/74/750/90", height: "90" })
      )
    ]);
  }
});
</script>

One last note I want to highlight is that you can actually use raw html if virtual nodes prove to be to tricky, but it’s not recommended and it’s much better to use virtual nodes. So the same script with raw html would like like this

<script type="text/discourse-plugin" version="0.8">
  const RawHtml = require("discourse/widgets/raw-html").default;
  api.decorateWidget("post:after", helper => {
    const firstPost = helper.attrs.firstPost;
    if (firstPost) {
      return [
        new RawHtml({
          html: `<div id="custom-ad">
                   <a href="example.com">
                     <img src="https://picsum.photos/id/74/750/90" height="90">
                   </a>
                 </div>`
        })
      ];
    }
  });
</script>

but again, this is not recommended.

25 Likes

I always appreciate the solutions that explain how to get from A-Z rather than just an answer. Thank you.

3 Likes

I liked this post, but I just want to add: This is a fanatic explanation. I really appreciate posts like this.

4 Likes

That’s because @johani is :fire: :hot_pepper: :muscle:

7 Likes

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.