📖

.NET Core Blazor + MicroCMSでブログサイト作成⑧

2021/09/06に公開

8という数字は日本では末広がりとして、縁起の良い数字として扱われています。
麻雀の八連荘や中華料理の八宝菜のように、中国でも8はプラスのニュアンスで扱われているような気がします。
英語では「eightが2つでダブルeight ⇒ weight」ということで、"That one weights 88kg."という言葉があるそうです(大嘘)。

今回の内容は以下のとおりです。
内容が細かいですが、私自身の備忘録的なところもあるのでどうかお許しください。

  • ブログ記事ページに機能追加、スタイリング
    • シンタックスハイライト(Highlight.js)
    • 一覧に戻るリンク
    • 次の記事/前の記事リンク うまいやり方が見つからないので後回しにしますが、方針だけ記しておきます
  • タグ選択処理をもっと簡単にする
    • ついでにタグ選択を解除する「すべて」ボタンを足す

シンタックスハイライト

https://highlightjs.org/

どの言語で書かれているのか自動で検出してハイライトしてくれるライブラリです。
ハイライトした言語を指定してCSS/JavaScriptをダウンロードすることもできるし、CDNから引っ張ってくるだけで済ますこともできます。
今回は脳死でCDNを採用します。

ここを参考にしてつくりました。
https://sciencevikinglabs.com/blog/development/2021-01-02-blazor-code-snippet-highlightjs/

wwwroot/index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorBlog</title>
    <base href="/" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous" />
    <!-- 追加 -->
    <link href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/styles/atom-one-dark.min.css" rel="stylesheet" >
    <link href="css/app.css" rel="stylesheet" />
</head>

<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.3/dist/umd/popper.min.js" integrity="sha384-eMNCOe7tC1doHpGoWe/6oMVemdAVTMs2xqW4mwXrXsW0L84Iytr2wi5v2QjrP/xp" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.min.js" integrity="sha384-cn7l7gDp0eyniUwwAZgrzD06kc/tftFf19TOAs2zVinnD/C7E91j9yyk5//jjpt/" crossorigin="anonymous"></script>
    <!-- 以下追加 -->
    <script src="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/highlight.min.js"></script>
    <script>
        window.highlightSnippet = function() {
	    // <pre><code>部を全部抜き出して
            document.querySelectorAll('pre code').forEach((el) => {
	        // ハイライトする
                hljs.highlightElement(el);
            });
        }
    </script>
</body>

</html>
BlogPage.razor
// 抜粋

// 冒頭に追加
@inject IJSRuntime JSRuntime

// ...

@code {
    [Parameter]
    public string blogId { get; set; }

    private BlogContents contents;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            contents = await Http.GetFromJsonAsync<BlogContents>($"MicroCMS/BlogContents/{blogId}");
        }
        catch (Exception e)
        {
            Console.WriteLine(e.ToString());
        }
    }

    // 追加
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        await JSRuntime.InvokeVoidAsync("highlightSnippet");
    }
}

OnAfterRenderAsyncはレンダリングが終了したあとに実行するメソッドです。名前のままです。
IJSRuntime.InvokeVoidAsyncはナマのJavaScriptを実行するメソッドで、関数名(文字列)と引数を渡すとよしなに実行してくれます。
https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync?view=dotnet-plat-ext-6.0#Microsoft_JSInterop_JSRuntimeExtensions_InvokeVoidAsync_Microsoft_JSInterop_IJSRuntime_System_String_System_Object___

実行してみると、キレイにハイライトされてます。すごいなぁ...

highlightBlock will be removed entirely in v12.0

参考サイトではhighlightBlock関数を使っていますが、どうやらv12.0で廃止されるそうです。
コンソールにメッセージが出てきました。

代わりにhighlightElementを使ってくれと言っているので、上のindex.htmlではhighlightElementに書き換えています。

各種リンク追加

戻るリンク

普通にaタグをつけるだけです。

index.html
// 抜粋
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>BlazorBlog</title>
    <base href="/" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css"  integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.5.0/font/bootstrap-icons.css" />
    <!-- 追加(Bootstrap Icon) -->
    <link rel="stylesheet" href="//cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.2.0/build/styles/atom-one-dark.min.css" />
    <link href="css/app.css" rel="stylesheet" />
</head>
BlogPage.razor
<!-- 抜粋 -->

    <!-- 追加 -->
    <div class="mb-3">
        <i class="bi bi-arrow-90deg-left"></i>
        <a href="blog">一覧に戻る</a>
    </div>
    <h2>@contents.Title</h2>
    <p>Tag:@contents.Tag.Name</p>
    <p>Publish:@contents.PublishedAt.ToLocalTime().ToString("yyyy/MM/dd")</p>
    <div>
        @((MarkupString)contents.Body)
    </div>

前後のブログIDを渡すには...?

次の記事/前の記事リンクは少し工夫が必要です。
MicroCMSには指定したIDの前後のIDを対象にコンテンツを取得する機能がまだありません。
https://document.microcms.io/faq/get-previous-or-next

そこで以下のような流れでIDを取得します。

  1. ブログ一覧で取得したIDから、指定したIDのインデックスを特定する
  2. 前後のインデックスのIDを取得する
  3. 「何らかの方法で」2.で取得したIDをブログコンテンツページに渡す

1.と2.はすぐにできるのですが、3.をキレイにやる方法が見つかりません。

  • クエリパラメータで渡す ⇒ 簡単だけどURLを汚したくない。
  • セッションで渡す ⇒ いまのところ最有力候補。DIが必要。
  • Cookieで渡す ⇒ しっとりクッキー好き。
  • 両隣のIDを取得するリクエストをつくる ⇒ スマホに優しくない。

というわけで、セッションでやろうと思うのですがまだ確定ではないので、方針を書いておくだけにします。

タグ選択の改善

前回実装したタグ選択ですが、タグボタンを選択するたびにMicroCMSへリクエストを送っていました。
しかしタグを押すたびに通信が発生するとリクエスト回数の制限に引っかかったり、なによりスマホに優しくないです。
せっかく全記事のデータを取得しているので、クライアント内でフィルタリングするように修正してみます。
ついでにタグ選択を解除する「すべて」ボタンもつくります。

いろいろ修正箇所があるので、全体書いておきます。

BlogTop.razor
@page "/blog"
@using BlazorBlog.Shared.MicroCMS
// 追加
@using System.Linq
@inject HttpClient Http

@if (sitedata == null || tagList == null || allBlogContentsList == null || filteredBlogContentsList == null)
{
    <div class="d-flex justify-content-center">
        <div class="spinner-border" style="width: 3rem; height: 3rem;" role="status">
            <span class="visually-hidden">Loading...</span>
        </div>
    </div>
}
else
{
    <h1>@sitedata.BlogTitle</h1>

    <div class="mb-3">
	<!-- すべてボタン追加 -->
        <button type="button" class="btn btn-dark btn-sm rounded-pill mx-2" @onclick="@(e => TagSelected("ALL"))">すべて</button>
        @foreach (TagContents contents in tagList)
        {
            <button type="button" class="btn btn-dark btn-sm rounded-pill mx-2" @onclick="@(e => TagSelected(contents.Id))">@contents.Name</button>
        }
    </div>

    @foreach (BlogContents contents in filteredBlogContentsList)
    {
        <div class="row my-2 d-flex justify-content-center">
            <div class="col-10 col-md-8 text-truncate cardbox">
                <div class="m-3">
                    <a href="blog/@contents.Id">@contents.Title</a>
                    <div>Tag:@contents.Tag.Name</div>
                    <div>Publish:@contents.PublishedAt.ToLocalTime().ToString("yyyy/MM/dd")</div>
                </div>
            </div>
        </div>
    }
}

@code {
    // タグとブログコンテンツをListで受け取るように変更
    private SiteDataModel sitedata;
    private List<TagContents> tagList;
    // ブログコンテンツ全件
    private List<BlogContents> allBlogContentsList;
    // タグでフィルタリングされたブログコンテンツ
    private List<BlogContents> filteredBlogContentsList;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            //Task作成
            Task<SiteDataModel> taskSiteData = Http.GetFromJsonAsync<SiteDataModel>("MicroCMS/SiteData");
            Task<TagModel> taskTag = Http.GetFromJsonAsync<TagModel>("MicroCMS/TagList");
            Task<BlogModel> taskBlog = Http.GetFromJsonAsync<BlogModel>("MicroCMS/BlogList");

            // 並列処理
            await Task.WhenAll(taskSiteData, taskTag, taskBlog);

            // 結果取り出し
            sitedata = taskSiteData.Result;
            tagList = taskTag.Result.TagList;
            allBlogContentsList = taskBlog.Result.BlogList;
            filteredBlogContentsList = taskBlog.Result.BlogList;
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

    private void TagSelected(string tagId)
    {
	// タグIDが"ALL"なら全件、それ以外ならtagIdでフィルタリング
        filteredBlogContentsList = tagId == "ALL" ?
            allBlogContentsList : 
            allBlogContentsList.Where(bc => bc.Tag.Id == tagId).ToList();
    }
}

実行してみると、タグ選択してもリクエストが送られていないことがわかります。


   👇

すべてを押すと全件表示されることも確認できます。


   👇

次回

ようやくというか何というか、ずっと放置してきたエラー処理を実装します。
リダイレクト、メッセージ表示、404ページ/500ページ作成など、いろいろやることがあります。

Discussion