jekyll-og-image 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7498d6e0fca6a90a0c70efb97a4514293b396e20af0900cfa5c734d88ed2cbf0
4
- data.tar.gz: fea25610cb98a2b2954440c796800087a5379142b796c503ac7822034950fb17
3
+ metadata.gz: 5a95a1847180a7d4565e7e951467ff2293fb88e7182b08a58f362aee77f4a1d9
4
+ data.tar.gz: f11ab1b7b52feec3c6052b46884da96dd1973fc863293d74ac36ac7c22142bd4
5
5
  SHA512:
6
- metadata.gz: b6177ac6d1493b968a2eb2d33a13f84429d90be409839d2e08407a20e8b4296a5e9a50dc38d630f867ad113a931efb6fd9d6302d4a5732a167b82aec0e08dfa2
7
- data.tar.gz: 3661a2e8dcb52b274d73d2ef713bc6fa365b9a3102bc652098bf7759e0babe3b6c253133fafdcc3c7c0ef6d57272da47c78d1d937ac111efa1abff4231d25860
6
+ metadata.gz: e49a4a7fb8884c6f3898dae7ec54b8185809d1211544d16ad8cbe8b70a60aa4be81aff27a60f182873b6e62048f2fb9bc402dd92c1f4b3d8f8b1b8ceda411ec3
7
+ data.tar.gz: 5879cfbe269524ebf28e08dfd68f79d0e1782471b09f7dd74390133a7c62930a846957ac42e2351414f0a7c3d99738acc571905dc2449034771874fd5a6c2d6f
data/README.md CHANGED
@@ -1,16 +1,29 @@
1
- # Jekyll OG Image
1
+ <p align="center">
2
+ <img src="assets/icon.svg" alt="Jekyll OG Image" width="128" height="128">
3
+ </p>
2
4
 
3
- A Jekyll plugin to automatically generate open graph images for posts.
5
+ <h1 align="center">Jekyll OG Image</h1>
4
6
 
5
- [![Gem Version](https://badge.fury.io/rb/jekyll-og-image.svg)](https://badge.fury.io/rb/jekyll-og-image)
6
- [![Lint](https://github.com/igor-alexandrov/jekyll-og-image/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/igor-alexandrov/jekyll-og-image/actions/workflows/lint.yml)
7
- [![Tests](https://github.com/igor-alexandrov/jekyll-og-image/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/igor-alexandrov/jekyll-og-image/actions/workflows/tests.yml)
7
+ <p align="center">
8
+ A Jekyll plugin to automatically generate open graph images for posts.
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://badge.fury.io/rb/jekyll-og-image"><img src="https://badge.fury.io/rb/jekyll-og-image.svg" alt="Gem Version"></a>
13
+ <a href="https://github.com/igor-alexandrov/jekyll-og-image/actions/workflows/lint.yml"><img src="https://github.com/igor-alexandrov/jekyll-og-image/actions/workflows/lint.yml/badge.svg?branch=main" alt="Lint"></a>
14
+ <a href="https://github.com/igor-alexandrov/jekyll-og-image/actions/workflows/tests.yml"><img src="https://github.com/igor-alexandrov/jekyll-og-image/actions/workflows/tests.yml/badge.svg?branch=main" alt="Tests"></a>
15
+ </p>
16
+
17
+ <p align="center">
18
+ <img src="examples/1.png" alt="Example open graph image" width="600">
19
+ </p>
8
20
 
9
21
  ## Installation
10
22
 
11
23
  Add this line to your site's Gemfile:
12
24
 
13
25
  ```ruby
26
+ gem 'jekyll-seo-tag'
14
27
  gem 'jekyll-og-image'
15
28
  ```
16
29
 
@@ -22,13 +35,11 @@ plugins:
22
35
  - jekyll-og-image
23
36
  ```
24
37
 
25
- This plugin requires `libvips` to be installed. If you are using GitHub Pages to host your Jekyll site, don't forget to install `libvips` before running `jekyll build`. See the example below.
38
+ The plugin requires Ruby 3.2 or newer and [libvips](https://www.libvips.org/install.html). If you are using GitHub Pages to host your Jekyll site, don't forget to install `libvips` before running `jekyll build`. See the example below.
26
39
 
27
40
  ``` yaml
28
41
  jobs:
29
42
  build:
30
- needs:
31
- - lint
32
43
  runs-on: ubuntu-latest
33
44
  steps:
34
45
  - name: Checkout
@@ -40,16 +51,6 @@ jobs:
40
51
  ruby-version: .ruby-version
41
52
  bundler-cache: true
42
53
 
43
- - name: Set Node.js 20.x
44
- uses: actions/setup-node@v3
45
- with:
46
- node-version: 20.x
47
-
48
- - name: Run install
49
- uses: borales/actions-yarn@v4
50
- with:
51
- cmd: install
52
-
53
54
  - name: Update apt
54
55
  env:
55
56
  DEBIAN_FRONTEND: noninteractive
@@ -65,19 +66,28 @@ jobs:
65
66
  uses: actions/configure-pages@v5
66
67
 
67
68
  - name: Build with Jekyll
68
- run: ./bin/jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
69
+ run: bundle exec jekyll build --baseurl "${{ steps.pages.outputs.base_path }}"
69
70
  env:
70
71
  JEKYLL_ENV: production
71
72
 
72
73
  - name: Upload artifact
73
74
  # Automatically uploads an artifact from the './_site' directory by default
74
75
  uses: actions/upload-pages-artifact@v3
75
-
76
76
  ```
77
77
 
78
78
  ## Usage
79
79
 
80
- Jekyll OG Image works together with [jekyll-seo-tag](https://github.com/jekyll/jekyll-seo-tag) plugin. It automatically generates open graph images for posts and inserts them into the posts metadata.
80
+ Jekyll OG Image works together with the [jekyll-seo-tag](https://github.com/jekyll/jekyll-seo-tag) plugin. It generates open graph images for posts and sets them as the `image` in each post's front matter, which jekyll-seo-tag turns into `og:image` and `twitter:image` meta tags. jekyll-seo-tag isn't installed automatically, so add it to your Gemfile yourself, as shown above.
81
+
82
+ ### Regenerating images
83
+
84
+ An image is regenerated when anything that affects it changes: the document's title or metadata, the plugin configuration (site-wide or in front matter), the logo or background image files, or the plugin version. Otherwise the existing image is kept, so builds stay fast and `jekyll serve` doesn't rewrite unchanged files.
85
+
86
+ To track this, the plugin keeps a fingerprint of each image in `.jekyll-og-image.json` inside `output_dir`. Jekyll doesn't publish it. If you commit generated images to your repository, commit this file too; without it, every image is regenerated once.
87
+
88
+ ### Upgrading from 2.x
89
+
90
+ Images used to be named after the document's title or slug, which let two documents with the same title share one image. They are now named after the source file (see `output_dir` below), so every image gets a new path and URL on the first build after upgrading. The old images are left in place. Delete them from `output_dir` if you committed them to your repository.
81
91
 
82
92
  ## Configuration
83
93
 
@@ -85,11 +95,15 @@ The plugin can be configured in the `_config.yml` file or in the post's front ma
85
95
 
86
96
  The following configuration options are available:
87
97
 
98
+ Colors are hex values in `"#RRGGBB"` or `"#RGB"` form. Quote them in YAML, because an unquoted `#` starts a comment.
99
+
100
+ Paths to images (`image.path`, `canvas.background_image`) are relative to the site's source directory. If a file doesn't exist, the plugin logs a warning and generates the image without it.
101
+
88
102
  * `collections` - An array specifying which types of collections to generate images for. Supports `"posts"`, `"pages"`, and the names of any custom collections. Default: `["posts"]`
89
103
 
90
- * `output_dir` – The directory where the generated images will be saved. Images will be placed in subdirectories named after their collection type (e.g., `assets/images/og/posts`, `assets/images/og/pages`). Default: `assets/images/og`
104
+ * `output_dir` – The directory where the generated images will be saved. Images are placed in a subdirectory named after their collection type and named after their source file, e.g. `_posts/2024-02-15-hello.md` → `assets/images/og/posts/2024-02-15-hello.png` and `blog/index.md` → `assets/images/og/pages/blog/index.png`. Default: `assets/images/og`
91
105
 
92
- * `force` – If set to `true`, the plugin will generate an image for every document, even if the document already has an image. Default: `false`
106
+ * `force` – If set to `true`, every image is regenerated on each build, even when it is up to date. Rarely needed, since images are regenerated automatically when they change (see [Regenerating images](#regenerating-images)). Under `jekyll serve`, it applies to the first build only. Default: `false`
93
107
  * `verbose` – If set to `true`, the plugin will output additional information about the image generation process. Default: `false`
94
108
 
95
109
  * `skip_drafts` – If set to `true`, the plugin will skip post drafts when generating images. Default: `true`
@@ -97,10 +111,10 @@ The following configuration options are available:
97
111
  * `canvas` – The canvas configuration options:
98
112
  * `background_color` – The background color of the canvas. Default: `#FFFFFF`
99
113
  * `background_image` – The background image of the canvas. Default: `nil`
100
- * `width` – The width of the generated image in pixels. Default: `1200`
114
+ * `width` – The width of the generated image in pixels. The layout is designed for a 1200px-wide canvas and scales with this width: margins, text sizes, and the logo and border sizes below are all given for 1200px. Default: `1200`
101
115
  * `height` – The height of the generated image in pixels. Default: `600`
102
116
 
103
- * `header` – The header configuration options:
117
+ * `header` – The header configuration options. A title too long for the space above the metadata line is shrunk to fit.
104
118
  * `font_family` – The font family of the header text. Default: `Helvetica, Bold`
105
119
  * `color` – The color of the header text. Default: `#2f313d`
106
120
  * `prefix` – Text to prepend to the title. Default: `""`
@@ -111,7 +125,7 @@ The following configuration options are available:
111
125
  * `color` – The color of the content text. Default: `#535358`
112
126
 
113
127
  * `border_bottom` – The border bottom configuration options:
114
- * `width` – The width of the border bottom. Default: `20`
128
+ * `width` – The width of the border bottom. Default: `0`
115
129
  * `fill` – The array of colors to fill the border bottom. Default: `["#000000"]`
116
130
 
117
131
  * `domain` – The domain name to use in the image. Default: `nil`
@@ -122,7 +136,7 @@ The following configuration options are available:
122
136
  * `height` – The height of the logo in pixels. Default: `150`
123
137
  * `radius` – The radius for rounded corners on the logo. Default: `50`
124
138
  * `position` – The position of the logo as `{x, y}` coordinates. Default: `{x: 80, y: 100}`
125
- * `gravity` – The gravity anchor for logo positioning (nw, n, ne, w, e, sw, s, se). Default: `ne`
139
+ * `gravity` – The gravity anchor for logo positioning (nw, ne, sw, se). Default: `ne`
126
140
 
127
141
  * `metadata` – The metadata configuration options:
128
142
  * `fields` – Array of metadata fields to display. Available options: `"date"`, `"tags"`, or any custom front matter field. Default: `["date", "tags"]`
data/assets/icon.png ADDED
Binary file
data/assets/icon.svg ADDED
@@ -0,0 +1,39 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0" stop-color="#3a3d4c"/>
5
+ <stop offset="1" stop-color="#1f2029"/>
6
+ </linearGradient>
7
+ <linearGradient id="logo" x1="0" y1="0" x2="1" y2="1">
8
+ <stop offset="0" stop-color="#EDA895"/>
9
+ <stop offset="1" stop-color="#D51F06"/>
10
+ </linearGradient>
11
+ <clipPath id="card">
12
+ <rect x="48" y="148" width="416" height="248" rx="24"/>
13
+ </clipPath>
14
+ </defs>
15
+
16
+ <rect width="512" height="512" rx="112" fill="url(#bg)"/>
17
+
18
+ <rect x="96" y="108" width="320" height="248" rx="24" fill="#ffffff" opacity="0.18"/>
19
+ <rect x="72" y="128" width="368" height="248" rx="24" fill="#ffffff" opacity="0.36"/>
20
+
21
+ <g clip-path="url(#card)">
22
+ <rect x="48" y="148" width="416" height="248" fill="#ffffff"/>
23
+
24
+ <rect x="84" y="184" width="224" height="30" rx="15" fill="#2f313d"/>
25
+ <rect x="84" y="226" width="148" height="30" rx="15" fill="#2f313d"/>
26
+
27
+ <rect x="360" y="184" width="68" height="68" rx="18" fill="url(#logo)"/>
28
+
29
+ <rect x="84" y="290" width="112" height="12" rx="6" fill="#a4a4ab"/>
30
+ <rect x="84" y="316" width="168" height="12" rx="6" fill="#a4a4ab"/>
31
+ <rect x="360" y="290" width="68" height="12" rx="6" fill="#a4a4ab"/>
32
+
33
+ <rect x="48" y="364" width="83.2" height="32" fill="#820C02"/>
34
+ <rect x="131.2" y="364" width="83.2" height="32" fill="#A91401"/>
35
+ <rect x="214.4" y="364" width="83.2" height="32" fill="#D51F06"/>
36
+ <rect x="297.6" y="364" width="83.2" height="32" fill="#DE3F24"/>
37
+ <rect x="380.8" y="364" width="83.2" height="32" fill="#EDA895"/>
38
+ </g>
39
+ </svg>
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # require "anyway_config"
4
-
5
3
  class JekyllOgImage::Configuration
6
4
  Canvas = Data.define(:background_color, :background_image, :width, :height) do
7
5
  def initialize(background_color: "#FFFFFF", background_image: nil, width: 1200, height: 600)
@@ -23,14 +21,16 @@ class JekyllOgImage::Configuration
23
21
 
24
22
  Border = Data.define(:width, :fill) do
25
23
  def initialize(width: 0, fill: nil)
26
- fill.is_a?(Array) ? fill : [ fill ]
27
- super(width: width, fill: fill)
24
+ super(width: width, fill: Array(fill || "#000000"))
28
25
  end
29
26
  end
30
27
 
31
28
  Image = Data.define(:path, :width, :height, :radius, :position, :gravity) do
32
- def initialize(path: nil, width: 150, height: 150, radius: 50, position: { x: 80, y: 100 }, gravity: :ne)
33
- super
29
+ def initialize(path: nil, width: 150, height: 150, radius: 50, position: {}, gravity: :ne)
30
+ # Values read from YAML have string keys and string gravity
31
+ position = { x: 80, y: 100 }.merge(position.transform_keys(&:to_sym))
32
+
33
+ super(path: path, width: width, height: height, radius: radius, position: position, gravity: gravity.to_sym)
34
34
  end
35
35
  end
36
36
 
@@ -44,7 +44,7 @@ class JekyllOgImage::Configuration
44
44
  @raw_config = raw_config
45
45
  end
46
46
 
47
- def merge!(other)
47
+ def merge(other)
48
48
  config = Jekyll::Utils.deep_merge_hashes(
49
49
  @raw_config,
50
50
  other.to_h
@@ -86,26 +86,22 @@ class JekyllOgImage::Configuration
86
86
  end
87
87
 
88
88
  def canvas
89
- @raw_config["canvas"] ? Canvas.new(**Jekyll::Utils.symbolize_hash_keys(@raw_config["canvas"])) : Canvas.new
89
+ build_section(Canvas, "canvas")
90
90
  end
91
91
 
92
92
  def header
93
- @raw_config["header"] ? Header.new(**Jekyll::Utils.symbolize_hash_keys(@raw_config["header"])) : Header.new
93
+ build_section(Header, "header")
94
94
  end
95
95
 
96
96
  def content
97
- @raw_config["content"] ? Content.new(**Jekyll::Utils.symbolize_hash_keys(@raw_config["content"])) : Content.new
97
+ build_section(Content, "content")
98
98
  end
99
99
 
100
100
  def image
101
- if @raw_config["image"].is_a?(String)
102
- # Legacy support: if image is just a string, convert it to the new format
103
- Image.new(path: @raw_config["image"])
104
- elsif @raw_config["image"]
105
- Image.new(**Jekyll::Utils.symbolize_hash_keys(@raw_config["image"]))
106
- else
107
- Image.new
108
- end
101
+ # Legacy support: if image is just a string, convert it to the new format
102
+ return Image.new(path: @raw_config["image"]) if @raw_config["image"].is_a?(String)
103
+
104
+ build_section(Image, "image")
109
105
  end
110
106
 
111
107
  def domain
@@ -113,15 +109,31 @@ class JekyllOgImage::Configuration
113
109
  end
114
110
 
115
111
  def border_bottom
116
- @raw_config["border_bottom"] ? Border.new(**Jekyll::Utils.symbolize_hash_keys(@raw_config["border_bottom"])) : nil
112
+ build_section(Border, "border_bottom") if @raw_config["border_bottom"]
117
113
  end
118
114
 
119
- def margin_bottom
120
- 80 + (border_bottom&.width || 0)
115
+ def metadata
116
+ build_section(Metadata, "metadata")
121
117
  end
122
118
 
119
+ private
123
120
 
124
- def metadata
125
- @raw_config["metadata"] ? Metadata.new(**Jekyll::Utils.symbolize_hash_keys(@raw_config["metadata"])) : Metadata.new
121
+ def build_section(klass, name)
122
+ value = @raw_config[name]
123
+ return klass.new unless value
124
+
125
+ unless value.is_a?(Hash)
126
+ raise Jekyll::Errors::InvalidConfigurationError, "og_image.#{name} must be a set of options, got #{value.inspect}"
127
+ end
128
+
129
+ options = Jekyll::Utils.symbolize_hash_keys(value)
130
+ unknown = options.keys - klass.members
131
+
132
+ if unknown.any?
133
+ raise Jekyll::Errors::InvalidConfigurationError,
134
+ "Unknown og_image.#{name} option: #{unknown.join(", ")}. Valid options: #{klass.members.join(", ")}"
135
+ end
136
+
137
+ klass.new(**options)
126
138
  end
127
139
  end
@@ -5,14 +5,22 @@ class JekyllOgImage::Element::Base
5
5
 
6
6
  private
7
7
 
8
+ VALID_GRAVITY.each do |gravity|
9
+ define_method("gravity_#{gravity}?") do
10
+ @gravity == gravity
11
+ end
12
+ end
13
+
8
14
  def hex_to_rgb(input)
9
15
  case input
10
- when String
11
- input.match(/#(..)(..)(..)/)[1..3].map(&:hex)
12
16
  when Array
13
17
  input
18
+ when /\A#\h{3}\z/
19
+ input[1..].chars.map { |digit| (digit * 2).hex }
20
+ when /\A#\h{6}\z/
21
+ input[1..].scan(/\h\h/).map(&:hex)
14
22
  else
15
- raise ArgumentError, "Unknown input #{input.inspect}"
23
+ raise ArgumentError, "Invalid color #{input.inspect}, expected a hex color like \"#FFFFFF\" or \"#FFF\""
16
24
  end
17
25
  end
18
26
 
@@ -31,18 +39,9 @@ class JekyllOgImage::Element::Base
31
39
  end
32
40
 
33
41
  def composite_with_gravity(canvas, overlay, x, y)
34
- if gravity_nw?
35
- canvas.composite(overlay, :over, x: [ x ], y: [ y ]).flatten
36
- elsif gravity_ne?
37
- x = canvas.width - overlay.width - x
38
- canvas.composite(overlay, :over, x: [ x ], y: [ y ]).flatten
39
- elsif gravity_sw?
40
- y = canvas.height - overlay.height - y
41
- canvas.composite(overlay, :over, x: [ x ], y: [ y ]).flatten
42
- elsif gravity_se?
43
- x = canvas.width - overlay.width - x
44
- y = canvas.height - overlay.height - y
45
- canvas.composite(overlay, :over, x: [ x ], y: [ y ]).flatten
46
- end
42
+ x = canvas.width - overlay.width - x if gravity_ne? || gravity_se?
43
+ y = canvas.height - overlay.height - y if gravity_sw? || gravity_se?
44
+
45
+ canvas.composite(overlay, :over, x: [ x ], y: [ y ]).flatten
47
46
  end
48
47
  end
@@ -7,7 +7,7 @@ class JekyllOgImage::Element::Border < JekyllOgImage::Element::Base
7
7
  def initialize(size, position: :bottom, fill: "#000000")
8
8
  @size = size
9
9
  @position = position
10
- @fill = fill
10
+ @fill = Array(fill)
11
11
 
12
12
  validate_position!
13
13
  end
@@ -15,14 +15,11 @@ class JekyllOgImage::Element::Border < JekyllOgImage::Element::Base
15
15
  def apply_to(canvas, &block)
16
16
  border = Vips::Image.black(*dimensions(canvas))
17
17
 
18
- parts(canvas).each.with_index do |part, index|
18
+ parts(canvas).each do |part|
19
19
  image = Vips::Image.black(part.width, part.height).ifthenelse([ 0, 0, 0 ], part.rgb)
20
+ x, y = vertical? ? [ 0, part.offset ] : [ part.offset, 0 ]
20
21
 
21
- if vertical?
22
- border = border.composite(image, :over, x: [ 0 ], y: [ part.offset ]).flatten
23
- else
24
- border = border.composite(image, :over, x: [ part.offset ], y: [ 0 ]).flatten
25
- end
22
+ border = border.composite(image, :over, x: [ x ], y: [ y ]).flatten
26
23
  end
27
24
 
28
25
  result = block.call(canvas, border) if block_given?
@@ -40,10 +37,6 @@ class JekyllOgImage::Element::Border < JekyllOgImage::Element::Base
40
37
 
41
38
  private
42
39
 
43
- def hex_to_rgb(hex)
44
- hex.match(/#(..)(..)(..)/)[1..3].map { |x| x.hex }
45
- end
46
-
47
40
  def dimensions(canvas)
48
41
  if vertical?
49
42
  [ @size, canvas.height ]
@@ -53,21 +46,15 @@ class JekyllOgImage::Element::Border < JekyllOgImage::Element::Base
53
46
  end
54
47
 
55
48
  def parts(canvas)
56
- if @fill.is_a?(Array)
57
- width, height = vertical? ? [ @size, (canvas.height / @fill.size) ] : [ (canvas.width / @fill.size), @size ]
58
-
59
- @fill.map.with_index do |item, index|
60
- Part.new(
61
- rgb: hex_to_rgb(item),
62
- width: width,
63
- height: height,
64
- offset: index * (vertical? ? height : width)
65
- )
66
- end
67
- else
68
- length = vertical? ? canvas.height : canvas.width
49
+ length = vertical? ? canvas.height : canvas.width
69
50
 
70
- [ Part.new(rgb: hex_to_rgb(@fill), length: length, offset: 0) ]
51
+ @fill.map.with_index do |color, index|
52
+ # Stripe edges are rounded down, so the last stripe absorbs any remainder
53
+ from = length * index / @fill.size
54
+ to = length * (index + 1) / @fill.size
55
+ width, height = vertical? ? [ @size, to - from ] : [ to - from, @size ]
56
+
57
+ Part.new(rgb: hex_to_rgb(color), width: width, height: height, offset: from)
71
58
  end
72
59
  end
73
60
 
@@ -75,10 +62,6 @@ class JekyllOgImage::Element::Border < JekyllOgImage::Element::Base
75
62
  @position == :left || @position == :right
76
63
  end
77
64
 
78
- def horizontal?
79
- @position == :top || @position == :bottom
80
- end
81
-
82
65
  def validate_position!
83
66
  unless %i[left right top bottom].include?(@position)
84
67
  raise ArgumentError, "Invalid position: #{@position.inspect}"
@@ -14,8 +14,6 @@ class JekyllOgImage::Element::Canvas < JekyllOgImage::Element::Base
14
14
 
15
15
  @canvas = @canvas.composite(overlay, :over, x: [ 0 ], y: [ 0 ]).flatten
16
16
  end
17
-
18
- @canvas
19
17
  end
20
18
 
21
19
  def image(source, **opts, &block)
@@ -13,13 +13,19 @@ class JekyllOgImage::Element::Image < JekyllOgImage::Element::Base
13
13
 
14
14
  def apply_to(canvas, &block)
15
15
  image = Vips::Image.new_from_buffer(@source, "")
16
- image = round_corners(image) if @radius
16
+
17
+ if image.bands != canvas.bands
18
+ image = image.flatten
19
+ end
17
20
 
18
21
  if @width && @height
19
22
  ratio = calculate_ratio(image, @width, @height, :min)
20
23
  image = image.resize(ratio)
21
24
  end
22
25
 
26
+ # Rounded after resizing, so the radius is in output pixels whatever the source size
27
+ image = round_corners(image) if @radius
28
+
23
29
  result = block.call(canvas, image) if block_given?
24
30
 
25
31
  x, y = result ? [ result.fetch(:x, 0), result.fetch(:y, 0) ] : [ 0, 0 ]
@@ -29,12 +35,6 @@ class JekyllOgImage::Element::Image < JekyllOgImage::Element::Base
29
35
 
30
36
  private
31
37
 
32
- VALID_GRAVITY.each do |gravity|
33
- define_method("gravity_#{gravity}?") do
34
- @gravity == gravity
35
- end
36
- end
37
-
38
38
  def round_corners(image)
39
39
  mask = %(
40
40
  <svg viewBox="0 0 #{image.width} #{image.height}">
@@ -1,10 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class JekyllOgImage::Element::Text < JekyllOgImage::Element::Base
4
- def initialize(message, gravity: :nw, width: nil, dpi: nil, color: "#000000", font: nil)
4
+ MARKUP_ESCAPES = { "&" => "&amp;", "<" => "&lt;", ">" => "&gt;" }.freeze
5
+
6
+ # Vips::Image.text parses its input as Pango markup, so plain text has to be escaped
7
+ def self.escape_markup(message)
8
+ message.to_s.gsub(/[&<>]/, MARKUP_ESCAPES)
9
+ end
10
+
11
+ def initialize(message, gravity: :nw, width: nil, height: nil, dpi: nil, color: "#000000", font: nil)
5
12
  @message = message
6
13
  @gravity = gravity
7
14
  @width = width
15
+ @height = height
8
16
  @dpi = dpi
9
17
  @color = color
10
18
  @font = font
@@ -22,7 +30,12 @@ class JekyllOgImage::Element::Text < JekyllOgImage::Element::Base
22
30
 
23
31
  params[:wrap] = :word if wrap_supported?
24
32
 
25
- text = Vips::Image.text(@message, **params)
33
+ text = Vips::Image.text(self.class.escape_markup(@message), **params)
34
+
35
+ # Too tall: given a width and height instead of a dpi, libvips picks the largest dpi that fits
36
+ if @height && text.height > @height && height_supported?
37
+ text = Vips::Image.text(self.class.escape_markup(@message), **params.except(:dpi), height: @height)
38
+ end
26
39
 
27
40
  text = text
28
41
  .new_from_image(hex_to_rgb(@color))
@@ -38,10 +51,9 @@ class JekyllOgImage::Element::Text < JekyllOgImage::Element::Base
38
51
 
39
52
  private
40
53
 
41
- VALID_GRAVITY.each do |gravity|
42
- define_method("gravity_#{gravity}?") do
43
- @gravity == gravity
44
- end
54
+ def height_supported?
55
+ # Vips::Image.text fits text to a height since vips 8.8.0
56
+ Vips.at_least_libvips?(8, 8)
45
57
  end
46
58
 
47
59
  def wrap_supported?
@@ -1,60 +1,66 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+
3
5
  class JekyllOgImage::Generator < Jekyll::Generator
4
6
  safe true
5
7
 
6
8
  def generate(site)
7
9
  config = JekyllOgImage.config
10
+ @missing_files = Set.new
11
+ @file_digests = {}
12
+ manifest = JekyllOgImage::Manifest.new(File.join(site.source, config.output_dir))
13
+
14
+ # `jekyll serve` reuses this generator for every rebuild. Forcing only the first
15
+ # build stops rewritten images from triggering the watcher in an endless loop.
16
+ @first_build = !@built
17
+ @built = true
8
18
 
9
19
  config.collections.each do |type|
10
- process_collection(site, type, config)
20
+ process_collection(site, type, config, manifest)
11
21
  end
22
+
23
+ manifest.save
12
24
  end
13
25
 
14
26
  private
15
27
 
16
- def process_collection(site, type, config)
28
+ def process_collection(site, type, config, manifest)
17
29
  Jekyll.logger.info "Jekyll Og Image:", "Processing type: #{type}" if config.verbose?
18
30
 
19
- items = get_items_for_collection(site, type)
20
- return if items.empty?
21
-
22
- base_output_dir = File.join(config.output_dir, type)
23
- absolute_output_dir = File.join(site.config["source"], base_output_dir)
24
- FileUtils.mkdir_p absolute_output_dir
25
-
26
- items.each do |item|
31
+ get_items_for_collection(site, type).each do |item|
27
32
  if item.respond_to?(:draft?) && item.draft? && config.skip_drafts?
28
33
  Jekyll.logger.info "Jekyll Og Image:", "Skipping draft: #{item.data['title']}" if config.verbose?
29
34
  next
30
35
  end
31
36
 
32
- fallback_basename = if item.respond_to?(:basename_without_ext)
33
- item.basename_without_ext
34
- # rubocop:disable Layout/ElseAlignment # Disabled due to RuboCop error in v3.3.0
35
- else
36
- # rubocop:enable Layout/ElseAlignment
37
- File.basename(item.name, File.extname(item.name))
38
- end
37
+ item_config = config.merge(item.data["og_image"] || {})
38
+ next unless item_config.enabled?
39
39
 
40
- slug = item.data["slug"] || Jekyll::Utils.slugify(item.data["title"] || fallback_basename)
41
- image_filename = "#{slug}.png"
42
- absolute_image_path = File.join(absolute_output_dir, image_filename)
43
- relative_image_path = File.join("/", base_output_dir, image_filename) # Use leading slash for URL
40
+ image_path = File.join(type, "#{image_basename(item)}.png")
41
+ site_image_path = File.join(config.output_dir, image_path)
42
+ absolute_image_path = File.join(site.source, site_image_path)
43
+ digest = image_digest(site, item, item_config)
44
44
 
45
- if !File.exist?(absolute_image_path) || config.force?
45
+ force = @first_build && item_config.force?
46
+
47
+ if force || !File.exist?(absolute_image_path) || !manifest.fresh?(image_path, digest)
46
48
  Jekyll.logger.info "Jekyll Og Image:", "Generating image #{absolute_image_path}" if config.verbose?
47
- generate_image_for_document(site, item, absolute_image_path, config)
49
+ FileUtils.mkdir_p(File.dirname(absolute_image_path))
50
+ generate_image_for_document(site, item, absolute_image_path, item_config)
48
51
  else
49
- Jekyll.logger.info "Jekyll Og Image:", "Skipping image generation for #{relative_image_path} as it already exists." if config.verbose?
52
+ Jekyll.logger.info "Jekyll Og Image:", "Skipping #{site_image_path}, it is up to date" if config.verbose?
50
53
  end
51
54
 
52
- register_static_file(site, base_output_dir, image_filename, config) if File.exist?(absolute_image_path)
55
+ next unless File.exist?(absolute_image_path)
56
+
57
+ manifest.record(image_path, digest)
58
+ register_static_file(site, site_image_path, config)
53
59
 
54
60
  item.data["image"] ||= {
55
- "path" => relative_image_path,
56
- "width" => JekyllOgImage.config.canvas.width,
57
- "height" => JekyllOgImage.config.canvas.height,
61
+ "path" => File.join("/", site_image_path), # Use leading slash for URL
62
+ "width" => item_config.canvas.width,
63
+ "height" => item_config.canvas.height,
58
64
  "alt" => item.data["title"]
59
65
  }
60
66
  end
@@ -65,7 +71,7 @@ class JekyllOgImage::Generator < Jekyll::Generator
65
71
  when "posts"
66
72
  site.posts.docs
67
73
  when "pages"
68
- site.pages.reject { |page| !page.html? }
74
+ site.pages.select(&:html?)
69
75
  else
70
76
  if site.collections.key?(type)
71
77
  site.collections[type].docs
@@ -76,104 +82,145 @@ class JekyllOgImage::Generator < Jekyll::Generator
76
82
  end
77
83
  end
78
84
 
79
- def register_static_file(site, base_output_dir, image_filename, config)
80
- relative_path = File.join("/", base_output_dir, image_filename)
85
+ # The source file's path without its collection directory and extension, which is
86
+ # unique within the site: "_posts/2024-01-01-hello.md" => "2024-01-01-hello",
87
+ # "blog/index.md" => "blog/index".
88
+ def image_basename(item)
89
+ path = item.relative_path.delete_prefix("/")
90
+ path = path.delete_prefix("#{item.collection.relative_directory}/") if item.respond_to?(:collection) && item.collection
91
+ path = path.delete_suffix(File.extname(path))
92
+
93
+ path.split("/").map { |segment| slugify_path_segment(segment) }.join("/")
94
+ end
95
+
96
+ def slugify_path_segment(segment)
97
+ slug = Jekyll::Utils.slugify(segment)
98
+ # A name made only of characters slugify removes, e.g. emoji
99
+ slug.empty? ? Digest::SHA256.hexdigest(segment)[0, 12] : slug
100
+ end
101
+
102
+ # Changes whenever anything that affects the rendered image changes
103
+ def image_digest(site, item, config)
104
+ inputs = [
105
+ JekyllOgImage::VERSION,
106
+ config.canvas.to_h,
107
+ config.header.to_h,
108
+ config.content.to_h,
109
+ config.image.to_h,
110
+ config.border_bottom&.to_h,
111
+ config.metadata.to_h,
112
+ config.domain,
113
+ item.data["title"],
114
+ config.metadata.fields.map { |field| metadata_value_for(item, field, config) },
115
+ file_digest(site, config.image.path),
116
+ file_digest(site, config.canvas.background_image)
117
+ ]
118
+
119
+ Digest::SHA256.hexdigest(JSON.generate(inputs))
120
+ end
121
+
122
+ def file_digest(site, path)
123
+ return unless path
124
+
125
+ absolute_path = File.join(site.source, path)
126
+ @file_digests[absolute_path] ||= Digest::SHA256.file(absolute_path).hexdigest if File.file?(absolute_path)
127
+ end
128
+
129
+ def register_static_file(site, site_image_path, config)
130
+ relative_path = File.join("/", site_image_path)
81
131
  return if site.static_files.any? { |file| file.relative_path == relative_path }
82
132
 
133
+ # Leading slash in dir, as Jekyll's reader does, so relative_path matches the check above
83
134
  static_file = Jekyll::StaticFile.new(
84
135
  site,
85
136
  site.source,
86
- base_output_dir,
87
- image_filename
137
+ File.dirname(relative_path),
138
+ File.basename(relative_path)
88
139
  )
89
140
 
90
141
  site.static_files << static_file
91
- Jekyll.logger.info "Jekyll Og Image:", "Added #{base_output_dir}/#{image_filename} to static files" if config.verbose?
142
+ Jekyll.logger.info "Jekyll Og Image:", "Added #{site_image_path} to static files" if config.verbose?
92
143
  end
93
144
 
94
- def generate_image_for_document(site, item, path, base_config)
95
- config = base_config.merge!(item.data["og_image"] || {})
96
-
97
- return unless config.enabled?
145
+ def generate_image_for_document(site, item, path, config)
146
+ layout = JekyllOgImage::Layout.new(config)
98
147
 
99
148
  canvas = generate_canvas(site, config)
100
- canvas = add_border_bottom(canvas, config) if config.border_bottom
101
- canvas = add_image(canvas, File.read(File.join(site.config["source"], config.image.path)), config) if config.image.path
102
- canvas = add_header(canvas, item, config)
103
- canvas = add_metadata(canvas, item, config)
104
- canvas = add_domain(canvas, item, config) if config.domain
149
+ canvas = add_border_bottom(canvas, config, layout) if config.border_bottom
150
+ logo = read_source_file(site, config.image.path, "og_image.image.path") if config.image.path
151
+ canvas = add_image(canvas, logo, config, layout) if logo
152
+ canvas = add_header(canvas, item, config, layout)
153
+ canvas = add_metadata(canvas, item, config, layout)
154
+ canvas = add_domain(canvas, config, layout) if config.domain
105
155
 
106
156
  canvas.save(path)
107
157
  end
108
158
 
109
159
  def generate_canvas(site, config)
110
- background_image = if config.canvas.background_image
111
- bg_path = File.join(site.config["source"], config.canvas.background_image.gsub(/^\//, ""))
112
- File.exist?(bg_path) ? File.read(bg_path) : nil
160
+ if config.canvas.background_image
161
+ background_image = read_source_file(site, config.canvas.background_image, "og_image.canvas.background_image")
113
162
  end
114
163
 
115
- JekyllOgImage::Element::Canvas.new(JekyllOgImage.config.canvas.width, JekyllOgImage.config.canvas.height,
164
+ JekyllOgImage::Element::Canvas.new(config.canvas.width, config.canvas.height,
116
165
  background_color: config.canvas.background_color,
117
166
  background_image: background_image
118
167
  )
119
168
  end
120
169
 
121
- def add_border_bottom(canvas, config)
122
- canvas.border(config.border_bottom.width,
170
+ def read_source_file(site, path, option)
171
+ absolute_path = File.join(site.source, path)
172
+ return File.binread(absolute_path) if File.file?(absolute_path)
173
+
174
+ # Warn once per build, not once per document
175
+ Jekyll.logger.warn "Jekyll Og Image:", "#{option} file not found: #{path}, skipping it" if @missing_files.add?(path)
176
+ nil
177
+ end
178
+
179
+ def add_border_bottom(canvas, config, layout)
180
+ canvas.border(layout.border_width,
123
181
  position: :bottom,
124
182
  fill: config.border_bottom.fill
125
183
  )
126
184
  end
127
185
 
128
- def add_image(canvas, image_data, config)
129
- image_config = config.image
186
+ def add_image(canvas, image_data, config, layout)
130
187
  canvas.image(image_data,
131
- gravity: image_config.gravity,
132
- width: image_config.width,
133
- height: image_config.height,
134
- radius: image_config.radius
135
- ) { |_canvas, _text| { x: image_config.position[:x], y: image_config.position[:y] } }
188
+ gravity: config.image.gravity,
189
+ width: layout.logo_width,
190
+ height: layout.logo_height,
191
+ radius: layout.logo_radius
192
+ ) { |_canvas, _image| layout.logo_position }
136
193
  end
137
194
 
138
- def add_header(canvas, item, config)
195
+ def add_header(canvas, item, config, layout)
139
196
  title = item.data["title"] || "Untitled"
140
197
  full_title = "#{config.header.prefix}#{title}#{config.header.suffix}"
141
198
 
142
- # Calculate available width for header text to avoid overlap with image
143
- header_width = if config.image.path
144
- # Canvas width - left margin - right margin - image width - spacing
145
- # 1200 - 80 - 80 - image_width - 30 (spacing)
146
- 1040 - config.image.width - 30
147
- else
148
- 1040
149
- end
150
-
151
199
  canvas.text(full_title,
152
- width: header_width,
200
+ width: layout.header_width,
201
+ height: layout.header_max_height(content_line_height(config, layout)),
153
202
  color: config.header.color,
154
- dpi: 400,
203
+ dpi: layout.header_dpi,
155
204
  font: config.header.font_family
156
- ) { |_canvas, _text| { x: 80, y: 100 } }
205
+ ) { |_canvas, _text| { x: layout.margin, y: layout.header_top } }
157
206
  end
158
207
 
159
- def add_metadata(canvas, item, config)
160
- metadata_text = metadata_text_for(item, config)
208
+ def add_metadata(canvas, item, config, layout)
209
+ metadata_text = metadata_text_for(item, config, layout)
161
210
  return canvas if metadata_text.empty?
162
211
 
163
- metadata_width = metadata_width_for(config)
164
-
165
212
  canvas.text(metadata_text,
166
213
  gravity: :sw,
167
- width: metadata_width,
214
+ width: metadata_width_for(config, layout),
168
215
  color: config.content.color,
169
- dpi: 150,
216
+ dpi: layout.content_dpi,
170
217
  font: config.content.font_family
171
- ) { |_canvas, _text| { x: 80, y: config.margin_bottom } }
218
+ ) { |_canvas, _text| { x: layout.margin, y: layout.bottom } }
172
219
  end
173
220
 
174
- def metadata_text_for(item, config)
221
+ def metadata_text_for(item, config, layout)
175
222
  metadata_parts = []
176
- metadata_width = metadata_width_for(config)
223
+ metadata_width = metadata_width_for(config, layout)
177
224
 
178
225
  config.metadata.fields.each do |field|
179
226
  metadata_value = metadata_value_for(item, field, config)
@@ -181,7 +228,7 @@ class JekyllOgImage::Generator < Jekyll::Generator
181
228
 
182
229
  if field == "description"
183
230
  candidate_text = (metadata_parts + [ metadata_value ]).join(config.metadata.separator)
184
- next unless metadata_text_fits_single_line?(candidate_text, metadata_width, config)
231
+ next unless metadata_text_fits_single_line?(candidate_text, metadata_width, config, layout)
185
232
  end
186
233
 
187
234
  metadata_parts << metadata_value
@@ -195,7 +242,7 @@ class JekyllOgImage::Generator < Jekyll::Generator
195
242
  when "date"
196
243
  item.respond_to?(:date) && item.date ? item.date.strftime(config.metadata.date_format) : nil
197
244
  when "tags"
198
- return nil unless item.data["tags"]&.is_a?(Array) && item.data["tags"].any?
245
+ return nil unless item.data["tags"].is_a?(Array) && item.data["tags"].any?
199
246
 
200
247
  item.data["tags"].map { |tag| "##{tag}" }.join(" ")
201
248
  else
@@ -204,34 +251,38 @@ class JekyllOgImage::Generator < Jekyll::Generator
204
251
  end
205
252
  end
206
253
 
207
- def metadata_width_for(config)
208
- config.domain ? 600 : 1040
254
+ # The metadata line shares the bottom row with the domain
255
+ def metadata_width_for(config, layout)
256
+ return layout.content_width unless config.domain
257
+
258
+ layout.content_width - render_content_text(config.domain, config, layout).width - layout.gap
209
259
  end
210
260
 
211
- def metadata_text_fits_single_line?(text, metadata_width, config)
212
- options = {
213
- width: metadata_width,
214
- dpi: 150,
215
- font: config.content.font_family,
216
- align: :low
217
- }
218
- options[:wrap] = :word if Vips.at_least_libvips?(8, 14)
261
+ def metadata_text_fits_single_line?(text, metadata_width, config, layout)
262
+ rendered_height = render_content_text(text, config, layout, width: metadata_width).height
219
263
 
220
- single_line_height = Vips::Image.text("Ay", **options).height
221
- rendered_height = Vips::Image.text(text, **options).height
264
+ rendered_height <= (content_line_height(config, layout) * 1.6)
265
+ end
222
266
 
223
- rendered_height <= (single_line_height * 1.6)
267
+ def content_line_height(config, layout)
268
+ render_content_text("Ay", config, layout).height
224
269
  end
225
270
 
271
+ def render_content_text(text, config, layout, width: nil)
272
+ options = { dpi: layout.content_dpi, font: config.content.font_family, align: :low }
273
+ options[:width] = width if width
274
+ options[:wrap] = :word if width && Vips.at_least_libvips?(8, 14)
275
+
276
+ Vips::Image.text(JekyllOgImage::Element::Text.escape_markup(text), **options)
277
+ end
226
278
 
227
- def add_domain(canvas, _item, config)
228
- y_pos = config.margin_bottom
229
279
 
280
+ def add_domain(canvas, config, layout)
230
281
  canvas.text(config.domain,
231
282
  gravity: :se,
232
283
  color: config.content.color,
233
- dpi: 150,
284
+ dpi: layout.content_dpi,
234
285
  font: config.content.font_family
235
- ) { |_canvas, _text| { x: 80, y: y_pos } }
286
+ ) { |_canvas, _text| { x: layout.margin, y: layout.bottom } }
236
287
  end
237
288
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Positions and sizes for an image. Everything, including the pixel sizes in the
4
+ # configuration (logo, border), is designed for a 1200px-wide canvas and scaled
5
+ # with the canvas width, so any canvas size keeps the same proportions.
6
+ class JekyllOgImage::Layout
7
+ REFERENCE_WIDTH = 1200.0
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @scale = config.canvas.width / REFERENCE_WIDTH
12
+ end
13
+
14
+ # Left, right and bottom margin
15
+ def margin
16
+ scaled(80)
17
+ end
18
+
19
+ def header_top
20
+ scaled(100)
21
+ end
22
+
23
+ def header_dpi
24
+ scaled(400)
25
+ end
26
+
27
+ def content_dpi
28
+ scaled(150)
29
+ end
30
+
31
+ # Space between the header and whatever is next to or below it
32
+ def gap
33
+ scaled(30)
34
+ end
35
+
36
+ def border_width
37
+ scaled(@config.border_bottom&.width || 0)
38
+ end
39
+
40
+ # Distance from the bottom edge to the metadata line, above the border
41
+ def bottom
42
+ margin + border_width
43
+ end
44
+
45
+ def logo_width
46
+ scaled(@config.image.width)
47
+ end
48
+
49
+ def logo_height
50
+ scaled(@config.image.height)
51
+ end
52
+
53
+ def logo_radius
54
+ scaled(@config.image.radius) if @config.image.radius
55
+ end
56
+
57
+ def logo_position
58
+ { x: scaled(@config.image.position[:x]), y: scaled(@config.image.position[:y]) }
59
+ end
60
+
61
+ def content_width
62
+ @config.canvas.width - 2 * margin
63
+ end
64
+
65
+ # Leaves room for the logo next to the header
66
+ def header_width
67
+ @config.image.path ? content_width - logo_width - gap : content_width
68
+ end
69
+
70
+ # How tall the header can be without reaching the metadata line
71
+ def header_max_height(metadata_line_height)
72
+ @config.canvas.height - header_top - bottom - metadata_line_height - gap
73
+ end
74
+
75
+ private
76
+
77
+ def scaled(value)
78
+ (value * @scale).round
79
+ end
80
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ # Remembers a digest of the inputs each image was generated from, so images are
6
+ # regenerated only when something that affects them changes.
7
+ #
8
+ # Stored as a dotfile in the output directory, which Jekyll never publishes.
9
+ class JekyllOgImage::Manifest
10
+ FILENAME = ".jekyll-og-image.json"
11
+
12
+ def initialize(dir)
13
+ @path = File.join(dir, FILENAME)
14
+ @previous = load
15
+ @current = {}
16
+ end
17
+
18
+ def fresh?(image_path, digest)
19
+ @current.fetch(image_path) { @previous[image_path] } == digest
20
+ end
21
+
22
+ def record(image_path, digest)
23
+ @current[image_path] = digest
24
+ end
25
+
26
+ # Writes the images recorded during this build. Entries for images that weren't
27
+ # recorded (deleted or disabled documents) are dropped.
28
+ def save
29
+ entries = @current.sort.to_h
30
+ return if entries == @previous
31
+
32
+ FileUtils.mkdir_p(File.dirname(@path))
33
+ File.write(@path, JSON.pretty_generate(entries))
34
+ end
35
+
36
+ private
37
+
38
+ def load
39
+ JSON.parse(File.read(@path))
40
+ rescue Errno::ENOENT, JSON::ParserError
41
+ {}
42
+ end
43
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JekyllOgImage
4
- VERSION = "2.1.0"
4
+ VERSION = "3.0.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-og-image
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Igor Alexandrov
@@ -9,20 +9,6 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: jekyll-seo-tag
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '2.8'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '2.8'
26
12
  - !ruby/object:Gem::Dependency
27
13
  name: zeitwerk
28
14
  requirement: !ruby/object:Gem::Requirement
@@ -43,14 +29,14 @@ dependencies:
43
29
  requirements:
44
30
  - - "~>"
45
31
  - !ruby/object:Gem::Version
46
- version: 2.2.0
32
+ version: '2.2'
47
33
  type: :runtime
48
34
  prerelease: false
49
35
  version_requirements: !ruby/object:Gem::Requirement
50
36
  requirements:
51
37
  - - "~>"
52
38
  - !ruby/object:Gem::Version
53
- version: 2.2.0
39
+ version: '2.2'
54
40
  - !ruby/object:Gem::Dependency
55
41
  name: jekyll
56
42
  requirement: !ruby/object:Gem::Requirement
@@ -71,20 +57,6 @@ dependencies:
71
57
  - - "<"
72
58
  - !ruby/object:Gem::Version
73
59
  version: '5.0'
74
- - !ruby/object:Gem::Dependency
75
- name: csv
76
- requirement: !ruby/object:Gem::Requirement
77
- requirements:
78
- - - ">="
79
- - !ruby/object:Gem::Version
80
- version: '0'
81
- type: :development
82
- prerelease: false
83
- version_requirements: !ruby/object:Gem::Requirement
84
- requirements:
85
- - - ">="
86
- - !ruby/object:Gem::Version
87
- version: '0'
88
60
  email:
89
61
  - igor.alexandrov@gmail.com
90
62
  executables: []
@@ -97,6 +69,8 @@ files:
97
69
  - LICENSE.txt
98
70
  - README.md
99
71
  - Rakefile
72
+ - assets/icon.png
73
+ - assets/icon.svg
100
74
  - examples/1.png
101
75
  - examples/2.png
102
76
  - examples/3.png
@@ -110,6 +84,8 @@ files:
110
84
  - lib/jekyll_og_image/element/image.rb
111
85
  - lib/jekyll_og_image/element/text.rb
112
86
  - lib/jekyll_og_image/generator.rb
87
+ - lib/jekyll_og_image/layout.rb
88
+ - lib/jekyll_og_image/manifest.rb
113
89
  - lib/jekyll_og_image/version.rb
114
90
  homepage: https://github.com/igor-alexandrov/jekyll-og-image
115
91
  licenses:
@@ -130,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
106
  - !ruby/object:Gem::Version
131
107
  version: '0'
132
108
  requirements: []
133
- rubygems_version: 4.0.3
109
+ rubygems_version: 4.0.20
134
110
  specification_version: 4
135
111
  summary: Jekyll plugin to generate GitHub-style open graph images
136
112
  test_files: []