Headless CMSを作る
最初の業務で使ったVue.jsと業務で使うことの多い.NET系のフレームワークで何か作ってみたいと思い、Headless CMSでブログを作ってみた。CI/CDまで組んで継続的に開発をしていこうと思ったら土台を作るだけで1週間ほどかかったのでその時の手順を後世に残していこうと思う。 Headless CMSの選定 Vueの公式サイトではButter CMSを使ったブログの作成手順が紹介されているがこれは個人利用であってもお金がかかってしまうため、1ユーザーであればFreeのPrismicを使った。Prismicは言語ごとのチュートリアルがものすごく充実しているのでやりたいことは探せばほぼDocumentに乗っていると思う。これで無料はヤバイ。 ローカルで動かす。 .NET Coreプロジェクトの作成 以下のコマンドで.NET Coreプロジェクトの作成とSPA Extensionパッケージの追加をやります。
1 2 3 |
dotnet new webapi -n core-spa-sample --framework netcoreapp3.1 --no-https cd core-spa-sample dotnet add package Microsoft.AspNetCore.SpaServices.Extensions --version 3.1.3 |
ここにVue cliプロジェクトを追加し、Coreに設定を追加していきます。 Vue.jsを埋め込むための設定 PrismicのGitHubにVueのテンプレートがあるのでクローンします。
1 |
git clone https://github.com/prismicio/vuejs-starter |
npm installも忘れずに ここまででフォルダ構成はこんな感じ
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
>>> tree . ├── Controllers │ └── WeatherForecastController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── WeatherForecast.cs ├── appsettings.Development.json ├── appsettings.json ├── core-spa-sample.csproj ├── obj │ ├── core-spa-sample.csproj.nuget.cache │ ├── core-spa-sample.csproj.nuget.dgspec.json │ ├── core-spa-sample.csproj.nuget.g.props │ ├── core-spa-sample.csproj.nuget.g.targets │ └── project.assets.json └── vuejs-starter ├── README.md ├── babel.config.js ├── custom_types │ ├── index.json │ └── page.json ├── package-lock.json ├── package.json ├── public │ ├── favicon.png │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ ├── css │ │ │ ├── prismic-edit-button.css │ │ │ ├── resetr.css │ │ │ └── tutorial │ │ │ ├── highlight.min.css │ │ │ └── tutorial.min.css │ │ └── img │ │ ├── prismic-logo.svg │ │ ├── tutorial │ │ │ ├── bb.gif │ │ │ ├── chevron.svg │ │ │ ├── open.svg │ │ │ └── rocket.svg │ │ └── vue-logo.png │ ├── components │ │ └── FooterPrismic.vue │ ├── main.js │ ├── prismic │ │ ├── html-serializer.js │ │ └── link-resolver.js │ ├── router.js │ └── views │ ├── NotFound.vue │ ├── Preview.vue │ └── Tutorial.vue └── vue.config.js |
次は.csprojを書き換えていきます。vue cli使ったことのある人なら見覚えのあるコマンドが並んでいるはず。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <RootNamespace>core_spa_sample</RootNamespace> <!-- root directoryを変数として定義 --> <SpaRoot>client-app\</SpaRoot> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.3" /> </ItemGroup> <ItemGroup> <!-- vue cli project以下のフォルダがデプロイされないように設定 --> <Content Remove="$(SpaRoot)**" /> <None Remove="$(SpaRoot)**" /> <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> </ItemGroup> <Target Name="ExecNpmInstall" BeforeTargets="Build" Condition="'$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)\node_modules')"> <Exec WorkingDirectory="$(SpaRoot)\" Command="npm install" /> </Target> <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish"> <!-- As part of publishing, ensure the JS resources are freshly built in production mode --> <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" /> <Exec WorkingDirectory="$(SpaRoot)" Command="npm install --save vue-router" /> <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build" /> <!-- Include the newly-built files in the publish output --> <ItemGroup> <DistFiles Include="$(SpaRoot)\dist\**" /> <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)"> <RelativePath>%(DistFiles.Identity)</RelativePath> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> </ResolvedFileToPublish> </ItemGroup> </Target> </Project> |
コード側 Vue.jsとCoreを繋ぐコードを書きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.SpaServices; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Diagnostics; using System.IO; using System.Linq; using System.Net.NetworkInformation; using System.Runtime.InteropServices; using System.Threading.Tasks; namespace core_spa_sample.VueCoreConnections { public static class Connection { private static int Port { get; } = 8080; private static Uri DevelopmentServerEndpoint { get; } = new Uri($"http://localhost:{Port}"); private static TimeSpan Timeout { get; } = TimeSpan.FromSeconds(60); private static string DoneMessage { get; } = "DONE Compiled successfully in"; public static void UseVueDevelopmentServer(this ISpaBuilder spa) { spa.UseProxyToSpaDevelopmentServer(async () => { var loggerFactory = spa.ApplicationBuilder.ApplicationServices.GetService<ILoggerFactory>(); var logger = loggerFactory.CreateLogger("Vue"); if (IsRunning()) { return DevelopmentServerEndpoint; } var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var processInfo = new ProcessStartInfo { FileName = isWindows ? "cmd" : "npm", Arguments = $"{(isWindows ? "/c npm " : "")}run serve", WorkingDirectory = "vuejs-starter", RedirectStandardError = true, RedirectStandardInput = true, RedirectStandardOutput = true, UseShellExecute = false, }; var process = Process.Start(processInfo); var tcs = new TaskCompletionSource<int>(); _ = Task.Run(() => { try { string line; while ((line = process.StandardOutput.ReadLine()) != null) { logger.LogInformation(line); if (!tcs.Task.IsCompleted && line.Contains(DoneMessage)) { tcs.SetResult(1); } } } catch (EndOfStreamException ex) { logger.LogError(ex.ToString()); tcs.SetException(new InvalidOperationException("'npm run serve' failed.", ex)); } }); _ = Task.Run(() => { try { string line; while ((line = process.StandardError.ReadLine()) != null) { logger.LogError(line); } } catch (EndOfStreamException ex) { logger.LogError(ex.ToString()); tcs.SetException(new InvalidOperationException("'npm run serve' failed.", ex)); } }); var timeout = Task.Delay(Timeout); if (await Task.WhenAny(timeout, tcs.Task) == timeout) { throw new TimeoutException(); } return DevelopmentServerEndpoint; }); } private static bool IsRunning() => IPGlobalProperties.GetIPGlobalProperties() .GetActiveTcpListeners() .Select(x => x.Port) .Contains(Port); } } |
次にStartup.csの編集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
using MyCMS_API.VueCoreConnections; // 省略 // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSpaStaticFiles(options => options.RootPath = "vuejs-starter/dist"); } // 省略 // use middleware and launch server for Vue app.UseSpaStaticFiles(); app.UseSpa(spa => { spa.Options.SourcePath = "vuejs-starter"; if (env.IsDevelopment()) { spa.UseVueDevelopmentServer(); } }); |
これで.NET Coreの上でVue.jsを動かすことができます。