/blog/

2022 0409 Failed Experiment: Svg Tweets: First attempt: SVG generation

(This is part 1 in a set of failed experiments trying to generate SVG tweets in Hugo. I eventually abandoned this goal, and generate HTML archives of tweets instead.)

I can create an SVG that works pretty well, but it doesn’t get displayed properly. The browser just cuts it off at some point and doesn’t display the whole lthing.

I tried messing with viewBox but wasn’t able to solve my problem.

Included here are some things I tried:

A shortcode to generate the SVG

{{/* Given a tweet JSON from scripts/tweet, make an SVG embed of it */}}
{{- $tweetId := .Get 0 -}}
{{- $tweet := index .Site.Data.tweets $tweetId -}}
{{- $tweetUri := printf "https://twitter.com/%s/status/%s" $tweet.username $tweetId -}}
{{- $userUri := printf "https://twitter.com/%s" $tweet.user_displayname -}}
<div style="">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0 0 1000 1000" >
<foreignObject
  width="100%" height="100%"
  fill="#eade52">
  <style>
    .tweetsvg{
      clear:none;
    }
    a.tweetsvg {
      color: rgb(27, 149, 224);
      text-decoration:none;
    }
    blockquote.tweetsvg {
      margin:0;
      background-color:#fefefe;
      border-radius:2%;
      border-style:solid;
      border-width:.1em;
      border-color:#ddd;
      padding:1em;
      font-family:sans-serif;
      width:17rem
    }
    .avatar-tweetsvg {
      float:left;
      width:4rem;
      height:4rem;
      border-radius:50%;
      margin-right:.5rem;
      margin-bottom:.5rem;
      border-style: solid;
      border-width:.1em;
      border-color:#ddd;
    }
    h1.tweetsvg {
      margin:0;
      font-size:1rem;
      text-decoration:none;
      color:#000;
    }
    h2.tweetsvg {
      margin:0;
      font-size:1rem;
      font-weight:normal;
      text-decoration:none;
      color:rgb(101, 119, 134);
    }
    p.tweetsvg {
      font-size:1rem;
      clear:both;
    }
    hr.tweetsvg {
      color:#ddd;
    }
    time.tweetsvg {
      font-size:.9rem;
      font-family:sans-serif;
      margin:0;
      padding-bottom:1rem;
      color:rgb(101, 119, 134);
      text-decoration:none;
    }
    .media-tweetsvg {
      max-width: 100%;
      border-radius:2%;
      border-radius: 2%;
      border-style: solid;
      border-width: .1em;
      border-color: #ddd;
    }
    ol.media-tweetsvg-list {
      list-style-type: none;
      padding: 0;
    }
    ol.media-tweetsvg-list li {
      display: inline-block;
    }
    ol.media-tweetsvg-list-1 li {
      max-width: 100%;
    }
    ol.media-tweetsvg-list-2 li {
      max-width: 45%;
    }
    ol.media-tweetsvg-list-3 li {
      max-width: 30%;
    }
    ol.media-tweetsvg-list-4 li {
      max-width: 20%;
    }
  </style>
  <blockquote class="tweetsvg" xmlns="http://www.w3.org/1999/xhtml">
    <a class="tweetsvg" href="{{ $userUri }}">
      <img class="avatar-tweetsvg" alt="" src="data:image/jpeg;base64,{{ $tweet.user_pfp }}" />
      <h1 class="tweetsvg">{{ $tweet.user_displayname }}</h1>
      <h2 class="tweetsvg">@{{ $tweet.username }}</h2>
    </a>
    <p class="tweetsvg">{{ $tweet.full_html | safeHTML }}</p>
    <ol class="media-tweetsvg-list media-tweetsvg-list-{{ len $tweet.media }}">
      {{- range $tweet.media -}}
      <li>
        <a href="{{ .url }}">
          <img class="media-tweetsvg" width="{{ .width }}" src="data:image/jpeg;base64,{{ .data }}" alt="{{ .alttext }}" />
        </a>
      </li>
      {{- end -}}
    </ol>
    <a class="tweetsvg" href="{{ $tweetUri }}">
      <time class="tweetsvg" datetime="{{ $tweet.date }}">{{ $tweet.date_original_format }}</time>
    </a>
  </blockquote>
</foreignObject>
</svg>
</div>

A layout section to generate SVGs as a separate output format

This requires a section in the config:

outputFormats:
  # ... snip ...
  SVG:
    mediatype: image/svg
    suffix: svg
    isPlainText: true
    notAlternative: true
  # ... snip ...

And it requires adding tweets as metadata in (otherwise empty) markdown files. I created a new section called svgtweet (that is, a directory content/svgtweet/), with its section index (content/svgtweet/_index.md) containing this frontmatter:

---
cascade:
    outputs:
        - SVG
        - HTML
---

That section index was rendered via the layout file themes/micahrl/layouts/svgtweet/section.html, which just listed the HTML page for each post in the section:

{{ partial "header.html" . }}

<section id=content>

  {{ .Content }}

  <ul class=posts_listing>
    {{ range .Data.Pages.ByPublishDate.Reverse }}
      <li><h3><a href="{{ .Permalink }}">Tweet ID {{ .Params.tweet.id }}</a></h3>
        <p class="posts-listing-desc">{{ .Params.tweet.full_text }}</p>
        <div id=date>
          <time>{{ .Params.tweet.date }}</time>
        </div>
      </li>
    {{ end }}
  </ul>
</section>

{{ partial "taxonomyList.html" . }}
{{ partial "footer.html" . }}

Tweets were saved to JSON files, and the JSON placed in front matter as tweet:, with the path content/svgtweet/<tweetID>.md, e.g. content/svgtweet/1401685221947420675.md:

---
"tweet": {
  "id": "1401685221947420675",
  "date": "2021-06-06T23:39:33+00:00",
  "date_original_format": "Sun Jun 06 23:39:33 +0000 2021",
  "full_text": "Holy shit, that\u2019s amazing. Pins is a truly great app - I started working on a Pinboard app something like 9 months ago (because alternatives were hot garbage) and have abandoned it completely because of how great Pins is. I can\u2019t believe something this good is only 8 months old. https://t.co/PIhJp28csk",
  "full_html": "Holy shit, that\u2019s amazing. Pins is a truly great app - I started working on a Pinboard app something like 9 months ago (because alternatives were hot garbage) and have abandoned it completely because of how great Pins is. I can\u2019t believe something this good is only 8 months old. <a href=\"https://twitter.com/GetPinsApp/status/1401634722212630533\">twitter.com/GetPinsApp/sta\u2026</a>",
  "media": [],
  "entities": {
    "hashtags": [],
    "symbols": [],
    "user_mentions": [],
    "urls": [
      {
        "url": "https://t.co/PIhJp28csk",
        "expanded_url": "https://twitter.com/GetPinsApp/status/1401634722212630533",
        "display_url": "twitter.com/GetPinsApp/sta\u2026",
        "indices": [
          280,
          303
        ]
      }
    ]
  },
  "username": "mrled",
  "user_displayname": "Micah R Ledbetter / m \u0279\u0329 \u02c8 l e d /",
  "user_pfp": "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"
}
---

Finally, that was rendered via an SVG layout file themes/micahrl/layouts/svgtweet/single.svg:

{{- $tweet := .Params.tweet -}}
{{- $tweetUri := printf "https://twitter.com/%s/status/%s" $tweet.username $tweet.id -}}
{{- $userUri := printf "https://twitter.com/%s" $tweet.user_displayname -}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" >
<foreignObject x="0" y="0"
  width="20em" height="100%"
  fill="#eade52">
  <style>
    .tweetsvg{clear:none;}
    a.tweetsvg{color: rgb(27, 149, 224); text-decoration:none;}
    blockquote.tweetsvg{margin:0; background-color:#fefefe; border-radius:2%; border-style:solid; border-width:.1em; border-color:#ddd; padding:1em; font-family:sans-serif; width:17rem}
    .avatar-tweetsvg{float:left; width:4rem; height:4rem; border-radius:50%;margin-right:.5rem;;margin-bottom:.5rem;border-style: solid; border-width:.1em; border-color:#ddd;}
    h1.tweetsvg{margin:0;font-size:1rem;text-decoration:none;color:#000;}
    h2.tweetsvg{margin:0;font-size:1rem;font-weight:normal;text-decoration:none;color:rgb(101, 119, 134);}
    p.tweetsvg{font-size:1rem; clear:both;}
    hr.tweetsvg{color:#ddd;}
    time.tweetsvg{font-size:.9rem; font-family:sans-serif; margin:0; padding-bottom:1rem;color:rgb(101, 119, 134);text-decoration:none;}
    .media-tweetsvg {max-width: 100%; border-radius:2%; border-radius: 2%; border-style: solid; border-width: .1em; border-color: #ddd;}
    ol.media-tweetsvg-list {list-style-type: none; padding: 0;}
    ol.media-tweetsvg-list li {display: inline-block;}
    ol.media-tweetsvg-list-1 li {max-width: 100%;}
    ol.media-tweetsvg-list-2 li {max-width: 45%;}
    ol.media-tweetsvg-list-3 li {max-width: 30%;}
    ol.media-tweetsvg-list-4 li {max-width: 20%;}
  </style>
  <blockquote class="tweetsvg" xmlns="http://www.w3.org/1999/xhtml">
    <a class="tweetsvg" href="{{ $userUri }}">
      <img class="avatar-tweetsvg" alt="" src="data:image/jpeg;base64,{{ $tweet.user_pfp }}" />
      <h1 class="tweetsvg">{{ $tweet.user_displayname }}</h1>
      <h2 class="tweetsvg">@{{ $tweet.username }}</h2>
    </a>
    <p class="tweetsvg">{{ $tweet.full_html }}</p>
    <ol class="media-tweetsvg-list media-tweetsvg-list-{{ len $tweet.media }}">
      {{- range $tweet.media -}}
      <li>
        <a href="{{ .url }}">
          <img class="media-tweetsvg" width="{{ .width }}" src="data:image/jpeg;base64,{{ .data }}" alt="{{ .alttext }}" />
        </a>
      </li>
      {{- end -}}
    </ol>
    <a class="tweetsvg" href="{{ $tweetUri }}">
      <time class="tweetsvg" datetime="{{ $tweet.date }}">{{ $tweet.date_original_format }}</time>
    </a>
  </blockquote>
</foreignObject>
</svg>

That resulted in index.svg files getting created. I also made HTML pages that referenced for each one:

{{ partial "header.html" . }}

<section id=content>
  <h1>Tweet ID {{ .Params.tweet.id }}</h1>

  <a href="index.svg"><img src="index.svg" height="500px" /></a>

</section>

{{ partial "taxonomyList.html" . }}
{{ partial "footer.html" . }}

Responses

Webmentions

Hosted on remote sites, and collected here via Webmention.io (thanks!).

Comments

Comments are hosted on this site and powered by Remark42 (thanks!).