リンクカードを導入した
本当に react でいい
これを導入した1。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
require 'net/http'
require 'uri'
require 'cgi'
require 'json'
module Jekyll
class LinkCardProcessor
LINK_PATTERN = /^https?:\/\/[^\s]+$/
def self.process_content(content)
content.lines.map do |line|
trimmed_line = line.strip
if trimmed_line.match?(LINK_PATTERN)
convert_url_to_card(trimmed_line)
else
line.chomp
end
end.join("\n")
end
private
def self.convert_url_to_card(url)
begin
metadata = fetch_page_metadata(url)
generate_link_card_html(url, metadata)
rescue => e
# フォールバック: シンプルなリンクに戻す
"<p><a href=\"#{url}\">#{url}</a></p>"
end
end
def self.fetch_page_metadata(url)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = (uri.scheme == 'https')
http.read_timeout = 5
http.open_timeout = 5
request = Net::HTTP::Get.new(uri.path.empty? ? '/' : uri.path)
request['User-Agent'] = 'Mozilla/5.0 (compatible; Jekyll Link Card)'
response = http.request(request)
if response.code == '200'
body = response.body.force_encoding('UTF-8')
parse_html_metadata(body, url)
else
default_metadata(url)
end
rescue
default_metadata(url)
end
def self.parse_html_metadata(html, url)
title = nil
description = nil
favicon = nil
# Title
if html.match(/<title[^>]*>([^<]+)<\/title>/im)
title = $1.strip
elsif html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']+)["'][^>]*>/im)
title = $1.strip
end
# Description
if html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']+)["'][^>]*>/im)
description = $1.strip
elsif html.match(/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']+)["'][^>]*>/im)
description = $1.strip
end
# Favicon
if html.match(/<link[^>]*rel=["'](?:shortcut )?icon["'][^>]*href=["']([^"']+)["'][^>]*>/im)
favicon = $1.strip
unless favicon.start_with?('http')
uri = URI.parse(url)
favicon = "#{uri.scheme}://#{uri.host}#{favicon.start_with?('/') ? '' : '/'}#{favicon}"
end
end
{
title: title || url,
description: description || '',
favicon: favicon || default_favicon(url)
}
end
def self.default_metadata(url)
uri = URI.parse(url)
{
title: uri.host,
description: '',
favicon: default_favicon(url)
}
end
def self.default_favicon(url)
uri = URI.parse(url)
"#{uri.scheme}://#{uri.host}/favicon.ico"
end
def self.generate_link_card_html(url, metadata)
# 説明文があれば p タグを生成
description_html = if metadata[:description] && !metadata[:description].empty?
"<p class=\"link-card-description\">#{CGI.escapeHTML(metadata[:description])}</p>"
else
""
end
# favicon は <div> 要素として出力
favicon_html = if metadata[:favicon] && !metadata[:favicon].empty?
"<div class=\"link-card-favicon\" style=\"background-image: url('#{metadata[:favicon]}'); background-size: contain; background-repeat: no-repeat; background-position: center;\"></div>"
else
""
end
html = <<HTML
<div class="link-card-wrapper">
<a href="#{url}" class="link-card">
<div class="link-card-content">
<div class="link-card-text">
<div class="link-card-title">#{CGI.escapeHTML(metadata[:title])}</div>
#{description_html}
<p class="link-card-url">#{CGI.escapeHTML(URI.parse(url).host)}</p>
</div>
#{favicon_html}
</div>
</a>
</div>
HTML
html.strip
end
end
module Hooks
Jekyll::Hooks.register :posts, :pre_render do |post|
post.content = LinkCardProcessor.process_content(post.content)
end
Jekyll::Hooks.register :pages, :pre_render do |page|
page.content = LinkCardProcessor.process_content(page.content)
end
Jekyll::Hooks.register :documents, :pre_render do |doc|
if doc.collection.label == 'tabs'
doc.content = LinkCardProcessor.process_content(doc.content)
end
end
end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/* Link Cards Styles */
.link-card-wrapper {
margin: 1rem 0;
}
.link-card {
display: block;
padding: 1rem;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
text-decoration: none;
color: inherit;
transition: all 0.3s ease;
background-color: var(--card-bg);
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: var(--link-color);
text-decoration: none;
color: inherit;
}
.link-card-content {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.link-card-favicon {
width: 20px;
height: 20px;
flex-shrink: 0;
border-radius: 2px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.link-card-text {
flex-grow: 1;
min-width: 0;
}
.link-card-title {
font-size: 1.15rem;
font-weight: 700;
margin: 0 0 0.5rem 0;
line-height: 1.3;
color: var(--heading-color);
display: block;
}
.link-card-description {
font-size: 0.9rem;
color: var(--text-muted-color);
margin: 0 0 0.5rem 0;
line-height: 1.4;
}
.link-card-url {
font-size: 0.8rem;
color: var(--link-color);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.link-card-image {
width: 100px;
height: 60px;
object-fit: cover;
border-radius: 4px;
flex-shrink: 0;
}
@media (max-width: 576px) {
.link-card-content {
flex-direction: column;
}
.link-card-image {
width: 100%;
height: 120px;
}
}
}
/* Dark theme adjustments */
[data-mode="dark"] .link-card {
background-color: var(--card-bg);
border-color: var(--border-color);
&:hover {
box-shadow: 0 4px 12px rgba(255, 255, 255, 0.1);
}
}
将来的にはサムネイルの追加も行いたい、気が乗ったらやる。
本当はこのブログを Astro に移行する予定だったが、思ったよりもレイアウトの移行に手こずったので実装に舵を切った。 ↩︎
This post is licensed under CC BY 4.0 by the author.