Skip to content
This repository has been archived by the owner on Aug 16, 2021. It is now read-only.

Releases: pqt-graveyard/social-preview

Dynamically Generated Images

12 Nov 12:38
@pqt pqt
7ce1e52
Compare
Choose a tag to compare

v3.0.0

Hey! It's been quite some time since I made some upgrades to this project so it was about time and there are some sweet things happening in this update.

Completely Dynamic Images

I've always loved the way that GitHub has used dots and squares in a seemingly random way to create the textures seen on their marketing pages.

It makes use of the fantastic library https://github.com/davidbau/seedrandom library by @davidbau and honestly couldn't have been done without such a great library to jump-start the way I wanted to approach this new idea I had. It needed to be random, yet consistent and that's exactly the problem this library solves.

I also get a lot more intimate with the https://github.com/oliver-moran/jimp library by @oliver-moran. Another brilliant creation that makes this entire project possible.

Features

  • Acknowledges your Repository Language Colors (and percentages)
  • Use your own GitHub Access Token (optional)
  • Dark mode
  • Squares or Circles
  • Customize the unique seeder ID (optional)

Examples

repo languages light dark
pqt/social-preview pqt-social-preview pqt-social-preview (1)
microsoft/CCF microsoft-CCF microsoft-CCF (1)
laravel/laravel laravel-laravel laravel-laravel (1)
tailwindlabs/tailwindcss tailwindlabs-tailwindcss tailwindlabs-tailwindcss (1)

Technicals (Long read ahead, grab that popcorn!)

Let's get into the meat and potatoes of how this works. Using NextJS API Routing I instantiate Octokit. Either using a user-provided GitHub Access Token or one I have sorted as an environment variable.

/**
 * Instantiate the GitHub API Client
 */
const octokit = new Octokit({
  auth: token || process.env.GITHUB_TOKEN,
});

Next, I fetch the repository that was passed into the endpoint (owner/repository). I also grab the languages used on the repository.

/**
 * Fetch the GitHub Repository
 */
const { data: repository } = await octokit.repos.get({
  owner,
  repo,
});

/**
 * Fetch the GitHub Repository Language Data
 */
const { data: languages } = await octokit.repos.listLanguages({
  owner,
  repo,
});

I decided to add an extra layer of personalization using the above-listed languages. The dots are colored relative to the language percentages found on the repository. In other words, if the project is 50% typescript, roughly half of the colored dots are going match the GitHub-assigned color for the TypeScript language. This accounts for all listed languages. I source this directly from the https://github.com/github/linguist repository (see the actual file we need here.).

/**
 * Fetch the GitHub language colors source-of-truth
 */
const { data: linguistInitial } = await octokit.repos.getContents({
  owner: 'github',
  repo: 'linguist',
  path: 'lib/linguist/languages.yml',
  mediaType: {
    format: 'base64',
  },
});

/**
 * Create a Buffer for the linguistInitial content
 * Parse the YML file so we can extract the colors we need from it later
 */
const linguistContent = linguistInitial as { content: string };
const linguistBuffer = Buffer.from(linguistContent.content as string, 'base64');
const linguist = YAML.parse(linguistBuffer.toString('utf-8'));

So after fetching the file contents and parsing it we now have access to all of the colors GitHub uses for its languages!

Now it's time to make use of the aforementioned https://github.com/davidbau/seedrandom library. I accept a custom seed string from the user or use the returned repository id value as a fallback.

/**
 * Initialize the random function with a custom seed so it's consistent
 * This accepts the query param and will otherwise fallback on the unique repository ID
 */
const random = seedrandom(seed || repository.id.toString());

I conditionally assign a template object some values depending on whether or not the user has requested a darkmode image be generated.

```js
/**
 * Remote Template Images
 */
const template = {
  base:
    displayParameter === 'light'
      ? await Jimp.read(fromAWS('/meta/base.png'))
      : await Jimp.read(fromAWS('/meta/base_dark.png')),
  dot:
    dotTypeParameter === 'circle'
      ? await Jimp.read(fromAWS('/meta/dot_black.png'))
      : await Jimp.read(fromAWS('/meta/square_black.png')),

  circle: await Jimp.read(fromAWS('/meta/dot_black.png')),
  square: await Jimp.read(fromAWS('/meta/square_black.png')),

  githubLogo:
    displayParameter === 'light'
      ? await Jimp.read(fromAWS('/meta/github_black.png'))
      : await Jimp.read(fromAWS('/meta/github_white.png')),
};

This carries into the font selection too.

/**
 * Font family used for writing
 */
const font =
  displayParameter === 'light'
    ? await Jimp.loadFont(
        'https://unpkg.com/@jimp/plugin-print@0.10.3/fonts/open-sans/open-sans-64-black/open-sans-64-black.fnt'
      )
    : await Jimp.loadFont(
        'https://unpkg.com/@jimp/plugin-print@0.10.3/fonts/open-sans/open-sans-64-white/open-sans-64-white.fnt'
      );

Nothing notable about this, just some boring variable reference definitions for both Jimp and so I didn't lose my mind with future calculations (you'll see what I mean). I specify the image dimensions as suggested by GitHub's social preview setting module and can definitely say that there's going to be 64 dots in each row (horizontal) and 32 dots in each column (vertical). I make it easy to not have to update both variables so I just divide by 20 on both. Spacing is basically the margins I use.

/**
 * Required Image Dimensions
 */
const width = 1280;
const height = 640;

/**
 * Total Count of Squares (Dimensions)
 */
const horizontal = width / 20;
const vertical = height / 20;

/**
 * Spacing
 */
const spacing = 40;

The next step I take is to make a dots array which will give me the total count of how many dots will be placed on the image. I will need to loop over this later. I also clone the base template for future reference and ensure it's resized to exactly what I need.

/**
 * Base Image
 */
const dots = [...new Array(horizontal * vertical)];
const image = template.base.clone().resize(width, height);

Every generated image has an area where dots cannot be placed. I decided to opt for a range. These are pixel coordinates from the top left of the image.

/**
 * Protected Area Coordinates
 */
const protectedArea = {
  x: { min: 185, max: 1085 },
  y: { min: 185, max: 445 },
};

I quickly started to need some helper functions to determine how to understand where each dot was going to be placed. For example, if I wanted to see where a dot with the index 129 would be placed I would pass it into this function.

const getXPosition = (index: number): number => {
  return 5 + (index % horizontal) * 20;
};

This applies the 5-pixel padding from the edge, runs the modulo operation against how many dots are in each row, and then scales that by 20px reserved space per dot (10px width and 10px gutter from the next dot). The same logic is applied vertically.

const getYPosition = (index: number): number => {
  return 5 + 20 * Math.floor(index / horizontal);
};

With these helper functions, I can now convert the protected area units into a simple boolean.

const isProtectedArea = (i...
Read more

v2.0.2

21 Sep 09:21
@pqt pqt
4c460f1
Compare
Choose a tag to compare
  • (4c460f1) add the ability for a direct image response from the API

v2.0.1

21 May 16:05
@pqt pqt
e0e949f
Compare
Choose a tag to compare

Small tweaks and improvements

  • (9e8df3c) fix visual inconsistency on safari - Thanks to @veksen for the absolutely heroic visual debugging.
  • (278562e) add spinner to suggest loading state while form is submitting
  • (6baf7cf) add github token to avoid rate limits - Preventing issues before they happen.
  • (807382d) update canonical URL hook - Provides the correct meta URL now when server-side rendered.
  • (aac77d2) increment padding around preview as viewport scales
  • (e0e949f) Create LICENSE - this is just for anyone interested in cloning, deleted it accidentally in the v2 rewrite.

v2.0.0

18 May 01:19
@pqt pqt
Compare
Choose a tag to compare

This is a rewrite to NextJS and a new image generation approach using Jimp.

This opens the door to some really important items.

  • Improved customization flexibility, initially I tried to use SVG to render the preview and then use html2canvas to capture a snapshot of the preview, convert it to PNG and export. This worked, but was very restricted and simply did not recognize certain qualities about SVG (resulting in them being totally ignored in the rendering process). This new approach uses some base images and positions text and overlays in a much more elegant fashion thanks to Jimp. I don't know what might be required to start powering users with more control, but I'm far more confident in the current implementation than v1.

  • Consistent experience on both desktop and mobile, previously the rendered download would be different depending on which device actually triggered the download, this should no longer be the case.

  • Future API capability, the front-end has access to a serverless backend thanks to Vercel. This has the opportunity to become an incredibly powerful resource for areas outside of GitHub (or maybe GitHub could just use the API to generate nice social previews for new repos???). I don't have a solid plan for this yet but there loads of opportunity here for sure.

Initial Launch

22 Mar 02:08
@pqt pqt
Compare
Choose a tag to compare
v1.0.0

first commit