SSG (Astro) で作成した静的サイトを SharePoint で公開する

この記事は[NTTコムウェア Advent Calendar 2024](https://qiita.com/advent-calendar/2024/nttcomware)の14日目の記事です。

# はじめに
こんにちは、NTTコムウェアの北山です。普段の業務では Azure などの Microsoft のサービスを用いた開発や技術的なサポートを行いつつ、業務外ではプレミアリーグのサッカーチーム (スパーズ) を応援しています。

SSG (Static Site Generation) とは、Web サイトを作成する方法の一つです。事前に、静的な HTML ファイルをビルドしてサーバーに配置しておき、それをクライアントへ渡します。
これをサーバーではなく SharePoint へ配置して、Web サイトを公開する方法を紹介します。

# SSG (Static Site Generation)

SSG の詳細については、別の記事や Web サイトに説明を譲りますが、本記事でも簡単に説明します。

前述した通り、SSG (Static Site Generation) とは、Web サイトを作成する方法の一つです。事前に、静的な HTML ファイルをビルドしてサーバーに配置しておき、それをクライアントへ渡します。
SSG の一つである [Astro](https://astro.build/) を例に挙げると、[2023 JavaScript Rising Stars - Back-end/Full-stack](https://risingstars.js.org/2023/en#section-nodejs-framework) で Next.js に次いで 2 位となっており、2024 年 12 月現在の GitHub の star 数が 47.4k と、引き続き注目されていることが分かります。

SSG の特徴は下記の通りです。

- **高速なページ表示**: ビルド時にあらかじめ HTML ファイルを生成するため、ユーザーがページを開く際にサーバーから新たに HTML を生成する必要がなく、表示が非常に速くなります
- **サーバー負荷の軽減**: 静的ファイルを配信するだけなので、サーバーの負荷が軽減されます
- **SEO 対策**: 静的なHTMLファイルは検索エンジンのクローラーにとって読み取りやすく、インデックスされやすいため、SEO 対策としても効果的です
- **セキュリティの向上**: 動的な処理を含まないため、サーバーサイドの脆弱性を突かれるリスクが低くなります
- **動的コンテンツは扱えない**: HTML ファイルはビルド時に生成されるため、リアルタイムのデータ変更を反映することができません

上記の特徴を持つため、SSG は、ブログやドキュメントサイトなど、更新頻度が低いページに適しています。

また、SharePoint のフォーマットでサイトを作成することと比べると、Web サイトを柔軟に作成しやすいことや、自分の好みや組織の目的に合ったテーマやフォーマットを選択しやすいことが挙げられます。
例えば、SSG の 1 つである [Astro](https://astro.build/) では、[テーマ](https://astro.build/themes/)が公開されています。「ブログ」「ドキュメントサイト」「ランディングページ」「ポートフォリオ」などのカテゴリがあり、そのカテゴリの中で様々なデザインのテーマが公開されています。

# SharePoint

SharePoint についても、簡単に説明します。
SharePoint は、Microsoft が提供する企業向けのコラボレーションおよび情報共有ツールです。

SharePoint の特徴は下記の通りです。

- **ファイル共有と管理**: ドキュメントや画像をクラウド上に保存し、社内外からアクセス可能です
- **チームサイトの作成**: 部門やプロジェクトごとに専用のサイトを作成し、情報を整理・共有できます
- **ワークフローの自動化**: 業務手続きを自動化し、効率化を図ることができます
- **セキュリティ機能**: データのアクセス権限を細かく設定でき、情報の安全性を確保します

本記事では、ファイル共有と管理の機能を用いて、SSG で作成した静的な HTML ファイルへアクセスします。

# 静的サイトを SharePoint で公開する

さて、本題に入ります。

SSG として [Astro](https://astro.build/) を用います。また、Astro で公開されている[テーマ](https://astro.build/themes/)を利用します。ここでは、ドキュメントサイトを作ることに特化したテーマである [Starlight](https://astro.build/themes/details/starlight/) を用います。

![20241209_2101_msedge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/059ef349-a8b5-e844-c44d-75b85a682e63.png)
`出典：Astro Contributors「入門 | Starlight」
https://starlight.astro.build/ja/getting-started/（2024.12.13）`

:::note info
Astro や Starlight の導入方法は下記などのドキュメントを参照ください。
- [はじめに | Astro Docs](https://docs.astro.build/ja/getting-started/)
- [Starlight 🌟 Astroでドキュメントサイトを作る](https://starlight.astro.build/ja/)

なお、[Starlight 🌟 Astroでドキュメントサイトを作る](https://starlight.astro.build/ja/)は Starlight を用いて作成された Web サイトであるため、どのような Web サイトを作成できるか参考となります。 
:::

本記事では、Starlight で静的な HTML ファイルが作成できる環境を導入できていることを前提とします。また、Astro や Starlight、SharePoint は数多くの設定があるため、ここで紹介した方法以外にも適した方法がある可能性があります。

なお、Astro の他のテーマを用いた場合や、他の SSG を用いた場合でも、ここで紹介する手順のいくつかは有効と考えられます。ただし、テーマや SSG によって異なる点が多々あると考えられる点について、ご了承ください。

## SharePoint の前提条件

SharePoint を用いて Web サイトを公開するためには、[SharePoint サイトでカスタムスクリプトを許可する](https://learn.microsoft.com/ja-jp/sharepoint/allow-or-prevent-custom-script)必要があります。
これを有効とするためには、以降の前提条件を満たしていることを確認してください。

:::note warn
カスタムスクリプトを許可せずに、ビルドしたファイルを SharePoint にアップロードしたとしても、Web サイトとして表示されずに、単純にファイルがダウンロードされる形となります。
:::

### セキュリティ関する注意事項を確認済みであること

カスタムスクリプトの実行を許可することで、ページへのスクリプトの追加や、スクリプトが含まれるファイルのアップロード、ソリューションの追加など、柔軟にサイトをカスタマイズできるようになります。サイトの編集権限を持つ一般ユーザーがスクリプトを追加できるようになるため、悪意のあるスクリプトが挿入されるなどのセキュリティへの注意が必要です。

詳細については、[カスタム スクリプトを許可する場合のセキュリティに関する考慮事項 - SharePoint in Microsoft 365 | Microsoft Learn](https://learn.microsoft.com/ja-jp/sharepoint/security-considerations-of-allowing-custom-script) を参照ください。

### Web サイトを公開する際に、SharePoint 管理者ロールを持つアカウントを利用できること

SharePoint サイトでカスタムスクリプトを許可するには、SharePoint 管理者ロールが必要です (グローバル管理者ロールでも可) 。

また、前述したように、カスタムスクリプトを許可すると、悪意のあるスクリプトが挿入されるなどのリスクがあります。そのため、カスタムスクリプトを許可すると、24 時間以内に再び自動で無効化されます。

つまり、Web サイトを公開するタイミングのみ、 SharePoint 管理者ロールを持つアカウントでカスタムスクリプトを許可する運用となります。

:::note info
カスタムスクリプトを許可した際に Web サイトやスクリプトなどを SharePoint サイトにアップロードしておけば、24 時間後にカスタムスクリプトが無効となっても、Web サイトやスクリプトは有効な状態が続きます。
そのため、Web サイトを公開するタイミングのみ、カスタムスクリプトを許可すれば問題ありません。
:::

## ビルド時の設定を適用する

Astro で Web サイトを作成する際に作成される設定ファイルである `astro.config.mjs` にビルドの設定を適用します。

### リンクを修正する

SharePoint でリンクを有効とするために、相対パスでビルドするモジュール `astro-relative-links` をインストールし、インポートします。  

:::note info
インストールの手順については、[astro-relative-links - npm](https://www.npmjs.com/package/astro-relative-links) などを参照ください。
:::

Starlight のデフォルト設定では、`<a href="/tmp/hoge/index.html">` のようにHTML の a タグの href 属性などが絶対パスで入力されます。
このような絶対パスはローカルで Starlight を起動した際などでは利用できますが、SharePoint に配置した際に有効なリンクとならないため、相対パスで出力するようにモジュールをインポートします。

```JavaScript:astro.config.mjs
// SharePointでリンクを有効にさせるために、相対パスでビルドするモジュールをインポート
import relativeLinks from "astro-relative-links";
export default defineConfig({
  integrations: [
    /* 
      省略(その他の設定)
    */
    // SharePointでリンクを有効にさせるために、相対パスでビルドするモジュールを設定
    relativeLinks(),
    /* 
      省略(その他の設定)
    */
  ]
});
```

### 文字化けを回避する

Starlight のデフォルトの出力ファイルを SharePoint に配置した場合、文字化けが発生するため、ヘッダーに `meta` タグを追加し、文字化けを回避します。

```JavaScript:astro.config.mjs
export default defineConfig({
  integrations: [
    starlight({
      /* 
        省略(その他の設定)
      */
      // SharePointで文字化けを回避するために、metaタグを追加
      head: [{
        tag: 'meta',
        attrs: {
          'http-equiv': "content-type",
          content: "text/html; charset=utf-8"
        }
      }]
      /* 
        省略(その他の設定)
      */
    })
  ]
});
```

### 検索機能を修正する

Starlight は、デフォルトで [Pagefind](https://pagefind.app/) による全文検索機能が組み込まれています。Pagefind は、静的サイト向けの高速で通信量が少ない検索ツールです。

![20241209_2102_msedge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/d40492ca-49f7-8857-f657-abc14326a06d.png)

![20241209_2103_msedge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/6d4813e5-ee48-a685-4edd-53666467d3fe.png)
`出典：Astro Contributors「サイト内検索 | Starlight」
https://starlight.astro.build/ja/guides/site-search/（2024.12.13）`

SharePoint に配置した際に、この検索機能を用いたリンクの生成が適切に行われないため、検索機能を設定するコンポーネントをオーバーライドして、検索機能を用いることができるようにします。

```JavaScript:astro.config.mjs
export default defineConfig({
  integrations: [
    starlight({
      /* 
      省略(その他の設定)
      */
      components: {
        // デフォルトの`Search`コンポーネントをオーバーライド
        Search: './src/components/SearchSharePoint.astro',
      },
      /* 
      省略(その他の設定)
      */
    })
  ]
});
```

:::note info
コンポーネントのオーバーライドの仕様については、[オーバーライド | Starlight](https://starlight.astro.build/ja/reference/overrides/) などを参照ください。
:::

## 検索機能を設定するコンポーネントを作成する

オーバーライドを行うために、検索機能を設定するコンポーネント `SearchSharePoint.astro` を作成します。
[starlight/components/Search.astro](https://github.com/withastro/starlight/blob/main/packages/starlight/components/Search.astro)
 をコピーして `SearchSharePoint.astro` として保存し、下記の修正を行います。
 上記の `Search.astro` は GitHub 上のリンクとしていますが、Starlight をインストールした際に `/node_modules/@astrojs/starlight/components/Search.astro` として保存されているため、こちらのファイルをコピーしても構いません。

::: note info
下記の修正箇所として挙げている `'/Shared%20Documents/astro'` の部分は、自身の環境の SharePoint のフォルダパスを確認して、書き換えてください。
:::

```diff_javascript:SearchSharePoint.astro
import '@pagefind/default-ui/css/ui.css';
- import Icon from '../user-components/Icon.astro';
+ import Icon from '../../node_modules/@astrojs/starlight/user-components/Icon.astro';
import project from 'virtual:starlight/project-context';
- import type { Props } from '../props';
+ import type { Props } from '@astrojs/starlight/props';

/*
  省略
*/

new PagefindUI({
	element: '#starlight__search',
	// 検索に利用する pagefind.js ファイルのパスを設定する必要があるため、ファイルを配置する SharePoint のパスを追加
-	baseUrl: import.meta.env.BASE_URL,						
+	baseUrl: import.meta.env.BASE_URL.replace(/\/$/, '') + '/Shared%20Documents/astro',
	// 検索に利用する pagefind.js ファイルのパスを設定する必要があるため、ファイルを配置する SharePoint のパスを追加
-	bundlePath: import.meta.env.BASE_URL.replace(/\/$/, '') + '/pagefind/',
+	bundlePath: import.meta.env.BASE_URL.replace(/\/$/, '') + '/Shared%20Documents/astro' + '/pagefind/',
	showImages: false,
	translations,
	showSubResults: true,
	processResult: (result: { url: string; sub_results: Array<{ url: string }> }) => {
		// SharePoint で公開するために、｢index.aspx｣ファイルとして公開しているため、そのファイルへのリンクとなるように修正
-		result.url = formatURL(result.url);
+		result.url = formatURL(result.url) + 'index.aspx';
		result.sub_results = result.sub_results.map((sub_result) => {
            // SharePoint で公開するために、｢index.aspx｣ファイルとして公開しているため、そのファイルへのリンクとなるように修正
-			sub_result.url = formatURL(sub_result.url);
+			sub_result.url = formatURL(sub_result.url.slice(0, sub_result.url.indexOf('#')) + 'index.aspx' + sub_result.url.slice(sub_result.url.indexOf('#')));
			return sub_result;
		});
	},
});

/*
  省略
*/
```

このファイルを[検索機能を修正する](#検索機能を修正する)で、`astro.config.mjs` に記載したディレクトリ (`./src/components/`) に保存します。


## ビルド後の設定を適用する

SharePoint では、HTML ファイルをそのままアップロードしても Web ページとして表示されません。下記のように、Web ページとして開かれず、ファイルとして表示されます。

![1097_QMIH0jXdYH.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/e9fb148f-5554-584d-3d2c-7a5ddd3cb77b.png)
`出典：Microsoft Corporation「Microsoft SharePoint」（2024.12.13）`

しかし、ファイルの拡張子を `.aspx` とすることで、Web ページとして表示することができます。ASPX ファイルは、ASP.NET に基づく Web ページファイルで利用されます。

![1096_w8A6TTD25q.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/e73a21ff-38b1-ef03-6c71-c5a01bdfa59f.png)
`出典：Astro Contributors「Starlight」（2024.12.13）`

このような仕様であるため、SSG でビルドして HTML ファイルを生成した後に、ASPX ファイルに変更します。

### リンクとファイル名を変更する

リンクとファイル名を変更するスクリプトを作成し、ルートフォルダ (`package.json` が存在するフォルダ) に保存します。このスクリプトでは、下記 2 つの処理を行います。

1. HTML ファイルを ASPX ファイルへ変更するため、各ファイルに存在するリンクを、その変更に対応したリンクに変更する
1. HTML ファイルを ASPX ファイル (`.html` から `.aspx`) へ変更する

```JavaScript:replaceLinksAndRenameFiles.js
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';

console.log('--- replaceLinksAndRenameFiles start ---');

// 現在のファイル名を取得します
const __filename = fileURLToPath(import.meta.url);
// 現在のディレクトリ名を取得します
const __dirname = dirname(__filename);

// HTML ファイルが配置されているディレクトリを定義します
const buildDirectory = path.join(__dirname, 'dist');

// ディレクトリとそのサブディレクトリを再帰的に読み取る関数
function readDirectory(directory) {
  const entries = fs.readdirSync(directory, { withFileTypes: true });

  // すべての HTML ファイルを保持する配列
  const htmlFiles = [];

  for (const entry of entries) {
    const entryPath = path.join(directory, entry.name);
    if (entry.isDirectory()) {
      // サブディレクトリを再帰的に読み取ります
      htmlFiles.push(...readDirectory(entryPath));
    } else if (entry.isFile() && entry.name.endsWith('.html')) {
      // HTML ファイルを配列に追加します
      htmlFiles.push(entryPath);
    }
  }

  return htmlFiles;
}

// HTML ファイル内のリンクを置換する関数
function replaceLinksInHtml(files) {
  files.forEach(file => {
    let content = fs.readFileSync(file, 'utf8');

    // 文字列置換を使用してリンクを置換します
    content = content.replace(/href="(\.\/|\.\.\/)([^"]+\/)"/g, 'href="$1$2index.aspx"');

    fs.writeFileSync(file, content, 'utf8');
    console.log(`右記のファイル内のリンクを更新しました: ${file}`);
  });
}

// ファイルの拡張子を変更する関数
function changeExtension(fileList, oldExt, newExt) {
  fileList.forEach((file) => {
    if (path.extname(file) === oldExt) {
      // 新しい拡張子に置換します
      const newFile = file.replace(oldExt, newExt);
      // ファイル名を変更します
      fs.renameSync(file, newFile);
      console.log(`右記のファイル名を変更しました: ${file} -> ${newFile}`);
    }
  });
}

// ビルドディレクトリとそのサブディレクトリにあるすべての HTML ファイルを取得します
const htmlFiles = readDirectory(buildDirectory);

// リンクの置換を開始する関数を呼び出します
replaceLinksInHtml(htmlFiles);

// ファイルの拡張子を変更する関数を呼び出します
changeExtension(htmlFiles, '.html', '.aspx');

console.log('--- replaceLinksAndRenameFiles finish ---');
```

### スクリプトを実行する設定を追加する

ビルド後に、自動的にスクリプトを実行させるために、`package.json` の `scripts` に、リンクとファイル名を変更するスクリプトを実行するコマンドを追加します。

```diff_json:package.json
{
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro build",
+   "buildAspx": "astro build && node replaceLinksAndRenameFiles.js",
    "preview": "astro preview",
    "astro": "astro"
  },
}
```

::: note warn
上記の `package.json` ファイルは、`name` `dependencies` などの他のセクションの記載は省略しています。
:::

## SharePoint のカスタムスクリプトを有効にする

SharePoint のカスタムスクリプトを有効にします。

1. SharePoint 管理者ロール (あるいは、グローバル管理者ロール) を持つアカウントで、[SharePoint 管理センターの [アクティブなサイト]](https://go.microsoft.com/fwlink/?linkid=2185220) にアクセスし、カスタムスクリプトを有効にする SharePoint サイトをクリックします
![20241209_2108_RDCMan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/07a5fd11-ce24-9ff8-dc0e-b8a01b1beabd.png)
`出典：Microsoft Corporation「Microsoft SharePoint」（2024.12.13）`

   ::: note warn
   SharePoint 管理者ロールを持っていないアカウントで [SharePoint 管理センターの [アクティブなサイト]](https://go.microsoft.com/fwlink/?linkid=2185220) にアクセスすると、下記のように表示されます。このように表示された場合、利用しているアカウントは SharePoint 管理者ロールを持っていないため、カスタムスクリプトを有効にすることはできません。
   ![20241212_2141_msedge.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/30be3e6f-720a-b69b-a95b-af253c3a6668.png)
   :::

1. [設定] タブ - [カスタムスクリプト] - [編集] をクリックします
![20241209_2109_RDCMan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/4d9b7b33-ef19-2672-fbc4-1f318da2b971.png)
1. [許可済み] を選択し、[保存] をクリックします
![20241209_2110_RDCMan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/32c3f5f4-ec53-ca8c-d624-2034725d3b72.png)
1. [確認] をクリックします
![20241209_2111_RDCMan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/8c127fd5-aa90-8983-03b1-8b9643a9b76a.png)
1. [変更が保存されました。] と表示されることを確認します
![20241209_2112_RDCMan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/aaab1892-716e-3c09-4638-71b94283a139.png)

## ビルドを行い、SharePoint に Web サイトを公開する

下記コマンドでビルドを行い、静的ファイルを生成します。

```bash
npm run buildAspx
```

`dist` フォルダに出力されたファイルを SharePoint サイトにアップロードします。

![20241209_2113_RDCMan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/3df84b36-9884-22b9-f234-5249e0a51931.png)
`出典：Microsoft Corporation「Microsoft SharePoint」（2024.12.13）`

`index.aspx` ファイルなどをクリックし、Web サイトが表示されることを確認します。

![1098_nF94xTqs6s.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/972de864-b51b-a912-31f4-2a4c370bf3ff.png)

![1099_EczXokMJ7T.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3635196/28a25ee0-3116-4b78-e150-6b7b2af0d210.png)
`出典：Astro Contributors「Starlight」（2024.12.13）`

# まとめ

本記事で紹介した通り、SSG (Astro) で作成した静的サイトを SharePoint で公開するためには、いくつかの設定が必要となります。
しかし、SharePoint で公開すれば、静的ファイルを配置するサーバーが不要となり、サーバーレスで運用ができます。静的ファイルを配置する際に、SharePoint 管理者ロールを持つアカウントでカスタムスクリプトを許可する必要があることなど、運用における作業もありますが、それを考慮してもサーバーレスで運用することによる管理作業削減の効果の方が大きいかと考えています。

以上、SSG (Astro) で作成した静的サイトを SharePoint で公開する手順を紹介しました。

記載されている会社名、製品名、サービス名は、各社の商標または登録商標です。

# 参考

- [はじめに | Astro Docs](https://docs.astro.build/ja/getting-started/)
- [Starlight 🌟 Astroでドキュメントサイトを作る](https://starlight.astro.build/ja/)
- [コンテンツの共同作業と共有を安全に | Microsoft SharePoint](https://www.microsoft.com/ja-jp/microsoft-365/sharepoint/collaboration?msockid=0007f6f8af2169673af6e44aaecb6817)