hugoの予約投稿をtauriで作る2

こんにちは、ナナオです。 前回の続きです。 hugoを予約投稿するための超ミニマムなCMSを作った hugoの予約投稿アプリをtauriで作る windows側で起動できるようにする 前回は画面表示はできたけど表示がおかしい感じになっていました。 おそらくWSLで起動したことが原因なので、修正します。 windows側のディレクトリに移動して、powershellで再度tauri-cliをインストールします。 cargo install tauri-cli 再度起動してみます。 cargo tauri dev 今度はちゃんと表示されました。 hugoディレクトリの監視とデプロイを行うようにする 今の状態だと予約投稿する記事の管理はGUIでできるようになりましたが、予約投稿自体ができていません。 ということで少し実装を修正します。 lib.rsを以下のように修正します。 use chrono::{Local, NaiveDateTime}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use std::thread; use std::time::Duration; #[derive(Debug, Serialize, Deserialize, Clone)] struct ScheduledPost { date: String, file: String, } #[derive(Debug, Serialize, Deserialize, Clone)] struct Config { workdir: String, scheduled: Vec<ScheduledPost>, } const CONFIG_PATH: &str = "../config.yaml"; // パスは適宜調整 #[tauri::command] fn get_config() -> Result<Config, String> { let content = fs::read_to_string(CONFIG_PATH).map_err(|e| e.to_string())?; let config: Config = serde_yaml::from_str(&content).map_err(|e| e.to_string())?; Ok(config) } #[tauri::command] fn save_config(config: Config) -> Result<(), String> { let yaml = serde_yaml::to_string(&config).map_err(|e| e.to_string())?; fs::write(CONFIG_PATH, yaml).map_err(|e| e.to_string())?; Ok(()) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .setup(|app| { if cfg!(debug_assertions) { app.handle().plugin( tauri_plugin_log::Builder::default() .level(log::LevelFilter::Info) .build(), )?; } // スレッドを立てて監視を開始 thread::spawn(|| { start_monitoring(); }); Ok(()) }) .invoke_handler(tauri::generate_handler![get_config, save_config]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } fn start_monitoring() { println!("Hugo Local CMS: 予約投稿監視を開始しました..."); // 処理済みファイルを記録するSet (日付+ファイル名 をキーにする) let mut processed: HashSet<String> = HashSet::new(); loop { println!("{} ファイルチェック開始", Local::now().to_rfc3339()); // 設定ファイルの読み込み let config = match load_config(CONFIG_PATH) { Ok(cfg) => cfg, Err(e) => { eprintln!("設定読み込みエラー: {}", e); thread::sleep(Duration::from_secs(10)); continue; } }; let now = chrono::Local::now().naive_local(); for post in config.scheduled { let key = format!("{}{}", post.date, post.file); // 既に処理済みならスキップ if processed.contains(&key) { continue; } // 日付パース (フォーマット: 2026/01/20 18:45:00) let pub_date = match NaiveDateTime::parse_from_str(&post.date, "%Y/%m/%d %H:%M:%S") { Ok(dt) => dt, Err(e) => { eprintln!("日付パースエラー ({}): {}", post.file, e); continue; } }; // 時間チェック if now > pub_date { println!("⏰ 予約時刻になりました: {}", post.file); // publish処理を実行 publish(&config.workdir, &post.file, &post.date); // 処理済みリストに追加 processed.insert(key); } } println!("{} ファイルチェック終了", Local::now().to_rfc3339()); // 30秒待機 thread::sleep(Duration::from_secs(30)); } } fn load_config(path: &str) -> Result<Config, Box<dyn std::error::Error>> { let content = fs::read_to_string(path)?; let config: Config = serde_yaml::from_str(&content)?; Ok(config) } fn publish(work_dir: &str, relative_path: &str, target_date_str: &str) { let full_path = Path::new(work_dir).join(relative_path); // 1. ファイルのドラフト解除 if let Err(e) = modify_draft_status(&full_path, target_date_str) { eprintln!("❌ ファイル修正失敗 [{:?}]: {}", full_path, e); return; } // 2. Hugoコマンド実行 execute_hugo(work_dir); } fn modify_draft_status(path: &PathBuf, target_date_str: &str) -> std::io::Result<()> { // ファイル読み込み let content = fs::read_to_string(path)?; // 書き換えるコンテンツ let mut new_content = content; // 正規表現: 行頭の "draft = true" (スペース許容) を検索 // (?m) はマルチラインモード有効化 let re = Regex::new(r"(?m)^(draft\s*:\s*)true").unwrap(); if re.is_match(&new_content) { // 置換実行 ( ${1} は "draft = " の部分 ) new_content = re.replace(&new_content, "${1}false").to_string(); println!( "📝 Draftフラグを解除しました: {:?}", path.file_name().unwrap() ); } // dateの書き換え // --- 日付のフォーマット変換 --- // config.yaml の "2026/01/20 18:45:00" -> "2026-01-20T18:45:00+09:00" let dt = NaiveDateTime::parse_from_str(target_date_str, "%Y/%m/%d %H:%M:%S").unwrap(); let formatted_date = format!("{}T{}+09:00", dt.date(), dt.time()); let re = Regex::new(r"(?m)^(\s*date\s*:\s*).*").unwrap(); if re.is_match(&new_content) { let replacement = format!("${{1}}{}", formatted_date); new_content = re.replace(&new_content, replacement.as_str()).to_string(); println!("📅 日付を更新しました: {}", formatted_date); } // 書き込み fs::write(path, new_content.as_bytes())?; println!("📝 Frontmatterの更新完了: {:?}", path.file_name().unwrap()); Ok(()) } fn execute_hugo(work_dir: &str) { println!("🏗️ Hugoビルドを開始します..."); // 1. hugo if run_command(work_dir, "hugo", &[]).is_err() { return; } // 2. hugo deploy if run_command(work_dir, "hugo", &["deploy"]).is_err() { return; } println!("✅ Hugoのビルドとデプロイが完了しました。"); } fn run_command(dir: &str, command: &str, args: &[&str]) -> Result<(), ()> { println!("🚀 実行中: {} {:?} (in {})", command, args, dir); let status = Command::new(command) .args(args) .current_dir(dir) // 作業ディレクトリ指定 .status(); // 実行して終了ステータスを待つ match status { Ok(s) if s.success() => Ok(()), Ok(s) => { eprintln!("エラー: コマンドが失敗しました (Exit Code: {:?})", s.code()); Err(()) } Err(e) => { eprintln!("エラー: コマンドを実行できませんでした: {}", e); Err(()) } } } これで既存の機能を移し替えることができました。 ...

2026年2月11日 · にあえん

hugoの予約投稿アプリをtauriで作る

こんにちは、ナナオです。 今回は、前回作ったhugoの予約投稿ツールをtauriでUI作って見やすくしようよ、という記事になります。 Githubはこちら↓ GitHub - satodaiki/hugo-local-cms: hugoの予約投稿管理 tauriのインストールと起動まで まずはプロジェクトにtauriを入れます。 やり方はこちらを参考にします。 プロジェクトの作成 | Tauri フロントエンドのビルドにはviteを利用します。 ❯ npm create vite@latest . > npx > "create-vite" . │ ◇ Current directory is not empty. Please choose how to proceed: │ Ignore files and continue │ ◇ Select a framework: │ React │ ◇ Select a variant: │ TypeScript │ ◇ Use rolldown-vite (Experimental)?: │ No │ ◇ Install with npm and start now? │ Yes │ ◇ Scaffolding project in /home/nanao/Project/personal/hugo-local-cms... │ ◇ Installing dependencies with npm... added 175 packages, and audited 176 packages in 14s 45 packages are looking for funding run `npm fund` for details found 0 vulnerabilities │ ◇ Starting dev server... React + TypeScriptで実装することにしました。 ...

2026年2月5日 · にあえん

Disqus使ってhugoのPaperModにコメントできるようにした

こんにちは、ナナオです。 今回はこのブログにコメント機能を導入したいと思います。 とはいえ、このブログ自体はHugoで構築しており、動的サイトではないのでdisqusというサービスを使ってコメント機能を埋め込もうと思います。 まぁ大体の手順はこちらに書いてあります。 Hugo で作ったブログに Disqus を使ってコメント機能を追加する | michimani log ここではこのブログに使っているテーマのPaperModでコメント機能を埋め込むための方法を書いておこうと思います。 方法 まずはPaperModのlayouts/partials/comment.htmlを以下のように編集します。 {{- /* Comments area start */ -}} {{- /* to add comments read => https://gohugo.io/content-management/comments/ */ -}} {{ template "_internal/disqus.html" . }} {{- /* Comments area end */ -}} {{ template "_internal/disqus.html" . }}を追加しています。 また、hugoの設定ファイルも編集します。 disqusShortname = "neer-engineer-com" [params] # ...中略... comments = true これでOKです。 感想 disqusでコメントくれるとうれしいです。 参考 [BUG] Comments section not appearing on post page · Issue #502 · adityatelange/hugo-PaperMod · GitHub

2026年1月6日 · にあえん

hugoのテーマを新しくしました

今のHugoのテーマから、記事の検索機能などもあるいい感じのテンプレートに乗り換えたいなーと思った際の備忘録です。 何個かのテーマをインストールして実際に起動して確認してます。 hugoplateを導入する デモを見る限り、リッチなUIや記事の検索機能などがあり結構いいなと思ったのがこのテーマです。 Hugoplate git submoduleを使用してテーマを追加します。 hugoプロジェクトのthemesディレクトリで以下のコマンドを実行します。 git submodule add https://github.com/zeon-studio/hugoplate.git config.tomlを変更します。 theme = "hugoplate" この状態で一旦ローカルから起動してみます。 hugo serve -D しかしエラーが出てしまいました。。 render of "page" failed: execute of template failed: template: _default/single.html:8:7: executing "_default/single.html" at <partial "essentials/head.html" .>: error calling partial: "/home/username/Project/personal/hugo-blog/themes/hugoplate/layouts/partials/essentials/head.html:13:3": execute of template failed: template: partials/essentials/head.html:13:3: executing "partials/essentials/head.html" at <partialCached "favicon" .>: error calling partialCached: partial "favicon" not found render of "page" failed: execute of template failed: template: _default/single.html:8:7: executing "_default/single.html" at <partial "essentials/head.html" .>: error calling partial: "/home/username/Project/personal/hugo-blog/themes/hugoplate/layouts/partials/essentials/head.html:13:3": execute of template failed: template: partials/essentials/head.html:13:3: executing "partials/essentials/head.html" at <partialCached "favicon" .>: error calling partialCached: partial "favicon" not found render of "page" failed: execute of template failed: template: _default/single.html:8:7: executing "_default/single.html" at <partial "essentials/head.html" .>: error calling partial: "/home/username/Project/personal/hugo-blog/themes/hugoplate/layouts/partials/essentials/head.html:13:3": execute of template failed: template: partials/essentials/head.html:13:3: executing "partials/essentials/head.html" at <partialCached "favicon" .>: error calling partialCached: partial "favicon" not found render of "page" failed: execute of template failed: template: _default/single.html:8:7: executing "_default/single.html" at <partial "essentials/head.html" .>: error calling partial: "/home/username/Project/personal/hugo-blog/themes/hugoplate/layouts/partials/essentials/head.html:13:3": execute of template failed: template: partials/essentials/head.html:13:3: executing "partials/essentials/head.html" at <partialCached "favicon" .>: error calling partialCached: partial "favicon" not found Rebuild failed: Failed to render pages: render of "page" failed: execute of template failed: template: _default/single.html:8:7: executing "_default/single.html" at <partial "essentials/head.html" .>: error calling partial: "/home/username/Project/personal/hugo-blog/themes/hugoplate/layouts/partials/essentials/head.html:13:3": execute of template failed: template: partials/essentials/head.html:13:3: executing "partials/essentials/head.html" at <partialCached "favicon" .>: error calling partialCached: partial "favicon" not found hugo v0.92.2+extended linux/amd64 BuildDate=2023-01-31T11:11:57Z VendorInfo=ubuntu:0.92.2-1ubuntu0.1 Reload Page なんかファビコンが足りないっぽい? ...

2023年8月18日 · にあえん

hugoのショートコードで目次を生成した〜〜〜い

一部の記事がかなりの分量になってしまい、目次が設定したかったので設定したときの備忘録。 どうやるか まず、目次に関するテンプレートは公式で既に用意されていました。 TableOfContents なるほど、{{ .TableOfContents }}で参照すればいいみたいです。 なので、めちゃくちゃシンプルに目次を表示したいだけだった僕は以下のようなショートコードを実装しました。 {{ .TableOfContents }} この実装をtoc.htmlとして保存し、マークダウンからショートコードで参照してみましたが、hugo serveするとエラーが発生しました。 Rebuild failed: executing "shortcodes/toc.html" at <.TableOfContents>: can't evaluate field TableOfContents in type *hugolib.ShortcodeWithPage 調べてみたら、どうやらショートコードからは{{ .Page.TableOfContents }}と参照するのが正しい模様。 Hugoでドキュメント作るときに手っ取り早く痒いところに手が届くようになるTips #Hugo - Qiita 修正したら使用できるようになりました。 .Pageをつけなければいけない理由がいまいちよくわからないまま終わってしまったので、もし分かる人がいればコメントお願いします。。

2023年7月15日 · にあえん

hugoのショートコード(shortcode)でリンク先のタイトルを自動取得する

このブログはhugoを使って作成しています。 Hugoの何がいいかと言われれば、一番は静的なので軽いってところです。 ReactやVueでちょっとSPA作ろうとすると、すぐもっさりした動作になっちゃう&コンポーネント管理が大変なので、僕にはHugoが結構刺さっています。 (まぁコンポーネント管理に関してはアトミックデザインで作ろうとした僕の自業自得なんですが。。) Hugoをいろいろいじりつつ、最近の記事でも早速Hugoを活用してSEO対策などを行っていました。 その中で、ShortCodeという機能がプログラマーの僕としてはワクワクが止まらない機能だったので、ちょっと作りたいものもあったのでついでに使いつつここで紹介します。 ShortCodeとは markdownファイルから、関数などを組み込んだテンプレートHTMLを呼び出せる機能です。 デフォルトでは以下のようなshortcodeが用意されています。(詳細はこちら) figure gist highlight param ref (relref) instagram tweet vimeo youtube youtubeを表示したい場合、shortcodeを使って動画のIDを引数として渡してあげれば簡単に実装できます。 {{< youtube w7Ft2ymGmfc >}} これ以外にも自分でshortcodeを作成して、組み込むことも可能です。 shortcodeで使われる構文はGo Templateというみたいです。 参考: Go Template、最高のプログラミング言語 #go-template - Qiita URLからタイトルを取得したaタグを生成するshortcode では本題です。 (前提として、Hugoのサイトはすでに用意してあるものとします。) まずはshortcodeの実装のためのディレクトリとHTMLファイルを準備します。 mkdir layouts/shortcodes touch layouts/shortcodes/url_title.html ファイルの準備ができたら、以下のようなコードを実装します。 resources.GetRemoteを使用してリンク先のコンテンツを取得し、titleタグと組み合わせてaタグを生成するshortcodeです。 早速使ってみましょう。 ...

2023年7月11日 · にあえん

HugoでGoogleAnalyticsを実装してもユーザーとしてカウントされなかった原因がBraveのせいだった

当ブログにもGoogleAnalyticsを仕込んでみたんですが、その際に「設定したのに反映されていない!なんで!?」みたいなことがあったので。 公式に参考ページがあったので、これをもとにGoogleAnalyticsを仕込んでみました。 ただ、設定したうえでこのブログにアクセスしてみても、閲覧数がカウントされませんでした。 まじかーと思っていろいろ調べてみたんですが、ふとブラウザのDevToolから開いてみると… GoogleAnalyticsのコードが別のものに差し替わっている 謎の「uBlock Origin」というブラウザ拡張機能が実装したコードになっています。 更に調べていくと、私の使用しているブラウザが関係していることがわかりました。 使用しているブラウザがBraveなのですが、デフォルトでコードを置き換えるようになっていたようです。 サイト機能に影響を与えずトラッキングコードを置換 試しにFireFoxでアクセスしたら、ちゃんとユーザー数が反映されるようになりました。 GoogleAnalyticsから見たユーザー数 ブラウザのセキュリティが高いがゆえに起きた事象ですが、勉強になりました。 参考 Brave connects to googletagmanager.com · Issue #14027 · brave/brave-browser · GitHub このブログでは、GoogleAnalyticsに自分のアクセスを反映させない方法として紹介されています。なるほど。。 Googleアナリティクスで自分のアクセスを除外する方法 | sinpe-Blog

2023年7月11日 · にあえん

Hugoでかんたんpermalinks作成

SEO対策として、一応このブログにもパーマリンクを仕込むか〜〜〜となったときの備忘録。 ただ僕はそもそもパーマリンク自体あんまり良くわかってなかったので、そこから調べました。 そもそもパーマリンクって? SEO対策の一環として、ページに振られるURLを固有のものにすることです。 パーマリンクとは?SEOへの影響や注意点について解説 hugoの場合、デフォルト設定のままだと、タイトルとかディレクトリ構造をリファクタリングするとページURLも変わっちゃうので、せっかく検索して上位に来てもURLが変わっちゃったら全部台無しになっちゃいます。 それを防ぐためのパーマリンクです。 Hugoでパーマリンクは設定できる? 結論から言えば、公式でパーマリンクを生成する方法はありません。 前述の通り、Hugoが生成するページリンクは記事タイトルやディレクトリ構成に影響するため、後にタイトルを変えるといったケースがある場合にはそれに対応できません。 逆に言えば、そういった変更を今後一切行わないのであれば別にデフォルト設定のままでも十分ということになります。 なので、ここではhugoのテンプレート構文を利用してIDを生成する関数を実装します。 単純にランダムに英数字を並べかるだけでは味気ないので、せっかくなので現在時刻を元にしたmd5ハッシュを使ってID生成します。 archetypes/default.mdを開いて、以下のような実装を追記します。 --- title: "{{ replace .Name "-" " " }}" slug: "{{ template "permanentId" }}" date: {{ .Date }} draft: true --- {{ define "permanentId" -}} {{ md5 now.Unix }} {{- end -}} ハッシュ値はslugに設定しました。 次に、config.tomlからブログのパスにslugを参照するように設定します。 [permalinks] posts = "/:slug/" (postsの部分はcontentの名前によって変えてください) これであとはhugo newで記事を作ると、slugにハッシュ値が設定されるようになり、結果としてURLが時刻を元にしたmd5ハッシュ値になっています。 感想 Hugoおもしろい 参考 Hugoのテンプレート構文「template」「partial」「block」「define」のわかりやすい解説 Hugo でサイト構造を変えてもページの URL が変わらないようにする (Permalink) Hugo テンプレート内で define による部分テンプレート定義を行う(関数もどき) Hugo の URL をカスタマイズする

2023年7月9日 · にあえん