前言

在逛博客友链时发现了一个好看的个人首页,于是进行了修改。

https://icl.moe/

修改

自动获取文章

2025-01-25-0k6W.avif

这里是在articles_list.ts里获取的,由于halo提供了RESTful API,所以我写了一个js脚本来获取数据。

import axios from 'axios';
import dotenv from 'dotenv';
import fs from 'fs';

// 加载环境变量
dotenv.config();

// 从环境变量中获取基础 URL、API Token 和每类文章的最大数量
const baseUrl = process.env.BASE_URL;
const apiToken = process.env.API_TOKEN;
const maxArticlesPerCategory = parseInt(process.env.MAX_ARTICLES_PER_CATEGORY) || 10;  // 默认最大数量为 10

// 设置请求头,包含 API Token
const headers = {
    Authorization: `Bearer ${apiToken}`,
};

/**
 * 异步获取文章帖子数据
 * 此函数从 API 获取文章数据,处理并分类,然后写入文件系统
 */
async function fetchPosts() {
    try {
        // 发起 GET 请求获取数据
        const response = await axios.get(baseUrl, { headers });

        // 检查响应状态
        if (response.status === 200) {
            // 解析基础域名
            const baseDomain = new URL(baseUrl).origin;

            // 获取响应数据中的项目
            const items = response.data.items;

            // 初始化数据,存储“最新”
            const categoriesData = {
                latest: {
                    title: "最新",
                    url: `${baseDomain}/archives`,
                    articles: [],
                },
            };

            // 如果项目存在且为数组,则进行处理
            if (items && Array.isArray(items)) {
                // 按发布日期降序排序项目
                items.sort((a, b) => new Date(b.post.spec.publishTime) - new Date(a.post.spec.publishTime));

                // 遍历每个项目
                items.forEach(item => {
                    const post = item.post.spec;
                    const postTitle = post.title;
                    const postUrl = item.post.status.permalink;
                    const postTime = post.publishTime;
                    const categories = item.categories.map(category => category.spec.displayName);

                    // 将时间格式修改为年月日
                    const formattedDate = new Date(postTime).toISOString().split('T')[0].replace(/-/g, '/');
                    const fullPostUrl = `${baseDomain}${postUrl}`;

                    // 向“最新”分类中添加文章,直到达到最大数量
                    if (categoriesData.latest.articles.length < maxArticlesPerCategory) {
                        categoriesData.latest.articles.push({
                            title: postTitle,
                            url: fullPostUrl,
                            time: formattedDate,
                        });
                    }

                    // 遍历每个分类,创建或更新分类数据
                    categories.forEach(category => {
                        if (!categoriesData[category]) {
                            categoriesData[category] = {
                                title: category,
                                url: `${baseDomain}/categories/${item.categories[0].spec.slug}`,
                                articles: [],
                            };
                        }

                        // 向分类中添加文章,直到达到最大数量
                        if (categoriesData[category].articles.length < maxArticlesPerCategory) {
                            categoriesData[category].articles.push({
                                title: postTitle,
                                url: fullPostUrl,
                                time: formattedDate,
                            });
                        }
                    });
                });
            } else {
                console.error('没有找到 items 数据');
            }

            // 将分类数据转换为数组并调整顺序,将“最新”分类置于首位
            const formattedCategories = Object.values(categoriesData);
            const latestCategory = formattedCategories.shift();
            formattedCategories.unshift(latestCategory);

            // 将分类数据转换为字符串并写入文件
            const outputData = `export default ${JSON.stringify(formattedCategories, null, 2)};`;
            fs.writeFileSync('./src/data/articles_list.ts', outputData, 'utf8');
            console.log('数据已成功写入到 articles_list.ts 文件');
        } else {
            console.error(`请求失败,状态码: ${response.status}`);
        }
    } catch (error) {
        console.error('请求过程中发生错误:', error.message);
    }
}

// 调用函数获取文章数据
fetchPosts().then(r => {}).catch(error => console.error(error));

结合GitHub Actions定期获取数据来进行展示。

name: Fetch Posts

on:
  schedule:
    - cron: '0 0 * * 1'  # 每周一零点执行
  workflow_dispatch:      # 允许手动触发

jobs:
  fetch-and-update:
    runs-on: ubuntu-latest
    
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.PAT_TOKEN }}
        
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          
      - name: Install dependencies
        run: |
          npm install axios
          npm install dotenv
          
      - name: Run fetch script
        run: node src/data/getPost.js
        env:
          BASE_URL: ${{ secrets.BASE_URL }}
          API_TOKEN: ${{ secrets.API_TOKEN }}
          MAX_ARTICLES_PER_CATEGORY: ${{ secrets.MAX_ARTICLES_PER_CATEGORY }}
          
      - name: Commit and push if changes
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add src/data/articles_list.ts
          git diff --quiet && git diff --staged --quiet || (git commit -m "自动更新文章列表" && git push)

这里的执行需要在Actions secrets and variables中设置BASE_URLAPI_TOKENMAX_ARTICLES_PER_CATEGORYPAT_TOKEN

BASE_URL:请求路径,如---/apis/api.console.halo.run/v1alpha1/posts?publishPhase=PUBLISHED

API_TOKEN:个人令牌,在halo的个人页面中获取。

MAX_ARTICLES_PER_CATEGORY:每列最大数据量。

PAT_TOKEN:GitHub机器人提交所需要的Personal access tokens (classic)。

选择卡片

原有的卡片不想展示需要对代码进行删除,过于繁琐,于是我将其修改为可在info.ts进行控制。

  // 控制显示的配置
  showTitleCard: true,
  showBlogCard: true,
  showSecondaryCards: false,
  showPrimaryCards1: false,
  showPrimaryCards2: false,
  showMap: true,
  showArticles: true,
  showProjects: false,
  showMusic: false,
  showContact: true,
<!--社交网络1-->
{info.showPrimaryCards1 && (
<div class="hidden md:block col-span-4 md:col-span-2 md:col-start-3">
  {info.secondaryCards.map((card) => <Card_2x1 info={card} />)}
</div>
    )}
<!--社交网络2-->
{info.showPrimaryCards2 && (
<div class="col-span-2 md:col-span-1">
  <Card_2x2 info={info.primaryCards[0]} />
  <Card_2x2 info={info.primaryCards[1]} />
</div>
<div class="col-span-2 md:col-span-1">
  <Card_2x2 info={info.primaryCards[2]} />
  <Card_2x2 info={info.primaryCards[3]} />
</div>
    )}

<!--地图-->
{info.showMap && (
<div
    style="display: none"
    class="map col-span-4 md:col-span-2 w-full lg:w-[26rem] lg:h-[26rem] relative md:aspect-[1/1]"
>
  <div
      class="rounded-xl shadow-xl shadow-accent hover:shadow-gray-500 w-full h-full lg:w-[26rem] lg:h-[26rem] absolute"
      id="map-container"
  >
  </div>
  <div
      class="absolute bottom-8 right-8 badge badge-neutral p-4 rounded-xl font-semibold"
  >
    {info.location}
  </div>
</div>
    )}

<!-- 文章列表 -->
{info.showArticles && (
<div class="col-span-4">
  <Card_8x1 info={info.sectionTitles[0]} />
  <Articles_List></Articles_List>
</div>
    )}

<!-- 个人项目 -->
{info.showProjects && (
<>
  <div class="col-span-4">
    <Card_8x1 info={info.sectionTitles[1]} />
  </div>
  <div class="col-span-4 md:col-span-2">
    <Card_4x2 info={info.projectCardStart} />
  </div>
  {info.projectCards.map((card) => (
      <div class="col-span-2 md:col-span-1">
        <Card_2x2 info={card} />
      </div>
  ))}
  <div class="col-span-4 md:col-span-2">
    <Card_4x2 info={info.projectCardEnd} />
  </div>
</>
    )}

<!-- 音乐创作 -->
{info.showMusic && (
<>
  <div class="col-span-4">
    <Card_8x1 info={info.sectionTitles[2]} />
  </div>
  {info.musicCards.map((card) => (
      <div class="col-span-4 md:col-span-2">
        <Card_4x2 info={card} />
      </div>
  ))}
</>
    )}

<!-- 联系方式 -->
{info.showContact && (
<>
  <div class="col-span-4">
    <Card_8x1 info={info.sectionTitles[3]} />
  </div>
  {info.teleCards.map((card) => (
      <div class="col-span-4 md:col-span-2">
        <Card_2x1 info={card} />
      </div>
  ))}
</>
    )}

部署

使用vercel进行托管,不仅免去服务器而且能与GitHub Actions进行配合,当提交更新数据时vercel也会进行更新,避免手动进行更新。

https://vercel.com/

预览

https://www.houxiongxiong.icu/