Compare commits
10 Commits
a9e9185b41
...
master
Author | SHA1 | Date | |
---|---|---|---|
7334e247d2
|
|||
6872a9357f
|
|||
b6ecdc0b64 | |||
04ca37fe14 | |||
a3274f8df1 | |||
e392e7ba8b | |||
22e1d72e01 | |||
48599bac5f | |||
036e9fa4a1 | |||
262e4ec4d7 |
@@ -7,15 +7,15 @@ Available at https://opengraph.cluster.fun/
|
|||||||
## Example
|
## Example
|
||||||
|
|
||||||
```html
|
```html
|
||||||
<img src="https://opengraph.cluster.fun/opengraph/?siteTitle=Marcus%20Noble&title=OpenGraph-Image-Gen&tags=opengraph%2Cgolang%2Ctwitter%2Cshare%2Csocial&image=https%3A%2F%2Fmarcusnoble.co.uk%2Fimages%2Fmarcus.jpg&twitter=Marcus_Noble_&github=AverageMarcus&website=www.MarcusNoble.co.uk&bgColor=%23ffffff&fgColor=%23263943" />
|
<img src="https://opengraph.cluster.fun/opengraph/?siteTitle=Example&title=Heading&tags=example%2Csample%2Cfoo%2Cbar&image=https%3A%2F%2Fmarcusnoble.co.uk%2Fimages%2Fmarcus.jpg&bluesky=%40averagemarcus.bsky.social&fediverse=%40marcus%40k8s.social&github=AverageMarcus&website=www.MarcusNoble.co.uk&bgColor=%23ffffff&fgColor=%23263943" />
|
||||||
```
|
```
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Dynamically generate a PNG image for use as an OpenGraph share image
|
* Dynamically generate a PNG image for use as an OpenGraph share image
|
||||||
* Ideally sized for Twitter previews
|
* Ideally sized for social card previews
|
||||||
* All text elements configurable
|
* All text elements configurable
|
||||||
* Configurable colours
|
* Configurable colours
|
||||||
* All text fields optional
|
* All text fields optional
|
||||||
|
12
index.html
12
index.html
@@ -13,7 +13,7 @@
|
|||||||
<meta property="og:url" content="https://opengraph.cluster.fun">
|
<meta property="og:url" content="https://opengraph.cluster.fun">
|
||||||
<meta property="og:description" content="Dynamically generate OpenGraph social share images">
|
<meta property="og:description" content="Dynamically generate OpenGraph social share images">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:image" content="https://opengraph.cluster.fun/opengraph/?siteTitle=Marcus%20Noble&title=OpenGraph-Image-Gen&tags=opengraph%2Cgolang%2Ctwitter%2Cshare%2Csocial&image=https%3A%2F%2Fmarcusnoble.co.uk%2Fimages%2Fmarcus.jpg&twitter=Marcus_Noble_&github=AverageMarcus&website=www.MarcusNoble.co.uk">
|
<meta property="og:image" content="https://opengraph.cluster.fun/opengraph/?siteTitle=Marcus%20Noble&title=OpenGraph-Image-Gen&tags=opengraph%2Cgolang%2Ctshare%2Csocial&image=https%3A%2F%2Fmarcusnoble.co.uk%2Fimages%2Fmarcus.jpg&fediverse=marcus@k8s.social&github=AverageMarcus&website=www.MarcusNoble.co.uk">
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:creator" content="@Marcus_Noble_" />
|
<meta name="twitter:creator" content="@Marcus_Noble_" />
|
||||||
|
|
||||||
@@ -60,7 +60,8 @@
|
|||||||
<label>Heading: <input id="title" type="text" value="Heading" /></label>
|
<label>Heading: <input id="title" type="text" value="Heading" /></label>
|
||||||
<label>Tags: <input id="tags" type="text" value="example,sample,foo,bar" /></label>
|
<label>Tags: <input id="tags" type="text" value="example,sample,foo,bar" /></label>
|
||||||
<label>Image: <input id="image" type="text" value="https://marcusnoble.co.uk/images/marcus.jpg" /></label>
|
<label>Image: <input id="image" type="text" value="https://marcusnoble.co.uk/images/marcus.jpg" /></label>
|
||||||
<label>Twitter Handle: <input id="twitter" type="text" value="Marcus_Noble_" /></label>
|
<label>Bluesky Handle: <input id="bluesky" type="text" value="@averagemarcus.bsky.social" /></label>
|
||||||
|
<label>Fediverse Handle: <input id="fediverse" type="text" value="@marcus@k8s.social" /></label>
|
||||||
<label>GitHub Username: <input id="github" type="text" value="AverageMarcus" /></label>
|
<label>GitHub Username: <input id="github" type="text" value="AverageMarcus" /></label>
|
||||||
<label>Website: <input id="website" type="text" value="www.MarcusNoble.co.uk" /></label>
|
<label>Website: <input id="website" type="text" value="www.MarcusNoble.co.uk" /></label>
|
||||||
<br />
|
<br />
|
||||||
@@ -77,7 +78,7 @@
|
|||||||
|
|
||||||
<label>
|
<label>
|
||||||
URL:
|
URL:
|
||||||
<input type="text" id="imageURL" readonly />
|
<input type="text" id="imageURL" />
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,13 +121,14 @@
|
|||||||
let title = encodeURIComponent(document.getElementById('title').value);
|
let title = encodeURIComponent(document.getElementById('title').value);
|
||||||
let tags = encodeURIComponent(document.getElementById('tags').value);
|
let tags = encodeURIComponent(document.getElementById('tags').value);
|
||||||
let image = encodeURIComponent(document.getElementById('image').value);
|
let image = encodeURIComponent(document.getElementById('image').value);
|
||||||
let twitter = encodeURIComponent(document.getElementById('twitter').value);
|
let bluesky = encodeURIComponent(document.getElementById('bluesky').value);
|
||||||
|
let fediverse = encodeURIComponent(document.getElementById('fediverse').value);
|
||||||
let github = encodeURIComponent(document.getElementById('github').value);
|
let github = encodeURIComponent(document.getElementById('github').value);
|
||||||
let website = encodeURIComponent(document.getElementById('website').value);
|
let website = encodeURIComponent(document.getElementById('website').value);
|
||||||
let bgColor = encodeURIComponent(document.getElementById('bgColor').value);
|
let bgColor = encodeURIComponent(document.getElementById('bgColor').value);
|
||||||
let fgColor = encodeURIComponent(document.getElementById('fgColor').value);
|
let fgColor = encodeURIComponent(document.getElementById('fgColor').value);
|
||||||
|
|
||||||
let url = `/opengraph/?siteTitle=${siteTitle}&title=${title}&tags=${tags}&image=${image}&twitter=${twitter}&github=${github}&website=${website}&bgColor=${bgColor}&fgColor=${fgColor}`;
|
let url = `/opengraph/?siteTitle=${siteTitle}&title=${title}&tags=${tags}&image=${image}&bluesky=${bluesky}&fediverse=${fediverse}&github=${github}&website=${website}&bgColor=${bgColor}&fgColor=${fgColor}`;
|
||||||
document.getElementById('exampleImage').src = url;
|
document.getElementById('exampleImage').src = url;
|
||||||
document.getElementById('imageURL').value = `https://opengraph.cluster.fun${url}`;
|
document.getElementById('imageURL').value = `https://opengraph.cluster.fun${url}`;
|
||||||
}
|
}
|
||||||
|
57
main.go
57
main.go
@@ -5,23 +5,35 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/canhlinh/svg2png"
|
"github.com/canhlinh/svg2png"
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/patrickmn/go-cache"
|
"github.com/patrickmn/go-cache"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/compress"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||||
|
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed index.html svg.tmpl
|
//go:embed index.html svg.tmpl
|
||||||
|
|
||||||
var content embed.FS
|
var content embed.FS
|
||||||
|
|
||||||
|
var chrome *svg2png.Chrome
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
chrome = svg2png.NewChrome().SetHeight(600).SetWith(1200).SetTimeout(10 * time.Second)
|
||||||
|
ch := cache.New(24*time.Hour, 48*time.Hour)
|
||||||
|
|
||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
ch := cache.New(5*24*time.Hour, 7*24*time.Hour)
|
app.Use(compress.New())
|
||||||
|
app.Use(cors.New())
|
||||||
|
app.Use(logger.New())
|
||||||
|
|
||||||
app.Get("/", func(c *fiber.Ctx) error {
|
app.Get("/", func(c *fiber.Ctx) error {
|
||||||
c.Type("html", "UTF8")
|
c.Type("html", "UTF8")
|
||||||
@@ -31,27 +43,30 @@ func main() {
|
|||||||
|
|
||||||
app.Get("/opengraph", func(c *fiber.Ctx) error {
|
app.Get("/opengraph", func(c *fiber.Ctx) error {
|
||||||
vars := map[string]string{
|
vars := map[string]string{
|
||||||
"siteTitle": c.Query("siteTitle", ""),
|
"siteTitle": ensureDecoded(c.Query("siteTitle", "")),
|
||||||
"title": c.Query("title", ""),
|
"title": ensureDecoded(c.Query("title", "")),
|
||||||
"tags": c.Query("tags", ""),
|
"tags": ensureDecoded(c.Query("tags", "")),
|
||||||
"image": c.Query("image", ""),
|
"image": ensureDecoded(c.Query("image", "")),
|
||||||
"twitter": c.Query("twitter", ""),
|
"twitter": stripAt(ensureDecoded(c.Query("twitter", ""))),
|
||||||
"github": c.Query("github", ""),
|
"bluesky": stripAt(ensureDecoded(c.Query("bluesky", ""))),
|
||||||
"website": c.Query("website", ""),
|
"fediverse": stripAt(ensureDecoded(c.Query("fediverse", ""))),
|
||||||
"bgColor": c.Query("bgColor", c.Query("bgColour", "#fff")),
|
"github": stripAt(ensureDecoded(c.Query("github", ""))),
|
||||||
"fgColor": c.Query("fgColor", c.Query("fgColour", "#2B414D")),
|
"website": ensureDecoded(c.Query("website", "")),
|
||||||
|
"bgColor": ensureDecoded(c.Query("bgColor", c.Query("bgColour", "#fff"))),
|
||||||
|
"fgColor": ensureDecoded(c.Query("fgColor", c.Query("fgColour", "#2B414D"))),
|
||||||
}
|
}
|
||||||
|
|
||||||
key := generateKey(vars)
|
key := generateKey(vars)
|
||||||
|
|
||||||
png, found := ch.Get(key)
|
png, found := ch.Get(key)
|
||||||
if !found {
|
if !found || len(png.([]byte)) == 0 {
|
||||||
var err error
|
var err error
|
||||||
png, err = generateImage(vars)
|
png, err = generateImage(vars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
fmt.Println(err)
|
||||||
|
return c.SendStatus(500)
|
||||||
}
|
}
|
||||||
ch.Set(key, png, -1)
|
ch.Set(key, png, cache.DefaultExpiration)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Type("png")
|
c.Type("png")
|
||||||
@@ -82,7 +97,6 @@ func generateImage(vars map[string]string) ([]byte, error) {
|
|||||||
|
|
||||||
imageFile, err := os.CreateTemp(os.TempDir(), "img-*.png")
|
imageFile, err := os.CreateTemp(os.TempDir(), "img-*.png")
|
||||||
|
|
||||||
chrome := svg2png.NewChrome().SetHeight(600).SetWith(1200)
|
|
||||||
if err := chrome.Screenshoot(fmt.Sprintf("file://%s", file.Name()), imageFile.Name()); err != nil {
|
if err := chrome.Screenshoot(fmt.Sprintf("file://%s", file.Name()), imageFile.Name()); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -90,3 +104,16 @@ func generateImage(vars map[string]string) ([]byte, error) {
|
|||||||
|
|
||||||
return os.ReadFile(imageFile.Name())
|
return os.ReadFile(imageFile.Name())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Some sites (LinkedIn) encode the already encoded URL so we need to double-decode to be sure
|
||||||
|
func ensureDecoded(str string) string {
|
||||||
|
decoded, err := url.QueryUnescape(str)
|
||||||
|
if err != nil {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return decoded
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripAt(str string) string {
|
||||||
|
return strings.TrimPrefix(str, "@")
|
||||||
|
}
|
||||||
|
56
svg.tmpl
56
svg.tmpl
@@ -42,15 +42,19 @@
|
|||||||
font-family: 'Roboto Bold';
|
font-family: 'Roboto Bold';
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 80px;
|
top: 100px;
|
||||||
left: 50px;
|
left: 50px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
width: 800px;
|
width: 780px;
|
||||||
font-size: 70px;
|
font-size: 80px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
line-height: 70px;
|
line-height: 75px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-family: 'Roboto Medium';
|
font-family: 'Roboto Medium';
|
||||||
@@ -58,23 +62,26 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50px;
|
top: 50px;
|
||||||
left: 50px;
|
left: 50px;
|
||||||
|
font-size: 40px;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
|
width: 780px;
|
||||||
}
|
}
|
||||||
#profilePic {
|
#profilePic {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 50px;
|
top: 50px;
|
||||||
right: 50px;
|
right: 50px;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
|
width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
left: 50px;
|
left: 50px;
|
||||||
top: 400px;
|
top: 420px;
|
||||||
width: 800px;
|
width: 800px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
font-size: 15px;
|
font-size: 25px;
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -90,7 +97,7 @@
|
|||||||
bottom: 25px;
|
bottom: 25px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
font-size: 15px;
|
font-size: 20px;
|
||||||
font-weight: 200;
|
font-weight: 200;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -99,7 +106,7 @@
|
|||||||
fill: var(--foreground-color);
|
fill: var(--foreground-color);
|
||||||
}
|
}
|
||||||
.social span {
|
.social span {
|
||||||
margin: 0 3px;
|
margin: 0 5px;
|
||||||
}
|
}
|
||||||
.social svg:not(:first-of-type) {
|
.social svg:not(:first-of-type) {
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
@@ -154,6 +161,39 @@
|
|||||||
<span>@{{ . }}</span>
|
<span>@{{ . }}</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
{{ with .bluesky}}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512" height="25px" width="25px">
|
||||||
|
<path d="m0 0H512V512H0" fill="#fff"/>
|
||||||
|
<path d="M159 126c-28-22-74-38-74 14 0 11 6 88 9 101 13 43 57 54 97 48-69 11-87 50-49 89 72 75 104-18 112-42l2-5 2 5c8 24 40 117 112 42 38-39 20-78-49-89 40 6 84-5 97-48 3-13 9-90 9-101 0-52-46-36-74-14-39 29-82 89-97 121-15-32-58-92-97-121Z" />
|
||||||
|
</svg>
|
||||||
|
<span>@{{ . }}</span>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
|
{{ with .fediverse}}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" height="25px" width="25px">
|
||||||
|
<g stroke-width="25">
|
||||||
|
<path stroke="#000" d="m106 179 31 197" />
|
||||||
|
<path stroke="#000" d="m137 375 201 32" />
|
||||||
|
<path stroke="#000" d="m423 229-90 178" />
|
||||||
|
<path stroke="#000" d="m282 88 141 141" />
|
||||||
|
<path stroke="#000" d="m105 179 178-90" />
|
||||||
|
</g>
|
||||||
|
<path fill="#000" d="m276 125h25l12 70-26-4m31 39-26-4 23 146h25" />
|
||||||
|
<path fill="#000" d="m164 347v28l136-69-4-26m36 10-4-26 67-34v28" />
|
||||||
|
<path fill="#000" d="m125 180v35l97 98 23-12m2 37 23-12 45 45v35" />
|
||||||
|
<path fill="#000" d="m254 118h28l-72 141-19-19m3 51-19-19-39 77h28" />
|
||||||
|
<path fill="#000" d="m137 171v25l66 11 12-23m24 29 12-24 140 23v25" />
|
||||||
|
<g stroke-width="3.5" stroke="#000">
|
||||||
|
<circle cx="106" cy="179" r="39" fill="#000" />
|
||||||
|
<circle cx="333" cy="406" r="39" fill="#000" />
|
||||||
|
<circle cx="137" cy="375" r="39" fill="#000" />
|
||||||
|
<circle cx="283" cy="88.5" r="39" fill="#000" />
|
||||||
|
<circle cx="423" cy="230" r="39" fill="#000" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<span>@{{ . }}</span>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ with .github }}
|
{{ with .github }}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 291.32 291.32" height="25px" width="25px">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 291.32 291.32" height="25px" width="25px">
|
||||||
<path d="M145.66,0C65.219,0,0,65.219,0,145.66c0,80.45,65.219,145.66,145.66,145.66
|
<path d="M145.66,0C65.219,0,0,65.219,0,145.66c0,80.45,65.219,145.66,145.66,145.66
|
||||||
|
Reference in New Issue
Block a user