diff --git a/docs-tech/browser-secure-context/banner.png b/docs-tech/browser-secure-context/banner.png new file mode 100644 index 0000000..f6dfe5c Binary files /dev/null and b/docs-tech/browser-secure-context/banner.png differ diff --git a/docs-tech/browser-secure-context/index.md b/docs-tech/browser-secure-context/index.md index 8cdbad8..67930f7 100644 --- a/docs-tech/browser-secure-context/index.md +++ b/docs-tech/browser-secure-context/index.md @@ -4,6 +4,7 @@ title: ローカルサーバーでマイク・カメラを使う方法 description: HTTP + IPアドレスでブラウザのマイク・カメラが動かない問題の解決策 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- # ローカルサーバーでマイク・カメラを使う方法 diff --git a/docs-tech/dgx-spark-anythingllm/banner.png b/docs-tech/dgx-spark-anythingllm/banner.png new file mode 100644 index 0000000..ef9cf84 Binary files /dev/null and b/docs-tech/dgx-spark-anythingllm/banner.png differ diff --git a/docs-tech/dgx-spark-anythingllm/index.md b/docs-tech/dgx-spark-anythingllm/index.md index 5369e2d..db667fd 100644 --- a/docs-tech/dgx-spark-anythingllm/index.md +++ b/docs-tech/dgx-spark-anythingllm/index.md @@ -4,6 +4,7 @@ title: DGX SparkにAnythingLLMを導入してローカルLLMエージェント description: Docker + Ollama + AnythingLLMで完全ローカルなLLMエージェント環境を構築する方法 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- # DGX Spark に AnythingLLM を導入してローカルLLMエージェントを構築する diff --git a/docs-tech/dgx-spark-claude-code-local/banner.png b/docs-tech/dgx-spark-claude-code-local/banner.png new file mode 100644 index 0000000..6f496df Binary files /dev/null and b/docs-tech/dgx-spark-claude-code-local/banner.png differ diff --git a/docs-tech/dgx-spark-claude-code-local/index.md b/docs-tech/dgx-spark-claude-code-local/index.md index c95cef0..56839c1 100644 --- a/docs-tech/dgx-spark-claude-code-local/index.md +++ b/docs-tech/dgx-spark-claude-code-local/index.md @@ -4,6 +4,7 @@ title: DGX SparkでClaude Code + Qwen3-Coder-Nextをローカル実行する description: Claude Codeを完全ローカル化!Qwen3-Coder-Next(80B MoE)で動かす方法を解説 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- # DGX SparkでClaude Codeを完全ローカル化!Qwen3-Coder-Nextで動かしてみた diff --git a/docs-tech/dgx-spark-claude-code-playwright/banner.png b/docs-tech/dgx-spark-claude-code-playwright/banner.png new file mode 100644 index 0000000..691760e Binary files /dev/null and b/docs-tech/dgx-spark-claude-code-playwright/banner.png differ diff --git a/docs-tech/dgx-spark-claude-code-playwright/index.md b/docs-tech/dgx-spark-claude-code-playwright/index.md index 4f8df5a..f0ba240 100644 --- a/docs-tech/dgx-spark-claude-code-playwright/index.md +++ b/docs-tech/dgx-spark-claude-code-playwright/index.md @@ -4,6 +4,7 @@ title: ローカルClaude Code + Playwright CLIでブラウザ自動化エージ description: Claude Code + Playwright CLIで安定したブラウザ自動化環境を構築する方法 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- # ローカルClaude Code + Playwright CLIで「自分だけのブラウザ自動化エージェント」を作る diff --git a/docs-tech/dgx-spark-dual/banner.png b/docs-tech/dgx-spark-dual/banner.png new file mode 100644 index 0000000..3922d5c Binary files /dev/null and b/docs-tech/dgx-spark-dual/banner.png differ diff --git a/docs-tech/dgx-spark-dual/index.md b/docs-tech/dgx-spark-dual/index.md index 90fac59..45bcd7f 100644 --- a/docs-tech/dgx-spark-dual/index.md +++ b/docs-tech/dgx-spark-dual/index.md @@ -4,6 +4,7 @@ displayed_sidebar: null sidebar_position: 3 title: DGX Spark デュアル構成ガイド description: 2台のDGX Sparkを接続して256GBメモリ環境を構築する方法を初心者向けに解説 +image: ./banner.png --- # DGX Spark デュアル構成ガイド diff --git a/docs-tech/dgx-spark-minimax/banner.png b/docs-tech/dgx-spark-minimax/banner.png new file mode 100644 index 0000000..c060def Binary files /dev/null and b/docs-tech/dgx-spark-minimax/banner.png differ diff --git a/docs-tech/dgx-spark-minimax/index.md b/docs-tech/dgx-spark-minimax/index.md index 783d42f..296496f 100644 --- a/docs-tech/dgx-spark-minimax/index.md +++ b/docs-tech/dgx-spark-minimax/index.md @@ -4,6 +4,7 @@ title: DGX SparkでMiniMax-M2.5-REAP-172Bを動かす description: DGX Sparkデュアル構成で172Bパラメータの最新LLMを動かす手順 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- ## MiniMax-M2.5-REAP-172Bとは? diff --git a/docs-tech/dgx-spark-qwen3-coder-next/banner.png b/docs-tech/dgx-spark-qwen3-coder-next/banner.png new file mode 100644 index 0000000..d279ae8 Binary files /dev/null and b/docs-tech/dgx-spark-qwen3-coder-next/banner.png differ diff --git a/docs-tech/dgx-spark-qwen3-coder-next/index.md b/docs-tech/dgx-spark-qwen3-coder-next/index.md index c16df90..414e213 100644 --- a/docs-tech/dgx-spark-qwen3-coder-next/index.md +++ b/docs-tech/dgx-spark-qwen3-coder-next/index.md @@ -4,6 +4,7 @@ title: DGX SparkでQwen3-Coder-Next(80B MoE)を動かす description: NVIDIA DGX Sparkの128GB統合メモリでQwen3-Coder-Next(80B MoE)をFP8量子化で動かす方法を解説 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- # DGX SparkでQwen3-Coder-Nextを動かす diff --git a/docs-tech/dgx-spark-vibevoice-asr/banner.png b/docs-tech/dgx-spark-vibevoice-asr/banner.png new file mode 100644 index 0000000..4315893 Binary files /dev/null and b/docs-tech/dgx-spark-vibevoice-asr/banner.png differ diff --git a/docs-tech/dgx-spark-vibevoice-asr/index.md b/docs-tech/dgx-spark-vibevoice-asr/index.md index 72d0866..c14db97 100644 --- a/docs-tech/dgx-spark-vibevoice-asr/index.md +++ b/docs-tech/dgx-spark-vibevoice-asr/index.md @@ -4,6 +4,7 @@ title: DGX SparkでVibeVoice ASRを動かす — リアルタイム日本語音 description: Microsoft VibeVoice ASRをDGX Spark環境でDockerベースで動かし、バッチ処理とリアルタイム音声認識を実現する方法 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- # DGX Spark で VibeVoice ASR を動かす — リアルタイム日本語音声認識 diff --git a/docs-tech/dgx-spark-video-analyzer/banner.png b/docs-tech/dgx-spark-video-analyzer/banner.png new file mode 100644 index 0000000..f0caa53 Binary files /dev/null and b/docs-tech/dgx-spark-video-analyzer/banner.png differ diff --git a/docs-tech/dgx-spark-video-analyzer/index.md b/docs-tech/dgx-spark-video-analyzer/index.md index 9b8d896..f743dcb 100644 --- a/docs-tech/dgx-spark-video-analyzer/index.md +++ b/docs-tech/dgx-spark-video-analyzer/index.md @@ -4,6 +4,7 @@ title: DGX SparkでGUI操作動画解析システムを作る description: NVIDIA DGX SparkでQwen3-VLとLanceDBを使ったGUI操作動画解析システムの構築方法 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- # DGX Sparkで作る!GUI操作動画解析システム diff --git a/docs-tech/gitea-webhook-ai-review/banner.png b/docs-tech/gitea-webhook-ai-review/banner.png new file mode 100644 index 0000000..bd2859e Binary files /dev/null and b/docs-tech/gitea-webhook-ai-review/banner.png differ diff --git a/docs-tech/gitea-webhook-ai-review/index.md b/docs-tech/gitea-webhook-ai-review/index.md index d3b1cdc..b276fd5 100644 --- a/docs-tech/gitea-webhook-ai-review/index.md +++ b/docs-tech/gitea-webhook-ai-review/index.md @@ -4,6 +4,7 @@ title: ローカルGitea × Webhook連携で、AIとの開発をもっと楽に description: GiteaのIssue/PR WebhookとOpenClawを連携し、実運用で壊れにくく回す開発プロセスを構築する実践記録 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- # ローカルGitea × Webhook連携で、AIとの開発をもっと楽にしよう! diff --git a/docs-tech/searxng-local-search/banner.png b/docs-tech/searxng-local-search/banner.png new file mode 100644 index 0000000..79356d8 Binary files /dev/null and b/docs-tech/searxng-local-search/banner.png differ diff --git a/docs-tech/searxng-local-search/index.md b/docs-tech/searxng-local-search/index.md index fadc8d0..11f21f9 100644 --- a/docs-tech/searxng-local-search/index.md +++ b/docs-tech/searxng-local-search/index.md @@ -4,6 +4,7 @@ title: SearXNGでローカル検索APIを構築する description: プライバシー重視のメタ検索エンジンSearXNGをセルフホストして、無料で検索APIを手に入れる方法 hide_table_of_contents: false displayed_sidebar: null +image: ./banner.png --- # SearXNGでローカル検索APIを構築する diff --git a/docs/2026-02-19-headline/banner.png b/docs/2026-02-19-headline/banner.png new file mode 100644 index 0000000..8e9ce92 Binary files /dev/null and b/docs/2026-02-19-headline/banner.png differ diff --git a/docs/2026-02-19-headline/index.md b/docs/2026-02-19-headline/index.md index bd56ea0..a238995 100644 --- a/docs/2026-02-19-headline/index.md +++ b/docs/2026-02-19-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/20 AIヘッドライン(朝刊) description: 2026年2月20日のAI関連ニュースまとめ +image: ./banner.png --- # 02/20 AIヘッドライン(朝刊) diff --git a/docs/2026-02-20-evening-headline/banner.png b/docs/2026-02-20-evening-headline/banner.png new file mode 100644 index 0000000..0fad7d8 Binary files /dev/null and b/docs/2026-02-20-evening-headline/banner.png differ diff --git a/docs/2026-02-20-evening-headline/index.md b/docs/2026-02-20-evening-headline/index.md index 2f5c3f6..b81178a 100644 --- a/docs/2026-02-20-evening-headline/index.md +++ b/docs/2026-02-20-evening-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/20 AIヘッドライン(夕刊) description: 2026年2月20日のAI関連ニュースまとめ - GPT-OSS Swallow、Rork Max AI、LavaSRなど +image: ./banner.png --- # 02/20 AIヘッドライン(夕刊) diff --git a/docs/2026-02-21-evening-headline/banner.png b/docs/2026-02-21-evening-headline/banner.png new file mode 100644 index 0000000..ea6182b Binary files /dev/null and b/docs/2026-02-21-evening-headline/banner.png differ diff --git a/docs/2026-02-21-evening-headline/index.md b/docs/2026-02-21-evening-headline/index.md index 29bd6b2..753b8f0 100644 --- a/docs/2026-02-21-evening-headline/index.md +++ b/docs/2026-02-21-evening-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/21 AIヘッドライン(夕刊) description: 2026年2月21日夕方のAI関連ニュースまとめ - Claude Code Security、Taalas HC1チップ、cmux、tornado +image: ./banner.png --- # 02/21 AIヘッドライン(夕刊) diff --git a/docs/2026-02-21-morning-headline/banner.png b/docs/2026-02-21-morning-headline/banner.png new file mode 100644 index 0000000..479dc51 Binary files /dev/null and b/docs/2026-02-21-morning-headline/banner.png differ diff --git a/docs/2026-02-21-morning-headline/index.md b/docs/2026-02-21-morning-headline/index.md index 4917b95..07e1747 100644 --- a/docs/2026-02-21-morning-headline/index.md +++ b/docs/2026-02-21-morning-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/21 AIヘッドライン(朝刊) description: 2026年2月21日のAI関連ニュースまとめ - llmfit、UltraRAG 3.0、Zvec、AudioX-MAF-MMDiT、YCエージェント経済 +image: ./banner.png --- # 02/21 AIヘッドライン(朝刊) diff --git a/docs/2026-02-22-evening-headline/banner.png b/docs/2026-02-22-evening-headline/banner.png new file mode 100644 index 0000000..ef41d65 Binary files /dev/null and b/docs/2026-02-22-evening-headline/banner.png differ diff --git a/docs/2026-02-22-evening-headline/index.md b/docs/2026-02-22-evening-headline/index.md index 010137d..00aa97f 100644 --- a/docs/2026-02-22-evening-headline/index.md +++ b/docs/2026-02-22-evening-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/22 AIヘッドライン(夕刊) description: 2026年2月22日のAI関連ニュースまとめ +image: ./banner.png --- # 02/22 AIヘッドライン(夕刊) diff --git a/docs/2026-02-23-evening-headline/banner.png b/docs/2026-02-23-evening-headline/banner.png new file mode 100644 index 0000000..7cef97e Binary files /dev/null and b/docs/2026-02-23-evening-headline/banner.png differ diff --git a/docs/2026-02-23-evening-headline/index.md b/docs/2026-02-23-evening-headline/index.md index 44c69cd..9e4a31b 100644 --- a/docs/2026-02-23-evening-headline/index.md +++ b/docs/2026-02-23-evening-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/23 AIヘッドライン(夕刊) description: 2026年2月23日のAI関連ニュースまとめ +image: ./banner.png --- # 02/23 AIヘッドライン(夕刊) diff --git a/docs/2026-02-23-morning-headline/banner.png b/docs/2026-02-23-morning-headline/banner.png new file mode 100644 index 0000000..6d3847d Binary files /dev/null and b/docs/2026-02-23-morning-headline/banner.png differ diff --git a/docs/2026-02-23-morning-headline/index.md b/docs/2026-02-23-morning-headline/index.md index 769e9ab..18406b1 100644 --- a/docs/2026-02-23-morning-headline/index.md +++ b/docs/2026-02-23-morning-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/23 AIヘッドライン(朝刊) description: 2026年2月23日のAI関連ニュースまとめ +image: ./banner.png --- # 02/23 AIヘッドライン(朝刊) diff --git a/docs/2026-02-24-evening-headline/banner.png b/docs/2026-02-24-evening-headline/banner.png new file mode 100644 index 0000000..d1bf003 Binary files /dev/null and b/docs/2026-02-24-evening-headline/banner.png differ diff --git a/docs/2026-02-24-evening-headline/index.md b/docs/2026-02-24-evening-headline/index.md index ab52140..2d604e8 100644 --- a/docs/2026-02-24-evening-headline/index.md +++ b/docs/2026-02-24-evening-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/24 AIヘッドライン(夕刊) description: 2026年2月24日夕刻のAI関連ニュースまとめ +image: ./banner.png --- # 02/24 AIヘッドライン(夕刊) diff --git a/docs/2026-02-24-morning-headline/banner.png b/docs/2026-02-24-morning-headline/banner.png new file mode 100644 index 0000000..355123b Binary files /dev/null and b/docs/2026-02-24-morning-headline/banner.png differ diff --git a/docs/2026-02-24-morning-headline/index.md b/docs/2026-02-24-morning-headline/index.md index 0460e1c..bfc3835 100644 --- a/docs/2026-02-24-morning-headline/index.md +++ b/docs/2026-02-24-morning-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/24 AIヘッドライン(朝刊) description: 2026年2月24日のAI関連ニュースまとめ +image: ./banner.png --- # 02/24 AIヘッドライン(朝刊) diff --git a/docs/2026-02-25-evening-headline/banner.png b/docs/2026-02-25-evening-headline/banner.png new file mode 100644 index 0000000..61c7a24 Binary files /dev/null and b/docs/2026-02-25-evening-headline/banner.png differ diff --git a/docs/2026-02-25-evening-headline/index.md b/docs/2026-02-25-evening-headline/index.md index 12083fc..239be4e 100644 --- a/docs/2026-02-25-evening-headline/index.md +++ b/docs/2026-02-25-evening-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/25 AIヘッドライン(夕刊) description: 2026年2月25日のAI関連ニュースまとめ +image: ./banner.png --- # 02/25 AIヘッドライン(夕刊) @@ -112,4 +113,4 @@ AI時代のクリエイティブ価値の根本的な変化を示唆。アート --- -*情報は2026年2月25日時点のものです。* \ No newline at end of file +*情報は2026年2月25日時点のものです。* diff --git a/docs/2026-02-25-morning-headline/banner.png b/docs/2026-02-25-morning-headline/banner.png new file mode 100644 index 0000000..622f81d Binary files /dev/null and b/docs/2026-02-25-morning-headline/banner.png differ diff --git a/docs/2026-02-25-morning-headline/index.md b/docs/2026-02-25-morning-headline/index.md index 48fbfe9..ab09f8d 100644 --- a/docs/2026-02-25-morning-headline/index.md +++ b/docs/2026-02-25-morning-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/25 AIヘッドライン(朝刊) description: 2026年2月25日のAI関連ニュースまとめ +image: ./banner.png --- # 02/25 AIヘッドライン(朝刊) diff --git a/docs/2026-02-26-evening-headline/banner.png b/docs/2026-02-26-evening-headline/banner.png new file mode 100644 index 0000000..0003bb5 Binary files /dev/null and b/docs/2026-02-26-evening-headline/banner.png differ diff --git a/docs/2026-02-26-evening-headline/index.md b/docs/2026-02-26-evening-headline/index.md index 8bc3b83..6c25b89 100644 --- a/docs/2026-02-26-evening-headline/index.md +++ b/docs/2026-02-26-evening-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/26 AIヘッドライン(夕刊) description: 2026年2月26日のAI関連ニュースまとめ +image: ./banner.png --- # 02/26 AIヘッドライン(夕刊) @@ -118,4 +119,4 @@ AIX(AI X)プロジェクトの詳細: --- -*情報は2026年02月26日時点のものです。* \ No newline at end of file +*情報は2026年02月26日時点のものです。* diff --git a/docs/2026-02-26-morning-headline/banner.png b/docs/2026-02-26-morning-headline/banner.png new file mode 100644 index 0000000..1b40a52 Binary files /dev/null and b/docs/2026-02-26-morning-headline/banner.png differ diff --git a/docs/2026-02-26-morning-headline/index.md b/docs/2026-02-26-morning-headline/index.md index 941eef9..8e1d113 100644 --- a/docs/2026-02-26-morning-headline/index.md +++ b/docs/2026-02-26-morning-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/26 AIヘッドライン(朝刊) description: 2026年2月26日のAI関連ニュースまとめ +image: ./banner.png --- # 02/26 AIヘッドライン(朝刊) diff --git a/docs/2026-02-27-evening-headline/banner.png b/docs/2026-02-27-evening-headline/banner.png new file mode 100644 index 0000000..36f627c Binary files /dev/null and b/docs/2026-02-27-evening-headline/banner.png differ diff --git a/docs/2026-02-27-evening-headline/index.md b/docs/2026-02-27-evening-headline/index.md index 0559697..1171500 100644 --- a/docs/2026-02-27-evening-headline/index.md +++ b/docs/2026-02-27-evening-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/27 AIヘッドライン(夕刊) description: 2026年2月27日のAI関連ニュースまとめ +image: ./banner.png --- # 02/27 AIヘッドライン(夕刊) @@ -88,4 +89,4 @@ GGUF形式での効率的な推論が可能で、関数呼び出しとツール --- -*情報は2026年02月27日時点のものです。* \ No newline at end of file +*情報は2026年02月27日時点のものです。* diff --git a/docs/2026-02-27-morning-headline/banner.png b/docs/2026-02-27-morning-headline/banner.png new file mode 100644 index 0000000..3fcae34 Binary files /dev/null and b/docs/2026-02-27-morning-headline/banner.png differ diff --git a/docs/2026-02-27-morning-headline/index.md b/docs/2026-02-27-morning-headline/index.md index 3cd7245..73d5684 100644 --- a/docs/2026-02-27-morning-headline/index.md +++ b/docs/2026-02-27-morning-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/27 AIヘッドライン(朝刊) description: 2026年2月27日のAI関連ニュースまとめ +image: ./banner.png --- # 02/27 AIヘッドライン(朝刊) diff --git a/docs/2026-02-28-morning-headline/banner.png b/docs/2026-02-28-morning-headline/banner.png new file mode 100644 index 0000000..ba04d9a Binary files /dev/null and b/docs/2026-02-28-morning-headline/banner.png differ diff --git a/docs/2026-02-28-morning-headline/index.md b/docs/2026-02-28-morning-headline/index.md index 2483638..181a65f 100644 --- a/docs/2026-02-28-morning-headline/index.md +++ b/docs/2026-02-28-morning-headline/index.md @@ -2,6 +2,7 @@ sidebar_position: 100 title: 02/28 AIヘッドライン(朝刊) description: 2026年2月28日のAI関連ニュースまとめ +image: ./banner.png --- # 02/28 AIヘッドライン(朝刊) diff --git a/docs/ollama-local-ai-hub/banner.png b/docs/ollama-local-ai-hub/banner.png new file mode 100644 index 0000000..4a6a94e Binary files /dev/null and b/docs/ollama-local-ai-hub/banner.png differ diff --git a/docs/ollama-local-ai-hub/index.md b/docs/ollama-local-ai-hub/index.md index 5375367..2bca51a 100644 --- a/docs/ollama-local-ai-hub/index.md +++ b/docs/ollama-local-ai-hub/index.md @@ -1,6 +1,7 @@ --- sidebar_position: 2 title: Ollama がローカルAIのハブとしてめちゃ最強な件 +image: ./banner.png --- # Ollama がローカルAIのハブとしてめちゃ最強な件 diff --git a/package-lock.json b/package-lock.json index a28575b..6b986a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,9 @@ "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", "@mdx-js/react": "^3.0.0", + "canvas": "^3.2.1", "clsx": "^2.0.0", + "gray-matter": "^4.0.3", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", "react-dom": "^19.0.0" @@ -6210,6 +6212,26 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", @@ -6246,6 +6268,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -6387,6 +6420,30 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -6563,6 +6620,20 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.1.tgz", + "integrity": "sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -6700,6 +6771,12 @@ "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -7841,6 +7918,15 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -8077,6 +8163,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.19.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", @@ -8457,6 +8552,15 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -8900,6 +9004,12 @@ "node": ">= 0.6" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", @@ -9001,6 +9111,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/github-slugger": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", @@ -9770,6 +9886,26 @@ "postcss": "^8.1.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -12964,6 +13100,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -13010,6 +13152,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -13035,6 +13183,24 @@ "tslib": "^2.0.3" } }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -13253,6 +13419,15 @@ "node": ">= 0.8" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/onetime": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", @@ -15108,6 +15283,33 @@ "postcss": "^8.4.31" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/pretty-error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", @@ -15217,6 +15419,16 @@ "node": ">= 0.10" } }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -16596,6 +16808,51 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sirv": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", @@ -17036,6 +17293,34 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/terser": { "version": "5.46.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", @@ -17256,6 +17541,18 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-fest": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", @@ -18336,6 +18633,12 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/write-file-atomic": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", diff --git a/package.json b/package.json index ee65a7e..ad7dcc1 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "@docusaurus/core": "3.9.2", "@docusaurus/preset-classic": "3.9.2", "@mdx-js/react": "^3.0.0", + "canvas": "^3.2.1", "clsx": "^2.0.0", + "gray-matter": "^4.0.3", "prism-react-renderer": "^2.3.0", "react": "^19.0.0", "react-dom": "^19.0.0" diff --git a/scripts/generate-banner.js b/scripts/generate-banner.js new file mode 100644 index 0000000..54955b6 --- /dev/null +++ b/scripts/generate-banner.js @@ -0,0 +1,218 @@ +#!/usr/bin/env node +// Banner image generator for docs.techswan.online +// Usage: +// node scripts/generate-banner.js # single article +// node scripts/generate-banner.js --all # all missing +// node scripts/generate-banner.js --all --force # regenerate all + +const { createCanvas, registerFont } = require('canvas'); +const matter = require('gray-matter'); +const fs = require('fs'); +const path = require('path'); + +// --- Config --- +const W = 1200, H = 630; +const MAX_TITLE_WIDTH = 1000; +const FONT_SIZES = [52, 44, 38]; +const MAX_LINES = 3; + +const FONT_PATH = '/usr/share/fonts/opentype/noto/NotoSansCJK-Bold.ttc'; +if (fs.existsSync(FONT_PATH)) { + registerFont(FONT_PATH, { family: 'Noto Sans CJK JP', weight: 'bold' }); +} +const FONT_REGULAR = '/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc'; +if (fs.existsSync(FONT_REGULAR)) { + registerFont(FONT_REGULAR, { family: 'Noto Sans CJK JP', weight: 'normal' }); +} + +const CATEGORIES = { + 'docs': { label: 'Blog', color: '#0ea5e9' }, + 'docs-tech': { label: 'Tech', color: '#10b981' }, +}; + +const ROOT = path.resolve(__dirname, '..'); + +// --- Text wrapping --- +function tokenize(text) { + // Split into tokens: consecutive ASCII word chars as one token, each CJK char as one token + const tokens = []; + const re = /([A-Za-z0-9._\-]+)|(\s+)|([\s\S])/g; + let m; + while ((m = re.exec(text)) !== null) { + if (m[1]) tokens.push({ text: m[1], breakable: false }); + else if (m[2]) tokens.push({ text: ' ', breakable: true }); + else if (m[3]) tokens.push({ text: m[3], breakable: true }); + } + return tokens; +} + +function wrapText(ctx, text, maxWidth, fontSize, fontFamily) { + ctx.font = `bold ${fontSize}px "${fontFamily}", sans-serif`; + const tokens = tokenize(text); + const lines = []; + let currentLine = ''; + + for (const token of tokens) { + if (token.text === ' ' && currentLine === '') continue; // skip leading space + const testLine = currentLine + token.text; + const metrics = ctx.measureText(testLine); + if (metrics.width > maxWidth && currentLine !== '') { + lines.push(currentLine); + currentLine = token.breakable && token.text === ' ' ? '' : token.text; + } else { + currentLine = testLine; + } + } + if (currentLine) lines.push(currentLine); + return lines; +} + +// --- Drawing --- +function drawBanner(title, category) { + const canvas = createCanvas(W, H); + const ctx = canvas.getContext('2d'); + const cat = CATEGORIES[category] || CATEGORIES['docs']; + + // Background gradient + const grad = ctx.createLinearGradient(0, 0, W, H); + grad.addColorStop(0, '#1a1a2e'); + grad.addColorStop(1, '#16213e'); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, W, H); + + // Decorative circles + ctx.fillStyle = 'rgba(233,69,96,0.15)'; + ctx.beginPath(); ctx.arc(1100, 100, 150, 0, Math.PI * 2); ctx.fill(); + ctx.fillStyle = 'rgba(14,165,233,0.1)'; + ctx.beginPath(); ctx.arc(1050, 500, 200, 0, Math.PI * 2); ctx.fill(); + + // Accent line + ctx.fillStyle = '#e94560'; + ctx.fillRect(60, 80, 6, 120); + + // Title - try font sizes until it fits + ctx.fillStyle = '#ffffff'; + ctx.textBaseline = 'top'; + let lines, fontSize; + for (fontSize of FONT_SIZES) { + lines = wrapText(ctx, title, MAX_TITLE_WIDTH, fontSize, 'Noto Sans CJK JP'); + if (lines.length <= MAX_LINES) break; + } + // If still too many lines, truncate + if (lines.length > MAX_LINES) { + lines = lines.slice(0, MAX_LINES); + lines[MAX_LINES - 1] = lines[MAX_LINES - 1].replace(/.$/, '…'); + } + + const lineHeight = Math.round(fontSize * 1.35); + const titleStartY = 90; + ctx.font = `bold ${fontSize}px "Noto Sans CJK JP", sans-serif`; + lines.forEach((line, i) => { + ctx.fillText(line, 90, titleStartY + i * lineHeight); + }); + + // Category badge + const badgeY = Math.max(titleStartY + lines.length * lineHeight + 40, 350); + ctx.fillStyle = cat.color; + ctx.beginPath(); + ctx.roundRect(60, badgeY, 100, 36, 8); + ctx.fill(); + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 18px "Noto Sans CJK JP", sans-serif'; + ctx.textBaseline = 'middle'; + ctx.fillText(cat.label, 60 + 50 - ctx.measureText(cat.label).width / 2, badgeY + 18); + + // Footer + ctx.textBaseline = 'top'; + ctx.fillStyle = 'rgba(255,255,255,0.6)'; + ctx.font = '20px "Noto Sans CJK JP", sans-serif'; + ctx.fillText('雑記 by swallow', 60, 550); + ctx.fillStyle = 'rgba(255,255,255,0.3)'; + const siteText = 'docs.techswan.online'; + ctx.fillText(siteText, W - 60 - ctx.measureText(siteText).width, 550); + + return canvas.toBuffer('image/png'); +} + +// --- Article discovery --- +function findArticle(slug) { + for (const dir of ['docs-tech', 'docs']) { + const mdPath = path.join(ROOT, dir, slug, 'index.md'); + if (fs.existsSync(mdPath)) return { mdPath, category: dir }; + } + return null; +} + +function getAllArticles() { + const articles = []; + for (const dir of ['docs-tech', 'docs']) { + const dirPath = path.join(ROOT, dir); + if (!fs.existsSync(dirPath)) continue; + for (const slug of fs.readdirSync(dirPath)) { + const mdPath = path.join(dirPath, slug, 'index.md'); + if (fs.existsSync(mdPath)) { + articles.push({ slug, mdPath, category: dir }); + } + } + } + return articles; +} + +// --- Frontmatter update --- +function ensureFrontmatterImage(mdPath) { + const raw = fs.readFileSync(mdPath, 'utf-8'); + const parsed = matter(raw); + if (parsed.data.image) return false; // already set + parsed.data.image = './banner.png'; + const updated = matter.stringify(parsed.content, parsed.data); + fs.writeFileSync(mdPath, updated); + return true; +} + +// --- Main --- +function generateOne(slug, force = false) { + const article = findArticle(slug); + if (!article) { + console.error(`❌ Article not found: ${slug}`); + return false; + } + const { mdPath, category } = article; + const bannerPath = path.join(path.dirname(mdPath), 'banner.png'); + + if (fs.existsSync(bannerPath) && !force) { + console.log(`⏭️ Skip (exists): ${slug}`); + return true; + } + + const parsed = matter(fs.readFileSync(mdPath, 'utf-8')); + const title = parsed.data.title || slug; + + const buf = drawBanner(title, category); + fs.writeFileSync(bannerPath, buf); + + const updated = ensureFrontmatterImage(mdPath); + console.log(`✅ ${slug} → banner.png (${(buf.length / 1024).toFixed(0)}KB)${updated ? ' + frontmatter updated' : ''}`); + return true; +} + +// --- CLI --- +const args = process.argv.slice(2); +const force = args.includes('--force'); +const all = args.includes('--all'); + +if (all) { + const articles = getAllArticles(); + console.log(`Found ${articles.length} articles`); + let generated = 0; + for (const { slug } of articles) { + if (generateOne(slug, force)) generated++; + } + console.log(`\nDone: ${generated}/${articles.length}`); +} else { + const slug = args.find(a => !a.startsWith('--')); + if (!slug) { + console.log('Usage: node scripts/generate-banner.js | --all [--force]'); + process.exit(1); + } + generateOne(slug, force); +}