# What is Razor Press? Source: https://razor-press.web-templates.io/what-is-razor-press Razor Press is a Razor Pages powered Markdown alternative to Ruby's Jekyll, Vue & VitePress that's ideal for generating fast, static content-centric & documentation websites. Inspired by [VitePress](https://vitepress.dev), it's designed to effortlessly create documentation around content written in Markdown, rendered using C# Razor Pages and beautifully styled with [tailwindcss](https://tailwindcss.com) and [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin). The resulting statically generated HTML pages can easily be deployed anywhere, where it can be hosted by any HTTP Server or CDN. By default it includes GitHub Actions to deploy it your GitHub Repo's **gh-pages** branch where it's hosted for FREE on [GitHub Pages](https://pages.github.com) CDN which can be easily configured to use your [Custom Domain](https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site). ## Use Cases Razor Press utilizes the same technology as [Razor SSG](https://razor-ssg.web-templates.io/posts/razor-ssg) which is the template we recommend for developing any statically generated sites with Razor like Blogs, Portfolios, and Marketing Sites as it includes more Razor & Markdown features like blogs and integration with [Creator Kit](https://servicestack.net/creatorkit/) - a companion OSS project offers the necessary tools any static website can use to reach and retain users, from managing subscriber mailing lists to moderating a feature-rich comments system. Some examples built with Razor SSG include:
servicestack.net
diffusion.works
jamstacks.net
xkcd.netcore.io
## Documentation Razor Press is instead optimized for creating documentation and content-centric websites, with built-in features useful for documentation websites including: - Customizable Sidebar Menus - Document Maps - Document Page Navigation - Autolink Headers #### Markdown Extensions - Markdown Content Includes - Tip, Info, Warning, Danger sections - Copy and Shell command widgets But given **Razor Press** and **Razor SSG** share the same implementation, their features are easily transferable, e.g. The [What's New](/whatsnew) and [Videos](/videos) sections are [features copied](https://razor-ssg.web-templates.io/posts/razor-ssg#whats-new-feature) from Razor SSG as they can be useful in Documentation websites. ## Customizable {#custom-anchor .custom} The source code of all Markdown and Razor Pages features are included in the template with all Markdown extensions implemented in the [Markdown*.cs](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp) files allowing for easier inspection, debugging and customization. To simplify updating Markdown features in future we recommend against modifying the included `Markdown.*` files and instead add any Markdig pipeline extensions or custom containers using `MarkdigConfig` in `Configure.Ssg.cs`: ```csharp MarkdigConfig.Set(new MarkdigConfig { ConfigurePipeline = pipeline => { // Extend Markdig Pipeline }, ConfigureContainers = config => { config.AddBuiltInContainers(); // Add Custom Block or Inline containers } }); ``` ### Update Markdown Extensions & Dependencies Updating to the latest JavaScript dependencies and Markdown extensions can be done by running: :::sh npm install ::: Which as the template has no npm dependencies, is just an alias for running `node postinstall.js` ## Example The largest website generated with Razor Press is currently the ServiceStack's documentation at [docs.servicestack.net](https://docs.servicestack.net):
docs.servicestack.net
docs.servicestack.net
A **500+** pages documentation website ported from VitePress, which prompted the creation of Razor Press after experiencing issues with VitePress's SSR/SPA model whose workaround became too time consuming to maintain. The new Razor SSG implementation now benefits from Razor Pages flexible layouts and partials where pages can be optionally implemented in just markdown, Razor or a hybrid mix of both. The [Vue](/vue/) splash page is an example of this implemented in a custom [/Vue/Index.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/Vue/Index.cshtml) Razor Page.
docs.servicestack.net
## Feedback & Feature Requests Welcome Up to this stage [docs.servicestack.net](https://docs.servicestack.net) has been the primary driver for Razor Press current feature-set, re-implementing all the previous VitePress features it used with C#, Razor Pages and Markdig extensions. In future we'll look at expanding this template with generic Markdown features suitable for documentation or content-centric websites, for which we welcome any feedback or new feature requests at:

https://servicestack.net/ideas

# Structure Source: https://razor-press.web-templates.io/structure ### 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-press/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: ```files /_pages what-is-razor-press.md structure.md privacy.md ``` Are made available from: - [/what-is-razor-press](/what-is-razor-press) - [/structure](/structure) - [/privacy](/privacy) This is primarily where most Markdown documentation will be maintained. ### Document Collections Folders can be used to maintain different document collections as seen in [/vue/](/vue/) and [/creatorkit/](/creatorkit/) folders: ```files /_pages /creatorkit about.md components.md customize.md /vue alerts.md autocomplete.md autoform.md ``` Each documentation collection needs a Razor Page to render each page in that collection, which can be configured independently and include additional features when needed, examples of this include: - [/Vue/Page.cshtml](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/Pages/Vue/Page.cshtml) - [/CreatorKit/Page.cshtml](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/Pages/CreatorKit/Page.cshtml) They can contain custom Razor Pages as needed, e.g. both [/vue/](/and/) and [/creatorkit/](/creatorkit/) have custom index pages: - [/Vue/Index.cshtml](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/Pages/Vue/Index.cshtml) - [/CreatorKit/Index.cshtml](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/Pages/CreatorKit/Index.cshtml) If no custom home page is needed, a `/{slug?}` or `/{**slug}` wildcard route can be used to handle a collection's index and content pages, e.g: - [/AutoQuery.cshtml](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/Pages/AutoQuery.cshtml) - [/OrmLite.cshtml](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/Pages/OrmLite.cshtml) - [/Redis.cshtml](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/Pages/Redis.cshtml) Which are used to render all pages in each documentation collection: - [docs.servicestack.net/autoquery/](https://docs.servicestack.net/autoquery/) - [docs.servicestack.net/ormlite/](https://docs.servicestack.net/ormlite/) - [docs.servicestack.net/redis/](https://docs.servicestack.net/redis/) ::: tip See [Sidebars](/sidebars) for how to configure different Sidebar menus for each collection ::: ### Loading Markdown Pages The code that loads the Pages feature markdown content is in [Markdown.Pages.cs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Markdown.Pages.cs), which ultimately just loads Markdown files using the configured [Markdig](https://github.com/xoofx/markdig) pipeline that is made available via its `VisiblePages` property which returns all documents **during development** but hides any **Draft** or content published at a **Future Date** from **production builds**. ## What's New Feature The [/whatsnew](/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: ```files /_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-press/blob/main/MyApp/Markdown.WhatsNew.cs) and rendered in: - [WhatsNew.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/WhatsNew.cshtml) ## Markdown Videos Feature Videos is another Markdown powered feature for display collections of YouTube videos populated from a Directory of Markdown Video pages in [/_videos](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/_videos): ```files /_videos /projects video1.md video2.md /vue video1.md video2.md ``` Loaded with: - [Markdown.Videos.cs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Markdown.Videos.cs) and Rendered with Razor Pages: - [Shared/VideoGroup.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/Shared/VideoGroup.cshtml) - Razor Partial for displaying a Video Collection - [Videos.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/Videos.cshtml) - Razor Page displaying multiple Video Collections ## Metadata APIs Feature Typically a disadvantage of statically generated websites is the lack of having APIs we can call to query website data in a easily readable data format like JSON. However we can also easily support this by also pre-rendering static `*.json` data structures along with the pre-rendered website at deployment. This capability is provided by the new [Markdown.Meta.cs](https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Markdown.Meta.cs) feature which generates multiple projections of the Markdown metadata for each type of content added in every year, e.g: ```files /meta /2021 all.json posts.json videos.json /2022 all.json posts.json /2023 all.json pages.json posts.json videos.json whatsnew.json all.json index.json ``` With this you can fetch the metadata of all the new **Blog Posts** added in **2023** from: [/2023/posts.json](https://razor-ssg.web-templates.io/meta/2023/posts.json) Or all the website content added in **2023** from: [/2023/all.json](https://razor-ssg.web-templates.io/meta/2023/all.json) Or **ALL** the website metadata content from: [/all.json](https://razor-ssg.web-templates.io/meta/all.json) This feature makes it possible to support use-cases like CreatorKit's [Generating Newsletters](https://servicestack.net/creatorkit/portal-mailruns#generating-newsletters) feature which generates a Monthly Newsletter Email with all new content added within a specified period. ## 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 - **Order** - Specify custom ordering for a collection pages ### Initializing and Loading Markdown Features All markdown features are initialized in the same way in [Configure.Ssg.cs](https://github.com/NetCoreTemplates/razor-press/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 => { context.Configuration.GetSection(nameof(AppConfig)).Bind(AppConfig.Instance); services.AddSingleton(AppConfig.Instance); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); }) .ConfigureAppHost(afterPluginsLoaded: appHost => { MarkdigConfig.Set(new MarkdigConfig { ConfigurePipeline = pipeline => { // Extend Markdig Pipeline }, ConfigureContainers = config => { config.AddBuiltInContainers(); // Add Custom Block or Inline containers } }); var pages = appHost.Resolve(); var whatsNew = appHost.Resolve(); var videos = appHost.Resolve(); var meta = appHost.Resolve(); meta.Features = new() { pages, whatsNew, videos }; meta.Features.ForEach(x => x.VirtualFiles = appHost.VirtualFiles); pages.LoadFrom("_pages"); whatsNew.LoadFrom("_whatsnew"); videos.LoadFrom("_videos"); }); }); //... } ``` 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 version You can easily update all the JavaScript dependencies used in [postinstall.js](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/postinstall.js) by running: :::sh npm install ::: 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 `Markdown*.cs` files, you'll want to exclude them from being updated by removing them from: ```js const hostFiles = [ 'Markdown.Meta.cs', 'Markdown.Pages.cs', 'Markdown.WhatsNew.cs', 'Markdown.Videos.cs', 'MarkdownPagesBase.cs', 'MarkdownTagHelper.cs', ] ``` ## Markdown Tag Helper The included [MarkdownTagHelper.cs](https://github.com/NetCoreTemplates/razor-press/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](/about) page which uses the flexibility of Razor Pages and static content component maintained with inline 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 Site Generation (SSG) Source: https://razor-press.web-templates.io/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 with: :::sh npm run prerender ::: That can then be previewed by launching a HTTP Server from the `/dist` folder with the built-in npm script: :::sh npm run serve ::: That runs **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`: ```csharp @page "/vue/" @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 render to instead with: ```csharp @page "/vue/" @attribute [RenderStatic("/vue/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 [Vue/Page.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/Vue/Page.cshtml) and [Page.cshtml](https://github.com/NetCoreTemplates/razor-press/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.GetVisiblePages().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`: - `/what-is-razor-press.html` - `/structure.html` - `/privacy.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 instead 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 need to instead be 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-press/blob/main/MyApp/Configure.Ssg.cs): ```csharp .ConfigureAppHost(afterAppHostInit: appHost => { // prerender with: `$ npm run prerender` AppTasks.Register("prerender", args => { appHost.Resolve().RenderToAsync( metaDir: appHost.ContentRootDirectory.RealPath.CombineWith("wwwroot/meta"), baseUrl: HtmlHelpers.ToAbsoluteContentUrl("")).GetAwaiter().GetResult(); 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)); // Render .html redirect files RazorSsg.PrerenderRedirectsAsync(appHost.ContentRootDirectory.GetFile("redirects.json"), distDir) .GetAwaiter().GetResult(); 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. [Generates redirect](/redirects) `.html` files for all paths in `redirects.json` 4. 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`. # Deployments Source: https://razor-press.web-templates.io/deployments ## Enable GitHub Pages 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 you'll need to enable GitHub Pages to be hosted from your **gh-pages** branch in your Repository settings: ![](/img/posts/razor-ssg/gh-pages.png) ## Register Custom Domains You should then register 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 ``` # Markdown Syntax Source: https://razor-press.web-templates.io/syntax ## Configuring Markdig Razor Press uses the high-quality [Markdig](https://github.com/xoofx/markdig) CommonMark compliant implementation for its Markdown parsing in .NET. Each [Markdown*.cs](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp) feature is able to customize which [Markdig features](https://github.com/xoofx/markdig#features) it wants to use by providing a custom `CreatePipeline()` implementation with all the Markdig extensions it needs. Alternatively the Markdig pipeline can be globally extended for all Markdown features by adding it to `MarkdigConfig` pipeline in [Configure.Ssg.cs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Configure.Ssg.cs): ```csharp MarkdigConfig.Set(new MarkdigConfig { ConfigurePipeline = pipeline => { // Extend Markdig Pipeline }, ConfigureContainers = config => { config.AddBuiltInContainers(); // Add Custom Block or Inline containers } }); ``` ## Header Anchors Headers, like above, automatically get anchor links applied with an **id** that's automatically generated from the Header text. ### Custom Anchors {#my-anchor} To specify a custom anchor tag for a heading instead of using the auto-generated one, add a suffix to the heading: ```markdown ### Custom Anchors {#my-anchor} ``` This allows you to link to the heading as `#my-anchor` instead of the default `#custom-anchors`. ### Custom Classes Custom Classes can be added to headings with the suffix: ```markdown ### Custom Classes {.my-class} ### Custom Classes {#my-anchor .my-class .my-class2} ``` However to override the default [@tailwindcss/typography](https://tailwindcss.com/docs/typography-plugin) styles applied to headings they'll need to included within a `not-prose` class which can be done with: ```markdown :::{.not-prose} ## Custom Class {.text-5xl .font-extrabold .tracking-tight .text-indigo-600} ::: ``` Which generates the HTML: ```html

Custom Class

``` To render it with the custom tailwind styles we want: :::{.not-prose} ## Custom Class {.text-5xl .font-extrabold .tracking-tight .text-indigo-600} ::: ## Document Map A Document Map is created for each Markdown document from its **Heading 2** and **Heading 3** headings, e.g: ```markdown ## Heading 2 ### Heading 3 ### Heading 3a ## Heading 2a ``` Which populates the `MarkdownFileInfo.DocumentMap` collection that renders the Document Map on the right column of each document, that's displayed in devices with larger resolutions that can fit them. The document map also makes use of the Auto heading anchors for its navigation, that's kept updated as you scroll. ## GitHub-Style Tables Many [GitHub Flavored Markdown](https://github.github.com/gfm/) syntax is also supported in Markdig like their ASCII [Tables](https://github.github.com/gfm/#tables-extension-), e.g: #### Input ```markdown | Tables | Are | Cool | | ------------- | :-----------: | ----: | | col 3 is | right-aligned | $1600 | | col 2 is | centered | $12 | | zebra stripes | are neat | $1 | ``` #### Output | Tables | Are | Cool | | ------------- | :-----------: | ----: | | col 3 is | right-aligned | $1600 | | col 2 is | centered | $12 | | zebra stripes | are neat | $1 | Which can be further styled with custom classes: #### Input ```markdown :::{.not-prose .table .table-striped} | Tables | Are | Cool | | ------------- | :-----------: | ----: | | col 3 is | right-aligned | $1600 | | col 2 is | centered | $12 | | zebra stripes | are neat | $1 | ::: ``` #### Output :::{.not-prose .table .table-striped} | Tables | Are | Cool | | ------------- | :-----------: | ----: | | col 3 is | right-aligned | $1600 | | col 2 is | centered | $12 | | zebra stripes | are neat | $1 | ::: ## Syntax Highlighting in Code Blocks Razor Press uses [highlight.js](https://highlightjs.org) to highlight code blocks allowing you to add syntax highlighting using the same syntax as [GitHub Code Blocks](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks), e.g: #### Input :::pre ```csharp class A { public string? B { get; set; } } ``` ::: #### Output ```csharp class A { public string? B { get; set; } } ``` #### Input :::pre ```json { "A": 1, "B": true } ``` ::: #### Output ```json { "A": 1, "B": true } ``` ## Markdown Fragments Markdown fragments should be maintained in `_pages/_include` - a special folder rendered with [Pages/Includes.cshtml](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/Pages/Includes.cshtml) using an Empty Layout which can be included in other Markdown and Razor Pages or fetched on demand with Ajax from [/includes/vue/formatters](/includes/vue/formatters). ## Includes Markdown Fragments can be 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 markdown fragments maintained in `_pages/_include`, which in this case embeds the rendered contents of [_include/vue/formatters.md](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/_include/vue/formatters.md). ### Include Markdown in Razor Pages Markdown Fragments can 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). ``` # Custom Markdown Containers Source: https://razor-press.web-templates.io/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. ## Built-in Containers Most of [VitePress Containers](https://vitepress.dev/guide/markdown#custom-containers) are also implemented in Razor Press, e.g: #### Input ```markdown ::: info This is an info box. ::: ::: tip This is a tip. ::: ::: warning This is a warning. ::: ::: danger This is a dangerous warning. ::: ``` #### Output ::: info This is an info box. ::: ::: tip This is a tip. ::: ::: warning This is a warning. ::: ::: danger This is a dangerous warning. ::: ### Custom Title You can specify a custom title by appending the text right after the container type: #### Input ```markdown ::: danger STOP Danger zone, do not proceed ::: ``` #### Output ::: danger STOP Danger zone, do not proceed ::: ### Pre The **pre** container can be used to capture its content in a `
` element instead of it's default markdown rendering:

```markdown
:::pre
...
:::
```

### copy

The **copy** container is ideal for displaying text snippets in a component that allows for easy copying:

#### Input

```markdown
:::copy
Copy Me!
:::
```

#### Output

:::copy
Copy Me!
:::

HTML or XML fragments can also be copied by escaping them first:

#### Input

```markdown
:::copy
``
:::
```

#### Output

:::copy
``
:::

### sh

Similarly the **sh** container is ideal for displaying and copying shell commands:

#### Input

```markdown
:::sh
npm run prerender
:::
```

#### Output

:::sh
npm run prerender
:::

## Implementing Block Containers

[Markdig Containers](https://github.com/xoofx/markdig/blob/master/src/Markdig.Tests/Specs/CustomContainerSpecs.md) are a
great way to create rich widgets that can be used directly in Markdown. 

They're useful for ensuring similar content is displayed consistently across all your documentation. A good use-case for
this could be to implement a YouTube component for standardizing how YouTube videos are displayed.

For this example we want to display a YouTube video using just its YouTube **id** and a **title** for the video which we can
capture in the Custom Container: 

```markdown
:::YouTube MRQMBrXi5Sc
Using Razor SSG to Create Websites in GitHub Codespaces
:::
```

Which we can implement with a normal Markdig `HtmlObjectRenderer`:

```csharp
public class YouTubeContainer : HtmlObjectRenderer
{
    protected override void Write(HtmlRenderer renderer, CustomContainer obj)
    {
        if (obj.Arguments == null)
        {
            renderer.WriteLine($"Missing YouTube Id, Usage :::{obj.Info} ");
            return;
        }
        
        renderer.EnsureLine();

        var youtubeId = obj.Arguments!;
        var attrs = obj.TryGetAttributes()!;
        attrs.Classes ??= new();
        attrs.Classes.Add("not-prose text-center");
        
        renderer.Write("');
        renderer.WriteLine("
"); renderer.WriteChildren(obj); renderer.WriteLine("
"); renderer.WriteLine(@$"
"); } } ``` That should be registered in `Configure.Ssg.cs` with the name we want to use for the container: ```csharp MarkdigConfig.Set(new MarkdigConfig { ConfigureContainers = config => { // Add Custom Block or Inline containers config.AddBlockContainer("YouTube", new YouTubeContainer()); } }); ``` After which it can be used in your Markdown documentation: #### Input ```markdown :::YouTube MRQMBrXi5Sc Using Razor SSG to Create Websites in GitHub Codespaces ::: ``` #### Output :::YouTube MRQMBrXi5Sc Using Razor SSG to Create Websites in GitHub Codespaces ::: ### Custom Attributes Since we use `WriteAttributes(obj)` to emit any attributes we're also able to customize the widget to use a custom **id** and classes, e.g: #### Input ```markdown :::YouTube MRQMBrXi5Sc {.text-indigo-600} Using Razor SSG to Create Websites in GitHub Codespaces ::: ``` #### Output :::YouTube MRQMBrXi5Sc {.text-indigo-600} Using Razor SSG to Create Websites in GitHub Codespaces ::: ## Implementing Inline Containers Custom Inline Containers are useful when you don't need a to capture a block of content, like if we just want to display a video without a title, e.g: ```markdown ::YouTube MRQMBrXi5Sc:: ``` Inline Containers can be implemented with a Markdig `HtmlObjectRenderer`, e.g: ```csharp public class YouTubeInlineContainer : HtmlObjectRenderer { protected override void Write(HtmlRenderer renderer, CustomContainerInline obj) { var youtubeId = obj.FirstChild is Markdig.Syntax.Inlines.LiteralInline literalInline ? literalInline.Content.AsSpan().RightPart(' ').ToString() : null; if (string.IsNullOrEmpty(youtubeId)) { renderer.WriteLine($"Missing YouTube Id, Usage ::YouTube ::"); return; } renderer.WriteLine(@$"
"); } } ``` That can be registered in `Configure.Ssg.cs` with: ```csharp MarkdigConfig.Set(new MarkdigConfig { ConfigureContainers = config => { // Add Custom Block or Inline containers config.AddInlineContainer("YouTube", new YouTubeInlineContainer()); } }); ``` Where it can then be used in your Markdown documentation: #### Input ```markdown ::YouTube MRQMBrXi5Sc:: ``` #### Output ::YouTube MRQMBrXi5Sc:: # Using Vue in Markdown Source: https://razor-press.web-templates.io/vue-in-markdown ## Progressive Enhancement Thanks to the Vue's elegant approach for progressively enhancing HTML content, Razor Press markdown documents can embed interactive reactive Vue components directly in Markdown which makes it possible to document the [Vue Tailwind Component Library](/vue/autoquerygrid) and its interactive component examples, embedded directly in Markdown. ## Markdown Documents are Vue Apps We can embed Vue components directly in Markdown simply because all Markdown Documents are themselves Vue Apps, which by default are created with the same [/mjs/app.mjs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/wwwroot/mjs/app.mjs) configuration that all Razor Pages uses. This allows using any [Vue DOM syntax](https://vuejs.org/guide/essentials/template-syntax.html) or global Vue components directly in Markdown, in the same way that they're used in Vue Apps defined in Razor or HTML pages. For example we can display the [GettingStarted.mjs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/wwwroot/mjs/components/GettingStarted.mjs) component that's on the home page with: #### Input ```html ``` #### Output ## Markdown with Custom Vue Apps Pages that need advanced functionality beyond what's registered in the global App configuration can add additional functionality by adding a JavaScript module with the same **path** and **filename** of the markdown page with an `.mjs` extension: ``` /wwwroot/pages//.mjs ``` This is utilized by most [/pages/vue](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/wwwroot/pages/vue) Markdown pages to handle the unique requirements of each page's live examples. E.g. the [autoquerygrid.mjs](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/wwwroot/pages/vue/autoquerygrid.mjs) uses a custom Vue App component that registers a custom **client** dependency and **Responsive** and **CustomBooking** components that are only used in [/vue/autoquerygrid.md](https://github.com/NetCoreTemplates/razor-press/blob/main/MyApp/_pages/vue/autoquerygrid.md) page to render the interactive live examples in the [/vue/autoquerygrid](/vue/autoquerygrid) page: ```js import { onMounted } from "vue" import { Authenticate } from "./dtos.mjs" import { useAuth, useClient } from '@servicestack/vue' import { JsonApiClient } from "@servicestack/client" import Responsive from "./autoquerygrid/Responsive.mjs" import CustomBooking from "./autoquerygrid/CustomBooking.mjs" export default { install(app) { app.provide('client', JsonApiClient.create('https://blazor-gallery-api.jamstacks.net')) }, components: { Responsive, CustomBooking, }, setup() { const client = useClient() onMounted(async () => { const api = await client.api(new Authenticate({ provider: 'credentials', userName:'admin@email.com', password:'p@55wOrd' })) if (api.succeeded) { const { signIn } = useAuth() signIn(api.response) } }) return { } } } ``` ### Convert from Vue script setup This is roughly equivalent to the sample below if coming from VitePress or other npm Vue project that uses Vue's compile-time syntactic [` ``` ## Custom Styles in Markdown If needed custom styles can also be added for individual pages by adding a `.css` file at the following location: ``` /wwwroot/pages//.css ``` # Sidebars Source: https://razor-press.web-templates.io/sidebars ## Main Sidebar The sidebar defines the main navigation for your documentation, you can configure the sidebar menu in `_pages/sidebar.json` which adopts the same structure as [VitePress Sidebars](https://vitepress.dev/reference/default-theme-sidebar#sidebar), e.g: ```json [ { "text": "Introduction", "link": "/", "children": [ { "text": "What is Razor Press?", "link": "/what-is-razor-press" }, { "text": "Structure", "link": "/structure" } ] }, { "text": "Markdown", "children": [ { "text": "Sidebars", "link": "/sidebars" } ] } ] ``` ## Navigation Headings Primary navigation headings can optionally have `"links"` to make them linkable and `"icon"` to render them with a custom icon, e.g: ```json { "icon": "....", "text": "Markdown", "link": "/markdown/", "children": [ ] } ``` ## Documentation Group Sidebars If your happy to use the same document page title for its menu item label, you can use an implicitly generated Sidebar navigation like [/creatorkit/](/creatorkit/about) uses for its Sidebar navigation which can be ordered with the **order** index defined in its frontmatter, e.g: ```yaml title: About order: 1 ``` Which can also be grouped into different navigation sections using the **group** frontmatter, e.g: ```yaml title: Overview order: 6 group: Portal ``` ### Custom Sidebars For more flexibility a custom sidebar can be defined for each group by defining a `sidebar.json` in its folder `_pages//sidebar.json` which [/vue/](/vue/install) uses for its explicit Sidebar Navigation, e.g: ```json [ { "text": "Vue", "link": "/vue", "children": [ { "text": "Install", "link": "/vue/install" } ] }, { "text": "Component Gallery", "children": [ { "text": "AutoQueryGrid", "link": "/vue/autoquerygrid" } ] } ] ``` # Redirects Source: https://razor-press.web-templates.io/redirects ## Static HTML Page Redirects The [redirects.json](https://github.com/NetCoreTemplates/razor-press/tree/main/MyApp/redirects.json) file allows you to define a map of routes with what routes they should redirect to, e.g: ```json { "/creatorkit": "/creatorkit/", "/vue": "/vue/" } ``` When prerendering this will generate a `*.html` page for each mapping containing a [meta refresh](https://www.w3.org/TR/WCAG20-TECHS/H76.html) to perform a client-side redirect to the new route which will work in all static file hosts and CDNs like GitHub Pages CDN. Where it will redirect invalid routes like [/vue](https://razor-press.web-templates.io/vue) and [/creatorkit](https://razor-press.web-templates.io/creatorkit) to their appropriate `/vue/` and `/creatorkit/` paths. ## Redirects in AWS S3 Alternatively if you deploy your static site to a smarter static file host like an AWS S3 bucket you can perform these redirects on the server by defining them in [Custom Redirection Rules](https://docs.aws.amazon.com/AmazonS3/latest/userguide/how-to-page-redirect.html). # Typesense Real-Time Search Source: https://razor-press.web-templates.io/typesense Many popular Open Source products use [Algolia DocSearch](https://docsearch.algolia.com) to power their real-time search features, however, it's a less appealing product for commercial products which is a paid service with a per request pricing model that made it difficult to determine what costs would be in the long run. We discovered [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. Given its effortless integration, simplicity-focus and end-user UX, it quickly became our preferred way to navigate [docs.servicestack.net](https://docs.servicestack.net). To make it easier to adopt Typesense's amazing OSS Search product we've documented the approach we use to create and deploy an index of our site automatically using GitHub Actions that you could also utilize in your Razor Press websites. Documentation search is a common use case which Typesense caters for with their [typesense-docsearch-scraper](https://github.com/typesense/typesense-docsearch-scraper) - a utility designed to easily scrape a website and post the results to a Typesense server to create a fast searchable index. ## Self hosting option We recommend using running their [easy to use Docker image](https://hub.docker.com/r/typesense/typesense/) to run an instance of their Typesense server, which you can run in a **t2.small** AWS EC2 instance or in a [Hetzner Cloud](https://www.hetzner.com/cloud) VM for a more cost effective option. Trying it locally, we used the following commands to spin up a local Typesense server ready to scrape out docs site. ```sh 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. Or if you use their cloud offering, you can use their web dashboard to monitor and manage your index data. ## Populating the index With your local server is running, you can scrape your docs site using the [typesense-docsearch-scraper](https://github.com/typesense/typesense-docsearch-scraper). This needs some configuration to tell the scraper: - Where the Typesense server is - How to authenticate with the Typesense server - Where the docs website is - 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](https://github.com/ServiceStack/docs/blob/master/search-server/typesense-scraper/typesense-scraper.env) file related to the Typesense server information and a [.json](https://github.com/ServiceStack/docs/blob/master/search-server/typesense-scraper/typesense-scraper-config.json) file related to what site will be getting scraped. With a 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 to configure the `.json` config for the scraper. The **typesense-docsearch-scraper** has [an example of this](https://github.com/typesense/typesense-docsearch-scraper/blob/master/configs/public/typesense_docs.json) config in their repository. The default selectors will need to match the your websites HTML, which for Razor Press sites can start with the configuration, updated with your website domains: ```json { "index_name": "typesense_docs", "allowed_domains": ["docs.servicestack.net"], "start_urls": [ { "url": "https://docs.servicestack.net/" } ], "selectors": { "default": { "lvl0": "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": " .,;:#" } ``` With 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` which we can pass our configuration to, using the following command: ```sh docker run -it --env-file typesense-scraper.env \ -e "CONFIG=$(cat typesense-scraper-config.json | jq -r tostring)" \ typesense/docsearch-scraper ``` Here `-i` is used to reference a local `--env-file` and use `cat` and `jq` used to populate the `CONFIG` environment variable with the `.json` config file. ## Docker networking We had a slight issue here since the scraper itself is running in Docker via WSL and `localhost` doesn't resolve 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 after which we see the communication between the servers 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 After you have a Typesense server with an index full of content, you'll want to be able to use it to search your docs site. You can query the index using `curl` which needs to known 3 key pieces of information: - Collection name, eg `typesense_docs` - Query term, `?q=test` - What to query, `&query_by=content` ```sh 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 the scraper was 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 A problem that becomes apparent when running the scraper is that it increases the size of the index since it currently doesn't detect and update existing documents. It wasn't clear if this is possible to configure from the current scraper, but 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 to avoid manual intervention - Have high uptime so documentation search is always available Typesense server itself performs extremely well, so a full update from the scraper doesn't generate an amount of load. However, every additional scrape uses additional disk space and memory that will eventually require periodically resetting and repopulating the index. One option is to switch to a new collection everytime the documentation is updated and delete the old collection, adopting a workflow that looks something like: 1. Docs are updated 2. Publish updated docs 3. Create new collection, store new and old names 4. Scrape updated docs 5. Update client with new collection 6. Delete old collection However this would require orchestration across a number of GitHub Action workflows which we anticipated would be fragile and non-deterministic as to how long it will take to scrape, update, and deploy our changes. ## Read-only Docker container The approach we ended up adopting was to develop and deploy read only Typesense Docker images containing an immutable copy of the index data in it as part of the GitHub Action deployments. 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 into the docker image**. 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. ## Typesense Performance 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%** whilst serving constant typeahead searching. ![](https://servicestack.net/img/posts/typesense/typesense-cpu-utilization.png) Since our docs site index is small (500 pages), the memory footprint is also tiny and stable at **~50MB** or **~10%** of the the service's soft memory limit. ![](https://servicestack.net/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) Whilst this approach of shipping an index along with the Docker image isn't practical for large or 'living' indexes, many small to medium-sized documentation sites would likely benefit from the simplified approach of deploying readonly Docker images. ## GitHub Actions Workflow To create our own Docker image for our search server we need to perform the following tasks in our GitHub Action: 1. Run a local Typesense server in the GitHub Action using Docker 2. Scrape our hosted docs populating the local Typesense server 3. Copy the `data` folder of our local Typesense server during `docker build` Which is done with: ```sh 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 ``` To avoid updating our search client between updates we also want to use the same **search-only API Key** everytime a new server is created. This can be achieved by specifying `value` in the `POST` command sent to the local Typesense server: ```sh 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":["*"]}' ``` If you're interested in adopting a similar approach you can find the whole GitHub Action workflow in our [search-index-update.yml](https://github.com/ServiceStack/docs/blob/master/.github/workflows/search-index-update.yml) workflow. ## Search UI Dialog After 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 dialog which we've implemented in the [Typesense.mjs](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/wwwroot/mjs/components/Typesense.mjs) Vue component which you can register as a global component in your [app.mjs](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/wwwroot/mjs/app.mjs): ```js import Typesense from "./components/Typesense.mjs" const Components = { //... Typesense, } ``` Which renders as a **Search Button** that we've added next to our **Dark Mode Toggle** button in our [Header.cshtml](https://github.com/ServiceStack/docs.servicestack.net/blob/main/MyApp/Pages/Shared/Header.cshtml): ```html ``` ![](https://servicestack.net/img/posts/typesense/typesense-header.png) The button also encapsulates the dialog component which uses Typesense REST API to 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' } }) ``` This 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 ![](https://servicestack.net/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 [docs.servicestack.net](https://docs.servicestack.net). It also does a great job handling typos and has quickly become the fastest way to navigate our extensive documentation that we hope also serves useful for implementing Typesense real-time search in your own documentation websites. # Installation Source: https://razor-press.web-templates.io/vue/install ## Manual Installation **@servicestack/vue** can be added to existing Vue SPA Apps by installing via npm: ```bash $ npm install @servicestack/vue ``` Where it will also install its **vue** and **@servicestack/client** dependencies. ## Installation-less option Alternatively you can take advantage of modern browsers [JS Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules) support to use these libraries without installation by registering an [importmap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) to define where it should load the ESM builds of these libraries from, e.g: ```html ``` For intranet Web Apps that need to work without internet access, save and reference local copies of these libraries, e.g: ```html ``` ## @Html.ImportMap Razor Pages or MVC Apps can use the `Html.ImportMaps()` to use local debug builds during development and optimal CDN hosted minified production builds in production: ```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") }) ``` > It's recommended to use exact versions to eliminate redirect latencies and to match the local version your App was developed against ### 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 pre-configured [ES Module Shims](https://github.com/guybedford/es-module-shims): ```html @if (Context.Request.Headers.UserAgent.Any(x => x.Contains("Safari") && !x.Contains("Chrome"))) { } ``` ## Registration Then register the `@servicestack/vue` component library with your Vue app with: ```js import { JsonApiClient } from "@servicestack/client" import ServiceStackVue from "@servicestack/vue" const client = JsonApiClient.create() const app = createApp(component, props) app.provide('client', client) app.use(ServiceStackVue) //... app.mount('#app') ``` The **client** instance is used by API-enabled components to call your APIs using the [/api predefined route](/routing#json-api-pre-defined-route). ServiceStack Apps not running on .NET 6+ or have the **/api** route disabled should use `JsonServiceClient` instead: ```js const client = new JsonServiceClient() ``` ## Not using Vue Router Non SPA Vue Apps that don't use [Vue Router](https://router.vuejs.org) should register a replacement `` component that uses the browser's native navigation in [navigational components](/vue/navigation): ```js app.component('RouterLink', ServiceStackVue.component('RouterLink')) ``` # AutoQueryGrid Component Source: https://razor-press.web-templates.io/vue/autoquerygrid
## Default CRUD By default you can create an AutoQueryGrid that allows authorized users the ability to Create, Read, Update & Delete records with just the DataModel, e.g: ```html ```
This will utilize your App's existing [AutoQuery APIs](/autoquery/rdbms) for the specified DataModel to enable its CRUD functionality. ## Read Only You can use `apis` to limit which AutoQuery APIs AutoQueryGrid should use, so if only the AutoQuery DTO is provided, the AutoQueryGrid will only be browsable in **read-only** mode: ```html ```

Table Styles

The same [DataGrid Table Styles](/vue/datagrid#table-styles) can also be used to style AutoQueryGrid, e.g: ```html ```
**Custom Styles** The AutoQueryGrid's appearance is further customizable with the property classes & functions below: ```ts defineProps<{ toolbarButtonClass: string tableStyle: "simple" | "fullWidth" | "stripedRows" | "whiteBackground" | "uppercaseHeadings" | "verticalLines" gridClass: string grid2Class: string grid3Class: string grid4Class: string tableClass: string theadClass: string tbodyClass: string theadRowClass: string theadCellClass: string rowClass:(model:any,i:number) => string rowStyle:(model:any,i:number) => StyleValue }>() ```

Custom AutoQueryGrid

Different AutoQueryGrid features can be hidden with `hide` and functionality disabled with `deny`, e.g: ```html ```
Features that can be hidden and disabled include: ```ts defineProps<{ deny: "filtering" | "queryString" | "queryFilters" hide: "toolbar" | "preferences" | "pagingNav" | "pagingInfo" | "downloadCsv" | "refresh" | "copyApiUrl" | "filtersView" | "newItem" | "resetPreferences" }>() ```

Global AutoQueryGrid Configuration

These features can also be disabled at a global level, applying to all `` components with [setConfig](/vue/use-config), e.g: ```js const { setAutoQueryGridDefaults } = useConfig() setAutoQueryGridDefaults({ hide: ['pagingNav','copyApiUrl','downloadCsv'] }) ```

Limit Columns

By default AutoQueryGrid displays all public properties returned in its AutoQuery API which can be further limited with `selected-columns`: ```html ```

Simple Responsive Columns

Using `visible-from` is a simple way to enable a responsive DataGrid by specifying at which [Tailwind breakpoints](https://tailwindcss.com/docs/responsive-design) columns should be visible from and `header-titles` to use friendlier aliases for different columns, e.g: ```html ```

Custom Responsive Columns

Which columns are displayed and how they're formatted are further customizable with `