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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +177 -0
- data/Rakefile +13 -0
- data/assets/post-card.css +364 -0
- data/demo.html +821 -0
- data/lib/jekyll-post-card/fetcher.rb +201 -0
- data/lib/jekyll-post-card/generator.rb +47 -0
- data/lib/jekyll-post-card/post_tag.rb +246 -0
- data/lib/jekyll-post-card/version.rb +8 -0
- data/lib/jekyll-post-card.rb +32 -0
- metadata +160 -0
|
@@ -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("&", "&")
|
|
235
|
+
.gsub("<", "<")
|
|
236
|
+
.gsub(">", ">")
|
|
237
|
+
.gsub('"', """)
|
|
238
|
+
.gsub("'", "'")
|
|
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,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: []
|