Skip to content

Commit 8d58062

Browse files
committed
Merge branch 'develop'
2 parents a599e5f + 0ba4d86 commit 8d58062

File tree

2 files changed

+266
-1
lines changed

2 files changed

+266
-1
lines changed

WebSite/assets/css/blog.css

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,174 @@
2222
margin-bottom: 1.5rem;
2323
}
2424

25+
/* ==========================================================================
26+
Link Card (OGP)
27+
========================================================================== */
28+
.og-card {
29+
display: flex;
30+
width: 80%;
31+
align-items: stretch;
32+
background: var(--color-surface);
33+
border: 1px solid var(--color-border);
34+
border-radius: 12px;
35+
overflow: hidden;
36+
text-decoration: none !important;
37+
transition: background-color 0.2s ease;
38+
margin: 1.5rem 0;
39+
height: 130px;
40+
color: inherit;
41+
}
42+
43+
.og-card:hover {
44+
background-color: rgba(125, 125, 125, 0.05);
45+
border-color: var(--color-border);
46+
transform: none;
47+
box-shadow: none;
48+
}
49+
50+
/* Thumbnail (Left side of CARD) */
51+
.og-thumbnail {
52+
display: block;
53+
flex: 0 0 130px;
54+
width: 130px;
55+
height: 130px;
56+
background: var(--color-surface);
57+
position: relative;
58+
border-right: 1px solid var(--color-border);
59+
overflow: hidden;
60+
}
61+
62+
.og-thumbnail img {
63+
width: 100%;
64+
height: 100%;
65+
object-fit: cover;
66+
display: block;
67+
/* Reset markdown styles */
68+
max-width: none !important;
69+
box-sizing: border-box !important;
70+
background-color: transparent !important;
71+
margin: 0 !important;
72+
padding: 0 !important;
73+
border: none !important;
74+
}
75+
76+
.og-thumbnail.no-image {
77+
display: flex;
78+
align-items: center;
79+
justify-content: center;
80+
background: #eee;
81+
}
82+
83+
/* Content (Right side of CARD) */
84+
.og-content {
85+
flex: 1;
86+
padding: 12px 15px;
87+
display: flex;
88+
flex-direction: column;
89+
justify-content: center;
90+
min-width: 0;
91+
height: 100%;
92+
overflow: hidden;
93+
}
94+
95+
.og-site {
96+
font-size: 0.85rem;
97+
color: var(--color-text-muted);
98+
margin-bottom: 2px;
99+
white-space: nowrap;
100+
overflow: hidden;
101+
text-overflow: ellipsis;
102+
}
103+
104+
.og-title {
105+
font-size: 1rem;
106+
font-weight: 700;
107+
color: var(--color-heading);
108+
margin-bottom: 2px;
109+
white-space: nowrap;
110+
overflow: hidden;
111+
text-overflow: ellipsis;
112+
line-height: 1.3;
113+
}
114+
115+
.og-description {
116+
font-size: 0.85rem;
117+
color: var(--color-text-muted);
118+
display: -webkit-box;
119+
overflow: hidden;
120+
line-height: 1.3;
121+
}
122+
123+
/* Link Card (OGP) - Wide (WILDCARD) */
124+
.og-card.wide {
125+
display: block;
126+
height: auto;
127+
width: 60%;
128+
border: none;
129+
background: transparent;
130+
border-radius: 0;
131+
overflow: visible;
132+
margin: 1.5rem 0;
133+
}
134+
135+
.og-card.wide:hover {
136+
background: transparent;
137+
opacity: 0.7;
138+
}
139+
140+
.og-image-wide {
141+
width: 100%;
142+
aspect-ratio: 1.91 / 1;
143+
background: #000;
144+
position: relative;
145+
border-radius: 16px;
146+
overflow: hidden;
147+
border: 1px solid var(--color-border);
148+
display: flex;
149+
align-items: center;
150+
justify-content: center;
151+
}
152+
153+
.og-image-wide img {
154+
width: 100%;
155+
height: 100%;
156+
object-fit: contain;
157+
display: block;
158+
/* Reset markdown styles */
159+
max-width: none !important;
160+
box-sizing: border-box !important;
161+
background-color: transparent !important;
162+
margin: 0 !important;
163+
padding: 0 !important;
164+
border: none !important;
165+
}
166+
167+
.og-title-overlay {
168+
position: absolute;
169+
bottom: 12px;
170+
left: 12px;
171+
background: rgba(0, 0, 0, 0.6);
172+
color: #fff;
173+
padding: 4px 8px;
174+
border-radius: 4px;
175+
font-size: 14px;
176+
font-weight: 700;
177+
max-width: calc(100% - 24px);
178+
white-space: nowrap;
179+
overflow: hidden;
180+
text-overflow: ellipsis;
181+
pointer-events: none;
182+
}
183+
184+
.og-site-footer {
185+
margin-top: 2px;
186+
font-size: 0.85rem;
187+
color: var(--color-text-muted);
188+
padding-left: 4px;
189+
display: flex;
190+
align-items: center;
191+
}
192+
25193
.blog-filter {
26194
display: flex;
27195
flex-direction: column;

WebSite/tools/build-blog.js

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,60 @@ const POST_API_URL =
8585
process.env.POST_API_URL ||
8686
"https://script.google.com/macros/s/AKfycbyuVrlM-7-Jps0GuZxLJtGw_y5R2bouUVYapYBhk5-CFL-xUiS8bIYUlw2crFnkcrWg/exec";
8787

88+
async function fetchOgp(url) {
89+
try {
90+
const res = await fetch(url);
91+
if (!res.ok) return null;
92+
const html = await res.text();
93+
94+
const getMeta = (prop) => {
95+
const regex = new RegExp(
96+
`<meta\\s+(?:property|name)="${prop}"\\s+content="([^"]+)"`,
97+
"i",
98+
);
99+
const m = html.match(regex);
100+
return m ? m[1] : "";
101+
};
102+
103+
const title =
104+
getMeta("og:title") ||
105+
html.match(/<title>([^<]+)<\/title>/i)?.[1] ||
106+
"";
107+
const description =
108+
getMeta("og:description") || getMeta("description");
109+
const image = getMeta("og:image");
110+
111+
let domain = "";
112+
try {
113+
domain = new URL(url).hostname;
114+
} catch (e) {}
115+
116+
return { title, description, image, domain, url };
117+
} catch (e) {
118+
return null;
119+
}
120+
}
121+
122+
function createLinkCardHtml(ogp, type) {
123+
const isWide = type === "WILDCARD";
124+
125+
if (isWide) {
126+
// WILDCARD: 大きな画像、画像内左下にタイトル、下にドメイン
127+
const imageHtml = ogp.image
128+
? `<span class="og-image-wide"><img src="${escapeHtmlAttr(ogp.image)}" alt="${escapeHtmlAttr(ogp.title)}" loading="lazy"><span class="og-title-overlay">${escapeHtml(ogp.title)}</span></span>`
129+
: `<span class="og-image-wide no-image"><span class="og-title-overlay">${escapeHtml(ogp.title)}</span></span>`;
130+
131+
return `<a href="${escapeHtmlAttr(ogp.url)}" class="og-card wide" target="_blank" rel="noopener noreferrer">${imageHtml}<span class="og-site-footer">${escapeHtml(ogp.domain)} から</span></a>`;
132+
} else {
133+
// CARD: 左にサムネイル、右にドメイン・タイトル・説明
134+
const imageHtml = ogp.image
135+
? `<span class="og-thumbnail"><img src="${escapeHtmlAttr(ogp.image)}" alt="${escapeHtmlAttr(ogp.title)}" loading="lazy"></span>`
136+
: `<span class="og-thumbnail no-image"></span>`;
137+
138+
return `<a href="${escapeHtmlAttr(ogp.url)}" class="og-card" target="_blank" rel="noopener noreferrer">${imageHtml}<span class="og-content"><span class="og-site">${escapeHtml(ogp.domain)}</span><span class="og-title">${escapeHtml(ogp.title)}</span><span class="og-description">${escapeHtml(ogp.description)}</span></span></a>`;
139+
}
140+
}
141+
88142
// ---------------------------
89143
// helper: XML/HTML escape
90144
// ---------------------------
@@ -692,7 +746,50 @@ function createHtml({
692746
);
693747

694748
const raw = fs.readFileSync(sourcePath, "utf8");
695-
const { data, content } = matter(raw);
749+
const { data, content: rawContent } = matter(raw);
750+
let content = rawContent;
751+
752+
// Process Link Cards: [!CARD](url) and [!WILDCARD](url)
753+
const linkCardRegex =
754+
/\[!(CARD|WILDCARD)\]\((.*?)\)/g;
755+
const matches = [...content.matchAll(linkCardRegex)];
756+
757+
// Deduplicate matches to avoid redundant fetches
758+
const uniqueMatches = Array.from(
759+
new Map(matches.map((m) => [m[0], m])).values(),
760+
);
761+
const replacements = new Map();
762+
763+
await Promise.all(
764+
uniqueMatches.map(async (match) => {
765+
const type = match[1];
766+
const url = match[2];
767+
const key = match[0];
768+
769+
try {
770+
const ogp = await fetchOgp(url);
771+
if (ogp) {
772+
const cardHtml = createLinkCardHtml(
773+
ogp,
774+
type,
775+
);
776+
replacements.set(key, cardHtml);
777+
} else {
778+
replacements.set(key, `[${url}](${url})`);
779+
}
780+
} catch (e) {
781+
logger.warn(
782+
`Failed to fetch OGP for ${url}: ${e.message}`,
783+
);
784+
replacements.set(key, `[${url}](${url})`);
785+
}
786+
}),
787+
);
788+
789+
// Apply replacements globally
790+
for (const [key, value] of replacements) {
791+
content = content.split(key).join(value);
792+
}
696793

697794
const headings = [];
698795
const slugger = new marked.Slugger();

0 commit comments

Comments
 (0)