howto

How to make opengraph screenshots for hugo

it sure looks a lot nicer!

tags
hugo
static_sites
shot-scaper

Mostly I write these posts for myself – the act of writing is great to clarify my thinking, and I actually do use the search on the site a lot to remember stuff that I forgot. (Will I ever remember how to use asdf? Doubtful.) I did notice though that if you share the links on social media, it looks boring. So lets fix it.

We are going to generate images automatically from the posts. We'll loop through all of the files – a historical mix of markdown and lately all org files – pull out the title and subtitle, stick all of that into an HTML page, and the use shot-scraper to generate an image and move it into the leaf pages. Which means that we'll need to make sure that everything is in a leaf page.

Lets go:

HTML Template

This is our template. It's straight HTML and we're going to replace TITLE with the title and SUBTITLE with.. you get it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Sample Project</title>
        <script src="https://kit.fontawesome.com/9a33cfed92.js" crossorigin="anonymous"></script>
        <style>
  @import url('https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,100..900;1,9..144,100..900&display=swap');

  :root {
      --main-font-family: "Fraunces", system-ui;
      --background: #fafaf9;
      --text-color: #451a03;
      --header-color: #032e45;
      --diminished-text-color: #78716c;
  }

  body {
  font-family: var( --main-font-family );
  color: var( --text-color );
  background: var( --background );
  margin: 0;

  }

  main {
    max-width: 1200px;
    height: 630px;
    display: flex;
    align-items: center;
    justify-content: center;
    height: 600px;
    padding-left: 100px;
    padding-right: 100px;
  }

  h1 {
  font-size: 80px;
  color: var( --header-color );
  margin: 0;
  }

  h2 {
  font-size: 60px;
  color: var( --diminished-text-color );
  margin: 0;
  }

  h3 { font-size: 40px;
  color: var( --diminished-header-color );
  text-transform: uppercase;
  margin:0;
  }


        </style>
        <link rel="stylesheet" href="styles.css" />
      </head>
      <body>

        <main>

          <div>
            <h3>SECTION</h3>
          
            <h1>TITLE</h1>
            <h2>SUBTITLE</h2>
            <!--
            <ul>
              <li>tags</li>
              <li>tags</li>
            </ul>
            -->
          </div>
      </body>
    </html>

Test

If you don't have shot-scraper

1
  pip install shot-scraper

Then

1
  shot-scraper -w 1200 -h 630 -o shot.png og.html 2>&1
Screenshot of 'file:/Users/wschenk/willschenk.com/content/howto/2024/how_to_make_opengraph_screenshots_for_hugo/og.html' written to 'shot.png'

Move all pages to leaf pages

My posts are in the form

content/{section}/{year}/title

So my glob looks like that. You may need to change it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  #!/bin/env/ruby

  BASE_DIR="/Users/wschenk/willschenk.com"

  Dir.glob( "#{BASE_DIR}/content/*/*/*{md,org}" ).each do |page|
    puts page
    base = File.dirname( page )
    puts "base", base
    name = File.basename( page, File.extname(page))
    puts "name", name

    cmd = "mkdir -p #{base}/#{name}"
    puts cmd
    system( cmd )

    cmd = "mv #{page} #{base}/#{name}/index#{File.extname(page)}"
    puts cmd
    system( cmd )
    puts
  end

Make the images

This goes through everything and creates a cover.png for each of the leaf pages.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
  #!/bin/env/ruby

  require 'bundler/inline'

  gemfile do
    source 'https://rubygems.org'
    gem 'front_matter_parser'
  end

  require 'fileutils'
  require 'date'

  BASE_DIR="/Users/wschenk/willschenk.com"
  WORK_DIR="/tmp"
  TEMPLATE="og.html"

  class Page
    attr_reader :title, :subtitle, :section, :tags
    def initialize( file )
      @file = file
      read_attributes_org if File.extname(@file) == ".org"
      read_attributes_md if File.extname(@file) == ".md"
      @section = @file.split( /\// )[-4]
    end

    def outdir
      File.dirname(@file.gsub( /#{BASE_DIR}/, WORK_DIR ))
    end

    def outfile
      "#{outdir}/cover.png"
    end

    def read_attributes_md
      loader = FrontMatterParser::Loader::Yaml.new(allowlist_classes: [Date,Time])
      parsed = FrontMatterParser::Parser.parse_file(@file, loader: loader)
            
      fm = parsed.front_matter
      @title = fm['title']
      @subtitle = fm['subtitle'] || ""
      @tags = fm['tags']
    end

    def read_attributes_org
      contents = File.read( @file ).split( /\n/ );
      @title = contents.grep( /#\+title/ ).first.split( /:/ ).last
      subtitle = contents.grep( /#\+subtitle/ ).first
      if subtitle
        @subtitle = subtitle.split(/:/).last
      else
        @subtitle = ""
      end
      @tags = contents.grep( /#\+tags/ )
    end

    def make_image
      FileUtils.mkdir_p outdir
      file = "#{outdir}/og.html"
      puts "writing #{file}"
      File.open( file, "w" ) do |out|
        template = File.read TEMPLATE
        template.gsub!( /SUBTITLE/, @subtitle )
        template.gsub!( /TITLE/, @title )
        template.gsub!( /SECTION/, @section )
        out << template
      end

      cmd = "shot-scraper -w 1200 -h 630 -o #{outdir}/cover.png #{file}"

      puts "Running #{cmd}"
      system(cmd)

      cmd = "cp #{outfile} #{File.dirname( @file )}"
      puts "Running #{cmd}"
      system(cmd)
    end

    def exist?
      File.exist? outfile
    end
  end

  Dir.glob( "#{BASE_DIR}/content/*/*/*/index.{org,md}" ).each do |file|
    p = Page.new( file )
    if !p.exist?
      p.make_image
    end
  end

Adding the hugo short codes.

Inside of the <head> tag, which for me is in the template layouts/partials/head.html be sure to add in the opengraph and twitter_cards internal hugo templates.

1
2
  {{ template "_internal/opengraph.html" . }}
  {{ template "_internal/twitter_cards.html" . }}

Dynamically

This all started by going down a rabbit how for dynamically generating og-images but ultimately the static version was easier.

Previously

howto

Creating a start page

launch pad for all the things

tags
static_site
start_page

Next

fragments

The raven

tags