Page 3

Okay. It’s 2023. I just copied all the font release Tweets I’ve ever bookmarked on Twitter just in case that feature suddenly disappears. Let’s recommend some downloads.

Liquidie by Forberas is a fluid, hand drawn font. It’s free for personal use only and includes upper and lower case characters.

Download at Font Space or buy at Creative Market.

Finally moving away from the sixties inspired font recommendations.

Rocker Squad by Letterara is a thick, punk rock looking hand drawn font. Free for personal use only. 

Download at Font Space or buy at Creative Fabrica.

These first Friday font recommendations are increasingly any day of the week and exclusively sixties inspired font recommendations. In keeping with that, it’s a perfect Wednesday for downloading Groovy Daisy by Jimtype Studio. It’s free for personal use only and is, indeed, very groovy.

Download at Font Space.

Next.js and Tumblr as a CMS Part 4: Open Graph Images

Way back in my first post on using Next.js with Tumblr, I mentioned getting more control over my blog as one of the big motivations for the switch. So, I thought I’d wrap up by going over a couple of the specific things I meant by that: generating Open Graph images and adding syntax highlight to code blocks.

This was originally intended as one post but it got a little long and I’m a little slow so I’ll start with Open Graph images.

Open Graph images are the preview images you’ve probably seen when sharing a link on a social media site like Twitter or Facebook. By default, Tumblr will display a generic image with the Tumblr logo and some themes might pull in your avatar or let you upload a custom image. However, there isn’t a good way to attach different images to different posts or to dynamically generate them. My goal was for each of my posts to have a unique, text-based image displaying its title or description and type.

Sample Open Graph sharing image

There are a lot of good articles on the topic but their instructions didn’t get me exactly what I wanted so I wound up picking and choosing to cobble my solution together. Dynamic Open Graph images with Next.js was so close but it uses next-api-og-image which uses chrome-aws-lambda under the hood and I couldn’t get it to work on Vercel. I even tried the suggestion to install an older version of chrome-aws-lambda but it just wouldn’t deploy. Generate Open Graph images for your static Next.js site generated images during the build process instead of on the fly but it introduced using Playwright which was invaluable. Those two articles were big influences on the code below.

Start by installing the necessary playwright packages:

yarn add playwright playwright-core playwright-aws-lambda

Next, in your post component, add the meta tag inside the next/head block:

<Head>
  <meta property="og:image" content={`${process.env.NEXT_PUBLIC_BASE_URL}/api/og-image?headline=${post.headline || post.summary}&type=${post.type}`} />
  ...
</Head>

Two things to note: 1) for convenience, I store my base URL as an environmental variable so you’ll probable need to replace NEXT_PUBLIC_BASE_URL and 2) we’ll be using query params to pass the post headline and type.

The content URL points to a route we’ll create in pages/api/og-image.js. I’ve truncated a lot of the HTML and CSS since those will depend on how you want your image to look:

const playwright = require('playwright-aws-lambda');

export default async function handler (req, res) {
  const html = `
    <html>
      <head>
        <meta charset="UTF-8">
        
        <style>
          *, *:after, *:before {
            box-spacing: border-box;
          }

          html {
            font: 8px 'museo-sans-rounded', sans-serif;
            line-height: 1.4;
          }

          body {
            background: #f4f4f4;
            margin: 0;
            padding: 2rem;
          }
          
          ...
        </style>
      </head>

      <body>
        <div class="og">
          ...

          <div class="og__type">
            <span>laurenashpole.com</span>  —  ${(req.query || {}).type || ''} post
          </div>

          <h1 class="og__headline">${(req.query || {}).headline || ''}</h1>
        </div>        
      </body>
    </html>
  `;

  if (process.env.NODE_ENV === 'development') {
    res.setHeader('Content-Type', 'text/html');
    return res.end(html);
  }

  const browser = await playwright.launchChromium({ headless: true });
  const page = await browser.newPage();
  await page.setViewportSize({ width: 1200, height: 630 });
  await page.goto('about:blank');
  await page.setContent(html, { waitUntil: 'networkidle' });
  const img = await page.screenshot({ type: 'png' });
  await browser.close();

  res.setHeader('Cache-Control', 's-maxage=31536000, stale-while-revalidate');
  res.setHeader('Content-Type', 'image/png');
  res.end(img);
}

Now, if you visit that URL while developing locally, you should see an HTML page so you can inspect and tweak your designs. In production, Playwright will launch a headless browser, open a new page and insert your HTML, and then take and return a PNG screenshot.

And that’s how I set up my Open Graph images. Next time I’ll actually finish the series with syntax highlighting for code blocks.

Creative Market - Explore the World's Marketplace for Design
Advertisement
Creative Market - Explore the World's Marketplace for Design
Advertisement

A couple days late but Hello Margarine by Prioritype Co is a fun font. Maybe I’m in a real sixties mood lately but I’m loving the groovy, bell-bottoms-y style. And it comes in regular, outline, and shadow varieties. Free for personal use only. 

Download at Font Space or buy at Creative Market.

Getting in a font rec before the long weekend and it’s a good one for summer. California Vibes by Alpaprana Studio is a casual, handwritten font with matching lower and uppercase letters. Free for personal use only.

Download at Font Space or buy at Font Bundles.

Friday font day! Charley by Storytype Studio is an all caps font with a sixties, psychedelic rock poster vibe. Free for personal use only.

Download at Font Space or buy at Creative Market.

Next.js and Tumblr as a CMS Part 3: Sitemap and RSS

If you’ve been following along with my series on using Tumblr as a Next.js CMS, last time we looked at fetching data and re-creating all the pages you’d expect to find on your average blog. That tutorial skipped two items that Tumblr normally handles: the sitemap and RSS feed. They require a little extra attention so in this post we’ll build on the earlier code to generate those files.

Sitemap

The basic Tumblr sitemap contains links to your homepage and each individual post. You can also add any other URLs you want to include (I like to throw in a list of featured tags) but for now we’ll focus on posts.

In the tumblr.js file from the last entry, reuse the CLIENT and getPosts code to create a method to return all posts:

export async function findAll (limit = 50) {
  const client = tumblr.createClient(CLIENT);
  const initialResponse = await getPosts(client, limit, 0);
  const totalPages = Math.floor(initialResponse.total_posts / limit);

  const posts = await [...Array(totalPages).keys()].reduce(async (arr, i) => {
    const response = await getPosts(client, limit * (i + 1));
    return [ ...(await arr), ...response.posts ];
  }, []);

  return { ...initialResponse, posts: [ ...initialResponse.posts, ...posts ] };
}

The Tumblr API documentation isn’t super clear when it comes to the number of posts you can retrieve in one request. It implies that 20 is the maximum but I haven’t found that to be the case in practice so, to make updating easier just in case, the findAll method accepts limit as a parameter.

Next, in the pages directory, add a new file called sitemap.xml.js. The file will return an empty React component and most of the action takes place in getServerSideProps where we’ll get the posts, use them to generate an XML string, and then switch the Content-Type to XML with setHeader.

import { findAll } from '../utils/tumblr';

const Sitemap = () => {};

export async function getServerSideProps ({ res }) {
  const response = await findAll();

  const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
    <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 <a href="http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd</a>">
      <url>
        <loc>${process.env.NEXT_PUBLIC_BASE_URL}</loc>
        <changefreq>weekly</changefreq>
        <lastmod>${new Date().toISOString().substring(0, 10)}</lastmod>
      </url>

      ${response.posts.map((post) => `
        <url>
          <loc>${process.env.NEXT_PUBLIC_BASE_URL}${new URL(post.post_url).pathname}</loc>
          <lastmod>${new Date(post.date).toISOString().substring(0, 10)}</lastmod>
        </url>
      `).join('')}
    </urlset>
  `;

  res.setHeader('Content-Type', 'text/xml');
  res.write(sitemap);
  res.end();

  return {
    props: {}
  };
}

One thing to note, I store my blog’s base URL as an environmental variable so you’ll need to either replace NEXT_PUBLIC_BASE_URL above or add it to your .env files to get this working.

RSS

For the RSS feed, we’ll be using the Feed package to handle the data formatting. You can install it with:

yarn add feed

Create a directory called rss inside pages and then add an index.js file inside of it. By default, the Tumblr feed shows the ten most recent posts so we can use the same find method we used before for pages.

The file for the RSS feed will look pretty similar to the sitemap. The main difference is that instead of using string interpolation to generate XML we’ll follow the example from the Feed docs:

import { Feed } from 'feed';
import { find } from '../../utils/tumblr';

const Rss = () => {};

export async function getServerSideProps ({ res }) {
  const response = await find();

  const feed = new Feed({
    title: 'Your Tumblr Title',
    description: 'Your Tumblr description.',
    link: process.env.NEXT_PUBLIC_BASE_URL
  });

  response.posts.forEach((post) => {
    feed.addItem({
      title: post.title || post.summary,
      description: `${post.type === 'photo' ? post.photos.map((photo) => `<img src=${photo.original_size.url} /><br /><br />`) : ''}${post.type === 'video' ? `<iframe width="700" height="383" src="https://www.youtube.com/embed/${post.video.youtube.video_id}" frameborder="0" /><br /><br />` : ''}${post.type === 'link' ? `<a href=${post.url}>${post.title}</a>: ` : ''}${post.type === 'answer' ? `${post.question}: ` : ''}${post.trail[0].content_raw}`,
      link: `${process.env.NEXT_PUBLIC_BASE_URL}${new URL(post.post_url).pathname}`,
      pubDate: new Date(post.date).toISOString().substring(0, 10),
      category: post.tags.map((tag) => { return { name: tag }; })
    });
  });

  res.setHeader('Content-Type', 'text/xml');
  res.write(feed.rss2());
  res.end();

  return {
    props: {}
  };
}

export default Rss;

To match the Tumblr provided feed, I added some post type specific intros before the raw content in the description text.


That should cover sitemaps and RSS. My next post will dig into some of the bells and whistles that inspired the move to a separate app in the first place like code syntax highlighting and custom social sharing images.