jekyll-post-card 0.1.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.
@@ -0,0 +1,201 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "nokogiri"
6
+ require "json"
7
+
8
+ module Jekyll
9
+ module PostCard
10
+ # Fetches metadata from external URLs using Open Graph, Twitter Cards, and HTML meta tags
11
+ class Fetcher
12
+ TIMEOUT = 10
13
+ USER_AGENT = "Jekyll-Post-Card/#{VERSION} (+https://github.com/r0x0d/jekyll-post-card)"
14
+
15
+ class FetchError < StandardError; end
16
+
17
+ # Metadata structure for a fetched post
18
+ PostMetadata = Struct.new(
19
+ :title,
20
+ :description,
21
+ :image,
22
+ :url,
23
+ :site_name,
24
+ :date,
25
+ :author,
26
+ :type,
27
+ keyword_init: true
28
+ )
29
+
30
+ class << self
31
+ def fetch(url)
32
+ uri = validate_url(url)
33
+ html = fetch_html(uri)
34
+ parse_metadata(html, uri)
35
+ rescue FetchError => e
36
+ PostCard.logger.warn("Failed to fetch metadata for #{url}: #{e.message}")
37
+ error_metadata(url, e.message)
38
+ rescue StandardError => e
39
+ PostCard.logger.error("Unexpected error fetching #{url}: #{e.message}")
40
+ error_metadata(url, "Unexpected error occurred")
41
+ end
42
+
43
+ private
44
+
45
+ def validate_url(url)
46
+ uri = URI.parse(url)
47
+ raise FetchError, "Invalid URL scheme" unless %w[http https].include?(uri.scheme)
48
+ raise FetchError, "Missing host" unless uri.host
49
+
50
+ uri
51
+ rescue URI::InvalidURIError => e
52
+ raise FetchError, "Invalid URL: #{e.message}"
53
+ end
54
+
55
+ def fetch_html(uri)
56
+ http = Net::HTTP.new(uri.host, uri.port)
57
+ http.use_ssl = uri.scheme == "https"
58
+ http.open_timeout = TIMEOUT
59
+ http.read_timeout = TIMEOUT
60
+
61
+ request = Net::HTTP::Get.new(uri)
62
+ request["User-Agent"] = USER_AGENT
63
+ request["Accept"] = "text/html,application/xhtml+xml"
64
+
65
+ response = http.request(request)
66
+
67
+ case response
68
+ when Net::HTTPSuccess
69
+ response.body
70
+ when Net::HTTPRedirection
71
+ redirect_url = response["location"]
72
+ redirect_uri = URI.parse(redirect_url)
73
+ redirect_uri = URI.join(uri, redirect_url) unless redirect_uri.absolute?
74
+ fetch_html(redirect_uri)
75
+ else
76
+ raise FetchError, "HTTP #{response.code}: #{response.message}"
77
+ end
78
+ end
79
+
80
+ def parse_metadata(html, uri)
81
+ doc = Nokogiri::HTML(html)
82
+
83
+ PostMetadata.new(
84
+ title: extract_title(doc),
85
+ description: extract_description(doc),
86
+ image: extract_image(doc, uri),
87
+ url: uri.to_s,
88
+ site_name: extract_site_name(doc, uri),
89
+ date: extract_date(doc),
90
+ author: extract_author(doc),
91
+ type: extract_type(doc)
92
+ )
93
+ end
94
+
95
+ def extract_title(doc)
96
+ # Priority: og:title > twitter:title > <title>
97
+ og_title = doc.at_css('meta[property="og:title"]')&.[]("content")
98
+ twitter_title = doc.at_css('meta[name="twitter:title"]')&.[]("content")
99
+ html_title = doc.at_css("title")&.text
100
+
101
+ (og_title || twitter_title || html_title || "Untitled").strip
102
+ end
103
+
104
+ def extract_description(doc)
105
+ # Priority: og:description > twitter:description > meta description
106
+ og_desc = doc.at_css('meta[property="og:description"]')&.[]("content")
107
+ twitter_desc = doc.at_css('meta[name="twitter:description"]')&.[]("content")
108
+ meta_desc = doc.at_css('meta[name="description"]')&.[]("content")
109
+
110
+ (og_desc || twitter_desc || meta_desc || "").strip
111
+ end
112
+
113
+ def extract_image(doc, base_uri)
114
+ # Priority: og:image > twitter:image
115
+ og_image = doc.at_css('meta[property="og:image"]')&.[]("content")
116
+ twitter_image = doc.at_css('meta[name="twitter:image"]')&.[]("content")
117
+
118
+ image_url = og_image || twitter_image
119
+ return nil unless image_url
120
+
121
+ # Make relative URLs absolute
122
+ begin
123
+ image_uri = URI.parse(image_url)
124
+ image_uri.absolute? ? image_url : URI.join(base_uri, image_url).to_s
125
+ rescue URI::InvalidURIError
126
+ image_url
127
+ end
128
+ end
129
+
130
+ def extract_site_name(doc, uri)
131
+ og_site = doc.at_css('meta[property="og:site_name"]')&.[]("content")
132
+ og_site || uri.host.sub(/^www\./, "")
133
+ end
134
+
135
+ def extract_date(doc)
136
+ # Try various date meta tags
137
+ date_selectors = [
138
+ 'meta[property="article:published_time"]',
139
+ 'meta[name="date"]',
140
+ 'meta[name="DC.date"]',
141
+ 'meta[name="publish-date"]',
142
+ 'time[datetime]'
143
+ ]
144
+
145
+ date_selectors.each do |selector|
146
+ element = doc.at_css(selector)
147
+ next unless element
148
+
149
+ date_str = element["content"] || element["datetime"]
150
+ next unless date_str
151
+
152
+ begin
153
+ return Date.parse(date_str)
154
+ rescue Date::Error
155
+ next
156
+ end
157
+ end
158
+
159
+ nil
160
+ end
161
+
162
+ def extract_author(doc)
163
+ # Try various author meta tags
164
+ author_selectors = [
165
+ 'meta[property="article:author"]',
166
+ 'meta[name="author"]',
167
+ 'meta[name="DC.creator"]',
168
+ 'meta[name="twitter:creator"]'
169
+ ]
170
+
171
+ author_selectors.each do |selector|
172
+ element = doc.at_css(selector)
173
+ author = element&.[]("content")
174
+ return author.strip if author && !author.empty?
175
+ end
176
+
177
+ nil
178
+ end
179
+
180
+ def extract_type(doc)
181
+ og_type = doc.at_css('meta[property="og:type"]')&.[]("content")
182
+ og_type || "article"
183
+ end
184
+
185
+ def error_metadata(url, message)
186
+ PostMetadata.new(
187
+ title: "Unable to load post",
188
+ description: "Could not fetch metadata: #{message}",
189
+ image: nil,
190
+ url: url,
191
+ site_name: nil,
192
+ date: nil,
193
+ author: nil,
194
+ type: "error"
195
+ )
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module PostCard
5
+ # Custom static file that allows specifying the destination directory
6
+ class CssFile < Jekyll::StaticFile
7
+ def initialize(site, base, dir, name, dest_dir)
8
+ super(site, base, dir, name)
9
+ @dest_dir = dest_dir
10
+ end
11
+
12
+ def destination_rel_dir
13
+ @dest_dir
14
+ end
15
+
16
+ def destination(dest)
17
+ File.join(dest, @dest_dir, @name)
18
+ end
19
+ end
20
+
21
+ # Generator that copies the post-card CSS to the site's assets
22
+ class Generator < Jekyll::Generator
23
+ safe true
24
+ priority :lowest
25
+
26
+ def generate(site)
27
+ asset_source = Jekyll::PostCard.asset_path
28
+ css_source = File.join(asset_source, "post-card.css")
29
+
30
+ unless File.exist?(css_source)
31
+ Jekyll::PostCard.logger.warn("post-card.css not found at #{css_source}")
32
+ return
33
+ end
34
+
35
+ # Add CSS file as a static file with custom destination
36
+ site.static_files << CssFile.new(
37
+ site,
38
+ asset_source,
39
+ "",
40
+ "post-card.css",
41
+ "/assets/css"
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module PostCard
5
+ # Liquid tag for rendering post cards
6
+ # Usage:
7
+ # {% post_card /2024/01/01/my-post %}
8
+ # {% post_card https://example.com/article %}
9
+ # {% post_card /my-post variant:compact %}
10
+ class PostTag < Liquid::Tag
11
+ VALID_VARIANTS = %w[default compact vertical].freeze
12
+ EXTERNAL_URL_PATTERN = %r{^https?://}i
13
+
14
+ def initialize(tag_name, markup, tokens)
15
+ super
16
+ parse_arguments(markup.strip)
17
+ end
18
+
19
+ def render(context)
20
+ site = context.registers[:site]
21
+
22
+ begin
23
+ if external_url?
24
+ render_external_card(context)
25
+ else
26
+ render_internal_card(site, context)
27
+ end
28
+ rescue StandardError => e
29
+ render_error_card("Error rendering card: #{e.message}")
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def parse_arguments(markup)
36
+ parts = markup.split(/\s+/)
37
+ @url = parts.shift || ""
38
+ @options = {}
39
+
40
+ parts.each do |part|
41
+ if part.include?(":")
42
+ key, value = part.split(":", 2)
43
+ @options[key.to_sym] = value
44
+ end
45
+ end
46
+
47
+ @variant = @options[:variant] || "default"
48
+ @variant = "default" unless VALID_VARIANTS.include?(@variant)
49
+
50
+ # Parse hide_image option (accepts true, false, yes, no, 1, 0)
51
+ hide_image_value = @options[:hide_image]&.downcase
52
+ @hide_image = %w[true yes 1].include?(hide_image_value)
53
+ end
54
+
55
+ def external_url?
56
+ @url.match?(EXTERNAL_URL_PATTERN)
57
+ end
58
+
59
+ def render_internal_card(site, _context)
60
+ post = find_post(site)
61
+
62
+ if post
63
+ render_card(
64
+ title: post.data["title"] || "Untitled",
65
+ description: extract_excerpt(post),
66
+ image: post.data["image"] || post.data["thumbnail"] || post.data["og_image"],
67
+ url: post.url,
68
+ date: post.date,
69
+ source_type: "internal",
70
+ source_name: "Internal"
71
+ )
72
+ else
73
+ render_error_card("Post not found: #{@url}")
74
+ end
75
+ end
76
+
77
+ def extract_excerpt(post)
78
+ # Handle Jekyll's Excerpt object or string excerpt
79
+ excerpt = post.data["excerpt"] || post.data["description"]
80
+
81
+ if excerpt
82
+ # Clean up whitespace but preserve HTML for proper escaping
83
+ text = excerpt.to_s.gsub(/\s+/, " ").strip
84
+ return text unless text.empty?
85
+ end
86
+
87
+ excerpt_from_content(post)
88
+ end
89
+
90
+ def render_external_card(_context)
91
+ metadata = Fetcher.fetch(@url)
92
+
93
+ if metadata.type == "error"
94
+ render_error_card(metadata.description)
95
+ else
96
+ render_card(
97
+ title: metadata.title,
98
+ description: metadata.description,
99
+ image: metadata.image,
100
+ url: metadata.url,
101
+ date: metadata.date,
102
+ source_type: "external",
103
+ source_name: metadata.site_name
104
+ )
105
+ end
106
+ end
107
+
108
+ def find_post(site)
109
+ # Try to find by URL/permalink
110
+ site.posts.docs.find do |post|
111
+ post.url == @url || post.url == "/#{@url}" || post.url == @url.chomp("/")
112
+ end
113
+ end
114
+
115
+ def excerpt_from_content(post)
116
+ # Get first 160 characters of content, stripping HTML
117
+ content = post.content.to_s.gsub(/<[^>]*>/, " ").gsub(/\s+/, " ").strip
118
+ content.length > 160 ? "#{content[0, 157]}..." : content
119
+ end
120
+
121
+ def render_card(title:, description:, image:, url:, date:, source_type:, source_name:)
122
+ variant_class = @variant == "default" ? "" : " #{@variant}"
123
+ external_class = source_type == "external" ? " external" : ""
124
+ # Hide image if explicitly requested or if no image available
125
+ show_image = image && !@hide_image
126
+ image_class = show_image ? "" : " no-image"
127
+
128
+ formatted_date = format_date(date)
129
+ escaped_title = escape_html(title)
130
+ escaped_description = escape_html(description)
131
+ escaped_source = escape_html(source_name)
132
+ escaped_url = escape_html(url)
133
+
134
+ target = source_type == "external" ? "_blank" : "_self"
135
+ rel_attr = source_type == "external" ? ' rel="noopener noreferrer"' : ""
136
+
137
+ # Use div wrapper like jekyll-github-card to prevent Markdown interference
138
+ # Render image if available, placeholder if no image (unless explicitly hidden)
139
+ image_html = if @hide_image
140
+ ""
141
+ else
142
+ render_image(image, escaped_title)
143
+ end
144
+
145
+ <<~HTML
146
+ <div class="post-card#{variant_class}#{external_class}#{image_class}" data-url="#{escaped_url}">
147
+ <a href="#{url}" class="post-card-link" target="#{target}"#{rel_attr}>
148
+ <div class="post-card-inner">
149
+ #{image_html}
150
+ <div class="post-card-content">
151
+ <div class="post-card-meta">
152
+ <span class="post-card-source">
153
+ #{source_icon(source_type)}
154
+ #{escaped_source}
155
+ </span>
156
+ #{formatted_date ? "<span class=\"post-card-date\">#{formatted_date}</span>" : ""}
157
+ </div>
158
+ <h3 class="post-card-title">#{escaped_title}</h3>
159
+ <p class="post-card-excerpt">#{escaped_description}</p>
160
+ </div>
161
+ <div class="post-card-arrow">
162
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
163
+ </div>
164
+ </div>
165
+ </a>
166
+ </div>
167
+ HTML
168
+ end
169
+
170
+ def render_image(image, alt)
171
+ if image
172
+ <<~HTML
173
+ <div class="post-card-image-container">
174
+ <img src="#{image}" alt="#{alt}" class="post-card-image" loading="lazy">
175
+ </div>
176
+ HTML
177
+ else
178
+ <<~HTML
179
+ <div class="post-card-image-container">
180
+ <div class="post-card-placeholder">
181
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-1 16H6c-.55 0-1-.45-1-1V6c0-.55.45-1 1-1h12c.55 0 1 .45 1 1v12c0 .55-.45 1-1 1zm-4.44-6.19l-2.35 3.02-1.56-1.88c-.2-.25-.58-.24-.78.01l-1.74 2.23c-.26.33-.02.81.39.81h8.98c.41 0 .65-.47.4-.8l-2.55-3.39c-.19-.26-.59-.26-.79 0z"/></svg>
182
+ </div>
183
+ </div>
184
+ HTML
185
+ end
186
+ end
187
+
188
+ def render_error_card(message)
189
+ <<~HTML
190
+ <div class="post-card error no-image">
191
+ <div class="post-card-inner">
192
+ <div class="post-card-image-container">
193
+ <div class="post-card-placeholder">
194
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
195
+ </div>
196
+ </div>
197
+ <div class="post-card-content">
198
+ <div class="post-card-meta">
199
+ <span class="post-card-source" style="background: rgba(255, 100, 100, 0.15); color: #ff6b6b;">
200
+ <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
201
+ Error
202
+ </span>
203
+ </div>
204
+ <h3 class="post-card-title">Unable to load post</h3>
205
+ <p class="post-card-excerpt">#{escape_html(message)}</p>
206
+ </div>
207
+ </div>
208
+ </div>
209
+ HTML
210
+ end
211
+
212
+ def source_icon(type)
213
+ if type == "internal"
214
+ '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>'
215
+ else
216
+ '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/></svg>'
217
+ end
218
+ end
219
+
220
+ def format_date(date)
221
+ return nil unless date
222
+
223
+ if date.respond_to?(:strftime)
224
+ date.strftime("%B %d, %Y")
225
+ else
226
+ date.to_s
227
+ end
228
+ end
229
+
230
+ def escape_html(text)
231
+ return "" unless text
232
+
233
+ text.to_s
234
+ .gsub("&", "&amp;")
235
+ .gsub("<", "&lt;")
236
+ .gsub(">", "&gt;")
237
+ .gsub('"', "&quot;")
238
+ .gsub("'", "&#39;")
239
+ end
240
+ end
241
+ end
242
+ end
243
+
244
+ # Register the tag - using "post_card" to avoid conflicts with Jekyll internals
245
+ Liquid::Template.register_tag("post_card", Jekyll::PostCard::PostTag)
246
+
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module PostCard
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
8
+
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "jekyll"
4
+ require "logger"
5
+
6
+ require_relative "jekyll-post-card/version"
7
+ require_relative "jekyll-post-card/fetcher"
8
+ require_relative "jekyll-post-card/post_tag"
9
+ require_relative "jekyll-post-card/generator"
10
+
11
+ module Jekyll
12
+ module PostCard
13
+ # Get the gem's root directory (parent of lib/)
14
+ GEM_ROOT = File.expand_path("..", __dir__)
15
+ ASSET_PATH = File.join(GEM_ROOT, "assets")
16
+
17
+ class << self
18
+ def logger
19
+ @logger ||= Logger.new($stdout, level: Logger::INFO)
20
+ end
21
+
22
+ def logger=(logger)
23
+ @logger = logger
24
+ end
25
+
26
+ def asset_path
27
+ ASSET_PATH
28
+ end
29
+ end
30
+ end
31
+ end
32
+
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: jekyll-post-card
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rodolfo Olivieri
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: jekyll
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '3.7'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '5.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '3.7'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '5.0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: logger
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.5'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.5'
46
+ - !ruby/object:Gem::Dependency
47
+ name: nokogiri
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '1.15'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '1.15'
60
+ - !ruby/object:Gem::Dependency
61
+ name: bundler
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - "~>"
65
+ - !ruby/object:Gem::Version
66
+ version: '2.0'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - "~>"
72
+ - !ruby/object:Gem::Version
73
+ version: '2.0'
74
+ - !ruby/object:Gem::Dependency
75
+ name: minitest
76
+ requirement: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - "~>"
79
+ - !ruby/object:Gem::Version
80
+ version: '5.0'
81
+ type: :development
82
+ prerelease: false
83
+ version_requirements: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '5.0'
88
+ - !ruby/object:Gem::Dependency
89
+ name: rake
90
+ requirement: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - "~>"
93
+ - !ruby/object:Gem::Version
94
+ version: '13.0'
95
+ type: :development
96
+ prerelease: false
97
+ version_requirements: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - "~>"
100
+ - !ruby/object:Gem::Version
101
+ version: '13.0'
102
+ - !ruby/object:Gem::Dependency
103
+ name: webmock
104
+ requirement: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - "~>"
107
+ - !ruby/object:Gem::Version
108
+ version: '3.18'
109
+ type: :development
110
+ prerelease: false
111
+ version_requirements: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - "~>"
114
+ - !ruby/object:Gem::Version
115
+ version: '3.18'
116
+ description: Embed beautiful, responsive post cards in your Jekyll site using a simple
117
+ Liquid tag. Works with both internal Jekyll posts and external URLs. Automatically
118
+ fetches metadata including title, description, and images.
119
+ email:
120
+ - rodolfo.olivieri3@gmail.com
121
+ executables: []
122
+ extensions: []
123
+ extra_rdoc_files: []
124
+ files:
125
+ - LICENSE
126
+ - README.md
127
+ - Rakefile
128
+ - assets/post-card.css
129
+ - demo.html
130
+ - lib/jekyll-post-card.rb
131
+ - lib/jekyll-post-card/fetcher.rb
132
+ - lib/jekyll-post-card/generator.rb
133
+ - lib/jekyll-post-card/post_tag.rb
134
+ - lib/jekyll-post-card/version.rb
135
+ homepage: https://github.com/r0x0d/jekyll-post-card
136
+ licenses:
137
+ - MIT
138
+ metadata:
139
+ homepage_uri: https://github.com/r0x0d/jekyll-post-card
140
+ source_code_uri: https://github.com/r0x0d/jekyll-post-card
141
+ changelog_uri: https://github.com/r0x0d/jekyll-post-card/blob/main/CHANGELOG.md
142
+ rubygems_mfa_required: 'true'
143
+ rdoc_options: []
144
+ require_paths:
145
+ - lib
146
+ required_ruby_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: 2.7.0
151
+ required_rubygems_version: !ruby/object:Gem::Requirement
152
+ requirements:
153
+ - - ">="
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ requirements: []
157
+ rubygems_version: 3.6.9
158
+ specification_version: 4
159
+ summary: A Jekyll plugin to display beautiful post cards in your Markdown
160
+ test_files: []