当サイトを最適な状態で閲覧していただくにはブラウザのJavaScriptを有効にしてご利用下さい。
JavaScriptを無効のままご覧いただいた場合には一部機能がご利用頂けない場合や正しい情報を取得できない場合がございます。
承知しました
本サイトではWebサイトのエクスペリエンスを向上させるために、Cookieを使用しています。Cookieはブラウザの設定から無効にできます。本サイトで使用するCookieについては、プライバシーポリシーをご確認ください。

Blog

ブログ

開発者向け

KenticoCloudでのナビゲーションメニューの管理

By Jan Lenoch  

KenticoCloudでメニューナビゲーションを管理するためのコンテンツ寄稿者と情報アーキテクトのベストプラクティスを見つけましょう。このチュートリアルの一部として、コンテンツインベントリで設計されたページまたは従来の方法で動的にレンダリングされたページに自動的に解決されるSEO対応URLとともに、このようなナビゲーションをレンダリングできるMVCサイトを構築する方法について説明します。 MVCアクション。

ビジネス上の問題

コンテンツの寄稿者がKenticoCloudのナビゲーションメニューを設計および保守したい場合はどうなりますか?彼らの要件は何ですか? Kentico Cloudはそのようなシナリオをサポートできますか?最善の解決策は何でしょうか?それに関連する技術的な課題は何ですか?このステップバイステップの記事で調べてみましょう。

要求事項

ナビゲーションは、プロジェクトの情報アーキテクチャの重要な部分として、複数の関係者が共同で取り組む必要があります。すべての役割が設計プロセスに関与する必要があります。ただし、Kenticoパートナーから聞いたように、コンテンツ編集者はサイトの特定のセクションでより多くの制御を要求することがよくあります。製品、ランディングページ、ナレッジベースセクションなどのように。従来のCMSとは異なり、ヘッドレスCMSシステムには古き良きコンテンツツリーが組み込まれていません。それらは他のタイプのナビゲーションを可能にします。

Kenticoパートナーとの話し合いから、ナビゲーションアイテムを定義する一般的な方法は3つあると結論付けました。

  1. ナビゲーションアイテムは特定のページを直接指します(たとえば、メニューのリンクをクリックすると、一意のページが開きます)。
  2. ナビゲーションアイテムは、他のナビゲーションアイテムの親です(つまり、子アイテムをラップします)。
  3. ナビゲーションの一部は、分類法(製品カテゴリなど)またはカスタムコンテンツ要素(ブログ投稿日など)によって決定されます。

Kenticoクラウドでのサポート

では、Kentico Cloudはこれらすべてのナビゲーションタイプをサポートできますか?ヘッドレスCMSとして、KenticoCloudはオープン性と汎用性を念頭に置いて設計されています。つまり、簡単な答えは「はい」です。詳細な回答は、「アプリの構築」の章にあります。

ソリューションの概要

サンプルソリューションは、理解しやすく、使いやすく、拡張可能でありながら、技術的な制限やハッキングがないように設計しました。

サンプルアプリがどのように見えるかを見てみましょう。

Kentico Kontent


やや階層構造のトップメニューがあります。メニューは折りたたみ可能です。

メニューを機能させるために、私は2つの面で作業します。まず、クラウドでのナビゲーションを維持するために、特別なコンテンツタイプであるナビゲーションアイテムを作成します。次に、次の機能を備えたASP.NET MVCCoreアプリを作成します。

  • 「ナビゲーションアイテム」アイテムで構成されるレンダリングメニュー
  • それらのメニューのハイパーリンクのURLに基づいてページをレンダリングする

次のスキームは、MVCアプリとKenticoCloudの関係を明確にしています。また、ナビゲーションアイテムのコンテンツタイプの性質も示しています。

Kentico Kontent


このアプリは、サンプルコンテンツである架空のコーヒー販売会社「DancingGoat」を使用しています。このアプリは、Kentico Cloudのコンテンツアイテムから構築されたメインメニューを自動的にレンダリングし、メニューアイテムで行われたHTTPリクエストに従ってページを表示します。一部のページは静的です。コンテンツインベントリ内のモジュラーコンテンツアイテムで構成されます。それらのいくつかは動的です。つまり、実際のURLに基づいたリストとフィルタリングを備えた通常のMVCページです。

アプリの完全なソースコードはGitHubからダウンロードできます。

アプリの構築

コンテンツモデルサービスを開始して、ナビゲーションアイテムのコンテンツタイプを設計します。次に、コンテンツインベントリに移動して、そのタイプのアイテムをいくつか作成します。最後に、VisualStudioでいくつかの甘いMVCコーディングを行います。

コンテンツタイプの作成

誰もがトライアルアカウントと無料アカウントで入手できるDancingGoatサンプルプロジェクトを取り上げます。そのプロジェクトにナビゲーションアイテムのコンテンツタイプを追加します。

写真は千の言葉の価値があります。ナビゲーションアイテムのコンテンツタイプのスクリーンショットを見てみましょう。

Kentico Kontent


ご覧のとおり、子ナビゲーションアイテムを「子ナビゲーションアイテム」要素に配置できます。したがって、この概念により、従来の階層型コンテンツツリーをすばやく作成できます。しかし、あなたは決してそのコンテンツツリーだけに限定されません。ナビゲーションアイテムのコンテンツタイプは、実際には、前述の3つのタイプのナビゲーションすべてをサポートしています。 1つのWebサイトにまとめた場合でも、それらをモデル化する方法を見てみましょう。

タイプ1:ナビゲーションアイテムが特定のページを直接指す

メインメニューなどとしてレンダリングできるナビゲーションアイテムの階層をモデル化できることはすでにご存知でしょう。各アイテムにはURLがあります。しかし、ユーザーがナビゲーションアイテムをクリックするとどうなりますか?どのページが表示されますか?

アプリに静的ページを表示させるには、「コンテンツアイテム」要素でコンテンツアイテムを割り当てるだけです。たとえば、「ホーム」ナビゲーション項目でこのタイプを使用します。

Kentico Kontent


動的ページに関する限り、「タイプ3:ナビゲーションアイテムは分類法またはカスタムコンテンツ要素によって定義される」の章で触れます。

タイプ2:ナビゲーションアイテムグループその他のアイテム

一部のナビゲーションアイテムは、他のアイテムをグループ化するだけでよい場合があります。これを実現するには、「子ナビゲーションアイテム」要素で複数の子アイテムを作成または割り当てるだけです。このようにして、親と子のナビゲーションアイテムの階層を作成します。

また、ユーザーがメニューの親ナビゲーション項目をクリックしたときに何が起こるかを決定することもできます。 JavaScriptを使用して子アイテム(サブメニュー)を展開/折りたたみできます。または、特定のセクションを説明するページをレンダリングするようにサーバーに要求することもできます。この場合、「コンテンツアイテム」要素でコンテンツアイテムを割り当てるだけです。親ナビゲーションアイテムが説明ページを指すようにしたくない場合は、代わりにユーザーを別のナビゲーションアイテム(最初の子など)にリダイレクトすることをお勧めします。

私のサンプルアプリでは、「製品カタログ」ナビゲーションアイテムにこのアプローチを選択しました。メインメニューでは、アイテムは子アイテムを展開/折りたたみするだけです。ただし、ユーザーがアドレスバーに直接http://example.com/product-catalogと入力すると、アプリはユーザーをコーヒーページにリダイレクトします(404を返す代わりに)。

Kentico Kontent


ご想像のとおり、「アイテムにリダイレクト」要素は、コンテンツの日常のメンテナンス、つまり、削除、移動、または名前変更されたコンテンツのリクエストを処理する場合にも非常に便利です。

タイプ3:ナビゲーションアイテムは分類法またはカスタムコンテンツ要素によって定義されます

このアプローチでは、アプリは分類法またはカスタムコンテンツ要素値に基づいてコンテンツをフィルタリングします。たとえば、URL http://example.com/shop/phones/appleは、Brand要素がAppleに設定されている電話コンテンツアイテムをフィルタリングする場合があります。または、URL http://example.com/blog/2014/10を使用すると、2014年10月に公開されたブログ投稿アイテムのアプリフィルターが作成される場合があります。

要するに、このタイプのナビゲーションをファセットナビゲーションと呼びます。

最初の例(電話)は、KenticoCloud分類機能を使用して実装するのが最適です。この場合、アプリはDelivery / Preview APIから分類構造を取得し、それを「/ shop」アイテムのサブツリーとしてナビゲーション階層全体に追加する必要があります。注:分類法は非常に便利ですが、現在、分類法アイテムのURLに適したコード名を保存することはできません。そのため、この記事では、このタイプのファセットナビゲーションの実装を省略しました。

2番目の例(ブログ投稿)では、ブログ投稿が作成された年と月をアプリが認識している必要があります。アプリは、既存のブログ投稿の日付を取得し、年と月を抽出して、「/ blog」ナビゲーション項目の下に追加する必要があります。この2番目のタイプのファセットナビゲーションをアプリでデモンストレーションします。

その2番目のタイプのファセットナビゲーションには数十の実装がある可能性があるため、コードを使用して実装することにしました。ファセットナビゲーションの幅広いサポートをナビゲーションアイテムのコンテンツタイプに組み込みませんでした。私の考えは次のとおりでした:

  • 開発者は、最初にコンテンツストラテジストと開始URLのリストについて合意することができます。開始URLの例は「/ blog」です。
  • 次に、開始URLは、コンテンツストラテジストによって、通常のナビゲーションアイテムとしてナビゲーション階層に配置されます。
  • 最後に、メニューを表示するときに、アプリは特定のビジネス要件に従って、任意のコードを使用して子ナビゲーションアイテムを動的に追加します。

このようなアイデアには、次の利点があります。

  • コンテンツストラテジストは、これらの開始点をナビゲーション階層のどこにでも自由に配置できます。
  • 彼らは時間をかけて自由に動き回ることができます。
  • 開発者は、コードを自由に使用して、アプリに動的ナビゲーションアイテムを追加させることができます。

了解しました。これまで、ナビゲーション項目がメニューに追加される方法を扱ってきました。では、これらのメニュー項目に従ってコンテンツを表示してみませんか?簡単な答え:同じように、コードを使用します。 MVCの場合、着信要求は通常のルートとコントローラーアクションで処理するのが最適です。

サンプルアプリに戻りましょう。単純なファセットナビゲーションの開始点として、「ブログ」ナビゲーション項目を設定します。 「URLにリダイレクト」要素を「/ blog」に設定します。次に、インベントリ内のすべての「Article」コンテンツアイテムの「Postdate」要素に基づいて、子ナビゲーションアイテムを動的に生成するクラスを作成します。 (ドラッグアンドドロップツールを使用するのではなく)コードを使用する柔軟性を示すために、ナビゲーションアイテムの階層ブランチまたはフラットブランチのいずれかを生成できるクラスを作成します。階層的なものは次のようになります。

  • ブログ(/ blog)
    • 2014(/ blog / 2014)
      • 10(/ blog / 2014/10)
      • 11(/ blog / 2014/11)

フラットなものは次のようになります。

  • ブログ(/ blog)
    • 2014年10月(/ blog / 2014/10)
    • 2014年11月(/ blog / 2014/11)

上記のように、URLを動的ページにルーティングするプロセスは、クリーンで従来のMVCルートとコントローラーアクションによって処理されます。以下は、BlogControllerクラスのIndexメソッドのシグネチャです。

 public async Task Index(int? year, int? month)


さて、これまでのところ、ソリューションの基本について概説しました。実装自体に飛び込みましょう!

ナビゲーションアイテムの作成

この記事の目的のために、次の簡単なナビゲーション構造を作成します。 (括弧内には、常にURLスラッグがあります。)

  • ナビゲーション( '[root]')
    • ホーム( '')
    • 製品カタログ( 'product-catalog')
      • コーヒー(「コーヒー」)
    • ブログ(「ブログ」)
      • 2014年10月
      • 2014年11月

「ホーム」および「コーヒー」アイテムは静的コンテンツを指します。参考までに、上記の「タイプ1:ナビゲーションアイテムが特定のページを直接指す」の章の「ホーム」アイテムのスクリーンショットを参照してください。

注:複数のコーヒー製品を1つのコンテンツアイテムにラップするために、単純な「コンテンツリスト」コンテンツタイプを作成しました。他のアイテムを保持するためのモジュラーコンテンツ要素が1つだけあります。ページのレイアウトを定義するため、再利用できます。

Kentico Kontent


「製品カタログ」と「ブログ」のアイテムは、JavaScriptを使用して子を展開および折りたたみます。ユーザーが手動で「/ product-catalog」に移動すると、アプリは最初の子、つまり「/ product-catalog / coffee」にリダイレクトします。繰り返しになりますが、参考までに、上記の「タイプ2:ナビゲーションアイテムグループその他のアイテム」の章にある「製品カタログ」ナビゲーションアイテムのスクリーンショットをご覧ください。

「ブログ」アイテムの子アイテムは、すべての「記事」コンテンツアイテムの作成日に基づいて動的に生成されます。 '/ blog'、 '/ blog / 2014'、 '/ blog / 2014/10、または' / blog / 2014/11 'に移動すると、コントローラーアクションは記事を動的にフィルタリングし、対応するリストをレンダリングします。 「ブログ」アイテムは通常のナビゲーションアイテムであり、「URLにリダイレクト」要素のみが「/ blog」に設定されています。それでおしまい。

MVCアプリのコーディング

あなたがMVCに精通していると仮定して、コードの重要な部分を指摘します。コードはそれほど複雑ではないため、インラインコードコメントまたはコード自体から実装の詳細を学ぶことができます。

まず、KenticoCloud.CloudBoilerplateNetテンプレートがインストールされた状態で「dotnetnew」コマンドを使用します。それは依存関係を追加し、私のためにいくつかの一般的なものを準備します。

メニューのレンダリング

高く評価されているドロワーメニュープロジェクトを使用してメインメニューを作成します。この折りたたみ可能なメニュープロジェクトは、フロントエンドメニュー開発のベストプラクティスを表しています。すべてのビューポートに対して単一のHTMLマークアップのみを処理でき、最小限のJS依存関係(jQuery、iScroll)を使用し、そのコードファイルはCDNネットワークを介して配布されています。

Layout.cshtmlファイルでナビゲーションマークアップのセクションを定義してから、DrawerMenuPartial.cshtml部分ビューを出力にプラグインします。ビューは、「ナビゲーションアイテム」KenticoCloudコンテンツタイプを表すNavigationItemタイプに強く入力されます。

_DrawerMenuPartial.cshtmlファイル:

 @model NavigationItem


:16行目では、明示的な表示テンプレートを使用して、階層の最初のレベルをレンダリングしています。

NavigationItemMain-1.cshtmlファイルを見ると、ナビゲーションアイテムの階層の第2レベルに対して基本的に同じことを行っていることがわかります。

 @model NavigationItem@if (Model.ChildNavigationItems == null || !Model.ChildNavigationItems.Any()){ 
  • @Html.DisplayFor(vm => vm.Title)
  • }else{
    • @foreach (var item in Model.ChildNavigationItems) { @Html.DisplayFor(vm => item, 'NavigationItemMain-2') }
  • }


    では、どうすればそのNavigationItemオブジェクトを取得できますか?魔法はありません。 NavigationProviderヘルパークラスを作成して、Delivery / Preview APIからデータを取得し、計算値で装飾して、アプリのメモリ内キャッシュに保存します。このクラスは、ボイラープレートコードのCachedDeliveryClientクラスとは別のキャッシュインスタンスを使用します。これは有益です。ナビゲーションデータは、ニーズに応じて、より短い期間またはより長い期間キャッシュできます。

    (補足:計算されたプロパティと言えば、そのうちの1つであるAllParentsを使用して、ブレッドクラムメニューをレンダリングできます。)

    NavigationProviderクラスの最も重要なメソッドはGetNavigationAsyncです。これは、前の段落で説明したプロセスを調整します。 2つのオプションのパラメーターを使用して、ナビゲーション項目の明示的なコード名とロードする優先深度を指定できます。 それらを指定しない場合、メソッドは、NavigationOptionsクラスを介して取得したappsettings.jsonで設定されたデフォルトを使用します。明示的なコードネームパラメーターは、アプリに他のメニュー(下部のメニュー、サイトマップページなど)を含めることができるようにするためのものです。

    /// /// Gets the root  item either off of the  or the Delivery/Preview API endpoint./// /// The explicit codename of the root item. If , the value supplied in the constructor is taken./// The explicit maximum depth of the hierarchy to be fetched/// The root item of either the explicit codename, or default codenamepublic async Task GetNavigationAsync(string navigationCodeName = null, int? maxDepth = null){ string cn = navigationCodeName ?? _navigationCodename; int d = maxDepth ?? _maxDepth; return await _cache.GetOrCreate(NAVIGATION_CACHE_KEY, async entry => { var navigation = await LoadNavigationItemsAsync(cn, d); var emptyList = new List(); // Add the UrlPath property values to the navigation items first. AddUrlPaths(emptyList, navigation, string.Empty); emptyList.Clear(); // Then, add the RedirectPath, Parent, and AllParents property values. UrlPath value is needed for that, hence a separate iteration through the hierarchy. AddRedirectPathsAndParents(navigation, emptyList, navigation); entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_navigationCacheExpirationMinutes); return navigation; });}


    したがって、NavigationItemオブジェクトを取得するには、コントローラーアクション内からそのメソッドを呼び出し、ビューモデルを介してビューに渡すだけで、完了です。

     await _navigationProvider.GetNavigationAsync()


    ちょっと待って!ファセットナビゲーション用の追加メニュー項目が必要です。そのため、結果をGenerateItemsAsyncメソッドに渡します(詳細については、次の章を参照してください)。

     var navigation = await _menuItemGenerator.GenerateItemsAsync(await _navigationProvider.GetNavigationAsync());


    先に進む前に、NavigationProviderクラスの残りの重要なメソッドを見てみましょう。

    public async Task LoadNavigationItemsAsync(string navigationCodeName = null, int? maxDepth = null){ string cn = navigationCodeName ?? _navigationCodename; int d = maxDepth ?? _maxDepth; var response = await _client.GetItemsAsync( new EqualsFilter('system.type', ITEM_TYPE), new EqualsFilter('system.codename', cn), new LimitParameter(1), new DepthParameter(d) ); return response.Items.FirstOrDefault();}private void AddUrlPaths(IList processedParents, NavigationItem currentItem, string pathStub){ if (processedParents == null) { throw new ArgumentNullException(nameof(processedParents)); } if (currentItem == null) { throw new ArgumentNullException(nameof(currentItem)); } // Check for infinite loops. if (!processedParents.Contains(currentItem)) { AddUrlPath(currentItem, pathStub); processedParents.Add(currentItem); // Spawn a tree of recursions. foreach (var currentChild in currentItem.ChildNavigationItems) { AddUrlPaths(processedParents, currentChild, currentItem.UrlPath); } }}private void AddRedirectPathsAndParents(NavigationItem cachedNavigation, IList processedParents, NavigationItem currentItem){ if (currentItem == null) { throw new ArgumentNullException(nameof(currentItem)); } // Check for infinite loops. if (!processedParents.Contains(currentItem)) { var redirect = currentItem.RedirectToItem.FirstOrDefault(); if (redirect != null) { currentItem.RedirectPath = GetRedirectPath(cachedNavigation, redirect); } currentItem.Parent = processedParents.Count > 0 ? processedParents.Last() : null; currentItem.AllParents = processedParents; processedParents.Add(currentItem); // Spawn a tree of recursions. foreach (var currentChild in currentItem.ChildNavigationItems) { AddRedirectPathsAndParents(cachedNavigation, processedParents, currentChild); } }}private void AddUrlPath(NavigationItem navigationItem, string pathStub){ if (navigationItem == null) { throw new ArgumentNullException(nameof(navigationItem)); } if (navigationItem.UrlSlug != _rootToken && navigationItem.UrlSlug != _homepageToken) { navigationItem.UrlPath = !string.IsNullOrEmpty(pathStub) ? $'{pathStub}/{navigationItem.UrlSlug}' : navigationItem.UrlSlug; } else { navigationItem.UrlPath = string.Empty; }}private string GetRedirectPath(NavigationItem cachedNavigation, NavigationItem itemToLocate){ if (cachedNavigation == null) { throw new ArgumentNullException(nameof(cachedNavigation)); } if (itemToLocate == null) { throw new ArgumentNullException(nameof(itemToLocate)); } if (cachedNavigation.UrlPath == null) { throw new ArgumentException($'The {nameof(cachedNavigation.UrlPath)} property cannot be null.', nameof(cachedNavigation.UrlPath)); } var match = cachedNavigation.ChildNavigationItems.FirstOrDefault(i => i.System.Codename == itemToLocate.System.Codename); if (match != null) { return match.UrlPath; } else { return cachedNavigation.ChildNavigationItems.Select(i => GetRedirectPath(i, itemToLocate)).FirstOrDefault(r => !string.IsNullOrEmpty(r)); }}


    動的メニュー項目の生成

    上記のように、ファセットナビゲーションでは、アプリがページ本文データ(この場合、すべての「記事」コンテンツアイテムの「投稿日」要素)をスキャンして、ナビゲーションアイテムのセットを抽出する必要があります。これらのアイテムを後でブラウザリクエストを実行するために使用すると、アプリは記事を適切にフィルタリングし、リストページに返します。

    そこで、そのためのMenuItemGeneratorクラスを作成します。その中に、2つの重要なメンバーがいます。

    • 開始URL(私の場合は「/ blog」など)の辞書とそれに対応するメニュー項目の生成方法
    • これらすべてのメニュー項目生成メソッドを呼び出す中央メソッド

    辞書は次のようになります。

     private Dictionary>> _startingUrls = new Dictionary>>();


    (「ブログ」相対URLと、コンストラクターのGenerateNavigationWithBlogItemsAsyncメソッドへのデリゲートが入力されます。)

    中心的な方法:

    /// /// Wraps all methods that generate additional navigation items./// /// The original root navigation item/// A copy of the  with additional itemspublic async Task GenerateItemsAsync(NavigationItem sourceItem){ return await _cache.GetOrCreateAsync('generatedNavigationItems', async entry => { foreach (var url in _startingUrls) { sourceItem = await url.Value(sourceItem, url.Key); } entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(_navigationCacheExpirationMinutes); return sourceItem; });}


    MenuItemGeneratorクラスのポイントは、上記のNavigationProvider.GetNavigationAsyncメソッドによって生成された階層を再生成し、いくつかの動的アイテムを追加することです。ただし、メニューをレンダリングする目的でのみ階層を生成するため(着信要求のルーティングではない)、ProcessLevelForBlogプライベートメソッドは、元のすべてのプロパティを使用してNavigationItemオブジェクトを再生成する必要はありません。いくつかのプロパティを使用して、元のオブジェクトの軽量コピーを作成するだけです。

     /// /// Traverses the original hierarchy, creates a lightweight clone of it (not all original properties), and adds  items for the Blog section./// /// The root /// Dictionary of years and months of existing 'Article' content items/// The starting URL where new year and month  items will be added/// Flag indicating whether flat structure like 'October 2014' should be generated/// Collection of processed parent navigation items/// A copy of  with generated Blog navigation itemsprivate NavigationItem ProcessLevelForBlog(NavigationItem currentItem, IDictionary yearsMonths, string startingUrl, bool flat, IList processedParents){ processedParents = processedParents ?? new List(); var newItem = new NavigationItem(); // Check for infinite loops. if (!processedParents.Contains(currentItem)) { // Add only those properties that are needed to render the menu (as opposed to routing the incoming requests). newItem.Title = currentItem.Title; newItem.UrlPath = currentItem.UrlPath; newItem.AllParents = processedParents; processedParents.Add(currentItem); // The '/blog' item is currently being iterated over. if (currentItem.UrlPath.Equals(startingUrl, StringComparison.OrdinalIgnoreCase)) { // Example of a flat variant: 'October 2014', 'November 2014' etc. if (flat) { var items = new List(); items.AddRange(yearsMonths.OrderBy(k => k.Key, new YearMonthComparer()).Select(i => GetItem(startingUrl, i.Value, i.Key.Year, i.Key.Month))); newItem.ChildNavigationItems = items.ToList(); } // Example of a deep variant: // '2014' // '10' // '11' else { var yearItems = new List(); // Distill years of existing 'Article' items. foreach (var year in yearsMonths.Distinct(new YearEqualityComparer())) { var yearItem = GetItem(startingUrl, null, year.Key.Year); yearItems.Add(yearItem); var monthItems = new List(); // Distill months. foreach (var month in yearsMonths.Keys.Where(k => k.Year == year.Key.Year).OrderBy(k => k.Month)) { monthItems.Add(GetItem(startingUrl, null, year.Key.Year, month.Month)); } yearItem.ChildNavigationItems = monthItems; } newItem.ChildNavigationItems = yearItems; } } else { newItem.ChildNavigationItems = currentItem.ChildNavigationItems.Select(i => ProcessLevelForBlog(i, yearsMonths, startingUrl, flat, processedParents)).ToList(); } return newItem; } return null;}


    :このメソッドは、「フラット」ブールパラメーターをfalseに設定して呼び出すこともできます。これにより、浅い階層ではなく、深い階層が生成されます。ドロワーメニュープロジェクトはより深いメニューをサポートしていないため、「フラット」をtrueに設定します。

    了解しました。ナビゲーションアイテムをアプリにキャッシュし、ファセットナビゲーション用の追加アイテムを含む階層のコピーを生成しました。これで、任意のコントローラー方法でそのデータを表示できます。たとえば、私のアプリでは、2つのコントローラーを作成します。

    これらのコントローラーでは、メニューを表示するだけでなく、これらのメニュー内からの要求にも対応する必要があります。

    HTTPリクエストの処理

    ロジックは着信リクエスト順に説明します。ルート、コントローラーアクションから、アクション結果まで。

    ルートから始めるために、これら2つをStartup.csコードファイルに入れます。

     app.UseMvc(routes =>{ routes.MapRoute( name: 'facetedNavigation', template: 'blog/{year?}/{month?}', defaults: new { controller = 'Blog', action = 'Index' }); routes.MapRoute( name: 'staticContent', template: '{*urlPath}', defaults: new { controller = 'StaticContent', action = 'Index' }, constraints: new { urlPath = new StaticContentConstraint(app.ApplicationServices.GetRequiredService()) }); routes.MapRoute( name: 'default', template: '{controller=Home}/{action=Index}/{id?}');});


    'facetedNavigation'ルートはデフォルトでBlogControllerクラスになりますが、 'staticContent'はStaticContentControllerを指します。両方のコントローラーによって解決される可能性のあるURLがアプリ内にない限り、2つのルートの順序は実際には重要ではありません。このような場合、最初に一致するルート/コントローラーが要求を処理します。

    StaticContentConstraintは、存在しない静的コンテンツのリクエストの場合に、MVCが「デフォルト」ルートにフォールバックできるようにするためだけにあります(リダイレクトでは処理されません。上記の「タイプ2:ナビゲーションアイテムグループその他のアイテム」の章を参照してください)。 'staticContent'ルート定義から制約を削除すると、アプリは 'default'ルートを介してリクエストを照合しようとせずに、すぐに404を返します。

    BlogControllerクラス

    このクラスを作成して、「記事」コンテンツアイテムのリストをレンダリングするための従来のMVCアプローチを示します。ファセットナビゲーションを紹介します。

    クラスのポイントは、ページ本文データ、ナビゲーションデータをどれだけ迅速に取得できるか、およびビューモデルを介してそれらをどのように表示するかを示すことです。

    public async Task Index(int? year, int? month){ List filters = new List(); filters.AddRange(new IQueryParameter[] { new EqualsFilter('system.type', TYPE_NAME), new DepthParameter(0), new OrderParameter(ELEMENT_NAME) }); string yearString = null; string monthString = null; if (year.HasValue && !month.HasValue) { yearString = $'{year}-01'; monthString = $'{year + 1}-01'; } else if (year.HasValue && month.HasValue) { if (month < 12) { yearString = $'{year}-{GetMonthFormatted(month.Value)}'; monthString = $'{year}-{GetMonthFormatted(month.Value + 1)}'; } else { yearString = $'{year}-12'; monthString = $'{year + 1}-01'; } } if (year.HasValue) { filters.Add(new RangeFilter(ELEMENT_NAME, yearString, monthString)); } var pageBody = await _deliveryClient.GetItemsAsync
    (filters); var navigation = await _menuItemGenerator.GenerateItemsAsync(await _navigationProvider.GetNavigationAsync()); var pageViewModel = new PageViewModel { Navigation = navigation, Body = pageBody.Items }; return View(DEFAULT_VIEW, pageViewModel);}


    StaticContentControllerクラス

    このクラスでは、アプリがコンテンツインベントリでモデル化および構成されたコンテンツアイテムを自動的にレンダリングする方法を紹介します。 MVC表示テンプレートを使用して、SEO対応のURLをページにルーティングして解決します。

    Kentico Kontent


    MVCテンプレートについて少し触れさせてください。一言で言えば、MVCテンプレートは部分ビューと同じですが、命名規則だけでMVCによって自動的にレンダリングされるという利点があり、既知のタイプのデータがビューモデルに表示されるたびに表示されます。型のテンプレートが特定のフォルダー(〜/ Views / Shared / DisplayTemplates / [型名] .cshtmlなど)に存在する限り、MVCでHTMLをレンダリングするために必要なのは、その型のデータを任意のフォルダーに渡すことだけです。ビューモデルとして表示します。そのタイプのデータがモデルの奥深くに隠されているかどうかは関係ありません。 MVCは、とにかくそれをレンダリングするのに十分スマートです。テンプレートは明示的に呼び出すこともできます。これは、Webサイト全体で単一のHTML出力のみで型をレンダリングすることが常に望ましいとは限らないため便利です。代わりに、サイトのさまざまなセクションまたはさまざまなコンテキストでさまざまな出力が必要になる場合があります。私の例では、複数のコンテキストを設定していません。しかし、私がそれをどのように行うかについて興味がある場合は、レイアウトの表示テンプレートでこれらのコンテキストを表示テンプレート名のサフィックスとして確立することで実行できます(たとえば、ContentListing.cshtmlファイルなど)。次に、ContentListing.cshtmlは、名前に「Listing」サフィックスが付いたテンプレートを呼び出すことができます。

    コントローラに戻りましょう。コードはあまりありません。コントローラはかなりシンプルに保つ必要があります。代わりに、コントローラー内からContentResolver.ResolveRelativeUrlPathAsyncメソッドを呼び出すだけです。 そのメソッド呼び出しによって返されるContentResolverResultsに応じて、残りのコントローラーコードは、ブラウザーに返すActionResultの派生タイプを決定します。結果にコンテンツアイテムのコード名が含まれている場合、コントローラーは通常の方法でそのコンテンツを取得し、ViewResultを返します。リダイレクトの場合、RedirectPermanentを返す場合があります。これは、コントローラー自体についてです。

    ContentResolverResultsクラス:

     using System.Collections.Generic;namespace NavigationMenusMvc.Models{ public class ContentResolverResults { public bool Found { get; set; } public IEnumerable ContentItemCodenames { get; set; } public string ViewName { get; set; } public string RedirectUrl { get; set; } }}


    StaticContentControllerクラス:

    public async Task Index(string urlPath){ ContentResolverResults results; try { results = await _contentResolver.ResolveRelativeUrlPathAsync(urlPath); } catch (Exception ex) { return new ContentResult { Content = $'There was an error while resolving the URL. Check if your URL was correct and try again. Details: {ex.Message}', StatusCode = 500 }; } if (results != null) { if (results.Found) { if (results.ContentItemCodenames != null && results.ContentItemCodenames.Any()) { return await RenderViewAsync(results.ContentItemCodenames, results.ViewName); } else if (!string.IsNullOrEmpty(results.RedirectUrl)) { return LocalRedirectPermanent($'/{results.RedirectUrl}'); } } else if (!string.IsNullOrEmpty(results.RedirectUrl)) { return RedirectPermanent(results.RedirectUrl); } } return NotFound();}


    興味深い部分は、ContentResolverヘルパークラスにあります。

    ContentResolverクラス

    このクラスは以下を担当します:

    • ページ本文のレンダリングに必要なコンテンツアイテムのコードネームを見つける
    • またはリダイレクト先の適切なURLを見つける

    ResolveRelativeUrlPathAsyncメソッドは、そのジョブを調整する主要なエントリポイントです。これは、最初に着信HTTPリクエストから取得した相対パスをいくつかのURLスラッグに分割するように機能します。次に、これらのスラッグを(左から右に)繰り返し、キャッシュされた階層内で一致するナビゲーションアイテムを見つけようとします。パス全体の最後のスラッグ(右端のスラッグ)に到達すると、コード名またはリダイレクトパスのいずれかを返そうとします。

    コード自体のコメントから実装を学ぶのが最善だと思います。

     #region 'Public methods'/// /// Resolves the relative URL path into  containing either the codenames of content items, or a redirect URL./// /// The relative URL from the HTTP request/// The . If Found is true and the RedirectUrl isn't empty, then it means a local redirect to a static content URL.public async Task ResolveRelativeUrlPathAsync(string urlPath, string navigationCodeName = null, int? maxDepth = null){ string cn = navigationCodeName ?? _navigationCodename; int d = maxDepth ?? _maxDepth; // Get the 'Navigation' item, ideally with 'depth' set to the actual depth of the menu. var navigationItem = await _navigationProvider.GetNavigationAsync(cn, d); // Strip the trailing slash and split. string[] urlSlugs = NavigationProvider.GetUrlSlugs(urlPath); // Recursively iterate over modular content and match the URL slugs for the each recursion level. return await ProcessUrlLevelAsync(urlSlugs, navigationItem, _rootLevel);}/// /// Gets the codenames of  content items using ./// /// The shallow content items to be fetched again using their codenames/// The codenamespublic static IEnumerable GetContentItemCodenames(IEnumerable contentItems){ if (contentItems == null) { throw new ArgumentNullException(nameof(contentItems)); } var codenames = new List(); foreach (var item in contentItems) { ContentItemSystemAttributes system = item.GetType().GetTypeInfo().GetProperty('System', typeof(ContentItemSystemAttributes)).GetValue(item) as ContentItemSystemAttributes; codenames.Add(system.Codename); } return codenames;}#endregion


    ResolveContentAsyncプライベートメソッドに関する注意:ご存知のように、Navigation Itemコンテンツタイプを使用すると、別のナビゲーションアイテムにリダイレクトでき、次に別のナビゲーションアイテムにもリダイレクトできます。最終的な(最終的な)リダイレクトURLを計算するのはこのメソッドです。

    ContentResolverクラスの処理結果は、単純なContentResolverResultsオブジェクトに再びラップされます。

    結果の規則を設定することにしました。リダイレクトが返された場合、内部の「Found」ブールプロパティは、それが静的コンテンツアイテムへのリダイレクトであるか、それとも他の何か(のような固定相対URLへのリダイレクト)であるかをクライアントコードに通知します。 / blog 'または絶対外部URL)。そうすれば、特定の状況では、コントローラーコードがRedirectPermanentだけでなく、より安全なLocalRedirectPermanentを返す場合があります。

    これで完了です。信じられないかもしれませんが、MVCアプリがKenticoCloudのユーザー編集可能なメニューとユーザー編集可能なページを自動的に表示できるようにするために必要なのはこれだけです。

    コードを取得する

    アプリの完全なソースは、GitHubの記事例の一般的なリポジトリからダウンロードできます。このフォルダーには、サンプルアプリのKenticoCloudプロジェクトのDelivery / PreviewAPIダンプも含まれています。そのファイルを使用すると、コンテンツの構造をすばやく調べることができます。

    概要

    柔軟なナビゲーション構造は、KenticoCloudコンテンツタイプを使用してモデル化できることを説明しました。次に、頻繁に使用される3つのタイプのナビゲーションをこれらのコンテンツタイプで実装する方法を学習しました。最後に、KenticoCloudで設計されたナビゲーションとコンテンツをアプリが自動的に反映できるようにする簡単なMVCコードを示しました。このようなアプローチと設計により、コンテンツストラテジストと開発者の両方がお互いの仕事をする必要がなくなり、最も重要なことに集中できるようになります。

    Headless CMSの導入をお考えでしょうか?

    クラウドとマルチデバイスに最適化されたKentico Kontentをお試しください