Initial release

This commit is contained in:
Marcus Noble 2021-05-11 05:23:11 +01:00
commit 32a4e658f0
10 changed files with 614 additions and 0 deletions

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
Dockerfile
README.md
Makefile
logo.png

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM golang:1.16-alpine AS builder
RUN apk update && apk add --no-cache git && apk add -U --no-cache ca-certificates
WORKDIR /app/
ADD go.mod go.sum ./
RUN go mod download
ADD . .
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-w -s" -o gopengraph-image-gen .
FROM westy92/headless-chrome-alpine
WORKDIR /app/
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/gopengraph-image-gen /app/gopengraph-image-gen
RUN mkdir -p /tmp
ENTRYPOINT ["/app/gopengraph-image-gen"]

61
Makefile Normal file
View File

@ -0,0 +1,61 @@
.DEFAULT_GOAL := default
IMAGE ?= docker.cluster.fun/averagemarcus/opengraph-image-gen:latest
.PHONY: test # Run all tests, linting and format checks
test: lint check-format run-tests
.PHONY: lint # Perform lint checks against code
lint:
@go vet && golint -set_exit_status ./...
.PHONY: check-format # Checks code formatting and returns a non-zero exit code if formatting errors found
check-format:
@gofmt -e -l .
.PHONY: format # Performs automatic format fixes on all code
format:
@gofmt -s -w .
.PHONY: run-tests # Runs all tests
run-tests:
@echo "⚠️ 'run-tests' unimplemented"
.PHONY: fetch-deps # Fetch all project dependencies
fetch-deps:
@go mod tidy
.PHONY: build # Build the project
build: lint check-format fetch-deps
@go build -o opengraph-image-gen main.go
.PHONY: docker-build # Build the docker image
docker-build:
@docker build -t $(IMAGE) .
.PHONY: docker-publish # Push the docker image to the remote registry
docker-publish:
@docker push $(IMAGE)
.PHONY: run # Run the application
run:
@go run .
.PHONY: ci # Perform CI specific tasks to perform on a pull request
ci:
@echo "⚠️ 'ci' unimplemented"
.PHONY: release # Release the latest version of the application
release:
@kubectl --namespace opengraph-image-gen set image deployment opengraph-image-gen web=docker.cluster.fun/averagemarcus/opengraph-image-gen:$(SHA)
.PHONY: help # Show this list of commands
help:
@echo "opengraph-image-gen"
@echo "Usage: make [target]"
@echo ""
@echo "target description" | expand -t20
@echo "-----------------------------------"
@grep '^.PHONY: .* #' Makefile | sed 's/\.PHONY: \(.*\) # \(.*\)/\1 \2/' | expand -t20
default: test build

47
README.md Normal file
View File

@ -0,0 +1,47 @@
![OpenGraph-Image-Gen](logo.png)
Dynamically generate OpenGraph social share images
Available at https://opengraph.cluster.fun/
## Example
```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" />
```
![Preview Image](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)
## Features
* Dynamically generate a PNG image for use as an OpenGraph share image
* Ideally sized for Twitter previews
* All text elements configurable
* Configurable colours
* All text fields optional
## Building from source
With Docker:
```sh
make docker-build
```
Standalone:
```sh
make build
```
## Contributing
If you find a bug or have an idea for a new feature please [raise an issue](issues/new) to discuss it.
Pull requests are welcomed but please try and follow similar code style as the rest of the project and ensure all tests and code checkers are passing.
Thank you 💛
## License
See [LICENSE](LICENSE)

12
go.mod Normal file
View File

@ -0,0 +1,12 @@
module opengraph-image-gen
go 1.16
require (
github.com/canhlinh/svg2png v0.0.0-20201124065332-6ba87c82371f
github.com/gofiber/fiber/v2 v2.9.0
github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/stretchr/testify v1.7.0 // indirect
)

48
go.sum Normal file
View File

@ -0,0 +1,48 @@
github.com/andybalholm/brotli v1.0.1 h1:KqhlKozYbRtJvsPrrEeXcO+N2l6NYT5A2QAFmSULpEc=
github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/canhlinh/svg2png v0.0.0-20201124065332-6ba87c82371f h1:Km7aXA1/+77OZ6mq8VV/QJ9nP6y4OUwxj+GQ5nW7X5Y=
github.com/canhlinh/svg2png v0.0.0-20201124065332-6ba87c82371f/go.mod h1:u13M4umOwLc1fTX2itKxGff/6S+YWc7l15kJGtm2IJY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofiber/fiber/v2 v2.9.0 h1:sZsTKlbyGGZ0UdTUn3ItQv5J9FTQUc4J3OS+03lE5m0=
github.com/gofiber/fiber/v2 v2.9.0/go.mod h1:Ah3IJikrKNRepl/HuVawppS25X7FWohwfCSRn7kJG28=
github.com/klauspost/compress v1.11.8/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4=
github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2 h1:YocNLcTBdEdvY3iDK6jfWXvEaM5OCKkjxPKoJRdB3Gg=
github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2/go.mod h1:76rfSfYPWj01Z85hUf/ituArm797mNKcvINh1OlsZKo=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.23.0 h1:0ufwSD9BhWa6f8HWdmdq4FHQ23peRo3Ng/Qs8m5NcFs=
github.com/valyala/fasthttp v1.23.0/go.mod h1:0mw2RjXGOzxf4NL2jni3gUQ7LfjjUSiG5sskOUUSEpU=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a h1:0R4NLDRDZX6JcmhJgXi5E4b8Wg84ihbmUKp/GvSPEzc=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226101413-39120d07d75e/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073 h1:8qxJSnu+7dRq6upnbntrmriWByIakBuct5OM/MdQC1M=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

146
index.html Normal file
View File

@ -0,0 +1,146 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenGraph-Image-Gen</title>
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' viewBox='0 0 256 245' style='height: 50px; fill: %2371cad1; '%3E%3Cpath d='M253.5 121.1a9.3 9.3 0 00-14.6-4.3l-42.4-12 30.4-10a9.3 9.3 0 10-3-9.3l-30.8 10 26.7-34.9a9.2 9.2 0 0010.2-12 9.3 9.3 0 00-17.7 5.7l-29.8 38.9a11.2 11.2 0 00-11.1 9.4l-33.5 10.9a13 13 0 00-5-3.7V74.6a11.2 11.2 0 005.6-14l26.8-39.9h.8a9.3 9.3 0 10-8.6-5.8l-24.6 36.7V19.4a9.3 9.3 0 10-9.8 0v32.5l-25-36.3A9.2 9.2 0 0090 2.2a9.3 9.3 0 000 18.6l27.5 40a11.2 11.2 0 005.7 13.8v35.2c-2 .8-3.7 2-5 3.7l-33.5-10.9a11.2 11.2 0 00-11.3-9.4L43.3 55l.4-.8a9.3 9.3 0 10-8.3 6.4l27.2 34.7-30.5-9.9a9.3 9.3 0 10-3 9.3l30.7 10-42 12.6c-1-1-2.2-1.8-3.7-2.2a9.3 9.3 0 106 11.7L66.4 113a11.2 11.2 0 0015.5-1.1l33.2 10.8c.1 2.1.8 4.1 1.9 5.9l-20.8 28.6a11.2 11.2 0 00-12.4 7.7l-45.4 16.7a9.3 9.3 0 102.9 9.3l41.4-15.2-18.9 26a9.3 9.3 0 108 5.8l19-26.2-1.1 43.9a9.2 9.2 0 00-1.2 15.7 9.3 9.3 0 0011-15l1.1-48.2a11.2 11.2 0 003.7-15l20.6-28.4a13 13 0 006.2 0l20.7 28.5a11.2 11.2 0 003.1 14.5l2.3 48.2-.7.4a9.3 9.3 0 1010.5-.3l-2.1-44.8 19.4 26.7a9.3 9.3 0 107.9-5.8l-19-26 41.4 14.4c.2 1.4.7 2.7 1.6 4a9.3 9.3 0 102-13l-45.9-16a11.2 11.2 0 00-12.5-7.9L139 128.7c1-1.8 1.8-3.8 2-6L174 112a11.2 11.2 0 0015.7 1l45.9 13 .2.9a9.3 9.3 0 1017.6-5.8z'/%3E%3C/svg%3E">
<meta property="og:title" content="OpenGraph-Image-Gen">
<meta property="og:site_name" content="OpenGraph-Image-Gen">
<meta property="og:url" content="https://opengraph.cluster.fun">
<meta property="og:description" content="Dynamically generate OpenGraph social share images">
<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 name="twitter:card" content="summary" />
<meta name="twitter:creator" content="@Marcus_Noble_" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.css">
<link rel="stylesheet" href="https://githubraw.com/AverageMarcus/milligram/master/dist/milligram.min.css">
<style>
body {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
}
img {
max-width: 100%;
}
textarea {
height: 200px;
}
fieldset label {
width: 49%;
padding: 0 5px;
display: inline-block;
}
</style>
</head>
<body>
<div class="container">
<h1 class="heading-fancy">
OpenGraph-Image-Gen
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 256 245" style="height: 50px; fill: #71cad1; border: 3px solid #ee70a8; border-radius: 100px;"><path d="M253.5 121.1a9.3 9.3 0 00-14.6-4.3l-42.4-12 30.4-10a9.3 9.3 0 10-3-9.3l-30.8 10 26.7-34.9a9.2 9.2 0 0010.2-12 9.3 9.3 0 00-17.7 5.7l-29.8 38.9a11.2 11.2 0 00-11.1 9.4l-33.5 10.9a13 13 0 00-5-3.7V74.6a11.2 11.2 0 005.6-14l26.8-39.9h.8a9.3 9.3 0 10-8.6-5.8l-24.6 36.7V19.4a9.3 9.3 0 10-9.8 0v32.5l-25-36.3A9.2 9.2 0 0090 2.2a9.3 9.3 0 000 18.6l27.5 40a11.2 11.2 0 005.7 13.8v35.2c-2 .8-3.7 2-5 3.7l-33.5-10.9a11.2 11.2 0 00-11.3-9.4L43.3 55l.4-.8a9.3 9.3 0 10-8.3 6.4l27.2 34.7-30.5-9.9a9.3 9.3 0 10-3 9.3l30.7 10-42 12.6c-1-1-2.2-1.8-3.7-2.2a9.3 9.3 0 106 11.7L66.4 113a11.2 11.2 0 0015.5-1.1l33.2 10.8c.1 2.1.8 4.1 1.9 5.9l-20.8 28.6a11.2 11.2 0 00-12.4 7.7l-45.4 16.7a9.3 9.3 0 102.9 9.3l41.4-15.2-18.9 26a9.3 9.3 0 108 5.8l19-26.2-1.1 43.9a9.2 9.2 0 00-1.2 15.7 9.3 9.3 0 0011-15l1.1-48.2a11.2 11.2 0 003.7-15l20.6-28.4a13 13 0 006.2 0l20.7 28.5a11.2 11.2 0 003.1 14.5l2.3 48.2-.7.4a9.3 9.3 0 1010.5-.3l-2.1-44.8 19.4 26.7a9.3 9.3 0 107.9-5.8l-19-26 41.4 14.4c.2 1.4.7 2.7 1.6 4a9.3 9.3 0 102-13l-45.9-16a11.2 11.2 0 00-12.5-7.9L139 128.7c1-1.8 1.8-3.8 2-6L174 112a11.2 11.2 0 0015.7 1l45.9 13 .2.9a9.3 9.3 0 1017.6-5.8z"/></svg>
</h1>
<blockquote>
Dynamically generate OpenGraph social share images
</blockquote>
<div class="row">
<form class="column column-80 column-offset-10">
<fieldset>
<label>Site Title: <input id="siteTitle" type="text" value="Example" /></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>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>GitHub Username: <input id="github" type="text" value="AverageMarcus" /></label>
<label>Website: <input id="website" type="text" value="www.MarcusNoble.co.uk" /></label>
<br />
<label>Background Colour: <input id="bgColor" type="color" value="#FFFFFF" /></label>
<label>Foreground Colour: <input id="fgColor" type="color" value="#263943" /></label>
</fieldset>
<p>All values are optional and their elements can be hidden from the image by leaving the value blank.</p>
<figure class="text-center">
<img id="exampleImage" src="/opengraph" />
<figcaption>Live Preview</figcaption>
</figure>
<label>
URL:
<input type="text" id="imageURL" readonly />
</label>
</form>
</div>
<div>
Source code available on <a href="https://github.com/AverageMarcus/OpenGraph-Image-Gen" target="_blank" rel="noopener noreferrer">GitHub</a>, <a href="https://gitlab.com/AverageMarcus/OpenGraph-Image-Gen" target="_blank" rel="noopener noreferrer">GitLab</a>, <a href="https://bitbucket.org/AverageMarcus/OpenGraph-Image-Gen/" target="_blank" rel="noopener noreferrer">Bitbucket</a> & <a href="https://git.cluster.fun/AverageMarcus/OpenGraph-Image-Gen" target="_blank" rel="noopener noreferrer">my own Gitea server</a>.
</div>
</div>
<div class="container">
<div class="row">
<div class="column column-60 column-offset-20">
<footer>
Made with
<svg height="20" class="fill-primary" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 449.3 449.3" xmlns:xlink="http://www.w3.org/1999/xlink"><title>love</title><g><path d="M0 162.7c1.5-7.7 2.7-15.4 4.5-23A125.5 125.5 0 0132 88a136.3 136.3 0 0162.7-40.6c8.3-2.9 17.7-3.2 26.6-3.7a134 134 0 0155.6 6.6c14.9 5.7 30 11 41 23.6 17-20 36.4-36.3 60-46.4 12-5.2 25.7-6.9 38.7-9.4a79.4 79.4 0 0140.3 3.2 96.4 96.4 0 0143.2 26 209.8 209.8 0 0137.8 55.4 133.2 133.2 0 0111 65.7c-3.2 42.2-21 79-41.5 114.8a431.2 431.2 0 01-47.6 64.3c-19.6 23-39.7 45.7-59.6 68.5-3.7 4.3-7.2 9-11.7 12.4-7.3 5.4-15.9 4.9-23.8 1.5-21.9-9.2-43.8-18.5-65.3-28.5a520.1 520.1 0 01-98-58.7c-28.2-21.5-55.5-44.3-74-75.3a183.8 183.8 0 01-25-61.4c-1-6.2-1.6-12.6-2.4-18.8v-24.5zM138 281l2.5.1c0 2.1.3 4.3 0 6.3l-9 55.5c-.3 1.6 0 4.2 1 5 5.8 4.1 11.8 7.8 17.9 11.8l11-59.7 2.5.3c3.7 21.6-6.3 42-7.6 63.5l19.4 9.6c.5-18 2.2-35 6.9-51.6 1.3 4 1.5 8 1.2 12-.7 11.2-1 22.5-2.6 33.7-1 7.3 1.9 10.6 8 13.3 27 11.7 54.1 23.5 81 35.7 5.5 2.5 8.8 1.5 12.6-2.9 12.5-14.4 25.7-28.1 38-42.7 17.2-20.2 34.5-40.5 50.5-61.7 29.5-39.2 52.7-81.7 61-131 3.6-21.7 2-43-5.6-63.5a176 176 0 00-33.9-54.5 84.8 84.8 0 00-38-26 91.2 91.2 0 00-44.2-2.5c-13 2-25.6 5.1-37.2 12.3a208.6 208.6 0 00-45.9 37c-5.7 6.3-7.8 6.4-15 1.4-5.7-4-11-8.8-17.2-11.9-15.1-7.5-30.7-14.4-48-14.9-13.7-.4-28-2-41.2 1-36 8.4-62.7 30-80.3 62.8a111 111 0 00-11.7 56.6c.3 10 1.4 20 2.2 29.9 7-18.9 11-38.3 19.7-56.3.6 3 .6 6 0 8.7l-14.5 53.2c-.7 2.6-2 5.7-1.2 8 2.7 8.4 6.2 16.6 9.4 24.8 0-20 13.7-63.9 21.8-66.8 0 .6.3 1.1.2 1.6a11936 11936 0 01-17 64c-.3 1.4-1.6 2.6-2.9 4.5l9.7 17a573 573 0 0120-67.3l2.9.7c-1 5-1.5 10-2.8 14.9-4.7 17.2-9.6 34.3-14.2 51.5-.7 2.5-1.5 6-.3 7.8 3.6 5.6 8 10.7 12.4 16.1 1.8-8.6 3.1-16.6 5.2-24.4 3.1-11.5 6.6-22.9 10.2-34.3.8-2.6 2.7-5 4-7.4l2 .8c-.1 1.6-.1 3.4-.5 5l-12.2 47.4c-4.6 17.9-4.3 18.9 7.8 29.4 2.2 1.8 4.5 3.5 7 5.3 3.7-31 12.5-64.7 18.4-68-.3 3.5-.3 6.6-.9 9.6l-12.7 58.8c-.3 1.2-.7 3-.1 3.5 4.7 4.4 9.6 8.6 14.5 12.9 3.5-21.4 7.6-41.7 13.6-61.6 3.4 8.7.6 17-.8 25.2-1.9 11.6-4.5 23.1-6.5 34.7-.4 2.3-.4 6 1 7 4.8 4.2 10.3 7.4 16 11.3 3.9-21.5 5.5-42.6 12.6-62.5z"/><path d="M323.2 180.5a24 24 0 01-24.5-19.7c-2-9.7.7-20.2 15-27 16.6-7.8 38.3 2.3 41.6 19.5 1.2 6.6-1.6 12.1-6 16.3a35.1 35.1 0 01-26 11z"/><path d="M138.9 167.1a31 31 0 0125 12.7 22 22 0 01-13.6 34.9 29.9 29.9 0 01-31-9.9c-6-7-7.6-15.3-4.1-24.1 3.7-9.5 12-12 21-13.5.8-.2 1.8 0 2.7 0z"/><path d="M233.2 202c-18.5-.4-33.7-13.6-34-29.7 0-4.2.4-8 5.4-8.7 4.5-.7 6.6 2.3 8 6 2.3 6.6 5.5 12.4 12.4 15.4 7.5 3.2 14 1.5 20.4-3.3 6.2-4.7 6.4-11 4.7-17.5-1.4-5.2.4-8.4 4.7-10.2 4.3-1.7 9 1.3 10.9 6.2 7.2 19.8-5.7 34.6-22.8 40.2-3 1-6.5 1-9.7 1.6z"/><path d="M201.2 384.7c-.9-2-2.7-4.2-2.5-6.2 1.2-14 2.8-28.1 4.4-42.2 0-.9.7-1.7 1-2.6l2.6 1.1-3.1 49.1-2.4.8z"/><path d="M222 387.4c-4.8-5.7-5-15-1-37.8 3.1 3 4 30.5 1 37.8z"/><path d="M240.9 361.7l-1.4 20.2c-4.8-3.3-4.6-11.9-.2-20.4l1.6.2z"/><path d="M257.4 380.4c-4.1-5.2-3.7-9.7 1-14.7l-1 14.7z"/></g></svg>
by <a href="https://marcusnoble.co.uk" class="fancy-link">Marcus Noble</a>
</footer>
</div>
</div>
</div>
<script>
function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
var later = function() {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
function loadimage() {
let siteTitle = encodeURIComponent(document.getElementById('siteTitle').value);
let title = encodeURIComponent(document.getElementById('title').value);
let tags = encodeURIComponent(document.getElementById('tags').value);
let image = encodeURIComponent(document.getElementById('image').value);
let twitter = encodeURIComponent(document.getElementById('twitter').value);
let github = encodeURIComponent(document.getElementById('github').value);
let website = encodeURIComponent(document.getElementById('website').value);
let bgColor = encodeURIComponent(document.getElementById('bgColor').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}`;
document.getElementById('exampleImage').src = url;
document.getElementById('imageURL').value = `https://opengraph.cluster.fun${url}`;
}
[...document.querySelectorAll('fieldset input')].forEach(el => {
el.addEventListener('keyup', debounce(function() {
loadimage();
}, 1000));
el.addEventListener('change', function() {
loadimage();
});
})
loadimage()
</script>
</body>
</html>

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

92
main.go Normal file
View File

@ -0,0 +1,92 @@
package main
import (
"embed"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"strings"
"text/template"
"time"
"github.com/canhlinh/svg2png"
"github.com/gofiber/fiber/v2"
"github.com/patrickmn/go-cache"
)
//go:embed index.html svg.tmpl
var content embed.FS
func main() {
app := fiber.New()
ch := cache.New(5*24*time.Hour, 7*24*time.Hour)
app.Get("/", func(c *fiber.Ctx) error {
c.Type("html", "UTF8")
body, _ := content.ReadFile("index.html")
return c.Send(body)
})
app.Get("/opengraph", func(c *fiber.Ctx) error {
vars := map[string]string{
"siteTitle": c.Query("siteTitle", ""),
"title": c.Query("title", ""),
"tags": c.Query("tags", ""),
"image": c.Query("image", ""),
"twitter": c.Query("twitter", ""),
"github": c.Query("github", ""),
"website": c.Query("website", ""),
"bgColor": c.Query("bgColor", c.Query("bgColour", "#fff")),
"fgColor": c.Query("fgColor", c.Query("fgColour", "#2B414D")),
}
key := generateKey(vars)
png, found := ch.Get(key)
if !found {
var err error
png, err = generateImage(vars)
if err != nil {
return err
}
ch.Set(key, png, -1)
}
c.Type("png")
return c.Send(png.([]byte))
})
app.Listen(":3000")
}
func generateKey(vars map[string]string) string {
varsByte, _ := json.Marshal(vars)
return base64.StdEncoding.EncodeToString(varsByte)
}
func generateImage(vars map[string]string) ([]byte, error) {
file, err := os.CreateTemp(os.TempDir(), "img-*.html")
if err != nil {
return nil, err
}
defer os.Remove(file.Name())
t := template.Must(template.New("svg.tmpl").Funcs(template.FuncMap{
"split": func(input string) []string {
return strings.Split(input, ",")
},
}).ParseFS(content, "svg.tmpl"))
t.Execute(file, vars)
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 {
return nil, err
}
defer os.Remove(imageFile.Name())
return os.ReadFile(imageFile.Name())
}

190
svg.tmpl Normal file
View File

@ -0,0 +1,190 @@
<DOCTYPE html>
<html>
<head><style>html, head, body { margin: 0; padding: 0; background-color: {{ .fgColor }}; } * { box-sizing: border-box; }</style></head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1200px" height="600px" style="border: 15px solid {{ .fgColor }}; border-radius: 20px;">
<foreignObject x="0" y="0" width="100%" height="100%">
<style>
@font-face {
font-family: "Roboto Bold";
src: url("https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmYUtfBBc4AMP6lQ.woff2") format("woff2");
font-style: normal;
unicode-range: U+0-FF, U+131, U+152-153, U+2BB-2BC, U+2C6, U+2DA, U+2DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Roboto Medium";
src: url("https://fonts.gstatic.com/s/roboto/v18/KFOlCnqEu92Fr1MmEU9fBBc4AMP6lQ.woff2") format("woff2");
font-style: normal;
font-weight: 500;
unicode-range: U+0-FF, U+131, U+152-153, U+2BB-2BC, U+2C6, U+2DA, U+2DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
font-family: "Roboto Light";
src: url("https://fonts.gstatic.com/s/roboto/v18/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2") format("woff2");
font-style: normal;
font-weight: 400;
unicode-range: U+0-FF, U+131, U+152-153, U+2BB-2BC, U+2C6, U+2DA, U+2DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
* {
font-family: 'Roboto Light';
}
:root {
--background-color: {{ .bgColor }};
--foreground-color: {{ .fgColor }};
}
#main {
width: 100%;
height: 100%;
background: var(--background-color);
color: var(--foreground-color);
}
h1 {
font-family: 'Roboto Bold';
margin: 0;
position: fixed;
top: 80px;
left: 50px;
height: 300px;
width: 800px;
font-size: 70px;
font-weight: 900;
line-height: 70px;
overflow: hidden;
text-overflow: ellipsis;
}
h2 {
font-family: 'Roboto Medium';
margin: 0;
position: fixed;
top: 50px;
left: 50px;
font-weight: 900;
}
#profilePic {
position: fixed;
top: 50px;
right: 50px;
border-radius: 50px;
}
.tags {
position: fixed;
left: 50px;
top: 400px;
width: 800px;
height: 25px;
line-height: 25px;
font-size: 15px;
font-weight: 200;
display: flex;
flex-direction: row;
}
.tags span {
margin: 0 3px;
}
.social {
position: fixed;
left: 50px;
right: 50px;
bottom: 25px;
height: 25px;
line-height: 25px;
font-size: 15px;
font-weight: 200;
display: flex;
flex-direction: row;
color: var(--foreground-color);
fill: var(--foreground-color);
}
.social span {
margin: 0 3px;
}
.social svg:not(:first-of-type) {
margin-left: 20px;
}
hr {
position: fixed;
height: 15px;
left: 0;
right: 0;
bottom: 65px;
border: none;
outline: none;
background-color: var(--foreground-color);
}
</style>
<div id="main">
<h2>{{ .siteTitle }}</h2>
<h1>{{ .title }}</h1>
<img id="profilePic" src="{{ with .image }}{{ . }}{{ end }}" />
<div class="tags">
{{ with .tags }}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tag meta-icon">
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path>
<line x1="7" y1="7" x2="7" y2="7"></line>
</svg>
{{ range (split .) }}
<span>#{{ . }}</span>
{{ end }}
{{ end }}
</div>
<hr />
<div class="social">
{{ with .twitter}}
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 310 310" height="25px" width="25px">
<path d="M302.973,57.388c-4.87,2.16-9.877,3.983-14.993,5.463c6.057-6.85,10.675-14.91,13.494-23.73
c0.632-1.977-0.023-4.141-1.648-5.434c-1.623-1.294-3.878-1.449-5.665-0.39c-10.865,6.444-22.587,11.075-34.878,13.783
c-12.381-12.098-29.197-18.983-46.581-18.983c-36.695,0-66.549,29.853-66.549,66.547c0,2.89,0.183,5.764,0.545,8.598
C101.163,99.244,58.83,76.863,29.76,41.204c-1.036-1.271-2.632-1.956-4.266-1.825c-1.635,0.128-3.104,1.05-3.93,2.467
c-5.896,10.117-9.013,21.688-9.013,33.461c0,16.035,5.725,31.249,15.838,43.137c-3.075-1.065-6.059-2.396-8.907-3.977
c-1.529-0.851-3.395-0.838-4.914,0.033c-1.52,0.871-2.473,2.473-2.513,4.224c-0.007,0.295-0.007,0.59-0.007,0.889
c0,23.935,12.882,45.484,32.577,57.229c-1.692-0.169-3.383-0.414-5.063-0.735c-1.732-0.331-3.513,0.276-4.681,1.597
c-1.17,1.32-1.557,3.16-1.018,4.84c7.29,22.76,26.059,39.501,48.749,44.605c-18.819,11.787-40.34,17.961-62.932,17.961
c-4.714,0-9.455-0.277-14.095-0.826c-2.305-0.274-4.509,1.087-5.294,3.279c-0.785,2.193,0.047,4.638,2.008,5.895
c29.023,18.609,62.582,28.445,97.047,28.445c67.754,0,110.139-31.95,133.764-58.753c29.46-33.421,46.356-77.658,46.356-121.367
c0-1.826-0.028-3.67-0.084-5.508c11.623-8.757,21.63-19.355,29.773-31.536c1.237-1.85,1.103-4.295-0.33-5.998
C307.394,57.037,305.009,56.486,302.973,57.388z"/>
</svg>
<span>@{{ . }}</span>
{{ end }}
{{ 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">
<path d="M145.66,0C65.219,0,0,65.219,0,145.66c0,80.45,65.219,145.66,145.66,145.66
s145.66-65.21,145.66-145.66C291.319,65.219,226.1,0,145.66,0z M186.462,256.625c-0.838-11.398-1.775-25.518-1.83-31.235
c-0.364-4.388-0.838-15.549-11.434-22.677c42.068-3.523,62.087-26.774,63.526-57.499c1.202-17.497-5.754-32.883-18.107-45.3
c0.628-13.282-0.401-29.023-1.256-35.941c-9.486-2.731-31.608,8.949-37.79,13.947c-13.037-5.062-44.945-6.837-64.336,0
c-13.747-9.668-29.396-15.64-37.926-13.974c-7.875,17.452-2.813,33.948-1.275,35.914c-10.142,9.268-24.289,20.675-20.447,44.572
c6.163,35.04,30.816,53.94,70.508,58.564c-8.466,1.73-9.896,8.048-10.606,10.788c-26.656,10.997-34.275-6.791-37.644-11.425
c-11.188-13.847-21.23-9.832-21.849-9.614c-0.601,0.218-1.056,1.092-0.992,1.511c0.564,2.986,6.655,6.018,6.955,6.263
c8.257,6.154,11.316,17.27,13.2,20.438c11.844,19.473,39.374,11.398,39.638,11.562c0.018,1.702-0.191,16.032-0.355,27.184
C64.245,245.992,27.311,200.2,27.311,145.66c0-65.365,52.984-118.348,118.348-118.348S264.008,80.295,264.008,145.66
C264.008,196.668,231.69,239.992,186.462,256.625z"/>
</svg>
<span>{{ . }}</span>
{{ end }}
{{ with .website }}
<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">
<polygon points="478.609,55.652 478.609,155.826 33.391,155.826 33.391,55.652 0,55.652 0,456.348 33.391,456.348 33.391,189.217 478.609,189.217 478.609,456.348 512,456.348 512,55.652"/>
<rect x="33.391" y="22.261" width="445.217" height="33.391"/>
<rect x="33.391" y="456.348" width="445.217" height="33.391"/>
<rect x="411.826" y="89.044" width="33.391" height="33.391"/>
<rect x="345.043" y="89.044" width="33.391" height="33.391"/>
<rect x="77.913" y="233.739" width="356.174" height="33.391"/>
<rect x="77.913" y="311.652" width="178.087" height="33.391"/>
</svg>
<span>{{ . }}</span>
{{ end }}
</div>
</div>
</foreignObject>
</svg>
</body>
</html>