feat: add link card shortcode
This commit is contained in:
parent
27201e2be8
commit
fd3b47fa91
2 changed files with 248 additions and 2 deletions
239
src/components/shortcodes/LinkCard.astro
Normal file
239
src/components/shortcodes/LinkCard.astro
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
url: string;
|
||||||
|
showArchive?: boolean;
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
siteName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url, showArchive = true} = Astro.props;
|
||||||
|
|
||||||
|
const siteMetadata = {
|
||||||
|
title: Astro.props.title || '',
|
||||||
|
description: Astro.props.description || '',
|
||||||
|
siteName: Astro.props.siteName || '',
|
||||||
|
image: '',
|
||||||
|
domain: new URL(url).hostname || ''
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get metadata from the URL
|
||||||
|
async function fetchMetadata(url: string) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': 'Mozilla/5.0 (compatible; LinkCard/1.0)'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
|
||||||
|
// 提取元数据
|
||||||
|
const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
|
||||||
|
const descriptionMatch = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i) ||
|
||||||
|
html.match(/<meta[^>]+property=["']og:description["'][^>]+content=["']([^"']+)["']/i);
|
||||||
|
const imageMatch = html.match(/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i) ||
|
||||||
|
html.match(/<meta[^>]+name=["']twitter:image["'][^>]+content=["']([^"']+)["']/i);
|
||||||
|
const siteNameMatch = html.match(/<meta[^>]+property=["']og:site_name["'][^>]+content=["']([^"']+)["']/i);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: titleMatch?.[1]?.trim() || new URL(url).hostname,
|
||||||
|
description: descriptionMatch?.[1]?.trim() || '',
|
||||||
|
image: imageMatch?.[1]?.trim() || '',
|
||||||
|
siteName: siteNameMatch?.[1]?.trim() || new URL(url).hostname,
|
||||||
|
domain: new URL(url).hostname
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to fetch metadata for ${url}:`, error);
|
||||||
|
const domain = new URL(url).hostname;
|
||||||
|
return {
|
||||||
|
title: domain,
|
||||||
|
description: '',
|
||||||
|
image: '',
|
||||||
|
siteName: domain,
|
||||||
|
domain
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the URL is archived on the Wayback Machine at the build time
|
||||||
|
async function checkArchive(url: string) {
|
||||||
|
try {
|
||||||
|
const archiveUrl = `https://archive.org/wayback/available?url=${encodeURIComponent(url)}`;
|
||||||
|
const response = await fetch(archiveUrl);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.archived_snapshots?.closest?.available) {
|
||||||
|
return data.archived_snapshots.closest.url;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to check archive for ${url}:`, error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract metadata and archive URL
|
||||||
|
const metadata = Astro.props.title ? siteMetadata : await fetchMetadata(url);
|
||||||
|
const archiveUrl = showArchive ? await checkArchive(url) : null;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="link-card">
|
||||||
|
<a href={url} target="_blank" rel="noopener noreferrer" class="link-card__main">
|
||||||
|
{metadata.image && (
|
||||||
|
<div class="link-card__image">
|
||||||
|
<img src={metadata.image} alt={metadata.title} loading="lazy" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="link-card__content">
|
||||||
|
<div class="link-card__header">
|
||||||
|
<h3 class="link-card__title">{metadata.title}</h3>
|
||||||
|
<span class="link-card__domain">{metadata.domain}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{metadata.description && (
|
||||||
|
<p class="link-card__description">{metadata.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{metadata.siteName && <div class="link-card__footer">
|
||||||
|
<span class="link-card__site-name">{metadata.siteName}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
{showArchive && archiveUrl && (
|
||||||
|
<div class="link-card__archive">
|
||||||
|
<a href={archiveUrl} target="_blank" rel="noopener noreferrer" title="View archived version">
|
||||||
|
View archived version
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.link-card {
|
||||||
|
border: 1px solid var(--border-color, #e1e5e9);
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__main {
|
||||||
|
display: flex;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
min-height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__image {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 200px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__header {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--header-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__domain {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
text-transform: lowercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__description {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 8px 0;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__footer {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__site-name {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #888;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__archive {
|
||||||
|
border-top: 1px solid var(--border-color, #e1e5e9);
|
||||||
|
padding: 8px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__archive a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* mobile devices */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.link-card__main {
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__image {
|
||||||
|
width: 100%;
|
||||||
|
height: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__content {
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-card__title {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image not exist*/
|
||||||
|
.link-card__main:not(:has(.link-card__image)) .link-card__content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -5,6 +5,7 @@ description: "Sample article showcasing basic Markdown syntax and formatting for
|
||||||
tags: ["markdown", "css", "html", "sample"]
|
tags: ["markdown", "css", "html", "sample"]
|
||||||
---
|
---
|
||||||
import Callout from '/src/components/shortcodes/Callout.astro';
|
import Callout from '/src/components/shortcodes/Callout.astro';
|
||||||
|
import LinkCard from '/src/components/shortcodes/LinkCard.astro';
|
||||||
|
|
||||||
This article offers a sample of basic and extended Markdown formatting that can be used, also it shows how some basic HTML elements are decorated.
|
This article offers a sample of basic and extended Markdown formatting that can be used, also it shows how some basic HTML elements are decorated.
|
||||||
|
|
||||||
|
@ -43,8 +44,14 @@ You can use callouts to highlight important information or warnings in your cont
|
||||||
This is an error callout. It should be used to indicate critical issues that need immediate attention.
|
This is an error callout. It should be used to indicate critical issues that need immediate attention.
|
||||||
</Callout>
|
</Callout>
|
||||||
```
|
```
|
||||||
|
### LinkCard
|
||||||
|
You can use the `LinkCard` component to create cards that link to external resources or pages. This is useful for showcasing projects, documentation, or any other relevant links.
|
||||||
|
<LinkCard url="https://astro.build"/>
|
||||||
|
```mdx
|
||||||
|
<LinkCard url="https://astro.build"/>
|
||||||
|
```
|
||||||
|
Or to customize the card further with a title and description:
|
||||||
|
<LinkCard url="https://www.bilibili.com/video/BV1PC4y1L7mq/" title="Don't check the description" description="Don't click on the link" />
|
||||||
## Headings
|
## Headings
|
||||||
|
|
||||||
The following HTML `<h1>`—`<h6>` elements represent six levels of section headings. `<h1>` is the highest section level while `<h6>` is the lowest.
|
The following HTML `<h1>`—`<h6>` elements represent six levels of section headings. `<h1>` is the highest section level while `<h6>` is the lowest.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue