<#
Windows PowerShell 5.1 / PnP.PowerShell 1.12.0向け修正版。接続済みセッションで実行: .\New-KumePortal.ps1
通常実行は対象サイトのテーマを変更し、新しい下書きとバナーを作成します。
-ThemeOnly は配色のみ変更。テーマは対象サイトの既存ページにも反映されます。
ナビ・権限・Power Apps本体は変更しません。-SkipTheme で配色変更を省略できます。
編集箇所: 冒頭の設定欄(param)と $cards。独自バナーは -BannerPath でPNG/JPEGを指定。
標準テーマ対応箇所に黒紺・シアン・白を適用します。任意CSSは使用しません。
UTF-8(BOM付き)で保存してください。実テナントでは未実行。
会社側のキャンバスアプリのWebリンクを使用。アプリの公開・共有・データ接続は別途必要です。
標準の埋め込みWebパーツを使用します。埋め込み制限がある場合は起動リンクを利用できます。
#>
[CmdletBinding()]
param(
# ===== 設定欄:引用符の中を書き換えてください =====
# 【必須】対象サイトのURL。/SitePages/...aspx は含めません。
# 例: https://yourtenant.sharepoint.com/sites/your-site
[string]$SiteUrl='ここを対象サイトのURLに置き換えてください',
# 表示名(ページタイトルと自動生成バナーに反映)
[string]$PageTitle='dorago01',
# 配色だけを既存サイトへ適用する場合: .\New-KumePortal.ps1 -ThemeOnly
[switch]$ThemeOnly,
# ページだけ作成し、配色を変更しない場合: -SkipTheme
[switch]$SkipTheme,
# 会社側のキャンバスアプリの「詳細」→「Webリンク」を貼り付けます。
# 空欄なら掲示板の埋め込みを省略します。編集画面のURLは指定しません。
[string]$PowerAppUrl='',
# おみくじPower AppsのWebリンク(未設定でも紹介文は表示します)
[string]$OmikujiAppUrl='',
# おみくじをページ内にも表示する場合は -EmbedOmikuji を付けて実行
[switch]$EmbedOmikuji,
[ValidateRange(300,2000)]
[int]$PowerAppHeight=720,
# ===== 以下は通常、変更不要です =====
[ValidatePattern('^[A-Za-z0-9_-]+$')]
[string]$PageName=('Portal-Draft-'+(Get-Date -Format 'yyyyMMdd-HHmmss')),
[string]$BannerPath,
[switch]$SkipPowerApps
)
$ErrorActionPreference='Stop'
if ($ThemeOnly -and $SkipTheme) { throw '-ThemeOnly と -SkipTheme は同時に指定できません。' }
$SiteUrl=$SiteUrl.Trim().TrimEnd('/')
$targetUri=$null
if (-not [Uri]::TryCreate($SiteUrl,[UriKind]::Absolute,[ref]$targetUri) -or
$targetUri.Scheme -ne 'https' -or $targetUri.AbsolutePath -match '/SitePages/' -or
$targetUri.Query -or $targetUri.Fragment) {
throw '冒頭の $SiteUrl に対象サイトのHTTPS URLを設定してください。ページURLではなく、/sites/サイト名 までのURLを指定します。'
}
$PowerAppUrl=$PowerAppUrl.Trim()
$OmikujiAppUrl=$OmikujiAppUrl.Trim()
foreach ($appSetting in @(@{Name='PowerAppUrl'; Url=$PowerAppUrl},@{Name='OmikujiAppUrl'; Url=$OmikujiAppUrl})) {
if ($appSetting.Url) {
$appUri=$null
if (-not [Uri]::TryCreate($appSetting.Url,[UriKind]::Absolute,[ref]$appUri) -or
$appUri.Scheme -ne 'https' -or $appUri.Host -notin @('apps.powerapps.com','web.powerapps.com') -or
$appUri.AbsolutePath -notmatch '^/(play|apps)(/|$)') {
throw "$($appSetting.Name) にキャンバスアプリのWebリンクを貼り付けてください(apps.powerapps.com/play/...)。"
}
}
}
# ===== 自己紹介:自由に書き換えられます =====
$AboutText='こんにちは、dorago01です。普段はセキュリティクラウドを担当しています。仕事の外でもWebサイトを作ったり、自宅でサーバーを動かしたりしています。「これ、作れそう」と思うと、つい試してみたくなるタイプです。'
$WorkText='セキュリティクラウドに関わる業務に取り組んでいます。日々の気づきや学びを整理し、わかりやすく共有できる形にしていきたいと思っています。ネットワークやセキュリティに加えて、手作業を減らす仕組みづくりにも関心があります。'
$MakingText='個人でもいろいろなWebサイトを作っています。見た目を考えるのも、裏側の仕組みを動かすのも好きです。最近はPower AppsやSharePointも触りながら、掲示板やおみくじなど、実際に使ってもらえるものを作っています。'
$PersonalText='思い立ったら、まずやってみる。そんな勢いで、気になった技術や創作を少しずつ試しています。うまく動いたときはもちろん、つまずいて原因がわかったときも楽しいです。このページでは、仕事の担当だけでは伝わらない普段の自分も紹介していきます。'
$Notes=@(
@{Title='自宅も、ちょっとした検証場所'; Body='自宅ではサーバーやネットワーク機器を触っています。仮想環境を作ったり、Webサービスを動かしたり。仕組みを読んで終わりにせず、自分の手元で試してみるのが好きです。'},
@{Title='技術のほかに、物語も作ります'; Body='小説を書いたり、AIを使った音楽づくりを試したりしています。システムを作るときとはまた違う難しさがありますが、頭の中にあったものが形になるところは共通していて、どちらも楽しんでいます。'},
@{Title='AIと一緒に、試行錯誤'; Body='AIには文章やコードの相談をしながら、アイデアを実際に動く形にしています。最近はPower Appsもコードから組み立てられることを知り、できることが増える面白さを感じています。'},
@{Title='ちょっとした遊び心も'; Body='便利なツールだけでなく、気軽に楽しめるものも作ってみたくて、Power Appsでおみくじを作りました。作ったものをきっかけに、会話が生まれたらうれしいです。'},
@{Title='気軽に話しかけてください'; Body='サイト制作、サーバー、ネットワーク、AI、創作など、気になる話題があればぜひ。掲示板への感想や「こんなものを作ってみたい」という話も歓迎です。'}
)
$OmikujiText='Power Appsで、おみくじアプリを作りました。仕事の合間の気分転換に、気軽にのぞいてもらえたらうれしいです。使ってみた感想も、ぜひ掲示板で教えてください。'
# 接続済みの旧版を使い、PowerShell 7専用の新版を自動選択しないよう固定します。
Import-Module PnP.PowerShell -RequiredVersion 1.12.0 -ErrorAction Stop
# 作成前に、読み込まれたPnPのコマンド・パラメーターを確認します。
$requiredCommands = @{
'Get-PnPConnection' = @()
'Get-PnPWeb' = @('Includes','Connection')
'Add-PnPPage' = @('Name','LayoutType','Connection')
'Set-PnPPage' = @('Identity','Title','Connection')
'Add-PnPFile' = @('Path','Folder','NewFileName','Connection')
'Add-PnPPageSection' = @('Page','SectionTemplate','Order','ZoneEmphasis','Connection')
'Add-PnPPageTextPart' = @('Page','Section','Column','Order','Text','Connection')
'Add-PnPPageWebPart' = @('Page','DefaultWebPartType','WebPartProperties','Section','Column','Order','Connection')
'Get-PnPPage' = @('Identity','Connection')
}
foreach ($entry in $requiredCommands.GetEnumerator()) {
$cmd = Get-Command $entry.Key -ErrorAction Stop
foreach ($parameterName in $entry.Value) {
if (-not $cmd.Parameters.ContainsKey($parameterName)) {
throw "$($entry.Key) に -$parameterName がありません。ページはまだ作成していません。このエラーを共有してください。"
}
}
}
$imageCommand = Get-Command Add-PnPPageImageWebPart -ErrorAction SilentlyContinue
$canUseImageCommand = $null -ne $imageCommand
if ($canUseImageCommand) {
foreach ($parameterName in @('Page','Section','Column','Order','ImageUrl','ImageWidth','ImageHeight','AlternativeText','Connection')) {
if (-not $imageCommand.Parameters.ContainsKey($parameterName)) { $canUseImageCommand = $false }
}
}
$connection=Get-PnPConnection -ErrorAction Stop
if (-not $connection) { throw 'Connect-PnPOnlineで接続してください。' }
$web=Get-PnPWeb -Includes Url -Connection $connection
if ($web.Url.TrimEnd('/') -ine $SiteUrl.TrimEnd('/')) { throw "接続先が違います: $($web.Url)" }
$SiteUrl=$web.Url.TrimEnd('/')
# Microsoft公開のApplyTheme APIで、接続済みの対象サイトだけに適用します。
# https://learn.microsoft.com/sharepoint/dev/declarative-customization/site-theming/sharepoint-site-theming-rest-api
# テナント共通テーマの登録や権限設定の変更は行いません。
$themeApplied=$false
if (-not $SkipTheme) {
$themeCommand=Get-Command Invoke-PnPSPRestMethod -ErrorAction Stop
foreach ($parameterName in @('Url','Method','Content','ContentType','Connection','Raw')) {
if (-not $themeCommand.Parameters.ContainsKey($parameterName)) {
throw "Invoke-PnPSPRestMethod に -$parameterName がありません。テーマとページはまだ変更していません。"
}
}
# ダークテーマでは white が背景、neutralPrimary が本文色になります。
$palette=@{
themePrimary='#28DCDC'; themeLighterAlt='#0D1D22'; themeLighter='#103438'
themeLight='#155257'; themeTertiary='#208F94'; themeSecondary='#25C2C6'
themeDarkAlt='#42E0E0'; themeDark='#69E8E8'; themeDarker='#9BF0F0'
neutralLighterAlt='#101820'; neutralLighter='#142029'; neutralLight='#1B2D35'
neutralQuaternaryAlt='#29404A'; neutralQuaternary='#38515C'; neutralTertiaryAlt='#53717D'
neutralTertiary='#92A8B2'; neutralSecondaryAlt='#AEC1C9'; neutralSecondary='#C6D7DE'
neutralPrimaryAlt='#D9E7EC'; neutralPrimary='#EBF5F8'; neutralDark='#F4FAFC'
black='#FFFFFF'; white='#0C1218'; primaryBackground='#0C1218'; primaryText='#EBF5F8'
bodyBackground='#0C1218'; bodyText='#EBF5F8'; disabledBackground='#142029'; disabledText='#92A8B2'
error='#FF8A8A'; accent='#28DCDC'
}
$themeJson=@{isInverted=$true; palette=$palette} | ConvertTo-Json -Depth 5 -Compress
$themeBody=@{name='dorago01 Dark Cyan'; themeJson=$themeJson} | ConvertTo-Json -Depth 6 -Compress
# PnP 1.12.0 のオブジェクト変換を避けるため、新しい.NET文字列として渡します。
$themeBody=[string]::Concat('', [string]$themeBody)
# 送信前にJSONの形式と二重構造を検証します。
$themePayloadCheck=ConvertFrom-Json -InputObject $themeBody -ErrorAction Stop
$null=ConvertFrom-Json -InputObject $themePayloadCheck.themeJson -ErrorAction Stop
Write-Host "サイト配色を適用中: $SiteUrl(既存ページにも反映)" -ForegroundColor Cyan
try {
$themeRaw=Invoke-PnPSPRestMethod -Url "$SiteUrl/_api/thememanager/ApplyTheme" -Method Post -Content ($themeBody.ToString()) -ContentType 'application/json; charset=utf-8' -Raw -Connection $connection -ErrorAction Stop
# 応答もPnP内蔵のオブジェクト変換を使わず、PowerShell標準で解析します。
$themeResponse=$null
if (-not [string]::IsNullOrWhiteSpace([string]$themeRaw)) {
$themeResponse=ConvertFrom-Json -InputObject ([string]$themeRaw) -ErrorAction Stop
}
if ($themeResponse -and $themeResponse.PSObject.Properties['error']) {
throw ($themeResponse.error | ConvertTo-Json -Depth 5 -Compress)
}
if (($themeResponse -is [bool] -and -not $themeResponse) -or
($themeResponse -and $themeResponse.PSObject.Properties['value'] -and $themeResponse.value -eq $false)) {
throw 'ApplyTheme が失敗を返しました。'
}
$themeApplied=$true
Write-Host '配色の適用要求が完了しました。ブラウザを再読み込みして確認してください。' -ForegroundColor Green
} catch {
throw "テーマの適用に失敗しました。ページ作成は開始していません。テーマが反映済みかはブラウザでもご確認ください。詳細: $($_.Exception.Message)"
}
}
if ($ThemeOnly) {
[pscustomobject]@{SiteUrl=$SiteUrl; Theme='dorago01 Dark Cyan'; ThemeApplied=$themeApplied; PageCreated=$false}
return
}
$pageFile="$PageName.aspx"
$pageUrl="$SiteUrl/SitePages/$pageFile"
# 個人サイトの紹介文は、それぞれの用途に合わせて編集してください。
$cards=@(
@{Heading='01 / PERSONAL SITE'; Title='dorago01.xyz'; Description='個人で制作しているサイト。'; Url='https://dorago01.xyz'; Label='サイトを開く'},
@{Heading='02 / PERSONAL SITE'; Title='dorago01.net'; Description='個人で制作しているサイト。'; Url='https://dorago01.net'; Label='サイトを開く'},
@{Heading='03 / PERSONAL SITE'; Title='dorago01.com'; Description='個人で制作しているサイト。'; Url='https://dorago01.com'; Label='サイトを開く'}
)
function Encode-Html([string]$Value) { [System.Net.WebUtility]::HtmlEncode($Value) }
function New-TextBlock([string]$Heading,[string]$Body) {
'<h2>'+(Encode-Html $Heading)+'</h2><p>'+((Encode-Html $Body) -replace '\r?\n','<br>')+'</p>'
}
# 同名ページがある場合、Add-PnPPageは上書きせず停止します。
Add-PnPPage -Name $PageName -LayoutType Home -Connection $connection | Out-Null
Write-Host "作成中: $pageUrl" -ForegroundColor Cyan
try {
Set-PnPPage -Identity $pageFile -Title $PageTitle -Connection $connection | Out-Null
$tempBanner=$null
try {
if ($BannerPath) {
$imagePath=(Resolve-Path -LiteralPath $BannerPath).Path
if ([IO.Path]::GetExtension($imagePath) -notmatch '^\.(png|jpg|jpeg)$') { throw 'PNG/JPEGを指定してください。' }
} else {
# バナーをコードで生成。表示名を変えると画像内の文字も変わります。
Add-Type -AssemblyName System.Drawing
$tempBanner=Join-Path ([IO.Path]::GetTempPath()) ('dorago01-'+[guid]::NewGuid().ToString('N')+'.png')
$bitmap=[System.Drawing.Bitmap]::new(1600,440)
$graphics=$null; $gridPen=$null; $accent=$null; $white=$null; $font=$null; $smallFont=$null
try {
$graphics=[System.Drawing.Graphics]::FromImage($bitmap)
$graphics.Clear([System.Drawing.Color]::FromArgb(12,18,24))
$graphics.TextRenderingHint=[System.Drawing.Text.TextRenderingHint]::AntiAliasGridFit
$gridPen=[System.Drawing.Pen]::new([System.Drawing.Color]::FromArgb(27,45,53))
for ($x=0; $x -lt 1600; $x+=48) { $graphics.DrawLine($gridPen,$x,0,$x,440) }
for ($y=0; $y -lt 440; $y+=48) { $graphics.DrawLine($gridPen,0,$y,1600,$y) }
$accent=[System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(40,220,220))
$white=[System.Drawing.SolidBrush]::new([System.Drawing.Color]::FromArgb(235,245,248))
$font=[System.Drawing.Font]::new('Segoe UI',64,[System.Drawing.FontStyle]::Bold)
$smallFont=[System.Drawing.Font]::new('Segoe UI',22)
$graphics.FillRectangle($accent,80,86,8,258)
$graphics.DrawString($PageTitle,$font,$white,[single]120,[single]110)
$graphics.DrawString('CONNECT. SHARE. CREATE.',$smallFont,$accent,[single]126,[single]260)
$bitmap.Save($tempBanner,[System.Drawing.Imaging.ImageFormat]::Png)
} finally {
foreach ($resource in @($smallFont,$font,$white,$accent,$gridPen,$graphics,$bitmap)) {
if ($null -ne $resource) { $resource.Dispose() }
}
}
$imagePath=$tempBanner
}
$assetName=$PageName+'-'+[guid]::NewGuid().ToString('N').Substring(0,8)+[IO.Path]::GetExtension($imagePath)
$asset=Add-PnPFile -Path $imagePath -Folder 'SiteAssets/PortalDraft' -NewFileName $assetName -Connection $connection
$imageUrl=$asset.ServerRelativeUrl
if (-not $imageUrl) { throw '画像URLを取得できませんでした。' }
} finally {
if ($tempBanner -and (Test-Path -LiteralPath $tempBanner)) { Remove-Item -LiteralPath $tempBanner }
}
Add-PnPPageSection -Page $pageFile -SectionTemplate OneColumn -Order 1 -Connection $connection | Out-Null
if ($canUseImageCommand) {
Add-PnPPageImageWebPart -Page $pageFile -Section 1 -Column 1 -Order 1 -ImageUrl $imageUrl -ImageWidth 1600 -ImageHeight 440 -AlternativeText ($PageTitle+': CONNECT. SHARE. CREATE.') -Connection $connection | Out-Null
} else {
$imageProperties=@{imageSource=$imageUrl; altText=($PageTitle+': CONNECT. SHARE. CREATE.'); imgWidth=1600; imgHeight=440}
Add-PnPPageWebPart -Page $pageFile -DefaultWebPartType Image -WebPartProperties $imageProperties -Section 1 -Column 1 -Order 1 -Connection $connection | Out-Null
}
Add-PnPPageTextPart -Page $pageFile -Section 1 -Column 1 -Order 2 -Text (New-TextBlock 'ABOUT / 自己紹介' $AboutText) -Connection $connection | Out-Null
Add-PnPPageSection -Page $pageFile -SectionTemplate TwoColumn -Order 2 -ZoneEmphasis 1 -Connection $connection | Out-Null
Add-PnPPageTextPart -Page $pageFile -Section 2 -Column 1 -Order 1 -Text (New-TextBlock 'WORK / 普段の担当' $WorkText) -Connection $connection | Out-Null
Add-PnPPageTextPart -Page $pageFile -Section 2 -Column 2 -Order 1 -Text (New-TextBlock 'MAKING / サイトを作る' $MakingText) -Connection $connection | Out-Null
Add-PnPPageSection -Page $pageFile -SectionTemplate ThreeColumn -Order 3 -Connection $connection | Out-Null
for ($i=0; $i -lt $cards.Count; $i++) {
$card=$cards[$i]
$html='<p><strong>'+(Encode-Html $card.Heading)+'</strong></p><h2>'+(Encode-Html $card.Title)+'</h2><p>'+(Encode-Html $card.Description)+'</p><p><a href="'+(Encode-Html $card.Url)+'" target="_blank" rel="noopener noreferrer">'+(Encode-Html $card.Label)+' →</a></p>'
Add-PnPPageTextPart -Page $pageFile -Section 3 -Column ($i+1) -Order 1 -Text $html -Connection $connection | Out-Null
}
Add-PnPPageSection -Page $pageFile -SectionTemplate OneColumn -Order 4 -ZoneEmphasis 1 -Connection $connection | Out-Null
Add-PnPPageTextPart -Page $pageFile -Section 4 -Column 1 -Order 1 -Text (New-TextBlock 'PERSONAL / ひとこと' $PersonalText) -Connection $connection | Out-Null
$noteOrder=2
foreach ($note in $Notes) {
Add-PnPPageTextPart -Page $pageFile -Section 4 -Column 1 -Order $noteOrder -Text (New-TextBlock $note.Title $note.Body) -Connection $connection | Out-Null
$noteOrder++
}
Add-PnPPageSection -Page $pageFile -SectionTemplate OneColumn -Order 5 -Connection $connection | Out-Null
$heading='<h2>COMMUNITY / 掲示板</h2><p>質問や雑談、サイトへの感想など、気軽にどうぞ。</p>'
if ($PowerAppUrl) {
$heading+='<p><a href="'+(Encode-Html $PowerAppUrl)+'" target="_blank" rel="noopener noreferrer">掲示板を別画面で開く →</a></p>'
} else {
$heading+='<p>掲示板は準備中です。</p>'
}
Add-PnPPageTextPart -Page $pageFile -Section 5 -Column 1 -Order 1 -Text $heading -Connection $connection | Out-Null
$embedded=$false
if ($PowerAppUrl -and -not $SkipPowerApps) {
try {
# 標準「埋め込み」Webパーツを使用。元ページの設定コピーは不要です。
# サイトの埋め込み許可設定は変更しません。ブラウザで表示確認してください。
$iframe='<iframe src="'+(Encode-Html $PowerAppUrl)+'" width="1200" height="'+$PowerAppHeight+'" frameborder="0" title="掲示板"></iframe>'
$properties=@{embedCode=$iframe; cachedEmbedCode=$iframe; shouldScaleWidth=$true}
Add-PnPPageWebPart -Page $pageFile -DefaultWebPartType ContentEmbed -WebPartProperties $properties -Section 5 -Column 1 -Order 2 -Connection $connection | Out-Null
$embedded=$true
} catch { Write-Warning "埋め込みパーツを追加できませんでした。別画面で開くリンクを利用できます: $($_.Exception.Message)" }
}
Add-PnPPageSection -Page $pageFile -SectionTemplate OneColumn -Order 6 -ZoneEmphasis 1 -Connection $connection | Out-Null
$fortuneHtml=New-TextBlock 'PLAY / おみくじアプリ' $OmikujiText
if ($OmikujiAppUrl) {
$fortuneHtml+='<p><a href="'+(Encode-Html $OmikujiAppUrl)+'" target="_blank" rel="noopener noreferrer">おみくじを開く →</a></p>'
} else {
$fortuneHtml+='<p>アプリのリンクは準備中です。</p>'
}
Add-PnPPageTextPart -Page $pageFile -Section 6 -Column 1 -Order 1 -Text $fortuneHtml -Connection $connection | Out-Null
$omikujiEmbedded=$false
if ($OmikujiAppUrl -and $EmbedOmikuji -and -not $SkipPowerApps) {
try {
$fortuneFrame='<iframe src="'+(Encode-Html $OmikujiAppUrl)+'" width="1200" height="'+$PowerAppHeight+'" frameborder="0" title="おみくじアプリ"></iframe>'
$fortuneProperties=@{embedCode=$fortuneFrame; cachedEmbedCode=$fortuneFrame; shouldScaleWidth=$true}
Add-PnPPageWebPart -Page $pageFile -DefaultWebPartType ContentEmbed -WebPartProperties $fortuneProperties -Section 6 -Column 1 -Order 2 -Connection $connection | Out-Null
$omikujiEmbedded=$true
} catch { Write-Warning "おみくじの埋め込みを追加できませんでした。起動リンクを利用できます: $($_.Exception.Message)" }
}
$result=Get-PnPPage -Identity $pageFile -Connection $connection
Write-Host "下書き作成完了: $pageUrl" -ForegroundColor Green
Write-Host "埋め込みパーツ追加: $embedded / パーツ数: $($result.Controls.Count)"
Write-Host '追加成功はアプリの表示成功を保証しません。ブラウザでサインインと表示を確認してください。'
Write-Host 'ブラウザで確認して編集してください。公開とホーム切替は行っていません。'
[pscustomobject]@{PageUrl=$pageUrl; PageName=$pageFile; PowerAppsEmbedAdded=$embedded; OmikujiEmbedAdded=$omikujiEmbedded; ThemeApplied=$themeApplied; Published=$false}
} catch {
Write-Warning "途中まで作成された下書き: $pageUrl"
if ($themeApplied) { Write-Warning 'サイトの配色は適用済みです。ページ作成の失敗では元に戻りません。' }
Write-Warning '再実行すると別名の下書きが作られます。不要な下書きは確認後に削除してください。'
throw
}