Backstage at the Lab: Building Dynamic Rich Twitter Cards for Catalyst Proposals

From our “How we built this” series

Lido Nation is known for Project Catalyst Explorer tools, for our Swahili Learn to Earn Program, for our Every Epoch “learn and earn” program and for providing blockchain education in simple English, Swahili and Spanish. What is lesser known is that we have an apprenticeship blockchain lab in Kenya. This series is a diary of how some of the tools we build at the lab come to life.

Project Catalyst, Cardano’s mechanism for funding teams building on Cardano, wrapped up voting for the 10th round of funding on September 14. This was a milestone round in terms of real-time data accessibility. For the first time in Project Catalyst history, third-party teams like Lido Nation were able to build infrastructure to provide real-time data on proposal engagement during voting. While the data remains anonymous—showing neither who voted nor how—it reveals how many votes each proposal garners, acting as a digital mirror reflecting real-time community interest.

Picture this: You’re an innovator with a groundbreaking proposal. You’ve submitted your project, and now you’re in the vast ocean of more than 1100 other proposals, swimming to get attention and votes. Unlike before, this time you had a lens—Lido Nation’s newly launched “Catalyst by the Numbers” page. With the near real-time metrics on this page, proposing teams are able to better understand their traction with voters. Previously, the lack of live data was akin to navigating a labyrinth blindfolded. But now, this feedback is your Ariadne’s thread through the maze, leading to heightened social media activity and sharing. With links to proposals being shared everywhere, the familiar elephant in the room came back into focus: voter fatigue.

The Challenge of Voter Fatigue

Scrolling through over a thousand proposals is overwhelming. If you’re a voter, it’s like walking into a library with no catalog and a ticking clock. Lido Nation’s big idea is that with a more streamlined and engaging user interface, reviewing each proposal becomes less like solving a Rubik’s cube and more like swiping through a well-curated digital magazine. The hypothesis is: the more seamless the experience, the more proposals voters will see and engage with.

Rich Social Media & Open Graph Card

To that end, our latest effort to improve the voting experience is introducing unique, dynamic rich social media and open graph cards for proposals. Open Graph is a standard that allows web content providers to add a few lines of code that instruct other websites on how the content is displayed when a link is shared on social media. Here is how proposal links from cardano.ideascale.com are presented when shared on social media and messaging apps (ie, twitter, facebook, discord, slack, telegram, etc): ideascale previews

This presentation gets the job done, but it doesn’t do enough to help voters form an educated first opinion about the link. Our tall order took shape in the form of some whatifs: Instead of a generic lightbulb image on every single card, what if we generated a unique image for each proposal? Even better, what if we generate a series of images for each proposal, in different languages? So when you share the Chinese link to your proposal, the image the voter sees presents information in Chinese! After much consternation and wrestling with headless Chrome, Docker, and a series of PHP image manipulation extensions, we confirmed that our what ifs were possible.

How we built it

Our tech stack is bare metal servers, then Kubernetes, then Docker, then mostly PHP, Python, Typescript, and CSS, sometimes Golang, and very soon Aiken. To generate rich social media cards we thought the straight line would be to just generate an image and let social media and messaging apps pick them up. An added bonus was to do this without actually having to save physical image files on the server. More on that later.

Generating the images The first step was figuring out how to make an informational image. We first tried playing with tools like imagecreate in PHP. They were promising but too verbose and required us to manually paint everything we wanted on the image. Lido doesn’t have a UI/UX person on the team so even having the language to tell the tools what to do proved challenging. We got anxiety thinking about having to maintain and iterate this type of solution.

Our next thought was, what if we somehow figure a way to create a screenshot of part of the normal proposal page. We already have a pretty cool summary area on each proposal page. What if we could just screenshot that summary area.

single proposal preview

To prototype this idea, after some google searching, we turned to an open source library by Spatie called browsershot. Browsershot is a Laravel PHP “package that lets you convert a webpage to an image or pdf. The conversion is done behind the scenes by Puppeteer which controls a headless version of Google Chrome.” Things looked promising! The requirements:

  • Node
  • Puppeteer package
  • A bunch of operating system packages: gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget libgbm-dev libxshmfence-dev
  • Google Chrome on the server

Installing the first three things was a breeze. When it came to Chrome however, that quickly became a nightmare. The crux of the issue was that Ubuntu, the operating system we use to run Lido, is forcing their dev ecosystem to switch over to Snap, a software packaging and deployment system operating systems that use the Linux kernel and the Systemd init system. Because we use Docker, Systemd is not running while installing Chrome during the container image build process. A quick Google search made it clear that it is a fool’s errand to try to run Systemd while also building your image. Try as we might, we could not get Google Chrome to install from Snap. Installing from other package managers resulted in error when Puppeteer tries to launch Chrome, directing us to install Chrome via snap. We lost a day of work trying to install Google Chrome. Sheesh!

This feature felt too important to abandon so instead of continuing to bang our heads against the wall, we switched our operating system from Ubuntu to Alpine Linux. Finding the equivalent system packages was challenging but installing Chrome this time was easy. Finally, we were ready to take some screenshots! Here’s the invocable class that does the final magic:

generate proposal image class

Line 26: this uses Laravel’s built-in HTML generator to render a custom page that just has the Javascript and CSS needed to render the proposal summary widget. Essentially, we can blow up the widget to be the only thing that is rendered. Since Laravel is the one generating it, all of our normal CSS and Javascript just work. We don’t have to be designers to make an image!

Line 29: Once we have our HTML, we pass it to Browsershot which returns what is essentially a headless version of Chrome.

Line 42: We have the option to either return the Chrome instance or tell Browsershot to take a screenshot of the HTML being rendered in the embedded Chrome instance. With the screen we can save the file to a folder, one for each proposal, and return null.

Storing the images Before adding the option to store the image this is what we first tried:

serve image direct to browser

Line 455: we get the instance of a wrapped up headless chrome from our GenerateProposalImage invocable. We then resize the chrome window.

Line 458: We tell Browsershot to take a screenshot of the embedded page and convert it to a base64 string, instead of saving the physical image to disk. There are over 5000 proposals, supporting them means 5000X the number of languages Lido partially or fully supports, which currently sits at 7. With an average of 1000+ proposals with every funding round, the numbers will quickly multiply! What we were trying to avoid is having to deal with tens of thousand physical images.

Line 459: Finally, we generate a response to tell social media sites and message apps that the link it’s accessing is a image, and we decode the base64 string to an image blob in memory and return that blob to your browser; in our case, the blob is returned to twitter, discord, facebook, slack, etc.

It worked!

We were actually generating images that looked exactly like the colored summary card on the single proposal page. When we visit the link in our browser, we can see the image and save it to our computer. But the images would not render in any of the apps. We went on another research adventure and came up empty. Our best guess is that generating the image on the fly either takes too long or there’s something about what the apps and social media sites are expecting that we’re not including. So we caved and added the option to save the file locally to the server to our GenerateProposalImage invocable class.

A few lines of code

After all that, we could finally add that view line of code to single proposal pages. With these lines, when you share a single proposal from Lido Nation, a unique rich Open Graph card is generated for each proposal on any site or app that supports the Open Graph card spec.

open graph snippet

Win win and next steps

This is how a Lido Nation proposal link is now presented when shared on social media or in messaging apps: LIDO open graph snippet

Every proposal gets a unique image! The goal here is to make this image educational, a way to make an educated first impression on a potential voter. Because we’re simply transforming regular html to image, this means we can put anything we want on these cards! But what do we put on these cards? We do not have user experience experts on our team. What we do have is an incredible hive mind in Project Catalyst and Cardano - More on that in the coming article!

Delivering on these whatifs largely rested on the shoulders of Darlington, our fearless leader and mentor at the lab. While he was working on this, the rest of the team was working hard to bring other improvements to Catalyst: Titi on automated sync with the voting sidechain to reduce tally updates from 2 to 4 hrs to to 1, Teddy on other metrics on the Catalyst By the Numbers page, and Emanuel was fielding and resolving small enhancements and issues reported by you the community.

We hope this small UI/UX improvement helps level the playing field for teams who are not great at making cool graphics of their proposals. We hope voters enjoy engaging with these proposals on social media, especially for those times when you don’t have time to click the link to read a 10,000 word proposal. We build a lot of cool things at the Ngong Road Blockchain Lab. We enjoy building for Cardano, on Cardano. Until the next one!

Get more articles like this in your inbox

Was the article useful?

Or leave comment

No comments yet…

close

Playlist

  • EP2: epoch_length

    Authored by: Darlington Kofa

    3m 24s
    Darlington Kofa
  • EP1: 'd' parameter

    Authored by: Darlington Kofa

    4m 3s
    Darlington Kofa
  • EP3: key_deposit

    Authored by: Darlington Kofa

    3m 48s
    Darlington Kofa
  • EP4: epoch_no

    Authored by: Darlington Kofa

    2m 16s
    Darlington Kofa
  • EP5: max_block_size

    Authored by: Darlington Kofa

    3m 14s
    Darlington Kofa
  • EP6: pool_deposit

    Authored by: Darlington Kofa

    3m 19s
    Darlington Kofa
  • EP7: max_tx_size

    Authored by: Darlington Kofa

    4m 59s
    Darlington Kofa
0:00
/
~0:00