commit 32a4e658f0c7ff874461f3b2210b7a7f2d0e20b5 Author: Marcus Noble Date: Tue May 11 05:23:11 2021 +0100 Initial release diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..04eb75d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +Dockerfile +README.md +Makefile +logo.png diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3fe4eff --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2c09645 --- /dev/null +++ b/Makefile @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d3fe68 --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +![OpenGraph-Image-Gen](logo.png) + +Dynamically generate OpenGraph social share images + +Available at https://opengraph.cluster.fun/ + +## Example + +```html + +``` + +![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) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..bc9a39d --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..cf4bdc6 --- /dev/null +++ b/go.sum @@ -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= diff --git a/index.html b/index.html new file mode 100644 index 0000000..9dd1fc4 --- /dev/null +++ b/index.html @@ -0,0 +1,146 @@ + + + + + + + OpenGraph-Image-Gen + + + + + + + + + + + + + + + + + + + +
+

+ OpenGraph-Image-Gen + +

+ +
+ Dynamically generate OpenGraph social share images +
+ +
+
+
+ + + + + + + +
+ + +
+ +

All values are optional and their elements can be hidden from the image by leaving the value blank.

+ +
+ +
Live Preview
+
+ + +
+
+ +
+ Source code available on GitHub, GitLab, Bitbucket & my own Gitea server. +
+
+ +
+
+
+ +
+
+
+ + + + diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..df4b4cb Binary files /dev/null and b/logo.png differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..184074c --- /dev/null +++ b/main.go @@ -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()) +} diff --git a/svg.tmpl b/svg.tmpl new file mode 100644 index 0000000..c9a4d30 --- /dev/null +++ b/svg.tmpl @@ -0,0 +1,190 @@ + + + + + + + +
+

{{ .siteTitle }}

+

{{ .title }}

+ + +
+ {{ with .tags }} + + + + + {{ range (split .) }} + #{{ . }} + {{ end }} + {{ end }} +
+ +
+ + +
+
+
+ +