feat: トップページの最新記事を自動生成に切り替え
All checks were successful
Deploy Docusaurus Site / deploy (push) Successful in 29s
All checks were successful
Deploy Docusaurus Site / deploy (push) Successful in 29s
This commit is contained in:
parent
7a509d5dc5
commit
8fe2e55d81
@ -22,6 +22,9 @@ jobs:
|
|||||||
- name: Install npm dependencies
|
- name: Install npm dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Generate recent posts
|
||||||
|
run: node scripts/generate-recent-posts.js
|
||||||
|
|
||||||
- name: Build static site
|
- name: Build static site
|
||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
|
|||||||
77
scripts/generate-recent-posts.js
Normal file
77
scripts/generate-recent-posts.js
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const ROOT = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
function parseFrontmatter(content) {
|
||||||
|
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
||||||
|
if (!m) return {};
|
||||||
|
const fm = {};
|
||||||
|
for (const line of m[1].split('\n')) {
|
||||||
|
const idx = line.indexOf(':');
|
||||||
|
if (idx === -1) continue;
|
||||||
|
const key = line.slice(0, idx).trim();
|
||||||
|
let val = line.slice(idx + 1).trim();
|
||||||
|
// Remove quotes
|
||||||
|
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'")))
|
||||||
|
val = val.slice(1, -1);
|
||||||
|
fm[key] = val;
|
||||||
|
}
|
||||||
|
return fm;
|
||||||
|
}
|
||||||
|
|
||||||
|
function guessTag(folderName) {
|
||||||
|
if (folderName.startsWith('dgx-spark')) return 'DGX Spark';
|
||||||
|
if (folderName.startsWith('gitea')) return 'Gitea';
|
||||||
|
if (folderName.startsWith('searxng')) return 'SearXNG';
|
||||||
|
if (folderName.startsWith('ollama')) return 'Ollama';
|
||||||
|
if (folderName.startsWith('browser')) return 'Browser';
|
||||||
|
if (folderName.startsWith('game')) return 'Game';
|
||||||
|
return 'Tech';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTag(fm, folderName, defaultTag) {
|
||||||
|
if (fm.tag) return fm.tag;
|
||||||
|
if (fm.tags) return fm.tags.replace(/[\[\]]/g, '').split(',')[0].trim();
|
||||||
|
if (defaultTag === 'blog') return 'AI';
|
||||||
|
return guessTag(folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanDir(dir, urlPrefix, isBlog) {
|
||||||
|
if (!fs.existsSync(dir)) return [];
|
||||||
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||||
|
const posts = [];
|
||||||
|
for (const e of entries) {
|
||||||
|
if (!e.isDirectory()) continue;
|
||||||
|
const indexPath = path.join(dir, e.name, 'index.md');
|
||||||
|
if (!fs.existsSync(indexPath)) continue;
|
||||||
|
const content = fs.readFileSync(indexPath, 'utf8');
|
||||||
|
const fm = parseFrontmatter(content);
|
||||||
|
const title = fm.title || e.name;
|
||||||
|
const tag = getTag(fm, e.name, isBlog ? 'blog' : 'tech');
|
||||||
|
|
||||||
|
// Extract date
|
||||||
|
let date = null;
|
||||||
|
const dateMatch = e.name.match(/^(\d{4}-\d{2}-\d{2})/);
|
||||||
|
if (dateMatch) {
|
||||||
|
date = dateMatch[1];
|
||||||
|
} else if (fm.date) {
|
||||||
|
date = fm.date.slice(0, 10);
|
||||||
|
} else {
|
||||||
|
// Use file mtime
|
||||||
|
const stat = fs.statSync(indexPath);
|
||||||
|
date = stat.mtime.toISOString().slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
posts.push({ title, date, tag, url: `${urlPrefix}${e.name}` });
|
||||||
|
}
|
||||||
|
posts.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
|
return posts.slice(0, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
const tech = scanDir(path.join(ROOT, 'docs-tech'), '/tech/', false);
|
||||||
|
const blog = scanDir(path.join(ROOT, 'docs'), '/blog/', true);
|
||||||
|
|
||||||
|
const outPath = path.join(ROOT, 'src', 'data', 'recent-posts.json');
|
||||||
|
fs.writeFileSync(outPath, JSON.stringify({ tech, blog }, null, 2));
|
||||||
|
console.log(`Generated ${outPath} (tech: ${tech.length}, blog: ${blog.length})`);
|
||||||
66
src/data/recent-posts.json
Normal file
66
src/data/recent-posts.json
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"tech": [
|
||||||
|
{
|
||||||
|
"title": "ローカルサーバーでマイク・カメラを使う方法",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"tag": "Browser",
|
||||||
|
"url": "/tech/browser-secure-context"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "DGX SparkにAnythingLLMを導入してローカルLLMエージェントを構築",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"tag": "DGX Spark",
|
||||||
|
"url": "/tech/dgx-spark-anythingllm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "DGX SparkでClaude Code + Qwen3-Coder-Nextをローカル実行する",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"tag": "DGX Spark",
|
||||||
|
"url": "/tech/dgx-spark-claude-code-local"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "ローカルClaude Code + Playwright CLIでブラウザ自動化エージェントを作る",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"tag": "DGX Spark",
|
||||||
|
"url": "/tech/dgx-spark-claude-code-playwright"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "DGX Spark デュアル構成ガイド",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"tag": "DGX Spark",
|
||||||
|
"url": "/tech/dgx-spark-dual"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"blog": [
|
||||||
|
{
|
||||||
|
"title": "02/28 AIヘッドライン(朝刊)",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"tag": "AI",
|
||||||
|
"url": "/blog/2026-02-28-morning-headline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Ollama がローカルAIのハブとしてめちゃ最強な件",
|
||||||
|
"date": "2026-02-28",
|
||||||
|
"tag": "AI",
|
||||||
|
"url": "/blog/ollama-local-ai-hub"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "02/27 AIヘッドライン(夕刊)",
|
||||||
|
"date": "2026-02-27",
|
||||||
|
"tag": "AI",
|
||||||
|
"url": "/blog/2026-02-27-evening-headline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "02/27 AIヘッドライン(朝刊)",
|
||||||
|
"date": "2026-02-27",
|
||||||
|
"tag": "AI",
|
||||||
|
"url": "/blog/2026-02-27-morning-headline"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "02/26 AIヘッドライン(夕刊)",
|
||||||
|
"date": "2026-02-26",
|
||||||
|
"tag": "AI",
|
||||||
|
"url": "/blog/2026-02-26-evening-headline"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import React from 'react';
|
|||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import Link from '@docusaurus/Link';
|
import Link from '@docusaurus/Link';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
import recentPosts from '../data/recent-posts.json';
|
||||||
|
|
||||||
function HomepageHeader() {
|
function HomepageHeader() {
|
||||||
return (
|
return (
|
||||||
@ -25,39 +26,14 @@ function HomepageHeader() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RecentTech() {
|
function RecentTech() {
|
||||||
const posts = [
|
const posts = recentPosts.tech;
|
||||||
{
|
|
||||||
title: "ローカルGitea × Webhook連携でAI自動コードレビュー",
|
|
||||||
date: "2026-02-26",
|
|
||||||
tag: "Gitea",
|
|
||||||
url: "/tech/gitea-webhook-ai-review",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "DGX SparkにAnythingLLMでローカルLLMエージェント構築",
|
|
||||||
date: "2026-02-20",
|
|
||||||
tag: "DGX Spark",
|
|
||||||
url: "/tech/dgx-spark-anythingllm",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'DGX Spark デュアル構成ガイド',
|
|
||||||
date: '2026-02-19',
|
|
||||||
tag: 'DGX Spark',
|
|
||||||
url: '/tech/dgx-spark-dual',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'DGX SparkでMiniMax-M2.5-REAP-172Bを動かす',
|
|
||||||
date: '2026-02-18',
|
|
||||||
tag: 'DGX Spark',
|
|
||||||
url: '/tech/dgx-spark-minimax',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<h2 className={styles.sectionTitle}>// Tech</h2>
|
<h2 className={styles.sectionTitle}>// Tech</h2>
|
||||||
<p className={styles.sectionDesc}>技術記事・検証レポート</p>
|
<p className={styles.sectionDesc}>技術記事・検証レポート</p>
|
||||||
<div className={styles.posts}>
|
<div className={styles.posts}>
|
||||||
{posts.map((post, idx) => (
|
{posts.length > 0 ? posts.map((post, idx) => (
|
||||||
<Link key={idx} to={post.url} className={styles.postCard}>
|
<Link key={idx} to={post.url} className={styles.postCard}>
|
||||||
<h3 className={styles.postTitle}>{post.title}</h3>
|
<h3 className={styles.postTitle}>{post.title}</h3>
|
||||||
<div className={styles.postMeta}>
|
<div className={styles.postMeta}>
|
||||||
@ -65,7 +41,7 @@ function RecentTech() {
|
|||||||
<span className={styles.postTag}>{post.tag}</span>
|
<span className={styles.postTag}>{post.tag}</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
)) : <p>記事がまだありません。</p>}
|
||||||
</div>
|
</div>
|
||||||
<Link to="/tech" className={styles.moreLink}>
|
<Link to="/tech" className={styles.moreLink}>
|
||||||
すべて見る →
|
すべて見る →
|
||||||
@ -75,18 +51,22 @@ function RecentTech() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function RecentBlog() {
|
function RecentBlog() {
|
||||||
|
const posts = recentPosts.blog;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<h2 className={styles.sectionTitle}>// Blog</h2>
|
<h2 className={styles.sectionTitle}>// Blog</h2>
|
||||||
<p className={styles.sectionDesc}>日常の思考・情報収集</p>
|
<p className={styles.sectionDesc}>日常の思考・情報収集</p>
|
||||||
<div className={styles.posts}>
|
<div className={styles.posts}>
|
||||||
<Link to="/blog/2026-02-19-headline" className={styles.postCard}>
|
{posts.length > 0 ? posts.map((post, idx) => (
|
||||||
<h3 className={styles.postTitle}>02/19 AIヘッドライン</h3>
|
<Link key={idx} to={post.url} className={styles.postCard}>
|
||||||
<div className={styles.postMeta}>
|
<h3 className={styles.postTitle}>{post.title}</h3>
|
||||||
<span className={styles.postDate}>2026-02-19</span>
|
<div className={styles.postMeta}>
|
||||||
<span className={styles.postTag}>AI</span>
|
<span className={styles.postDate}>{post.date}</span>
|
||||||
</div>
|
<span className={styles.postTag}>{post.tag}</span>
|
||||||
</Link>
|
</div>
|
||||||
|
</Link>
|
||||||
|
)) : <p>記事がまだありません。</p>}
|
||||||
</div>
|
</div>
|
||||||
<Link to="/blog" className={styles.moreLink}>
|
<Link to="/blog" className={styles.moreLink}>
|
||||||
すべて見る →
|
すべて見る →
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user