# Podcasts now in Razor SSG Source: https://razor-ssg.web-templates.io/posts/razor-ssg-podcasts ## Razor SSG now supports Podcasts! [Razor SSG](https://razor-ssg.web-templates.io) is our FREE Project Template for creating fast, statically generated Websites and Blogs with Markdown & C# Razor Pages. A benefit of using Razor SSG to maintain our [github.com/ServiceStack/servicestack.net](https://github.com/ServiceStack/servicestack.net) website is that any improvements added to **servicestack.net** end up being rolled into the Razor SSG Project Template for everyone else to enjoy. The latest feature recently added is [ServiceStack Podcasts](https://servicestack.net/podcasts), providing an easy alternative to learning about new features in our [TL;DR Release Notes](https://docs.servicestack.net/releases/v8_04) during a commute as well as a fun and more informative experience whilst reading [blog posts](https://servicestack.net/blog). The same podcast feature has now been rolled into the Razor SSG template allowing anyone to add the same feature to their Razor SSG Websites which can be developed and hosted for FREE on GitHub Pages CDN: ### Create a new Razor SSG Project
Razor SSG
### Markdown Powered The Podcast feature is very similar to the Markdown Blog Posts where each podcast is a simple `.md` Markdown page seperated by a publish date and its unique slug, e.g: **[/_podcasts](https://github.com/NetCoreTemplates/razor-ssg/tree/main/MyApp/_podcasts)** ```files /_pages /_podcasts config.json 2024-10-02_razor-ssg-podcasts.md 2024-09-19_scalable-sqlite.md 2024-09-17_sqlite-request-logs.md ... /_posts /_videos /_whatsnew ``` All editable content within different Podcast pages like the Podcast Sidebar is customizable within [_podcasts/config.json](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_podcasts/config.json). [![](https://servicestack.net/img/posts/razor-ssg-podcasts/razor-ssg-podcast-layout.webp)](https://razor-ssg.web-templates.io/podcasts) ### Podcast Page Whilst all content about a podcast is contained within its `.md` file and frontmatter which just like Blog Posts can contain interactive Vue Components and custom [Markdown Containers](https://razor-press.web-templates.io/containers). The [Backgrounds Jobs Podcast Page](https://razor-ssg.web-templates.io/podcasts/background-jobs) is a good example of this where its [2024-09-12_background-jobs.md](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_podcasts/2024-09-12_background-jobs.md?plain=1) contains both a `` Vue Component as well as `sh` and `youtube` custom markdown containers to render its page: [![](https://servicestack.net/img/posts/razor-ssg-podcasts/razor-ssg-podcast-page.webp)](https://razor-ssg.web-templates.io/podcasts/background-jobs) ### Audio Player Podcasts are played using the [AudioPlayer.mjs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/pages/podcasts/AudioPlayer.mjs) Vue Component that's enabled on each podcast page which will appear at the bottom of the page when played: [![](https://servicestack.net/img/posts/razor-ssg-podcasts/razor-ssg-podcast-audioplayer.webp)](https://razor-ssg.web-templates.io/podcasts) The `AudioPlayer` component is also independently usable as a standard Vue Component in markdown content like [this .md page](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_posts/2024-10-02_razor-ssg-podcasts.md?plain=1#L72): ```html ``` :::{.py-8 .mx-auto .w-2/3 .not-prose} ::: It can also be embeddable inside Razor `.cshtml` pages using [Declarative Vue Components](https://servicestack.net/posts/net8-best-blazor#declarative-vue-components), e.g: ```html @{ var episode = Podcasts.GetEpisodes().FirstOrDefault(x => x.Slug == doc.Slug);
} ``` ### Dark Mode As Razor SSG is built with Tailwind CSS, Dark Mode is also easily supported: [![](https://servicestack.net/img/posts/razor-ssg-podcasts/razor-ssg-podcast-dark.webp)](https://razor-ssg.web-templates.io/podcasts/background-jobs) ### Browse by Tags Just like [blog post archives](https://razor-ssg.web-templates.io/posts/), the frontmatter collection of `tags` is used to generate related podcast pages, aiding discoverability by grouping related podcasts by **tag** at the following route: /podcasts/tagged/{tag} https://razor-ssg.web-templates.io/podcasts/tagged/release [![](https://servicestack.net/img/posts/razor-ssg-podcasts/razor-ssg-podcast-tag.webp)](https://razor-ssg.web-templates.io/podcasts/tagged/release) ### Browse by Year Likewise podcast archives are also browsable by the year their published at the route: /podcasts/year/{year} https://razor-ssg.web-templates.io/podcasts/year/2024 [![](https://servicestack.net/img/posts/razor-ssg-podcasts/razor-ssg-podcast-year.webp)](https://razor-ssg.web-templates.io/podcasts/year/2024) ### iTunes-compatible Podcast RSS Feed The information in [config.json](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_podcasts/config.json) is also used in the generated podcast RSS feed at: [/podcasts/feed.xml](https://razor-ssg.web-templates.io/podcasts/feed.xml) Which is a popular format podcast Applications can use to get notified when new Podcast episodes are available. The RSS Feed is also compatible with [podcasters.apple.com](https://podcasters.apple.com) and can be used to publish your podcast to [Apple Podcasts](https://podcasts.apple.com). ```xml Their Side https://razor-ssg.web-templates.io/podcasts https://razor-ssg.web-templates.io/img/posts/cover.png Their Side /podcasts razor-ssg Razor SSG Wed, 02 Oct 2024 03:54:03 GMT email@example.org (Razor SSG) email@example.org (Razor SSG) Razor SSG Razor SSG email@example.org ... ``` # ASP.NET Core JWT Identity Auth Source: https://razor-ssg.web-templates.io/posts/jwt-identity-auth JWTs enable stateless authentication of clients without servers needing to maintain any Auth state in server infrastructure or perform any I/O to validate a token. As such, [JWTs are a popular choice for Microservices](https://docs.servicestack.net/auth/jwt-authprovider#stateless-auth-microservices) as they only need to configured with confidential keys to validate access. ### ASP.NET Core JWT Authentication ServiceStack's JWT Identity Auth reimplements many of the existing [ServiceStack JWT AuthProvider](https://docs.servicestack.net/auth/jwt-authprovider) features but instead of its own implementation, integrates with and utilizes ASP.NET Core's built-in JWT Authentication that's configurable in .NET Apps with the `.AddJwtBearer()` extension method, e.g: #### Program.cs ```csharp services.AddAuthentication() .AddJwtBearer(options => { options.TokenValidationParameters = new() { ValidIssuer = config["JwtBearer:ValidIssuer"], ValidAudience = config["JwtBearer:ValidAudience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(config["JwtBearer:IssuerSigningKey"]!)), ValidateIssuerSigningKey = true, }; }) .AddIdentityCookies(options => options.DisableRedirectsForApis()); ``` Then use the `JwtAuth()` method to enable and configure ServiceStack's support for ASP.NET Core JWT Identity Auth: #### Configure.Auth.cs ```csharp public class ConfigureAuth : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { services.AddPlugin(new AuthFeature(IdentityAuth.For( options => { options.SessionFactory = () => new CustomUserSession(); options.CredentialsAuth(); options.JwtAuth(x => { // Enable JWT Auth Features... }); }))); }); } ``` ### Enable in Swagger UI Once configured we can enable JWT Auth in Swagger UI by installing **Swashbuckle.AspNetCore**: :::copy `` ::: Then enable Open API, Swagger UI, ServiceStack's support for Swagger UI and the JWT Bearer Auth option: ```csharp public class ConfigureOpenApi : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { if (context.HostingEnvironment.IsDevelopment()) { services.AddEndpointsApiExplorer(); services.AddSwaggerGen(); services.AddServiceStackSwagger(); services.AddJwtAuth(); //services.AddBasicAuth(); services.AddTransient(); } }); public class StartupFilter : IStartupFilter { public Action Configure(Action next) => app => { // Provided by Swashbuckle library app.UseSwagger(); app.UseSwaggerUI(); next(app); }; } } ``` This will enable the **Authorize** button in Swagger UI where you can authenticate with a JWT Token: ![](https://servicestack.net/img/posts/jwt-identity-auth/jwt-swagger-ui.png) ### JWT Auth in Built-in UIs This also enables the **JWT** Auth Option in ServiceStack's built-in [API Explorer](https://docs.servicestack.net/api-explorer), [Locode](https://docs.servicestack.net/locode/) and [Admin UIs](https://docs.servicestack.net/admin-ui): ### Authenticating with JWT JWT Identity Auth is a drop-in replacement for ServiceStack's JWT AuthProvider where Authenticating via Credentials will convert the Authenticated User into a JWT Bearer Token returned in the **HttpOnly**, **Secure** `ss-tok` Cookie that will be used to Authenticate the client: ```csharp var client = new JsonApiClient(BaseUrl); await client.SendAsync(new Authenticate { provider = "credentials", UserName = Username, Password = Password, }); var bearerToken = client.GetTokenCookie(); // ss-tok Cookie ``` ## JWT Refresh Tokens Refresh Tokens can be used to allow users to request a new JWT Access Token when the current one expires. To enable support for JWT Refresh Tokens your `IdentityUser` model should implement the `IRequireRefreshToken` interface which will be used to store the 64 byte Base64 URL-safe `RefreshToken` and its `RefreshTokenExpiry` in its persisted properties: ```csharp public class ApplicationUser : IdentityUser, IRequireRefreshToken { public string? RefreshToken { get; set; } public DateTime? RefreshTokenExpiry { get; set; } } ``` Now after successful authentication, the `RefreshToken` will also be returned in the `ss-reftok` Cookie: ```csharp var refreshToken = client.GetRefreshTokenCookie(); // ss-reftok Cookie ``` ### Transparent Server Auto Refresh of JWT Tokens To be able to terminate a users access, Users need to revalidate their eligibility to verify they're still allowed access (e.g. deny Locked out users). This JWT revalidation pattern is implemented using Refresh Tokens which are used to request revalidation of their access and reissuing a new JWT Access Token which can be used to make authenticated requests until it expires. As Cookies are used to return Bearer and Refresh Tokens ServiceStack is able to implement the revalidation logic on the server where it transparently validates Refresh Tokens, and if a User is eligible will reissue a new JWT Token Cookie that replaces the expired Access Token Cookie. Thanks to this behavior HTTP Clients will be able to Authenticate with just the Refresh Token, which will transparently reissue a new JWT Access Token Cookie and then continue to perform the Authenticated Request: ```csharp var client = new JsonApiClient(BaseUrl); client.SetRefreshTokenCookie(RefreshToken); var response = await client.SendAsync(new Secured { ... }); ``` There's also opt-in sliding support for extending a User's RefreshToken after usage which allows Users to treat their Refresh Token like an API Key where it will continue extending whilst they're continuously using it to make API requests, otherwise expires if they stop. How long to extend the expiry of Refresh Tokens after usage can be configured with: ```csharp options.JwtAuth(x => { // How long to extend the expiry of Refresh Tokens after usage (default None) x.ExtendRefreshTokenExpiryAfterUsage = TimeSpan.FromDays(90); }); ``` ## Convert Session to Token Service Another useful Service that's available is being able to Convert your current Authenticated Session into a Token with the `ConvertSessionToToken` Service which can be enabled with: ```csharp options.JwtAuth(x => { x.IncludeConvertSessionToTokenService = true; }); ``` This can be useful for when you want to Authenticate via an external OAuth Provider that you then want to convert into a stateless JWT Token by calling the `ConvertSessionToToken` on the client, e.g: #### .NET Clients ```csharp await client.SendAsync(new ConvertSessionToToken()); ``` #### TypeScript/JavaScript ```ts fetch('/session-to-token', { method:'POST', credentials:'include' }) ``` The default behavior of `ConvertSessionToToken` is to remove the Current Session from the Auth Server which will prevent access to protected Services using our previously Authenticated Session. If you still want to preserve your existing Session you can indicate this with: ```csharp await client.SendAsync(new ConvertSessionToToken { PreserveSession = true }); ``` ### JWT Options Other configuration options available for Identity JWT Auth include: ```csharp options.JwtAuth(x => { // How long should JWT Tokens be valid for. (default 14 days) x.ExpireTokensIn = TimeSpan.FromDays(14); // How long should JWT Refresh Tokens be valid for. (default 90 days) x.ExpireRefreshTokensIn = TimeSpan.FromDays(90); x.OnTokenCreated = (req, user, claims) => { // Customize which claims are included in the JWT Token }; // Whether to invalidate Refresh Tokens on Logout (default true) x.InvalidateRefreshTokenOnLogout = true; // How long to extend the expiry of Refresh Tokens after usage (default None) x.ExtendRefreshTokenExpiryAfterUsage = null; }); ``` # Built-In Identity Auth Admin UI Source: https://razor-ssg.web-templates.io/posts/identity-auth-admin-ui With ServiceStack now [deeply integrated into ASP.NET Core Apps](/posts/servicestack-endpoint-routing) we're back to refocusing on adding value-added features that can benefit all .NET Core Apps. ## Registration The new Identity Auth Admin UI is an example of this, which can be enabled when registering the `AuthFeature` Plugin: ```csharp public class ConfigureAuth : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { services.AddPlugin(new AuthFeature(IdentityAuth.For( options => { options.SessionFactory = () => new CustomUserSession(); options.CredentialsAuth(); options.AdminUsersFeature(); }))); }); } ``` Which just like the ServiceStack Auth [Admin Users UI](https://docs.servicestack.net/admin-ui-users) enables a Admin UI that's only accessible to **Admin** Users for managing **Identity Auth** users at `/admin-ui/users`. ## User Search Results Which displays a limited view due to the minimal properties on the default `IdentityAuth` model:
### Custom Search Result Properties These User's search results are customizable by specifying the `ApplicationUser` properties to display instead, e.g: ```csharp options.AdminUsersFeature(feature => { feature.QueryIdentityUserProperties = [ nameof(ApplicationUser.Id), nameof(ApplicationUser.DisplayName), nameof(ApplicationUser.Email), nameof(ApplicationUser.UserName), nameof(ApplicationUser.LockoutEnd), ]; }); ```
### Custom Search Result Behavior The default display Order of Users is also customizable: ```csharp feature.DefaultOrderBy = nameof(ApplicationUser.DisplayName); ``` As well as the Search behavior which can be replaced to search any custom fields, e.g: ```csharp feature.SearchUsersFilter = (q, query) => { var queryUpper = query.ToUpper(); return q.Where(x => x.DisplayName!.Contains(query) || x.Id.Contains(queryUpper) || x.NormalizedUserName!.Contains(queryUpper) || x.NormalizedEmail!.Contains(queryUpper)); }; ``` ## Default Create and Edit Users Forms The default Create and Edit Admin Users UI are also limited to editing the minimal `IdentityAuth` properties:
Whilst the Edit page includes standard features to lockout users, change user passwords and manage their roles:
### Custom Create and Edit Forms By default Users are locked out indefinitely, but this can also be changed to lock users out to a specific date, e.g: ```csharp feature.ResolveLockoutDate = user => DateTimeOffset.Now.AddDays(7); ``` The forms editable fields can also be customized to include additional properties, e.g: ```csharp feature.FormLayout = [ Input.For(x => x.UserName, c => c.FieldsPerRow(2)), Input.For(x => x.Email, c => { c.Type = Input.Types.Email; c.FieldsPerRow(2); }), Input.For(x => x.FirstName, c => c.FieldsPerRow(2)), Input.For(x => x.LastName, c => c.FieldsPerRow(2)), Input.For(x => x.DisplayName, c => c.FieldsPerRow(2)), Input.For(x => x.PhoneNumber, c => { c.Type = Input.Types.Tel; c.FieldsPerRow(2); }), ]; ``` That can override the new `ApplicationUser` Model that's created and any Validation: ### Custom User Creation ```csharp feature.CreateUser = () => new ApplicationUser { EmailConfirmed = true }; feature.CreateUserValidation = async (req, createUser) => { await IdentityAdminUsers.ValidateCreateUserAsync(req, createUser); var displayName = createUser.GetUserProperty(nameof(ApplicationUser.DisplayName)); if (string.IsNullOrEmpty(displayName)) throw new ArgumentNullException(nameof(AdminUserBase.DisplayName)); return null; }; ```
### Admin User Events Should you need to, Admin User Events can use used to execute custom logic before and after creating, updating and deleting users, e.g: ```csharp feature.OnBeforeCreateUser = (request, user) => { ... }; feature.OnAfterCreateUser = (request, user) => { ... }; feature.OnBeforeUpdateUser = (request, user) => { ... }; feature.OnAfterUpdateUser = (request, user) => { ... }; feature.OnBeforeDeleteUser = (request, userId) => { ... }; feature.OnAfterDeleteUser = (request, userId) => { ... }; ``` # System.Text.Json ServiceStack APIs Source: https://razor-ssg.web-templates.io/posts/system-text-json-apis In continuing our focus to enable ServiceStack to become a deeply integrated part of .NET 8 Application's, ServiceStack latest .NET 8 templates now default to using standardized ASP.NET Core features wherever possible, including: - [ASP.NET Core Identity Auth](/posts/net8-identity-auth) - [ASP.NET Core IOC](/posts/servicestack-endpoint-routing#asp.net-core-ioc) - [Endpoint Routing](/posts/servicestack-endpoint-routing#endpoint-routing) - [Swashbuckle for Open API v3 and Swagger UI](/posts/openapi-v3-support) - [System.Text.Json APIs](/posts/system-text-json-apis) This reduces friction for integrating ServiceStack into existing .NET 8 Apps, encourages greater knowledge and reuse and simplifies .NET development as developers have a reduced number of concepts to learn, fewer technology implementations to configure and maintain that are now applied across their entire .NET App. The last integration piece supported was utilizing **System.Text.Json** - the default high-performance async JSON serializer used in .NET Applications, can now be used by ServiceStack APIs to serialize and deserialize its JSON API Responses that's enabled by default when using **Endpoint Routing**. This integrates ServiceStack APIs more than ever where just like Minimal APIs and Web API, uses **ASP.NET Core's IOC** to resolve dependencies, uses **Endpoint Routing** to Execute APIs that's secured with **ASP.NET Core Identity Auth** then uses **System.Text.Json** to deserialize and serialize its JSON payloads. ### Enabled by Default when using Endpoint Routing ```csharp app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); }); ``` ### Enhanced Configuration ServiceStack uses a custom `JsonSerializerOptions` to improve compatibility with existing ServiceStack DTOs and ServiceStack's rich ecosystem of generic [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) Service Clients, which is configured to: - Not serialize `null` properties - Supports Case Insensitive Properties - Uses `CamelCaseNamingPolicy` for property names - Serializes `TimeSpan` and `TimeOnly` Data Types with [XML Schema Time format](https://www.w3.org/TR/xmlschema-2/#isoformats) - Supports `[DataContract]` annotations - Supports Custom Enum Serialization ### Benefits all Add ServiceStack Reference Languages This compatibility immediately benefits all of ServiceStack's [Add ServiceStack Reference](https://docs.servicestack.net/add-servicestack-reference) native typed integrations for **11 programming languages** which all utilize ServiceStack's JSON API endpoints - now serialized with System.Text.Json ### Support for DataContract Annotations Support for .NET's `DataContract` serialization attributes was added using a custom `TypeInfoResolver`, specifically it supports: - `[DataContract]` - When annotated, only `[DataMember]` properties are serialized - `[DataMember]` - Specify a custom **Name** or **Order** of properties - `[IgnoreDataMember]` - Ignore properties from serialization - `[EnumMember]` - Specify a custom value for Enum values ### Custom Enum Serialization Below is a good demonstration of the custom Enum serialization support which matches ServiceStack.Text's behavior: ```csharp public enum EnumType { Value1, Value2, Value3 } [Flags] public enum EnumTypeFlags { Value1, Value2, Value3 } public enum EnumStyleMembers { [EnumMember(Value = "lower")] Lower, [EnumMember(Value = "UPPER")] Upper, } return new EnumExamples { EnumProp = EnumType.Value2, // String value by default EnumFlags = EnumTypeFlags.Value2 | EnumTypeFlags.Value3, // [Flags] as int EnumStyleMembers = EnumStyleMembers.Upper, // Serializes [EnumMember] value NullableEnumProp = null, // Ignores nullable enums }; ``` Which serializes to: ```json { "enumProp": "Value2", "enumFlags": 3, "enumStyleMembers": "UPPER" } ``` ### Custom Configuration You can further customize the `JsonSerializerOptions` used by ServiceStack by using `ConfigureJsonOptions()` to add any customizations that you can optionally apply to ASP.NET Core's JSON APIs and MVC with: ```csharp builder.Services.ConfigureJsonOptions(options => { options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; }) .ApplyToApiJsonOptions() // Apply to ASP.NET Core's JSON APIs .ApplyToMvcJsonOptions(); // Apply to MVC ``` ### Control over when and where System.Text.Json is used Whilst `System.Text.Json` is highly efficient, it's also very strict in the inputs it accepts where you may want to revert back to using ServiceStack's JSON Serializer for specific APIs, especially when you need to support external clients that can't be updated. This can done by annotating Request DTOs with `[SystemJson]` attribute, e.g: you can limit to only use `System.Text.Json` for an **APIs Response** with: ```csharp [SystemJson(UseSystemJson.Response)] public class CreateUser : IReturn { //... } ``` Or limit to only use `System.Text.Json` for an **APIs Request** with: ```csharp [SystemJson(UseSystemJson.Request)] public class CreateUser : IReturn { //... } ``` Or not use `System.Text.Json` at all for an API with: ```csharp [SystemJson(UseSystemJson.Never)] public class CreateUser : IReturn { //... } ``` ### JsonApiClient Support When Endpoints Routing is configured, the `JsonApiClient` will also be configured to utilize the same `System.Text.Json` options to send and receive its JSON API Requests which also respects the `[SystemJson]` specified behavior. Clients external to the .NET App can be configured to use `System.Text.Json` with: ```csharp ClientConfig.UseSystemJson = UseSystemJson.Always; ``` Whilst any custom configuration can be applied to its `JsonSerializerOptions` with: ```csharp TextConfig.ConfigureSystemJsonOptions(options => { options.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; }); ``` ### Scoped JSON Configuration We've also added partial support for [Customized JSON Responses](https://docs.servicestack.net/customize-json-responses) for the following customization options: :::{.table,w-full} | Name | Alias | |------------------------------|-------| | EmitCamelCaseNames | eccn | | EmitLowercaseUnderscoreNames | elun | | EmitPascalCaseNames | epcn | | ExcludeDefaultValues | edv | | IncludeNullValues | inv | | Indent | pp | ::: These can be applied to the JSON Response by returning a decorated `HttpResult` with a custom `ResultScope`, e.g: ```csharp return new HttpResult(responseDto) { ResultScope = () => JsConfig.With(new() { IncludeNullValues = true, ExcludeDefaultValues = true }) }; ``` They can also be requested by API consumers by adding a `?jsconfig` query string with the desired option or its alias, e.g: ```csharp /api/MyRequest?jsconfig=EmitLowercaseUnderscoreNames,ExcludeDefaultValues /api/MyRequest?jsconfig=eccn,edv ``` ### SystemJsonCompatible Another configuration automatically applied when `System.Text.Json` is enabled is: ```csharp JsConfig.SystemJsonCompatible = true; ``` Which is being used to make ServiceStack's JSON Serializer more compatible with `System.Text.Json` output so it's easier to switch between the two with minimal effort and incompatibility. Currently this is only used to override `DateTime` and `DateTimeOffset` behavior which uses `System.Text.Json` for its Serialization/Deserialization. # OpenAPI v3 and Swagger UI Source: https://razor-ssg.web-templates.io/posts/openapi-v3 In the ServiceStack v8.1 release, we have introduced a way to better incorporate your ServiceStack APIs into the larger ASP.NET Core ecosystem by mapping your ServiceStack APIs to standard [ASP.NET Core Endpoints](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-8.0#endpoints). This enables your ServiceStack APIs integrate with your larger ASP.NET Core application in the same way other middleware does, opening up more opportunities for reuse of your ServiceStack APIs. This opens up the ability to use common third party tooling. A good example of this is adding OpenAPI v3 specification generation for your endpoints offered by the `Swashbuckle.AspNetCore` package. :::youtube zAq9hp7ojn4 .NET 8 Open API v3 and Swagger UI ::: Included in the v8.1 Release is the `ServiceStack.AspNetCore.OpenApi` package to make this integration as easy as possible, and incorporate additional information from your ServiceStack APIs into Swagger metadata. ![](https://servicestack.net/img/posts/openapi-v3/openapi-v3-swagger-ui.png) Previously, without the ability to map Endpoints, we've maintained a ServiceStack specific OpenAPI specification generation via the `OpenApiFeature` plugin. While this provided a lot of functionality by accurately describing your ServiceStack APIs, it could be tricky to customize those API descriptions to the way some users wanted to. In this post we will look at how you can take advantage of the new OpenAPI v3 Swagger support using mapped Endpoints, customizing the generated specification, as well as touch on other related changes to ServiceStack v8.1. ## AppHost Initialization To use ServiceStack APIs as mapped Endpoints, the way ServiceStack is initialized in . To convert your App to use [Endpoint Routing and ASP.NET Core IOC](/posts/servicestack-endpoint-routing) your ASPNET Core application needs to be updated to replace any usage of `Funq` IoC container to use ASP.NET Core's IOC. Previously, the following was used to initialize your ServiceStack `AppHost`: #### Program.cs ```csharp app.UseServiceStack(new AppHost()); ``` The `app` in this example is a `WebApplication` resulting from an `IHostApplicationBuilder` calling `builder.Build()`. Whilst we still need to call `app.UseServiceStack()`, we also need to move the discovery of your ServiceStack APIs to earlier in the setup before the `WebApplication` is built, e.g: ```csharp // Register ServiceStack APIs, Dependencies and Plugins: services.AddServiceStack(typeof(MyServices).Assembly); var app = builder.Build(); //... // Register ServiceStack AppHost app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); }); app.Run(); ``` Once configured to use Endpoint Routing we can the [mix](https://docs.servicestack.net/mix-tool) tool to apply the [openapi3](https://gist.github.com/gistlyn/dac47b68e77796902cde0f0b7b9c6ac2) Startup Configuration with: :::sh x mix openapi3 ::: ### Manually Configure OpenAPI v3 and Swagger UI This will install the required ASP.NET Core Microsoft, Swashbuckle and ServiceStack Open API NuGet packages: ```xml ``` Then add the `Configure.OpenApi.cs` [Modular Startup](https://docs.servicestack.net/modular-startup) class to your project: ```csharp [assembly: HostingStartup(typeof(MyApp.ConfigureOpenApi))] namespace MyApp; public class ConfigureOpenApi : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { if (context.HostingEnvironment.IsDevelopment()) { services.AddEndpointsApiExplorer(); services.AddSwaggerGen(); // Swashbuckle services.AddServiceStackSwagger(); services.AddBasicAuth(); // Enable HTTP Basic Auth //services.AddJwtAuth(); // Enable & Use JWT Auth services.AddTransient(); } }); public class StartupFilter : IStartupFilter { public Action Configure(Action next) => app => { // Provided by Swashbuckle library app.UseSwagger(); app.UseSwaggerUI(); next(app); }; } } ``` All this setup is done for you in ServiceStack's updated [Identity Auth .NET 8 Templates](https://servicestack.net/start), but for existing applications, you will need to do [convert to use Endpoint Routing](https://docs.servicestack.net/endpoints-migration) to support this new way of running your ServiceStack applications. ## More Control One point of friction with our previous `OpenApiFeature` plugin was the missing customization ability to the OpenAPI spec to somewhat disconnect from the defined ServiceStack service, and related C# Request and Response Data Transfer Objects (DTOs). Since the `OpenApiFeature` plugin used class and property attributes on your Request DTOs, making the *structure* of the OpenAPI schema mapping quite ridged, preventing the ability for certain customizations. For example, if we have an `UpdateTodo` Request DTO that looks like the following: ```csharp [Route("/todos/{Id}", "PUT")] public class UpdateTodo : IPut, IReturn { public long Id { get; set; } [ValidateNotEmpty] public string Text { get; set; } public bool IsFinished { get; set; } } ``` Previously, we would get a default Swagger UI that enabled all the properties as `Paramters` to populate. ![](https://servicestack.net/img/posts/openapi-v3/openapi-v2-defaults.png) While this correctly describes the Request DTO structure, sometimes as developers we get requirements for how we want to present our APIs to our users from within the Swagger UI. With the updated SwaggerUI, and the use of the `Swashbuckle` library, we get the following UI by default. ![](https://servicestack.net/img/posts/openapi-v3/openapi-v3-defaults-application-json.png) These are essentially the same, we have a CRUD Todo API that takes a `UpdateTodo` Request DTO, and returns a `Todo` Response DTO. ServiceStack needs to have uniquely named Request DTOs, so we can't have a `Todo` schema as the Request DTO despite the fact that it is the same structure as our `Todo` model. This is a good thing, as it allows us to have a clean API contract, and separation of concerns between our Request DTOs and our models. However, it might not be desired to present this to our users, since it can be convenient to think about CRUD services as taking the same resource type as the response. To achieve this, we use the Swashbuckle library to customize the OpenAPI spec generation. Depending on what you want to customize, you can use the `SchemaFilter` or `OperationFilter` options. In this case, we want to customize the matching operation to reference the `Todo` schema for the Request Body. First, we create a new class that implements the `IOperationFilter` interface. ```csharp public class OperationRenameFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { if (context.ApiDescription.HttpMethod == "PUT" && context.ApiDescription.RelativePath == "todos/{Id}") { operation.RequestBody.Content["application/json"].Schema.Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = "Todo" }; } } } ``` The above matches some information about the `UpdateTodo` request we want to customize, and then sets the `Reference` property of the `RequestBody` to the `Todo` schema. We can then add this to the `AddSwaggerGen` options in the `Program.cs` file. ```csharp builder.Services.AddSwaggerGen(o => { o.OperationFilter(); }); ``` The result is the following Swagger UI. ![](https://servicestack.net/img/posts/openapi-v3/openapi-v3-customized-application-json.png) This is just one simple example of how you can customize the OpenAPI spec generation, and `Swashbuckle` has some great documentation on the different ways you can customize the generated spec. And these customizations impact any of your ASP.NET Core Endpoints, not just your ServiceStack APIs. ## Closing Now that ServiceStack APIs can be mapped to standard ASP.NET Core Endpoints, it opens up a lot of possibilities for integrating your ServiceStack APIs into the larger ASP.NET Core ecosystem. The use of the `Swashbuckle` library via the `ServiceStack.AspNetCore.OpenApi` library is just one example of how you can take advantage of this new functionality. # ServiceStack Endpoint Routing Source: https://razor-ssg.web-templates.io/posts/servicestack-endpoint-routing In an effort to reduce friction and improve integration with ASP.NET Core Apps, we've continued the trend from last year for embracing ASP.NET Core's built-in features and conventions which saw the latest ServiceStack v8 release converting all its newest .NET 8 templates to adopt [ASP.NET Core Identity Auth](https://docs.servicestack.net/auth/identity-auth). This is a departure from building upon our own platform-agnostic abstractions which allowed the same ServiceStack code-base to run on both .NET Core and .NET Framework. Our focus going forward will be to instead adopt De facto standards and conventions of the latest .NET platform which also means ServiceStack's new value-added features are only available in the latest **.NET 8+** release. ### ServiceStack Middleware Whilst ServiceStack integrates into ASP.NET Core Apps as custom middleware into ASP.NET Core's HTTP Request Pipeline, it invokes its own black-box of functionality from there, implemented using its own suite of overlapping features. Whilst this allows ServiceStack to have full control over how to implement its features, it's not as integrated as it could be, with there being limits on what ServiceStack Functionality could be reused within external ASP .NET Core MVC Controllers, Razor Pages, etc. and inhibited the ability to apply application-wide authorization policies across an Application entire surface area, using and configuring different JSON Serialization implementations. ### Areas for tighter integration The major areas we've identified that would benefit from tighter integration with ASP.NET Core include: - [Funq IOC Container](https://docs.servicestack.net/ioc) - [ServiceStack Routing](https://docs.servicestack.net/routing) and [Request Pipeline](https://docs.servicestack.net/order-of-operations) - [ServiceStack.Text JSON Serializer](https://docs.servicestack.net/json-format) ### ServiceStack v8.1 is fully integrated! We're happy to announce the latest release of ServiceStack v8.1 now supports utilizing the optimal ASP.NET Core's standardized features to reimplement all these key areas - fostering seamless integration and greater reuse which you can learn about below: - [ASP.NET Core Identity Auth](https://docs.servicestack.net/auth/identity-auth) - [ASP.NET Core IOC](https://docs.servicestack.net/releases/v8_01#asp.net-core-ioc) - [Endpoint Routing](https://docs.servicestack.net/releases/v8_01#endpoint-routing) - [System.Text.Json APIs](https://docs.servicestack.net/releases/v8_01#system.text.json) - [Open API v3 and Swagger UI](https://docs.servicestack.net/releases/v8_01#openapi-v3) - [ASP.NET Core Identity Auth Admin UI](https://docs.servicestack.net/releases/v8_01#asp.net-core-identity-auth-admin-ui) - [JWT Identity Auth](https://docs.servicestack.net/releases/v8_01#jwt-identity-auth) Better yet, this new behavior is enabled by default in all of ServiceStack's new ASP .NET Identity Auth .NET 8 templates! ### Migrating to ASP.NET Core Endpoints To assist ServiceStack users in upgrading their existing projects we've created a migration guide walking through the steps required to adopt these new defaults: :::youtube RaDHkk4tfdU Upgrade your APIs to use ASP.NET Core Endpoints ::: ### ASP .NET Core IOC The primary limitation of ServiceStack using its own Funq IOC is that any dependencies registered in Funq are not injected into Razor Pages, Blazor Components, MVC Controllers, etc. That's why our [Modular Startup](https://docs.servicestack.net/modular-startup) configurations recommend utilizing custom `IHostingStartup` configurations to register application dependencies in ASP .NET Core's IOC where they can be injected into both ServiceStack Services and ASP.NET Core's external components, e.g: ```csharp [assembly: HostingStartup(typeof(MyApp.ConfigureDb))] namespace MyApp; public class ConfigureDb : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { services.AddSingleton(new OrmLiteConnectionFactory( context.Configuration.GetConnectionString("DefaultConnection"), SqliteDialect.Provider)); }); } ``` But there were fundamental restrictions on what could be registered in ASP .NET Core's IOC as everything needed to be registered before AspNetCore's `WebApplication` was built and before ServiceStack's AppHost could be initialized, which prohibited being able to register any dependencies created by the AppHost including Services, AutoGen Services, Validators and internal functionality like App Settings, Virtual File System and Caching providers, etc. ## Switch to use ASP .NET Core IOC To enable ServiceStack to switch to using ASP .NET Core's IOC you'll need to move registration of all dependencies and Services to before the WebApplication is built by calling the `AddServiceStack()` extension method with the Assemblies where your ServiceStack Services are located, e.g: ```csharp builder.Services.AddServiceStack(typeof(MyServices).Assembly); var app = builder.Build(); //... app.UseServiceStack(new AppHost()); ``` Which now registers all ServiceStack dependencies in ASP .NET Core's IOC, including all ServiceStack Services prior to the AppHost being initialized which no longer needs to specify the Assemblies where ServiceStack Services are created and no longer needs to use Funq as all dependencies should now be registered in ASP .NET Core's IOC. ### Registering Dependencies and Plugins Additionally ASP.NET Core's IOC requirement for all dependencies needing to be registered before the WebApplication is built means you'll no longer be able to register any dependencies or plugins in ServiceStack's `AppHost.Configure()` method. ```csharp public class AppHost() : AppHostBase("MyApp"), IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { // Register IOC Dependencies and ServiceStack Plugins }); public override void Configure() { // DO NOT REGISTER ANY PLUGINS OR DEPENDENCIES HERE } } ``` Instead anything that needs to register dependencies in ASP.NET Core IOC should now use the `IServiceCollection` extension methods: - Use `IServiceCollection.Add*` APIs to register dependencies - Use `IServiceCollection.AddPlugin` API to register ServiceStack Plugins - Use `IServiceCollection.RegisterService*` APIs to dynamically register ServiceStack Services in external Assemblies This can be done whenever you have access to `IServiceCollection`, either in `Program.cs`: ```csharp builder.Services.AddPlugin(new AdminDatabaseFeature()); ``` Or in any Modular Startup `IHostingStartup` configuration class, e.g: ```csharp public class ConfigureDb : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices((context, services) => { services.AddSingleton(new OrmLiteConnectionFactory( context.Configuration.GetConnectionString("DefaultConnection"), SqliteDialect.Provider)); // Enable Audit History services.AddSingleton(c => new OrmLiteCrudEvents(c.GetRequiredService())); // Enable AutoQuery RDBMS APIs services.AddPlugin(new AutoQueryFeature { MaxLimit = 1000, }); // Enable AutoQuery Data APIs services.AddPlugin(new AutoQueryDataFeature()); // Enable built-in Database Admin UI at /admin-ui/database services.AddPlugin(new AdminDatabaseFeature()); }) .ConfigureAppHost(appHost => { appHost.Resolve().InitSchema(); }); } ``` The `ConfigureAppHost()` extension method can continue to be used to execute any startup logic that requires access to registered dependencies. ### Authoring ServiceStack Plugins To enable ServiceStack Plugins to support both Funq and ASP .NET Core IOC, any dependencies and Services a plugin needs should be registered in the `IConfigureServices.Configure(IServiceCollection)` method as seen in the refactored [ServerEventsFeature.cs](https://github.com/ServiceStack/ServiceStack/blob/main/ServiceStack/src/ServiceStack/ServerEventsFeature.cs) plugin, e.g: ```csharp public class ServerEventsFeature : IPlugin, IConfigureServices { //... public void Configure(IServiceCollection services) { if (!services.Exists()) { services.AddSingleton(new MemoryServerEvents { IdleTimeout = IdleTimeout, HouseKeepingInterval = HouseKeepingInterval, OnSubscribeAsync = OnSubscribeAsync, OnUnsubscribeAsync = OnUnsubscribeAsync, OnUpdateAsync = OnUpdateAsync, NotifyChannelOfSubscriptions = NotifyChannelOfSubscriptions, Serialize = Serialize, OnError = OnError, }); } if (UnRegisterPath != null) services.RegisterService(UnRegisterPath); if (SubscribersPath != null) services.RegisterService(SubscribersPath); } public void Register(IAppHost appHost) { //... } } ``` #### All Plugins refactored to support ASP .NET Core IOC All of ServiceStack's Plugins have been refactored to make use of `IConfigureServices` which supports registering in both Funq and ASP.NET Core's IOC when enabled. #### Funq IOC implements IServiceCollection and IServiceProvider interfaces To enable this Funq now implements both `IServiceCollection` and`IServiceProvider` interfaces to enable 100% source-code compatibility for registering and resolving dependencies with either IOC, which we now recommend using over Funq's native Registration and Resolution APIs to simplify migration efforts to ASP.NET Core's IOC in future. ## Dependency Injection The primary difference between the IOC's is that ASP.NET Core's IOC does not support property injection by default, which will require you to refactor your ServiceStack Services to use constructor injection of dependencies, although this has become a lot more pleasant with C# 12's [Primary Constructors](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/primary-constructors) which now requires a lot less boilerplate to define, assign and access dependencies, e.g: ```csharp public class TechStackServices(IAutoQueryDb autoQuery) : Service { public async Task Any(QueryTechStacks request) { using var db = autoQuery.GetDb(request, base.Request); var q = autoQuery.CreateQuery(request, Request, db); return await autoQuery.ExecuteAsync(request, q, db); } } ``` This has become our preferred approach for injecting dependencies in ServiceStack Services which have all been refactored to use constructor injection utilizing primary constructors in order to support both IOC's. To make migrations easier we've also added support for property injection convention in **ServiceStack Services** using ASP.NET Core's IOC where you can add the `[FromServices]` attribute to any public properties you want to be injected, e.g: ```csharp public class TechStackServices : Service { [FromServices] public required IAutoQueryDb AutoQuery { get; set; } [FromServices] public MyDependency? OptionalDependency { get; set; } } ``` This feature can be useful for Services wanting to access optional dependencies that may or may not be registered. :::info NOTE `[FromServices]` is only supported in ServiceStack Services (i.e. not other dependencies) ::: ### Built-in ServiceStack Dependencies This integration now makes it effortless to inject and utilize optional ServiceStack features like [AutoQuery](https://docs.servicestack.net/autoquery/) and [Server Events](https://docs.servicestack.net/server-events) in other parts of ASP.NET Core inc. Blazor Components, Razor Pages, MVC Controllers, Minimal APIs, etc. Whilst the Built-in ServiceStack features that are registered by default and immediately available to be injected, include: - `IVirtualFiles` - Read/Write [Virtual File System](https://docs.servicestack.net/virtual-file-system), defaults to `FileSystemVirtualFiles` at `ContentRootPath` - `IVirtualPathProvider` - Multi Virtual File System configured to scan multiple read only sources, inc `WebRootPath`, In Memory and Embedded Resource files - `ICacheClient` and `ICacheClientAsync` - In Memory Cache, or distributed Redis cache if [ServiceStack.Redis](https://docs.servicestack.net/redis/) is configured - `IAppSettings` - Multiple [AppSettings](https://docs.servicestack.net/appsettings) configuration sources With ASP.NET Core's IOC now deeply integrated we moved onto the next area of integration: API Integration and Endpoint Routing. ## Endpoint Routing Whilst ASP.NET Core's middleware is a flexible way to compose and execute different middleware in a HTTP Request pipeline, each middleware is effectively their own island of functionality that's able to handle HTTP Requests in which ever way they see fit. In particular ServiceStack's middleware would execute its own [Request Pipeline](https://docs.servicestack.net/order-of-operations) which would execute ServiceStack API's registered at user-defined routes with its own [ServiceStack Routing](https://docs.servicestack.net/routing). We're happy to announce that ServiceStack **.NET 8** Apps support an entirely new and integrated way to run all of ServiceStack requests including all APIs, metadata and built-in UIs with support for [ASP.NET Core Endpoint Routing](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing) - enabled by calling the `MapEndpoints()` extension method when configuring ServiceStack, e.g: ```csharp app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(); }); ``` Which configures ServiceStack APIs to be registered and executed along-side Minimal APIs, Razor Pages, SignalR, MVC and Web API Controllers, etc, utilizing the same routing, metadata and execution pipeline. #### View ServiceStack APIs along-side ASP.NET Core APIs Amongst other benefits, this integration is evident in endpoint metadata explorers like the `Swashbuckle` library which can now show ServiceStack APIs in its Swagger UI along-side other ASP.NET Core APIs in ServiceStack's new [Open API v3](/posts/openapi-v3) support. ### Routing Using Endpoint Routing also means using ASP.NET Core's Routing System which now lets you use ASP.NET Core's [Route constraints](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#route-constraints) for defining user-defined routes for your ServiceStack APIs, e.g: ```csharp [Route("/users/{Id:int}")] [Route("/users/{UserName:string}")] public class GetUser : IGet, IReturn { public int? Id { get; set; } public int? UserName { get; set; } } ``` For the most part ServiceStack Routing implements a subset of ASP.NET Core's Routing features so your existing user-defined routes should continue to work as expected. ### Wildcard Routes The only incompatibility we found was when using wildcard paths which in ServiceStack Routing would use an '*' suffix, e.g: `[Route("/wildcard/{Path*}")]` which will need to change to use a ASP.NET Core's Routing prefix, e.g: ```csharp [Route("/wildcard/{*Path}")] [Route("/wildcard/{**Path}")] public class GetFile : IGet, IReturn { public string Path { get; set; } } ``` #### ServiceStack Routing Compatibility To improve compatibility with ASP.NET Core's Routing, ServiceStack's Routing (when not using Endpoint Routing) now supports parsing ASP.NET Core's Route Constraints but as they're inert you would need to continue to use [Custom Route Rules](https://docs.servicestack.net/routing#custom-rules) to distinguish between different routes matching the same path at different specificity: ```csharp [Route("/users/{Id:int}", Matches = "**/{int}")] [Route("/users/{UserName:string}")] public class GetUser : IGet, IReturn { public int? Id { get; set; } public int? UserName { get; set; } } ``` It also supports defining Wildcard Routes using ASP.NET Core's syntax which we now recommend using instead for compatibility when switching to use Endpoint Routing: ```csharp [Route("/wildcard/{*Path}")] [Route("/wildcard/{**Path}")] public class GetFile : IGet, IReturn { public string Path { get; set; } } ``` ### Primary HTTP Method Another difference is that an API will only register its Endpoint Route for its [primary HTTP Method](https://docs.servicestack.net/api-design#all-apis-have-a-preferred-default-method), if you want an API to be registered for multiple HTTP Methods you can specify them in the `Route` attribute, e.g: ```csharp [Route("/users/{Id:int}", "GET,POST")] public class GetUser : IGet, IReturn { public required int Id { get; set; } } ``` As such we recommend using the IVerb `IGet`, `IPost`, `IPut`, `IPatch`, `IDelete` interface markers to specify the primary HTTP Method for an API. This isn't needed for [AutoQuery Services](https://docs.servicestack.net/autoquery/) which are implicitly configured to use their optimal HTTP Method. If no HTTP Method is specified, the Primary HTTP Method defaults to HTTP **POST**. ### Authorization Using Endpoint Routing also means ServiceStack's APIs are authorized the same way, where ServiceStack's [Declarative Validation attributes](https://docs.servicestack.net/auth/#declarative-validation-attributes) are converted into ASP.NET Core's `[Authorize]` attribute to secure the endpoint: ```csharp [ValidateIsAuthenticated] [ValidateIsAdmin] [ValidateHasRole(role)] [ValidateHasClaim(type,value)] [ValidateHasScope(scope)] public class Secured {} ``` #### Authorize Attribute on ServiceStack APIs Alternatively you can now use ASP.NET Core's `[Authorize]` attribute directly to secure ServiceStack APIs should you need more fine-grained Authorization: ```csharp [Authorize(Roles = "RequiredRole")] [Authorize(Policy = "RequiredPolicy")] [Authorize(AuthenticationSchemes = "Identity.Application,Bearer")] public class Secured {} ``` #### Configuring Authentication Schemes ServiceStack will default to using the major Authentication Schemes configured for your App to secure the APIs endpoint with, this can be overridden to specify which Authentication Schemes to use to restrict ServiceStack APIs by default, e.g: ```csharp app.UseServiceStack(new AppHost(), options => { options.AuthenticationSchemes = "Identity.Application,Bearer"; options.MapEndpoints(); }); ``` ### Hidden ServiceStack Endpoints Whilst ServiceStack Requests are registered and executed as endpoints, most of them are marked with `builder.ExcludeFromDescription()` to hide them from polluting metadata and API Explorers like Swagger UI and [API Explorer](https://docs.servicestack.net/api-explorer). To also hide your ServiceStack APIs you can use `[ExcludeMetadata]` attribute to hide them from all metadata services or use `[Exclude(Feature.ApiExplorer)]` to just hide them from API Explorer UIs: ```csharp [ExcludeMetadata] [Exclude(Feature.ApiExplorer)] public class HiddenRequest {} ``` ### Content Negotiation An example of these hidden routes is the support for invoking and returning ServiceStack APIs in different Content Types via hidden Endpoint Routes mapped with the format `/api/{Request}.{format}`, e.g: - [/api/QueryBookings](https://blazor-vue.web-templates.io/api/QueryBookings) - [/api/QueryBookings.jsonl](https://blazor-vue.web-templates.io/api/QueryBookings.jsonl) - [/api/QueryBookings.csv](https://blazor-vue.web-templates.io/api/QueryBookings.csv) - [/api/QueryBookings.xml](https://blazor-vue.web-templates.io/api/QueryBookings.xml) - [/api/QueryBookings.html](https://blazor-vue.web-templates.io/api/QueryBookings.html) #### Query String Format That continues to support specifying the Mime Type via the `?format` query string, e.g: - [/api/QueryBookings?format=jsonl](https://blazor-vue.web-templates.io/api/QueryBookings?format=jsonl) - [/api/QueryBookings?format=csv](https://blazor-vue.web-templates.io/api/QueryBookings?format=csv) ### Predefined Routes Endpoints are only created for the newer `/api/{Request}` [pre-defined routes](https://docs.servicestack.net/routing#pre-defined-routes), which should be easier to use with less conflicts now that ServiceStack APIs are executed along-side other endpoint routes APIs which can share the same `/api` base path with non-conflicting routes, e.g: `app.MapGet("/api/minimal-api")`. As a result clients configured to use the older `/json/reply/{Request}` pre-defined route will need to be configured to use the newer `/api` base path. No change is required for C#/.NET clients using the recommended `JsonApiClient` JSON Service Client which is already configured to use the newer `/api` base path. ```csharp var client = new JsonApiClient(baseUri); ``` Older .NET clients can be configured to use the newer `/api` pre-defined routes with: ```csharp var client = new JsonServiceClient(baseUri) { UseBasePath = "/api" }; var client = new JsonHttpClient(baseUri) { UseBasePath = "/api" }; ``` To further solidify that `/api` as the preferred pre-defined route we've also **updated all generic service clients** of other languages to use `/api` base path by default: #### JavaScript/TypeScript ```ts const client = new JsonServiceClient(baseUrl) ``` #### Dart ```dart var client = ClientFactory.api(baseUrl); ``` #### Java/Kotlin ```java JsonServiceClient client = new JsonServiceClient(baseUrl); ``` #### Python ```python client = JsonServiceClient(baseUrl) ``` #### PHP ```php $client = new JsonServiceClient(baseUrl); ``` ### Revert to Legacy Predefined Routes You can unset the base path to revert back to using the older `/json/reply/{Request}` pre-defined route, e.g: #### JavaScript/TypeScript ```ts client.basePath = null; ``` #### Dart ```dart var client = ClientFactory.create(baseUrl); ``` #### Java/Kotlin ```java client.setBasePath(); ``` #### Python ```python client.set_base_path() ``` #### PHP ```php $client->setBasePath(); ``` ### Customize Endpoint Mapping You can register a RouteHandlerBuilders to customize how ServiceStack APIs endpoints are registered which is also what ServiceStack uses to annotate its API endpoints to enable its new [Open API v3](/posts/openapi-v3) support: ```csharp options.RouteHandlerBuilders.Add((builder, operation, method, route) => { builder.WithOpenApi(op => { ... }); }); ``` ### Endpoint Routing Compatibility Levels The default behavior of `MapEndpoints()` is the strictest and recommended configuration that we want future ServiceStack Apps to use, however if you're migrating existing App's you may want to relax these defaults to improve compatibility with existing behavior. The configurable defaults for mapping endpoints are: ```csharp app.UseServiceStack(new AppHost(), options => { options.MapEndpoints(use:true, force:true, useSystemJson:UseSystemJson.Always); }); ``` - `use` - Whether to use registered endpoints for executing ServiceStack APIs - `force` - Whether to only allow APIs to be executed through endpoints - `useSystemJson` - Whether to use System.Text.Json for JSON API Serialization So you could for instance register endpoints and not `use` them, where they'll be visible in endpoint API explorers like [Swagger UI](https://docs.servicestack.net/releases/v8_01#openapi-v3) but continue to execute in ServiceStack's Request Pipeline. `force` disables fallback execution of ServiceStack Requests through ServiceStack's Request Pipeline for requests that don't match registered endpoints. You may need to disable this if you have clients calling ServiceStack APIs through multiple HTTP Methods, as only the primary HTTP Method is registered as an endpoint. When enabled `force` ensures the only ServiceStack Requests that are not executed through registered endpoints are `IAppHost.CatchAllHandlers` and `IAppHost.FallbackHandler` handlers. `useSystemJson` is a new feature that lets you specify when to use `System.Text.Json` for JSON API Serialization, which is our next exciting feature to standardize on using [ASP.NET Core's fast async System.Text.Json](https://docs.servicestack.net/releases/v8_01#system.text.json) Serializer. ## Endpoint Routing Everywhere Whilst the compatibility levels of Endpoint Routing can be relaxed, we recommend new projects use the strictest and most integrated defaults that's now configured on all [ASP.NET Core Identity Auth .NET 8 Projects](/start). For additional testing we've also upgraded many of our existing .NET Example Applications, which are now all running with our latest recommended Endpoint Routing configuration: - [BlazorDiffusionVue](https://github.com/NetCoreApps/BlazorDiffusionVue) - [BlazorDiffusionAuto](https://github.com/NetCoreApps/BlazorDiffusionAuto) - [TalentBlazor](https://github.com/NetCoreApps/TalentBlazor) - [TechStacks](https://github.com/NetCoreApps/TechStacks) - [Validation](https://github.com/NetCoreApps/Validation) - [NorthwindAuto](https://github.com/NetCoreApps/NorthwindAuto) - [FileBlazor](https://github.com/NetCoreApps/FileBlazor) - [Chinook](https://github.com/NetCoreApps/Chinook) - [Chat](https://github.com/NetCoreApps/Chat) # New Blogging features in Razor SSG Source: https://razor-ssg.web-templates.io/posts/razor-ssg-new-blog-features ## New Blogging features in Razor SSG [Razor SSG](https://razor-ssg.web-templates.io) is our Free Project Template for creating fast, statically generated Websites and Blogs with Markdown & C# Razor Pages. A benefit of using Razor SSG to maintain this [servicestack.net(github)](https://github.com/ServiceStack/servicestack.net) website is that any improvements added to our website end up being rolled into the Razor SSG Project Template for everyone else to enjoy. This latest release brings a number of features and enhancements to improve Razor SSG usage as a Blogging Platform - a primary use-case we're focused on as we pen our [22nd Blog Post for the year](https://servicestack.net/posts/year/2023) with improvements in both discoverability and capability of blog posts: ### RSS Feed Razor SSG websites now generates a valid RSS Feed for its blog to support their readers who'd prefer to read blog posts and notify them as they're published with their favorite RSS reader: ### Meta Headers support for Twitter cards and SEO Blog Posts and Pages now include additional `` HTML Headers to enable support for [Twitter Cards](https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards) in both Twitter and Meta's new [threads.net](https://threads.net), e.g: ### Improved Discoverability To improve discoverability and increase site engagement, bottom of blog posts now include links to other posts by the same Blog Author, including links to connect to their preferred social networks and contact preferences: ![](https://servicestack.net/img/posts/razor-ssg/other-author-posts.png) ### Posts can include Vue Components Blog Posts can now embed any global Vue Components directly in its Markdown, e.g: ```html ``` #### [/mjs/components/GettingStarted.mjs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/mjs/components/GettingStarted.mjs)
#### Individual Blog Post dependencies Just like Pages and Docs they can also include specific JavaScript **.mjs** or **.css** in the `/wwwroot/posts` folder which will only be loaded for that post: Now posts that need it can dynamically load large libraries like [Chart.js](https://www.chartjs.org) and use it inside a custom Vue component by creating a custom `/posts/.mjs` that exports what components and features your blog post needs, e.g: #### [/posts/new-blog-features.mjs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/posts/new-blog-features.mjs) ```js import ChartJs from './components/ChartJs.mjs' export default { components: { ChartJs } } ``` In this case it enables support for [Chart.js](https://www.chartjs.org) by including a custom Vue component that makes it easy to create charts from Vue Components embedded in markdown: #### [/posts/components/ChartJs.mjs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/posts/components/ChartJs.mjs) ```js import { ref, onMounted } from "vue" import { addScript } from "@servicestack/client" let loadJs = addScript('https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js') export default { template:`
`, props:['type','data','options'], setup(props) { const chart = ref() onMounted(async () => { await loadJs const options = props.options || { responsive: true, legend: { position: "top" } } new Chart(chart.value, { type: props.type || "bar", data: props.data, options, }) }) return { chart } } } ``` Which allows this post to embed Chart.js charts using the above custom `` Vue component and a JS Object literal, e.g: ```html ``` Which the [Bulk Insert Performance](https://servicestack.net/posts/bulk-insert-performance) Blog Post uses extensively to embeds its Chart.js Bar charts: ### New Markdown Containers [Custom Containers](https://github.com/xoofx/markdig/blob/master/src/Markdig.Tests/Specs/CustomContainerSpecs.md) are a popular method for implementing Markdown Extensions for enabling rich, wrist-friendly consistent content in your Markdown documents. Most of [VitePress Markdown Containers](https://vitepress.dev/guide/markdown#custom-containers) are also available in Razor SSG websites for enabling rich, wrist-friendly consistent markup in your Markdown pages, e.g: ```md ::: info This is an info box. ::: ::: tip This is a tip. ::: ::: warning This is a warning. ::: ::: danger This is a dangerous warning. ::: :::copy Copy Me! ::: ``` ::: info This is an info box. ::: ::: tip This is a tip. ::: ::: warning This is a warning. ::: ::: danger This is a dangerous warning. ::: :::copy Copy Me! ::: See Razor Press's [Markdown Containers docs](https://razor-press.web-templates.io/containers) for the complete list of available containers and examples on how to implement your own [Custom Markdown containers](https://razor-press.web-templates.io/containers#implementing-block-containers). ### Support for Includes Markdown fragments can be added to `_pages/_include` - a special folder rendered with [Pages/Includes.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Includes.cshtml) using an [Empty Layout](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Shared/_LayoutEmpty.cshtml) which can be included in other Markdown and Razor Pages or fetched on demand with Ajax. Markdown Fragments can be then included inside other markdown documents with the `::include` inline container, e.g: :::pre ::include vue/formatters.md:: ::: Where it will be replaced with the HTML rendered markdown contents of fragments maintained in `_pages/_include`. ### Include Markdown in Razor Pages Markdown Fragments can also be included in Razor Pages using the custom `MarkdownTagHelper.cs` `` tag: ```html ``` ### Inline Markdown in Razor Pages Alternatively markdown can be rendered inline with: ```html ## Using Formatters Your App and custom templates can also utilize @servicestack/vue's [built-in formatting functions](href="/vue/use-formatters). ``` ### Light and Dark Mode Query Params You can link to Dark and Light modes of your Razor SSG website with the `?light` and `?dark` query string params: - [https://razor-ssg.web-templates.io/?dark](https://razor-ssg.web-templates.io/?dark) - [https://razor-ssg.web-templates.io/?light](https://razor-ssg.web-templates.io/?light) ### Blog Post Authors threads.net and Mastodon links The social links for Blog Post Authors can now include [threads.net](https://threads.net) and [mastodon.social](https://mastodon.social) links, e.g: ```json { "AppConfig": { "BlogImageUrl": "https://servicestack.net/img/logo.png", "Authors": [ { "Name": "Lucy Bates", "Email": "lucy@email.org", "ProfileUrl": "img/authors/author1.svg", "TwitterUrl": "https://twitter.com/lucy", "ThreadsUrl": "https://threads.net/@lucy", "GitHubUrl": "https://github.com/lucy", "MastodonUrl": "https://mastodon.social/@lucy" } ] } } ``` ## Feature Requests Welcome Most of Razor SSG's features are currently being driven by requirements from the new [Websites built with Razor SSG](https://razor-ssg.web-templates.io/#showcase) and features we want available in our Blogs, we welcome any requests for missing features in other popular Blogging Platforms you'd like to see in Razor SSG to help make it a high quality blogging solution built with our preferred C#/.NET Technology Stack, by submitting them to: :::{.text-indigo-600 .text-3xl .text-center} [https://servicestack.net/ideas](https://servicestack.net/ideas) ::: ### SSG or Dynamic Features Whilst statically generated websites and blogs are generally limited to features that can be generated at build time, we're able to add any dynamic features we need in [CreatorKit](https://servicestack.net/creatorkit/) - a Free companion self-host .NET App Mailchimp and Disqus alternative which powers any dynamic functionality in Razor SSG Apps like the blogs comments and Mailing List features in this Blog Post. # Introducing Razor SSG Source: https://razor-ssg.web-templates.io/posts/razor-ssg Razor SSG is a Razor Pages powered Markdown alternative to [Ruby's Jekyll](https://jekyllrb.com/) & [Next.js](https://nextjs.org) that's ideal for generating static websites & blogs using C#, Razor Pages & Markdown. ### GitHub Codespaces Friendly In addition to having a pure Razor + .NET solution to create fast, CDN-hostable static websites, it also aims to provide a great experience from GitHub Codespaces, where you can create, modify, preview & check-in changes before the included GitHub Actions auto deploy changes to its GitHub Pages CDN - all from your iPad! [![](https://servicestack.net/img/posts/razor-ssg/codespaces.png)](https://github.com/features/codespaces) To see this in action, we walk through the entire workflow of creating, updating and adding features to a custom Razor SSG website from just a browser using Codespaces, that auto publishes changes to your GitHub Repo's **gh-pages** branch where it's hosted for free on GitHub Pages CDN: ### Enhance with simple, modern JavaScript For enhanced interactivity, static markdown content can be [progressively enhanced](https://servicestack.net/posts/javascript) with Vue 3 components, as done in this example that embed's the [GettingStarted.mjs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/mjs/components/GettingStarted.mjs) Vue Component to create new Razor SSG App's below with: ```html ```
Although with full control over the websites `_Layout.cshtml`, you're free to use any preferred JS Module or Web Component you prefer. ## Razor Pages Your website can be built using either Markdown `.md` or Razor `.cshtml` pages, although it's generally recommended to use Markdown to capture the static content for your website for improved productivity and ease of maintenance. ### Content in Markdown, Functionality in Razor Pages The basic premise behind most built-in features is to capture static content in markdown using a combination of folder structure & file name conventions in addition to each markdown page's frontmatter & content. This information is then used to power each feature using Razor pages for precise layout and functionality. The template includes the source code for each website feature, enabling full customization that also serves as good examples for how to implement your own custom markdown-powered website features. ### Markdown Feature Structure All markdown features are effectively implemented in the same way, starting with a **_folder** for maintaining its static markdown content, a **.cs** class to load the markdown and a **.cshtml** Razor Page to render it: | Location | Description | | - | - | | `/_{Feature}` | Maintains the static markdown for the feature | | `Markdown.{Feature}.cs` | Functionality to read the feature's markdown into logical collections | | `{Feature}.cshtml` | Functionality to Render the feature | | [Configure.Ssg.cs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Configure.Ssg.cs) | Initializes and registers the feature with ASP .NET's IOC | Lets see what this looks like in practice by walking through the "Pages" feature: ## Pages Feature The pages feature simply makes all pages in the **_pages** folder, available from `/{filename}`. Where the included pages: ### [/_pages](https://github.com/NetCoreTemplates/razor-ssg/tree/main/MyApp/_pages) - privacy.md - speaking.md - uses.md Are made available from: - [/privacy](https://razor-ssg.web-templates.io/privacy) - [/speaking](https://razor-ssg.web-templates.io/speaking) - [/uses](https://razor-ssg.web-templates.io/uses) ### Loading Pages Markdown The code that loads the Pages feature markdown content is in [Markdown.Pages.cs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Markdown.Pages.cs): ```csharp public class MarkdownPages : MarkdownPagesBase { public MarkdownPages(ILogger log, IWebHostEnvironment env) : base(log,env) {} List Pages { get; set; } = new(); public List VisiblePages => Pages.Where(IsVisible).ToList(); public MarkdownFileInfo? GetBySlug(string slug) => Fresh(VisiblePages.FirstOrDefault(x => x.Slug == slug)); public void LoadFrom(string fromDirectory) { Pages.Clear(); var fs = AssertVirtualFiles(); var files = fs.GetDirectory(fromDirectory).GetAllFiles().ToList(); var log = LogManager.GetLogger(GetType()); log.InfoFormat("Found {0} pages", files.Count); var pipeline = CreatePipeline(); foreach (var file in files) { var doc = Load(file.VirtualPath, pipeline); if (doc == null) continue; Pages.Add(doc); } } } ``` Which ultimately just loads Markdown files using the configured [Markdig](https://github.com/xoofx/markdig) pipeline in its `Pages` collection which is made available via its `VisiblePages` property which returns all documents in development whilst hiding **Draft** and content published at a **Future Date** from production builds. ### Rendering Markdown Pages The pages are then rendered in [Page.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Page.cshtml) Razor Page that's available from `/{slug}` ```csharp @page "/{slug}" @model MyApp.Page @inject MarkdownPages Markdown @implements IRenderStatic @functions { public List GetStaticProps(RenderContext ctx) { var markdown = ctx.Resolve(); return markdown.VisiblePages.Map(page => new Page { Slug = page.Slug! }); } } @{ var doc = Markdown.GetBySlug(Model.Slug); if (doc.Layout != null) Layout = doc.Layout == "none" ? null : doc.Layout; ViewData["Title"] = doc.Title; }

@doc.Title

@Html.Raw(doc.Preview)
@await Html.PartialAsync("HighlightIncludes") ``` Which uses a custom layout if one is defined in its frontmatter which [speaking.md](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_pages/speaking.md) utilizes in its **layout** frontmatter: ```yaml --- title: Speaking layout: _LayoutContent --- ``` To render the page using [_LayoutContent.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Shared/_LayoutContent.cshtml) visible by the background backdrop in its [/speaking](https://razor-ssg.web-templates.io/speaking) page. ## What's New Feature The [/whatsnew](https://razor-ssg.web-templates.io/whatsnew) page is an example of creating a custom Markdown feature to implement a portfolio or a product releases page where a new folder is created per release, containing both release date and release or project name, with all features in that release maintained markdown content sorted in alphabetical order: ### [/_whatsnew](https://github.com/NetCoreTemplates/razor-ssg/tree/main/MyApp/_whatsnew) - **/2023-03-08_Animaginary** - feature1.md - **/2023-03-18_OpenShuttle** - feature1.md - **/2023-03-28_Planetaria** - feature1.md What's New follows the same structure as Pages feature which is loaded in: - [Markdown.WhatsNew.cs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Markdown.WhatsNew.cs) and rendered in: - [WhatsNew.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/WhatsNew.cshtml) ## Blog Feature The blog maintains its markdown posts in a flat folder which each Markdown post containing its publish date and URL slug it should be published under: ### [/_posts](https://github.com/NetCoreTemplates/razor-ssg/tree/main/MyApp/_posts) - ... - 2023-01-21_start.md - 2023-03-21_javascript.md - 2023-03-28_razor-ssg.md As the Blog has more features it requires a larger [Markdown.Blog.cs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Markdown.Blog.cs) to load its Markdown posts that is rendered in several different Razor Pages for each of its Views: | Page | Description | Example | | - | - | - | | [Blog.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Blog.cshtml) | Main Blog layout | [/blog](https://razor-ssg.web-templates.io/blog) | | [Posts/Index.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Index.cshtml) | Navigable Archive grid of Posts | [/posts](https://razor-ssg.web-templates.io/posts) | | [Posts/Post.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Post.cshtml) | Individual Blog Post (like this!) | [/posts/razor-ssg](https://razor-ssg.web-templates.io/posts/razor-ssg) | | [Author.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Author.cshtml) | Display Posts by Author | [/posts/author/lucy-bates](https://razor-ssg.web-templates.io/posts/author/lucy-bates) | | [Tagged.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Tagged.cshtml) | Display Posts by Tag | [/posts/tagged/markdown](https://razor-ssg.web-templates.io/posts/tagged/markdown) | | [Year.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Year.cshtml) | Display Posts by Year | [/posts/year/2023](https://razor-ssg.web-templates.io/posts/year/2023) | ### General Features Most unique markdown features are captured in their Markdown's frontmatter metadata, but in general these features are broadly available for all features: - **Live Reload** - Latest Markdown content is displayed during **Development** - **Custom Layouts** - Render post in custom Razor Layout with `layout: _LayoutAlt` - **Drafts** - Prevent posts being worked on from being published with `draft: true` - **Future Dates** - Posts with a future date wont be published until that date ### Initializing and Loading Markdown Features All markdown features are initialized in the same way in [Configure.Ssg.cs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Configure.Ssg.cs) where they're registered in ASP.NET Core's IOC and initialized after the App's plugins are loaded by injecting with the App's [Virtual Files provider](https://docs.servicestack.net/virtual-file-system) before using it to read from the directory where the markdown content for each feature is maintained: ```csharp public class ConfigureSsg : IHostingStartup { public void Configure(IWebHostBuilder builder) => builder .ConfigureServices(services => { services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); }) .ConfigureAppHost(afterPluginsLoaded: appHost => { var pages = appHost.Resolve(); var whatsNew = appHost.Resolve(); var blogPosts = appHost.Resolve(); var features = new IMarkdownPages[] { pages, whatsNew, blogPosts }; features.Each(x => x.VirtualFiles = appHost.VirtualFiles); // Custom initialization blogPosts.Authors = Authors; // Load feature markdown content pages.LoadFrom("_pages"); whatsNew.LoadFrom("_whatsnew"); blogPosts.LoadFrom("_posts"); }); }); //... } ``` These dependencies are then injected in the feature's Razor Pages to query and render the loaded markdown content. ### Custom Frontmatter You can extend the `MarkdownFileInfo` type used to maintain the markdown content and metadata of each loaded Markdown file by adding any additional metadata you want included as C# properties on: ```csharp // Add additional frontmatter info to include public class MarkdownFileInfo : MarkdownFileBase { } ``` Any additional properties are automatically populated using ServiceStack's [built-in Automapping](https://docs.servicestack.net/auto-mapping) which includes rich support for converting string frontmatter values into native .NET types. ### Updating to latest versions You can easily update all the JavaScript dependencies used in [postinstall.js](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/postinstall.js) by running: :::sh node postinstall.js ::: This will also update the Markdown features `*.cs` implementations which is delivered as source files instead of an external NuGet package to enable full customization, easier debugging whilst supporting easy upgrades. If you do customize any of the `.cs` files, you'll want to exclude them from being updated by removing them from: ```js const hostFiles = [ 'Markdown.Blog.cs', 'Markdown.Pages.cs', 'Markdown.WhatsNew.cs', 'MarkdownPagesBase.cs', ] ``` ### Markdown Tag Helper The included [MarkdownTagHelper.cs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/MarkdownTagHelper.cs) can be used in hybrid Razor Pages like [About.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/About.cshtml) to render the [/about](https://razor-ssg.web-templates.io/about) page which requires the flexibility of Razor Pages with a static content component which you prefer to maintain inline with Markdown. The `` tag helper renders plain HTML, which you can apply [Tailwind's @typography](https://tailwindcss.com/docs/typography-plugin) styles by including **typography.css** and annotating it with your preferred `prose` variant, e.g: ```html Markdown content... ``` ## Static Static Generation (SSG) All features up till now describes how this template implements a Markdown powered Razor Pages .NET application, where this template differs in its published output, where instead of a .NET App deployed to a VM or App server it generates static `*.html` files that's bundled together with `/wwwroot` static assets in the `/dist` folder that can be previewed by launching a HTTP Server from that folder with the built-in npm script: :::sh npm run serve ::: To run **npx http-server** on `http://localhost:8080` that you can open in a browser to preview the published version of your site as it would be when hosted on a CDN. ### Static Razor Pages The static generation functionality works by scanning all your Razor Pages and prerendering the pages with prerendering instructions. ### Pages with Static Routes Pages with static routes can be marked to be prerendered by annotating it with the `[RenderStatic]` attribute as done in [About.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/About.cshtml): ```csharp @page "/about" @attribute [RenderStatic] ``` Which saves the pre-rendered page using the pages route with a .html suffix, e.g: `/{@page route}.html` whilst pages with static routes with a trailing `/` are saved to `/{@page route}/index.html` as done for [Posts/Index.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Index.cshtml): ```csharp @page "/posts/" @attribute [RenderStatic] ``` #### Explicit generated paths To keep the generated pages in-sync with using the same routes as your Razor Pages in development it's recommended to use the implied rendered paths, but if preferred you can specify which path the page should be rendered to instead with: ```csharp @page "/posts/" @attribute [RenderStatic("/posts/index.html")] ``` ### Pages with Dynamic Routes Prerendering dynamic pages follows [Next.js getStaticProps](https://nextjs.org/docs/basic-features/data-fetching/get-static-props) convention which you can implement using `IRenderStatic` by returning a Page Model for each page that should be generated as done in [Posts/Post.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Post.cshtml) and [Page.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Page.cshtml): ```csharp @page "/{slug}" @model MyApp.Page @implements IRenderStatic @functions { public List GetStaticProps(RenderContext ctx) { var markdown = ctx.Resolve(); return markdown.VisiblePages.Map(page => new Page { Slug = page.Slug! }); } } ... ``` In this case it returns a Page Model for every **Visible** markdown page in [/_pages](https://github.com/NetCoreTemplates/razor-ssg/tree/main/MyApp/_pages) that ends up rendering the following pages in `/dist`: - `/privacy.html` - `/speaking.html` - `/uses.html` ### Limitations The primary limitations for developing statically generated Apps is that a **snapshot** of entire App is generated at deployment, which prohibits being able to render different content **per request**, e.g. for Authenticated users which would require executing custom JavaScript after the page loads to dynamically alter the page's initial content. Otherwise in practice you'll be able develop your Razor Pages utilizing Razor's full feature-set, the primary concessions stem from Pages being executed in a static context which prohibits pages from returning dynamic content per request, instead any "different views" should be maintained in separate pages. #### No QueryString Params As the generated pages should adopt the same routes as your Razor Pages you'll need to avoid relying on **?QueryString** params and instead capture all required parameters for a page in its **@page route** as done for: [Posts/Author.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Author.cshtml) ```csharp @page "/posts/author/{slug}" @model AuthorModel @inject MarkdownBlog Blog @implements IRenderStatic @functions { public List GetStaticProps(RenderContext ctx) => ctx.Resolve() .AuthorSlugMap.Keys.Map(x => new AuthorModel { Slug = x }); } ... ``` Which lists all posts by an Author, e.g: [/posts/author/lucy-bates](https://razor-ssg.web-templates.io/posts/author/lucy-bates), likewise required for: [Posts/Tagged.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Tagged.cshtml) ```csharp @page "/posts/tagged/{slug}" @model TaggedModel @inject MarkdownBlog Blog @implements IRenderStatic @functions { public List GetStaticProps(RenderContext ctx) => ctx.Resolve() .TagSlugMap.Keys.Map(x => new TaggedModel { Slug = x }); } ... ``` Which lists all related posts with a specific tag, e.g: [/posts/tagged/markdown](https://razor-ssg.web-templates.io/posts/tagged/markdown), and for: [Posts/Year.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Posts/Year.cshtml) ```csharp @page "/posts/year/{year}" @model YearModel @inject MarkdownBlog Blog @implements IRenderStatic @functions { public List GetStaticProps(RenderContext ctx) => ctx.Resolve() .VisiblePosts.Select(x => x.Date.GetValueOrDefault().Year) .Distinct().Map(x => new YearModel { Year = x }); } ... ``` Which lists all posts published in a specific year, e.g: [/posts/year/2023](https://razor-ssg.web-templates.io/posts/year/2023). Conceivably these "different views" could've been implemented by the same page with different `?author`, `?tag` and `?year` QueryString params, but are instead extracted into different pages to support its statically generated `*.html` outputs. ## Prerendering Task The **prerender** [AppTask](https://docs.servicestack.net/app-tasks) that pre-renders the entire website is also registered in [Configure.Ssg.cs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Configure.Ssg.cs): ```csharp .ConfigureAppHost(afterAppHostInit: appHost => { // prerender with: `$ npm run prerender` AppTasks.Register("prerender", args => { var distDir = appHost.ContentRootDirectory.RealPath.CombineWith("dist"); if (Directory.Exists(distDir)) FileSystemVirtualFiles.DeleteDirectory(distDir); FileSystemVirtualFiles.CopyAll( new DirectoryInfo(appHost.ContentRootDirectory.RealPath.CombineWith("wwwroot")), new DirectoryInfo(distDir)); var razorFiles = appHost.VirtualFiles.GetAllMatchingFiles("*.cshtml"); RazorSsg.PrerenderAsync(appHost, razorFiles, distDir).GetAwaiter().GetResult(); }); }); //... ``` Which we can see: 1. Deletes `/dist` folder 2. Copies `/wwwroot` contents into `/dist` 3. Passes all App's Razor `*.cshtml` files to `RazorSsg` to do the pre-rendering Where it processes all pages with `[RenderStatic]` and `IRenderStatic` prerendering instructions to the specified `/dist` folder. ### Previewing prerendered site To preview your SSG website, run the prerendered task with: :::sh npm run prerender ::: Which renders your site to `/_dist` which you can run a HTTP Server from with: :::sh npm run serve ::: That you can preview with your browser at `http://localhost:8080`. ### Publishing The included [build.yml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/.github/workflows/build.yml) GitHub Action takes care of running the prerendered task and deploying it to your Repo's GitHub Pages where it will be available at: https://$org_name.github.io/$repo/ Alternatively you can use a [Custom domain for GitHub Pages](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/about-custom-domains-and-github-pages) by registering a CNAME DNS entry for your preferred Custom Domain, e.g: | Record | Type | Value | TTL| | - | - | - | - | | **mydomain.org** | CNAME | **org_name**.github.io | 3600 | That you can either [configure in your Repo settings](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site#configuring-a-subdomain) or if you prefer to maintain it with your code-base, save the domain name to `/wwwroot/CNAME`, e.g: ``` www.mydomain.org ``` ### Benefits after migrating from Jekyll Whilst still only at **v1** release, we found it already had a number of advantages over the existing Jekyll static website: - Faster live reloads - C#/Razor more type-save & productive than Ruby/Liquid - Greater flexibility in implementing new features - Better IDE support (from Rider) - Ability to reuse our .NET libraries - Better development experience The last point ultimately prompted seeking an alternative solution as previously Jekyll was used from Windows/WSL which was awkward to manage from a different filesystem with Jekyll upgrades breaking RubyMine support forcing the use of text editors to maintain its code-base and content. ### Used by the new [servicestack.net](https://servicestack.net) Deterred by the growing complexity of current SSG solutions, we decided to create a new solution using C#/Razor (our preferred technology for generating server HTML) with a clean implementation that allowed full control with an **npm dependency-free** solution letting us adopt our preferred approach to [Simple, Modern JavaScript](https://servicestack.net/posts/javascript) without any build-tooling or SPA complexity. We're happy with the results of [https://servicestack.net](https://servicestack.net) new Razor SSG website: [![](https://servicestack.net/img/posts/razor-ssg/servicestack.net.png)](https://servicestack.net) A clean, crisp code-base utilizing simple JS Module Vue 3 components, the source code of which is publicly maintained at: - [https://github.com/servicestack/servicestack.net](https://github.com/servicestack/servicestack.net) Which serves as a good example at how well this template scales for larger websites. #### Markdown Videos Feature It only needed one new Markdown feature to display our growing video library: - [/_videos](https://github.com/ServiceStack/servicestack.net/tree/main/MyApp/_videos) - Directory of Markdown Video collections - [Markdown.Videos.cs](https://github.com/ServiceStack/servicestack.net/blob/main/MyApp/Markdown.Videos.cs) - Loading Video feature markdown content - [Shared/VideoGroup.cshtml](https://github.com/ServiceStack/servicestack.net/blob/main/MyApp/Pages/Shared/VideoGroup.cshtml) - Razor Page for displaying Video Collection Which you're free to reuse in your own websites needing a similar feature. #### Feedback & Feature Requests Welcome In future we'll look at expanding this template with generic Markdown features suitable for websites, blogs & portfolios, or maintain a shared community collection if there ends up being community contributions of Razor SSG & Markdown features. In the meantime, we welcome any feedback or new feature requests at: ### [https://servicestack.net/ideas](https://servicestack.net/ideas) # Simple, Modern JavaScript Source: https://razor-ssg.web-templates.io/posts/javascript JavaScript has progressed significantly in recent times where many of the tooling & language enhancements that we used to rely on external tools for is now available in modern browsers alleviating the need for complex tooling and npm dependencies that have historically plagued modern web development. The good news is that the complex npm tooling that was previously considered mandatory in modern JavaScript App development can be considered optional as we can now utilize modern browser features like [async/await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function), [JavaScript Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), [dynamic imports](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import), [import maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) and [modern language features](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide) for a sophisticated development workflow without the need for any npm build tools. ### Bringing Simplicity Back The [vue-mjs](https://github.com/NetCoreTemplates/vue-mjs) template focuses on simplicity and eschews many aspects that has complicated modern JavaScript development, specifically: - No npm node_modules or build tools - No client side routing - No heavy client state Effectively abandoning the traditional SPA approach in lieu of a simpler [MPA](https://docs.astro.build/en/concepts/mpa-vs-spa/) development model using Razor Pages for Server Rendered content with any interactive UIs progressively enhanced with JavaScript. #### Freedom to use any JS library Avoiding the SPA route ends up affording more flexibility on which JS libraries each page can use as without heavy bundled JS blobs of all JS used in the entire App, it's free to only load the required JS each page needs to best implement its required functionality, which can be any JS library, preferably utilizing ESM builds that can be referenced from a [JavaScript Module](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), taking advantage of the module system native to modern browsers able to efficiently download the declarative matrix of dependencies each script needs. ### Best libraries for progressive Multi Page Apps It includes a collection of libraries we believe offers the best modern development experience in Progressive MPA Web Apps, specifically: #### [Tailwind CLI](https://tailwindcss.com/docs/installation) Tailwind enables a responsive, utility-first CSS framework for creating maintainable CSS at scale without the need for any CSS preprocessors like Sass, which is configured to run from an npx script to avoid needing any node_module dependencies. #### [Vue 3](https://vuejs.org/guide/introduction.html) Vue is a popular Progressive JavaScript Framework that makes it easy to create interactive Reactive Components whose [Composition API](https://vuejs.org/api/composition-api-setup.html) offers a nice development model without requiring any pre-processors like JSX. Where creating a component is as simple as: ```js const Hello = { template: `Hello, {{name}}!`, props: { name:String } } ```
Or a simple reactive example: ```js import { ref } from "vue" const Counter = { template: `Counter {{count}}`, setup() { let count = ref(1) return { count } } } ```
### Vue Components in Markdown Inside `.md` Markdown pages Vue Components can be embedded using Vue's progressive [HTML Template Syntax](https://vuejs.org/guide/essentials/template-syntax.html): ```html ``` ### Vue Components in Razor Pages Inside `.cshtml` Razor Pages these components can be mounted using the standard [Vue 3 mount](https://vuejs.org/api/application.html#app-mount) API, but to make it easier we've added additional APIs for declaratively mounting components to pages using `data-component` and `data-props` attributes: ```html
``` Alternatively they can be programatically added using the custom `mount` method in `api.mjs`: ```js import { mount } from "/mjs/api.mjs" mount('#counter', Counter) ``` Both methods create components with access to all your Shared Components and any 3rd Party Plugins which we can preview in this example that uses **@servicestack/vue**'s [PrimaryButton](https://docs.servicestack.net/vue//navigation#primarybutton) and [ModalDialog](https://docs.servicestack.net/vue//modals): ```js const Plugin = { template:`
Open Modal
Hello @servicestack/vue!
`, setup() { const show = ref(false) return { show } } } ``` ```html ```
### Vue HTML Templates An alternative progressive approach for creating Reactive UIs with Vue is by embedding its HTML markup directly in `.html` pages using [HTML Template Syntax](https://vuejs.org/guide/essentials/template-syntax.html) which is both great for performance as the DOM UI can be rendered before the Vue Component is initialized. UI elements you want hidden can use Vue's [v-cloak](https://vuejs.org/api/built-in-directives.html#v-cloak) attribute where they'll be hidden until components are initialized. It's also great for development as it lets you cohesively maintain most pages functionality need in the HTML page itself - in isolation with the rest of the website, i.e. instead of spread across multiple external `.js` source files that for SPAs unnecessarily increases the payload sizes of JS bundles with functionality that no other pages need. With Vue's HTML syntax you can maintain the Vue template in HTML and just use embedded JavaScript for the Reactive UI's functionality, e.g: ```html
Open Modal
Hello @servicestack/vue!
``` This is the approach used to develop [Vue Stable Diffusion](https://servicestack.net/posts/vue-stable-diffusion) where all functionality specific to the page is maintained in the page itself, whilst any common functionality is maintained in external JS Modules loaded on-demand by the Browser when needed. ### @servicestack/vue [@servicestack/vue](https://github.com/ServiceStack/servicestack-vue) is our growing Vue 3 Tailwind component library with a number of rich Tailwind components useful in .NET Web Apps, including Input Components with auto form validation binding which is used by all HTML forms in the [vue-mjs](https://github.com/NetCoreTemplates/vue-mjs) template. ### @servicestack/client [@servicestack/client](https://docs.servicestack.net/javascript-client) is our generic JS/TypeScript client library which enables a terse, typed API for using your App's typed DTOs from the built-in [JavaScript ES6 Classes](https://docs.servicestack.net/javascript-add-servicestack-reference) support to enable an effortless end-to-end Typed development model for calling your APIs **without any build steps**, e.g: ```html
``` For better IDE intelli-sense during development, save the annotated Typed DTOs to disk with: :::sh npm run dtos ::: That can be referenced instead to unlock your IDE's static analysis type-checking and intelli-sense benefits during development: ```js import { Hello } from '/js/dtos.mjs' client.api(new Hello({ name })) ``` You'll typically use all these libraries in your **API-enabled** components as seen in the [HelloApi.mjs](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/wwwroot/mjs/components/HelloApi.mjs) component on the home page which calls the [Hello](/ui/Hello) API on each key press: ```js import { ref } from "vue" import { useClient } from "@servicestack/vue" import { Hello } from "../dtos.mjs" export default { template:/*html*/`
{{ result }}
`, props:['value'], setup(props) { let name = ref(props.value) let result = ref('') let client = useClient() async function update() { let api = await client.api(new Hello({ name })) if (api.succeeded) { result.value = api.response.result } } update() return { name, update, result } } } ``` Which we can also mount below: ```html ``` We'll also go through and explain other features used in this component: #### `/*html*/` Although not needed in [Rider](rider) (which can automatically infer HTML in strings), the `/*html*/` type hint can be used to instruct tooling like the [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) VS Code extension to provide syntax highlighting and an enhanced authoring experience for HTML content in string literals. ### useClient [useClient()](https://docs.servicestack.net/vue//use-client) provides managed APIs around the `JsonServiceClient` instance registered in Vue App's with: ```js let client = JsonApiClient.create() app.provide('client', client) ``` Which maintains contextual information around your API calls like **loading** and **error** states, used by `@servicestack/vue` components to enable its auto validation binding. Other functionality in this provider include: ```js let { api, apiVoid, apiForm, apiFormVoid, // Managed Typed ServiceClient APIs loading, error, // Maintains 'loading' and 'error' states setError, addFieldError, // Add custom errors in client unRefs // Returns a dto with all Refs unwrapped } = useClient() ``` Typically you would need to unwrap `ref` values when calling APIs, i.e: ```js let client = JsonApiClient.create() let api = await client.api(new Hello({ name:name.value })) ``` #### useClient - api This is unnecessary in useClient `api*` methods which automatically unwraps ref values, allowing for the more pleasant API call: ```js let api = await client.api(new Hello({ name })) ``` #### useClient - unRefs But as DTOs are typed, passing reference values will report a type annotation warning in IDEs with type-checking enabled, which can be resolved by explicitly unwrapping DTO ref values with `unRefs`: ```js let api = await client.api(new Hello(unRefs({ name }))) ``` #### useClient - setError `setError` can be used to populate client-side validation errors which the [SignUp.mjs](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/wwwroot/Pages/SignUp.mjs) component uses to report an invalid submissions when passwords don't match: ```js const { api, setError } = useClient() async function onSubmit() { if (password.value !== confirmPassword.value) { setError({ fieldName:'confirmPassword', message:'Passwords do not match' }) return } //... } ``` ### Form Validation All `@servicestack/vue` Input Components support contextual validation binding that's typically populated from API [Error Response DTOs](https://docs.servicestack.net/error-handling) but can also be populated from client-side validation as done above. #### Explicit Error Handling This populated `ResponseStatus` DTO can either be manually passed into each component's **status** property as done in [/TodoMvc](/TodoMvc): ```html ``` Where if you try adding an empty Todo the `CreateTodo` API will fail and populate its `store.error` reactive property with the APIs Error Response DTO which the `` component checks to display any field validation errors adjacent to the HTML Input with matching `id` fields: ```js let store = { /** @type {Todo[]} */ todos: [], newTodo:'', error:null, async addTodo() { this.todos.push(new Todo({ text:this.newTodo })) let api = await client.api(new CreateTodo({ text:this.newTodo })) if (api.succeeded) this.newTodo = '' else this.error = api.error }, //... } ``` #### Implicit Error Handling More often you'll want to take advantage of the implicit validation support in `useClient()` which makes its state available to child components, alleviating the need to explicitly pass it in each component as seen in razor-tailwind's [Contacts.mjs](https://github.com/NetCoreTemplates/razor-tailwind/blob/main/MyApp/wwwroot/Pages/Contacts.mjs) `Edit` component for its [/Contacts](https://vue-mjs.web-templates.io/Contacts) page which doesn't do any manual error handling: ```js const Edit = { template:/*html*/`
`, props:['contact'], emits:['done'], setup(props, { emit }) { const client = useClient() const request = ref(new UpdateContact(props.contact)) const colorOptions = propertyOptions(getProperty('UpdateContact','Color')) async function submit() { const api = await client.api(request.value) if (api.succeeded) close() } async function onDelete () { const api = await client.apiVoid(new DeleteContact({ id:props.id })) if (api.succeeded) close() } const close = () => emit('done') return { request, enumOptions, colorOptions, submit, onDelete, close } } } ``` Effectively making form validation binding a transparent detail where all `@servicestack/vue` Input Components are able to automatically apply contextual validation errors next to the fields they apply to: ![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/scripts/edit-contact-validation.png) ### AutoForm Components We can elevate our productivity even further with [Auto Form Components](https://docs.servicestack.net/vue//autoform) that can automatically generate an instant API-enabled form with validation binding by just specifying the Request DTO you want to create the form of, e.g: ```html ```
The AutoForm components are powered by your [App Metadata](https://docs.servicestack.net/vue//use-appmetadata) which allows creating highly customized UIs from [declarative C# attributes](https://docs.servicestack.net/locode/declarative) whose customizations are reused across all ServiceStack Auto UIs, including: - [API Explorer](https://docs.servicestack.net/api-explorer) - [Locode](https://docs.servicestack.net/locode/) - [Blazor Tailwind Components](https://docs.servicestack.net/templates-blazor-components) ### Form Input Components In addition to including Tailwind versions of the standard [HTML Form Inputs](https://docs.servicestack.net/vue//form-inputs) controls to create beautiful Tailwind Forms, it also contains a variety of integrated high-level components: - [FileInput](https://docs.servicestack.net/vue//fileinput) - [TagInput](https://docs.servicestack.net/vue//taginput) - [Autocomplete](https://docs.servicestack.net/vue//autocomplete) ### useAuth Your Vue.js code can access Authenticated Users using [useAuth()](https://docs.servicestack.net/vue/use-auth) which can also be populated without the overhead of an Ajax request by embedding the response of the built-in [Authenticate API](/ui/Authenticate?tab=details) inside `_Layout.cshtml` with: ```html ``` Where it enables access to the below [useAuth()](https://docs.servicestack.net/vue/use-auth) utils for inspecting the current authenticated user: ```js const { signIn, // Sign In the currently Authenticated User signOut, // Sign Out currently Authenticated User user, // Access Authenticated User info in a reactive Ref isAuthenticated, // Check if the current user is Authenticated in a reactive Ref hasRole, // Check if the Authenticated User has a specific role hasPermission, // Check if the Authenticated User has a specific permission isAdmin // Check if the Authenticated User has the Admin role } = useAuth() ``` This is used in [Bookings.mjs](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/wwwroot/Pages/Bookings.mjs) to control whether the `` component should enable its delete functionality: ```js export default { template/*html*/:` `, setup(props) { const { hasRole } = useAuth() const canDelete = computed(() => hasRole('Manager')) return { canDelete } } } ``` #### [JSDoc](https://jsdoc.app) We get great value from using [TypeScript](https://www.typescriptlang.org) to maintain our libraries typed code bases, however it does mandate using an external tool to convert it to valid JS before it can be run, something the new Razor Vue.js templates expressly avoids. Instead it adds JSDoc type annotations to code where it adds value, which at the cost of slightly more verbose syntax enables much of the same static analysis and intelli-sense benefits of TypeScript, but without needing any tools to convert it to valid JavaScript, e.g: ```js /** @param {KeyboardEvent} e */ function validateSafeName(e) { if (e.key.match(/[\W]+/g)) { e.preventDefault() return false } } ``` #### TypeScript Language Service Whilst the code-base doesn't use TypeScript syntax in its code base directly, it still benefits from TypeScript's language services in IDEs for the included libraries from the TypeScript definitions included in `/lib/typings`, downloaded in [postinstall.js](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/postinstall.js) after **npm install**. ### Import Maps [Import Maps](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) is a useful browser feature that allows specifying optimal names for modules, that can be used to map package names to the implementation it should use, e.g: ```csharp @Html.StaticImportMap(new() { ["vue"] = "/lib/mjs/vue.mjs", ["@servicestack/client"] = "/lib/mjs/servicestack-client.mjs", ["@servicestack/vue"] = "/lib/mjs/servicestack-vue.mjs", }) ``` Where they can be freely maintained in one place without needing to update any source code references. This allows source code to be able to import from the package name instead of its physical location: ```js import { ref } from "vue" import { useClient } from "@servicestack/vue" import { JsonApiClient, $1, on } from "@servicestack/client" ``` It's a great solution for specifying using local unminified debug builds during **Development**, and more optimal CDN hosted production builds when running in **Production**, alleviating the need to rely on complex build tools to perform this code transformation for us: ```csharp @Html.ImportMap(new() { ["vue"] = ("/lib/mjs/vue.mjs", "https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js"), ["@servicestack/client"] = ("/lib/mjs/servicestack-client.mjs", "https://unpkg.com/@servicestack/client@2/dist/servicestack-client.min.mjs"), ["@servicestack/vue"] = ("/lib/mjs/servicestack-vue.mjs", "https://unpkg.com/@servicestack/vue@3/dist/servicestack-vue.min.mjs") }) ``` Note: Specifying exact versions of each dependency improves initial load times by eliminating latency from redirects. Or if you don't want your Web App to reference any external dependencies, have the ImportMap reference local minified production builds instead: ```csharp @Html.ImportMap(new() { ["vue"] = ("/lib/mjs/vue.mjs", "/lib/mjs/vue.min.mjs"), ["@servicestack/client"] = ("/lib/mjs/servicestack-client.mjs", "/lib/mjs/servicestack-client.min.mjs"), ["@servicestack/vue"] = ("/lib/mjs/servicestack-vue.mjs", "/lib/mjs/servicestack-vue.min.mjs") }) ``` #### Polyfill for Safari Unfortunately Safari is the last modern browser to [support import maps](https://caniuse.com/import-maps) which is only now in Technical Preview. Luckily this feature can be polyfilled with the [ES Module Shims](https://github.com/guybedford/es-module-shims): ```html @if (Context.Request.Headers.UserAgent.Any(x => x.Contains("Safari") && !x.Contains("Chrome"))) { } ``` ### Fast Component Loading SPAs are notorious for being slow to load due to needing to download large blobs of JavaScript bundles that it needs to initialize with their JS framework to mount their App component before it starts fetching the data from the server it needs to render its components. A complex solution to this problem is to server render the initial HTML content then re-render it again on the client after the page loads. A simpler solution is to avoid unnecessary ajax calls by embedding the JSON data the component needs in the page that loads it, which is what [/TodoMvc](/TodoMvc) does to load its initial list of todos using the [Service Gateway](https://docs.servicestack.net/service-gateway) to invoke APIs in process and embed its JSON response with: ```html ``` Where `ApiResultsAsJsonAsync` is a simplified helper that uses the `Gateway` to call your API and returns its unencoded JSON response: ```csharp (await Gateway.ApiAsync(new QueryTodos())).Response?.Results.AsRawJson(); ``` The result of which should render the List of Todos instantly when the page loads since it doesn't need to perform any additional Ajax requests after the component is loaded. ### Fast Page Loading We can get SPA-like page loading performance using htmx's [Boosting](https://htmx.org/docs/#boosting) feature which avoids full page reloads by converting all anchor tags to use Ajax to load page content into the page body, improving perceived performance from needing to reload scripts and CSS in ``. This is used in [Header.cshtml](https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/Pages/Shared/Header.cshtml) to **boost** all main navigation links: ```html ``` htmx has lots of useful [real world examples](https://htmx.org/examples/) that can be activated with declarative attributes, another useful feature is the [class-tools](https://htmx.org/extensions/class-tools/) extension to hide elements from appearing until after the page is loaded: ```html
``` Which is used to reduce UI yankiness from showing server rendered content before JS components have loaded. ### @servicestack/vue Library [@servicestack/vue](https://docs.servicestack.net/vue/) is our cornerstone library for enabling a highly productive Vue.js development model across our [Vue Tailwind Project templates](https://docs.servicestack.net/templates-vue) which we'll continue to significantly invest in to unlock even greater productivity benefits in all Vue Tailwind Apps. In addition to a variety of high-productive components, it also contains a core library of functionality underpinning the Vue Components that most Web Apps should also find useful: # Getting Started Source: https://razor-ssg.web-templates.io/posts/start ### Setup If project wasn't created with [x new](https://docs.servicestack.net/dotnet-new), ensure postinstall tasks are run with: ```bash $ npm install ``` ### Tailwind Configuration This template is configured with a stand-alone [Tailwind CSS CLI](https://tailwindcss.com/docs/installation) installation with a modified **tailwind.input.css** that includes [@tailwindcss/forms](https://github.com/tailwindlabs/tailwindcss-forms) and [@tailwindcss/aspect-ratio](https://github.com/tailwindlabs/tailwindcss-aspect-ratio) plugins so that no **node_modules** dependencies are needed. The [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin) plugin css is contained in `css/typography.css` which applies a beautiful default style to unstyled HTML, ideal for Markdown content like this. ### Running Tailwind during development Run tailwind in a new terminal during development to auto update your **app.css**: ```bash $ npm run ui:dev ``` For an optimal development experience run it together with `dotnet watch` to preview changes on each save. Or if using JetBrains Rider, **ui:dev** can be run directly from Rider in **package.json**: ![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/servicestack-reference/scripts-tailwind.png) ### Using JsonServiceClient in Web Pages Easiest way to call APIs is to use [@servicestack/client](https://docs.servicestack.net/javascript-client) with the built-in [/types/mjs](https://vue-mjs.web-templates.io/types/mjs) which returns your APIs annotated typed JS DTOs that can be used immediately (i.e. without any build steps): ```html
``` ```html ``` For better IDE intelli-sense during development, save the annotated Typed DTOs to disk with: ```bash $ npm run dtos ``` Where it will enable IDE static analysis when calling Typed APIs from JavaScript: ```js import { Hello } from '/mjs/dtos.mjs' client.api(new Hello({ name })) ``` # Develop using JetBrains Rider Source: https://razor-ssg.web-templates.io/posts/rider [JetBrains Rider](https://www.jetbrains.com/rider/) is our recommended IDE for any C# + JavaScript development as it offers a great development UX for both, including excellent support for TypeScript and popular JavaScript Framework SPA assets like [Vue SFC's](https://v3.vuejs.org/guide/single-file-component.html). #### Setup Rider IDE As Rider already understands and provides excellent HTML/JS/TypeScript support you'll be immediately productive out-of-the-box, we can further improve the development experience for Vue.js Apps by adding an empty **vue** dependency to **package.json**: ```json { "devDependencies": { "vue": "" } } ``` As this is just a heuristic Rider looks for to enable its Vue support, installing the dependency itself isn't used or required. Other than that the only plugin we recommend adding is: Tailwind CSS Plugin Which provides provides intelli-sense support for [Tailwind CSS](https://tailwindcss.com). ### Start both dotnet and Tailwind The only additional development workflow requirement to use tailwind is to start it running in the background which can be done from a new Terminal: ```bash $ npm run ui:dev ``` We find `dotnet watch` offers the most productive iterative development workflow for .NET which refreshes on save which works great with Tailwind which rewrites your `app.css` on save. How you want to run them is largely a matter of preference, our personal preference is to run the **dev** and **ui:dev** npm scripts in your **package.json**: ![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/scripts/dotnet-tailwind.png) ### Rider's Task Runner Where they will appear in Rider's useful task runner widget where you'll be able to easily, stop and rerun all project tasks: ![](https://github.com/ServiceStack/docs/raw/master/docs/images/spa/rider-run-widget.png) ### Running from the terminal These GUI tasks are just managing running CLI commands behind-the-scenes, which if you prefer you can use JetBrains excellent multi-terminal support to run `$ dotnet watch` and `$ npm run ui:dev` from separate or split Terminal windows. ### Deploying to Production This template also includes the necessary GitHub Actions to deploy this Apps production static assets to GitHub Pages CDN, for more info, checkout [GitHub Actions Deployments](deploy). ### Get Started If you're new to Vue 3 a good place to start is [Vue 3 Composition API](https://vuejs.org/api/composition-api-setup.html). # Develop using Visual Studio Source: https://razor-ssg.web-templates.io/posts/vs A popular alternative development environment to our preferred [JetBrains Rider](rider) IDE is to use Visual Studio, the primary issue with this is that VS Code is a better IDE with richer support for JavaScript and npm projects whilst Visual Studio is a better IDE for C# Projects. Essentially this is why we recommend Rider where it's best at both, where both C# and JS/TypeScript projects can be developed from within the same solution. ### Developing with just VS Code If you prefer the dev UX of a lightweight text editor or your C# project isn't large, than VS Code on its own can provide a great development UX which is also what [Vue recommends themselves](https://v3.vuejs.org/api/sfc-tooling.html#ide-support), to be used together with the [Volar extension](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar). VSCode's [Integrated Terminal](https://code.visualstudio.com/docs/editor/integrated-terminal) has great multi-terminal support you can toggle between the editor and terminal with `Ctrl+` or open a new Terminal Window with Ctrl+Shift+` to run Tailwind with: ```bash $ npm run ui:dev ``` Then in a new Terminal Window, start a new watched .NET App with: ```bash $ dotnet watch ``` With both projects started you can open a browser tab running at `https://localhost:5001` where it will automatically reload itself at every `Ctrl+S` save point. #### Useful VS Code extensions We recommend these extensions below to enhance the development experience of this template: - [Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) - Add Intellisense for Tailwind classes - [es6-string-html](https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html) - Add HTML Syntax Highlighting in string literals ### Using Visual Studio As your C# project grows you'll want to consider running the back-end C# Solution with Visual Studio .NET with its much improved intelli-sense, navigation, tests runner & debug capabilities. As we've never had a satisfactory experience trying develop npm or JS/TypeScript projects with VS.NET, we'd recommend only using VS.NET for C# and Razor and continuing to use VSCode for everything else. If you'd prefer to use Visual Studio for front-end development we recommend moving all JS to external files for a better Dev UX, e.g: ```html ``` ### Deploying to Production This template also includes the necessary GitHub Actions to deploy this Apps production static assets to GitHub Pages CDN, for more info, checkout [GitHub Actions Deployments](deploy). ### Get Started If you're new to Vue 3 a good place to start is [Vue 3 Composition API](https://vuejs.org/api/composition-api-setup.html). # Deployment with GitHub Actions Source: https://razor-ssg.web-templates.io/posts/deploy # ServiceStack GitHub Action Deployments The [release.yml](https://github.com/NetCoreTemplates/razor-tailwind/blob/main/.github/workflows/release.yml) in this template enables GitHub Actions CI deployment to a dedicated server with SSH access. ## Overview `release.yml` is designed to work with a ServiceStack app deploying directly to a single server via SSH. A docker image is built and stored on GitHub's `ghcr.io` docker registry when a GitHub Release is created. GitHub Actions specified in `release.yml` then copy files remotely via scp and use `docker-compose` to run the app remotely via SSH. ## What's the process of `release.yml`? ![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/release-ghr-vanilla-diagram.png) ## Deployment server setup To get this working, a server needs to be setup with the following: - SSH access - docker - docker-compose - ports 443 and 80 for web access of your hosted application This can be your own server or any cloud hosted server like Digital Ocean, AWS, Azure etc. We use [Hetzner Cloud](http://cloud.hetzner.com/) to deploy all ServiceStack's [GitHub Project Templates]( https://github.com/NetCoreTemplates/) as it was the [best value US cloud provider](https://servicestack.net/blog/finding-best-us-value-cloud-provider) we've found. When setting up your server, you'll want to use a dedicated SSH key for access to be used by GitHub Actions. GitHub Actions will need the *private* SSH key within a GitHub Secret to authenticate. This can be done via ssh-keygen and copying the public key to the authorized clients on the server. To let your server handle multiple ServiceStack applications and automate the generation and management of TLS certificates, an additional docker-compose file is provided in this template, `nginx-proxy-compose.yml`. This docker-compose file is ready to run and can be copied to the deployment server. For example, once copied to remote `~/nginx-proxy-compose.yml`, the following command can be run on the remote server. ``` docker-compose -f ~/nginx-proxy-compose.yml up -d ``` This will run an nginx reverse proxy along with a companion container that will watch for additional containers in the same docker network and attempt to initialize them with valid TLS certificates. ### GitHub Actions secrets The `release.yml` uses the following secrets. | Required Secrets | Description | | -- | -- | | `DEPLOY_HOST` | Hostname used to SSH deploy .NET App to, this can either be an IP address or subdomain with A record pointing to the server | | `DEPLOY_USERNAME` | Username to log in with via SSH e.g, **ubuntu**, **ec2-user**, **root** | | `DEPLOY_KEY` | SSH private key used to remotely access deploy .NET App | | `LETSENCRYPT_EMAIL` | Email required for Let's Encrypt automated TLS certificates | These secrets can use the [GitHub CLI](https://cli.github.com/manual/gh_secret_set) for ease of creation. Eg, using the GitHub CLI the following can be set. ```bash gh secret set DEPLOY_HOST -b"" gh secret set DEPLOY_USERNAME -b"" gh secret set DEPLOY_KEY < key.pem # DEPLOY_KEY gh secret set LETSENCRYPT_EMAIL -b"" ``` These secrets are used to populate variables within GitHub Actions and other configuration files. ## Deployments A published version of your .NET App created with the standard dotnet publish tool: ```yaml dotnet publish -c Release ``` is used to build a production build of your .NET App inside the standard `Dockerfile` for dockerizing .NET Applications. Additional custom deployment tasks can be added to your project's package.json **postinstall** script which also gets run at deployment. If preferred additional MS Build tasks can be run by passing in custom parameters in the publish command, e.g: ```yaml dotnet publish -c Release /p:APP_TASKS=prerender ``` Which your `MyApp.csproj` can detect with a target that checks for it: ```xml ``` ## Pushing updates and rollbacks By default, deployments occur on commit to your main branch. A new Docker image for your ServiceStack API is produced, pushed to GHCR.io and hosted on your Linux server with Docker Compose. The template also will run the release process on the creation of a GitHub Release making it easier to switch to manual production releases. Additionally, the `release.yml` workflow can be run manually specifying a version. This enables production rollbacks based on previously tagged releases. A release must have already been created for the rollback build to work, it doesn't create a new Docker build based on previous code state, only redeploys as existing Docker image. # Tailwind Typography Source: https://razor-ssg.web-templates.io/posts/typography

Until now, trying to style an article, document, or blog post with Tailwind has been a tedious task that required a keen eye for typography and a lot of complex custom CSS.

By default, Tailwind removes all of the default browser styling from paragraphs, headings, lists and more. This ends up being really useful for building application UIs because you spend less time undoing user-agent styles, but when you _really are_ just trying to style some content that came from a rich-text editor in a CMS or a markdown file, it can be surprising and unintuitive. We get lots of complaints about it actually, with people regularly asking us things like: > Why is Tailwind removing the default styles on my `h1` elements? How do I disable this? What do you mean I lose all the other base styles too? We hear you, but we're not convinced that simply disabling our base styles is what you really want. You don't want to have to remove annoying margins every time you use a `p` element in a piece of your dashboard UI. And I doubt you really want your blog posts to use the user-agent styles either — you want them to look _awesome_, not awful. The `@tailwindcss/typography` plugin is our attempt to give you what you _actually_ want, without any of the downsides of doing something stupid like disabling our base styles. It adds a new `prose` class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document: ```html

Garlic bread with cheese: What the science tells us

For years parents have espoused the health benefits of eating garlic bread with cheese to their children, with the food earning such an iconic status in our culture that kids will often dress up as warm, cheesy loaf for Halloween.

But a recent study shows that the celebrated appetizer may be linked to a series of rabies cases springing up around the country.

``` For more information about how to use the plugin and the features it includes, [read the documentation](https://github.com/tailwindcss/typography/blob/master/README.md). --- ## What to expect from here on out What follows from here is just a bunch of absolute nonsense I've written to dogfood the plugin itself. It includes every sensible typographic element I could think of, like **bold text**, unordered lists, ordered lists, code blocks, block quotes, _and even italics_. It's important to cover all of these use cases for a few reasons: 1. We want everything to look good out of the box. 2. Really just the first reason, that's the whole point of the plugin. 3. Here's a third pretend reason though a list with three items looks more realistic than a list with two items. Now we're going to try out another header style. ### Typography should be easy So that's a header for you — with any luck if we've done our job correctly that will look pretty reasonable. Something a wise person once told me about typography is: > Typography is pretty important if you don't want your stuff to look like trash. Make it good then it won't be bad. It's probably important that images look okay here by default as well:
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old.
Now I'm going to show you an example of an unordered list to make sure that looks good, too: - So here is the first item in this list. - In this example we're keeping the items short. - Later, we'll use longer, more complex list items. And that's the end of this section. # In pursuit of the best value US cloud provider Source: https://razor-ssg.web-templates.io/posts/hetzner-cloud At ServiceStack, we have been using AWS for hosting for over 10 years. It has served us well, but it suffers from complex pricing and possibility of bill shock due to its fractured pay-as-you-go design. Thankfully, more and more companies are providing simpler offerings for hosting needs, and AWS themselves launched [Lightsail](https://aws.amazon.com/lightsail) as their answer to market demands for simple hosting options that package everything you need for basic hosting. These simpler hosting options tend to bundle several things together as one fixed monthly price. A VM with a specific compute and memory allocation, as well as data transfer, and storage. ## Looking at different US offerings Something we wanted to do was to host our [live demo applications](https://github.com/ServiceStackApps/LiveDemos) on a US based host. We were using [Hetzner dedicated servers](https://www.hetzner.com/dedicated-rootserver) in the past for non-latency sensitive use cases like our build server and [Gist.Cafe (our interactive playground for multiple platforms)](https://gist.cafe) but we also wanted our demo applications to be snappy for US users. [DigitalOcean](https://www.digitalocean.com/pricing) provides ["Droplets"](https://www.digitalocean.com/pricing/droplets) with this fixed pricing model with a nice and simple interface. Their pricing was quite good and we realized we could run all 20+ of our demo applications on a single droplet for $40/month. For deployment, [we also like to keep things as simple as we can, whilst keeping portability](https://docs.servicestack.net/do-github-action-mix-deployment). Since all our projects are public and on GitHub, we use [GitHub Actions](https://docs.servicestack.net/do-github-action-mix-deployment#github-repository-setup) heavily along with a pattern that deploys our applications using Docker Compose via SSH. Each application runs in its own container behind an [NGINX proxy](https://docs.servicestack.net/do-github-action-mix-deployment#get-nginx-reverse-proxy-and-letsencrypt-companion-running) with a side car that handles renewing LetsEncrypt certificates. Below is an example of this pattern with Blazor and Litestream. A nice side effect of this approach is moving servers is relatively painless. We change the DNS entry for the application to point to our new server, update the GitHub Action Secrets if needed and run our Release workflow. A minute or so later, the application is back running again. Since their were 20+ of these repositories we took advantage of the [GitHub Organization Secrets](https://cli.github.com/manual/gh_secret_set) so we only needed to update values in one place, and [running the workflows again](https://cli.github.com/manual/gh_workflow_run) can also be done programmatically through the GitHub CLI. ## DigitalOcean Price Increase In June of 2022, we got a notification that [prices for droplets would be increasing](https://www.digitalocean.com/try/new-pricing), and for our droplet it would be going from **$40 to $48**. While this is a small amount of money, it prompted us to have a wider look into this market. Something we try to do at ServiceStack is to not only provide a comprehensive .NET Framework for building API first systems, but also seek out great value hosting options we can recommend in this ever change space which we're happy to share, like this blog post, that might be useful to our users and others. Not everyone builds massively distributed systems, and as hardware performance increases, and platforms like [.NET are becoming even more optimized](https://devblogs.microsoft.com/dotnet/performance-improvements-in-aspnet-core-6), a setup with just a server or two can manage larger loads and use cases. Our research and evaluations ended up right back at [Hetzner but this time with their Cloud offering](https://www.hetzner.com/cloud). For less than **$15 USD** per month, you can get a **4 vCPU, 8GB RAM, 160GB storage and 20TB** of data transfer **hosted in the US**. We found this was by far the cheapest offering for a simple fixed monthly hosting, and looked to compare how well it performed against the more traditional cloud hosting setups. ## Litestream and SQLite Our demo applications use [SQLite](https://www.sqlite.org) as a simple way to host the database storage and application together, taking advantage of SQLite's embedded nature. We were also testing out [Litestream](https://litestream.io) as a possible solution to the lack of data backups and safety when using SQLite for more production like workloads. Litestream runs as a separate process and watches your SQLite file for changes and replicates them to storage options like AWS S3, Azure Blob storage and SFTP. [We created several templates to make this easier](https://docs.servicestack.net/ormlite/litestream) and provide a way to bake in automated disaster recovery using Litestream when used with GitHub Actions and our SSH with Docker Compose deployment. With some basic load testing, we noticed that SQLite performed pretty well without any effort on our part, and decided we should see how this compares to the commonly suggested hosting patterns provided by the large cloud providers of AWS and Azure. We used the recommended "Production" setups provided by AWS RDS and Azure SQL Database wizards along with 2 vCPU application server to provide the basis on our comparison. The reason we chose to use the suggested defaults from these providers was to illustrate the power of defaults when offered by market leaders. When compared to a simple SQLite setup, and providers that offer fixed monthly pricing like Hetzner and DigitalOcean, which is often enough to small companies selling Business to Business (B2B) solutions, AWS and Azure recommended "Production" environments can look extremely over priced. One of the main reasons managed database solutions are chosen is the fact that they take care of automated backups and restore if things go wrong. There are other nice features that definitely have a lot of value, but managed disaster recovery is probably the most commonly cited one I've come across for why services like RDS are chosen during early development. Litestream provides this kind of data safety and disaster recovery functionality by targeting cost effective and robust storage solutions like AWS S3 and other cloud provided object stores, and making the backup process close to real-time, and accessible via their CLI. And the embedded nature of SQLite removes the uncertainty of the process of upgrading your database. ## The Test To get a clearer idea how each of these hosting options perform with a fairly modest workload, we used a [Gatling](https://gatling.io) test to simulate a user logging into our sample Bookings application, browsing around and creating a booking. These series of steps had 2 write requests and 8 read, separated by 2 seconds per step. We then setup a Gatling simulation that ramped up adding new users to our system from 5 per second to 15 per second, to add a growing number of users over 10 minutes, then sustained over another 10 minutes.
AWS Gatling Result.
Azure Gatling Result.
Hetzner Gatling Result.
All 3 setups could handle this rate of requests without issue, and though the "Recommended" AWS and Azure setups would have more headroom, the price difference is far too large to ignore, especially as the difference is paid every month. The requests throughput of that this test illustrated ~100rps can suit many many use cases, and SQLite is [really only limited by its single writer design](https://www.sqlite.org/whentouse.html#:~:text=An%20SQLite%20database%20is%20limited,to%20something%20less%20than%20this.). We did previous tests of upto 250rps on the same Hetzner Cloud instance with SQLite, but this was starting to reach the maximum throughput, again purely to do with the single writer limitation.
Previous test result price comparison without AWS using Provisioned IOPS.
This level of throughput is enough to service many kinds of businesses with a drastically more simple system to manage, with large cost savings. Also, with the use of an ORM like [OrmLite](https://docs.servicestack.net/ormlite), switching to another database provider can be migrated if and when the traditional offerings like Postgres are needed. ## The Setups The original setup for tests we did in June didn't default to provisioned IOPs for AWS, but when repeating the tests AWS costs blow out due to this feature being enabled by default. Without provisioned IOPs, it drops to around **$132/month** as an estimated cost. The **$300/month** default feature for a "Production" database is very hard for AWS to justify, and I think more of a sign of their poor performing GP2 storage option. Although this will only impact very "chatty" types of applications that need higher IOPs throughput, the difference in performance from RDS vs providers like DigitalOcean and Hetzner can be quite stark.
AWS RDS now defaults to provisioned IOPs for a Production setup, drastically increasing costs.
| | AWS (DB) | AWS (App) | Azure (DB) | Azure (App) | DigitalOcean | Hetzner Cloud | |--------------|-------------------|-----------|------------|-------------|--------------|---------------| | vCPU | 2 | 2 | 4 | 2 | 4 | 4 | | Memory (GB) | 8 | 4 | 10 | 8 | 8 | 8 | | Storage (GB) | 100 (provisioned) | 16 | 32 | 30 | 160 | 160 | | Cost | $442 | $34 | $373 | $70 | $48 | $15 | The above specs were provided as "Production" defaults when using a single database instance. Azure SQL Database defaults to costing $373, during the load test, the database CPU hit ~25%.
Azure SQL database without tuning performs poorly for cost, likely due to lack of indexes
| | AWS (DB) | AWS (App) | Azure (DB) | Azure (App) | Hetzner Cloud | |-----------|----------|-----------|------------|-------------|---------------| | Max CPU % | 8 | 35 | 25 | 45 | 40 | This is without any tuning on any of the databases, so while you like more performance out of the recommended setups, it is still clear SQLite performs well by default, and it is well worth considering not only Hetzner Cloud for value for money, but if your use can only needs a single host with SQLite. ## Hetzner Cloud While we were primarily looking for one of the lowest cost options with simplified pricing, Hetzner Cloud pleasantly surprised us with a few features the larger providers could learn from.
Hetzner Cloud Pricing.
### Creating a new instance is fast Most of the time if will be ready to remote to before you can open your terminal. Not sure if this is due to some kind of pre-creation process on Hetzner part during the creation screen, but everything is very responsive. In my testing from the time the "Create" button was clicked, my SSH commands would succeed within **20 seconds**. ### Live Graphs Another part of the responsiveness is their "Live" graphs for monitoring. It is surprisingly low latency and an extremely stark difference between AWS charging extra for "Detailed" monitoring on EC2 instances. The graphs update every 3-5 seconds in the browser and look to be over a few seconds behind real-time.
Live monitoring updates every 3-5 seconds.
CloudWatch is a major value add for AWS, and Hetzner's offering is very very basic in comparison, but it is nice to see live updating stats right in your web browser, and something hopefully the other providers can also offer in the future. ### Price This is the biggest draw card by a long way. The AWS and Azure "recommended" setups are extremely expensive for the hardware and performance they offer. Yes they are mature cloud offerings with a large array of features, but their **pricing scales with hardware resources**. Products like **Provisioned IOPs** are extremely expensive, and when other cloud providers are offering far more performant and competitive storage with their instances, it can feel like AWS is using it's market share and their defaults to upsell very expensive products. ### Transfer costs It's been long known that one of the ways large cloud providers keep customers in their network is by charging [excessively large and complex data egress costs](https://aws.amazon.com/blogs/architecture/overview-of-data-transfer-costs-for-common-architectures). Something attractive about simplified pricing from Hetzner Cloud (and DigitalOcean to a lesser degree) is the included data transfer of 20TB a month. Not only is AWS data transfer pricing extremely complicated (inter region vs cross region vs CloudFront vs Transit Gateway and so on), but if your application was sending a lot of data to clients, that same **20TB** you get for free with a **$15 server**, would cost **$1,791 just for data** when coming from AWS. Azure pricing also confusing, and in some ways more expensive. ## Defaults are powerful Both AWS and Azure "recommended" defaults are there not because the software selected (SQL Server and Postgres) need that amount of resources just to operate, but more as an upsell. Lots of projects and applications absolutely do not need features like "Provisioned IOPs", despite GP2 storage of AWS being incredibly slow. Performing disk speed check using the Linux utility `fio` an AWS EC2 instance with 100GB GP2 storage can do ~2250 IOPS and 9MB/s read, and ~750 IOPs at 3MB/s write. In contrast, Digital Ocean $48 instance, this is not even paying the extra $8/month for the faster storage can do 35.2k IOPS at 144MB/s read, and 11.8k IOPS at 48MB/s write. Hetzner again is the stand out, with the $15 instance tests resulting in 50.8k IOPS at 207MB/s read, and 16.9k IOPS at 69MB/s write. | | Read IOPS | Write IOPs | Read MBs | Write MBs | |---------------|-----------|------------|-----------|-----------| | AWS | 2.3k | 0.8k | 9.2 MB/s | 3.1 MB/s | | Azure | 3.0k | 1.0k | 12.5 MB/s | 4.2 MB/s | | DigitalOcean | 35.2k | 11.8k | 144 MB/s | 48.2 MB/s | | Hetzner Cloud | 50.5k | 16.9k | 207 MB/s | 69.2 MB/s | All tests used the following `fio` command. ```shell fio --randrepeat=1 --ioengine=libaio --direct=1 --gtod_reduce=1 --name=test \ --filename=test --bs=4k --iodepth=64 --size=4G --readwrite=randrw --rwmixread=75 ``` ## SQLite Part of the resurgence in popularity of using SQLite is not only the simplicity of a single server, but also as hardware is getting faster, issues surrounding limitations of a single writer are becoming less of an issue for a wider number of use cases. Litestream's elegant solution for streaming backups to cheap replica storage is definitely adding to that popularity as well since it was a sticking point for a lot of use cases that need that simple data redundancy functionality. Other solutions for Postgres like `pgbackrest` are similar, but the ease of use is another big part of what makes SQLite and Litestream a great combination. One command to watch and replicate, another to restore, and it runs completely independent of your application using the SQLite file. ## Hetzner Cloud is hard to beat on price We're going to keep testing Hetzner Cloud with new applications and use cases going into the future. While they are a very new player in the crowded Cloud Provider market, and their offerings are much more limited, the pricing is a breath of fresh air from the large three providers. More competition in this space is a great thing, and for those that can use solutions like SQLite for their projects, checking out some of the smaller players like DigitalOcean and Hetzner Cloud is well worth your time. The early signs from Hetzner Cloud is they not only have an amazing value product, but the features they do have improve on the equivalents from likes of AWS and Azure, which is hopefully a sign of things to come from them. # Real-time search with Typesense Source: https://razor-ssg.web-templates.io/posts/typesense We have [recently migrated](/blog/jekyll-to-vitepress) the [ServiceStack Docs](https://docs.servicestack.net) website from using Jekyll for static site generation (SSG) to using [VitePress](https://vitepress.vuejs.org) which enables us to use Vite with Vue 3 components and have an insanely fast hot reload while we update our documentation. VitePress is very well suited to documentation sites, and it is one of the primary use cases for VitePress at the time of writing. The default theme even has optional [integration with Algolia DocSearch](https://vitepress.vuejs.org/config/algolia-search). However, the Algolia DocSeach product didn't seem to offer the service for commercial products even as a paid service and their per request pricing model made it harder to determine what our costs would be in the long run for using their search service for our documentation. We found [Typesense](https://typesense.org) as an appealing alternative which offers [simple cost-effective cloud hosting](https://cloud.typesense.org) but even better, they also have an easy to use open source option for self-hosting or evaluation. We were so pleased with its effortless adoption, simplicity-focus and end-user UX that it quickly became our preferred way to [navigate our docs](https://docs.servicestack.net). So that more people can find out about Typesense's amazing OSS Search product we've documented our approach used for creating and deploying an index of our site using GitHub Actions. Documentation search is a common use case which Typesense caters for with their [typesense-docsearch-scraper](https://github.com/typesense/typesense-docsearch-scraper). This is a utility designed to easily scrape a documentation site and post the results to a Typesense server to create a fast searchable index. ## Self hosting option Since we already have several AWS instances hosting our example applications, we opted to start with a self hosting on AWS Elastic Container Service (ECS) since Typesense is already packaged into [an easy to use Docker image](https://hub.docker.com/r/typesense/typesense/). Trying it locally, we used the following commands to spin up a local Typesense server ready to scrape out docs site. ```shell mkdir /tmp/typesense-data docker run -p 8108:8108 -v/tmp/data:/data typesense/typesense:0.21.0 \ --data-dir /data --api-key= --enable-cors ``` To check that the server is running, we can open a browser at `/health` and we get back 200 OK with `ok: true`. The Typesense server has a [REST API](https://typesense.org/docs/0.21.0/api) which can be used to manage the indexes you create. The cloud offering comes with a web dashboard to manage your data which is a definite advantage over the self hosting, but for now we were still trying it out. ## Populating our index Now that our local server is running, we can scrape our docs site using the [typesense-docsearch-scraper](https://github.com/typesense/typesense-docsearch-scraper). This needs some configuration since the scraper needs to know: - Where is the Typesense server. - How to authenticate with the Typesense server. - Where is the docs website. - Rules for the scraper to follow extracting information from the docs website. These [pieces of configuration come from 2 sources](https://github.com/ServiceStack/docs/tree/master/search-server/typesense-scraper). A [`.env` file](https://github.com/ServiceStack/docs/blob/master/search-server/typesense-scraper/typesense-scraper.env) related to the Typesense server information and [a `.json` file](https://github.com/ServiceStack/docs/blob/master/search-server/typesense-scraper/typesense-scraper-config.json) related to what site will be getting scraped. With our Typesense running locally on port 8108, we configure the .env file with the following information. ``` TYPESENSE_API_KEY=${TYPESENSE_API_KEY} TYPESENSE_HOST=localhost TYPESENSE_PORT=8108 TYPESENSE_PROTOCOL=http ``` Next, we have the `.json` config for the scraper. The [typesense-docsearch-scraper gives an example of this config in their repository](https://github.com/typesense/typesense-docsearch-scraper/blob/master/configs/public/typesense_docs.json) for what this config should look like. Altering the default selectors to match the HTML for our docs site, we ended up with a configuration that looked like this. ```json { "index_name": "typesense_docs", "allowed_domains": ["docs.servicestack.net"], "start_urls": [ { "url": "https://docs.servicestack.net/" } ], "selectors": { "default": { "lvl0": ".page h1", "lvl1": ".content h2", "lvl2": ".content h3", "lvl3": ".content h4", "lvl4": ".content h5", "text": ".content p, .content ul li, .content table tbody tr" } }, "scrape_start_urls": false, "strip_chars": " .,;:#" } ``` Now we have both the configuration files ready to use, we can run the scraper itself. The scraper is also available using the docker image `typesense/docsearch-scraper` and we can pass our configuration to this process using the following command. ```shell docker run -it --env-file typesense-scraper.env \ -e "CONFIG=$(cat typesense-scraper-config.json | jq -r tostring)" \ typesense/docsearch-scraper ``` Here we are using `-i` so we can reference our local `--env-file` and use `cat` and `jq` to populate the `CONFIG` environment variable using our `.json` config file. ## Docker networking Here we run into a bit of a issue, since the scraper itself is running in Docker via WSL, `localhost` isn't resolving to our host machine to find the Typesense server also running in Docker. Instead we need to point the scraper to the Typesense server using the Docker local IP address space of 172.17.0.0/16 for it to resolve without additional configuration. We can see in the output of the Typesense server that it is running using `172.17.0.2`. We can swap the `localhost` with this IP address and communication is flowing. ``` DEBUG:typesense.api_call:Making post /collections/typesense_docs_1635392168/documents/import DEBUG:typesense.api_call:Try 1 to node 172.17.0.2:8108 -- healthy? True DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): 172.17.0.2:8108 DEBUG:urllib3.connectionpool:http://172.17.0.2:8108 "POST /collections/typesense_docs_1635392168/documents/import HTTP/1.1" 200 None DEBUG:typesense.api_call:172.17.0.2:8108 is healthy. Status code: 200 > DocSearch: https://docs.servicestack.net/azure 22 records) DEBUG:typesense.api_call:Making post /collections/typesense_docs_1635392168/documents/import DEBUG:typesense.api_call:Try 1 to node 172.17.0.2:8108 -- healthy? True DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): 172.17.0.2:8108 DEBUG:urllib3.connectionpool:http://172.17.0.2:8108 "POST /collections/typesense_docs_1635392168/documents/import HTTP/1.1" 200 None ``` The scraper crawls the docs site following all the links in the same domain to get a full picture of all the content of our docs site. This takes a minute or so, and in the end we can see in the Typesense sever output that we now have "committed_index: 443". ``` _index: 443, applying_index: 0, pending_index: 0, disk_index: 443, pending_queue_size: 0, local_sequence: 44671 I20211028 03:39:40.402626 328 raft_server.h:58] Peer refresh succeeded! ``` ## Searching content So now we have a Typesense server with an index full of content, we want to be able to search it on our docs site. Querying our index using straight `cURL`, we can see the query itself only needs to known 3 pieces of information at a minimum. - Collection name, eg `typesense_docs` - Query term, `?q=test` - What to query, `&query_by=content` ```shell curl -H 'x-typesense-api-key: ' \ 'http://localhost:8108/collections/typesense_docs/documents/search?q=test&query_by=content' ``` The collection name and `query_by` come from how our scraper were configured. The scraper was posting data to the `typesense_docs` collection and populating various fields, eg `content`. Which as it returns JSON can be easily queried in JavaScript using **fetch**: ```js fetch('http://localhost:8108/collections/typesense_docs/documents/search?q=' + encodeURIComponent(query) + '&query_by=content', { headers: { // Search only API key for Typesense. 'x-typesense-api-key': 'TYPESENSE_SEARCH_ONLY_API_KEY' } }) ``` In the above we have also used a different name for the API key token, this is important since the `--api-key` specified to the running Typesense server is the admin API key. You don't want to expose this to a browser client since they will have the ability to create,update and delete your collections or documents. Instead we want to generate a "Search only" API key that is safe to share on a browser client. This can be done using the Admin API key and the following REST API call to the Typesense server. ```bash curl 'http://localhost:8108/keys' -X POST \ -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \ -H 'Content-Type: application/json' \ -d '{"description": "Search only","actions": ["documents:search"],"collections":["*"]}' ``` Now we can share this generated key safely to be used with any of our browser clients. ## Keeping the index updated Another problem that becomes apparent is that subsequent usages of the scraper increases the size of our index since it currently doesn't detect and update existing documents. It wasn't clear if this is possible to configure or manage from the current scraper (ideally by using URL paths as the unique identifier), so we needed a way to achieve the following goals. - Update the search index automatically soon after docs have been changed - Don't let the index grow too big causing manual intervention - Have high uptime for our search so users can always search our docs Typesense server itself performs extremely well, so a full update from the scraper doesn't generate an amount of load that is of much a concern. This is also partly because the scraper seems to be sequentially crawling pages so it can only generate so many updates on single thread. However, every additional scrape will use additional disk space and memory for the running server causing us to periodically reset the index and repopulate, causing downtime. One option is to switch to a new collection everytime we update the docs sites and delete the old collection. This requires additional orchestration between client and the server, and to avoid down time the following order of operations would be needed. - Docs are updated - Publish updated docs - Create new collection, store new name + old name - Scrape updated docs - Update client with new collection name - Delete old collection This dance would require multiple commits/actions in GitHub (we use GitHub Actions), and also be time sensitive since it will be non-deterministic as to how long it will take to scrape, update, and deploy our changes. Additional operational burden is something we want to avoid since it an on going cost on developer time that would otherwise be spent improving ServiceStack offerings for our customers. ## Read-only Docker container Something to keep in mind when making architecture decisions is looking at the specifics of what is involved when it comes to the *flow of data* of your systems. You can ask yourself questions like: - What data is updated - When/How often is data updated - Who updates the data The answers to these questions can lead to choices that can exploit either the frequency, and/or availability of your data to make it easier to manage. A common example of this is when deciding how to cache information in your systems. Some data is write heavy, making it a poor choice for cache while other data might rarely change, be read heavy and the update process might be completely predictable making it a great candidate for caching. If update frequency of the data is completely in your control and/or deterministic, you have a lot more choices when it comes to how to manage that data. In the case of Typesense, when it starts up, it reads from its `data` directory from disk to populate the index in memory and since our index is small and only updates when our documentation is updated, we can simplify the management of the index data by **baking it straight into a docker image**. Making our hosted Typesense server read-only, we can build the index and our own Docker image, with the index data in it, as a part of our CI process. This has several key advantages. - Disaster recovery doesn't need any additional data management. - Shipping an updated index is a normal ECS deployment. - Zero down time deployments. - Index is of a fixed size once deployed. To make things even more simplified, the incremental improvement of our documentation means that the difference between search index between updates is very small. This means if our search index is updated even a day after the actual documentation, the vast majority of our documentation is still accurately searchable by our users. Search on our documentation site is a very light workload for Typesense. Running as an ECS service on a 2 vCPU instance, the service struggled to get close to 1% with constant typeahead searching. ![](img/posts/typesense/typesense-cpu-utilization.png) And since our docs site index is so small, the memory footprint is also tiny and stable at ~50MB or ~10% of the the service's soft memory limit. ![](img/posts/typesense/typesense-memory-utilization.png) This means we will be able to host this using a single EC2 instance among various other or the ServiceStack hosted example applications and use the same [deployment patterns we've shared in our GitHub Actions templates](https://docs.servicestack.net/mix-github-actions-aws-ecs). [![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/cloudcraft-host-digram-release-ecr-aws.png)](https://docs.servicestack.net/mix-github-actions-aws-ecs) So while this approach of shipping an index along with the Docker image isn't practical for large or 'living' indexes, many opensource documentation sites would likely be able to reuse this simplified approach. ## GitHub Actions Process Since the ServiceStack docs site is hosted using GitHub Pages and we already use GitHub Actions to publish updates to our docs, using GitHub Actions was the natural place for this automation. To create our own Docker image for our search server we need to perform the following tasks on our CI process. - Run a local Typesense server on the CI via Docker - Scrape our hosted docs populating the local Typesense server - Copy the `data` folder of our local Typesense server during `docker build` The whole process in GitHub Actions looks like this. ```shell mkdir -p ${GITHUB_WORKSPACE}/typesense-data cp ./search-server/typesense-server/Dockerfile ${GITHUB_WORKSPACE}/typesense-data/Dockerfile cp ./search-server/typesense-scraper/typesense-scraper-config.json typesense-scraper-config.json envsubst < "./search-server/typesense-scraper/typesense-scraper.env" > "typesense-scraper-updated.env" docker run -d -p 8108:8108 -v ${GITHUB_WORKSPACE}/typesense-data/data:/data \ typesense/typesense:0.21.0 --data-dir /data --api-key=${TYPESENSE_API_KEY} --enable-cors & # wait for typesense initialization sleep 5 docker run -i --env-file typesense-scraper-updated.env \ -e "CONFIG=$(cat typesense-scraper-config.json | jq -r tostring)" typesense/docsearch-scraper ``` Our `Dockerfile` then takes this data from the `data` folder during build. ```Dockerfile FROM typesense/typesense:0.21.0 COPY ./data /data ``` One additional problem we had was related to the search only API key generation. As expected when generating API keys, we don't want the process to generate reused API keys, but to avoid needing to update our search client between updates, we actually want to use the same search only API key everytime we generate a new server. This can be achieved by specifying `value` in the `POST` command sent to the local Typesense server. ```bash curl 'http://172.17.0.2:8108/keys' -X POST \ -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \ -H 'Content-Type: application/json' \ -d '{"value":,"description":"Search only","actions":["documents:search"],"collections":["*"]}' ``` Once our custom Docker image has been built, we deploy it to AWS Elastic Container Repository (ECR), register a new `task-defintion.json` with ECS pointing to our new image, and finally update the running ECS Service to use the new task definition. To make things more hands off and reduce any possible issues from GitHub Pages CDN caching, updates to our search index are done on a daily basis using GitHub Action `schedule`. Once a day, the process checks if the latest commit in the repository is less than 1 day old. If it is,we ship an updated search index, otherwise we actually cancel the GitHub Action process early to save on CI minutes. The whole GitHub Action can be seen in our [ServiceStack/docs repository](https://github.com/ServiceStack/docs/blob/master/.github/workflows/search-index-update.yml) if you are interested or are setting up your own process the same way. ## Search UI Dialog Now that our docs are indexed the only thing left to do is display the results. We set out to create a comparable UX to algolia's doc search which we've implemented in custom Vue3 components and have Open sourced in [this gist](https://gist.github.com/gistlyn/d215e9ff31abd9adce719a663a4bd8af) in hope it will serve useful in adopting typesearch for your own purposes. As VitePress is a SSG framework we need to wrap them in a [ClientOnly component](https://vitepress.vuejs.org/guide/global-component.html#clientonly) to ensure they're only rendered on the client: ```html ``` Where the logic to capture the window global shortcut keys is wrapped in a hidden [KeyboardEvents.vue](https://gist.github.com/gistlyn/d215e9ff31abd9adce719a663a4bd8af#file-keyboardevents-vue): ```html ``` Which is handled in our custom [Layout.vue](https://gist.github.com/gistlyn/d215e9ff31abd9adce719a663a4bd8af#file-layout-vue) VitePress theme to detect when the `esc` and `/` or `CTRL+K` keys are pressed to hide/open the dialog: ```ts const onKeyDown = (e:KeyboardEvent) => { if (e.code === 'Escape') { hideSearch(); } else if ((e.target as HTMLElement).tagName != 'INPUT') { if (e.code == 'Slash' || (e.ctrlKey && e.code == 'KeyK')) { showSearch(); e.preventDefault(); } } }; ``` The actual search dialog component is encapsulated in [TypeSenseDialog.vue](https://gist.github.com/gistlyn/d215e9ff31abd9adce719a663a4bd8af#file-typesensedialog-vue) (utilizing tailwind classes, scoped styles and inline SVGs so is easily portable), the integral part being the API search query to our typesense instance: ```js fetch('https://search.docs.servicestack.net/collections/typesense_docs/documents/search?q=' + encodeURIComponent(query.value) + '&query_by=content,hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3&group_by=hierarchy.lvl0', { headers: { // Search only API key for Typesense. 'x-typesense-api-key': 'TYPESENSE_SEARCH_ONLY_API_KEY' } }) ``` Which instructs Typesense to search through each documents content and h1-3 headings, grouping results by its page title. Refer to the [Typesense API Search Reference](https://typesense.org/docs/0.21.0/api/documents.html#search) to learn how to further fine-tune search results for your use-case. ## Search Results ![](img/posts/typesense/typesense-dart.gif) The results are **excellent**, [see for yourself](https://docs.servicestack.net) by using the search at the top right or using Ctrl+K shortcut key on our docs site. It handles typos really well, it is very quick and has become the fastest way to navigate our extensive documentation. We have been super impressed with the search experience that Typesense enabled, the engineers behind the Typesense product have created something with a better developer experience than even paid SaaS options and provided it with a clear value proposition. ## ServiceStack a Sponsor of Typesense We were so impressed by the amazing results, that as a way to show our thanks we've become sponsors of [Typesense on GitHub sponsors](https://github.com/sponsors/typesense) 🎉 🎉 We know how challenging it can be to try make open source software sustainable that if you find yourself using amazing open source products like Typesense which you want to continue to see flourish, we encourage finding out how you or your organization can support them, either via direct contributions or by helping spread the word with public posts (like this) sharing your experiences as their continued success will encourage further investments that ultimately benefits the project and all its users. # Migrating from Jekyll to VitePress Source: https://razor-ssg.web-templates.io/posts/jekyll-to-vitepress Since Jekyll support has been officially sunset, we decided to migrate our docs site to VitePress. VitePress is a new static site generator based on Vite and Vue 3. It is a cut down version of VuePress but using the blisteringly fast Vite build tool. This has given us the ability to update our docs locally with instant on save update to view the final result quickly while we edit/create docs.
Vite logo VitePress logo
We wanted to share our experience with this process in the hope it might help others that are performing the same migration, document common errors and help other developer to get an idea about what is involved before you might undertake such a task. ## Jekyll vs VitePress Below is not an exhaustive list of features but more focusing on pros and cons when it comes to comparing these two options for use as the static site generator for a documentation site. | Features | Jekyll | VitePress | |:-------------------|:------------------|:-------------------------------| | Native Language | Ruby | JavaScript | | Template Syntax | Liquid | Vue | | Update Time | 6-30 seconds | 30-500 ms | | Themes | ✅ - mature market | ✅ - limited | | Extensible | ✅ | ✅ (only themes + vite plugins) | | Client framework | None | Vue 3 | | Maintained | No longer | New project | | 1.0+ release | ✅ | ➖ | | Permalink | ✅ | ➖ (depends on filename) | | Markdown support | ✅ | ✅ | | HTML support | ✅ | ✅ (must use Vue component) | | Sitemap generation | ✅ | ➖ | | Tags | ✅ | ➖ | | Clean URLs | ✅ | ➖ | This list might look bad for VitePress, but it comes down to a young library that is still in active development. The default theme for VitePress is also centered around technical documentation making it quick to get up and running looking good for this use case. ## Killer feature - Performance The stand out feature and one of the most compelling reason for us to undertake this migration was not the default theme (although that helped) but the user experience when editing documentation locally. ![](img/posts/jekyll-to-vitepress/vitepress-update-large.gif) In contrast, editing this blog post for our main site which is still currently using Jekyll, it takes between 6-8 seconds for a small single page change. Previously our docs page with over 300 pages could take over a minute depending on the type of change. ``` Regenerating: 1 file(s) changed at 2021-10-29 16:17:00 _blog/posts/2021-10-29-jekyll-migration.md ...done in 6.3882767 seconds. ``` Having a statically generated site that is hard to preview or has a slow iteration cycle can be extremely frustrating to work with so we were looking for something that a pleasure to work with and well suited to documentation. ## Common Migration Problems When we started this task to migrate, we want to first have a proof of concept of migrating all the content to the same URLs. This required us to achieve a few things before we could get stuck into fixing content/syntax related changes. - Page URLs must be the same. - Project must be able to run locally and deploy. - Side menu links must be present. These were our minimum requirements before we wanted to commit to making all the changes required to migrate. ## File name vs `slug` The first one surfaced a design difference straight away. While Jekyll created HTML output based on frontmatter `slug` property, VitePress only works off the MarkDown file name. We would commonly create a MarkDown file called one thing but change our mind on the URL and change the frontmatter `permalink` to something else. This broke all our existing paths, so we needed a way to parse the markdown files, and produce copies with the updated name using the `slug` value in the frontmatter. Since we were still evaluating this process, we created a quick C# script that would take in a file, extract the `slug` value and copy a file with that name back out into a separate directory. ```csharp static void Main(string[] args) { var filename = args[0]; var fileLines = File.ReadAllLines(filename).ToList(); if (!Directory.Exists("updated")) { Directory.CreateDirectory("updated"); } foreach (var line in fileLines) { if (line.StartsWith("slug:")) { var newName = line.Split(":")[1].Trim(); File.WriteAllLines("./updated/" + newName + ".md", fileLines); } } } ``` Not very elegant, but it did the job. This was published as a `single file` executable targeting linux so it could be easily used with tools like `find` with `--exec`. ```shell find *.md -maxdepth 1 -type f -exec ./RenameMd {} \; ``` All round pretty hacky, but this was also while we were still evaluating VitePress, so it was considered a throw away script. > This was run in each directory as needed, if `slug` or `permalink` is controlling your nested pathing, this problem will be more complex to handle. This was run for our main folder of docs as well as our `releases` folder and we have successfully renamed files. ## Broken links are build failures VitePress is more strict with issues than Jekyll. This is actually a good thing, especially as your site content grows. VitePress will fail building your site if in your markdown to link to a relative link that it can't see is a file. This comes from the above design decision of not aliasing files to output paths. Markdown links like `[My link](/my-cool-page)` needs to be able to see `my-cool-page.md`. This means if you move or rename a file, it will break if something else links to it. Jekyll got around this by allowing the use of `permalink` and `slug` which is great for flexibility, but means at build time it can't be sure (without a lot more work) if the relative path won't be valid. There are drawbacks to this though. If you host multiple resources under the same root path as your VitePress site and you want to reference this, I'm not sure you will be able to. You might have to resort to absolute URLs to link out to resources like this. And since VitePress doesn't alias any paths, it means your hosting environment will need to do this. ## Syntax issues Jekyll is very forgiving when it comes to content that is passed around as straight html and put in various places using Liquid. For example if you have the following HTML in an `include` for Jekyll. ```html

This solution is <50 lines of code

``` Jekyll will just copy it and not bother you about the invalid HTML issues of having a `less-than (<)` in the middle of a `

` element. VitePress won't however, and you'll need to correctly use `<` and `>` encoded symbols appropriately. ## Include HTML Another issue is the difference of how to reuse content. In Jekyll, you would use `{% include my/path/to/file.html %}`. This will likely show up in errors like `[vite:vue] Duplicate attribute`. Instead in VitePress, an include of straight HTML will require migrating that content to a Vue component. For example, if we have content file `catchphrase.html` like the following. ```html

Catchphrase

It's.. what I do..

``` We would need to wrap this in a Vue component like `catchphrase.vue`: ```html ``` Then it would need to be imported. This can be declared globally in the vitepress theme config or adhoc in the consuming Markdown file itself. ```markdown ``` The `` is where it is injected into the output. For HTML so simple, this could be instead converted to Markdown and used the same way. ```markdown ## Catchphrase it's.. what I do.. ``` And then used: ```markdown ``` ## Jekyll markdownify redundant Something similar is done in Jekyll, but with the use of Liquid filters. ```markdown {% capture projects %} {% include web-new-netfx.md %} {% endcapture %} {{ projects | markdownify }} ``` This use of `capture` and passing the content to be converted is done by default when importing. ```markdown ``` If the module is declared global, then only the `` is needed anywhere in your site to embed the content. ## Templating syntax the same but different When moving from Jekyll to VitePress, I came across errors like `Cannot read property 'X' of undefined`. It was referring to some example code in a page we had that looked something like this. ```markdown Content text here with templating code example below. Value: {{X.prop}} More explanation here. ``` This error came about because we didn't religiously fence our code examples. Jekyll let us get away with this and actually produced the visuals we wanted without trying to render value in the handlebars `{{ }}` syntax. VitePress only ignores these if they are in a code fence using the triple tilda syntax OR if the content is within a `:::v-pre` block. ## Replacing `raw` and `endraw` Since some of our documentation used handlebar syntax in example code, we needed a way for Jekyll to ignore these statements and just present our code. `raw` and `endraw` were used which were usually wrapping code blocks. VitePress doesn't have a problem with this syntax which means the `{% raw %}` statements were included in our page which we didn't want. This was a matter of finding where these were used in all our documents and replace them with `::: v-pre` blocks as needed. ## Sidebar This was a different situation. In Jekyll, we had created a `SideBar.md` file to help us render a left hand side menu of contents for the docs site. In VitePress's default theme, we could provide a JSON representation and the client would then dynamically populate it further using different levels of headings. To do this, we used a simple NodeJs script that used the `markdown-it` library to parse the MarkDown itself and produce the expected JSON. Loading the file and extracting the elements we needed was quite straight forward. ```js let fs = require('fs') let MarkdownIt = require('markdown-it'); let md = new MarkdownIt(); let content = fs.readFileSync('SideBar.md','utf8') let res = md.parse(content).filter((element, index) => { return (element.level == '3' || element.level == '5') && element.type == 'inline'; }); ``` Now we had `res` that contained all the information we would need, we can iterate the array, transforming the data as we go. ```js let sidebarObjs = { '/': [] }; var lastHeading = null; var lastHeadingIndex = -1; for(let i = 0; i < res.length; i++) { let item = res[i]; if(item.level == '3') { lastHeading = item.content; sidebarObjs['/'].push({ text: lastHeading, children: [] }) lastHeadingIndex++; continue; } let text = item.children[1].content; let link = item.children[0].attrs[0][1]; sidebarObjs['/'][lastHeadingIndex].children.push({ text: text, link: link }) } fs.writeFileSync('SideBar_format.json',JSON.stringify(sidebarObjs),'utf-8') ``` Once working, we later split this into multiple menus and condensed what we already had to make the menu more manageable. ## Problems we worked around That covers the bulk of the changes we made that prevented us from running, building and deploying our application, however there were some short comings that we had to work around outside of VitePress itself. There were 2 main sticking points for which we had to come up with a creative work around. - Client router always appending `.html` (breaking clean URLs) - Failing to route to paths with addition dots (`.`) in the URL Hosting clean URLs can be done from the server side, so we used AWS CloudWatch functions to rewrite the request to append the `.html` in the backend if it was not provided. However, VitePress still generated all page links with `.html`, creating multiple paths for the single document which can cause problems with other processes such as search indexing. Since our documentation is something we edit quite frequently, we didn't want to be stuck in limbo due to these two problems while we workout and propose how VitePress itself might allow for such a setup. Our solution **is NOT recommended** since it will be quite brittle which we have accepted. We found the 2 locations in the VitePress source that were causing this problem during static site generation. Thankfully, the logic is simple and we can remove 2 lines of code during the `npm install` phase out of our `node_modules` directory so that our CI builds would be consistent. ```js const fs = require('fs'); const glob = require('glob'); let js = 'node_modules/vitepress/dist/client/app/router.js'; fs.writeFileSync(js, fs.readFileSync(js, 'utf8').replace("url.pathname += '.html';", '')) glob('node_modules/vitepress/dist/node/serve-*.js',{},(err,files) =>{ let file = files[0]; fs.writeFileSync(file,fs.readFileSync(file,'utf8').replace("cleanUrl += \".html\";",'')) }) console.log('Completed post install process...') ``` > NOT RECOMMENDED If you have standard `.html` paths on your existing site, you won't need the above. It is just a temporary workaround for getting clean URLs, which is again, *not recommended*. ## Verdict While we have made the jump to VitePress, it is still young and under heavy development. There is a good chance that some of the behaviour and lacking features will make this not a viable option for migrating off Jekyll. And we knew this going in as it is very clearly outlined on the front page of their docs: ![](img/posts/jekyll-to-vitepress/vitepress-warning.png) However, while there are still outstanding issues, the developer experience of Vue 3 combined with Vite and an SSG theme aimed at producing documentation is extremely compelling. The work Even You and the VitePress community are doing is something to look out for as currently we believe it offers one of the best content heavy site development experiences currently possible. ## ServiceStack a Sponsor of Vue's Evan You As maintainers of several [Vue & .NET Project Templates](https://docs.servicestack.net/templates-vue), we're big fans of Evan's work creating Vue, Vite and his overall stewardship of their surrounding ecosystems which greatly benefits from his master design skills and fine attention to detail in both library and UI design who has a talent in creating inherently simple technologies that progressively scales up to handle the complexest of Apps. We believe in and are excited for the future of Vue and Vite that to show our support ServiceStack is now a sponsor of [Evan You on GitHub sponsors](https://github.com/sponsors/yyx990803) 🎉 🎉 # Speaking Source: https://razor-ssg.web-templates.io/speaking ## I’ve spoken at events all around the world and been interviewed for many podcasts. One of my favorite ways to share my ideas is live on stage, where there’s so much more communication bandwidth than there is in writing, and I love podcast interviews because they give me the opportunity to answer questions instead of just present my opinions. ## Conferences > SysConf 2021 #### In space, no one can watch you stream — until now A technical deep-dive into HelioStream, the real-time streaming library I wrote for transmitting live video back to Earth. [Watch video](#) > Business of Startups 2020 #### Lessons learned from our first product recall They say that if you’re not embarrassed by your first version, you’re doing it wrong. Well when you’re selling DIY space shuttle kits it turns out it’s a bit more complicated. [Watch video](#) ## Podcasts > Encoding Design, July 2022 #### Using design as a competitive advantage How we used world-class visual design to attract a great team, win over customers, and get more press for Planetaria. [Listen to podcast](#) > The Escape Velocity Show, March 2022 #### Bootstrapping an aerospace company to $17M ARR The story of how we built one of the most promising space startups in the world without taking any capital from investors. [Listen to podcast](#) > How They Work Radio, September 2021 #### Programming your company operating system On the importance of creating systems and processes for running your business so that everyone on the team knows how to make the right decision no matter the situation. [Listen to podcast](#) # Things I use & love Source: https://razor-ssg.web-templates.io/uses ## Software I use, gadgets I love, and other things I recommend. I get asked a lot about the things I use to build software, stay productive, or buy to fool myself into thinking I’m being productive when I’m really just procrastinating. Here’s a big list of all of my favorite stuff. ## Workstation #### 16” MacBook Pro, M1 Max, 64GB RAM (2021) I was using an Intel-based 16” MacBook Pro prior to this and the difference is night and day. I’ve never heard the fans turn on a single time, even under the incredibly heavy loads I put it through with our various launch simulations. #### Apple Pro Display XDR (Standard Glass) The only display on the market if you want something HiDPI and bigger than 27”. When you’re working at planetary scale, every pixel you can get counts. #### IBM Model M SSK Industrial Keyboard They don’t make keyboards the way they used to. I buy these any time I see them go up for sale and keep them in storage in case I need parts or need to retire my main. #### Apple Magic Trackpad Something about all the gestures makes me feel like a wizard with special powers. I really like feeling like a wizard with special powers. #### Herman Miller Aeron Chair If I’m going to slouch in the worst ergonomic position imaginable all day, I might as well do it in an expensive chair. ## Development tools #### Sublime Text 4 I don’t care if it’s missing all of the fancy IDE features everyone else relies on, Sublime Text is still the best text editor ever made. #### iTerm2 I’m honestly not even sure what features I get with this that aren’t just part of the macOS Terminal but it’s what I use. #### TablePlus Great software for working with databases. Has saved me from building about a thousand admin interfaces for my various projects over the years. ## Design #### Figma We started using Figma as just a design tool but now it’s become our virtual whiteboard for the entire company. Never would have expected the collaboration features to be the real hook. ## Productivity #### Alfred It’s not the newest kid on the block but it’s still the fastest. The Sublime Text of the application launcher world. #### Reflect Using a daily notes system instead of trying to keep things organized by topics has been super powerful for me. And with Reflect, it’s still easy for me to keep all of that stuff discoverable by topic even though all of my writing happens in the daily note. #### SavvyCal Great tool for scheduling meetings while protecting my calendar and making sure I still have lots of time for deep work during the week. #### Focus Simple tool for blocking distracting websites when I need to just do the work and get some momentum going. # Community Rules Source: https://razor-ssg.web-templates.io/community-rules MyApp is where anyone is welcome to learn about what we do. We want to keep it a welcome place, so we have created this ruleset to help guide the content posted on this website. If you see a post or comment that breaks the rules, we welcome you to report it to the our moderators. These rules apply to all community aspects on this website: all parts of a public post (title, description, tags, visual content), comments, links, and messages. Moderators consider context and intent while enforcing the community rules. - No nudity or sexually explicit content. - Provocative, inflammatory, unsettling, or suggestive content should be marked as Mature. - No hate speech, abuse, or harassment. - No content that condones illegal or violent activity. - No gore or shock content. - No posting personal information. ### Good Sharing Practices Considering these tips when sharing with this community will help ensure you're contributing great content. #### 1. Value - Good sharing means posting content which brings value to the community. Content which opens up a discussion, shares something new and unique, or has a deeper story to tell beyond the image itself is content that generally brings value. Ask yourself first: is this something I would be interested in seeing if someone else posted it? #### 2. Transparency - We expect that the original poster (OP) will be explicit about if and how they are connected to the content they are posting. Trying to hide that relationship, or not explaining it well to others, is a common feature of bad sharing. #### 3. Respect - Good sharing means knowing when the community has spoken through upvotes and downvotes and respecting that. You should avoid constantly reposting content to User Submitted that gets downvoted. This kind of spamming annoys the community, and it won't make your posts any more popular. Repeated violations of the good sharing practices after warning may result in account ban. If content breaks these community rules, it will be removed and the original poster warned about the removal. Warnings will expire. If multiple submissions break the rules in a short time frame, warnings will accumulate, which could lead to a 24-hour suspension, and further, a ban. If you aren't sure if your post fits the community rules, please don't post it. Just because you've seen a rule-breaking image posted somewhere else on this website doesn't mean it's okay for you to repost it. # Privacy Policy Source: https://razor-ssg.web-templates.io/privacy [Your business name] is committed to providing quality services to you and this policy outlines our ongoing obligations to you in respect of how we manage your Personal Information. We have adopted the Australian Privacy Principles (APPs) contained in the Privacy Act 1988 (Cth) (the Privacy Act). The NPPs govern the way in which we collect, use, disclose, store, secure and dispose of your Personal Information. A copy of the Australian Privacy Principles may be obtained from the website of The Office of the Australian Information Commissioner at https://www.oaic.gov.au/. What is Personal Information and why do we collect it? Personal Information is information or an opinion that identifies an individual. Examples of Personal Information we collect includes names, addresses, email addresses, phone and facsimile numbers. This Personal Information is obtained in many ways including [interviews, correspondence, by telephone and facsimile, by email, via our website www.yourbusinessname.com.au, from your website, from media and publications, from other publicly available sources, from cookies- delete all that aren’t applicable] and from third parties. We don’t guarantee website links or policy of authorised third parties. We collect your Personal Information for the primary purpose of providing our services to you, providing information to our clients and marketing. We may also use your Personal Information for secondary purposes closely related to the primary purpose, in circumstances where you would reasonably expect such use or disclosure. You may unsubscribe from our mailing/marketing lists at any time by contacting us in writing. When we collect Personal Information we will, where appropriate and where possible, explain to you why we are collecting the information and how we plan to use it. Sensitive Information Sensitive information is defined in the Privacy Act to include information or opinion about such things as an individual's racial or ethnic origin, political opinions, membership of a political association, religious or philosophical beliefs, membership of a trade union or other professional body, criminal record or health information. Sensitive information will be used by us only: - For the primary purpose for which it was obtained - For a secondary purpose that is directly related to the primary purpose - With your consent; or where required or authorised by law. Third Parties Where reasonable and practicable to do so, we will collect your Personal Information only from you. However, in some circumstances we may be provided with information by third parties. In such a case we will take reasonable steps to ensure that you are made aware of the information provided to us by the third party. Disclosure of Personal Information Your Personal Information may be disclosed in a number of circumstances including the following: - Third parties where you consent to the use or disclosure; and - Where required or authorised by law. Security of Personal Information Your Personal Information is stored in a manner that reasonably protects it from misuse and loss and from unauthorized access, modification or disclosure. When your Personal Information is no longer needed for the purpose for which it was obtained, we will take reasonable steps to destroy or permanently de-identify your Personal Information. However, most of the Personal Information is or will be stored in client files which will be kept by us for a minimum of 7 years. Access to your Personal Information You may access the Personal Information we hold about you and to update and/or correct it, subject to certain exceptions. If you wish to access your Personal Information, please contact us in writing. [Your business name] will not charge any fee for your access request, but may charge an administrative fee for providing a copy of your Personal Information. In order to protect your Personal Information we may require identification from you before releasing the requested information. Maintaining the Quality of your Personal Information It is an important to us that your Personal Information is up to date. We will take reasonable steps to make sure that your Personal Information is accurate, complete and up-to-date. If you find that the information we have is not up to date or is inaccurate, please advise us as soon as practicable so we can update our records and ensure we can continue to provide quality services to you. Policy Updates This Policy may change from time to time and is available on our website. Privacy Policy Complaints and Enquiries If you have any queries or complaints about our Privacy Policy please contact us at: [Your business address] [Your business email address] [Your business phone number] # About Source: https://razor-ssg.web-templates.io/creatorkit/about [![](/img/pages/creatorkit/creatorkit-brand.svg)](/creatorkit/) [CreatorKit](/creatorkit/) is a simple, customizable alternative solution to using Mailchimp for accepting and managing website newsletter subscriptions and other mailing lists, sending rich emails with customizable email layouts and templates to your Customers and subscribers using your preferred SMTP provider of choice. It also provides a private alternative to using Disqus to enhance websites with threading and commenting on your preferred blog posts and website pages you want to be able to collaborate with your community on. ### Enhance static websites We're developing CreatorKit as an ideal companion for JAMStack or statically generated branded websites like [Razor SSG](https://razor-ssg.web-templates.io/posts/razor-ssg) enabling you to seamlessly integrate features such as newsletter subscriptions, email management, comments, voting, and moderation into your existing websites without the complexity of a custom solution, that's ideally suited for Websites who want to keep all Mailing Lists Contacts and Authenticated User Comments in a different site, isolated from your existing Customer Accounts and internal Systems. With CreatorKit, you can enjoy the convenience of managing your blog's comments, votes, and subscriptions directly from your own hosted [CreatorKit Portal](https://creatorkit.netcore.io/portal/) without needing to rely on complex content management systems to manage your blog's interactions with your readers. Additionally, CreatorKit makes it easy to send emails and templates to different mailing lists, making it the perfect tool for managing your email campaigns. Whether you're a blogger, marketer, or entrepreneur, CreatorKit is a great solution for maximizing your blog's functionality and engagement. ## Features The CreatorKit Portal offers a complete management UI to manage mailing lists, email newsletter and marketing campaigns, thread management and moderation workflow. ### Email Management [![](/img/pages/creatorkit/portal-messages.png)](/creatorkit/portal-messages) ### Optimized Email UI's with Live Previews [![](/img/pages/creatorkit/portal-messages-simple.png)](/creatorkit/portal-messages#email-ui) ### Custom HTML Templates [![](/img/pages/creatorkit/portal-messages-custom.png)](/creatorkit/portal-messages#sending-custom-html-emails) ### HTML Email Templates [![](/img/pages/creatorkit/portal-messages-markdown.png)](/creatorkit/portal-messages#sending-html-markdown-emails) ### Mailing List Email Runs [![](/img/pages/creatorkit/portal-mailrun-custom.png)](/creatorkit/portal-mailruns) ### Newsletter Generation [![](/img/pages/creatorkit/portal-mailrun-newsletter.png)](/creatorkit/portal-mailruns#generating-newsletters) ### Comment Moderation [![](/img/pages/creatorkit/portal-report.png)](/creatorkit/portal-posts) ### Use for FREE CreatorKit is a FREE customizable .NET App included with [ServiceStack](https://servicestack.net) which is [Free for Individuals and Open Source projects](https://servicestack.net/free) or for organizations that continue to host their forked CreatorKit projects on GitHub or GitLab. As a stand-alone hosted product there should be minimal need for any customizations with initial [Mailining Lists, Subscribers](/creatorkit/install#before-you-run), [App Settings](/creatorkit/install#whats-included) and branding information maintained in customizable [CSV](/creatorkit/install#before-you-run) and [text files](/creatorkit/customize). To get started follow the [installation instructions](/creatorkit/install) to download and configure it with your organization's website settings. ## Future As we're using CreatorKit ourselves to power all dynamic Mailing List and Comment System features on [https://servicestack.net](servicestack.net), we'll be continuing to develop it with useful features to empower static websites with more generic email templates and potential to expand it with commerce features, inc. Stripe integration, products & subscriptions, ordering system, invoicing, quotes, PDF generation, etc. Follow [@ServiceStack](https://twitter.com/ServiceStack), Watch or Star [NetCoreApps/CreatorKit](https://github.com/NetCoreApps/CreatorKit) or Join our CreatorKit-powered Monthly Newsletter to follow and keep up to date with new features:
As a design goal [CreatorKit's components](/creatorkit/components) will be easily embeddable into any external website, where it will be integrated into the [Razor SSG](/posts/razor-ssg) project template to serve as a working demonstration and reference implementation. As such it's a great option if you're looking to create a Fast, FREE, CDN hostable, [simple, modern](/posts/javascript) statically generated website created with Razor & Markdown like [ServiceStack/servicestack.net](https://github.com/ServiceStack/servicestack.net). ### Feedback welcome If you'd like to prioritize features you'd like to see first or propose new, generically useful features for static websites, please let us know in [servicestack.net/ideas](https://servicestack.net/ideas). # Install Source: https://razor-ssg.web-templates.io/creatorkit/install CreatorKit is a customizable .NET companion App that you would run alongside your Website which provides the backend for mailing list subscriptions, User repository and comment features which can be added to your website with CreatorKit's tailwind components which are loaded from and communicate back directly to your CreatorKit .NET App instance:
## Get CreatorKit To better be able to keep up-to-date with future CreatorKit improvements we recommend [forking CreatorKit](https://github.com/NetCoreApps/CreatorKit/fork) so you can easily apply future changes to your customized forks: Or if you're happy to take CreatorKit's current feature-set as it is, download the .zip to launch a local instance of CreatorKit: ## Extending CreatorKit To minimize disruption when upgrading to future versions of CreatorKit we recommend adding any new Services to [CreatorKit.Extensions](https://github.com/NetCoreApps/CreatorKit/tree/main/CreatorKit.Extensions) and their DTOs in [CreatorKit.Extensions.ServiceModel](https://github.com/NetCoreApps/CreatorKit/tree/main/CreatorKit.Extensions.ServiceModel): ```files /CreatorKit /CreatorKit.Extensions CustomEmailRunServices.cs CustomEmailServices.cs CustomRendererServices.cs /CreatorKit.Extensions.ServiceModel MarkdownEmail.cs NewsletterMailRun.cs RenderNewsletter.cs ``` These folders will be limited to optional extras which can added to or removed as needed where it will be isolated from the core set of functionality maintained in the other CreatorKit's folders. Any custom AppHost or IOC dependencies your Services require can be added to [Configure.Extensions.cs](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit/Configure.Extensions.cs). ### Before you Run We need to initialize CreatorKit's database which we can populate with our preferred App Users, Mailing Lists and Subscribers by modifying the CSV files in `/Migrations/seed`: ```files /Migrations /seed mailinglists.csv subscribers.csv users.csv Migration1000.cs Migration1001.cs ``` ## Mailing Lists You can define all Mailing Lists you wish to send and contacts can subscribe to in **mailinglists.csv**: #### mailinglists.csv ```csv Name,Description None,None TestGroup,Test Group MonthlyNewsletter,Monthly Newsletter BlogPostReleases,New Blog Posts VideoReleases,New Videos ProductReleases,New Product Releases YearlyUpdates,Yearly Updates ``` When the database is first created this list will be used to generate the [MailingList.cs](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit.ServiceModel/Types/MailingList.cs) Enum, e.g: ```csharp [Flags] public enum MailingList { None = 0, [Description("Test Group")] TestGroup = 1 << 0, //1 [Description("Monthly Newsletter")] MonthlyNewsletter = 1 << 1, //2 [Description("New Blog Posts")] BlogPostReleases = 1 << 2, //4 [Description("New Videos")] VideoReleases = 1 << 3, //8 [Description("New Product Releases")] ProductReleases = 1 << 4, //16 [Description("Yearly Updates")] YearlyUpdates = 1 << 5, //32 } ``` This is a `[Flags]` enum with each value increasing by a power of 2 allowing a single integer value to capture all the mailing lists contacts are subscribed to. #### subscribers.csv Add any mailing subscribers you wish to be included by default, it's a good idea to include all Website developer emails here so they can test sending emails to themselves: ```csv Email,FirstName,LastName,MailingLists test@subscriber.com,Test,Subscriber,3 ``` [Mailing Lists](creatorkit/customize#mailing-lists) is a flag enums where the integer values is a sub of all Mailing Lists you want them subscribed to, e.g. use `3` to subscribe to both the `TestGroup (1)` and `MonthlyNewsletter (2)` Mailing Lists. #### users.csv Add any App Users you want your CreatorKit App to include by default, at a minimum you'll need an `Admin` user which is required to access the Portal to be able to use CreatorKit: ```csv Id,Email,FirstName,LastName,Roles 1,admin@email.com,Admin,User,"[Admin]" 2,test@user.com,Test,User, ``` Once your happy with your seed data run the included [OrmLite DB Migrations](https://docs.servicestack.net/ormlite/db-migrations) with: Which will create the CreatorKit SQLite databases with your seed Users and Mailing List subscribers included. Should you need to recreate the database, you can delete the `App_Data/*.sqlite` databases then rerun `npm run migrate` to recreate the databases with your updated `*.csv` seed data. ### What's included The full .NET Source code is included with CreatorKit enabling unlimited customizations. It's a stand-alone download which doesn't require any external dependencies to run initially, although some features require configuration: #### SMTP Server You'll need to configure an SMTP Server to enable sending Emails by adding it to your **appsettings.json**, e.g: ```json { "smtp": { "UserName" : "SmtpUsername", "Password" : "SmtpPassword", "Host" : "smtp.example.org", "Port" : 587, "From" : "noreply@example.org", "FromName" : "My Organization", "Bcc": "optional.backup@example.org" } } ``` If you don't have an existing SMTP Server we recommend using [Amazon SES](https://aws.amazon.com/ses/) as a cost effective way to avoid managing your own SMTP Servers. #### OAuth Providers By default CreatorKit is configured to allow Sign In's for authenticated post comments from Facebook, Google, Microsoft OAuth Providers during development on its `https://localhost:5002`. You'll need to configure OAuth Apps for your production host in order to support OAuth Sign Ins at deployment: - Create App for Facebook at https://developers.facebook.com/apps - Create App for Google at https://console.developers.google.com/apis/credentials - Create App for Microsoft at https://apps.dev.microsoft.com You can Add/Remove to this from the list of [supported OAuth Providers](https://docs.servicestack.net/auth#oauth-providers). ### RDBMS CreatorKit by default is configured to use an embedded SQLite database which can be optionally configured to replicate backups to AWS S3 or Cloudflare R2 using [Litestream](https://docs.servicestack.net/ormlite/litestream). This is setup to be used with Cloudflare R2 by default which can be configured from the [.deploy/litestream-template.yml](https://github.com/NetCoreApps/CreatorKit/blob/main/.deploy/litestream-template.yml) file: ```yml access-key-id: ${R2_ACCESS_KEY_ID} secret-access-key: ${R2_SECRET_ACCESS_KEY} dbs: - path: /data/db.sqlite replicas: - type: s3 bucket: ${R2_BUCKET} path: db.sqlite region: auto endpoint: ${R2_ENDPOINT} ``` By adding the matching GitHub Action Secrets to your repository, this file will be populated and deployed to your own Linux server via SSH. This provides a realtime backup to your R2 bucket for minimal cost, enabling point in time recovery of data if you run into issues. Alternatively [Configure.Db.cs](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit/Configure.Db.cs) can be changed to use preferred [RDBMS supported by OrmLite](https://docs.servicestack.net/ormlite/installation). ### App Settings The **PublicBaseUrl** and **BaseUrl** properties in `appsettings.json` should be updated with the URL where your CreatorKit instance is deployed to and replace **WebsiteBaseUrl** with the website you want to use CreatorKit emails to be addressed from: ```json { "AppData": { "PublicBaseUrl": "https://creatorkit.netcore.io", "BaseUrl": "https://creatorkit.netcore.io", "WebsiteBaseUrl": "https://razor-ssg.web-templates.io" } } ``` ### CORS Any additional Website URLs that utilize CreatorKit's components should be included in the CORS **allowOriginWhitelist** to allow CORS requests from that website: ```json { "CorsFeature": { "allowOriginWhitelist": [ "http://localhost:5000", "http://localhost:8080" ] } } ``` ### Customize After configuring CreatorKit to run with your preferred Environment, you'll want to customize it to your Organization or Personal Brand: # Customize Source: https://razor-ssg.web-templates.io/creatorkit/customize The `/emails` folder contains all email templates and layouts made available to CreatorKit: ```files /emails /layouts basic.html empty.html marketing.html /partials button-centered.html divider.html image-centered.html section.html title.html /vars info.txt urls.txt empty.html newsletter-welcome.html newsletter.html verify-email.html ``` Which uses the [#Script](https://sharpscript.net) .NET Templating language to render Emails from Templates, where: - `/layouts` contains different kinds of email layouts - `/partials` contains all reusable [Partials](https://sharpscript.net/docs/partials) made available to your templates The remaining `*.html` contains different type of emails you want to send, e.g. **empty.html** is a blank template you can use to send custom Markdown email content with the your preferred email layout. ## Template Variables All Branding Information referenced in the templates are maintained in the `/vars` folder: ```files /vars info.txt urls.txt ``` At a minimum you'll want to replace all **info.txt** variables from ServiceStack's with your Organization's information: #### info.txt ```txt Company ServiceStack CompanyOfficial ServiceStack, Inc. Domain servicestack.net MailingAddress 470 Schooleys Mt Road #636, Hackettstown, NJ 07840-4096 MailPreferences Mail Preferences Unsubscribe Unsubscribe Contact Contact Privacy Privacy policy OurAddress Our mailing address: MailReason You received this email because you are subscribed to ServiceStack news and announcements. SignOffTeam The ServiceStack Team NewsletterFmt ServiceStack Newsletter, {0} SocialUrls Website,Twitter,YouTube SocialImages website_24x24,twitter_24x24,youtube_24x24 ``` Variables inside your email templates can be referenced using handlebars syntax, e.g: `{{info.Company}}` The **urls.txt** contains all URLs embedded in emails that you'll want to replace with URLs on your website, with `/mail-preferences` and `/signup-confirmed` being integration pages covered in [Integrations](./integrations). #### urls.txt ```txt BaseUrl {{BaseUrl}} PublicBaseUrl {{PublicBaseUrl}} WebsiteBaseUrl {{WebsiteBaseUrl}} Website {{WebsiteBaseUrl}} MailPreferences {{WebsiteBaseUrl}}/mail-preferences Unsubscribe {{WebsiteBaseUrl}}/mail-preferences Privacy {{WebsiteBaseUrl}}/privacy Contact {{WebsiteBaseUrl}}/#contact SignupConfirmed {{WebsiteBaseUrl}}/signup-confirmed Twitter https://twitter.com/ServiceStack YouTube https://www.youtube.com/channel/UC0kXKGVU4NHcwNdDdRiAJSA ``` - **BaseUrl** - Base URL of your Website that uses CreatorKit - **AppBaseUrl** - Base URL of the current CreatorKit instance - **PublicAppBaseUrl** - Base URL of a public CreatorKit instance The **PublicAppBaseUrl** is used to reference public images hosted on your deployed CreatorKit instance since most email clients wont render images hosted on `https://localhost`. ### Usage You're free to add to these existing collections or create new variable collections which are accessible from `{{info.*}}` and `{{urls.*}}` in your templates that's also available via dropdown in the Markdown Editor Variables dropdown: ![](/img/pages/creatorkit/markdown-vars.png) In addition, a `{{images.*}}` variable collection is also populated from all images in the `/img/mail` folder, e.g: ```files /img /mail blog_48x48@2x.png chat_48x48@2x.png email_100x100@2x.png logo_72x72@2x.png logofull_350x60@2x.png mail_48x48@2x.png speaker_48x48@2x.png twitter_24x24@2x.png video_48x48@2x.png website_24x24@2x.png welcome_650x487.jpg youtube_24x24@2x.png youtube_48x48@2x.png ``` That's prefixed with the `{{PublicAppBaseUrl}}` allowing them to be referenced directly in your `*.html` Email templates. e.g: ```html ``` Or from your Markdown Emails using Markdown Image syntax: ```markdown ![]({{images.welcome_650x487.jpg}}) ``` # Components Source: https://razor-ssg.web-templates.io/creatorkit/components After launching your customized CreatorKit instance, you can start integrating its features into your existing websites, or if you're also in need of a fast, beautiful website we highly recommend the [Razor SSG](https://razor-ssg.web-templates.io/posts/razor-ssg) template which is already configured to include CreatorKit's components. The components are included using a declarative progressive markup so that it doesn't affect the behavior of the website if the CreatorKit is down or unresponsive. ## Enabling CreatorKit Components To utilize CreatorKit's Components in your website you'll need to initialize the components you want to use by embedding this script at the bottom of your page, e.g. in [Footer.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Shared/Footer.cshtml): ```html ``` Where `mail()` will scan the document for declarative `data-mail` for any Mailing List components to create, likewise `post()` does the same for any Thread/Post components. The `@components` URL lets you load Components from your `localhost:5001` instance during development and your public CreatorKit instance in production which you'll need to replace `creatorkit.netcore.io` to use. ## Post Voting and Comments You can enable voting for individual posts or pages with and Thread comments by including the `PostComments` component with: ```html
``` Which when loaded will render a thread like icon where users can up vote posts or pages and either Sign In/Sign Up buttons for unauthenticated users or a comment box for Signed in Users:
#### PostComments Properties The available PostComments properties for customizing its behavior include: ```ts defineProps<{ hide?: "threadLikes"|"threadLikes"[] commentLink?: { href: string, label: string } }>() ``` ### Component Properties Any component properties can be either declared inline using `data-props`, e.g: ```html
```
Where it will hide the Thread Like icon and include a link to your `/community-rules` page inside each comment box. Alternatively properties can instead be populated in the `mail()` and `post()` initialize functions: ```html ``` ## Mailing List Components ### JoinMailingList The `JoinMailingList` component can be added anywhere you want to accept Mailing List subscriptions on your website, e.g: ```html
```
Which you can style as needed as this template wraps in a [Newsletter.cshtml](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Shared/Newsletter.cshtml) Tailwind component that's displayed on the [Home Page](/). #### JoinMailingList Properties Which allows for the following customizations: ```ts defineProps<{ //= MonthlyNewsletter mailingLists?: "TestGroup" | "MonthlyNewsletter" | "BlogPostReleases" | "VideoReleases" | "ProductReleases" | "YearlyUpdates" placeholder?: string //= Enter your email submitLabel?: string //= Subscribe thanksHeading?: string //= Thanks for signing up! thanksMessage?: string //= To complete sign up, look for the verification... thanksIcon?: { svg?:string, uri?:string, alt?:string, cls?:string } }> ``` ### MailPreferences The `MailPreferences` component manages a users Mailing List subscriptions which you can be linked in your Email footers for users wishing to manage or unsubscribe from mailing list emails. It can be include in any HTML or Markdown page as Razor SSG does in its [mail-preferences.md](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_pages/mail-preferences.md): ```html
``` Where if it's unable to locate the user will ask the user for their email:
Alternatively the page can jump directly to a contacts Mailing Lists by including a `?ref` query string parameter of the Contact's External Ref, e.g: `/mail-preferences?ref={{ExternalRef}}` You can also add `&unsubscribe=1` to optimize the page for users wishing to Unsubscribe where it will also display an **Unsubscribe** button to subscribe to all mailing lists. #### MailPreferences Properties Most of the copy used in the `MailPreferences` component can be overridden with: ```ts defineProps<{ emailPrompt?: string //= Enter your email to manage your email... submitEmailLabel?: string //= Submit updatedHeading?: string //= Updated! updatedMessage?: string //= Your email preferences have been saved. unsubscribePrompt?: string //= Unsubscribe from all future email... unsubscribeHeading?: string //= Updated! unsubscribeMessage?: string //= You've been unsubscribed from all email... submitLabel?: string //= Save Changes submitUnsubscribeLabel?: string //= Unsubscribe }>() ``` ## Tailwind Styles CreatorKit's components are styled with tailwind classes which will also need to be included in your website. For Tailwind projects we recommend copying a concatenation of all Components from [/CreatorKit/wwwroot/tailwind/all.components.txt](https://raw.githubusercontent.com/NetCoreApps/CreatorKit/main/CreatorKit/wwwroot/tailwind/all.components.txt) and include it in your project where the tailwind CLI can find it so any classes used are included in your App's Tailwind **.css** bundle. In Razor SSG projects this is already being copied in its [postinstall.js](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/postinstall.js) If you're not using Tailwind, websites will need to reference your CreatorKit's instance Tailwind .css bundle instead, e.g: ```html ``` # Integrations Source: https://razor-ssg.web-templates.io/creatorkit/integrations We recommend your website have pages for the following `info.txt` collection variables: ```txt MailPreferences {{WebsiteBaseUrl}}/mail-preferences Unsubscribe {{WebsiteBaseUrl}}/mail-preferences Privacy {{WebsiteBaseUrl}}/privacy Contact {{WebsiteBaseUrl}}/#contact SignupConfirmed {{WebsiteBaseUrl}}/signup-confirmed ``` You're also free to change the URLs in `info.txt` to reference existing pages on your website where they exist. The `info.SignupConfirmed` URL is redirected to after a contact verifies their email address. ## Example For reference here are example pages Razor SSG uses for this URLs: | Page | Source Code | |---------------------------------------|-------------------------------------------------------------------------------------------------------------------------| | [/signup-confirmed](signup-confirmed) | [/_pages/signup-confirmed.md](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_pages/signup-confirmed.md) | | [/mail-preferences](mail-preferences) | [/_pages/mail-preferences.md](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_pages/mail-preferences.md) | | [/privacy](privacy) | [/_pages/privacy.md](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_pages/privacy.md) | | [/community-rules](community-rules) | [/_pages/community-rules.md](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/_pages/community-rules.md) | # Overview Source: https://razor-ssg.web-templates.io/creatorkit/portal-overview All information captured by CreatorKit's components can be managed from your CreatorKit's instance portal at:

https://localhost:5003/portal/

Signing in with an Admin User will take you to the dashboard showing your Website activity: ![](/img/pages/creatorkit/portal.png) ## Mailing List Admin The first menu section is for managing your contact mailing lists including creating and sending emails and email campaigns to mailing list contacts, ### Contacts Mailing List Contacts can either be added via the [JoinMailingList](creatorkit/components#joinmailinglist) component on your website or using the Contacts Admin UI: ![](/img/pages/creatorkit/portal-contacts.png) ### Archive When you want to clear your workspace of sent emails you can archive them which moves them to a separate Database ensuring the current working database is always snappy and clear of clutter. ![](/img/pages/creatorkit/portal-archive.png) ## Posts Admin The **Manage Posts** section is for managing and moderating your website's post comments with most menu items manages data in different Tables using [AutoQueryGrid](https://docs.servicestack.net/vue/autoquerygrid) and custom [AutoForm](https://docs.servicestack.net/vue/autoform) components. # Messages Source: https://razor-ssg.web-templates.io/creatorkit/portal-messages ### Sending Single plain-text Emails **Messages** lets you craft and send emails to a single contact which can be sent immediately or saved as a draft so you can review the HTML rendered email and send later. ![](/img/pages/creatorkit/portal-messages.png) It also lists all available emails that can be sent which are any APIs that inherit the `CreateEmailBase` base class which contains the minimum contact fields required in each email: ```csharp public abstract class CreateEmailBase { [ValidateNotEmpty] [Input(Type="EmailInput")] public string Email { get; set; } [ValidateNotEmpty] [FieldCss(Field = "col-span-6 lg:col-span-3")] public string FirstName { get; set; } [ValidateNotEmpty] [FieldCss(Field = "col-span-6 lg:col-span-3")] public string LastName { get; set; } } ``` Plain text emails can be sent with the `SimpleTextEmail` API: ```csharp [Renderer(typeof(RenderSimpleText))] [Tag(Tag.Mail), ValidateIsAdmin] [Description("Simple Text Email")] public class SimpleTextEmail : CreateEmailBase, IPost, IReturn { [ValidateNotEmpty] [FieldCss(Field = "col-span-12")] public string Subject { get; set; } [ValidateNotEmpty] [Input(Type = "textarea"), FieldCss(Field = "col-span-12", Input = "h-36")] public string Body { get; set; } public bool? Draft { get; set; } } ``` ### Email UI Which are rendered using the [Vue AutoForm component](https://docs.servicestack.net/vue/autoform) from the API definition where the `SimpleTextEmail` Request DTO renders the new Email UI: ![](/img/pages/creatorkit/portal-messages-simple.png) Which uses the custom `EmailInput` component to search for contacts and populates their Email, First and Last name fields. The implementation for sending single emails are defined in [EmailServices.cs](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit.ServiceInterface/EmailServices.cs) which uses [EmailRenderer.cs](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit.ServiceInterface/EmailRenderer.cs) to save and send non draft emails which follow the pattern below: ```csharp public EmailRenderer Renderer { get; set; } public async Task Any(SimpleTextEmail request) { var contact = await Db.GetOrCreateContact(request); var viewRequest = request.ConvertTo().FromContact(contact); var bodyText = (string) await Gateway.SendAsync(typeof(string), viewRequest); var email = await Renderer.CreateMessageAsync(Db, new MailMessage { Draft = request.Draft ?? false, Message = new EmailMessage { To = contact.ToMailTos(), Subject = request.Subject, Body = request.Body, BodyText = bodyText, }, }.FromRequest(request)); return email; } ``` Live previews are generated and Emails rendered with renderer APIs that inherit `RenderEmailBase` e.g: ```csharp [Tag(Tag.Mail), ValidateIsAdmin, ExcludeMetadata] public class RenderSimpleText : RenderEmailBase, IGet, IReturn { public string Body { get; set; } } ``` Which renders the Request DTO inside a [#Script](https://sharpscript.net) email context: ```csharp public async Task Any(RenderSimpleText request) { var ctx = Renderer.CreateScriptContext(); return await ctx.RenderScriptAsync(request.Body,request.ToObjectDictionary()); } ``` ### Sending Custom HTML Emails `CustomHtmlEmail` is a configurable API for sending HTML emails utilizing custom Email Layout and Templates from populated dropdowns configured with available Templates in `/emails`: ```csharp [Renderer(typeof(RenderCustomHtml))] [Tag(Tag.Mail), ValidateIsAdmin] [Icon(Svg = Icons.RichHtml)] [Description("Custom HTML Email")] public class CustomHtmlEmail : CreateEmailBase, IPost, IReturn { [ValidateNotEmpty] [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailLayoutOptions")] public string Layout { get; set; } [ValidateNotEmpty] [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailTemplateOptions")] public string Template { get; set; } [ValidateNotEmpty] [FieldCss(Field = "col-span-12")] public string Subject { get; set; } [Input(Type = "MarkdownEmailInput", Label = ""), FieldCss(Field = "col-span-12", Input = "h-56")] public string? Body { get; set; } public bool? Draft { get; set; } } ``` ![](/img/pages/creatorkit/portal-messages-custom.png) #### Custom HTML Implementation It follows the same pattern as other email implementations where it uses the `EmailRenderer` to create and send emails: ```csharp public async Task Any(CustomHtmlEmail request) { var contact = await Db.GetOrCreateContact(request); var viewRequest = request.ConvertTo().FromContact(contact); var bodyHtml = (string) await Gateway.SendAsync(typeof(string), viewRequest); var email = await Renderer.CreateMessageAsync(Db, new MailMessage { Draft = request.Draft ?? false, Message = new EmailMessage { To = contact.ToMailTos(), Subject = request.Subject, Body = request.Body, BodyHtml = bodyHtml, }, }.FromRequest(viewRequest)); return email; } ``` Which uses the `RenderCustomHtml` to render the HTML and Live Previews which executes the populated Request DTO with the Email **#Script** context configured to use the selected Email Layout and Template: ```csharp public async Task Any(RenderCustomHtml request) { var context = Renderer.CreateMailContext(layout:request.Layout, page:request.Template); var evalBody = !string.IsNullOrEmpty(request.Body) ? await context.RenderScriptAsync(request.Body, request.ToObjectDictionary()) : string.Empty; return await Renderer.RenderToHtmlResultAsync(Db, context, request, args:new() { ["body"] = evalBody, }); } ``` ## CreatorKit.Extensions Any additional services should be maintained in [CreatorKit.Extensions](https://github.com/NetCoreApps/CreatorKit/tree/main/CreatorKit.Extensions) project with any custom email implementations added to [CustomEmailServices.cs](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit.Extensions/CustomEmailServices.cs). ### Sending HTML Markdown Emails [MarkdownEmail.cs](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit.Extensions.ServiceModel/MarkdownEmail.cs) is an example of a more user-friendly custom HTML Email you may want to send, which is pre-configured to use the [basic.html](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit/emails/layouts/basic.html) Layout and the [empty.html](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit/emails/empty.html) Email Template to allow sending plain HTML Emails with a custom Markdown Email body: ```csharp [Renderer(typeof(RenderCustomHtml), Layout = "basic", Template="empty")] [Tag(Tag.Mail), ValidateIsAdmin] [Icon(Svg = Icons.TextMarkup)] [Description("Markdown Email")] public class MarkdownEmail : CreateEmailBase, IPost, IReturn { [ValidateNotEmpty] [FieldCss(Field = "col-span-12")] public string Subject { get; set; } [ValidateNotEmpty] [Input(Type="MarkdownEmailInput",Label=""), FieldCss(Field="col-span-12",Input="h-56")] public string? Body { get; set; } public bool? Draft { get; set; } } ``` As defined, this DTO renders the form utilizing a custom `MarkdownEmailInput` rich text editor which provides an optimal UX for authoring Markdown content with icons to assist with discovery of Markdown's different formatting syntax. #### Template Variables The editor also includes a dropdown to provide convenient access to your [Template Variables](creatorkit/customize#template-variables): ![](/img/pages/creatorkit/portal-messages-markdown.png) The implementation of `MarkdownEmail` just sends a Custom HTML Email configured to use the **basic** Layout with the **empty** Email Template: ```csharp public async Task Any(MarkdownEmail request) { var contact = await Db.GetOrCreateContact(request); var viewRequest = request.ConvertTo().FromContact(contact); viewRequest.Layout = "basic"; viewRequest.Template = "empty"; var bodyHtml = (string) await Gateway.SendAsync(typeof(string), viewRequest); var email = await Renderer.CreateMessageAsync(Db, new MailMessage { Draft = request.Draft ?? false, Message = new EmailMessage { To = contact.ToMailTos(), Subject = request.Subject, Body = request.Body, BodyHtml = bodyHtml, }, }.FromRequest(viewRequest)); return email; } ``` # Mail Runs Source: https://razor-ssg.web-templates.io/creatorkit/portal-mailruns Mail Runs is where you would go to craft and send emails to an entire Mailing List group. It has the same Simple, Markdown and Custom HTML Email UIs as [Messages](creatorkit/portal-messages) except instead of a single contact, it will generate and send individual emails to every contact in the specified **Mailing List**: ![](/img/pages/creatorkit/portal-mailrun-custom.png) You'll also be able to send personalized emails with the contact's `{{Email}}`, `{{FirstName}}` and `{{LastName}}` template variables. ### MailRun Implementation All Mail Run APIs inherit `MailRunBase` which contains the Mailing List that the Mail Run should send emails to: ```csharp public abstract class MailRunBase { [ValidateNotEmpty] public MailingList MailingList { get; set; } } ``` It has the equivalent Standard, Markdown and Custom HTML Emails that messages has, which instead inherits from `MailRunBase`, e.g. here's the Request DTO definition that's used to render the above **Custom HTML Email** UI: ```csharp [Renderer(typeof(RenderCustomHtml))] [Tag(Tag.Mail), ValidateIsAdmin] [Icon(Svg = Icons.RichHtml)] [Description("Custom HTML Email")] public class CustomHtmlMailRun : MailRunBase, IPost, IReturn { [ValidateNotEmpty] [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailLayoutOptions")] public string Layout { get; set; } [ValidateNotEmpty] [Input(Type = "combobox", EvalAllowableValues = "AppData.EmailTemplateOptions")] public string Template { get; set; } [ValidateNotEmpty] public string Subject { get; set; } [ValidateNotEmpty] [Input(Type = "MarkdownEmailInput", Label = ""), FieldCss(Field = "col-span-12", Input = "h-56")] public string? Body { get; set; } } ``` It's implementation differs slightly from the Messages [Custom HTML Implementation](creatorkit/portal-messages#custom-html-implementation) as an email needs to be generated and sent per contact and are instead generated and saved to the `MailMessageRun` table: ```csharp public async Task Any(CustomHtmlMailRun request) { var response = CreateMailRunResponse(); var mailRun = await Renderer.CreateMailRunAsync(Db, new MailRun { Layout = request.Layout, Template = request.Template, }, request); foreach (var sub in await Db.GetActiveSubscribersAsync(request.MailingList)) { var viewRequest = request.ConvertTo().FromContact(sub); var bodyHtml = (string) await Gateway.SendAsync(typeof(string), viewRequest); response.AddMessage(await Renderer.CreateMessageRunAsync(Db, new MailMessageRun { Message = new EmailMessage { To = sub.ToMailTos(), Subject = request.Subject, Body = request.Body, BodyHtml = bodyHtml, } }.FromRequest(viewRequest), mailRun, sub)); } await Db.CompletedMailRunAsync(mailRun, response); return response; } ``` ### Verifying Mail Run Messages Creating a Mail Run generates messages for each Contact in the Mailing List, but doesn't send them immediately, it instead opens the saved Mail Run so you have an opportunity to inspect the generated messages to decide whether you want to send or delete the messages. ![](/img/pages/creatorkit/portal-mailrun-newsletter-send.png) Click **View Messages** to inspect a sample of the generated messages from the saved Mail Run then either **Send Messages** if you want to send them out or **Delete** to delete the Mail Run and start again. Whilst the Mail Run Messages are being sent out you can click Refresh to monitor progress. ## CreatorKit.Extensions Any additional services should be maintained in [CreatorKit.Extensions](https://github.com/NetCoreApps/CreatorKit/tree/main/CreatorKit.Extensions) project with any custom Mail Run implementations added to [CustomEmailRunServices.cs](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit.Extensions/CustomEmailRunServices.cs). ## Generating Newsletters The `NewsletterMailRun` API is an advanced Email Generation example for generating a Monthly Newsletter - that it automatically generates from new content added to [Razor SSG](https://razor-ssg.web-templates.io/posts/razor-ssg) Websites that it discovers from its pre-rendered API JSON metadata. Even if you're not using Razor SSG website it should still serve as a good example for how to implement a Mail Run for a custom mail campaign utilizing custom data sources. The `NewsletterMailRun` API has 2 optional properties for the Year and Month you want to generate the Newsletter for: ```csharp [Renderer(typeof(RenderNewsletter))] [Tag(Tag.Emails)] [ValidateIsAdmin] [Description("Generate Newsletter")] [Icon(Svg = Icons.Newsletter)] public class NewsletterMailRun : MailRunBase, IPost, IReturn { public int? Month { get; set; } public int? Year { get; set; } } ``` Which renders the **Generate Newsletter** UI: ![](/img/pages/creatorkit/portal-mailrun-newsletter.png) The implementation follows the standard Mail Run implementation, using the `EmailRenderer` to creating a `MailMessageRun` for every contact in the mailing list. We can also see it will default to the current Month/Year if not provided and that it uses the [marketing.html](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit/emails/layouts/marketing.html) Layout and the [newsletter.html](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit/emails/newsletter.html) Email template: ```csharp public async Task Any(NewsletterMailRun request) { var response = CreateMailRunResponse(); request.Year ??= DateTime.UtcNow.Year; request.Month ??= DateTime.UtcNow.Month; var viewRequest = request.ConvertTo(); var fromDate = new DateTime(request.Year.Value, request.Month.Value, 1); var bodyHtml = (string) await Gateway.SendAsync(typeof(string), viewRequest); var mailRun = await Renderer.CreateMailRunAsync(Db, new MailRun { Layout = "marketing", Template = "newsletter", }, request); foreach (var sub in await Db.GetActiveSubscribersAsync(request.MailingList)) { response.AddMessage(await Renderer.CreateMessageRunAsync(Db, new MailMessageRun { Message = new EmailMessage { To = sub.ToMailTos(), Subject = string.Format(AppData.Info.NewsletterFmt, $"{fromDate:MMMM} {fromDate:yyyy}"), BodyHtml = bodyHtml, } }.FromRequest(viewRequest), mailRun, sub)); } await Db.CompletedMailRunAsync(mailRun, response); return response; } ``` The **newsletter.html** Email Template uses the #Script templating language to render the different Newsletter sections, e.g: ```html {{#if meta.Posts.Count > 0 }} {{ 'divider' |> partial }} {{ 'section' |> partial({ iconSrc:images.blog_48x48, title:'New Posts' }) }} {{#each meta.Posts }}

{{ it.title }} →

{{ it.summary }}

{{/each}} {{/if}} ``` Which uses the `RenderNewsletter` API to render the Newsletter emails and live previews which in addition to the App's template variables adds a `meta` property containing the Data Source for the contents in **newsletter.html**: ```csharp public async Task Any(RenderNewsletter request) { var year = request.Year ?? DateTime.UtcNow.Year; var fromDate = new DateTime(year, request.Month ?? 1, 1); var meta = await MailData.SearchAsync(fromDate: fromDate, toDate: request.Month != null ? new DateTime(year, request.Month.Value, 1).AddMonths(1) : null); var context = Renderer.CreateMailContext(layout:"marketing", page:"newsletter", args:new() { ["meta"] = meta }); return await Renderer.RenderToHtmlResultAsync(Db, context, request, args: new() { ["title"] = $"{fromDate:MMMM} {fromDate:yyyy}" }); } ``` The implementation of [MailData](https://github.com/NetCoreApps/CreatorKit/blob/main/CreatorKit.ServiceInterface/MailData.cs) gets its data from [/meta/2023/all.json](https://razor-ssg.web-templates.io/meta/2023/all.json) which is prerendered with all the new website content added in **2023** which is filtered further to only include content published within the selected date range: ```csharp public class MailData { public DateTime LastUpdated { get; set; } public AppData AppData { get; } public MailData(AppData appData) { AppData = appData; } public TimeSpan CacheDuration { get; set; } = TimeSpan.FromMinutes(10); public ConcurrentDictionary MetaCache { get; } = new(); public async Task SearchAsync(DateTime? fromDate = null, DateTime? toDate = null) { var year = fromDate?.Year ?? DateTime.UtcNow.Year; var metaCache = MetaCache.TryGetValue(year, out var siteMeta) && siteMeta.CreatedDate < DateTime.UtcNow.Add(CacheDuration) ? siteMeta : null; if (metaCache == null) { var metaJson = await AppData.BaseUrl.CombineWith($"/meta/{year}/all.json") .GetJsonFromUrlAsync(); metaCache = metaJson.FromJson(); metaCache.CreatedDate = DateTime.UtcNow; MetaCache[year] = metaCache; } var results = new SiteMeta { CreatedDate = metaCache.CreatedDate, Pages = WithinRange(metaCache.Pages, fromDate, toDate).ToList(), Posts = WithinRange(metaCache.Posts, fromDate, toDate).ToList(), WhatsNew = WithinRange(metaCache.WhatsNew, fromDate, toDate).ToList(), Videos = WithinRange(metaCache.Videos, fromDate, toDate).ToList(), }; return results; } private static IEnumerable WithinRange( IEnumerable docs, DateTime? fromDate, DateTime? toDate) { if (fromDate != null) docs = docs.Where(x => x.Date >= fromDate); if (toDate != null) docs = docs.Where(x => x.Date < toDate); return docs; } } ``` The results of the external API Request are also cached for a short duration to speed up Live Previews when crafting emails. # Posts Source: https://razor-ssg.web-templates.io/creatorkit/portal-posts The **Manage Posts** section provides editable [AutoQuery Grid components](https://docs.servicestack.net/vue/autoquerygrid) to manage all the RDBMS tables used to implement CreatorKit's comment system like **Threads** which manages the `Thread` table which supports adding Thread comments to every unique URL: ![](/img/pages/creatorkit/portal-threads.png) ## Moderation Most of the time will be spent either reading through and deleting bad comments and responding to reported comments which includes special behavior to filter reports to only show reports that have yet to be moderated. The **Update Comment Report** includes different moderation options you can choose to perform based on the severity of a comment ranging from flagging a comment which marks the comment as flagged and hides the comment content, deleting the comment which also deletes any replies, temporarily banning the user for a day, a week, a month to permanently banning the user until they're explicitly unbanned: ![](/img/pages/creatorkit/portal-report.png) Thread Users are managed with the [User Admin Feature](https://docs.servicestack.net/admin-ui-users) UI who can be banned for any duration up to **Ban Until Date** or permanently banned by Locking the User Account: ![](/img/pages/creatorkit/admin-users.png)