Sakoamc

神仙鱼不吃鱼鱼

博客折腾记:手搓一个Astro NeoDB 书影音卡片

发布于 # 折腾 # 2026

之前看到不少博客里添加了书影音的小卡片,心痒痒的,毕竟光秃秃的超链接视觉上确实索然无味,于是想在博客里折腾个 NeoDB 书影音卡片(想挺久了),由于咱也不懂代码,就和Gemini慢慢边聊边改(其实还是不习惯用CLI)。我主要负责“提视觉和交互 Bug”,Gemini 负责“疯狂改代码”,提要求这块我可是一点不含糊。几轮死磕下来,也算是能看、能用了。

下面记录一下我在这场折腾中发现并解决的几个问题:

🎨


💻 最终成型的组件全量代码

这里放出 Gemini 最终帮我写好的、完美通过编译的 NeoCard.astro 代码。它还支持传入 subtitle 参数,用来手动写上导演、演员或作者年份,填补排版空白:

  ---
  export interface Props {
    url: string;
    myRating?: string;  
    myComment?: string; 
    subtitle?: string;  
  }

  const { url, myRating, myComment, subtitle } = Astro.props;

  let data = null;

  if (url) {
    try {
      const response = await fetch(url, {
        headers: {
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, Gecko) Chrome/120.0.0.0'
        }
      });
      
      if (response.ok) {
        const html = await response.text();
        
        const getMeta = (metaName: string) => {
          const rgx = new RegExp(`<meta[^>]*property=["']og:${metaName}["'][^>]*content=["']([^"']*)["']`, 'i');
          const match = html.match(rgx);
          if (match) return match[1];

          const rgxSwapped = new RegExp(`<meta[^>]*content=["']([^"']*)["'][^>]*property=["']og:${metaName}["']`, 'i');
          const matchSwapped = html.match(rgxSwapped);
          return matchSwapped ? matchSwapped[1] : null;
        };

        const title = getMeta('title');
        const image = getMeta('image');
        const description = getMeta('description');

        if (title) {
          const proxiedImage = image ? `https://wsrv.nl/?url=${encodeURIComponent(image)}` : null;

          data = {
            title: title.replace(' - NeoDB', '').trim(),
            image: proxiedImage,
            description: description ? description.replace(/&quot;/g, '"').replace(/&amp;/g, '&').trim() : '暂无简介...'
          };
        }
      }
    } catch (error) {
      console.error(`[NeoCard] 抓取失败: ${url}`);
    }
  }
  ---

  {data ? (
    <div class="neocard-container">
      
      {/* 左侧:海报区 */}
      {data.image && (
        <a href={url} target="_blank" rel="noreferrer" class="neocard-poster-link">
          <img src={data.image} alt={data.title} class="neocard-raw-poster" loading="lazy" />
        </a>
      )}
      
      {/* 右侧:排版内容区 */}
      <div class="neocard-text-layout">
        
        <div class="neocard-heading-group">
          <h4 class="neocard-title">
            <a href={url} target="_blank" rel="noreferrer" class="neocard-title-link">
              {data.title}
            </a>
          </h4>
          {subtitle && <div class="neocard-subtitle">{subtitle}</div>}
        </div>
        
        {/* 个人评价便签盒 */}
        {(myRating || myComment) && (
          <div class="neocard-review-sticker">
            {myRating && <div class="neocard-rating-row">RATING: <span class="stars">{myRating}</span></div>}
            {myComment && <p class="neocard-comment-row">“{myComment}”</p>}
          </div>
        )}
        
        {/* 官方简介 */}
        <p class="neocard-excerpt">{data.description}</p>
        
        {/* 页脚定位 */}
        <div class="neocard-footer-line">
          <a href={url} target="_blank" rel="noreferrer" class="neocard-footer-link">
            via neodb.social <span class="neocard-arrow">➔</span>
          </a>
        </div>
      </div>

    </div>
  ) : (
    <a href={url} target="_blank" class="fallback-link">🔗 查看书影音详情: {url}</a>
  )}

  <style>
  .neocard-container {
    display: flex;
    flex-direction: row;
    border: 1px solid var(--uno-colors-shadow, rgba(0, 0, 0, 0.12));
    border-radius: 12px;
    overflow: hidden;
    isolation: isolate;
    margin: 2rem 0;
    padding: 1rem 1.2rem;
    gap: 1.2rem;
    background-color: color-mix(in srgb, var(--uno-colors-background, #fff) 96%, var(--uno-colors-primary, #000) 4%); 
    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.03);
    transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s cubic-bezier(0.16, 1, 0.3, 1);
  }

  .neocard-container:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.06);
  }

  .neocard-poster-link {
    display: block;
    flex-shrink: 0;
    align-self: flex-start;
  }

  .neocard-raw-poster {
    width: 95px; 
    height: auto;
    object-fit: contain;
    border-radius: 6px;
    display: block;
  }

  .neocard-text-layout {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    gap: 0.4rem; 
  }

  .neocard-heading-group {
    margin-bottom: 0.05rem;
  }

  .neocard-title {
    font-family: var(--uno-fonts-ui), monospace !important; 
    font-weight: 700 !important;
    font-size: 1.25rem !important;
    margin: 0 !important;
    line-height: 1.2;
  }

  .neocard-title-link {
    color: var(--uno-colors-primary, #000) !important;
    text-decoration: none !important;
    border-bottom: 1px solid transparent;
    transition: border-color 0.2s ease;
  }

  .neocard-title-link:hover {
    border-bottom-color: var(--uno-colors-primary, #000);
    background-color: transparent !important;
    color: var(--uno-colors-primary, #000) !important;
  }

  .neocard-subtitle {
    font-family: var(--uno-fonts-ui), monospace;
    font-size: 0.78rem;
    opacity: 0.45;
    margin-top: 0.25rem;
    font-weight: 600;
  }

  .neocard-review-sticker {
    padding: 0.5rem 0.7rem;
    background-color: rgba(0, 0, 0, 0.025);
    border-radius: 8px;
  }

  .neocard-rating-row {
    font-family: monospace;
    font-size: 0.7rem;
    font-weight: 700;
    margin-bottom: 0.1rem;
    opacity: 0.5;
  }

  .neocard-rating-row .stars {
    color: #fbbf24; 
    font-size: 0.8rem;
  }

  .neocard-comment-row {
    font-size: 0.85rem !important;
    line-height: 1.45;
    font-weight: 600;
    margin: 0 !important;
  }

  .neocard-excerpt {
    font-size: 0.78rem !important;
    line-height: 1.4;
    opacity: 0.52; 
    margin: 0 !important;
    display: -webkit-box;
    -webkit-line-clamp: 2; 
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .neocard-footer-line {
    display: flex;
    justify-content: flex-end; 
    margin-top: 0.1rem;
  }

  .neocard-footer-link {
    font-size: 0.65rem;
    opacity: 0.5; 
    font-family: monospace;
    font-weight: bold;
    text-decoration: none !important;
    color: inherit !important;
    transition: opacity 0.2s ease;
  }

  .neocard-footer-link:hover {
    opacity: 0.85;
    background-color: transparent !important;
  }

  .neocard-arrow {
    display: inline-block;
    transition: transform 0.2s ease;
  }

  .neocard-container:hover .neocard-arrow {
    transform: translateX(2px);
  }

  @media (max-width: 540px) {
    .neocard-container {
      flex-direction: row; 
      gap: 0.9rem;
      padding: 0.8rem;
    }
    .neocard-raw-poster {
      width: 68px; 
    }
    .neocard-title {
      font-size: 1.15rem !important;
    }
  }
  </style>

📝 怎么日常调用它? 在 MDX 文件里,现在只需要简单地写下这几行代码,就可以输出一个好看好用的排版卡片了:

先加上import NeoCard from ’../../components/NeoCard.astro’;

代码段

<NeoCard 
  url="https://neodb.social/..." 
  subtitle="电影 · 历史 ..."
  myRating="★★★★★☆☆" 
  myComment="....。" 
/>

效果图如下:



PC、移动端效果

搞定!折腾完毕,暂时能用就行。