鍋綿ブログ

C#・SharePoint・SharePoint Framework・Office365を中心に扱うブログです。

SharePointのモダンサイトをテンプレート化しよう!

SharePointサイトのテンプレート展開は昔からネックでした。

  • 部署毎にポータルを持ちたい
  • 複数あるサイトコレクションに同じ機能を展開したい

などの要望があがると頭を抱えることが多いです。
何故ならSharePointは設定項目が非常に多く、
それらを手動で複数サイトに横展開するのは無理があるからです。

解決策の一つとして、「サイトテンプレート」と「リストテンプレート」の
機能がありますが、これらはモダンサイトでサポートされません。
また、クラシックサイトでも発行機能が有効になっているサイトではサポートされません。

モダンサイトでは、サイトのテンプレート化に対する新しい方法が提供されています。
「サイトデザイン」と「サイトスクリプト」です。

 

 

試してみた結果

まず、テンプレート化するサイトを手動で構築しました。

f:id:micknabewata:20180930100407j:plain

テンプレート化対象サイト

サンプルなのでコンテンツは非常にシンプルです。

f:id:micknabewata:20180930100645j:plain

サイトのページにタグ付けして2つの異なるコンテンツを表現

トップに表示している「お知らせ」と「社長メッセージ」はどちらも「ニュース」Webパーツです。
「サイトのページ」ライブラリに追加した「タグ」列でフィルタすることで
異なる2つのコンテンツを表現しています。
「タグ」列に使用する用語セットもサイトコレクション内に構築しています。
これをテンプレート化して、サイトコレクション作成時に選択できるようにしました。
下記画像の「サイトスクリプト&PnPプロビジョニング検証」がそれです。

f:id:micknabewata:20180930104427j:plain

サイトデザインの選択

SharePointホームからサイトコレクションを作成するか、
SharePoint管理センターの新しいデザインからサイトコレクションを作成すると、
上記画像のようにサイトデザインを選択することが出来ます。
で、言われるがままに進めるとこんなサイトが出来上がります。

f:id:micknabewata:20180930101335j:plain

サイトデザインを使用して構築したサイト

今回は初期データを登録するようにしていないので、
上記のように空っぽのWebパーツが表示されています。
またナビゲーション(左側のメニュー)は挙動が良くないのでテンプレートから除きました。
(追加はできるが削除ができない。既存のリンクが消えないのでうまくない。)

サイトデザインとは

サイトデザインでは、サイト作成時に実行する「サイトスクリプト」を定義します。
また、ユーザーにサイトのプレビュー画像を提供します。
ホントにそれだけの機能です。

f:id:micknabewata:20180930104852j:plain

サイトデザインが定義するのはタイトル、画像、サイトスクリプト

上記画像の赤枠部分と、サイトスクリプトへの紐付けを定義するだけの単純な構造になっています。
作成されるべきサイトの内容を定義するのは、サイトスクリプトの役目です。

サイトスクリプトとは

サイト作成時に実行すべきアクションをJSON形式で記述したものです。
サイト列を作成したり、ロゴを設定したりすることができます。
但し本稿執筆時点では定義できるアクションが少ないので、
運用レベルのテンプレートを作成することは難しいのが現状です。
JSONを手書きで定義するしんどさも考慮すると、
サイトスクリプトから後述の「PnPプロビジョニングエンジン」を
呼び出してサイトに対する操作を行うほうが良いでしょう。

{
  "$schema": "schema.json",
  "actions": [
    {
      "verb": "createSiteColumn",
      "fieldType": "User",
      "internalName": "siteColumn4User",
      "displayName": "Project Owner",
      "isRequired": false
    }
  ],
  "version": 1
}

参考までに、サイト列を追加するための定義を載せておきます。
actionsの中身を増やしていって完成させるわけですが、
全部手書きはしんどいですね。

PnPプロビジョニングエンジンとは

サイトの作成や更新を自動化するための仕組みです。
サイトデザイン・サイトスクリプトとはまた別の機能です。
自動化を目標にしているのでUIが無いのですが、
行える操作がサイトスクリプトよりも豊富で
既存サイトの定義を抽出することもできるためテンプレート化に適しています。
過去の記事で概要を紹介していますので参考にどうぞ。

www.micknabewata.com

既存サイトをテンプレート化する際の作業手順

残念ながらサイトスクリプトから直接PnPプロビジョニングエンジンを
起動することはできません。よって以下の流れで呼び出します。
回りくどいですが公式サイトにも案内がありますのでこれが正かと。。。

 サイトスクリプトからFlowを実行

  ⇒ Flowから自作のREST APIを実行

  ⇒ REST APIはPnPプロビジョニングを実行

1.テンプレートとなるサイトを手動で構築する

これは当然手動ですね。

2.PnPコマンドで1のサイトの定義を抽出する

こちらはPowerShellで行います。
SharePoint Online 管理シェルを端末にインストールして起動し、
以下を実行します。

# 定義ファイルの出力先フォルダに移動します。予めフォルダを作成しておいてください。
cd 'C:\work\PnP\templateSite'

# SharePoint Onlineに接続するアカウントとパスワードを入力します。
$cred = Get-Credential

# SharePoint Onlineサイトに接続します。
Connect-PnPOnline -Url https://yourdomain.sharepoint.com/sites/sample -Credential $cred

# 現在接続中のサイトの定義をファイルに抽出します。
Get-PnPProvisioningTemplate -Out template.xml

赤字部分は環境に合わせて置き換えてください。

3.抽出した定義ファイル(XML)を編集してテストする

抽出した定義ファイルは、SharePointの既定値などを含んでいて見づらいし無駄です。
リファレンスを参考にシェイプアップしたほうが良いでしょう。

編集した後は、以下のコマンドで別のサイトに適用してテストを行いましょう。
(コマンドは2の続きの部分だけ記述しています)

# 適用先のサイトに接続します。(重要)
Connect-PnPOnline -Url https://yourdomain.sharepoint.com/sites/sample2 -Credential $cred

# 現在接続中のサイトに定義ファイルを適用します。
Apply-PnPProvisioningTemplate -Path template.xml

PnPのXML定義が完成したら、それをサイトスクリプトから流せるようにしていきます。

4.3の定義をサイトに適用するREST APIを構築する

今回はAzure Functionsで構築しました。
サーバーレスですしコストも抑えられるので好きです。

Azure FunctionsはVisual Studio 2017経由で構築しました。
拡張機能としてAzure Functions and Web Jobs Toolsが必要です。
言語はC#です。

f:id:micknabewata:20181008093239j:plain

拡張機能がインストールできたら、ファイル > 新規作成 > プロジェクト から
Cloud > Azure Functions のテンプレートを選択します。 

f:id:micknabewata:20181008092607j:plain

Visual Studio 2017でプロジェクトを作成

で、Http Triggerを選択して作成。

f:id:micknabewata:20181008093354j:plain

設定値

C#でSharePointサイトにPnPプロビジョニングテンプレートを適用するには、
SharePointPnPCoreOnlineをNuGet経由でインストールします。
ただAzure Functionsの開発に必要なMicrosoft.NET.Sdk.Functionsパッケージと
利用するNewtonsoft.Jsonのバージョンが違ったので、
明示的にNewtonsoft.Jsonをインストールしました。
警告が出てますが動いています。

f:id:micknabewata:20181008094031j:plain

で、コードを書きます。

using System;
using System.Configuration;
using System.Net;
using System.Net.Http;
using System.Security;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;

using Microsoft.SharePoint.Client;
using OfficeDevPnP.Core.Framework.Provisioning.ObjectHandlers;
using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml;

namespace MySampleFunction
{
    /// <summary>
    /// 関数定義クラス
    /// </summary>
    public static class ApplyPnPTemplate
    {
        /// <summary>
        /// ApplyPnPTemplate関数
        /// </summary>
        /// <param name="req">HTTP要求</param>
        /// <param name="log">ログライター</param>
        /// <returns>HTTP応答</returns>
        [FunctionName("ApplyPnPTemplate")]
        public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, TraceWriter log)
        {
            try
            {
                log.Info("ApplyPnPTemplateを開始します。");

                // AppSettingsからSharePointの認証情報を取得
                string account = ConfigurationManager.AppSettings["SPOAccount"];
                string password = ConfigurationManager.AppSettings["SPOPassword"];
                string azureBlobKey = ConfigurationManager.AppSettings["AzureBlobKey"];
                string azureBlobContainer = ConfigurationManager.AppSettings["AzureBlobContainer"];
                string templateFileName = ConfigurationManager.AppSettings["TemplateFileName"];

                // リクエスト本文から適用先を取得
                dynamic body = await req.Content.ReadAsAsync<object>();
                string webUrl = body?.webUrl;
                if (webUrl == null)
                {
                    return req.CreateErrorResponse(HttpStatusCode.BadRequest, "webUrlパラメータが必要です。");
                }
                else
                {
                    // 適用先をURIに変換
                    Uri applyToUri = new Uri(webUrl);
                    log.Info($"webUrl:{ applyToUri.AbsoluteUri }");
                    
                    // テンプレートを適用
                    Apply(applyToUri, account, password, azureBlobKey, azureBlobContainer, templateFileName, log);

                    // 返却
                    return req.CreateResponse(HttpStatusCode.OK, "成功");
                }
            }
            catch(Exception ex)
            {
                return req.CreateErrorResponse(HttpStatusCode.InternalServerError, ex.Message);
            }
        }

        /// <summary>
        /// テンプレートを適用
        /// </summary>
        /// <param name="webUrl">適用先URI</param>
        /// <param name="account">ログインアカウント</param>
        /// <param name="password">ログインパスワード</param>
        /// <param name="log">ログライター</param>
        private static void Apply(Uri webUrl, string account, string password, string azureBlobKey, string azureBlobContainer, string templateFileName, TraceWriter log)
        {
            log.Info($"適用開始");
            using (ClientContext ctx = new ClientContext(webUrl))
            {
                log.Info($"認証情報作成");
                SecureString securePassword = new SecureString();
                foreach (char c in password.ToCharArray()) securePassword.AppendChar(c);
                ctx.Credentials = new SharePointOnlineCredentials(account, securePassword);

                log.Info($"テンプレート取得");
                var provider = new XMLAzureStorageTemplateProvider(azureBlobKey, azureBlobContainer);
                var template = provider.GetTemplate(templateFileName);
                log.Info($"リスト数:{ template.Lists?.Count }");
                foreach (var list in template.Lists)
                {
                    log.Info(list.Title);
                }

                log.Info($"テンプレート適用情報作成");
                var templateInfo = new ProvisioningTemplateApplyingInformation();
                
                templateInfo.ProgressDelegate = delegate (String message, Int32 progress, Int32 total)
                {
                    log.Info($"{progress}/{total} - {message}");
                };

                log.Info($"実行");
                Web web = ctx.Web;
                web.ApplyProvisioningTemplate(template, templateInfo);
            }
            log.Info($"適用終了");
        }
    }
}

ご覧の通り、今回はPnPプロビジョニングテンプレートの定義を
AzureStorageから取得しています。

Blobストレージを作ってAzure Storage Explorerで3の定義ファイルを入れました。

f:id:micknabewata:20181008094718j:plain

コードが書けたら発行します。
Visual Studioからプロジェクトを右クリック > 発行
とやって言われるままに発行プロファイルも作りました。
予めAzureポータルでサービスを構築しなくても、
発行の過程でやってくれますので楽ちんです。

f:id:micknabewata:20181008094952j:plain

発行中にAzure Functionsのサービスも作ってくれる

さてAzureポータルを確認します。
すべてのリソースの画面で無事存在を確認できました。

f:id:micknabewata:20181008095239j:plain

Azure FunctionsはApp Service上にWebアプリをホストする

App Serviceになっているリソースをクリックして設定画面を開きます。
更にプラットフォーム機能 > アプリケーション設定をクリックして
アプリケーションの設定値(Web.config的なもの)を登録します。

f:id:micknabewata:20181008095513j:plain

設定画面

f:id:micknabewata:20181008095916j:plain

アプリケーション設定

今回のサンプルソースでは以下が必要です。

  • SPOAccount

SharePoint接続アカウント。
例) user@contoso.com

  • SPOPassword

SharePoint接続アカウントのパスワード。

  • AzureBlobKey

上記で作成したAzure Storageの接続文字列。
(Azureポータル上で確認可能)

  • AzureBlobContainer

上記で作成したAzure Storage内のBlobコンテナ名。
(画像ではpnpprovisionningtemplate)

  • TemplateFileName

Azure Blobコンテナから取得するファイル名。
(画像ではtemplate.xml)

アプリケーション設定まで出来たら、Azureポータル上でテスト実行します。
テンプレート適用先のSharePointサイトを用意してwebUrlパラメータに指定してください。

f:id:micknabewata:20181008101016j:plain

適当なSharePointサイトに適用するテストを行う

5.4のREST APIを実行するFlowを構築する

Flowを一から作成して要求 > HTTP 要求の受信時のトリガを置きます。
メソッドはPOSTで要求本文のJSONスキーマは以下です。

{
    "type": "object",
    "properties": {
        "webUrl": {
            "type": "string"
        },
        "parameters": {
            "type": "object",
            "properties": {
                "key": {
                    "type": "string"
                }
            }
        },
        "webDescription": {
            "type": "string"
        },
        "creatorName": {
            "type": "string"
        },
        "creatorEmail": {
            "type": "string"
        },
        "createdTimeUTC": {
            "type": "string"
        }
    }
}

このFlowを実行するには、要求本文にkeyを渡す必要があるという定義です。
他にwebUrlなんかが定義されていますが、
これはサイトスクリプトからFlowを実行した時に受け取ることができる情報です。
サイトのURLなんかが動的に判断できるようにということですね。
以下の情報を受け取ることができます。

 webUrl:作成されたサイトのURL

 webDescription:作成されたサイトの説明

 creatorName:作成者表示名

 creatorEmail:作成者メールアドレス

 createdTimeUTC:作成日時(UTC)

で、トリガの次にHTTP > HTTPアクションを置いて4のREST APIを呼び出せば完成です。

f:id:micknabewata:20181008102040j:plain

4のREST APIのURLはAzureポータル上で確認できます。

6.5のFlowを実行するサイトスクリプトを登録する

適当なローカルフォルダにテキストファイルを作成します。

{
     "$schema": "schema.json",
     "actions": [
         {
             "verb": "triggerFlow",
             "url": "(FlowのURL)",
             "name": "Run PnP",
             "parameters": {
                   key:"(Azure Functionsのキー)"
              }
         }
     ]
 }

FlowのURLは、5のFlowを保存すればFlowの編集画面からコピーできるようになります。
Azure FunctionsのキーはAzureポータル上で確認できます。

テキストファイルが出来たらSharePoint Online管理シェルでサイトスクリプトを登録します。

cd (テキストファイルを保存したフォルダ)

$script = Get-Content -LiteralPath .\(テキストファイル名) -Raw

$userName = "sample@contoso.onmicrosoft.com"
$cred = Get-Credential -UserName $userName -Message "パスワードを入力してください"
Connect-SPOService -Url https://contoso-admin.sharepoint.com -Credential $cred

Add-SPOSiteScript -Title "サイトスクリプト&PnPプロビジョニング検証" -Content $script -Description "検証用"

赤字部分は環境に合わせて変更してください。

7.6のサイトスクリプトを実行するサイトデザインを登録する

6の続きのSharePoint Online管理シェルで以下を実行します。

Add-SPOSiteDesign -Title "サイトスクリプト&PnPプロビジョニング検証" -WebTemplate "68" -SiteScripts "f5fccd6c-dd6a-41dc-af30-bfb2916af077" -Description "検証用"

赤字部分はサイトスクリプトのIDです。
6のコマンド実行結果から取得できますので置き換えてください。

また、WebTemplateの「68」は「コミュニケーションサイト」です。
「64」は「チームサイト」です。
現状ではこの2種類しかありません。

8.テスト

長かった。。。適当なSharePointサイトを作成してみてください。

f:id:micknabewata:20181008103623j:plain

サイトデザインを選択して作成する

作成中にサイトスクリプトが実行されます。
Flowの実行結果を待たずに成功してしまいますが裏ではFlowの実行が続いています。

f:id:micknabewata:20181008103934j:plain

サイトスクリプトが動作した結果

しばらく待つとFlowの実行が終わりました。
SharePointサイトに結果が反映されています。

f:id:micknabewata:20181008112213j:plain

無事テンプレートが適用された

今回使ったコマンドの全文

 GitHubに公開しました。

github.com