Skip to content

Instantly share code, notes, and snippets.

@warrenbuckley
Created July 2, 2021 11:02
Show Gist options
  • Save warrenbuckley/1bb92a37f7b75d5ab9c9204406e6ed6b to your computer and use it in GitHub Desktop.
Save warrenbuckley/1bb92a37f7b75d5ab9c9204406e6ed6b to your computer and use it in GitHub Desktop.
Automatically Generate Open Graph Social Images in Umbraco V9
@using Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.BlogPost>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@{
Layout = null;
}
<html>
<head>
<meta property="og:title" content="@Model.Header" />
<meta property="og:image" content="/img/og/@(Model.Key).png" />
</head>
<body>
<h1>@Model.Header</h1>
@Model.BlogContent
</body>
</html>
using PuppeteerSharp;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Logging;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Routing;
namespace Umbraco.OpenGraph.Images
{
public class GenerateOpenGraph : INotificationAsyncHandler<ContentPublishingNotification>
{
private IPublishedUrlProvider _publishedUrlProvider;
private IProfilingLogger _profilingLogger;
public GenerateOpenGraph(IPublishedUrlProvider publishedUrlProvider, IProfilingLogger profilingLogger)
{
_publishedUrlProvider = publishedUrlProvider;
_profilingLogger = profilingLogger;
}
public async Task HandleAsync(ContentPublishingNotification notification, CancellationToken cancellationToken)
{
using (System.Threading.ExecutionContext.SuppressFlow())
{
foreach (var node in notification.PublishedEntities)
{
var filename = node.Key.ToString();
var pageUrl = _publishedUrlProvider.GetUrl(node.Key, UrlMode.Absolute);
var altTemplateUrl = $"{pageUrl}?altTemplate=opengraph";
// Fire & forget
Task.Run(() => GenerateImagesAsync(filename, altTemplateUrl));
}
}
}
public async Task GenerateImagesAsync(string filename, string pageUrl)
{
// Puppetter - Will fetch Chrome
// Will reuse Chrome binary once downloaded first time
using (_profilingLogger.DebugDuration<GenerateOpenGraph>("Downloading Chrome with Puppeteer"))
{
// EEK first download is about 22seconds
// Once downloaded it uses one from on disk to save time
var browserFetcher = new BrowserFetcher();
await browserFetcher.DownloadAsync();
}
await using var browser = await Puppeteer.LaunchAsync(
new LaunchOptions { Headless = true }
);
using (_profilingLogger.DebugDuration<GenerateOpenGraph>("Generating Open Graph Image for {PageUrl}", pageUrl))
{
// Generate an image on disk
// Use convention and store image URL at /img/og/{key}.png
// Use puppetter (headless chrome) to view the published page (alternative template)
// /my-blog-post?altTemplate=opengraph
// Get Puppetter to take a screenshot of the page and save as PNG
if(Directory.Exists("img/og") == false)
Directory.CreateDirectory("img/og");
var screenshotFileName = "img/og/" + filename + ".png";
using var page = await browser.NewPageAsync();
await page.GoToAsync(pageUrl);
await page.ScreenshotAsync(screenshotFileName);
}
}
}
}
@using Umbraco.Cms.Web.Common.PublishedModels;
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<ContentModels.BlogPost>
@using ContentModels = Umbraco.Cms.Web.Common.PublishedModels;
@{
Layout = null;
}
<html>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@100;400;900&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Montserrat', sans-serif;
background: #3644B1;
padding:15px;
}
h1 {
font-size:50px;
fontw-weight:900;
color:white;
margin:0;
}
h2 {
margin:0;
color:#F1BEBB;
font-weight:400;
}
</style>
</head>
<body>
<div>
<h1>@Model.Header</h1>
<h2>5 minute read</h2>
</div>
</body>
</html>
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Extensions;
using Umbraco.OpenGraph.Images;
namespace SocialPuppet
{
public class Startup
{
private readonly IWebHostEnvironment _env;
private readonly IConfiguration _config;
/// <summary>
/// Initializes a new instance of the <see cref="Startup"/> class.
/// </summary>
/// <param name="webHostEnvironment">The Web Host Environment</param>
/// <param name="config">The Configuration</param>
/// <remarks>
/// Only a few services are possible to be injected here https://github.com/dotnet/aspnetcore/issues/9337
/// </remarks>
public Startup(IWebHostEnvironment webHostEnvironment, IConfiguration config)
{
_env = webHostEnvironment ?? throw new ArgumentNullException(nameof(webHostEnvironment));
_config = config ?? throw new ArgumentNullException(nameof(config));
}
/// <summary>
/// Configures the services
/// </summary>
/// <remarks>
/// This method gets called by the runtime. Use this method to add services to the container.
/// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
/// </remarks>
public void ConfigureServices(IServiceCollection services)
{
#pragma warning disable IDE0022 // Use expression body for methods
services.AddUmbraco(_env, _config)
.AddBackOffice()
.AddWebsite()
.AddComposers()
.AddNotificationAsyncHandler<ContentPublishingNotification, GenerateOpenGraph>() // Added this to register this with Umbraco
.Build();
#pragma warning restore IDE0022 // Use expression body for methods
}
/// <summary>
/// Configures the application
/// </summary>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseUmbraco()
.WithMiddleware(u =>
{
u.WithBackOffice();
u.WithWebsite();
})
.WithEndpoints(u =>
{
u.UseInstallerEndpoints();
u.UseBackOfficeEndpoints();
u.UseWebsiteEndpoints();
});
}
}
}
@warrenbuckley
Copy link
Author

Proof of Concept

This works in the same approach as how GitHub is creating their new OpenGraph social images by using Puppetter to use a headless browser to take a screenshot/image of a HTML page that contains the information

https://github.com/puppeteer/puppeteer/

As Puppeteer is an NPM package and a bit of googling and there is an upto date/maintained C# library of this
https://github.com/hardkoded/puppeteer-sharp

Idea

This works by doing the following:

  • Register NotificationAsyncHandler for ContentPublishingNotification to be notified when Content Node is published
  • Grab the Guid/Key of the node and get the Absolute Url to the page
  • Fire & Forget task to not block the Save & Publish
  • Puppeter Sharp library downloads Chrome (First time only, uses local cached copy afterwards)
  • Puppeter Browses to an Alternative Template of the blog post page with ?altTemplate=opengraph
  • HTML Razor View/Template can have custom CSS & HTML to achieve look & feel as desired
  • Blog post template HTML meta data has link to generated open graph image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment