← Back to blog

Dynamic social media badges

 v0.6.28Avatar of adikhoffadikhoffJul 30, 2026, 12:26:52 PM

The problem was simple. When I paste a link to an event on social media platforms like Whatsapp, I wanted it to display a nice card with some info, like this.

image

Add tags to index.html

To accomplish this, I added some tags to my html header.

    <meta property="og:title" content="PhotoPaste - Live Photo Sharing and Competitions for Groups" />
    <meta property="og:description" content="Create an event, invite people, upload photos. See it all live on the big screen." />
    <meta property="og:image" content="https://photopaste.com/assets/images/screenshot-social.png" />
    <meta property="og:url" content="https://photopaste.com" />
    <meta property="og:type" content="website" />

    <meta property="og:site_name" content="PhotoPaste" />

    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:title" content="PhotoPaste - Live Photo Sharing and Competitions for Groups" />
    <meta name="twitter:description" content="Create an event, invite people, upload photos. See it all live on the big screen." />
    <meta name="twitter:image" content="https://photopaste.com/assets/images/screenshot-social.png" />

This worked fine, but quickly got annoying when I posted links to specific events. The badge is the same for any link from PhotoPaste, and the generic text sounded too much like an advertisement, so I had to delete the card manually whenever Whatsapp generated it.

The obvious solution was to change the system to create dynamic tags for each individual event.

Dynamic meta tags

The main problem here is that Angular apps are Single Page Applications (SPA) by default. That means that the only real HTML is loaded from the root at https://photopaste.com/index.html and all the other pages/routes, like https://photopaste.com/myevent/dashboard, are basically calculated/derived by the browser, based on the root HTML. Having a single access point like that has some advantages, but in our case also some disadvantages like the aforementioned social media cards, but also search engines who do not like executing Javascript (SEO).

Angular has something for this called Server Side Rendering (SSR). Basically it lets you designate routes to be rendered by the server. All you need to do is execute ng add @angular/ssr, configure some things, and you're golden.

Of course it's never that simple.

Upgrade project structure

Even though I'm running the latest version of Angular (22.0.8 at time of writing), my project structure was a little dated, given that it was generated at version 16.0.0. This meant that the ng add @angular/ssr command made some assumptions about files that weren't there. A younger version of me would have just changed individual files until the script got what it needed, but years of experience taught me that this attitude leads to the pits of hell. So I had some cleaning up to do.

For these things, I love to ask AI. I told it about my current configuration structure and it quickly pointed out some improvements. My app.component.ts was outdated and should just be app.ts, app.config.ts should only hold a config structure and the bootstrapping code should be separated into main.ts, and so on. I noticed other parts looked like the old config so I wanted to clean them up too.

AI recommended I use a VSCode plugin called Rename Angular Components that I used in the past when I was still using VSCode as my daily. This plugin has an extra feature called 'Rename all Angular suffixes to v20 styleguide.' It worked as advertised, but early attempts at a big bang approach did lead to major naming collisions (some components had the same name as data model entities), so I had to carefully rename some things before trying again. Most of the work was manually renaming all my services because many were named after database tables that collided with aforementioned data model. But in the end it worked.

Introduce SSR

I was able to run ng add @angular/ssr without problems now. This command generates an app.routes.server.ts file where you can tweak how the server treats different routes. You have three choices:

  • RenderMode.Prerender: render the page at build time, serve as static
  • RenderMode.Server: render the page on the server, repeat for each request (what we need, but adds CPU load)
  • RenderMode.Client: let the browser render the page (Angular default)

Currently, I am at this config.

export const serverRoutes: ServerRoute[] = [
  {
    path: ':event/**',
    renderMode: RenderMode.Server,
  },
  {
    path: ':event',
    renderMode: RenderMode.Server,
  },
  {
    path: '**',
    renderMode: RenderMode.Prerender,
  },
];

I played around with making specific paths client rendered (to maybe save CPU on the server) but that meant that those paths wouldn't benefit from the new SEO tags, so for now I'm keeping it like this. The only page that's prerendered currently is home (/). I can add more to this but for now I want to keep it simple.

The main issue I encountered was that my components were all written to be run from the browser environment. That meant free access to the document and window objects. In the NodeJs sandbox these items are not available, which leads to errors. A frequent construction I used was:

private readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
...
if (!isBrowser) return;

Convert DockerFile to NodeJs

Up until this point, I ran the frontend on a simple Nginx docker that just served static html and js. For server side rendering, we need something a little more intelligent.

FROM node:20-alpine

USER root
RUN apk --no-cache add curl
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

WORKDIR /app

COPY --chown=appuser:appgroup dist .

ENV PORT=9090
ENV LOG_LEVEL=warn
EXPOSE 9090

CMD ["node", "server/server.mjs"]

Running NodeJs within docker came with its own set of challenges. Relative URLs don't work because node doesn't have a base URL, so fetch requests to /api/v2/my-endpoint failed. The translate service expected files at /assets/i18n but couldn't find them.

Both of these were solved with an HTTP interceptor that inspects the URL and modifies it if it's relative and running on the server. It prefixes it with http://caddy to relay it back to Caddy internally so it doesn't interfere with blue-green deployments.

Result

Getting the new config through the CI/CD pipeline was its own story, with many setbacks and disappointments along the way. But finally, after what seemed way to long, the new tags work!

image

Read more articles

  • Moving towards Cloud storage
  • Add event and user statistics
  • Refactor Infrastructure
  • Add QR code to mobile
  • Video support and Scrolling Menu
  • Automatic blog updates with Git Webhooks
  • Add sliding sidebar menu to mobile home page
  • I've been doing WebFlux wrong
  • Implement Blog
  • Migrating event urls
  • Interesting bug
  • Dynamic social media badges
  • Improving Games and Teams
  • Blue Green deployments
  • Why I left the big cloud
  • Moving from AWS Amplify to Google Firebase
  • First version
← Back to blog