Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds dynamic og:image generation using middleware and plain components #10075

Closed
wants to merge 27 commits into from

Conversation

cannikin
Copy link
Member

@cannikin cannikin commented Feb 29, 2024

This PR adds Danny & Rob's Conference Hackathon project, now with 75% less hacks!

You can now create a custom og:image for any page in your site as just another regular component alongside your page:

image

Here's a very barebones example showing you can query for data, access query string parameters, and any route parameters that were present in the Router:

import { useLocation } from '@redwoodjs/router'

export const data = async () => {
  return { foo: 'bar' }
}

export const output = ({ data, userId }) => {
  const { searchParams } = useLocation()

  return (
    <div className="bg-red-500">
      <h1 className="text-sm text-white">OG Image</h1>
      <p>data: {data.foo}</p>
      <p>query: {JSON.stringify(Object.fromEntries(searchParams.entries()))}</p>
      <p>route params: userId = {userId}</p>
    </div>
  )
}

og:images are just URLs, and the URL is the same as the page you're viewing, plus .png on the end (other image types are possible). The one exception is for the homepage, since there is no filename to append .png to:

https://example.com/users/123      <- page
https://example.com/users/123.png  <- og:image 
https://example.com/               <- page
https://example.com/index.png      <- og:image 

The idea being that every page can have its own unique og:image, and there's no special directory structure you need to create or anything, just add an image-like extension to a regular JSX component and voilà.

The actual generation of the image happens inside middleware, by playwright rendering your component as a real page and then snapshotting it. Images are currently generated on the fly whenever the URL is hit, but we should be able to cache them, or maybe even write them out to the filesystem so that your web server serves them as regular static content (not implemented in this PR).

How to use in your app

See this gist for the entry.server.jsx config in your app. We're talking about potentially adding the renderOgImage() middleware function to Redwood itself, and then you just import and add to your middleware() function if you want to use it. We also have a stubbed renderExtension() which will register any extension and let you render stuff like .json or .rss or whatever else you want. This is a big feature we hadn't yet stolen from Rails—UNTIL NOW!

To get the URL to your og:image, use the new useOgImage() hook:

import { useOgImage, Metadata } from '@redwoodjs/web'

const HomePage = () => {
  const { ogProps } = useOgImage()

  return (
    <>
      <Metadata
        title="Home"
        description="My awesome homepage"
        og={ogProps}
      />
 
      <h1>Home Page</h1>
    </>
  )
}

Which creates all of the og:image tags for you:

image

useOgImage() takes several options:

  • the extension to render (defaults to png)
  • the width of the generated image
  • the height of the generated image

The general consensus (and Facebook docs) say that an og:image should be 1200x630 so that's currently the default if no width/height is passed. GitHub (on this very page, in fact) sets theirs to 1200x600.

If you don't want to automatically add the props, you can destructure out everything individually and set them yourself:

const { url, width, height } = useOgImage({ extension: 'jpg', width: 1024, height: 768 })

Non-breaking Changes

We modified the App.tsx template to accept possible children, and render them if present. This lets the og:image handler inject your component into the Document tree, without including the entire Router, but still style your og:image component using whatever you used to style the rest of your app (Tailwind, perhaps?)

We also modified the useLocation() hook to now return everything that the URL API returns. Previously it only returned three attributes of the url (pathname, search, hash), now it returns everything available to a call to new URL() (origin, href, searchParams, etc.).

TODO

  • Update App.tsx in fixtures
  • Implement useOgImageUrl() hook (basically useLocation().href + .png)
  • Make sure .png.tsx files get built (vite)
  • Implement renderExtension()
  • Add .jpg routes (.gif not supported by playwright)
  • yarn rw setup command to install
  • Generator for creating og:image component
  • Docs
  • Middelware tests
  • useOgImageUrl() tests
  • CHANGELOG

Decide

  • Where to include default renderOgImage() middleware (new Redwood package?)
  • How to register middleware?
  • Can middleware define their own path handlers? .jpg vs .png

@cannikin cannikin added the release:feature This PR introduces a new feature label Feb 29, 2024
@cannikin cannikin added this to the SSR milestone Feb 29, 2024
@cannikin cannikin requested a review from dac09 February 29, 2024 05:23
@cannikin cannikin self-assigned this Feb 29, 2024
packages/vite/src/middleware/extensionRouteDef.ts Dismissed Show dismissed Hide dismissed
packages/vite/src/middleware/extensionRouteDef.ts Dismissed Show dismissed Hide dismissed
cannikin and others added 14 commits February 29, 2024 11:22
…support

* 'main' of github.com:redwoodjs/redwood: (30 commits)
  fix(scenario): Make sure to cleanup even if test fails (#10112)
  Update babel monorepo to v7.24.0 (#10090)
  Update storybook monorepo to v7.6.17 (#10089)
  Update dependency @apollo/client to v3.9.5 (#10087)
  fix(serve): Allow periods in most paths (#10114)
  feat(rsc-streaming): Integrating RSC builds with Streaming and Client side hydration (#10031)
  chore(style): getDefaultViteConfig source format (#10111)
  chore(refactor): vite - extract default vite config (#10110)
  chore(comment): cli index FIXME comment about ugly big red box
  RSC: rscBuildAnalyze: Start at web/src/ (#10109)
  RSC: ensureProcessDirWeb() (#10108)
  RSC: Extract webpack shims into their own file (#10107)
  RSC: Remove completed TODO comment
  RSC: Babel react plugin not needed for analyze phase (#10106)
  RSC: runFeServer: wrap RSC code with `if (rscEnabled)` (#10105)
  RSC: Update comments, naming etc based on Danny's input (#10104)
  RSC: Rename to buildRscClientAndServer (#10103)
  RSC: Rename to rscBuildForServer, and tweak some comments (#10102)
  SSR: Extract buildForStreamingServer function (#10099)
  chore(unit-tests): Silence middleware error logging (#10097)
  ...
@dthyresson
Copy link
Contributor

One enhancement I can think of to:

 const { ogProps } = useOgImage()

would be to optionally request (or return) twitter image info such as

twitter:image
twitter:image:alt

as well. Maybe be able to deconstruct const { ogProps, twitterProps } = useOgImage() as well?

@dthyresson
Copy link
Contributor

Most of the time, the dimensions for the image are specific to the provider -- Twitter dimensions are different from Facebook which are different from LinkedIn, etc.

If there is just one:

useOgImage() takes several options:

the extension to render (defaults to png)
* the width of the generated image
* the height of the generated image

dimension then the image may not be tailored for the provider that will unfurl it.

Perhaps there is a default for ogProps, but maybe can wrap some dimensions in a provider when returning twitter:image urls?

@cannikin
Copy link
Member Author

cannikin commented Mar 9, 2024

Yeah I was wondering about those size differences, but then I checked out GitHub to see what they do (since their stuff seems to render nicely everywhere) and they just have a single og:image with dimensions of 1200x600. They include a twitter:image:src tag, but the URL is the same as the og:image.

But, that's what I'm seeing when I look at the source in my browser. It's possible that GitHub's servers actually recognize a real Twitter unfurl bot request and generate different <meta> tags with different dimensions?

@dac09
Copy link
Collaborator

dac09 commented Apr 11, 2024

Closing as superceded by #10441

@dac09 dac09 closed this Apr 11, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release:feature This PR introduces a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants