Last active
June 9, 2024 01:56
-
-
Save tak-dcxi/a0864b9954d21c247e7e9b96769da282 to your computer and use it in GitHub Desktop.
AstroLinkCard
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
--- | |
import { load } from 'cheerio' | |
import Image from 'astro/components/Image.astro' | |
import { writeFileSync, readFileSync } from 'node:fs' | |
import BaseIcon from '@/components/BaseIcon.astro' | |
type Props = { | |
href: string | |
} | |
type PostOGP = { | |
title: string | |
description: string | |
image: string | |
favicon: string | |
} | |
type OGPJson = Record<string, PostOGP> | |
const { href } = Astro.props | |
const generatedFilePath = new URL('../../generated/ogp.json', import.meta.url) | |
const faviconSize = 256 | |
const origin = new URL(href).origin | |
const fetchOGP = async (url: string): Promise<PostOGP> => { | |
const html = await fetch(url).then((res) => res.text()) | |
const $ = load(html) | |
const title = $("meta[property='og:title']").attr('content') ?? $('title').text() | |
const description = | |
$("meta[property='og:description']").attr('content') ?? $("meta[name='description']").attr('content') ?? '' | |
const favicon = `https://www.google.com/s2/favicons?sz=${faviconSize}&domain=${origin}` | |
let image = $("meta[property='og:image']").attr('content') ?? '' | |
if (image && !image.startsWith('http')) { | |
image = `${new URL(url).origin}/${image}` | |
} | |
return { title, description, favicon, image } | |
} | |
const ogp: OGPJson = JSON.parse(readFileSync(generatedFilePath, 'utf8')) | |
const postOGP: PostOGP = ogp[href] ?? (await fetchOGP(href)) | |
if (import.meta.env.PROD && !ogp[href]) { | |
ogp[href] = postOGP | |
writeFileSync(generatedFilePath, JSON.stringify(ogp, null, 2)) | |
} | |
const innerLink = href.startsWith('https://www.tak-dcxi.com') | |
--- | |
<div class="article-link-card" role="group" aria-label="参考リンク"> | |
<div class="meta"> | |
<p class="title"> | |
<a href={href} target={innerLink ? null : '_blank'} rel={innerLink ? null : 'external'}> | |
{postOGP.title} | |
{!innerLink && <BaseIcon type="launch-link" size="1em" align="-0.15em" label="新しいウィンドウが開きます" />} | |
</a> | |
</p> | |
{ | |
postOGP.description && ( | |
<p class="description u-text-sm"> | |
{postOGP.description.length > 150 ? `${postOGP.description?.slice(0, 150)}...` : postOGP.description} | |
</p> | |
) | |
} | |
<p class="url"> | |
<Image | |
src={postOGP.favicon} | |
alt="" | |
decoding="async" | |
loading="lazy" | |
width={16} | |
height={16} | |
onerror={`this.onerror=null; this.style.display='none'`} | |
/> | |
{new URL(href).hostname} | |
</p> | |
</div> | |
{ | |
postOGP.image && ( | |
<a | |
href={href} | |
class="thumbnail" | |
tabindex="-1" | |
aria-hidden="true" | |
target={innerLink ? null : '_blank'} | |
rel={innerLink ? null : 'external'} | |
> | |
<Image | |
src={postOGP.image} | |
alt="" | |
decoding="async" | |
loading="lazy" | |
width={1200} | |
height={630} | |
onerror={`this.onerror=null; this.style.display='none'`} | |
/> | |
</a> | |
) | |
} | |
</div> | |
<style> | |
.article-link-card { | |
--_card-background: var(--_is-card-active-false, var(--background-strong)) | |
var(--_is-card-active-true, var(--background-muted)); | |
--_card-duration: var(--duration-default); | |
--_is-card-active-true: ; | |
--_is-card-active-false: initial; | |
container: article-link-card / inline-size; | |
display: block grid; | |
grid-template-columns: 1fr max-content; | |
border: 1px solid var(--border-base); | |
background-color: var(--_card-background); | |
transition: background-color var(--_card-duration); | |
&:has(:focus-visible), | |
&:has(:focus-visible) * { | |
--_is-card-active-true: initial; | |
--_is-card-active-false: ; | |
} | |
&:has(:any-link:hover), | |
&:has(:any-link:hover) * { | |
@media (any-hover: hover) { | |
--_is-card-active-true: initial; | |
--_is-card-active-false: ; | |
} | |
} | |
} | |
.meta { | |
display: block grid; | |
row-gap: var(--spacing-md); | |
justify-content: space-between; | |
align-items: center; | |
padding: 1.5em; | |
@container article-link-card (inline-size <= 30rem) { | |
grid-column: 1 / -1; | |
grid-row: 2 / 3; | |
} | |
} | |
.title { | |
text-wrap: pretty; | |
word-break: auto-phrase; | |
} | |
.title :where(:any-link) { | |
display: -webkit-box; | |
overflow: clip; | |
text-overflow: ellipsis; | |
color: var(--_is-card-active-true, var(--foreground-active)); | |
text-decoration: revert; | |
transition: color var(--_card-duration); | |
-webkit-box-orient: block-axis; | |
-webkit-line-clamp: 3; | |
} | |
.description { | |
display: -webkit-box; | |
overflow: clip; | |
text-overflow: ellipsis; | |
color: var(--foreground-muted); | |
line-height: var(--leading-snug); | |
hanging-punctuation: last allow-end; | |
-webkit-box-orient: block-axis; | |
-webkit-line-clamp: 2; | |
} | |
.url { | |
display: block flex; | |
column-gap: 1ex; | |
line-height: var(--leading-tight); | |
text-justify: inter-character; | |
word-break: break-all; | |
} | |
.url :where(img) { | |
inline-size: auto; | |
block-size: 1lh; | |
} | |
.thumbnail { | |
contain: strict; | |
min-inline-size: 40cqi; | |
@container article-link-card (inline-size <= 30rem) { | |
grid-column: 1 / -1; | |
grid-row: 1 / 2; | |
aspect-ratio: var(--aspect-thumbnail); | |
} | |
&:has([style*='display: none']) { | |
display: none; | |
} | |
} | |
.thumbnail :where(img) { | |
inline-size: 100%; | |
block-size: 100%; | |
object-fit: cover; | |
scale: var(--_is-card-active-true, 1.1); | |
transition: scale var(--_card-duration); | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment