From 03b09249c5f14992550d3c94a7531fa34d15a92e Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Sun, 25 Feb 2024 08:17:41 +0000 Subject: [PATCH] deploy: de959dd8fe7e9f3ed92a853c91771a7174c7c5f2 --- 404.html | 4 +- api/2024/all.json | 25 +- api/2024/blog.json | 25 +- api/all.json | 25 +- ...s => 2022-09-06_hetzner-cloud-FBhVcTbt.js} | 2 +- ...k.js => 2022-12-31_typography-BhdtW7rK.js} | 2 +- ...E5f1j.js => 2023-01-01_deploy-BP7kyxZZ.js} | 2 +- ...-B3e8pVg7.js => 2023-01-10_vs-CjcgGp8Z.js} | 2 +- ...0f_TD6.js => 2023-01-11_rider-CM-m6doS.js} | 2 +- ...Evf4zv.js => 2023-01-21_start-BmNRwozK.js} | 2 +- ...I.js => 2023-02-01_javascript-CNPJxnIi.js} | 72 ++-- ...3g.js => 2023-03-30_razor-ssg-CI_Ztr2P.js} | 2 +- ...3_razor-ssg-new-blog-features-DfRWXA11.js} | 18 +- ...23-11-20_net8-blazor-template-CCLPpx-4.js} | 2 +- ...> 2023-11-22_net8-best-blazor-C_kPuv0r.js} | 2 +- ...-28_markdown-components-in-vue-CRnyvCSj.js | 133 +++++++ ... 2024-03-01_vite-press-plugin-ByqZZ45s.js} | 4 +- ...-02_markdown-components-in-vue-Cf6k3uGi.js | 1 - ...e_type_script_setup_true_lang-D7nVe9w2.js} | 2 +- assets/ChartJs-3tDylpqN.js | 1 + ...e_type_script_setup_true_lang-DcP0JNgA.js} | 2 +- ...e_type_script_setup_true_lang-C2rK_3Mh.js} | 2 +- ...ue_type_script_setup_true_lang-sMvvqITG.js | 1 + ...e_type_script_setup_true_lang-NH5JTqV0.js} | 2 +- ...e_type_script_setup_true_lang-CE9iaOou.js} | 2 +- ...e_type_script_setup_true_lang-pYHG5puA.js} | 2 +- ...ue_type_script_setup_true_lang-BcwdGCEk.js | 1 - ...ue_type_script_setup_true_lang-BhUzPs-y.js | 1 + ...ue_type_script_setup_true_lang-CwnMrSVu.js | 1 + ...ue_type_script_setup_true_lang-f5ax-dlV.js | 1 - ....all_-C7p4qHOO.js => _...all_-DFsd6e9a.js} | 2 +- ...{_name_-CAEGo4zM.js => _name_-B8N3fMGM.js} | 2 +- ...{_slug_-ruWWH_VQ.js => _slug_-6NcWv-MK.js} | 2 +- .../{_tag_-xH5OO5xZ.js => _tag_-Bh2bjPSi.js} | 2 +- ...{_year_-AXLL9ihc.js => _year_-Davmi6ss.js} | 2 +- .../{about-DNx8OFa6.js => about-vTOtZUIt.js} | 2 +- .../{admin-DGto2HXJ.js => admin-CTGNrLFc.js} | 2 +- ...-CORWu7dQ.js => autoquerygrid-qn9Rd5KH.js} | 2 +- assets/{blog-BHzLu2kY.js => blog-CiJ9FFvj.js} | 2 +- ...neXpB3W.js => component-links-pRcN8eHo.js} | 2 +- ...nts-CqPvZizH.js => components-BIC-ysCl.js} | 2 +- assets/counter-Bg15gkM9.js | 1 + assets/counter-D3HvQADj.js | 1 - ...efault-JfeifahK.js => default-Cov6iB8O.js} | 2 +- .../{empty-CPUCOumg.js => empty-Dy2qivJG.js} | 2 +- ...ture1-GW2UOmBC.js => feature1-BEYjs1mu.js} | 2 +- ...ture1-BYSWBwkE.js => feature1-CVVuygai.js} | 2 +- ...ture1-DPlyZW0O.js => feature1-oDMYy1M8.js} | 2 +- ...tures-Dw-J-nlV.js => features-9gSRura6.js} | 2 +- assets/index-BQNYpzqF.css | 1 - .../{index-UIP3uBrX.js => index-B_CDEXox.js} | 2 +- .../{index-CLJadxM_.js => index-BqGUp3nV.js} | 2 +- assets/index-Cef7LNnC.css | 1 + .../{index-BXwhoUEp.js => index-DWUkzBt0.js} | 336 ++++++++++++++++-- assets/{logo-BRdSS5Yp.js => logo-Bb6dZ7E7.js} | 2 +- ...{modern-D9U4qtEm.js => modern-Dvn-KTxg.js} | 2 +- ...rivacy-DynF4IjT.js => privacy-C1Fo50qS.js} | 2 +- .../{utils-C7W1_D5L.js => utils-cvgPy7Nt.js} | 2 +- assets/videos-BVXxEoSF.js | 1 - assets/videos-DtmXX1pZ.js | 1 + ...tsnew-HFN6qEQs.js => whatsnew-X_yDUrdU.js} | 2 +- index.html | 4 +- 62 files changed, 592 insertions(+), 145 deletions(-) rename assets/{2022-09-06_hetzner-cloud-D_tSyY8t.js => 2022-09-06_hetzner-cloud-FBhVcTbt.js} (99%) rename assets/{2022-12-31_typography-CqjuP9zk.js => 2022-12-31_typography-BhdtW7rK.js} (98%) rename assets/{2023-01-01_deploy-4CZE5f1j.js => 2023-01-01_deploy-BP7kyxZZ.js} (98%) rename assets/{2023-01-10_vs-B3e8pVg7.js => 2023-01-10_vs-CjcgGp8Z.js} (98%) rename assets/{2023-01-11_rider-DX0f_TD6.js => 2023-01-11_rider-CM-m6doS.js} (98%) rename assets/{2023-01-21_start-CiEvf4zv.js => 2023-01-21_start-BmNRwozK.js} (98%) rename assets/{2023-02-01_javascript-BJNPrkTI.js => 2023-02-01_javascript-CNPJxnIi.js} (76%) rename assets/{2023-03-30_razor-ssg-BsYRmb3g.js => 2023-03-30_razor-ssg-CI_Ztr2P.js} (99%) rename assets/{2023-08-23_razor-ssg-new-blog-features-D0tLlErM.js => 2023-08-23_razor-ssg-new-blog-features-DfRWXA11.js} (59%) rename assets/{2023-11-20_net8-blazor-template-DZPACl7V.js => 2023-11-20_net8-blazor-template-CCLPpx-4.js} (97%) rename assets/{2023-11-22_net8-best-blazor-khEMeRjt.js => 2023-11-22_net8-best-blazor-C_kPuv0r.js} (99%) create mode 100644 assets/2024-02-28_markdown-components-in-vue-CRnyvCSj.js rename assets/{2024-03-01_vite-press-plugin-DWr26rHL.js => 2024-03-01_vite-press-plugin-ByqZZ45s.js} (92%) delete mode 100644 assets/2024-03-02_markdown-components-in-vue-Cf6k3uGi.js rename assets/{BlogTitle.vue_vue_type_script_setup_true_lang-Cl3NiB5z.js => BlogTitle.vue_vue_type_script_setup_true_lang-D7nVe9w2.js} (92%) create mode 100644 assets/ChartJs-3tDylpqN.js rename assets/{Counter.vue_vue_type_script_setup_true_lang-BwW-seaC.js => Counter.vue_vue_type_script_setup_true_lang-DcP0JNgA.js} (70%) rename assets/{GettingStarted.vue_vue_type_script_setup_true_lang-CdAW7Hil.js => GettingStarted.vue_vue_type_script_setup_true_lang-C2rK_3Mh.js} (98%) create mode 100644 assets/HelloApi.vue_vue_type_script_setup_true_lang-sMvvqITG.js rename assets/{MarkdownComponent.vue_vue_type_script_setup_true_lang-5vfQ1lk2.js => MarkdownComponent.vue_vue_type_script_setup_true_lang-NH5JTqV0.js} (88%) rename assets/{MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js => MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js} (78%) rename assets/{SrcPage.vue_vue_type_script_setup_true_lang-CztkhkAE.js => SrcPage.vue_vue_type_script_setup_true_lang-pYHG5puA.js} (93%) delete mode 100644 assets/Templates.vue_vue_type_script_setup_true_lang-BcwdGCEk.js create mode 100644 assets/Templates.vue_vue_type_script_setup_true_lang-BhUzPs-y.js create mode 100644 assets/VideoGroup.vue_vue_type_script_setup_true_lang-CwnMrSVu.js delete mode 100644 assets/VideoGroup.vue_vue_type_script_setup_true_lang-f5ax-dlV.js rename assets/{_...all_-C7p4qHOO.js => _...all_-DFsd6e9a.js} (92%) rename assets/{_name_-CAEGo4zM.js => _name_-B8N3fMGM.js} (85%) rename assets/{_slug_-ruWWH_VQ.js => _slug_-6NcWv-MK.js} (97%) rename assets/{_tag_-xH5OO5xZ.js => _tag_-Bh2bjPSi.js} (88%) rename assets/{_year_-AXLL9ihc.js => _year_-Davmi6ss.js} (86%) rename assets/{about-DNx8OFa6.js => about-vTOtZUIt.js} (98%) rename assets/{admin-DGto2HXJ.js => admin-CTGNrLFc.js} (79%) rename assets/{autoquerygrid-CORWu7dQ.js => autoquerygrid-qn9Rd5KH.js} (92%) rename assets/{blog-BHzLu2kY.js => blog-CiJ9FFvj.js} (97%) rename assets/{component-links-yneXpB3W.js => component-links-pRcN8eHo.js} (85%) rename assets/{components-CqPvZizH.js => components-BIC-ysCl.js} (81%) create mode 100644 assets/counter-Bg15gkM9.js delete mode 100644 assets/counter-D3HvQADj.js rename assets/{default-JfeifahK.js => default-Cov6iB8O.js} (96%) rename assets/{empty-CPUCOumg.js => empty-Dy2qivJG.js} (81%) rename assets/{feature1-GW2UOmBC.js => feature1-BEYjs1mu.js} (94%) rename assets/{feature1-BYSWBwkE.js => feature1-CVVuygai.js} (93%) rename assets/{feature1-DPlyZW0O.js => feature1-oDMYy1M8.js} (94%) rename assets/{features-Dw-J-nlV.js => features-9gSRura6.js} (95%) delete mode 100644 assets/index-BQNYpzqF.css rename assets/{index-UIP3uBrX.js => index-B_CDEXox.js} (85%) rename assets/{index-CLJadxM_.js => index-BqGUp3nV.js} (90%) create mode 100644 assets/index-Cef7LNnC.css rename assets/{index-BXwhoUEp.js => index-DWUkzBt0.js} (68%) rename assets/{logo-BRdSS5Yp.js => logo-Bb6dZ7E7.js} (85%) rename assets/{modern-D9U4qtEm.js => modern-Dvn-KTxg.js} (91%) rename assets/{privacy-DynF4IjT.js => privacy-C1Fo50qS.js} (97%) rename assets/{utils-C7W1_D5L.js => utils-cvgPy7Nt.js} (84%) delete mode 100644 assets/videos-BVXxEoSF.js create mode 100644 assets/videos-DtmXX1pZ.js rename assets/{whatsnew-HFN6qEQs.js => whatsnew-X_yDUrdU.js} (91%) diff --git a/404.html b/404.html index a20cb95..df81fcf 100644 --- a/404.html +++ b/404.html @@ -5,8 +5,8 @@ Vite + Vue + TS - - + +
diff --git a/api/2024/all.json b/api/2024/all.json index fbc24be..54e023e 100644 --- a/api/2024/all.json +++ b/api/2024/all.json @@ -1,14 +1,5 @@ { "blog": [ - { - "slug": "markdown-components-in-vue", - "fileName": "markdown-components-in-vue.md", - "date": "2024-03-02", - "tags": [], - "wordCount": 0, - "lineCount": 1, - "url": "https://press-vue.servicestack.net/posts/markdown-components-in-vue" - }, { "slug": "vite-press-plugin", "fileName": "vite-press-plugin.md", @@ -24,6 +15,22 @@ "author": "Lucy Bates", "image": "https://images.unsplash.com/photo-1524668951403-d44b28200ce0?crop=entropy&fit=crop&h=1000&w=2000", "url": "https://press-vue.servicestack.net/posts/vite-press-plugin" + }, + { + "slug": "markdown-components-in-vue", + "fileName": "markdown-components-in-vue.md", + "date": "2024-02-28", + "tags": [ + "docs", + "markdown" + ], + "wordCount": 971, + "lineCount": 293, + "title": "Vue Components in Markdown", + "summary": "How to reference and Import Vue Components in Markdown", + "author": "Lucy Bates", + "image": "https://images.unsplash.com/photo-1700427296131-0cc4c4610fc6?crop=entropy&fit=crop&h=1000&w=2000", + "url": "https://press-vue.servicestack.net/posts/markdown-components-in-vue" } ], "videos": [], diff --git a/api/2024/blog.json b/api/2024/blog.json index 3e91062..17735e6 100644 --- a/api/2024/blog.json +++ b/api/2024/blog.json @@ -1,13 +1,4 @@ [ - { - "slug": "markdown-components-in-vue", - "fileName": "markdown-components-in-vue.md", - "date": "2024-03-02", - "tags": [], - "wordCount": 0, - "lineCount": 1, - "url": "https://press-vue.servicestack.net/posts/markdown-components-in-vue" - }, { "slug": "vite-press-plugin", "fileName": "vite-press-plugin.md", @@ -23,5 +14,21 @@ "author": "Lucy Bates", "image": "https://images.unsplash.com/photo-1524668951403-d44b28200ce0?crop=entropy&fit=crop&h=1000&w=2000", "url": "https://press-vue.servicestack.net/posts/vite-press-plugin" + }, + { + "slug": "markdown-components-in-vue", + "fileName": "markdown-components-in-vue.md", + "date": "2024-02-28", + "tags": [ + "docs", + "markdown" + ], + "wordCount": 971, + "lineCount": 293, + "title": "Vue Components in Markdown", + "summary": "How to reference and Import Vue Components in Markdown", + "author": "Lucy Bates", + "image": "https://images.unsplash.com/photo-1700427296131-0cc4c4610fc6?crop=entropy&fit=crop&h=1000&w=2000", + "url": "https://press-vue.servicestack.net/posts/markdown-components-in-vue" } ] \ No newline at end of file diff --git a/api/all.json b/api/all.json index 91e69a8..c249b3a 100644 --- a/api/all.json +++ b/api/all.json @@ -1,14 +1,5 @@ { "blog": [ - { - "slug": "markdown-components-in-vue", - "fileName": "markdown-components-in-vue.md", - "date": "2024-03-02", - "tags": [], - "wordCount": 0, - "lineCount": 1, - "url": "https://press-vue.servicestack.net/posts/markdown-components-in-vue" - }, { "slug": "vite-press-plugin", "fileName": "vite-press-plugin.md", @@ -25,6 +16,22 @@ "image": "https://images.unsplash.com/photo-1524668951403-d44b28200ce0?crop=entropy&fit=crop&h=1000&w=2000", "url": "https://press-vue.servicestack.net/posts/vite-press-plugin" }, + { + "slug": "markdown-components-in-vue", + "fileName": "markdown-components-in-vue.md", + "date": "2024-02-28", + "tags": [ + "docs", + "markdown" + ], + "wordCount": 971, + "lineCount": 293, + "title": "Vue Components in Markdown", + "summary": "How to reference and Import Vue Components in Markdown", + "author": "Lucy Bates", + "image": "https://images.unsplash.com/photo-1700427296131-0cc4c4610fc6?crop=entropy&fit=crop&h=1000&w=2000", + "url": "https://press-vue.servicestack.net/posts/markdown-components-in-vue" + }, { "slug": "net8-best-blazor", "fileName": "net8-best-blazor.md", diff --git a/assets/2022-09-06_hetzner-cloud-D_tSyY8t.js b/assets/2022-09-06_hetzner-cloud-FBhVcTbt.js similarity index 99% rename from assets/2022-09-06_hetzner-cloud-D_tSyY8t.js rename to assets/2022-09-06_hetzner-cloud-FBhVcTbt.js index 54fc404..3fb62b3 100644 --- a/assets/2022-09-06_hetzner-cloud-D_tSyY8t.js +++ b/assets/2022-09-06_hetzner-cloud-FBhVcTbt.js @@ -1,3 +1,3 @@ -import{_ as n}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{g as r,o as i,h as l,w as u,a as e,e as t}from"./index-BXwhoUEp.js";const d=e("div",{class:"markdown-body"},[e("p",null,[t("At "),e("a",{href:"./"},"ServiceStack"),t(", 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.")]),e("p",null,[t("Thankfully, more and more companies are providing simpler offerings for hosting needs, and AWS themselves launched "),e("a",{href:"https://aws.amazon.com/lightsail"},"Lightsail"),t(" as their answer to market demands for simple hosting options that package everything you need for basic hosting.")]),e("p",null,"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."),e("h2",null,"Looking at different US offerings"),e("p",null,[t("Something we wanted to do was to host our "),e("a",{href:"https://github.com/ServiceStackApps/LiveDemos"},"live demo applications"),t(" on a US based host. We were using "),e("a",{href:"https://www.hetzner.com/dedicated-rootserver"},"Hetzner dedicated servers"),t(" in the past for non-latency sensitive use cases like our build server and "),e("a",{href:"https://gist.cafe"},"Gist.Cafe (our interactive playground for multiple platforms)"),t(" but we also wanted our demo applications to be snappy for US users.")]),e("p",null,[e("a",{href:"https://www.digitalocean.com/pricing"},"DigitalOcean"),t(" provides "),e("a",{href:"https://www.digitalocean.com/pricing/droplets"},"“Droplets”"),t(" 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.")]),e("p",null,[t("For deployment, "),e("a",{href:"https://docs.servicestack.net/do-github-action-mix-deployment"},"we also like to keep things as simple as we can, whilst keeping portability"),t(". Since all our projects are public and on GitHub, we use "),e("a",{href:"https://docs.servicestack.net/do-github-action-mix-deployment#github-repository-setup"},"GitHub Actions"),t(" heavily along with a pattern that deploys our applications using Docker Compose via SSH. Each application runs in its own container behind an "),e("a",{href:"https://docs.servicestack.net/do-github-action-mix-deployment#get-nginx-reverse-proxy-and-letsencrypt-companion-running"},"NGINX proxy"),t(" with a side car that handles renewing LetsEncrypt certificates. Below is an example of this pattern with Blazor and Litestream.")]),e("iframe",{class:"youtube",src:"https://www.youtube.com/embed/fY50dWszpw4",frameborder:"0",allow:"autoplay; encrypted-media",allowfullscreen:""}),e("p",null,"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."),e("p",null,[t("A minute or so later, the application is back running again. Since their were 20+ of these repositories we took advantage of the "),e("a",{href:"https://cli.github.com/manual/gh_secret_set"},"GitHub Organization Secrets"),t(" so we only needed to update values in one place, and "),e("a",{href:"https://cli.github.com/manual/gh_workflow_run"},"running the workflows again"),t(" can also be done programmatically through the GitHub CLI.")]),e("h2",null,"DigitalOcean Price Increase"),e("p",null,[t("In June of 2022, we got a notification that "),e("a",{href:"https://www.digitalocean.com/try/new-pricing"},"prices for droplets would be increasing"),t(", and for our droplet it would be going from "),e("strong",null,"$40 to $48"),t(". While this is a small amount of money, it prompted us to have a wider look into this market.")]),e("p",null,"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."),e("p",null,[t("Not everyone builds massively distributed systems, and as hardware performance increases, and platforms like "),e("a",{href:"https://devblogs.microsoft.com/dotnet/performance-improvements-in-aspnet-core-6"},".NET are becoming even more optimized"),t(", a setup with just a server or two can manage larger loads and use cases.")]),e("p",null,[t("Our research and evaluations ended up right back at "),e("a",{href:"https://www.hetzner.com/cloud"},"Hetzner but this time with their Cloud offering"),t(". For less than "),e("strong",null,"$15 USD"),t(" per month, you can get a "),e("strong",null,"4 vCPU, 8GB RAM, 160GB storage and 20TB"),t(" of data transfer "),e("strong",null,"hosted in the US"),t(".")]),e("p",null,"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."),e("h2",null,"Litestream and SQLite"),e("p",null,[t("Our demo applications use "),e("a",{href:"https://www.sqlite.org"},"SQLite"),t(" as a simple way to host the database storage and application together, taking advantage of SQLite’s embedded nature. We were also testing out "),e("a",{href:"https://litestream.io"},"Litestream"),t(" as a possible solution to the lack of data backups and safety when using SQLite for more production like workloads.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("a",{href:"https://litestream.io"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/litestream.svg",alt:""})]),e("div",{class:"text-gray-500 text-center"},"litestream.io")])]),e("p",null,[t("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. "),e("a",{href:"https://docs.servicestack.net/ormlite/litestream"},"We created several templates to make this easier"),t(" and provide a way to bake in automated disaster recovery using Litestream when used with GitHub Actions and our SSH with Docker Compose deployment.")]),e("p",null,"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."),e("p",null,"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."),e("p",null,"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."),e("p",null,"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."),e("h2",null,"The Test"),e("p",null,[t("To get a clearer idea how each of these hosting options perform with a fairly modest workload, we used a "),e("a",{href:"https://gatling.io"},"Gatling"),t(" test to simulate a user logging into our sample Bookings application, browsing around and creating a booking.")]),e("p",null,"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."),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/aws-gatling-result.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"AWS Gatling Result.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/azure-gatling-result.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Azure Gatling Result.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/hetzner-gatling-result.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Hetzner Gatling Result.")]),e("p",null,[t("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 "),e("a",{href:"https://www.sqlite.org/whentouse.html#:~:text=An%20SQLite%20database%20is%20limited,to%20something%20less%20than%20this."},"really only limited by its single writer design"),t(". 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.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/litestream-costs.svg",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Previous test result price comparison without AWS using Provisioned IOPS.")]),e("p",null,[t("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 "),e("a",{href:"https://docs.servicestack.net/ormlite"},"OrmLite"),t(", switching to another database provider can be migrated if and when the traditional offerings like Postgres are needed.")]),e("h2",null,"The Setups"),e("p",null,"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."),e("p",null,[t("Without provisioned IOPs, it drops to around "),e("strong",null,"$132/month"),t(" as an estimated cost. The "),e("strong",null,"$300/month"),t(" 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.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/aws-rds-with-provisioned-iops.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"AWS RDS now defaults to provisioned IOPs for a Production setup, drastically increasing costs.")]),e("table",null,[e("thead",null,[e("tr",null,[e("th"),e("th",null,"AWS (DB)"),e("th",null,"AWS (App)"),e("th",null,"Azure (DB)"),e("th",null,"Azure (App)"),e("th",null,"DigitalOcean"),e("th",null,"Hetzner Cloud")])]),e("tbody",null,[e("tr",null,[e("td",null,"vCPU"),e("td",null,"2"),e("td",null,"2"),e("td",null,"4"),e("td",null,"2"),e("td",null,"4"),e("td",null,"4")]),e("tr",null,[e("td",null,"Memory (GB)"),e("td",null,"8"),e("td",null,"4"),e("td",null,"10"),e("td",null,"8"),e("td",null,"8"),e("td",null,"8")]),e("tr",null,[e("td",null,"Storage (GB)"),e("td",null,"100 (provisioned)"),e("td",null,"16"),e("td",null,"32"),e("td",null,"30"),e("td",null,"160"),e("td",null,"160")]),e("tr",null,[e("td",null,"Cost"),e("td",null,"$442"),e("td",null,"$34"),e("td",null,"$373"),e("td",null,"$70"),e("td",null,"$48"),e("td",null,"$15")])])]),e("p",null,"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%."),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/azure-db-cpu-during-test.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Azure SQL database without tuning performs poorly for cost, likely due to lack of indexes")]),e("table",null,[e("thead",null,[e("tr",null,[e("th"),e("th",null,"AWS (DB)"),e("th",null,"AWS (App)"),e("th",null,"Azure (DB)"),e("th",null,"Azure (App)"),e("th",null,"Hetzner Cloud")])]),e("tbody",null,[e("tr",null,[e("td",null,"Max CPU %"),e("td",null,"8"),e("td",null,"35"),e("td",null,"25"),e("td",null,"45"),e("td",null,"40")])])]),e("p",null,"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."),e("h2",null,"Hetzner Cloud"),e("p",null,"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."),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/hetzner-cloud-buy.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Hetzner Cloud Pricing.")]),e("h3",null,"Creating a new instance is fast"),e("p",null,[t("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 "),e("strong",null,"20 seconds"),t(".")]),e("h3",null,"Live Graphs"),e("p",null,"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."),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/hetzner-cloud-live-graphs.gif",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Live monitoring updates every 3-5 seconds.")]),e("p",null,"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."),e("h3",null,"Price"),e("p",null,[t("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 "),e("strong",null,"pricing scales with hardware resources"),t(". Products like "),e("strong",null,"Provisioned IOPs"),t(" 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.")]),e("h3",null,"Transfer costs"),e("p",null,[t("It’s been long known that one of the ways large cloud providers keep customers in their network is by charging "),e("a",{href:"https://aws.amazon.com/blogs/architecture/overview-of-data-transfer-costs-for-common-architectures"},"excessively large and complex data egress costs"),t(". Something attractive about simplified pricing from Hetzner Cloud (and DigitalOcean to a lesser degree) is the included data transfer of 20TB a month.")]),e("p",null,[t("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 "),e("strong",null,"20TB"),t(" you get for free with a "),e("strong",null,"$15 server"),t(", would cost "),e("strong",null,"$1,791 just for data"),t(" when coming from AWS. Azure pricing also confusing, and in some ways more expensive.")]),e("h2",null,"Defaults are powerful"),e("p",null,"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."),e("p",null,[t("Performing disk speed check using the Linux utility "),e("code",null,"fio"),t(" 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.")]),e("p",null,"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."),e("table",null,[e("thead",null,[e("tr",null,[e("th"),e("th",null,"Read IOPS"),e("th",null,"Write IOPs"),e("th",null,"Read MBs"),e("th",null,"Write MBs")])]),e("tbody",null,[e("tr",null,[e("td",null,"AWS"),e("td",null,"2.3k"),e("td",null,"0.8k"),e("td",null,"9.2 MB/s"),e("td",null,"3.1 MB/s")]),e("tr",null,[e("td",null,"Azure"),e("td",null,"3.0k"),e("td",null,"1.0k"),e("td",null,"12.5 MB/s"),e("td",null,"4.2 MB/s")]),e("tr",null,[e("td",null,"DigitalOcean"),e("td",null,"35.2k"),e("td",null,"11.8k"),e("td",null,"144 MB/s"),e("td",null,"48.2 MB/s")]),e("tr",null,[e("td",null,"Hetzner Cloud"),e("td",null,"50.5k"),e("td",null,"16.9k"),e("td",null,"207 MB/s"),e("td",null,"69.2 MB/s")])])]),e("p",null,[t("All tests used the following "),e("code",null,"fio"),t(" command.")]),e("pre",{class:"language-shell"},[e("code",{class:"language-shell"},[t("fio "),e("span",{class:"token parameter variable"},"--randrepeat"),e("span",{class:"token operator"},"="),e("span",{class:"token number"},"1"),t(),e("span",{class:"token parameter variable"},"--ioengine"),e("span",{class:"token operator"},"="),t("libaio "),e("span",{class:"token parameter variable"},"--direct"),e("span",{class:"token operator"},"="),e("span",{class:"token number"},"1"),t(),e("span",{class:"token parameter variable"},"--gtod_reduce"),e("span",{class:"token operator"},"="),e("span",{class:"token number"},"1"),t(),e("span",{class:"token parameter variable"},"--name"),e("span",{class:"token operator"},"="),t("test "),e("span",{class:"token punctuation"},"\\"),t(` +import{_ as n}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{i as r,o as i,h as l,w as u,a as e,e as t}from"./index-DWUkzBt0.js";const d=e("div",{class:"markdown-body"},[e("p",null,[t("At "),e("a",{href:"./"},"ServiceStack"),t(", 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.")]),e("p",null,[t("Thankfully, more and more companies are providing simpler offerings for hosting needs, and AWS themselves launched "),e("a",{href:"https://aws.amazon.com/lightsail"},"Lightsail"),t(" as their answer to market demands for simple hosting options that package everything you need for basic hosting.")]),e("p",null,"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."),e("h2",null,"Looking at different US offerings"),e("p",null,[t("Something we wanted to do was to host our "),e("a",{href:"https://github.com/ServiceStackApps/LiveDemos"},"live demo applications"),t(" on a US based host. We were using "),e("a",{href:"https://www.hetzner.com/dedicated-rootserver"},"Hetzner dedicated servers"),t(" in the past for non-latency sensitive use cases like our build server and "),e("a",{href:"https://gist.cafe"},"Gist.Cafe (our interactive playground for multiple platforms)"),t(" but we also wanted our demo applications to be snappy for US users.")]),e("p",null,[e("a",{href:"https://www.digitalocean.com/pricing"},"DigitalOcean"),t(" provides "),e("a",{href:"https://www.digitalocean.com/pricing/droplets"},"“Droplets”"),t(" 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.")]),e("p",null,[t("For deployment, "),e("a",{href:"https://docs.servicestack.net/do-github-action-mix-deployment"},"we also like to keep things as simple as we can, whilst keeping portability"),t(". Since all our projects are public and on GitHub, we use "),e("a",{href:"https://docs.servicestack.net/do-github-action-mix-deployment#github-repository-setup"},"GitHub Actions"),t(" heavily along with a pattern that deploys our applications using Docker Compose via SSH. Each application runs in its own container behind an "),e("a",{href:"https://docs.servicestack.net/do-github-action-mix-deployment#get-nginx-reverse-proxy-and-letsencrypt-companion-running"},"NGINX proxy"),t(" with a side car that handles renewing LetsEncrypt certificates. Below is an example of this pattern with Blazor and Litestream.")]),e("iframe",{class:"youtube",src:"https://www.youtube.com/embed/fY50dWszpw4",frameborder:"0",allow:"autoplay; encrypted-media",allowfullscreen:""}),e("p",null,"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."),e("p",null,[t("A minute or so later, the application is back running again. Since their were 20+ of these repositories we took advantage of the "),e("a",{href:"https://cli.github.com/manual/gh_secret_set"},"GitHub Organization Secrets"),t(" so we only needed to update values in one place, and "),e("a",{href:"https://cli.github.com/manual/gh_workflow_run"},"running the workflows again"),t(" can also be done programmatically through the GitHub CLI.")]),e("h2",null,"DigitalOcean Price Increase"),e("p",null,[t("In June of 2022, we got a notification that "),e("a",{href:"https://www.digitalocean.com/try/new-pricing"},"prices for droplets would be increasing"),t(", and for our droplet it would be going from "),e("strong",null,"$40 to $48"),t(". While this is a small amount of money, it prompted us to have a wider look into this market.")]),e("p",null,"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."),e("p",null,[t("Not everyone builds massively distributed systems, and as hardware performance increases, and platforms like "),e("a",{href:"https://devblogs.microsoft.com/dotnet/performance-improvements-in-aspnet-core-6"},".NET are becoming even more optimized"),t(", a setup with just a server or two can manage larger loads and use cases.")]),e("p",null,[t("Our research and evaluations ended up right back at "),e("a",{href:"https://www.hetzner.com/cloud"},"Hetzner but this time with their Cloud offering"),t(". For less than "),e("strong",null,"$15 USD"),t(" per month, you can get a "),e("strong",null,"4 vCPU, 8GB RAM, 160GB storage and 20TB"),t(" of data transfer "),e("strong",null,"hosted in the US"),t(".")]),e("p",null,"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."),e("h2",null,"Litestream and SQLite"),e("p",null,[t("Our demo applications use "),e("a",{href:"https://www.sqlite.org"},"SQLite"),t(" as a simple way to host the database storage and application together, taking advantage of SQLite’s embedded nature. We were also testing out "),e("a",{href:"https://litestream.io"},"Litestream"),t(" as a possible solution to the lack of data backups and safety when using SQLite for more production like workloads.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("a",{href:"https://litestream.io"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/litestream.svg",alt:""})]),e("div",{class:"text-gray-500 text-center"},"litestream.io")])]),e("p",null,[t("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. "),e("a",{href:"https://docs.servicestack.net/ormlite/litestream"},"We created several templates to make this easier"),t(" and provide a way to bake in automated disaster recovery using Litestream when used with GitHub Actions and our SSH with Docker Compose deployment.")]),e("p",null,"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."),e("p",null,"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."),e("p",null,"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."),e("p",null,"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."),e("h2",null,"The Test"),e("p",null,[t("To get a clearer idea how each of these hosting options perform with a fairly modest workload, we used a "),e("a",{href:"https://gatling.io"},"Gatling"),t(" test to simulate a user logging into our sample Bookings application, browsing around and creating a booking.")]),e("p",null,"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."),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/aws-gatling-result.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"AWS Gatling Result.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/azure-gatling-result.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Azure Gatling Result.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/hetzner-gatling-result.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Hetzner Gatling Result.")]),e("p",null,[t("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 "),e("a",{href:"https://www.sqlite.org/whentouse.html#:~:text=An%20SQLite%20database%20is%20limited,to%20something%20less%20than%20this."},"really only limited by its single writer design"),t(". 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.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/litestream-costs.svg",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Previous test result price comparison without AWS using Provisioned IOPS.")]),e("p",null,[t("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 "),e("a",{href:"https://docs.servicestack.net/ormlite"},"OrmLite"),t(", switching to another database provider can be migrated if and when the traditional offerings like Postgres are needed.")]),e("h2",null,"The Setups"),e("p",null,"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."),e("p",null,[t("Without provisioned IOPs, it drops to around "),e("strong",null,"$132/month"),t(" as an estimated cost. The "),e("strong",null,"$300/month"),t(" 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.")]),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/aws-rds-with-provisioned-iops.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"AWS RDS now defaults to provisioned IOPs for a Production setup, drastically increasing costs.")]),e("table",null,[e("thead",null,[e("tr",null,[e("th"),e("th",null,"AWS (DB)"),e("th",null,"AWS (App)"),e("th",null,"Azure (DB)"),e("th",null,"Azure (App)"),e("th",null,"DigitalOcean"),e("th",null,"Hetzner Cloud")])]),e("tbody",null,[e("tr",null,[e("td",null,"vCPU"),e("td",null,"2"),e("td",null,"2"),e("td",null,"4"),e("td",null,"2"),e("td",null,"4"),e("td",null,"4")]),e("tr",null,[e("td",null,"Memory (GB)"),e("td",null,"8"),e("td",null,"4"),e("td",null,"10"),e("td",null,"8"),e("td",null,"8"),e("td",null,"8")]),e("tr",null,[e("td",null,"Storage (GB)"),e("td",null,"100 (provisioned)"),e("td",null,"16"),e("td",null,"32"),e("td",null,"30"),e("td",null,"160"),e("td",null,"160")]),e("tr",null,[e("td",null,"Cost"),e("td",null,"$442"),e("td",null,"$34"),e("td",null,"$373"),e("td",null,"$70"),e("td",null,"$48"),e("td",null,"$15")])])]),e("p",null,"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%."),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/azure-db-cpu-during-test.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Azure SQL database without tuning performs poorly for cost, likely due to lack of indexes")]),e("table",null,[e("thead",null,[e("tr",null,[e("th"),e("th",null,"AWS (DB)"),e("th",null,"AWS (App)"),e("th",null,"Azure (DB)"),e("th",null,"Azure (App)"),e("th",null,"Hetzner Cloud")])]),e("tbody",null,[e("tr",null,[e("td",null,"Max CPU %"),e("td",null,"8"),e("td",null,"35"),e("td",null,"25"),e("td",null,"45"),e("td",null,"40")])])]),e("p",null,"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."),e("h2",null,"Hetzner Cloud"),e("p",null,"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."),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/hetzner-cloud-buy.png",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Hetzner Cloud Pricing.")]),e("h3",null,"Creating a new instance is fast"),e("p",null,[t("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 "),e("strong",null,"20 seconds"),t(".")]),e("h3",null,"Live Graphs"),e("p",null,"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."),e("div",{class:"mx-auto mt-4 mb-4"},[e("div",{class:"inline-flex justify-center w-full"},[e("img",{src:"https://servicestack.net/img/posts/hetzner-cloud/hetzner-cloud-live-graphs.gif",alt:""})]),e("div",{class:"text-gray-500 text-center"},"Live monitoring updates every 3-5 seconds.")]),e("p",null,"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."),e("h3",null,"Price"),e("p",null,[t("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 "),e("strong",null,"pricing scales with hardware resources"),t(". Products like "),e("strong",null,"Provisioned IOPs"),t(" 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.")]),e("h3",null,"Transfer costs"),e("p",null,[t("It’s been long known that one of the ways large cloud providers keep customers in their network is by charging "),e("a",{href:"https://aws.amazon.com/blogs/architecture/overview-of-data-transfer-costs-for-common-architectures"},"excessively large and complex data egress costs"),t(". Something attractive about simplified pricing from Hetzner Cloud (and DigitalOcean to a lesser degree) is the included data transfer of 20TB a month.")]),e("p",null,[t("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 "),e("strong",null,"20TB"),t(" you get for free with a "),e("strong",null,"$15 server"),t(", would cost "),e("strong",null,"$1,791 just for data"),t(" when coming from AWS. Azure pricing also confusing, and in some ways more expensive.")]),e("h2",null,"Defaults are powerful"),e("p",null,"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."),e("p",null,[t("Performing disk speed check using the Linux utility "),e("code",null,"fio"),t(" 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.")]),e("p",null,"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."),e("table",null,[e("thead",null,[e("tr",null,[e("th"),e("th",null,"Read IOPS"),e("th",null,"Write IOPs"),e("th",null,"Read MBs"),e("th",null,"Write MBs")])]),e("tbody",null,[e("tr",null,[e("td",null,"AWS"),e("td",null,"2.3k"),e("td",null,"0.8k"),e("td",null,"9.2 MB/s"),e("td",null,"3.1 MB/s")]),e("tr",null,[e("td",null,"Azure"),e("td",null,"3.0k"),e("td",null,"1.0k"),e("td",null,"12.5 MB/s"),e("td",null,"4.2 MB/s")]),e("tr",null,[e("td",null,"DigitalOcean"),e("td",null,"35.2k"),e("td",null,"11.8k"),e("td",null,"144 MB/s"),e("td",null,"48.2 MB/s")]),e("tr",null,[e("td",null,"Hetzner Cloud"),e("td",null,"50.5k"),e("td",null,"16.9k"),e("td",null,"207 MB/s"),e("td",null,"69.2 MB/s")])])]),e("p",null,[t("All tests used the following "),e("code",null,"fio"),t(" command.")]),e("pre",{class:"language-shell"},[e("code",{class:"language-shell"},[t("fio "),e("span",{class:"token parameter variable"},"--randrepeat"),e("span",{class:"token operator"},"="),e("span",{class:"token number"},"1"),t(),e("span",{class:"token parameter variable"},"--ioengine"),e("span",{class:"token operator"},"="),t("libaio "),e("span",{class:"token parameter variable"},"--direct"),e("span",{class:"token operator"},"="),e("span",{class:"token number"},"1"),t(),e("span",{class:"token parameter variable"},"--gtod_reduce"),e("span",{class:"token operator"},"="),e("span",{class:"token number"},"1"),t(),e("span",{class:"token parameter variable"},"--name"),e("span",{class:"token operator"},"="),t("test "),e("span",{class:"token punctuation"},"\\"),t(` `),e("span",{class:"token parameter variable"},"--filename"),e("span",{class:"token operator"},"="),t("test "),e("span",{class:"token parameter variable"},"--bs"),e("span",{class:"token operator"},"="),t("4k "),e("span",{class:"token parameter variable"},"--iodepth"),e("span",{class:"token operator"},"="),e("span",{class:"token number"},"64"),t(),e("span",{class:"token parameter variable"},"--size"),e("span",{class:"token operator"},"="),t("4G "),e("span",{class:"token parameter variable"},"--readwrite"),e("span",{class:"token operator"},"="),t("randrw "),e("span",{class:"token parameter variable"},"--rwmixread"),e("span",{class:"token operator"},"="),e("span",{class:"token number"},"75"),t(` `)])]),e("h2",null,"SQLite"),e("p",null,"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."),e("p",null,"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."),e("p",null,[t("Other solutions for Postgres like "),e("code",null,"pgbackrest"),t(" 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.")]),e("h2",null,"Hetzner Cloud is hard to beat on price"),e("p",null,"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."),e("p",null,"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.")],-1),w="In pursuit of the best value US cloud provider",v="We've been using AWS at ServiceStack for 10+ years, it's served us well but suffers from complex & expensive pricing",y="Brandon Foley",b=["dev","hosting","devops"],k="https://images.unsplash.com/photo-1451187580459-43490279c0fa?crop=entropy&fit=crop&h=1000&w=2000",S=[{property:"og:title",content:"In pursuit of the best value US cloud provider"},{name:"twitter:title",content:"In pursuit of the best value US cloud provider"},{property:"og:image",content:"https://images.unsplash.com/photo-1451187580459-43490279c0fa?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1451187580459-43490279c0fa?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}],x={__name:"2022-09-06_hetzner-cloud",setup(c,{expose:a}){const s={title:"In pursuit of the best value US cloud provider",summary:"We've been using AWS at ServiceStack for 10+ years, it's served us well but suffers from complex & expensive pricing",author:"Brandon Foley",tags:["dev","hosting","devops"],image:"https://images.unsplash.com/photo-1451187580459-43490279c0fa?crop=entropy&fit=crop&h=1000&w=2000",meta:[{property:"og:title",content:"In pursuit of the best value US cloud provider"},{name:"twitter:title",content:"In pursuit of the best value US cloud provider"},{property:"og:image",content:"https://images.unsplash.com/photo-1451187580459-43490279c0fa?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1451187580459-43490279c0fa?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]};return a({frontmatter:s}),r({title:"In pursuit of the best value US cloud provider",meta:[{property:"og:title",content:"In pursuit of the best value US cloud provider"},{name:"twitter:title",content:"In pursuit of the best value US cloud provider"},{property:"og:image",content:"https://images.unsplash.com/photo-1451187580459-43490279c0fa?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1451187580459-43490279c0fa?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]}),(h,m)=>{const o=n;return i(),l(o,{frontmatter:s},{default:u(()=>[d]),_:1})}}};export{y as author,x as default,k as image,S as meta,v as summary,b as tags,w as title}; diff --git a/assets/2022-12-31_typography-CqjuP9zk.js b/assets/2022-12-31_typography-BhdtW7rK.js similarity index 98% rename from assets/2022-12-31_typography-CqjuP9zk.js rename to assets/2022-12-31_typography-BhdtW7rK.js index 5d5471d..2f8451c 100644 --- a/assets/2022-12-31_typography-CqjuP9zk.js +++ b/assets/2022-12-31_typography-BhdtW7rK.js @@ -1,4 +1,4 @@ -import{_ as s}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{g as l,o as i,h as r,w as p,a as t,e}from"./index-BXwhoUEp.js";const c=t("div",{class:"markdown-body"},[t("p",{class:"lead"}," 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. "),t("p",null,[e("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 "),t("em",null,"really are"),e(" 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.")]),t("p",null,"We get lots of complaints about it actually, with people regularly asking us things like:"),t("blockquote",null,[t("p",null,[e("Why is Tailwind removing the default styles on my "),t("code",null,"h1"),e(" elements? How do I disable this? What do you mean I lose all the other base styles too?")])]),t("p",null,[e("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 "),t("code",null,"p"),e(" 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 "),t("em",null,"awesome"),e(", not awful.")]),t("p",null,[e("The "),t("code",null,"@tailwindcss/typography"),e(" plugin is our attempt to give you what you "),t("em",null,"actually"),e(" want, without any of the downsides of doing something stupid like disabling our base styles.")]),t("p",null,[e("It adds a new "),t("code",null,"prose"),e(" class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document:")]),t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),e("article")]),e(),t("span",{class:"token attr-name"},"class"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),e("prose"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),e(` +import{_ as s}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{i as l,o as i,h as r,w as p,a as t,e}from"./index-DWUkzBt0.js";const c=t("div",{class:"markdown-body"},[t("p",{class:"lead"}," 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. "),t("p",null,[e("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 "),t("em",null,"really are"),e(" 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.")]),t("p",null,"We get lots of complaints about it actually, with people regularly asking us things like:"),t("blockquote",null,[t("p",null,[e("Why is Tailwind removing the default styles on my "),t("code",null,"h1"),e(" elements? How do I disable this? What do you mean I lose all the other base styles too?")])]),t("p",null,[e("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 "),t("code",null,"p"),e(" 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 "),t("em",null,"awesome"),e(", not awful.")]),t("p",null,[e("The "),t("code",null,"@tailwindcss/typography"),e(" plugin is our attempt to give you what you "),t("em",null,"actually"),e(" want, without any of the downsides of doing something stupid like disabling our base styles.")]),t("p",null,[e("It adds a new "),t("code",null,"prose"),e(" class that you can slap on any block of vanilla HTML content and turn it into a beautiful, well-formatted document:")]),t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),e("article")]),e(),t("span",{class:"token attr-name"},"class"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),e("prose"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),e(` `),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),e("h1")]),t("span",{class:"token punctuation"},">")]),e("Garlic bread with cheese: What the science tells us"),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),e(` `),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),e("p")]),t("span",{class:"token punctuation"},">")]),e(` For years parents have espoused the health benefits of eating garlic bread with cheese to their diff --git a/assets/2023-01-01_deploy-4CZE5f1j.js b/assets/2023-01-01_deploy-BP7kyxZZ.js similarity index 98% rename from assets/2023-01-01_deploy-4CZE5f1j.js rename to assets/2023-01-01_deploy-BP7kyxZZ.js index 43ccbd9..6607db0 100644 --- a/assets/2023-01-01_deploy-4CZE5f1j.js +++ b/assets/2023-01-01_deploy-BP7kyxZZ.js @@ -1,4 +1,4 @@ -import{_ as o}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{g as l,o as c,h as i,w as r,a as e,e as t}from"./index-BXwhoUEp.js";const p=e("div",{class:"markdown-body"},[e("h1",null,"ServiceStack GitHub Action Deployments"),e("p",null,[t("The "),e("a",{href:"https://github.com/NetCoreTemplates/razor-tailwind/blob/main/.github/workflows/release.yml"},"release.yml"),t(" in this template enables GitHub Actions CI deployment to a dedicated server with SSH access.")]),e("h2",null,"Overview"),e("p",null,[e("code",null,"release.yml"),t(" 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 "),e("code",null,"ghcr.io"),t(" docker registry when a GitHub Release is created.")]),e("p",null,[t("GitHub Actions specified in "),e("code",null,"release.yml"),t(" then copy files remotely via scp and use "),e("code",null,"docker-compose"),t(" to run the app remotely via SSH.")]),e("h2",null,[t("What’s the process of "),e("code",null,"release.yml"),t("?")]),e("p",null,[e("img",{src:"https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/release-ghr-vanilla-diagram.png",alt:""})]),e("h2",null,"Deployment server setup"),e("p",null,"To get this working, a server needs to be setup with the following:"),e("ul",null,[e("li",null,"SSH access"),e("li",null,"docker"),e("li",null,"docker-compose"),e("li",null,"ports 443 and 80 for web access of your hosted application")]),e("p",null,[t("This can be your own server or any cloud hosted server like Digital Ocean, AWS, Azure etc. We use "),e("a",{href:"http://cloud.hetzner.com/"},"Hetzner Cloud"),t(" to deploy all ServiceStack’s "),e("a",{href:"https://github.com/NetCoreTemplates/"},"GitHub Project Templates"),t(" as it was the "),e("a",{href:"https://servicestack.net/blog/finding-best-us-value-cloud-provider"},"best value US cloud provider"),t(" we’ve found.")]),e("p",null,[t("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 "),e("em",null,"private"),t(" 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.")]),e("p",null,[t("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, "),e("code",null,"nginx-proxy-compose.yml"),t(". This docker-compose file is ready to run and can be copied to the deployment server.")]),e("p",null,[t("For example, once copied to remote "),e("code",null,"~/nginx-proxy-compose.yml"),t(", the following command can be run on the remote server.")]),e("pre",null,[e("code",null,`docker-compose -f ~/nginx-proxy-compose.yml up -d +import{_ as o}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{i as l,o as c,h as i,w as r,a as e,e as t}from"./index-DWUkzBt0.js";const p=e("div",{class:"markdown-body"},[e("h1",null,"ServiceStack GitHub Action Deployments"),e("p",null,[t("The "),e("a",{href:"https://github.com/NetCoreTemplates/razor-tailwind/blob/main/.github/workflows/release.yml"},"release.yml"),t(" in this template enables GitHub Actions CI deployment to a dedicated server with SSH access.")]),e("h2",null,"Overview"),e("p",null,[e("code",null,"release.yml"),t(" 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 "),e("code",null,"ghcr.io"),t(" docker registry when a GitHub Release is created.")]),e("p",null,[t("GitHub Actions specified in "),e("code",null,"release.yml"),t(" then copy files remotely via scp and use "),e("code",null,"docker-compose"),t(" to run the app remotely via SSH.")]),e("h2",null,[t("What’s the process of "),e("code",null,"release.yml"),t("?")]),e("p",null,[e("img",{src:"https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/mix/release-ghr-vanilla-diagram.png",alt:""})]),e("h2",null,"Deployment server setup"),e("p",null,"To get this working, a server needs to be setup with the following:"),e("ul",null,[e("li",null,"SSH access"),e("li",null,"docker"),e("li",null,"docker-compose"),e("li",null,"ports 443 and 80 for web access of your hosted application")]),e("p",null,[t("This can be your own server or any cloud hosted server like Digital Ocean, AWS, Azure etc. We use "),e("a",{href:"http://cloud.hetzner.com/"},"Hetzner Cloud"),t(" to deploy all ServiceStack’s "),e("a",{href:"https://github.com/NetCoreTemplates/"},"GitHub Project Templates"),t(" as it was the "),e("a",{href:"https://servicestack.net/blog/finding-best-us-value-cloud-provider"},"best value US cloud provider"),t(" we’ve found.")]),e("p",null,[t("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 "),e("em",null,"private"),t(" 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.")]),e("p",null,[t("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, "),e("code",null,"nginx-proxy-compose.yml"),t(". This docker-compose file is ready to run and can be copied to the deployment server.")]),e("p",null,[t("For example, once copied to remote "),e("code",null,"~/nginx-proxy-compose.yml"),t(", the following command can be run on the remote server.")]),e("pre",null,[e("code",null,`docker-compose -f ~/nginx-proxy-compose.yml up -d `)]),e("p",null,"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."),e("h3",null,"GitHub Actions secrets"),e("p",null,[t("The "),e("code",null,"release.yml"),t(" uses the following secrets.")]),e("table",null,[e("thead",null,[e("tr",null,[e("th",null,"Required Secrets"),e("th",null,"Description")])]),e("tbody",null,[e("tr",null,[e("td",null,[e("code",null,"DEPLOY_HOST")]),e("td",null,"Hostname used to SSH deploy .NET App to, this can either be an IP address or subdomain with A record pointing to the server")]),e("tr",null,[e("td",null,[e("code",null,"DEPLOY_USERNAME")]),e("td",null,[t("Username to log in with via SSH e.g, "),e("strong",null,"ubuntu"),t(", "),e("strong",null,"ec2-user"),t(", "),e("strong",null,"root")])]),e("tr",null,[e("td",null,[e("code",null,"DEPLOY_KEY")]),e("td",null,"SSH private key used to remotely access deploy .NET App")]),e("tr",null,[e("td",null,[e("code",null,"LETSENCRYPT_EMAIL")]),e("td",null,"Email required for Let’s Encrypt automated TLS certificates")])])]),e("p",null,[t("These secrets can use the "),e("a",{href:"https://cli.github.com/manual/gh_secret_set"},"GitHub CLI"),t(" for ease of creation. Eg, using the GitHub CLI the following can be set.")]),e("pre",{class:"language-bash"},[e("code",{class:"language-bash"},[t("gh secret "),e("span",{class:"token builtin class-name"},"set"),t(" DEPLOY_HOST -b"),e("span",{class:"token string"},'""'),t(` gh secret `),e("span",{class:"token builtin class-name"},"set"),t(" DEPLOY_USERNAME -b"),e("span",{class:"token string"},'""'),t(` gh secret `),e("span",{class:"token builtin class-name"},"set"),t(" DEPLOY_KEY "),e("span",{class:"token operator"},"<"),t(" key.pem "),e("span",{class:"token comment"},"# DEPLOY_KEY"),t(` diff --git a/assets/2023-01-10_vs-B3e8pVg7.js b/assets/2023-01-10_vs-CjcgGp8Z.js similarity index 98% rename from assets/2023-01-10_vs-B3e8pVg7.js rename to assets/2023-01-10_vs-CjcgGp8Z.js index bf46dae..901b5ee 100644 --- a/assets/2023-01-10_vs-B3e8pVg7.js +++ b/assets/2023-01-10_vs-CjcgGp8Z.js @@ -1,4 +1,4 @@ -import{_ as i}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{g as r,o,h as c,w as p,a as t,c as a,e}from"./index-BXwhoUEp.js";const u={class:"markdown-body"},d=t("p",null,[e("A popular alternative development environment to our preferred "),t("a",{href:"rider"},"JetBrains Rider"),e(" 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.")],-1),h=t("p",null,"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.",-1),m=t("h3",null,"Developing with just VS Code",-1),g={href:"https://visualstudio.microsoft.com/",title:"VS Code",class:"sm:float-left mr-8"},_={class:"w-24 h-24",style:{"margin-top":"1rem"},xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 256 254"},f=t("defs",null,[t("linearGradient",{id:"logosVisualStudioCode0",x1:"50%",x2:"50%",y1:"0%",y2:"100%"},[t("stop",{offset:"0%","stop-color":"#FFF"}),t("stop",{offset:"100%","stop-color":"#FFF","stop-opacity":"0"})]),t("path",{id:"logosVisualStudioCode1",d:"M180.828 252.605a15.872 15.872 0 0 0 12.65-.486l52.501-25.262a15.94 15.94 0 0 0 9.025-14.364V41.197a15.939 15.939 0 0 0-9.025-14.363l-52.5-25.263a15.877 15.877 0 0 0-18.115 3.084L74.857 96.35l-43.78-33.232a10.614 10.614 0 0 0-13.56.603L3.476 76.494c-4.63 4.211-4.635 11.495-.012 15.713l37.967 34.638l-37.967 34.637c-4.623 4.219-4.618 11.502.012 15.714l14.041 12.772a10.614 10.614 0 0 0 13.56.604l43.78-33.233l100.507 91.695a15.853 15.853 0 0 0 5.464 3.571Zm10.464-183.649l-76.262 57.889l76.262 57.888V68.956Z"})],-1),w=t("mask",{id:"logosVisualStudioCode2",fill:"#fff"},[t("use",{href:"#logosVisualStudioCode1"})],-1),v=t("path",{fill:"#0065A9",d:"M246.135 26.873L193.593 1.575a15.885 15.885 0 0 0-18.123 3.08L3.466 161.482c-4.626 4.219-4.62 11.502.012 15.714l14.05 12.772a10.625 10.625 0 0 0 13.569.604L238.229 33.436c6.949-5.271 16.93-.315 16.93 8.407v-.61a15.938 15.938 0 0 0-9.024-14.36Z",mask:"url(#logosVisualStudioCode2)"},null,-1),y=t("path",{fill:"#007ACC",d:"m246.135 226.816l-52.542 25.298a15.887 15.887 0 0 1-18.123-3.08L3.466 92.207c-4.626-4.218-4.62-11.502.012-15.713l14.05-12.773a10.625 10.625 0 0 1 13.569-.603l207.132 157.135c6.949 5.271 16.93.315 16.93-8.408v.611a15.939 15.939 0 0 1-9.024 14.36Z",mask:"url(#logosVisualStudioCode2)"},null,-1),V=t("path",{fill:"#1F9CF0",d:"M193.428 252.134a15.892 15.892 0 0 1-18.125-3.083c5.881 5.88 15.938 1.715 15.938-6.603V11.273c0-8.318-10.057-12.483-15.938-6.602a15.892 15.892 0 0 1 18.125-3.084l52.533 25.263a15.937 15.937 0 0 1 9.03 14.363V212.51c0 6.125-3.51 11.709-9.03 14.363l-52.533 25.262Z",mask:"url(#logosVisualStudioCode2)"},null,-1),S=t("path",{fill:"url(#logosVisualStudioCode0)","fill-opacity":".25",d:"M180.828 252.605a15.874 15.874 0 0 0 12.65-.486l52.5-25.263a15.938 15.938 0 0 0 9.026-14.363V41.197a15.939 15.939 0 0 0-9.025-14.363L193.477 1.57a15.877 15.877 0 0 0-18.114 3.084L74.857 96.35l-43.78-33.232a10.614 10.614 0 0 0-13.56.603L3.476 76.494c-4.63 4.211-4.635 11.495-.012 15.713l37.967 34.638l-37.967 34.637c-4.623 4.219-4.618 11.502.012 15.714l14.041 12.772a10.614 10.614 0 0 0 13.56.604l43.78-33.233l100.506 91.695a15.857 15.857 0 0 0 5.465 3.571Zm10.464-183.65l-76.262 57.89l76.262 57.888V68.956Z",mask:"url(#logosVisualStudioCode2)"},null,-1),k=[f,w,v,y,V,S],C=t("p",null,[e("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 "),t("a",{href:"https://v3.vuejs.org/api/sfc-tooling.html#ide-support"},"Vue recommends themselves"),e(", to be used together with the "),t("a",{href:"https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar"},"Volar extension"),e(".")],-1),b=t("p",null,[e("VSCode’s "),t("a",{href:"https://code.visualstudio.com/docs/editor/integrated-terminal"},"Integrated Terminal"),e(" has great multi-terminal support you can toggle between the editor and terminal with "),t("code",null,"Ctrl+"),e(" or open a new Terminal Window with "),t("code",null,"Ctrl+Shift+`"),e(" to run Tailwind with:")],-1),A=t("pre",{class:"language-bash"},[t("code",{class:"language-bash"},[e("$ "),t("span",{class:"token function"},"npm"),e(` run ui:dev +import{_ as i}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{i as r,o,h as c,w as p,a as t,c as a,e}from"./index-DWUkzBt0.js";const u={class:"markdown-body"},d=t("p",null,[e("A popular alternative development environment to our preferred "),t("a",{href:"rider"},"JetBrains Rider"),e(" 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.")],-1),h=t("p",null,"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.",-1),m=t("h3",null,"Developing with just VS Code",-1),g={href:"https://visualstudio.microsoft.com/",title:"VS Code",class:"sm:float-left mr-8"},_={class:"w-24 h-24",style:{"margin-top":"1rem"},xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 256 254"},f=t("defs",null,[t("linearGradient",{id:"logosVisualStudioCode0",x1:"50%",x2:"50%",y1:"0%",y2:"100%"},[t("stop",{offset:"0%","stop-color":"#FFF"}),t("stop",{offset:"100%","stop-color":"#FFF","stop-opacity":"0"})]),t("path",{id:"logosVisualStudioCode1",d:"M180.828 252.605a15.872 15.872 0 0 0 12.65-.486l52.501-25.262a15.94 15.94 0 0 0 9.025-14.364V41.197a15.939 15.939 0 0 0-9.025-14.363l-52.5-25.263a15.877 15.877 0 0 0-18.115 3.084L74.857 96.35l-43.78-33.232a10.614 10.614 0 0 0-13.56.603L3.476 76.494c-4.63 4.211-4.635 11.495-.012 15.713l37.967 34.638l-37.967 34.637c-4.623 4.219-4.618 11.502.012 15.714l14.041 12.772a10.614 10.614 0 0 0 13.56.604l43.78-33.233l100.507 91.695a15.853 15.853 0 0 0 5.464 3.571Zm10.464-183.649l-76.262 57.889l76.262 57.888V68.956Z"})],-1),w=t("mask",{id:"logosVisualStudioCode2",fill:"#fff"},[t("use",{href:"#logosVisualStudioCode1"})],-1),v=t("path",{fill:"#0065A9",d:"M246.135 26.873L193.593 1.575a15.885 15.885 0 0 0-18.123 3.08L3.466 161.482c-4.626 4.219-4.62 11.502.012 15.714l14.05 12.772a10.625 10.625 0 0 0 13.569.604L238.229 33.436c6.949-5.271 16.93-.315 16.93 8.407v-.61a15.938 15.938 0 0 0-9.024-14.36Z",mask:"url(#logosVisualStudioCode2)"},null,-1),y=t("path",{fill:"#007ACC",d:"m246.135 226.816l-52.542 25.298a15.887 15.887 0 0 1-18.123-3.08L3.466 92.207c-4.626-4.218-4.62-11.502.012-15.713l14.05-12.773a10.625 10.625 0 0 1 13.569-.603l207.132 157.135c6.949 5.271 16.93.315 16.93-8.408v.611a15.939 15.939 0 0 1-9.024 14.36Z",mask:"url(#logosVisualStudioCode2)"},null,-1),V=t("path",{fill:"#1F9CF0",d:"M193.428 252.134a15.892 15.892 0 0 1-18.125-3.083c5.881 5.88 15.938 1.715 15.938-6.603V11.273c0-8.318-10.057-12.483-15.938-6.602a15.892 15.892 0 0 1 18.125-3.084l52.533 25.263a15.937 15.937 0 0 1 9.03 14.363V212.51c0 6.125-3.51 11.709-9.03 14.363l-52.533 25.262Z",mask:"url(#logosVisualStudioCode2)"},null,-1),S=t("path",{fill:"url(#logosVisualStudioCode0)","fill-opacity":".25",d:"M180.828 252.605a15.874 15.874 0 0 0 12.65-.486l52.5-25.263a15.938 15.938 0 0 0 9.026-14.363V41.197a15.939 15.939 0 0 0-9.025-14.363L193.477 1.57a15.877 15.877 0 0 0-18.114 3.084L74.857 96.35l-43.78-33.232a10.614 10.614 0 0 0-13.56.603L3.476 76.494c-4.63 4.211-4.635 11.495-.012 15.713l37.967 34.638l-37.967 34.637c-4.623 4.219-4.618 11.502.012 15.714l14.041 12.772a10.614 10.614 0 0 0 13.56.604l43.78-33.233l100.506 91.695a15.857 15.857 0 0 0 5.465 3.571Zm10.464-183.65l-76.262 57.89l76.262 57.888V68.956Z",mask:"url(#logosVisualStudioCode2)"},null,-1),k=[f,w,v,y,V,S],C=t("p",null,[e("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 "),t("a",{href:"https://v3.vuejs.org/api/sfc-tooling.html#ide-support"},"Vue recommends themselves"),e(", to be used together with the "),t("a",{href:"https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar"},"Volar extension"),e(".")],-1),b=t("p",null,[e("VSCode’s "),t("a",{href:"https://code.visualstudio.com/docs/editor/integrated-terminal"},"Integrated Terminal"),e(" has great multi-terminal support you can toggle between the editor and terminal with "),t("code",null,"Ctrl+"),e(" or open a new Terminal Window with "),t("code",null,"Ctrl+Shift+`"),e(" to run Tailwind with:")],-1),A=t("pre",{class:"language-bash"},[t("code",{class:"language-bash"},[e("$ "),t("span",{class:"token function"},"npm"),e(` run ui:dev `)])],-1),L=t("p",null,"Then in a new Terminal Window, start a new watched .NET App with:",-1),x=t("pre",{class:"language-bash"},[t("code",{class:"language-bash"},[e("$ dotnet "),t("span",{class:"token function"},"watch"),e(` `)])],-1),T=t("p",null,[e("With both projects started you can open a browser tab running at "),t("code",null,"https://localhost:5001"),e(" where it will automatically reload itself at every "),t("code",null,"Ctrl+S"),e(" save point.")],-1),D=t("h4",null,"Useful VS Code extensions",-1),F=t("p",null,"We recommend these extensions below to enhance the development experience of this template:",-1),Z=t("ul",null,[t("li",null,[t("a",{href:"https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss"},"Tailwind CSS IntelliSense"),e(" - Add Intellisense for Tailwind classes")]),t("li",null,[t("a",{href:"https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html"},"es6-string-html"),e(" - Add HTML Syntax Highlighting in string literals")])],-1),E=t("h3",null,"Using Visual Studio",-1),M={href:"https://code.visualstudio.com/",title:"Visual Studio",class:"sm:float-left mr-8"},j={class:"w-24 h-24",style:{"margin-top":"1rem"},xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 256 256"},N=t("defs",null,[t("linearGradient",{id:"logosVisualStudio0",x1:"50%",x2:"50%",y1:".002%",y2:"100%"},[t("stop",{offset:"0%","stop-color":"#FFF"}),t("stop",{offset:"100%","stop-color":"#FFF","stop-opacity":"0"})])],-1),I=t("path",{fill:"#52218A",d:"M36.987 200.406a10.667 10.667 0 0 1-11.04 1.734L6.56 194.006A10.667 10.667 0 0 1 0 184.22V70.46a10.667 10.667 0 0 1 6.56-9.787l19.387-8a10.667 10.667 0 0 1 11.04 1.733l4.346 3.6a5.893 5.893 0 0 0-9.333 4.8v129.067a5.893 5.893 0 0 0 9.333 4.8l-4.346 3.733Z"},null,-1),B=t("path",{fill:"#6C33AF",d:"M6.56 194.006A10.667 10.667 0 0 1 0 184.22v-.88a6.16 6.16 0 0 0 10.667 4.133L176 4.673a16 16 0 0 1 18.187-3.093l52.746 25.386A16 16 0 0 1 256 41.393v.613a10.107 10.107 0 0 0-16.507-7.813l-198.16 162.48l-4.346 3.733a10.667 10.667 0 0 1-11.04 1.734L6.56 194.006Z"},null,-1),G=t("path",{fill:"#854CC7",d:"M6.56 60.673A10.667 10.667 0 0 0 0 70.46v.88a6.16 6.16 0 0 1 10.667-4.134L176 250.006a16 16 0 0 0 18.187 3.094l52.746-25.387A16 16 0 0 0 256 213.286v-.613a10.107 10.107 0 0 1-16.507 7.813L41.333 58.006l-4.346-3.733a10.667 10.667 0 0 0-11.04-1.6l-19.387 8Z"},null,-1),H=t("path",{fill:"#B179F1",d:"M194.187 253.1A16 16 0 0 1 176 250.006a9.387 9.387 0 0 0 16-6.64v-232a9.387 9.387 0 0 0-16-6.693a16 16 0 0 1 18.187-3.093l52.746 25.36A16 16 0 0 1 256 41.366v171.947a16 16 0 0 1-9.067 14.427l-52.746 25.36Z"},null,-1),J=t("path",{fill:"url(#logosVisualStudio0)","fill-opacity":".25",d:"M183.707 254.273a16 16 0 0 0 10.48-1.173l52.746-25.36A16 16 0 0 0 256 213.313V41.366a16 16 0 0 0-9.067-14.426L194.187 1.58A16 16 0 0 0 182.24.806A16 16 0 0 0 176 4.673L90.987 98.7L41.333 58.006l-4.346-3.733a10.667 10.667 0 0 0-9.627-2.213a6.8 6.8 0 0 0-1.413.48L6.56 60.673A10.667 10.667 0 0 0 0 69.66v115.36a10.664 10.664 0 0 0 6.56 8.986l19.387 8a6.8 6.8 0 0 0 1.413.48c3.378.882 6.973.056 9.627-2.213l4.346-3.6l49.654-40.693L176 250.006a16 16 0 0 0 7.707 4.267ZM192 73.153l-66.107 54.187L192 181.526V73.153ZM32 90.726l33.093 36.614L32 163.953V90.726Z"},null,-1),P=[N,I,B,G,H,J],U=t("p",null,"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.",-1),W=t("p",null,"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.",-1),R=t("p",null,"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:",-1),X=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),e("script")]),e(),t("span",{class:"token attr-name"},"type"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),e("module"),t("span",{class:"token punctuation"},'"')]),e(),t("span",{class:"token attr-name"},"src"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),e("./pages/SignIn.mjs"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),t("span",{class:"token script"}),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),e(` `)])],-1),$=t("h3",null,"Deploying to Production",-1),q=t("p",null,[e("This template also includes the necessary GitHub Actions to deploy this Apps production static assets to GitHub Pages CDN, for more info, checkout "),t("a",{href:"deploy"},"GitHub Actions Deployments"),e(".")],-1),z=t("h3",null,"Get Started",-1),K=t("p",null,[e("If you’re new to Vue 3 a good place to start is "),t("a",{href:"https://vuejs.org/api/composition-api-setup.html"},"Vue 3 Composition API"),e(".")],-1),st="Develop using Visual Studio",at="Exploring development workflow in VS Code and Visual Studio .NET",nt="Lucy Bates",lt=["c#","dev"],it="https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?crop=entropy&fit=crop&h=1000&w=2000",rt=[{property:"og:title",content:"Develop using Visual Studio"},{name:"twitter:title",content:"Develop using Visual Studio"},{property:"og:image",content:"https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}],ct={__name:"2023-01-10_vs",setup(O,{expose:n}){const s={title:"Develop using Visual Studio",summary:"Exploring development workflow in VS Code and Visual Studio .NET",author:"Lucy Bates",tags:["c#","dev"],image:"https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?crop=entropy&fit=crop&h=1000&w=2000",meta:[{property:"og:title",content:"Develop using Visual Studio"},{name:"twitter:title",content:"Develop using Visual Studio"},{property:"og:image",content:"https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]};return n({frontmatter:s}),r({title:"Develop using Visual Studio",meta:[{property:"og:title",content:"Develop using Visual Studio"},{name:"twitter:title",content:"Develop using Visual Studio"},{property:"og:image",content:"https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1513542789411-b6a5d4f31634?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]}),(Y,tt)=>{const l=i;return o(),c(l,{frontmatter:s},{default:p(()=>[t("div",u,[d,h,m,t("a",g,[(o(),a("svg",_,k))]),C,b,A,L,x,T,D,F,Z,E,t("a",M,[(o(),a("svg",j,P))]),U,W,R,X,$,q,z,K])]),_:1})}}};export{nt as author,ct as default,it as image,rt as meta,at as summary,lt as tags,st as title}; diff --git a/assets/2023-01-11_rider-DX0f_TD6.js b/assets/2023-01-11_rider-CM-m6doS.js similarity index 98% rename from assets/2023-01-11_rider-DX0f_TD6.js rename to assets/2023-01-11_rider-CM-m6doS.js index fbe193f..c4224a9 100644 --- a/assets/2023-01-11_rider-DX0f_TD6.js +++ b/assets/2023-01-11_rider-CM-m6doS.js @@ -1,4 +1,4 @@ -import{_ as i}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{g as r,o as n,h as c,w as l,a as e,c as p,e as t}from"./index-BXwhoUEp.js";const d={class:"markdown-body"},u=e("a",{href:"https://www.jetbrains.com/rider/"},[e("img",{src:"https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/svg/rider.svg",class:"sm:float-left mr-8 w-24 h-24",style:{"margin-top":"0"}})],-1),h=e("p",null,[e("a",{href:"https://www.jetbrains.com/rider/"},"JetBrains Rider"),t(" 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 "),e("a",{href:"https://v3.vuejs.org/guide/single-file-component.html"},"Vue SFC’s"),t(".")],-1),m=e("h4",null,"Setup Rider IDE",-1),g=e("p",null,[t("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 "),e("strong",null,"vue"),t(" dependency to "),e("strong",null,"package.json"),t(":")],-1),_=e("pre",{class:"language-json"},[e("code",{class:"language-json"},[e("span",{class:"token punctuation"},"{"),t(` +import{_ as i}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{i as r,o as n,h as c,w as l,a as e,c as p,e as t}from"./index-DWUkzBt0.js";const d={class:"markdown-body"},u=e("a",{href:"https://www.jetbrains.com/rider/"},[e("img",{src:"https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/svg/rider.svg",class:"sm:float-left mr-8 w-24 h-24",style:{"margin-top":"0"}})],-1),h=e("p",null,[e("a",{href:"https://www.jetbrains.com/rider/"},"JetBrains Rider"),t(" 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 "),e("a",{href:"https://v3.vuejs.org/guide/single-file-component.html"},"Vue SFC’s"),t(".")],-1),m=e("h4",null,"Setup Rider IDE",-1),g=e("p",null,[t("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 "),e("strong",null,"vue"),t(" dependency to "),e("strong",null,"package.json"),t(":")],-1),_=e("pre",{class:"language-json"},[e("code",{class:"language-json"},[e("span",{class:"token punctuation"},"{"),t(` `),e("span",{class:"token property"},'"devDependencies"'),e("span",{class:"token operator"},":"),t(),e("span",{class:"token punctuation"},"{"),t(` `),e("span",{class:"token property"},'"vue"'),e("span",{class:"token operator"},":"),t(),e("span",{class:"token string"},'""'),t(` `),e("span",{class:"token punctuation"},"}"),t(` diff --git a/assets/2023-01-21_start-CiEvf4zv.js b/assets/2023-01-21_start-BmNRwozK.js similarity index 98% rename from assets/2023-01-21_start-CiEvf4zv.js rename to assets/2023-01-21_start-BmNRwozK.js index 6b24fd6..2820e3d 100644 --- a/assets/2023-01-21_start-CiEvf4zv.js +++ b/assets/2023-01-21_start-BmNRwozK.js @@ -1,4 +1,4 @@ -import{_ as o}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{g as c,o as p,h as l,w as i,a as t,e as n}from"./index-BXwhoUEp.js";const u=t("div",{class:"markdown-body"},[t("h3",null,"Setup"),t("p",null,[n("If project wasn’t created with "),t("a",{href:"https://docs.servicestack.net/dotnet-new"},"x new"),n(", ensure postinstall tasks are run with:")]),t("pre",{class:"language-bash"},[t("code",{class:"language-bash"},[n("$ "),t("span",{class:"token function"},"npm"),n(),t("span",{class:"token function"},"install"),n(` +import{_ as o}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{i as c,o as p,h as l,w as i,a as t,e as n}from"./index-DWUkzBt0.js";const u=t("div",{class:"markdown-body"},[t("h3",null,"Setup"),t("p",null,[n("If project wasn’t created with "),t("a",{href:"https://docs.servicestack.net/dotnet-new"},"x new"),n(", ensure postinstall tasks are run with:")]),t("pre",{class:"language-bash"},[t("code",{class:"language-bash"},[n("$ "),t("span",{class:"token function"},"npm"),n(),t("span",{class:"token function"},"install"),n(` `)])]),t("h3",null,"Tailwind Configuration"),t("p",null,[n("This template is configured with a stand-alone "),t("a",{href:"https://tailwindcss.com/docs/installation"},"Tailwind CSS CLI"),n(" installation with a modified "),t("strong",null,"tailwind.input.css"),n(" that includes "),t("a",{href:"https://github.com/tailwindlabs/tailwindcss-forms"},"@tailwindcss/forms"),n(" and "),t("a",{href:"https://github.com/tailwindlabs/tailwindcss-aspect-ratio"},"@tailwindcss/aspect-ratio"),n(" plugins so that no "),t("strong",null,"node_modules"),n(" dependencies are needed.")]),t("p",null,[n("The "),t("a",{href:"https://tailwindcss.com/docs/typography-plugin"},"@tailwindcss/typography"),n(" plugin css is contained in "),t("code",null,"css/typography.css"),n(" which applies a beautiful default style to unstyled HTML, ideal for Markdown content like this.")]),t("h3",null,"Running Tailwind during development"),t("p",null,[n("Run tailwind in a new terminal during development to auto update your "),t("strong",null,"app.css"),n(":")]),t("pre",{class:"language-bash"},[t("code",{class:"language-bash"},[n("$ "),t("span",{class:"token function"},"npm"),n(` run ui:dev `)])]),t("p",null,[n("For an optimal development experience run it together with "),t("code",null,"dotnet watch"),n(" to preview changes on each save.")]),t("p",null,[n("Or if using JetBrains Rider, "),t("strong",null,"ui:dev"),n(" can be run directly from Rider in "),t("strong",null,"package.json"),n(":")]),t("p",null,[t("img",{src:"https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/servicestack-reference/scripts-tailwind.png",alt:""})]),t("h3",null,"Using JsonServiceClient in Web Pages"),t("p",null,[n("Easiest way to call APIs is to use "),t("a",{href:"https://docs.servicestack.net/javascript-client"},"@servicestack/client"),n(" with the built-in "),t("a",{href:"https://vue-mjs.web-templates.io/types/mjs"},"/types/mjs"),n(" which returns your APIs annotated typed JS DTOs that can be used immediately (i.e. without any build steps):")]),t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),n("input")]),n(),t("span",{class:"token attr-name"},"type"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),n("text"),t("span",{class:"token punctuation"},'"')]),n(),t("span",{class:"token attr-name"},"id"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),n("txtName"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),n(` `),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),n("div")]),n(),t("span",{class:"token attr-name"},"id"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),n("result"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),n(` diff --git a/assets/2023-02-01_javascript-BJNPrkTI.js b/assets/2023-02-01_javascript-CNPJxnIi.js similarity index 76% rename from assets/2023-02-01_javascript-BJNPrkTI.js rename to assets/2023-02-01_javascript-CNPJxnIi.js index 7919a23..0ffb9e3 100644 --- a/assets/2023-02-01_javascript-BJNPrkTI.js +++ b/assets/2023-02-01_javascript-CNPJxnIi.js @@ -1,8 +1,8 @@ -var b=Object.defineProperty;var S=(l,t,a)=>t in l?b(l,t,{enumerable:!0,configurable:!0,writable:!0,value:a}):l[t]=a;var g=(l,t,a)=>(S(l,typeof t!="symbol"?t+"":t,a),a);import{_ as x}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{d,o as k,c as h,t as y,j as f,r,b as e,w as c,e as s,h as w,m as A,a as n,J as C,g as j}from"./index-BXwhoUEp.js";import{_ as M}from"./Counter.vue_vue_type_script_setup_true_lang-BwW-seaC.js";const T=d({__name:"Hello",props:{name:{}},setup(l){return(t,a)=>(k(),h("b",null,"Hello, "+y(t.name)+"!",1))}}),I=n("div",{class:"p-8"},"Hello @servicestack/vue!",-1),H=d({__name:"Plugin",setup(l){const t=f(!1);return(a,p)=>{const u=r("PrimaryButton"),o=r("ModalDialog");return k(),h("div",null,[e(u,{onClick:p[0]||(p[0]=i=>t.value=!0)},{default:c(()=>[s("Open Modal")]),_:1}),t.value?(k(),w(o,{key:0,onDone:p[1]||(p[1]=i=>t.value=!1)},{default:c(()=>[I]),_:1})):A("",!0)])}}});class V{constructor(t){g(this,"result");g(this,"responseStatus");Object.assign(this,t)}}class P{constructor(t){g(this,"name");Object.assign(this,t)}getTypeName(){return"Hello"}getMethod(){return"GET"}createResponse(){return new V}}const L={class:"flex flex-wrap justify-center"},J={class:"ml-3 mt-2 text-lg"},q=d({__name:"HelloApi",props:{value:{}},setup(l){const a=f(l.value),p=f(""),u=new C("https://blazor-gallery.jamstacks.net");async function o(){let i=await u.api(new P({name:a.value}));i.succeeded&&(p.value=i.response.result)}return o(),(i,m)=>{const v=r("TextInput");return k(),h("div",L,[e(v,{modelValue:a.value,"onUpdate:modelValue":m[0]||(m[0]=_=>a.value=_),onKeyup:o},null,8,["modelValue"]),n("div",J,y(p.value),1)])}}}),z={class:"not-prose"},Z=n("div",{class:"mt-16 mx-auto flex justify-center"},[n("div",{class:"flex items-center"},[n("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",role:"img",class:"w-32 h-32 sm:w-44 sm:h-44 text-gray-700",viewBox:"0 0 32 32"},[n("path",{fill:"currentColor",d:"M10 6c1.544 1.76 2.276 4.15 2.217 6.61c3.968 1.67 9.924 6.12 11.181 12.39H28C26.051 14.31 14.918 6.77 10 6zm-2 7c4.67 4.913.81 11.582-4 12h18.97C21.5 18.289 11.95 13.533 8 13z"})]),n("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",role:"img",class:"w-32 h-32 sm:w-44 sm:h-44",viewBox:"0 0 32 32"},[n("path",{fill:"#41b883",d:"M24.4 3.925H30l-14 24.15L2 3.925h10.71l3.29 5.6l3.22-5.6Z"}),n("path",{fill:"#41b883",d:"m2 3.925l14 24.15l14-24.15h-5.6L16 18.415L7.53 3.925Z"}),n("path",{fill:"#35495e",d:"M7.53 3.925L16 18.485l8.4-14.56h-5.18L16 9.525l-3.29-5.6Z"})]),n("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",role:"img",class:"w-32 h-32 sm:w-44 sm:h-44",viewBox:"0 0 32 32"},[n("path",{fill:"#44a8b3",d:"M9 13.7q1.4-5.6 7-5.6c5.6 0 6.3 4.2 9.1 4.9q2.8.7 4.9-2.1q-1.4 5.6-7 5.6c-5.6 0-6.3-4.2-9.1-4.9q-2.8-.7-4.9 2.1Zm-7 8.4q1.4-5.6 7-5.6c5.6 0 6.3 4.2 9.1 4.9q2.8.7 4.9-2.1q-1.4 5.6-7 5.6c-5.6 0-6.3-4.2-9.1-4.9q-2.8-.7-4.9 2.1Z"})])])],-1),D=n("div",{class:"text-center"},[n("h1",{class:"mt-2 text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl"}," Component Gallery "),n("p",{class:"mt-2 text-lg text-gray-500 dark:text-gray-400"}," Explore Vue Tailwind Component Gallery ")],-1),N={class:"pt-4 pb-16"},R={class:"mx-auto"},U=d({__name:"VueComponentGallery",setup(l){const t=(p,u)=>`${u}`,a={AutoQueryGrid:t("0 0 28 28",""),DataGrid:t("0 0 16 16",""),AutoForms:t("0 0 1024 1024",""),FormInputs:t("0 0 36 36",""),Markdown:t("0 0 15 15",""),Modals:t("0 0 32 32",""),Navigation:t("0 0 12 12",""),Alerts:t("0 0 16 16",""),Formats:t("0 0 1024 1024",""),Code:t("0 0 24 24","")};return(p,u)=>{const o=r("NavListItem"),i=r("NavList");return k(),h("div",z,[Z,D,n("div",N,[n("div",R,[e(i,{title:""},{default:c(()=>[e(o,{title:"AutoQueryGrid",href:"https://docs.servicestack.net/vue/autoquerygrid",iconSvg:a.AutoQueryGrid},{default:c(()=>[s(" Instant customizable UIs for calling AutoQuery CRUD APIs ")]),_:1},8,["iconSvg"]),e(o,{title:"DataGrid",href:"https://docs.servicestack.net/vue/datagrid",iconSvg:a.DataGrid},{default:c(()=>[s(" DataGrid Component Examples for rendering tabular data ")]),_:1},8,["iconSvg"]),e(o,{title:"Auto Forms",href:"https://docs.servicestack.net/vue/autoform",iconSvg:a.AutoForms},{default:c(()=>[s(" Render Auto Form UIs from a Request DTO class ")]),_:1},8,["iconSvg"]),e(o,{title:"Form Inputs",href:"https://docs.servicestack.net/vue/form-inputs",iconSvg:a.FormInputs},{default:c(()=>[s(" Tailwind UI Input Components ")]),_:1},8,["iconSvg"]),e(o,{title:"Markdown Editor",href:"https://docs.servicestack.net/vue/markdown",iconSvg:a.Markdown},{default:c(()=>[s(" Rich Markdown Editing Input Control ")]),_:1},8,["iconSvg"]),e(o,{title:"Modals",href:"https://docs.servicestack.net/vue/modals",iconSvg:a.Modals},{default:c(()=>[s(" Modal Dialogs and Slide Overs ")]),_:1},8,["iconSvg"]),e(o,{title:"Navigation",href:"https://docs.servicestack.net/vue/navigation",iconSvg:a.Navigation},{default:c(()=>[s(" Breadcrumbs and Link navigation components ")]),_:1},8,["iconSvg"]),e(o,{title:"Alerts",href:"https://docs.servicestack.net/vue/alerts",iconSvg:a.Alerts},{default:c(()=>[s(" Tailwind Alert and Notification components ")]),_:1},8,["iconSvg"]),e(o,{title:"Formats",href:"https://docs.servicestack.net/vue/formats",iconSvg:a.Formats},{default:c(()=>[s(" HTML Value Formatters ")]),_:1},8,["iconSvg"])]),_:1})])])])}}}),B={class:"not-prose"},E=n("div",{class:"text-center"},[n("h1",{class:"mt-2 text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl"}," Vue Library ")],-1),F={class:"pt-4 pb-16"},O={class:"mx-auto"},$=d({__name:"VueComponentLibrary",setup(l){const a={Code:((p,u)=>`${u}`)("0 0 24 24","")};return(p,u)=>{const o=r("NavListItem"),i=r("NavList");return k(),h("div",B,[E,n("div",F,[n("div",O,[e(i,{class:"mt-8",title:""},{default:c(()=>[e(o,{title:"useMetadata",href:"https://docs.servicestack.net/vue/use-metadata",iconSvg:a.Code},{default:c(()=>[s(" Reflective utils for inspecting API AppMetadata ")]),_:1},8,["iconSvg"]),e(o,{title:"useClient",href:"https://docs.servicestack.net/vue/use-client",iconSvg:a.Code},{default:c(()=>[s(" Utilize JSON Api Client features in Components ")]),_:1},8,["iconSvg"]),e(o,{title:"useAuth",href:"https://docs.servicestack.net/vue/use-auth",iconSvg:a.Code},{default:c(()=>[s(" Inspect Authenticated Users Info, Roles & Permissions ")]),_:1},8,["iconSvg"]),e(o,{title:"useFormatters",href:"https://docs.servicestack.net/vue/use-formatters",iconSvg:a.Code},{default:c(()=>[s(" Built-in Formats and formatting functions ")]),_:1},8,["iconSvg"]),e(o,{title:"useFiles",href:"https://docs.servicestack.net/vue/use-files",iconSvg:a.Code},{default:c(()=>[s(" File utils for resolving SVG icons, extensions and MIME types ")]),_:1},8,["iconSvg"]),e(o,{title:"useConfig",href:"https://docs.servicestack.net/vue/use-config",iconSvg:a.Code},{default:c(()=>[s(" Manage global configuration & defaults ")]),_:1},8,["iconSvg"]),e(o,{title:"useUtils",href:"https://docs.servicestack.net/vue/use-utils",iconSvg:a.Code},{default:c(()=>[s(" General functionality and utils ")]),_:1},8,["iconSvg"])]),_:1})])])])}}}),W={class:"markdown-body"},G=n("p",null,"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.",-1),Q=n("p",null,[s("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 "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function"},"async/await"),s(", "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"},"JavaScript Modules"),s(", "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import"},"dynamic imports"),s(", "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap"},"import maps"),s(" and "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide"},"modern language features"),s(" for a sophisticated development workflow without the need for any npm build tools.")],-1),K=n("h3",null,"Bringing Simplicity Back",-1),Y=n("p",null,[s("The "),n("a",{href:"https://github.com/NetCoreTemplates/razor"},"razor"),s(" template focuses on simplicity and eschews many aspects that has complicated modern JavaScript development, specifically:")],-1),X=n("ul",null,[n("li",null,"No npm node_modules or build tools"),n("li",null,"No client side routing"),n("li",null,"No heavy client state")],-1),nn=n("p",null,[s("Effectively abandoning the traditional SPA approach in lieu of a simpler "),n("a",{href:"https://docs.astro.build/en/concepts/mpa-vs-spa/"},"MPA"),s(" development model using Razor Pages for Server Rendered content with any interactive UIs progressively enhanced with JavaScript.")],-1),sn=n("h4",null,"Freedom to use any JS library",-1),tn=n("p",null,[s("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 "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"},"JavaScript Module"),s(", taking advantage of the module system native to modern browsers able to efficiently download the declarative matrix of dependencies each script needs.")],-1),an=n("h3",null,"Best libraries for progressive Multi Page Apps",-1),en=n("p",null,"It includes a collection of libraries we believe offers the best modern development experience in Progressive MPA Web Apps, specifically:",-1),on=n("h4",null,[n("a",{href:"https://tailwindcss.com/docs/installation"},"Tailwind CLI")],-1),cn=n("p",null,"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.",-1),ln=n("h4",null,[n("a",{href:"https://vuejs.org/guide/introduction.html"},"Vue 3")],-1),pn=n("p",null,[s("Vue is a popular Progressive JavaScript Framework that makes it easy to create interactive Reactive Components whose "),n("a",{href:"https://vuejs.org/api/composition-api-setup.html"},"Composition API"),s(" offers a nice development model without requiring any pre-processors like JSX.")],-1),un=n("p",null,"Where creating a component is as simple as:",-1),rn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(" Hello "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` +import{_ as v}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{d,r as i,o as r,c as h,a as n,b as t,w as e,e as s,i as f,h as y}from"./index-DWUkzBt0.js";import{_ as w,a as _,b}from"./HelloApi.vue_vue_type_script_setup_true_lang-sMvvqITG.js";import{_ as S}from"./Counter.vue_vue_type_script_setup_true_lang-DcP0JNgA.js";const x={class:"not-prose"},A=n("div",{class:"mt-16 mx-auto flex justify-center"},[n("div",{class:"flex items-center"},[n("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",role:"img",class:"w-32 h-32 sm:w-44 sm:h-44 text-gray-700",viewBox:"0 0 32 32"},[n("path",{fill:"currentColor",d:"M10 6c1.544 1.76 2.276 4.15 2.217 6.61c3.968 1.67 9.924 6.12 11.181 12.39H28C26.051 14.31 14.918 6.77 10 6zm-2 7c4.67 4.913.81 11.582-4 12h18.97C21.5 18.289 11.95 13.533 8 13z"})]),n("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",role:"img",class:"w-32 h-32 sm:w-44 sm:h-44",viewBox:"0 0 32 32"},[n("path",{fill:"#41b883",d:"M24.4 3.925H30l-14 24.15L2 3.925h10.71l3.29 5.6l3.22-5.6Z"}),n("path",{fill:"#41b883",d:"m2 3.925l14 24.15l14-24.15h-5.6L16 18.415L7.53 3.925Z"}),n("path",{fill:"#35495e",d:"M7.53 3.925L16 18.485l8.4-14.56h-5.18L16 9.525l-3.29-5.6Z"})]),n("svg",{xmlns:"http://www.w3.org/2000/svg","aria-hidden":"true",role:"img",class:"w-32 h-32 sm:w-44 sm:h-44",viewBox:"0 0 32 32"},[n("path",{fill:"#44a8b3",d:"M9 13.7q1.4-5.6 7-5.6c5.6 0 6.3 4.2 9.1 4.9q2.8.7 4.9-2.1q-1.4 5.6-7 5.6c-5.6 0-6.3-4.2-9.1-4.9q-2.8-.7-4.9 2.1Zm-7 8.4q1.4-5.6 7-5.6c5.6 0 6.3 4.2 9.1 4.9q2.8.7 4.9-2.1q-1.4 5.6-7 5.6c-5.6 0-6.3-4.2-9.1-4.9q-2.8-.7-4.9 2.1Z"})])])],-1),C=n("div",{class:"text-center"},[n("h1",{class:"mt-2 text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl"}," Component Gallery "),n("p",{class:"mt-2 text-lg text-gray-500 dark:text-gray-400"}," Explore Vue Tailwind Component Gallery ")],-1),j={class:"pt-4 pb-16"},M={class:"mx-auto"},T=d({__name:"VueComponentGallery",setup(k){const c=(p,l)=>`${l}`,a={AutoQueryGrid:c("0 0 28 28",""),DataGrid:c("0 0 16 16",""),AutoForms:c("0 0 1024 1024",""),FormInputs:c("0 0 36 36",""),Markdown:c("0 0 15 15",""),Modals:c("0 0 32 32",""),Navigation:c("0 0 12 12",""),Alerts:c("0 0 16 16",""),Formats:c("0 0 1024 1024",""),Code:c("0 0 24 24","")};return(p,l)=>{const o=i("NavListItem"),u=i("NavList");return r(),h("div",x,[A,C,n("div",j,[n("div",M,[t(u,{title:""},{default:e(()=>[t(o,{title:"AutoQueryGrid",href:"https://docs.servicestack.net/vue/autoquerygrid",iconSvg:a.AutoQueryGrid},{default:e(()=>[s(" Instant customizable UIs for calling AutoQuery CRUD APIs ")]),_:1},8,["iconSvg"]),t(o,{title:"DataGrid",href:"https://docs.servicestack.net/vue/datagrid",iconSvg:a.DataGrid},{default:e(()=>[s(" DataGrid Component Examples for rendering tabular data ")]),_:1},8,["iconSvg"]),t(o,{title:"Auto Forms",href:"https://docs.servicestack.net/vue/autoform",iconSvg:a.AutoForms},{default:e(()=>[s(" Render Auto Form UIs from a Request DTO class ")]),_:1},8,["iconSvg"]),t(o,{title:"Form Inputs",href:"https://docs.servicestack.net/vue/form-inputs",iconSvg:a.FormInputs},{default:e(()=>[s(" Tailwind UI Input Components ")]),_:1},8,["iconSvg"]),t(o,{title:"Markdown Editor",href:"https://docs.servicestack.net/vue/markdown",iconSvg:a.Markdown},{default:e(()=>[s(" Rich Markdown Editing Input Control ")]),_:1},8,["iconSvg"]),t(o,{title:"Modals",href:"https://docs.servicestack.net/vue/modals",iconSvg:a.Modals},{default:e(()=>[s(" Modal Dialogs and Slide Overs ")]),_:1},8,["iconSvg"]),t(o,{title:"Navigation",href:"https://docs.servicestack.net/vue/navigation",iconSvg:a.Navigation},{default:e(()=>[s(" Breadcrumbs and Link navigation components ")]),_:1},8,["iconSvg"]),t(o,{title:"Alerts",href:"https://docs.servicestack.net/vue/alerts",iconSvg:a.Alerts},{default:e(()=>[s(" Tailwind Alert and Notification components ")]),_:1},8,["iconSvg"]),t(o,{title:"Formats",href:"https://docs.servicestack.net/vue/formats",iconSvg:a.Formats},{default:e(()=>[s(" HTML Value Formatters ")]),_:1},8,["iconSvg"])]),_:1})])])])}}}),I={class:"not-prose"},V=n("div",{class:"text-center"},[n("h1",{class:"mt-2 text-4xl font-bold tracking-tight text-gray-900 dark:text-gray-100 sm:text-5xl"}," Vue Library ")],-1),H={class:"pt-4 pb-16"},L={class:"mx-auto"},P=d({__name:"VueComponentLibrary",setup(k){const a={Code:((p,l)=>`${l}`)("0 0 24 24","")};return(p,l)=>{const o=i("NavListItem"),u=i("NavList");return r(),h("div",I,[V,n("div",H,[n("div",L,[t(u,{class:"mt-8",title:""},{default:e(()=>[t(o,{title:"useMetadata",href:"https://docs.servicestack.net/vue/use-metadata",iconSvg:a.Code},{default:e(()=>[s(" Reflective utils for inspecting API AppMetadata ")]),_:1},8,["iconSvg"]),t(o,{title:"useClient",href:"https://docs.servicestack.net/vue/use-client",iconSvg:a.Code},{default:e(()=>[s(" Utilize JSON Api Client features in Components ")]),_:1},8,["iconSvg"]),t(o,{title:"useAuth",href:"https://docs.servicestack.net/vue/use-auth",iconSvg:a.Code},{default:e(()=>[s(" Inspect Authenticated Users Info, Roles & Permissions ")]),_:1},8,["iconSvg"]),t(o,{title:"useFormatters",href:"https://docs.servicestack.net/vue/use-formatters",iconSvg:a.Code},{default:e(()=>[s(" Built-in Formats and formatting functions ")]),_:1},8,["iconSvg"]),t(o,{title:"useFiles",href:"https://docs.servicestack.net/vue/use-files",iconSvg:a.Code},{default:e(()=>[s(" File utils for resolving SVG icons, extensions and MIME types ")]),_:1},8,["iconSvg"]),t(o,{title:"useConfig",href:"https://docs.servicestack.net/vue/use-config",iconSvg:a.Code},{default:e(()=>[s(" Manage global configuration & defaults ")]),_:1},8,["iconSvg"]),t(o,{title:"useUtils",href:"https://docs.servicestack.net/vue/use-utils",iconSvg:a.Code},{default:e(()=>[s(" General functionality and utils ")]),_:1},8,["iconSvg"])]),_:1})])])])}}}),q={class:"markdown-body"},J=n("p",null,"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.",-1),z=n("p",null,[s("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 "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function"},"async/await"),s(", "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"},"JavaScript Modules"),s(", "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import"},"dynamic imports"),s(", "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap"},"import maps"),s(" and "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide"},"modern language features"),s(" for a sophisticated development workflow without the need for any npm build tools.")],-1),Z=n("h3",null,"Bringing Simplicity Back",-1),D=n("p",null,[s("The "),n("a",{href:"https://github.com/NetCoreTemplates/razor"},"razor"),s(" template focuses on simplicity and eschews many aspects that has complicated modern JavaScript development, specifically:")],-1),N=n("ul",null,[n("li",null,"No npm node_modules or build tools"),n("li",null,"No client side routing"),n("li",null,"No heavy client state")],-1),R=n("p",null,[s("Effectively abandoning the traditional SPA approach in lieu of a simpler "),n("a",{href:"https://docs.astro.build/en/concepts/mpa-vs-spa/"},"MPA"),s(" development model using Razor Pages for Server Rendered content with any interactive UIs progressively enhanced with JavaScript.")],-1),U=n("h4",null,"Freedom to use any JS library",-1),E=n("p",null,[s("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 "),n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules"},"JavaScript Module"),s(", taking advantage of the module system native to modern browsers able to efficiently download the declarative matrix of dependencies each script needs.")],-1),F=n("h3",null,"Best libraries for progressive Multi Page Apps",-1),B=n("p",null,"It includes a collection of libraries we believe offers the best modern development experience in Progressive MPA Web Apps, specifically:",-1),O=n("h4",null,[n("a",{href:"https://tailwindcss.com/docs/installation"},"Tailwind CLI")],-1),W=n("p",null,"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.",-1),$=n("h4",null,[n("a",{href:"https://vuejs.org/guide/introduction.html"},"Vue 3")],-1),G=n("p",null,[s("Vue is a popular Progressive JavaScript Framework that makes it easy to create interactive Reactive Components whose "),n("a",{href:"https://vuejs.org/api/composition-api-setup.html"},"Composition API"),s(" offers a nice development model without requiring any pre-processors like JSX.")],-1),Q=n("p",null,"Where creating a component is as simple as:",-1),Y=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(" Hello "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token literal-property property"},"template"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token template-string"},[n("span",{class:"token template-punctuation string"},"`"),n("span",{class:"token string"},"Hello, {{name}}!"),n("span",{class:"token template-punctuation string"},"`")]),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token literal-property property"},"props"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token punctuation"},"{"),s(),n("span",{class:"token literal-property property"},"name"),n("span",{class:"token operator"},":"),s("String "),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` -`)])],-1),kn={class:"text-center text-2xl py-2"},dn=n("p",null,"Or a simple reactive example:",-1),hn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" ref "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"vue"'),s(` +`)])],-1),K={class:"text-center text-2xl py-2"},X=n("p",null,"Or a simple reactive example:",-1),nn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" ref "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"vue"'),s(` `),n("span",{class:"token keyword"},"const"),s(" Counter "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token literal-property property"},"template"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token template-string"},[n("span",{class:"token template-punctuation string"},"`"),n("span",{class:"token string"},'Counter {{count}}'),n("span",{class:"token template-punctuation string"},"`")]),n("span",{class:"token punctuation"},","),s(` @@ -11,12 +11,12 @@ var b=Object.defineProperty;var S=(l,t,a)=>t in l?b(l,t,{enumerable:!0,configura `),n("span",{class:"token keyword"},"return"),s(),n("span",{class:"token punctuation"},"{"),s(" count "),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` -`)])],-1),mn={class:"text-center text-2xl py-2 cursor-pointer select-none"},gn=n("h3",null,"Vue Components in Markdown",-1),vn=n("p",null,[s("Inside "),n("code",null,".md"),s(" Markdown pages Vue Components can be embedded using Vue’s progressive "),n("a",{href:"https://vuejs.org/guide/essentials/template-syntax.html"},"HTML Template Syntax"),s(":")],-1),fn=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("hello")]),s(),n("span",{class:"token attr-name"},"name"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("Vue 3"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])],-1),sn={class:"text-center text-2xl py-2 cursor-pointer select-none"},tn=n("h3",null,"Vue Components in Markdown",-1),an=n("p",null,[s("Inside "),n("code",null,".md"),s(" Markdown pages Vue Components can be embedded using Vue’s progressive "),n("a",{href:"https://vuejs.org/guide/essentials/template-syntax.html"},"HTML Template Syntax"),s(":")],-1),en=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("hello")]),s(),n("span",{class:"token attr-name"},"name"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("Vue 3"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("counter")]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),yn=n("h3",null,"Vue Components in Razor Pages",-1),wn=n("p",null,[s("Inside "),n("code",null,".cshtml"),s(" Razor Pages these components can be mounted using the standard "),n("a",{href:"https://vuejs.org/api/application.html#app-mount"},"Vue 3 mount"),s(" API, but to make it easier we’ve added additional APIs for declaratively mounting components to pages using "),n("code",null,"data-component"),s(" and "),n("code",null,"data-props"),s(" attributes:")],-1),_n=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"data-component"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("Hello"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"data-props"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("{ name: 'Vue 3' }"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),bn=n("p",null,[s("Alternatively they can be programatically added using the custom "),n("code",null,"mount"),s(" method in "),n("code",null,"api.mjs"),s(":")],-1),Sn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" mount "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"/mjs/api.mjs"'),s(` +`)])],-1),on=n("h3",null,"Vue Components in Razor Pages",-1),cn=n("p",null,[s("Inside "),n("code",null,".cshtml"),s(" Razor Pages these components can be mounted using the standard "),n("a",{href:"https://vuejs.org/api/application.html#app-mount"},"Vue 3 mount"),s(" API, but to make it easier we’ve added additional APIs for declaratively mounting components to pages using "),n("code",null,"data-component"),s(" and "),n("code",null,"data-props"),s(" attributes:")],-1),pn=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"data-component"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("Hello"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"data-props"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("{ name: 'Vue 3' }"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])],-1),ln=n("p",null,[s("Alternatively they can be programatically added using the custom "),n("code",null,"mount"),s(" method in "),n("code",null,"api.mjs"),s(":")],-1),un=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" mount "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"/mjs/api.mjs"'),s(` `),n("span",{class:"token function"},"mount"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},"'#counter'"),n("span",{class:"token punctuation"},","),s(" Counter"),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),xn=n("p",null,[s("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 "),n("strong",null,"@servicestack/vue"),s("’s "),n("a",{href:"https://docs.servicestack.net/vue/navigation#primarybutton"},"PrimaryButton"),s(" and "),n("a",{href:"https://docs.servicestack.net/vue/modals"},"ModalDialog"),s(":")],-1),An=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(" Plugin "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` +`)])],-1),rn=n("p",null,[s("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 "),n("strong",null,"@servicestack/vue"),s("’s "),n("a",{href:"https://docs.servicestack.net/vue/navigation#primarybutton"},"PrimaryButton"),s(" and "),n("a",{href:"https://docs.servicestack.net/vue/modals"},"ModalDialog"),s(":")],-1),kn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(" Plugin "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token literal-property property"},"template"),n("span",{class:"token operator"},":"),n("span",{class:"token template-string"},[n("span",{class:"token template-punctuation string"},"`"),n("span",{class:"token string"},`
Open Modal @@ -28,8 +28,8 @@ var b=Object.defineProperty;var S=(l,t,a)=>t in l?b(l,t,{enumerable:!0,configura `),n("span",{class:"token keyword"},"return"),s(),n("span",{class:"token punctuation"},"{"),s(" show "),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` -`)])],-1),Cn=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("plugin")]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),jn={class:"text-center"},Mn=n("h3",null,"Vue HTML Templates",-1),Tn=n("p",null,[s("An alternative progressive approach for creating Reactive UIs with Vue is by embedding its HTML markup directly in "),n("code",null,".html"),s(" pages using "),n("a",{href:"https://vuejs.org/guide/essentials/template-syntax.html"},"HTML Template Syntax"),s(" 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 "),n("a",{href:"https://vuejs.org/api/built-in-directives.html#v-cloak"},"v-cloak"),s(" attribute where they’ll be hidden until components are initialized.")],-1),In=n("p",null,[s("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 "),n("code",null,".js"),s(" source files that for SPAs unnecessarily increases the payload sizes of JS bundles with functionality that no other pages need.")],-1),Hn=n("p",null,"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:",-1),Vn=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("app"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` +`)])],-1),dn=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("plugin")]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])],-1),hn={class:"text-center"},mn=n("h3",null,"Vue HTML Templates",-1),gn=n("p",null,[s("An alternative progressive approach for creating Reactive UIs with Vue is by embedding its HTML markup directly in "),n("code",null,".html"),s(" pages using "),n("a",{href:"https://vuejs.org/guide/essentials/template-syntax.html"},"HTML Template Syntax"),s(" 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 "),n("a",{href:"https://vuejs.org/api/built-in-directives.html#v-cloak"},"v-cloak"),s(" attribute where they’ll be hidden until components are initialized.")],-1),vn=n("p",null,[s("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 "),n("code",null,".js"),s(" source files that for SPAs unnecessarily increases the payload sizes of JS bundles with functionality that no other pages need.")],-1),fn=n("p",null,"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:",-1),yn=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("app"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("primary-button")]),s(),n("span",{class:"token attr-name"},[n("span",{class:"token namespace"},"v-on:"),s("click")]),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("show=true"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s("Open Modal"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("modal-dialog")]),s(),n("span",{class:"token attr-name"},"v-if"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("show"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},[n("span",{class:"token namespace"},"v-on:"),s("done")]),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("show=false"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"class"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("p-8"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s("Hello @servicestack/vue!"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` @@ -44,7 +44,7 @@ var b=Object.defineProperty;var S=(l,t,a)=>t in l?b(l,t,{enumerable:!0,configura `),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token function"},"mount"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},"'#app'"),n("span",{class:"token punctuation"},","),s(" App"),n("span",{class:"token punctuation"},")"),s(` `)])]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),Pn=n("p",null,[s("This is the approach used to develop "),n("a",{href:"/posts/vue-stable-diffusion"},"Vue Stable Diffusion"),s(" 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.")],-1),Ln=n("h3",null,"@servicestack/vue",-1),Jn=n("p",null,[n("a",{href:"https://github.com/ServiceStack/servicestack-vue"},"@servicestack/vue"),s(" 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 "),n("a",{href:"https://github.com/NetCoreTemplates/razor"},"razor"),s(" template.")],-1),qn=n("h3",null,"@servicestack/client",-1),zn=n("p",null,[n("a",{href:"https://docs.servicestack.net/javascript-client"},"@servicestack/client"),s(" is our generic JS/TypeScript client library which enables a terse, typed API for using your App’s typed DTOs from the built-in "),n("a",{href:"https://docs.servicestack.net/javascript-add-servicestack-reference"},"JavaScript ES6 Classes"),s(" support to enable an effortless end-to-end Typed development model for calling your APIs "),n("strong",null,"without any build steps"),s(", e.g:")],-1),Zn=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("input")]),s(),n("span",{class:"token attr-name"},"type"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("text"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("txtName"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` +`)])],-1),wn=n("p",null,[s("This is the approach used to develop "),n("a",{href:"/posts/vue-stable-diffusion"},"Vue Stable Diffusion"),s(" 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.")],-1),_n=n("h3",null,"@servicestack/vue",-1),bn=n("p",null,[n("a",{href:"https://github.com/ServiceStack/servicestack-vue"},"@servicestack/vue"),s(" 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 "),n("a",{href:"https://github.com/NetCoreTemplates/razor"},"razor"),s(" template.")],-1),Sn=n("h3",null,"@servicestack/client",-1),xn=n("p",null,[n("a",{href:"https://docs.servicestack.net/javascript-client"},"@servicestack/client"),s(" is our generic JS/TypeScript client library which enables a terse, typed API for using your App’s typed DTOs from the built-in "),n("a",{href:"https://docs.servicestack.net/javascript-add-servicestack-reference"},"JavaScript ES6 Classes"),s(" support to enable an effortless end-to-end Typed development model for calling your APIs "),n("strong",null,"without any build steps"),s(", e.g:")],-1),An=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("input")]),s(),n("span",{class:"token attr-name"},"type"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("text"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("txtName"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("result"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("script")]),s(),n("span",{class:"token attr-name"},"type"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("module"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token script"},[n("span",{class:"token language-javascript"},[s(` @@ -59,9 +59,9 @@ var b=Object.defineProperty;var S=(l,t,a)=>t in l?b(l,t,{enumerable:!0,configura `),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),s(` `)])]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),Dn=n("p",null,"For better IDE intelli-sense during development, save the annotated Typed DTOs to disk with:",-1),Nn=n("div",{class:"not-prose sh-copy cp flex cursor-pointer mb-3",onclick:"copy(this)"},[n("div",{class:"flex-grow bg-gray-800"},[n("div",{class:"pl-4 py-1 pb-1.5 align-middle whitespace-pre text-base text-gray-100"},[n("p",null,"npm run dtos")])]),n("div",{class:"flex"},[n("div",{class:"bg-green-600 text-white p-1.5 pb-0"},[n("svg",{class:"copied w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},[n("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"})]),n("svg",{class:"nocopy w-6 h-6",title:"copy",fill:"none",stroke:"white",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},[n("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"1",d:"M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"})])])])],-1),Rn=n("p",null,"That can be referenced instead to unlock your IDE’s static analysis type-checking and intelli-sense benefits during development:",-1),Un=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Hello "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},"'/js/dtos.mjs'"),s(` +`)])],-1),Cn=n("p",null,"For better IDE intelli-sense during development, save the annotated Typed DTOs to disk with:",-1),jn=n("div",{class:"not-prose sh-copy cp flex cursor-pointer mb-3",onclick:"copy(this)"},[n("div",{class:"flex-grow bg-gray-800"},[n("div",{class:"pl-4 py-1 pb-1.5 align-middle whitespace-pre text-base text-gray-100"},[n("p",null,"npm run dtos")])]),n("div",{class:"flex"},[n("div",{class:"bg-green-600 text-white p-1.5 pb-0"},[n("svg",{class:"copied w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},[n("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"})]),n("svg",{class:"nocopy w-6 h-6",title:"copy",fill:"none",stroke:"white",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},[n("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"1",d:"M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"})])])])],-1),Mn=n("p",null,"That can be referenced instead to unlock your IDE’s static analysis type-checking and intelli-sense benefits during development:",-1),Tn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Hello "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},"'/js/dtos.mjs'"),s(` client`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"api"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Hello"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"{"),s(" name "),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),Bn=n("p",null,[s("You’ll typically use all these libraries in your "),n("strong",null,"API-enabled"),s(" components as seen in the "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/main/MyApp/wwwroot/mjs/components/HelloApi.mjs"},"HelloApi.mjs"),s(" component on the home page which calls the "),n("a",{href:"/ui/Hello"},"Hello"),s(" API on each key press:")],-1),En=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" ref "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"vue"'),s(` +`)])],-1),In=n("p",null,[s("You’ll typically use all these libraries in your "),n("strong",null,"API-enabled"),s(" components as seen in the "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/main/MyApp/wwwroot/mjs/components/HelloApi.mjs"},"HelloApi.mjs"),s(" component on the home page which calls the "),n("a",{href:"/ui/Hello"},"Hello"),s(" API on each key press:")],-1),Vn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" ref "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"vue"'),s(` `),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" useClient "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@servicestack/vue"'),s(` `),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Hello "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"../dtos.mjs"'),s(` @@ -87,20 +87,20 @@ client`),n("span",{class:"token punctuation"},"."),n("span",{class:"token functi `),n("span",{class:"token keyword"},"return"),s(),n("span",{class:"token punctuation"},"{"),s(" name"),n("span",{class:"token punctuation"},","),s(" update"),n("span",{class:"token punctuation"},","),s(" result "),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` -`)])],-1),Fn=n("p",null,"Which we can also mount below:",-1),On=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("hello-api")]),s(),n("span",{class:"token attr-name"},"value"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("Vue 3"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),$n=n("p",null,"We’ll also go through and explain other features used in this component:",-1),Wn=n("h4",null,[n("code",null,"/*html*/")],-1),Gn=n("p",null,[s("Although not needed in "),n("a",{href:"rider"},"Rider"),s(" (which can automatically infer HTML in strings), the "),n("code",null,"/*html*/"),s(" type hint can be used to instruct tooling like the "),n("a",{href:"https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html"},"es6-string-html"),s(" VS Code extension to provide syntax highlighting and an enhanced authoring experience for HTML content in string literals.")],-1),Qn=n("h3",null,"useClient",-1),Kn=n("p",null,[n("a",{href:"https://docs.servicestack.net/vue/use-client"},"useClient()"),s(" provides managed APIs around the "),n("code",null,"JsonServiceClient"),s(" instance registered in Vue App’s with:")],-1),Yn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" client "),n("span",{class:"token operator"},"="),s(" JsonApiClient"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"create"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` +`)])],-1),Hn=n("p",null,"Which we can also mount below:",-1),Ln=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("hello-api")]),s(),n("span",{class:"token attr-name"},"value"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("Vue 3"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])],-1),Pn=n("p",null,"We’ll also go through and explain other features used in this component:",-1),qn=n("h4",null,[n("code",null,"/*html*/")],-1),Jn=n("p",null,[s("Although not needed in "),n("a",{href:"rider"},"Rider"),s(" (which can automatically infer HTML in strings), the "),n("code",null,"/*html*/"),s(" type hint can be used to instruct tooling like the "),n("a",{href:"https://marketplace.visualstudio.com/items?itemName=Tobermory.es6-string-html"},"es6-string-html"),s(" VS Code extension to provide syntax highlighting and an enhanced authoring experience for HTML content in string literals.")],-1),zn=n("h3",null,"useClient",-1),Zn=n("p",null,[n("a",{href:"https://docs.servicestack.net/vue/use-client"},"useClient()"),s(" provides managed APIs around the "),n("code",null,"JsonServiceClient"),s(" instance registered in Vue App’s with:")],-1),Dn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" client "),n("span",{class:"token operator"},"="),s(" JsonApiClient"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"create"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` app`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"provide"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},"'client'"),n("span",{class:"token punctuation"},","),s(" client"),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),Xn=n("p",null,[s("Which maintains contextual information around your API calls like "),n("strong",null,"loading"),s(" and "),n("strong",null,"error"),s(" states, used by "),n("code",null,"@servicestack/vue"),s(" components to enable its auto validation binding. Other functionality in this provider include:")],-1),ns=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(),n("span",{class:"token punctuation"},"{"),s(` +`)])],-1),Nn=n("p",null,[s("Which maintains contextual information around your API calls like "),n("strong",null,"loading"),s(" and "),n("strong",null,"error"),s(" states, used by "),n("code",null,"@servicestack/vue"),s(" components to enable its auto validation binding. Other functionality in this provider include:")],-1),Rn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(),n("span",{class:"token punctuation"},"{"),s(` api`),n("span",{class:"token punctuation"},","),s(" apiVoid"),n("span",{class:"token punctuation"},","),s(" apiForm"),n("span",{class:"token punctuation"},","),s(" apiFormVoid"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token comment"},"// Managed Typed ServiceClient APIs"),s(` loading`),n("span",{class:"token punctuation"},","),s(" error"),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token comment"},"// Maintains 'loading' and 'error' states"),s(` setError`),n("span",{class:"token punctuation"},","),s(" addFieldError"),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token comment"},"// Add custom errors in client"),s(` unRefs `),n("span",{class:"token comment"},"// Returns a dto with all Refs unwrapped"),s(` `),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token function"},"useClient"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),ss=n("p",null,[s("Typically you would need to unwrap "),n("code",null,"ref"),s(" values when calling APIs, i.e:")],-1),ts=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" client "),n("span",{class:"token operator"},"="),s(" JsonApiClient"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"create"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` +`)])],-1),Un=n("p",null,[s("Typically you would need to unwrap "),n("code",null,"ref"),s(" values when calling APIs, i.e:")],-1),En=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" client "),n("span",{class:"token operator"},"="),s(" JsonApiClient"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"create"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` `),n("span",{class:"token keyword"},"let"),s(" api "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"await"),s(" client"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"api"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Hello"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"{"),s(),n("span",{class:"token literal-property property"},"name"),n("span",{class:"token operator"},":"),s("name"),n("span",{class:"token punctuation"},"."),s("value "),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),as=n("h4",null,"useClient - api",-1),es=n("p",null,[s("This is unnecessary in useClient "),n("code",null,"api*"),s(" methods which automatically unwraps ref values, allowing for the more pleasant API call:")],-1),os=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" api "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"await"),s(" client"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"api"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Hello"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"{"),s(" name "),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),cs=n("h4",null,"useClient - unRefs",-1),ls=n("p",null,[s("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 "),n("code",null,"unRefs"),s(":")],-1),ps=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" api "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"await"),s(" client"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"api"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Hello"),n("span",{class:"token punctuation"},"("),n("span",{class:"token function"},"unRefs"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"{"),s(" name "),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),is=n("h4",null,"useClient - setError",-1),us=n("p",null,[n("code",null,"setError"),s(" can be used to populate client-side validation errors which the "),n("a",{href:"https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/wwwroot/Pages/SignUp.mjs"},"SignUp.mjs"),s(" component uses to report an invalid submissions when passwords don’t match:")],-1),rs=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(),n("span",{class:"token punctuation"},"{"),s(" api"),n("span",{class:"token punctuation"},","),s(" setError "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token function"},"useClient"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` +`)])],-1),Fn=n("h4",null,"useClient - api",-1),Bn=n("p",null,[s("This is unnecessary in useClient "),n("code",null,"api*"),s(" methods which automatically unwraps ref values, allowing for the more pleasant API call:")],-1),On=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" api "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"await"),s(" client"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"api"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Hello"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"{"),s(" name "),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),s(` +`)])],-1),Wn=n("h4",null,"useClient - unRefs",-1),$n=n("p",null,[s("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 "),n("code",null,"unRefs"),s(":")],-1),Gn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" api "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token keyword"},"await"),s(" client"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"api"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Hello"),n("span",{class:"token punctuation"},"("),n("span",{class:"token function"},"unRefs"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"{"),s(" name "),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),s(` +`)])],-1),Qn=n("h4",null,"useClient - setError",-1),Yn=n("p",null,[n("code",null,"setError"),s(" can be used to populate client-side validation errors which the "),n("a",{href:"https://github.com/NetCoreTemplates/vue-mjs/blob/main/MyApp/wwwroot/Pages/SignUp.mjs"},"SignUp.mjs"),s(" component uses to report an invalid submissions when passwords don’t match:")],-1),Kn=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(),n("span",{class:"token punctuation"},"{"),s(" api"),n("span",{class:"token punctuation"},","),s(" setError "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token function"},"useClient"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` `),n("span",{class:"token keyword"},"async"),s(),n("span",{class:"token keyword"},"function"),s(),n("span",{class:"token function"},"onSubmit"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("password"),n("span",{class:"token punctuation"},"."),s("value "),n("span",{class:"token operator"},"!=="),s(" confirmPassword"),n("span",{class:"token punctuation"},"."),s("value"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token function"},"setError"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},"{"),s(),n("span",{class:"token literal-property property"},"fieldName"),n("span",{class:"token operator"},":"),n("span",{class:"token string"},"'confirmPassword'"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token literal-property property"},"message"),n("span",{class:"token operator"},":"),n("span",{class:"token string"},"'Passwords do not match'"),s(),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),s(` @@ -108,14 +108,14 @@ app`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function" `),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token comment"},"//..."),s(` `),n("span",{class:"token punctuation"},"}"),s(` -`)])],-1),ks=n("h3",null,"Form Validation",-1),ds=n("p",null,[s("All "),n("code",null,"@servicestack/vue"),s(" Input Components support contextual validation binding that’s typically populated from API "),n("a",{href:"https://docs.servicestack.net/error-handling"},"Error Response DTOs"),s(" but can also be populated from client-side validation as done above.")],-1),hs=n("h4",null,"Explicit Error Handling",-1),ms=n("p",null,[s("This populated "),n("code",null,"ResponseStatus"),s(" DTO can either be manually passed into each component’s "),n("strong",null,"status"),s(" property as done in "),n("a",{href:"/TodoMvc"},"/TodoMvc"),s(":")],-1),gs=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("template")]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("TodoMvc-template"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` +`)])],-1),Xn=n("h3",null,"Form Validation",-1),ns=n("p",null,[s("All "),n("code",null,"@servicestack/vue"),s(" Input Components support contextual validation binding that’s typically populated from API "),n("a",{href:"https://docs.servicestack.net/error-handling"},"Error Response DTOs"),s(" but can also be populated from client-side validation as done above.")],-1),ss=n("h4",null,"Explicit Error Handling",-1),ts=n("p",null,[s("This populated "),n("code",null,"ResponseStatus"),s(" DTO can either be manually passed into each component’s "),n("strong",null,"status"),s(" property as done in "),n("a",{href:"/TodoMvc"},"/TodoMvc"),s(":")],-1),as=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("template")]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("TodoMvc-template"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"class"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("mb-3"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("text-input")]),s(),n("span",{class:"token attr-name"},":status"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("store.error"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("text"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"label"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"placeholder"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("What needs to be done?"),n("span",{class:"token punctuation"},'"')]),s(` `),n("span",{class:"token attr-name"},"v-model"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("store.newTodo"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},[n("span",{class:"token namespace"},"v-on:"),s("keyup.enter.stop")]),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("store.addTodo()"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `),n("span",{class:"token comment"},""),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),vs=n("p",null,[s("Where if you try adding an empty Todo the "),n("code",null,"CreateTodo"),s(" API will fail and populate its "),n("code",null,"store.error"),s(" reactive property with the APIs Error Response DTO which the "),n("code",null,""),s(" component checks to display any field validation errors adjacent to the HTML Input with matching "),n("code",null,"id"),s(" fields:")],-1),fs=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" store "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` +`)])],-1),es=n("p",null,[s("Where if you try adding an empty Todo the "),n("code",null,"CreateTodo"),s(" API will fail and populate its "),n("code",null,"store.error"),s(" reactive property with the APIs Error Response DTO which the "),n("code",null,""),s(" component checks to display any field validation errors adjacent to the HTML Input with matching "),n("code",null,"id"),s(" fields:")],-1),os=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"let"),s(" store "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token comment"},"/** @type {Todo[]} */"),s(` `),n("span",{class:"token literal-property property"},"todos"),n("span",{class:"token operator"},":"),s(),n("span",{class:"token punctuation"},"["),n("span",{class:"token punctuation"},"]"),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token literal-property property"},"newTodo"),n("span",{class:"token operator"},":"),n("span",{class:"token string"},"''"),n("span",{class:"token punctuation"},","),s(` @@ -130,7 +130,7 @@ app`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function" `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token comment"},"//..."),s(` `),n("span",{class:"token punctuation"},"}"),s(` -`)])],-1),ys=n("h4",null,"Implicit Error Handling",-1),ws=n("p",null,[s("More often you’ll want to take advantage of the implicit validation support in "),n("code",null,"useClient()"),s(" which makes its state available to child components, alleviating the need to explicitly pass it in each component as seen in razor’s "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/net6/MyApp/wwwroot/Pages/Contacts.mjs"},"Contacts.mjs"),s(),n("code",null,"Edit"),s(" component for its Contacts page which doesn’t do any manual error handling:")],-1),_s=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(" Edit "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` +`)])],-1),cs=n("h4",null,"Implicit Error Handling",-1),ps=n("p",null,[s("More often you’ll want to take advantage of the implicit validation support in "),n("code",null,"useClient()"),s(" which makes its state available to child components, alleviating the need to explicitly pass it in each component as seen in razor’s "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/net6/MyApp/wwwroot/Pages/Contacts.mjs"},"Contacts.mjs"),s(),n("code",null,"Edit"),s(" component for its Contacts page which doesn’t do any manual error handling:")],-1),ls=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(" Edit "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token literal-property property"},"template"),n("span",{class:"token operator"},":"),n("span",{class:"token comment"},"/*html*/"),n("span",{class:"token template-string"},[n("span",{class:"token template-punctuation string"},"`"),n("span",{class:"token string"},`
@@ -184,13 +184,13 @@ app`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function" `),n("span",{class:"token keyword"},"return"),s(),n("span",{class:"token punctuation"},"{"),s(" request"),n("span",{class:"token punctuation"},","),s(" enumOptions"),n("span",{class:"token punctuation"},","),s(" colorOptions"),n("span",{class:"token punctuation"},","),s(" submit"),n("span",{class:"token punctuation"},","),s(" onDelete"),n("span",{class:"token punctuation"},","),s(" close "),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` -`)])],-1),bs=n("p",null,[s("Effectively making form validation binding a transparent detail where all "),n("code",null,"@servicestack/vue"),s(" Input Components are able to automatically apply contextual validation errors next to the fields they apply to:")],-1),Ss=n("p",null,[n("img",{src:"https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/scripts/edit-contact-validation.png",alt:""})],-1),xs=n("h3",null,"AutoForm Components",-1),As=n("p",null,[s("We can elevate our productivity even further with "),n("a",{href:"https://docs.servicestack.net/vue/autoform"},"Auto Form Components"),s(" 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:")],-1),Cs=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("AutoCreateForm")]),s(),n("span",{class:"token attr-name"},"type"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("CreateBooking"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"formStyle"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("card"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token punctuation"},"/>")]),s(` -`)])],-1),js={class:"not-prose"},Ms=n("p",null,[s("The AutoForm components are powered by your "),n("a",{href:"https://docs.servicestack.net/vue/use-appmetadata"},"App Metadata"),s(" which allows creating highly customized UIs from "),n("a",{href:"https://docs.servicestack.net/locode/declarative"},"declarative C# attributes"),s(" whose customizations are reused across all ServiceStack Auto UIs, including:")],-1),Ts=n("ul",null,[n("li",null,[n("a",{href:"https://docs.servicestack.net/api-explorer"},"API Explorer")]),n("li",null,[n("a",{href:"https://docs.servicestack.net/locode/"},"Locode")]),n("li",null,[n("a",{href:"https://docs.servicestack.net/templates-blazor-components"},"Blazor Tailwind Components")])],-1),Is=n("h3",null,"Form Input Components",-1),Hs=n("p",null,[s("In addition to including Tailwind versions of the standard "),n("a",{href:"https://docs.servicestack.net/vue/form-inputs"},"HTML Form Inputs"),s(" controls to create beautiful Tailwind Forms, it also contains a variety of integrated high-level components:")],-1),Vs=n("ul",null,[n("li",null,[n("a",{href:"https://docs.servicestack.net/vue/fileinput"},"FileInput")]),n("li",null,[n("a",{href:"https://docs.servicestack.net/vue/taginput"},"TagInput")]),n("li",null,[n("a",{href:"https://docs.servicestack.net/vue/autocomplete"},"Autocomplete")])],-1),Ps=n("h3",null,"useAuth",-1),Ls=n("p",null,[s("Your Vue.js code can access Authenticated Users using "),n("a",{href:"https://docs.servicestack.net/vue/use-auth"},"useAuth()"),s(" which can also be populated without the overhead of an Ajax request by embedding the response of the built-in "),n("a",{href:"/ui/Authenticate?tab=details"},"Authenticate API"),s(" inside "),n("code",null,"_Layout.cshtml"),s(" with:")],-1),Js=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("script")]),s(),n("span",{class:"token attr-name"},"type"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("module"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token script"},[n("span",{class:"token language-javascript"},[s(` +`)])],-1),is=n("p",null,[s("Effectively making form validation binding a transparent detail where all "),n("code",null,"@servicestack/vue"),s(" Input Components are able to automatically apply contextual validation errors next to the fields they apply to:")],-1),us=n("p",null,[n("img",{src:"https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/scripts/edit-contact-validation.png",alt:""})],-1),rs=n("h3",null,"AutoForm Components",-1),ks=n("p",null,[s("We can elevate our productivity even further with "),n("a",{href:"https://docs.servicestack.net/vue/autoform"},"Auto Form Components"),s(" 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:")],-1),ds=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("AutoCreateForm")]),s(),n("span",{class:"token attr-name"},"type"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("CreateBooking"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"formStyle"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("card"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token punctuation"},"/>")]),s(` +`)])],-1),hs={class:"not-prose"},ms=n("p",null,[s("The AutoForm components are powered by your "),n("a",{href:"https://docs.servicestack.net/vue/use-appmetadata"},"App Metadata"),s(" which allows creating highly customized UIs from "),n("a",{href:"https://docs.servicestack.net/locode/declarative"},"declarative C# attributes"),s(" whose customizations are reused across all ServiceStack Auto UIs, including:")],-1),gs=n("ul",null,[n("li",null,[n("a",{href:"https://docs.servicestack.net/api-explorer"},"API Explorer")]),n("li",null,[n("a",{href:"https://docs.servicestack.net/locode/"},"Locode")]),n("li",null,[n("a",{href:"https://docs.servicestack.net/templates-blazor-components"},"Blazor Tailwind Components")])],-1),vs=n("h3",null,"Form Input Components",-1),fs=n("p",null,[s("In addition to including Tailwind versions of the standard "),n("a",{href:"https://docs.servicestack.net/vue/form-inputs"},"HTML Form Inputs"),s(" controls to create beautiful Tailwind Forms, it also contains a variety of integrated high-level components:")],-1),ys=n("ul",null,[n("li",null,[n("a",{href:"https://docs.servicestack.net/vue/fileinput"},"FileInput")]),n("li",null,[n("a",{href:"https://docs.servicestack.net/vue/taginput"},"TagInput")]),n("li",null,[n("a",{href:"https://docs.servicestack.net/vue/autocomplete"},"Autocomplete")])],-1),ws=n("h3",null,"useAuth",-1),_s=n("p",null,[s("Your Vue.js code can access Authenticated Users using "),n("a",{href:"https://docs.servicestack.net/vue/use-auth"},"useAuth()"),s(" which can also be populated without the overhead of an Ajax request by embedding the response of the built-in "),n("a",{href:"/ui/Authenticate?tab=details"},"Authenticate API"),s(" inside "),n("code",null,"_Layout.cshtml"),s(" with:")],-1),bs=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("script")]),s(),n("span",{class:"token attr-name"},"type"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("module"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token script"},[n("span",{class:"token language-javascript"},[s(` `),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" useAuth "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@@servicestack/vue"'),s(` `),n("span",{class:"token keyword"},"const"),s(),n("span",{class:"token punctuation"},"{"),s(" signIn "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token function"},"useAuth"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` `),n("span",{class:"token function"},"signIn"),n("span",{class:"token punctuation"},"("),s("@"),n("span",{class:"token keyword"},"await"),s(" Html"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"ApiAsJsonAsync"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"Authenticate"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),s(` `)])]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),qs=n("p",null,[s("Where it enables access to the below "),n("a",{href:"https://docs.servicestack.net/vue/use-auth"},"useAuth()"),s(" utils for inspecting the current authenticated user:")],-1),zs=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(),n("span",{class:"token punctuation"},"{"),s(` +`)])],-1),Ss=n("p",null,[s("Where it enables access to the below "),n("a",{href:"https://docs.servicestack.net/vue/use-auth"},"useAuth()"),s(" utils for inspecting the current authenticated user:")],-1),xs=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"const"),s(),n("span",{class:"token punctuation"},"{"),s(` signIn`),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token comment"},"// Sign In the currently Authenticated User"),s(` signOut`),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token comment"},"// Sign Out currently Authenticated User"),s(` user`),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token comment"},"// Access Authenticated User info in a reactive Ref"),s(` @@ -199,7 +199,7 @@ app`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function" hasPermission`),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token comment"},"// Check if the Authenticated User has a specific permission"),s(` isAdmin `),n("span",{class:"token comment"},"// Check if the Authenticated User has the Admin role"),s(` `),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token function"},"useAuth"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),Zs=n("p",null,[s("This is used in "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/main/MyApp/wwwroot/pages/Bookings.mjs"},"Bookings.mjs"),s(" to control whether the "),n("code",null,""),s(" component should enable its delete functionality:")],-1),Ds=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"default"),s(),n("span",{class:"token punctuation"},"{"),s(` +`)])],-1),As=n("p",null,[s("This is used in "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/main/MyApp/wwwroot/pages/Bookings.mjs"},"Bookings.mjs"),s(" to control whether the "),n("code",null,""),s(" component should enable its delete functionality:")],-1),Cs=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"export"),s(),n("span",{class:"token keyword"},"default"),s(),n("span",{class:"token punctuation"},"{"),s(` template`),n("span",{class:"token comment"},"/*html*/"),n("span",{class:"token operator"},":"),n("span",{class:"token template-string"},[n("span",{class:"token template-punctuation string"},"`"),n("span",{class:"token string"},` `),n("span",{class:"token template-punctuation string"},"`")]),n("span",{class:"token punctuation"},","),s(` @@ -209,51 +209,51 @@ app`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function" `),n("span",{class:"token keyword"},"return"),s(),n("span",{class:"token punctuation"},"{"),s(" canDelete "),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` -`)])],-1),Ns=n("h4",null,[n("a",{href:"https://jsdoc.app"},"JSDoc")],-1),Rs=n("p",null,[s("We get great value from using "),n("a",{href:"https://www.typescriptlang.org"},"TypeScript"),s(" 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.")],-1),Us=n("p",null,"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:",-1),Bs=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token comment"},"/** @param {KeyboardEvent} e */"),s(` +`)])],-1),js=n("h4",null,[n("a",{href:"https://jsdoc.app"},"JSDoc")],-1),Ms=n("p",null,[s("We get great value from using "),n("a",{href:"https://www.typescriptlang.org"},"TypeScript"),s(" 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.")],-1),Ts=n("p",null,"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:",-1),Is=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token comment"},"/** @param {KeyboardEvent} e */"),s(` `),n("span",{class:"token keyword"},"function"),s(),n("span",{class:"token function"},"validateSafeName"),n("span",{class:"token punctuation"},"("),n("span",{class:"token parameter"},"e"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token keyword"},"if"),s(),n("span",{class:"token punctuation"},"("),s("e"),n("span",{class:"token punctuation"},"."),s("key"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"match"),n("span",{class:"token punctuation"},"("),n("span",{class:"token regex"},[n("span",{class:"token regex-delimiter"},"/"),n("span",{class:"token regex-source language-regex"},"[\\W]+"),n("span",{class:"token regex-delimiter"},"/"),n("span",{class:"token regex-flags"},"g")]),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(` e`),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"preventDefault"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` `),n("span",{class:"token keyword"},"return"),s(),n("span",{class:"token boolean"},"false"),s(` `),n("span",{class:"token punctuation"},"}"),s(` `),n("span",{class:"token punctuation"},"}"),s(` -`)])],-1),Es=n("h4",null,"TypeScript Language Service",-1),Fs=n("p",null,[s("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 "),n("code",null,"/lib/typings"),s(", downloaded in "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/main/MyApp/postinstall.js"},"postinstall.js"),s(" after "),n("strong",null,"npm install"),s(".")],-1),Os=n("h3",null,"Import Maps",-1),$s=n("p",null,[n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap"},"Import Maps"),s(" 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:")],-1),Ws=n("pre",{class:"language-csharp"},[n("code",{class:"language-csharp"},[s("@Html"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"StaticImportMap"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(` +`)])],-1),Vs=n("h4",null,"TypeScript Language Service",-1),Hs=n("p",null,[s("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 "),n("code",null,"/lib/typings"),s(", downloaded in "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/main/MyApp/postinstall.js"},"postinstall.js"),s(" after "),n("strong",null,"npm install"),s(".")],-1),Ls=n("h3",null,"Import Maps",-1),Ps=n("p",null,[n("a",{href:"https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap"},"Import Maps"),s(" 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:")],-1),qs=n("pre",{class:"language-csharp"},[n("code",{class:"language-csharp"},[s("@Html"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"StaticImportMap"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"vue"'),n("span",{class:"token punctuation"},"]"),s(" "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token string"},'"/lib/mjs/vue.mjs"'),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"@servicestack/client"'),n("span",{class:"token punctuation"},"]"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token string"},'"/lib/mjs/servicestack-client.mjs"'),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"@servicestack/vue"'),n("span",{class:"token punctuation"},"]"),s(" "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token string"},'"/lib/mjs/servicestack-vue.mjs"'),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),Gs=n("p",null,"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:",-1),Qs=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" ref "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"vue"'),s(` +`)])],-1),Js=n("p",null,"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:",-1),zs=n("pre",{class:"language-js"},[n("code",{class:"language-js"},[n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" ref "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"vue"'),s(` `),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" useClient "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@servicestack/vue"'),s(` `),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" JsonApiClient"),n("span",{class:"token punctuation"},","),s(" $1"),n("span",{class:"token punctuation"},","),s(" on "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@servicestack/client"'),s(` -`)])],-1),Ks=n("p",null,[s("It’s a great solution for specifying using local unminified debug builds during "),n("strong",null,"Development"),s(", and more optimal CDN hosted production builds when running in "),n("strong",null,"Production"),s(", alleviating the need to rely on complex build tools to perform this code transformation for us:")],-1),Ys=n("pre",{class:"language-csharp"},[n("code",{class:"language-csharp"},[s("@Html"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"ImportMap"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` +`)])],-1),Zs=n("p",null,[s("It’s a great solution for specifying using local unminified debug builds during "),n("strong",null,"Development"),s(", and more optimal CDN hosted production builds when running in "),n("strong",null,"Production"),s(", alleviating the need to rely on complex build tools to perform this code transformation for us:")],-1),Ds=n("pre",{class:"language-csharp"},[n("code",{class:"language-csharp"},[s("@Html"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"ImportMap"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` `),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"vue"'),n("span",{class:"token punctuation"},"]"),s(" "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"/lib/mjs/vue.mjs"'),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token string"},'"https://unpkg.com/vue@3/dist/vue.esm-browser.prod.js"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"@servicestack/client"'),n("span",{class:"token punctuation"},"]"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"/lib/mjs/servicestack-client.mjs"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token string"},'"https://unpkg.com/@servicestack/client@2/dist/servicestack-client.min.mjs"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"@servicestack/vue"'),n("span",{class:"token punctuation"},"]"),s(" "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"/lib/mjs/servicestack-vue.mjs"'),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token string"},'"https://unpkg.com/@servicestack/vue@3/dist/servicestack-vue.min.mjs"'),n("span",{class:"token punctuation"},")"),s(` `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),Xs=n("p",null,"Note: Specifying exact versions of each dependency improves initial load times by eliminating latency from redirects.",-1),nt=n("p",null,"Or if you don’t want your Web App to reference any external dependencies, have the ImportMap reference local minified production builds instead:",-1),st=n("pre",{class:"language-csharp"},[n("code",{class:"language-csharp"},[s("@Html"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"ImportMap"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` +`)])],-1),Ns=n("p",null,"Note: Specifying exact versions of each dependency improves initial load times by eliminating latency from redirects.",-1),Rs=n("p",null,"Or if you don’t want your Web App to reference any external dependencies, have the ImportMap reference local minified production builds instead:",-1),Us=n("pre",{class:"language-csharp"},[n("code",{class:"language-csharp"},[s("@Html"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"ImportMap"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),s(` `),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"vue"'),n("span",{class:"token punctuation"},"]"),s(" "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"/lib/mjs/vue.mjs"'),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token string"},'"/lib/mjs/vue.min.mjs"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"@servicestack/client"'),n("span",{class:"token punctuation"},"]"),s(),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"/lib/mjs/servicestack-client.mjs"'),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token string"},'"/lib/mjs/servicestack-client.min.mjs"'),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},","),s(` `),n("span",{class:"token punctuation"},"["),n("span",{class:"token string"},'"@servicestack/vue"'),n("span",{class:"token punctuation"},"]"),s(" "),n("span",{class:"token operator"},"="),s(),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},'"/lib/mjs/servicestack-vue.mjs"'),n("span",{class:"token punctuation"},","),s(" "),n("span",{class:"token string"},'"/lib/mjs/servicestack-vue.min.mjs"'),n("span",{class:"token punctuation"},")"),s(` `),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),s(` -`)])],-1),tt=n("h4",null,"Polyfill for Safari",-1),at=n("p",null,[s("Unfortunately Safari is the last modern browser to "),n("a",{href:"https://caniuse.com/import-maps"},"support import maps"),s(" which is only now in Technical Preview. Luckily this feature can be polyfilled with the "),n("a",{href:"https://github.com/guybedford/es-module-shims"},"ES Module Shims"),s(":")],-1),et=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[s(`@if (Context.Request.Headers.UserAgent.Any(x => x.Contains("Safari") && !x.Contains("Chrome"))) +`)])],-1),Es=n("h4",null,"Polyfill for Safari",-1),Fs=n("p",null,[s("Unfortunately Safari is the last modern browser to "),n("a",{href:"https://caniuse.com/import-maps"},"support import maps"),s(" which is only now in Technical Preview. Luckily this feature can be polyfilled with the "),n("a",{href:"https://github.com/guybedford/es-module-shims"},"ES Module Shims"),s(":")],-1),Bs=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[s(`@if (Context.Request.Headers.UserAgent.Any(x => x.Contains("Safari") && !x.Contains("Chrome"))) { `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("script")]),s(),n("span",{class:"token attr-name"},"async"),s(),n("span",{class:"token attr-name"},"src"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("https://ga.jspm.io/npm:es-module-shims@1.6.3/dist/es-module-shims.js"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token script"}),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` } -`)])],-1),ot=n("h3",null,"Fast Component Loading",-1),ct=n("p",null,"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.",-1),lt=n("p",null,[s("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 "),n("a",{href:"/TodoMvc"},"/TodoMvc"),s(" does to load its initial list of todos using the "),n("a",{href:"https://docs.servicestack.net/service-gateway"},"Service Gateway"),s(" to invoke APIs in process and embed its JSON response with:")],-1),pt=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("script")]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token script"},[n("span",{class:"token language-javascript"},[s("todos "),n("span",{class:"token operator"},"="),s(" @"),n("span",{class:"token keyword"},"await"),s(),n("span",{class:"token function"},"ApiResultsAsJsonAsync"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"QueryTodos"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")")])]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])],-1),Os=n("h3",null,"Fast Component Loading",-1),Ws=n("p",null,"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.",-1),$s=n("p",null,[s("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 "),n("a",{href:"/TodoMvc"},"/TodoMvc"),s(" does to load its initial list of todos using the "),n("a",{href:"https://docs.servicestack.net/service-gateway"},"Service Gateway"),s(" to invoke APIs in process and embed its JSON response with:")],-1),Gs=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("script")]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token script"},[n("span",{class:"token language-javascript"},[s("todos "),n("span",{class:"token operator"},"="),s(" @"),n("span",{class:"token keyword"},"await"),s(),n("span",{class:"token function"},"ApiResultsAsJsonAsync"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token class-name"},"QueryTodos"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")")])]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("script")]),s(),n("span",{class:"token attr-name"},"type"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("module"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token script"},[n("span",{class:"token language-javascript"},[s(` `),n("span",{class:"token keyword"},"import"),s(" TodoMvc "),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"/Pages/TodoMvc.mjs"'),s(` `),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" mount "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"/mjs/app.mjs"'),s(` `),n("span",{class:"token function"},"mount"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},"'#todomvc'"),n("span",{class:"token punctuation"},","),s(" TodoMvc"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token punctuation"},"{"),s(" todos "),n("span",{class:"token punctuation"},"}"),n("span",{class:"token punctuation"},")"),s(` `)])]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),it=n("p",null,[s("Where "),n("code",null,"ApiResultsAsJsonAsync"),s(" is a simplified helper that uses the "),n("code",null,"Gateway"),s(" to call your API and returns its unencoded JSON response:")],-1),ut=n("pre",{class:"language-csharp"},[n("code",{class:"language-csharp"},[n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"await"),s(" Gateway"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"ApiAsync"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token constructor-invocation class-name"},"QueryTodos"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"."),s("Response"),n("span",{class:"token punctuation"},"?."),s("Results"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"AsRawJson"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(` -`)])],-1),rt=n("p",null,"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.",-1),kt=n("h3",null,"Fast Page Loading",-1),dt=n("p",null,[s("We can get SPA-like page loading performance using htmx’s "),n("a",{href:"https://htmx.org/docs/#boosting"},"Boosting"),s(" 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 "),n("code",null,""),s(".")],-1),ht=n("p",null,[s("This is used in "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/main/MyApp/Pages/Shared/Header.cshtml"},"Header.cshtml"),s(" to "),n("strong",null,"boost"),s(" all main navigation links:")],-1),mt=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("nav")]),s(),n("span",{class:"token attr-name"},"hx-boost"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("true"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` +`)])],-1),Qs=n("p",null,[s("Where "),n("code",null,"ApiResultsAsJsonAsync"),s(" is a simplified helper that uses the "),n("code",null,"Gateway"),s(" to call your API and returns its unencoded JSON response:")],-1),Ys=n("pre",{class:"language-csharp"},[n("code",{class:"language-csharp"},[n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"await"),s(" Gateway"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"ApiAsync"),n("span",{class:"token punctuation"},"("),n("span",{class:"token keyword"},"new"),s(),n("span",{class:"token constructor-invocation class-name"},"QueryTodos"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},"."),s("Response"),n("span",{class:"token punctuation"},"?."),s("Results"),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"AsRawJson"),n("span",{class:"token punctuation"},"("),n("span",{class:"token punctuation"},")"),n("span",{class:"token punctuation"},";"),s(` +`)])],-1),Ks=n("p",null,"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.",-1),Xs=n("h3",null,"Fast Page Loading",-1),nt=n("p",null,[s("We can get SPA-like page loading performance using htmx’s "),n("a",{href:"https://htmx.org/docs/#boosting"},"Boosting"),s(" 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 "),n("code",null,""),s(".")],-1),st=n("p",null,[s("This is used in "),n("a",{href:"https://github.com/NetCoreTemplates/razor/blob/main/MyApp/Pages/Shared/Header.cshtml"},"Header.cshtml"),s(" to "),n("strong",null,"boost"),s(" all main navigation links:")],-1),tt=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("nav")]),s(),n("span",{class:"token attr-name"},"hx-boost"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("true"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("ul")]),n("span",{class:"token punctuation"},">")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("li")]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("a")]),s(),n("span",{class:"token attr-name"},"href"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("/Blog"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s("Blog"),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),gt=n("p",null,[s("htmx has lots of useful "),n("a",{href:"https://htmx.org/examples/"},"real world examples"),s(" that can be activated with declarative attributes, another useful feature is the "),n("a",{href:"https://htmx.org/extensions/class-tools/"},"class-tools"),s(" extension to hide elements from appearing until after the page is loaded:")],-1),vt=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("signin"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])],-1),at=n("p",null,[s("htmx has lots of useful "),n("a",{href:"https://htmx.org/examples/"},"real world examples"),s(" that can be activated with declarative attributes, another useful feature is the "),n("a",{href:"https://htmx.org/extensions/class-tools/"},"class-tools"),s(" extension to hide elements from appearing until after the page is loaded:")],-1),et=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"id"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("signin"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"class"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("hidden mt-5 flex justify-center"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"classes"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("remove hidden:load"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),s(` @Html.SrcPage("SignIn.mjs") `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` -`)])],-1),ft=n("p",null,"Which is used to reduce UI yankiness from showing server rendered content before JS components have loaded.",-1),yt=n("h3",null,"@servicestack/vue Library",-1),wt=n("p",null,[n("a",{href:"https://docs.servicestack.net/vue/"},"@servicestack/vue"),s(" is our cornerstone library for enabling a highly productive Vue.js development model across our "),n("a",{href:"https://docs.servicestack.net/templates-vue"},"Vue Tailwind Project templates"),s(" which we’ll continue to significantly invest in to unlock even greater productivity benefits in all Vue Tailwind Apps.")],-1),_t=n("p",null,"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:",-1),Ct="Simple, Modern JavaScript",jt="Learn about JS Modules, Vue 3 and available rich UI Components",Mt=["js","dev"],Tt="https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000",It="Brandon Foley",Ht=[{property:"og:title",content:"Simple, Modern JavaScript"},{name:"twitter:title",content:"Simple, Modern JavaScript"},{property:"og:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}],Vt={__name:"2023-02-01_javascript",setup(l,{expose:t}){const a={title:"Simple, Modern JavaScript",summary:"Learn about JS Modules, Vue 3 and available rich UI Components",tags:["js","dev"],image:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000",author:"Brandon Foley",meta:[{property:"og:title",content:"Simple, Modern JavaScript"},{name:"twitter:title",content:"Simple, Modern JavaScript"},{property:"og:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]};return t({frontmatter:a}),j({title:"Simple, Modern JavaScript",meta:[{property:"og:title",content:"Simple, Modern JavaScript"},{name:"twitter:title",content:"Simple, Modern JavaScript"},{property:"og:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]}),(u,o)=>{const i=r("Iconify"),m=r("auto-create-form"),v=x;return k(),w(v,{frontmatter:a},{default:c(()=>[n("div",W,[e(i,{class:"sm:float-left mr-4 w-28 h-28",style:{"margin-top":"-4px"},icon:"vscode-icons:file-type-js-official"}),G,Q,K,Y,X,nn,sn,tn,an,en,on,cn,ln,pn,un,rn,n("div",kn,[e(T,{name:"Vue 3"})]),dn,hn,n("div",mn,[e(M)]),gn,vn,fn,yn,wn,_n,bn,Sn,xn,An,Cn,n("div",jn,[e(H,{id:"plugin",class:"text-2xl py-4"})]),Mn,Tn,In,Hn,Vn,Pn,Ln,Jn,e(U),qn,zn,Zn,Dn,Nn,Rn,Un,Bn,En,Fn,On,e(q,{value:"Vue 3",class:"w-full font-semibold"}),$n,Wn,Gn,Qn,Kn,Yn,Xn,ns,ss,ts,as,es,os,cs,ls,ps,is,us,rs,ks,ds,hs,ms,gs,vs,fs,ys,ws,_s,bs,Ss,xs,As,Cs,n("div",js,[e(m,{type:"CreateBooking","form-style":"card"})]),Ms,Ts,Is,Hs,Vs,Ps,Ls,Js,qs,zs,Zs,Ds,Ns,Rs,Us,Bs,Es,Fs,Os,$s,Ws,Gs,Qs,Ks,Ys,Xs,nt,st,tt,at,et,ot,ct,lt,pt,it,ut,rt,kt,dt,ht,mt,gt,vt,ft,yt,wt,_t,e($,{class:"mt-4"})])]),_:1})}}};export{It as author,Vt as default,Tt as image,Ht as meta,jt as summary,Mt as tags,Ct as title}; +`)])],-1),ot=n("p",null,"Which is used to reduce UI yankiness from showing server rendered content before JS components have loaded.",-1),ct=n("h3",null,"@servicestack/vue Library",-1),pt=n("p",null,[n("a",{href:"https://docs.servicestack.net/vue/"},"@servicestack/vue"),s(" is our cornerstone library for enabling a highly productive Vue.js development model across our "),n("a",{href:"https://docs.servicestack.net/templates-vue"},"Vue Tailwind Project templates"),s(" which we’ll continue to significantly invest in to unlock even greater productivity benefits in all Vue Tailwind Apps.")],-1),lt=n("p",null,"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:",-1),dt="Simple, Modern JavaScript",ht="Learn about JS Modules, Vue 3 and available rich UI Components",mt=["js","dev"],gt="https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000",vt="Brandon Foley",ft=[{property:"og:title",content:"Simple, Modern JavaScript"},{name:"twitter:title",content:"Simple, Modern JavaScript"},{property:"og:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}],yt={__name:"2023-02-01_javascript",setup(k,{expose:c}){const a={title:"Simple, Modern JavaScript",summary:"Learn about JS Modules, Vue 3 and available rich UI Components",tags:["js","dev"],image:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000",author:"Brandon Foley",meta:[{property:"og:title",content:"Simple, Modern JavaScript"},{name:"twitter:title",content:"Simple, Modern JavaScript"},{property:"og:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]};return c({frontmatter:a}),f({title:"Simple, Modern JavaScript",meta:[{property:"og:title",content:"Simple, Modern JavaScript"},{name:"twitter:title",content:"Simple, Modern JavaScript"},{property:"og:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1497515114629-f71d768fd07c?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]}),(l,o)=>{const u=i("Iconify"),m=i("auto-create-form"),g=v;return r(),y(g,{frontmatter:a},{default:e(()=>[n("div",q,[t(u,{class:"sm:float-left mr-4 w-28 h-28",style:{"margin-top":"-4px"},icon:"vscode-icons:file-type-js-official"}),J,z,Z,D,N,R,U,E,F,B,O,W,$,G,Q,Y,n("div",K,[t(w,{name:"Vue 3"})]),X,nn,n("div",sn,[t(S)]),tn,an,en,on,cn,pn,ln,un,rn,kn,dn,n("div",hn,[t(_,{id:"plugin",class:"text-2xl py-4"})]),mn,gn,vn,fn,yn,wn,_n,bn,t(T),Sn,xn,An,Cn,jn,Mn,Tn,In,Vn,Hn,Ln,t(b,{value:"Vue 3",class:"w-full font-semibold"}),Pn,qn,Jn,zn,Zn,Dn,Nn,Rn,Un,En,Fn,Bn,On,Wn,$n,Gn,Qn,Yn,Kn,Xn,ns,ss,ts,as,es,os,cs,ps,ls,is,us,rs,ks,ds,n("div",hs,[t(m,{type:"CreateBooking","form-style":"card"})]),ms,gs,vs,fs,ys,ws,_s,bs,Ss,xs,As,Cs,js,Ms,Ts,Is,Vs,Hs,Ls,Ps,qs,Js,zs,Zs,Ds,Ns,Rs,Us,Es,Fs,Bs,Os,Ws,$s,Gs,Qs,Ys,Ks,Xs,nt,st,tt,at,et,ot,ct,pt,lt,t(P,{class:"mt-4"})])]),_:1})}}};export{vt as author,yt as default,gt as image,ft as meta,ht as summary,mt as tags,dt as title}; diff --git a/assets/2023-03-30_razor-ssg-BsYRmb3g.js b/assets/2023-03-30_razor-ssg-CI_Ztr2P.js similarity index 99% rename from assets/2023-03-30_razor-ssg-BsYRmb3g.js rename to assets/2023-03-30_razor-ssg-CI_Ztr2P.js index bcfb4ff..7d57b73 100644 --- a/assets/2023-03-30_razor-ssg-BsYRmb3g.js +++ b/assets/2023-03-30_razor-ssg-CI_Ztr2P.js @@ -1,4 +1,4 @@ -import{_ as l}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{_ as c}from"./GettingStarted.vue_vue_type_script_setup_true_lang-CdAW7Hil.js";import{g as p,o as i,h as u,w as r,a as n,b as k,e as s}from"./index-BXwhoUEp.js";const d={class:"markdown-body"},h=n("p",null,[s("Razor SSG is a Razor Pages powered Markdown alternative to "),n("a",{href:"https://jekyllrb.com/"},"Ruby’s Jekyll"),s(" & "),n("a",{href:"https://nextjs.org"},"Next.js"),s(" that’s ideal for generating static websites & blogs using C#, Razor Pages & Markdown.")],-1),g=n("h3",null,"GitHub Codespaces Friendly",-1),m=n("p",null,"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!",-1),w=n("p",null,[n("a",{href:"https://github.com/features/codespaces"},[n("img",{src:"https://servicestack.net/img/posts/razor-ssg/codespaces.png",alt:""})])],-1),f=n("p",null,[s("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 "),n("strong",null,"gh-pages"),s(" branch where it’s hosted for free on GitHub Pages CDN:")],-1),y=n("iframe",{class:"youtube",src:"https://www.youtube.com/embed/MRQMBrXi5Sc",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",allowfullscreen:""},null,-1),b=n("h3",null,"Enhance with simple, modern JavaScript",-1),_=n("p",null,[s("For enhanced interactivity, static markdown content can be "),n("a",{href:"https://servicestack.net/posts/javascript"},"progressively enhanced"),s(" with Vue 3 components, as done in this example that embed’s the "),n("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/mjs/components/GettingStarted.mjs"},"GettingStarted.mjs"),s(" Vue Component to create new Razor SSG App’s below with:")],-1),v=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("getting-started")]),s(),n("span",{class:"token attr-name"},"template"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("razor-ssg"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +import{_ as l}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{_ as c}from"./GettingStarted.vue_vue_type_script_setup_true_lang-C2rK_3Mh.js";import{i as p,o as i,h as u,w as r,a as n,b as k,e as s}from"./index-DWUkzBt0.js";const d={class:"markdown-body"},h=n("p",null,[s("Razor SSG is a Razor Pages powered Markdown alternative to "),n("a",{href:"https://jekyllrb.com/"},"Ruby’s Jekyll"),s(" & "),n("a",{href:"https://nextjs.org"},"Next.js"),s(" that’s ideal for generating static websites & blogs using C#, Razor Pages & Markdown.")],-1),g=n("h3",null,"GitHub Codespaces Friendly",-1),m=n("p",null,"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!",-1),w=n("p",null,[n("a",{href:"https://github.com/features/codespaces"},[n("img",{src:"https://servicestack.net/img/posts/razor-ssg/codespaces.png",alt:""})])],-1),f=n("p",null,[s("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 "),n("strong",null,"gh-pages"),s(" branch where it’s hosted for free on GitHub Pages CDN:")],-1),y=n("iframe",{class:"youtube",src:"https://www.youtube.com/embed/MRQMBrXi5Sc",frameborder:"0",allow:"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",allowfullscreen:""},null,-1),b=n("h3",null,"Enhance with simple, modern JavaScript",-1),_=n("p",null,[s("For enhanced interactivity, static markdown content can be "),n("a",{href:"https://servicestack.net/posts/javascript"},"progressively enhanced"),s(" with Vue 3 components, as done in this example that embed’s the "),n("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/mjs/components/GettingStarted.mjs"},"GettingStarted.mjs"),s(" Vue Component to create new Razor SSG App’s below with:")],-1),v=n("pre",{class:"language-html"},[n("code",{class:"language-html"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("getting-started")]),s(),n("span",{class:"token attr-name"},"template"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("razor-ssg"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` `)])],-1),M={class:"py-8 not-prose text-base flex justify-center"},P=n("p",null,[s("Although with full control over the websites "),n("code",null,"_Layout.cshtml"),s(", you’re free to use any preferred JS Module or Web Component you prefer.")],-1),x=n("h2",null,"Razor Pages",-1),S=n("p",null,[s("Your website can be built using either Markdown "),n("code",null,".md"),s(" or Razor "),n("code",null,".cshtml"),s(" pages, although it’s generally recommended to use Markdown to capture the static content for your website for improved productivity and ease of maintenance.")],-1),z=n("h3",null,"Content in Markdown, Functionality in Razor Pages",-1),A=n("p",null,"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.",-1),C=n("p",null,"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.",-1),T=n("h3",null,"Markdown Feature Structure",-1),R=n("p",null,[s("All markdown features are effectively implemented in the same way, starting with a "),n("strong",null,"_folder"),s(" for maintaining its static markdown content, a "),n("strong",null,".cs"),s(" class to load the markdown and a "),n("strong",null,".cshtml"),s(" Razor Page to render it:")],-1),N=n("table",null,[n("thead",null,[n("tr",null,[n("th",null,"Location"),n("th",null,"Description")])]),n("tbody",null,[n("tr",null,[n("td",null,[n("code",null,"/_{Feature}")]),n("td",null,"Maintains the static markdown for the feature")]),n("tr",null,[n("td",null,[n("code",null,"Markdown.{Feature}.cs")]),n("td",null,"Functionality to read the feature’s markdown into logical collections")]),n("tr",null,[n("td",null,[n("code",null,"{Feature}.cshtml")]),n("td",null,"Functionality to Render the feature")]),n("tr",null,[n("td",null,[n("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Configure.Ssg.cs"},"Configure.Ssg.cs")]),n("td",null,"Initializes and registers the feature with ASP .NET’s IOC")])])],-1),L=n("p",null,"Lets see what this looks like in practice by walking through the “Pages” feature:",-1),F=n("h2",null,"Pages Feature",-1),G=n("p",null,[s("The pages feature simply makes all pages in the "),n("strong",null,"_pages"),s(" folder, available from "),n("code",null,"/{filename}"),s(".")],-1),I=n("p",null,"Where the included pages:",-1),B=n("h3",null,[n("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/tree/main/MyApp/_pages"},"/_pages")],-1),V=n("ul",null,[n("li",null,"privacy.md"),n("li",null,"speaking.md"),n("li",null,"uses.md")],-1),D=n("p",null,"Are made available from:",-1),H=n("ul",null,[n("li",null,[n("a",{href:"https://razor-ssg.web-templates.io/privacy"},"/privacy")]),n("li",null,[n("a",{href:"https://razor-ssg.web-templates.io/speaking"},"/speaking")]),n("li",null,[n("a",{href:"https://razor-ssg.web-templates.io/uses"},"/uses")])],-1),j=n("h3",null,"Loading Pages Markdown",-1),W=n("p",null,[s("The code that loads the Pages feature markdown content is in "),n("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Markdown.Pages.cs"},"Markdown.Pages.cs"),s(":")],-1),E=n("pre",{class:"language-csharp"},[n("code",{class:"language-csharp"},[n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token keyword"},"class"),s(),n("span",{class:"token class-name"},"MarkdownPages"),s(),n("span",{class:"token punctuation"},":"),s(),n("span",{class:"token type-list"},[n("span",{class:"token class-name"},[s("MarkdownPagesBase"),n("span",{class:"token punctuation"},"<"),s("MarkdownFileInfo"),n("span",{class:"token punctuation"},">")])]),s(` `),n("span",{class:"token punctuation"},"{"),s(` `),n("span",{class:"token keyword"},"public"),s(),n("span",{class:"token function"},"MarkdownPages"),n("span",{class:"token punctuation"},"("),n("span",{class:"token class-name"},[s("ILogger"),n("span",{class:"token punctuation"},"<"),s("MarkdownPages"),n("span",{class:"token punctuation"},">")]),s(" log"),n("span",{class:"token punctuation"},","),s(),n("span",{class:"token class-name"},"IWebHostEnvironment"),s(" env"),n("span",{class:"token punctuation"},")"),s(` diff --git a/assets/2023-08-23_razor-ssg-new-blog-features-D0tLlErM.js b/assets/2023-08-23_razor-ssg-new-blog-features-DfRWXA11.js similarity index 59% rename from assets/2023-08-23_razor-ssg-new-blog-features-D0tLlErM.js rename to assets/2023-08-23_razor-ssg-new-blog-features-DfRWXA11.js index 4dc3b48..edc69e3 100644 --- a/assets/2023-08-23_razor-ssg-new-blog-features-D0tLlErM.js +++ b/assets/2023-08-23_razor-ssg-new-blog-features-DfRWXA11.js @@ -1,10 +1,10 @@ -import{_ as f}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{_ as b}from"./GettingStarted.vue_vue_type_script_setup_true_lang-CdAW7Hil.js";import{p as y,d,j as v,q as S,s as k,r as x,o as n,c as a,F as p,i as u,a as t,t as m,h,g as C,w as z,b as i,u as M,e as s}from"./index-BXwhoUEp.js";const j=y("https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js"),B=d({props:["type","data","options"],setup(o){const e=v();return S(async()=>{await j;const r=o.options||{responsive:!0,legend:{position:"top"}};new Chart(e.value,{type:o.type||"bar",data:o.data,options:r})}),()=>k("div",{},[k("canvas",{ref:e})])}}),R={key:0},P={class:"ml-6 flex items-center text-base leading-8"},T={class:"mr-1 text-slate-600","aria-hidden":"true",focusable:"false",role:"img",viewBox:"0 0 16 16",width:"16",height:"16",fill:"currentColor",style:{display:"inline-block","user-select":"none","vertical-align":"text-bottom",overflow:"visible"}},G=t("path",{d:"M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"},null,-1),N=[G],A={key:1},F={class:"ml-6"},L={class:"flex items-center text-base leading-8"},$={class:"mr-1 text-slate-600","aria-hidden":"true",focusable:"false",role:"img",viewBox:"0 0 12 12",width:"12",height:"12",fill:"currentColor",style:{display:"inline-block","user-select":"none","vertical-align":"text-bottom",overflow:"visible"}},V=t("path",{d:"M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1.3-.3.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0 .3.3.3.8 0 1.1l-3.2 3.2c-.2.2-.4.3-.6.3Z"},null,-1),I=[V],W={class:"mr-1 text-sky-500","aria-hidden":"true",focusable:"false",role:"img",viewBox:"0 0 16 16",width:"16",height:"16",fill:"currentColor",style:{display:"inline-block","user-select":"none","vertical-align":"text-bottom",overflow:"visible"}},J=t("path",{d:"M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1H13a1 1 0 0 1 1 1v.5H2.75a.75.75 0 0 0 0 1.5h11.978a1 1 0 0 1 .994 1.117L15 13.25A1.75 1.75 0 0 1 13.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75c0-.464.184-.91.513-1.237Z"},null,-1),q=[J],E=d({__name:"File",props:{label:{},contents:{},depth:{}},setup(o){return(e,r)=>{const c=x("File",!0);return n(),a("div",null,[e.label=="_"?(n(),a("div",R,[(n(!0),a(p,null,u(e.contents,l=>(n(),a("div",P,[(n(),a("svg",T,N)),t("span",null,m(l),1)]))),256))])):(n(),a("div",A,[t("div",F,[t("div",L,[(n(),a("svg",$,I)),(n(),a("svg",W,q)),t("span",null,m(e.label),1)]),(n(!0),a(p,null,u(e.contents,(l,g)=>(n(),h(c,{label:g,contents:l},null,8,["label","contents"]))),256))])]))])}}}),H=d({__name:"FileLayout",props:{files:{}},setup(o){return(e,r)=>(n(),a("div",null,[(n(!0),a(p,null,u(e.files,(c,l)=>(n(),h(E,{label:l,contents:c},null,8,["label","contents"]))),256))]))}}),D={class:"markdown-body"},U=t("h2",null,"New Blogging features in Razor SSG",-1),Q=t("p",null,[t("a",{href:"https://razor-ssg.web-templates.io"},"Razor SSG"),s(" 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 "),t("a",{href:"https://github.com/ServiceStack/servicestack.net"},"servicestack.net(github)"),s(" website is that any improvements added to our website end up being rolled into the Razor SSG Project Template for everyone else to enjoy.")],-1),Z=t("p",null,[s("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 "),t("a",{href:"https://servicestack.net/posts/year/2023"},"22nd Blog Post for the year"),s(" with improvements in both discoverability and capability of blog posts:")],-1),O=t("h3",null,"RSS Feed",-1),Y=t("p",null,"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:",-1),K=t("div",{class:"not-prose my-8"},[t("a",{href:"https://razor-ssg.web-templates.io/feed.xml"},[t("div",{class:"block flex justify-center shadow hover:shadow-lg rounded overflow-hidden"},[t("img",{class:"max-w-3xl py-8",src:"https://servicestack.net/img/posts/razor-ssg/valid-rss.png"})])]),t("div",{class:"mt-4 flex justify-center"},[t("a",{class:"text-indigo-600 hover:text-indigo-800",href:"https://razor-ssg.web-templates.io/feed.xml"},"https://razor-ssg.web-templates.io/feed.xml"),t("a",{class:"ml-4 text-indigo-600 hover:text-indigo-800",href:"https://servicestack.net/feed.xml"},"https://servicestack.net/feed.xml")])],-1),X=t("h3",null,"Meta Headers support for Twitter cards and SEO",-1),tt=t("p",null,[s("Blog Posts and Pages now include additional "),t("code",null,""),s(" HTML Headers to enable support for "),t("a",{href:"https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards"},"Twitter Cards"),s(" in both Twitter and Meta’s new "),t("a",{href:"https://threads.net"},"threads.net"),s(", e.g:")],-1),st=t("div",{class:"not-prose my-8 flex justify-center"},[t("a",{class:"block max-w-2xl",href:"https://www.threads.net/@servicestack/post/CvIFobPBs5h"},[t("div",{class:"block flex justify-center shadow hover:shadow-lg rounded overflow-hidden"},[t("img",{class:"py-8",src:"https://servicestack.net/img/posts/razor-ssg/twitter-cards.png"})])])],-1),nt=t("h3",null,"Improved Discoverability",-1),et=t("p",null,"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:",-1),at=t("p",null,[t("img",{src:"https://servicestack.net/img/posts/razor-ssg/other-author-posts.png",alt:""})],-1),ot=t("h3",null,"Posts can include Vue Components",-1),rt=t("p",null,"Blog Posts can now embed any global Vue Components directly in its Markdown, e.g:",-1),lt=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),s("getting-started")]),s(),t("span",{class:"token attr-name"},"template"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),s("razor-ssg"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),s(` -`)])],-1),ct=t("h4",null,[t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/mjs/components/GettingStarted.mjs"},"/mjs/components/GettingStarted.mjs")],-1),it={class:"not-prose my-20 flex justify-center"},pt=t("h4",null,"Individual Blog Post dependencies",-1),ut=t("p",null,[s("Just like Pages and Docs they can also include specific JavaScript "),t("strong",null,".mjs"),s(" or "),t("strong",null,".css"),s(" in the "),t("code",null,"/wwwroot/posts"),s(" folder which will only be loaded for that post:")],-1),dt=t("p",null,[s("Now posts that need it can dynamically load large libraries like "),t("a",{href:"https://www.chartjs.org"},"Chart.js"),s(" and use it inside a custom Vue component by creating a custom "),t("code",null,"/posts/.mjs"),s(" that exports what components and features your blog post needs, e.g:")],-1),ht=t("h4",null,[t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/posts/new-blog-features.mjs"},"/posts/new-blog-features.mjs")],-1),gt=t("pre",{class:"language-js"},[t("code",{class:"language-js"},[t("span",{class:"token keyword"},"import"),s(" ChartJs "),t("span",{class:"token keyword"},"from"),s(),t("span",{class:"token string"},"'./components/ChartJs.mjs'"),s(` +import{_}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{_ as f}from"./GettingStarted.vue_vue_type_script_setup_true_lang-C2rK_3Mh.js";import{d as k,r as b,o as n,c as e,F as i,g as p,a as t,t as g,h as u,i as y,w as v,b as c,u as S,e as s}from"./index-DWUkzBt0.js";import{C as x}from"./ChartJs-3tDylpqN.js";const C={key:0},z={class:"ml-6 flex items-center text-base leading-8"},M={class:"mr-1 text-slate-600","aria-hidden":"true",focusable:"false",role:"img",viewBox:"0 0 16 16",width:"16",height:"16",fill:"currentColor",style:{display:"inline-block","user-select":"none","vertical-align":"text-bottom",overflow:"visible"}},j=t("path",{d:"M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"},null,-1),B=[j],R={key:1},P={class:"ml-6"},T={class:"flex items-center text-base leading-8"},G={class:"mr-1 text-slate-600","aria-hidden":"true",focusable:"false",role:"img",viewBox:"0 0 12 12",width:"12",height:"12",fill:"currentColor",style:{display:"inline-block","user-select":"none","vertical-align":"text-bottom",overflow:"visible"}},N=t("path",{d:"M6 8.825c-.2 0-.4-.1-.5-.2l-3.3-3.3c-.3-.3-.3-.8 0-1.1.3-.3.8-.3 1.1 0l2.7 2.7 2.7-2.7c.3-.3.8-.3 1.1 0 .3.3.3.8 0 1.1l-3.2 3.2c-.2.2-.4.3-.6.3Z"},null,-1),A=[N],F={class:"mr-1 text-sky-500","aria-hidden":"true",focusable:"false",role:"img",viewBox:"0 0 16 16",width:"16",height:"16",fill:"currentColor",style:{display:"inline-block","user-select":"none","vertical-align":"text-bottom",overflow:"visible"}},L=t("path",{d:"M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1H13a1 1 0 0 1 1 1v.5H2.75a.75.75 0 0 0 0 1.5h11.978a1 1 0 0 1 .994 1.117L15 13.25A1.75 1.75 0 0 1 13.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75c0-.464.184-.91.513-1.237Z"},null,-1),$=[L],V=k({__name:"File",props:{label:{},contents:{},depth:{}},setup(d){return(a,r)=>{const l=b("File",!0);return n(),e("div",null,[a.label=="_"?(n(),e("div",C,[(n(!0),e(i,null,p(a.contents,o=>(n(),e("div",z,[(n(),e("svg",M,B)),t("span",null,g(o),1)]))),256))])):(n(),e("div",R,[t("div",P,[t("div",T,[(n(),e("svg",G,A)),(n(),e("svg",F,$)),t("span",null,g(a.label),1)]),(n(!0),e(i,null,p(a.contents,(o,h)=>(n(),u(l,{label:h,contents:o},null,8,["label","contents"]))),256))])]))])}}}),I=k({__name:"FileLayout",props:{files:{}},setup(d){return(a,r)=>(n(),e("div",null,[(n(!0),e(i,null,p(a.files,(l,o)=>(n(),u(V,{label:o,contents:l},null,8,["label","contents"]))),256))]))}}),W={class:"markdown-body"},E=t("h2",null,"New Blogging features in Razor SSG",-1),H=t("p",null,[t("a",{href:"https://razor-ssg.web-templates.io"},"Razor SSG"),s(" 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 "),t("a",{href:"https://github.com/ServiceStack/servicestack.net"},"servicestack.net(github)"),s(" website is that any improvements added to our website end up being rolled into the Razor SSG Project Template for everyone else to enjoy.")],-1),J=t("p",null,[s("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 "),t("a",{href:"https://servicestack.net/posts/year/2023"},"22nd Blog Post for the year"),s(" with improvements in both discoverability and capability of blog posts:")],-1),q=t("h3",null,"RSS Feed",-1),D=t("p",null,"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:",-1),U=t("div",{class:"not-prose my-8"},[t("a",{href:"https://razor-ssg.web-templates.io/feed.xml"},[t("div",{class:"block flex justify-center shadow hover:shadow-lg rounded overflow-hidden"},[t("img",{class:"max-w-3xl py-8",src:"https://servicestack.net/img/posts/razor-ssg/valid-rss.png"})])]),t("div",{class:"mt-4 flex justify-center"},[t("a",{class:"text-indigo-600 hover:text-indigo-800",href:"https://razor-ssg.web-templates.io/feed.xml"},"https://razor-ssg.web-templates.io/feed.xml"),t("a",{class:"ml-4 text-indigo-600 hover:text-indigo-800",href:"https://servicestack.net/feed.xml"},"https://servicestack.net/feed.xml")])],-1),Q=t("h3",null,"Meta Headers support for Twitter cards and SEO",-1),Z=t("p",null,[s("Blog Posts and Pages now include additional "),t("code",null,""),s(" HTML Headers to enable support for "),t("a",{href:"https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/abouts-cards"},"Twitter Cards"),s(" in both Twitter and Meta’s new "),t("a",{href:"https://threads.net"},"threads.net"),s(", e.g:")],-1),O=t("div",{class:"not-prose my-8 flex justify-center"},[t("a",{class:"block max-w-2xl",href:"https://www.threads.net/@servicestack/post/CvIFobPBs5h"},[t("div",{class:"block flex justify-center shadow hover:shadow-lg rounded overflow-hidden"},[t("img",{class:"py-8",src:"https://servicestack.net/img/posts/razor-ssg/twitter-cards.png"})])])],-1),Y=t("h3",null,"Improved Discoverability",-1),K=t("p",null,"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:",-1),X=t("p",null,[t("img",{src:"https://servicestack.net/img/posts/razor-ssg/other-author-posts.png",alt:""})],-1),tt=t("h3",null,"Posts can include Vue Components",-1),st=t("p",null,"Blog Posts can now embed any global Vue Components directly in its Markdown, e.g:",-1),nt=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),s("getting-started")]),s(),t("span",{class:"token attr-name"},"template"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),s("razor-ssg"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),s(` +`)])],-1),et=t("h4",null,[t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/mjs/components/GettingStarted.mjs"},"/mjs/components/GettingStarted.mjs")],-1),at={class:"not-prose my-20 flex justify-center"},ot=t("h4",null,"Individual Blog Post dependencies",-1),rt=t("p",null,[s("Just like Pages and Docs they can also include specific JavaScript "),t("strong",null,".mjs"),s(" or "),t("strong",null,".css"),s(" in the "),t("code",null,"/wwwroot/posts"),s(" folder which will only be loaded for that post:")],-1),lt=t("p",null,[s("Now posts that need it can dynamically load large libraries like "),t("a",{href:"https://www.chartjs.org"},"Chart.js"),s(" and use it inside a custom Vue component by creating a custom "),t("code",null,"/posts/.mjs"),s(" that exports what components and features your blog post needs, e.g:")],-1),ct=t("h4",null,[t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/posts/new-blog-features.mjs"},"/posts/new-blog-features.mjs")],-1),it=t("pre",{class:"language-js"},[t("code",{class:"language-js"},[t("span",{class:"token keyword"},"import"),s(" ChartJs "),t("span",{class:"token keyword"},"from"),s(),t("span",{class:"token string"},"'./components/ChartJs.mjs'"),s(` `),t("span",{class:"token keyword"},"export"),s(),t("span",{class:"token keyword"},"default"),s(),t("span",{class:"token punctuation"},"{"),s(` `),t("span",{class:"token literal-property property"},"components"),t("span",{class:"token operator"},":"),s(),t("span",{class:"token punctuation"},"{"),s(" ChartJs "),t("span",{class:"token punctuation"},"}"),s(` `),t("span",{class:"token punctuation"},"}"),s(` -`)])],-1),kt=t("p",null,[s("In this case it enables support for "),t("a",{href:"https://www.chartjs.org"},"Chart.js"),s(" by including a custom Vue component that makes it easy to create charts from Vue Components embedded in markdown:")],-1),mt=t("h4",null,[t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/posts/components/ChartJs.mjs"},"/posts/components/ChartJs.mjs")],-1),wt=t("pre",{class:"language-js"},[t("code",{class:"language-js"},[t("span",{class:"token keyword"},"import"),s(),t("span",{class:"token punctuation"},"{"),s(" ref"),t("span",{class:"token punctuation"},","),s(" onMounted "),t("span",{class:"token punctuation"},"}"),s(),t("span",{class:"token keyword"},"from"),s(),t("span",{class:"token string"},'"vue"'),s(` +`)])],-1),pt=t("p",null,[s("In this case it enables support for "),t("a",{href:"https://www.chartjs.org"},"Chart.js"),s(" by including a custom Vue component that makes it easy to create charts from Vue Components embedded in markdown:")],-1),ut=t("h4",null,[t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/wwwroot/posts/components/ChartJs.mjs"},"/posts/components/ChartJs.mjs")],-1),dt=t("pre",{class:"language-js"},[t("code",{class:"language-js"},[t("span",{class:"token keyword"},"import"),s(),t("span",{class:"token punctuation"},"{"),s(" ref"),t("span",{class:"token punctuation"},","),s(" onMounted "),t("span",{class:"token punctuation"},"}"),s(),t("span",{class:"token keyword"},"from"),s(),t("span",{class:"token string"},'"vue"'),s(` `),t("span",{class:"token keyword"},"import"),s(),t("span",{class:"token punctuation"},"{"),s(" addScript "),t("span",{class:"token punctuation"},"}"),s(),t("span",{class:"token keyword"},"from"),s(),t("span",{class:"token string"},'"@servicestack/client"'),s(` `),t("span",{class:"token keyword"},"let"),s(" loadJs "),t("span",{class:"token operator"},"="),s(),t("span",{class:"token function"},"addScript"),t("span",{class:"token punctuation"},"("),t("span",{class:"token string"},"'https://cdn.jsdelivr.net/npm/chart.js/dist/chart.umd.min.js'"),t("span",{class:"token punctuation"},")"),s(` @@ -33,7 +33,7 @@ import{_ as f}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.j `),t("span",{class:"token keyword"},"return"),s(),t("span",{class:"token punctuation"},"{"),s(" chart "),t("span",{class:"token punctuation"},"}"),s(` `),t("span",{class:"token punctuation"},"}"),s(` `),t("span",{class:"token punctuation"},"}"),s(` -`)])],-1),_t=t("p",null,[s("Which allows this post to embed Chart.js charts using the above custom "),t("code",null,""),s(" Vue component and a JS Object literal, e.g:")],-1),ft=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),s("chart-js")]),s(),t("span",{class:"token attr-name"},":data"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),s(`{ +`)])],-1),ht=t("p",null,[s("Which allows this post to embed Chart.js charts using the above custom "),t("code",null,""),s(" Vue component and a JS Object literal, e.g:")],-1),gt=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),s("chart-js")]),s(),t("span",{class:"token attr-name"},":data"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),s(`{ labels: [ //... ], @@ -41,7 +41,7 @@ import{_ as f}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.j //... ] }`),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),s(` -`)])],-1),bt=t("p",null,[s("Which the "),t("a",{href:"https://servicestack.net/posts/bulk-insert-performance"},"Bulk Insert Performance"),s(" Blog Post uses extensively to embeds its Chart.js Bar charts:")],-1),yt=t("h3",null,"New Markdown Containers",-1),vt=t("p",null,[t("a",{href:"https://github.com/xoofx/markdig/blob/master/src/Markdig.Tests/Specs/CustomContainerSpecs.md"},"Custom Containers"),s(" are a popular method for implementing Markdown Extensions for enabling rich, wrist-friendly consistent content in your Markdown documents.")],-1),St=t("p",null,[s("Most of "),t("a",{href:"https://vitepress.dev/guide/markdown#custom-containers"},"VitePress Markdown Containers"),s(" are also available in Razor SSG websites for enabling rich, wrist-friendly consistent markup in your Markdown pages, e.g:")],-1),xt=t("pre",{class:"language-md"},[t("code",{class:"language-md"},`:::info +`)])],-1),kt=t("p",null,[s("Which the "),t("a",{href:"https://servicestack.net/posts/bulk-insert-performance"},"Bulk Insert Performance"),s(" Blog Post uses extensively to embeds its Chart.js Bar charts:")],-1),mt=t("h3",null,"New Markdown Containers",-1),wt=t("p",null,[t("a",{href:"https://github.com/xoofx/markdig/blob/master/src/Markdig.Tests/Specs/CustomContainerSpecs.md"},"Custom Containers"),s(" are a popular method for implementing Markdown Extensions for enabling rich, wrist-friendly consistent content in your Markdown documents.")],-1),_t=t("p",null,[s("Most of "),t("a",{href:"https://vitepress.dev/guide/markdown#custom-containers"},"VitePress Markdown Containers"),s(" are also available in Razor SSG websites for enabling rich, wrist-friendly consistent markup in your Markdown pages, e.g:")],-1),ft=t("pre",{class:"language-md"},[t("code",{class:"language-md"},`:::info This is an info box. ::: @@ -60,14 +60,14 @@ This is a dangerous warning. :::copy Copy Me! ::: -`)],-1),Ct=t("div",{class:"info custom-block"},[t("p",{class:"custom-block-title"},"INFO"),t("p",null,"This is an info box.")],-1),zt=t("div",{class:"tip custom-block"},[t("p",{class:"custom-block-title"},"TIP"),t("p",null,"This is a tip.")],-1),Mt=t("div",{class:"warning custom-block"},[t("p",{class:"custom-block-title"},"WARNING"),t("p",null,"This is a warning.")],-1),jt=t("div",{class:"danger custom-block"},[t("p",{class:"custom-block-title"},"DANGER"),t("p",null,"This is a dangerous warning.")],-1),Bt=t("div",{class:"not-prose copy cp flex cursor-pointer mb-3",onclick:"copy(this)"},[t("div",{class:"flex-grow bg-gray-700"},[t("div",{class:"pl-4 py-1 pb-1.5 align-middle text-lg text-white"},[t("p",null,"Copy Me!")])]),t("div",{class:"flex"},[t("div",{class:"bg-sky-500 text-white p-1.5 pb-0"},[t("svg",{class:"copied w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"})]),t("svg",{class:"nocopy w-6 h-6",title:"copy",fill:"none",stroke:"white",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"1",d:"M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"})])])])],-1),Rt=t("p",null,[s("See Razor Press’s "),t("a",{href:"https://razor-press.web-templates.io/containers"},"Markdown Containers docs"),s(" for the complete list of available containers and examples on how to implement your own "),t("a",{href:"https://razor-press.web-templates.io/containers#implementing-block-containers"},"Custom Markdown containers"),s(".")],-1),Pt=t("h3",null,"Support for Includes",-1),Tt=t("p",null,[s("Markdown fragments can be added to "),t("code",null,"_pages/_include"),s(" - a special folder rendered with "),t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Includes.cshtml"},"Pages/Includes.cshtml"),s(" using an "),t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Shared/_LayoutEmpty.cshtml"},"Empty Layout"),s(" which can be included in other Markdown and Razor Pages or fetched on demand with Ajax.")],-1),Gt=t("p",null,[s("Markdown Fragments can be then included inside other markdown documents with the "),t("code",null,"::include"),s(" inline container, e.g:")],-1),Nt=t("div",{class:"pre"},[t("p",null,"::include vue/formatters.md::")],-1),At=t("p",null,[s("Where it will be replaced with the HTML rendered markdown contents of fragments maintained in "),t("code",null,"_pages/_include"),s(".")],-1),Ft=t("h3",null,"Include Markdown in Razor Pages",-1),Lt=t("p",null,[s("Markdown Fragments can also be included in Razor Pages using the custom "),t("code",null,"MarkdownTagHelper.cs"),s(),t("code",null,""),s(" tag:")],-1),$t=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),s("markdown")]),s(),t("span",{class:"token attr-name"},"include"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),s("vue/formatters.md"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),s(` -`)])],-1),Vt=t("h3",null,"Inline Markdown in Razor Pages",-1),It=t("p",null,"Alternatively markdown can be rendered inline with:",-1),Wt=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),s("markdown")]),t("span",{class:"token punctuation"},">")]),s(` +`)],-1),bt=t("div",{class:"info custom-block"},[t("p",{class:"custom-block-title"},"INFO"),t("p",null,"This is an info box.")],-1),yt=t("div",{class:"tip custom-block"},[t("p",{class:"custom-block-title"},"TIP"),t("p",null,"This is a tip.")],-1),vt=t("div",{class:"warning custom-block"},[t("p",{class:"custom-block-title"},"WARNING"),t("p",null,"This is a warning.")],-1),St=t("div",{class:"danger custom-block"},[t("p",{class:"custom-block-title"},"DANGER"),t("p",null,"This is a dangerous warning.")],-1),xt=t("div",{class:"not-prose copy cp flex cursor-pointer mb-3",onclick:"copy(this)"},[t("div",{class:"flex-grow bg-gray-700"},[t("div",{class:"pl-4 py-1 pb-1.5 align-middle text-lg text-white"},[t("p",null,"Copy Me!")])]),t("div",{class:"flex"},[t("div",{class:"bg-sky-500 text-white p-1.5 pb-0"},[t("svg",{class:"copied w-6 h-6",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M5 13l4 4L19 7"})]),t("svg",{class:"nocopy w-6 h-6",title:"copy",fill:"none",stroke:"white",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"1",d:"M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"})])])])],-1),Ct=t("p",null,[s("See Razor Press’s "),t("a",{href:"https://razor-press.web-templates.io/containers"},"Markdown Containers docs"),s(" for the complete list of available containers and examples on how to implement your own "),t("a",{href:"https://razor-press.web-templates.io/containers#implementing-block-containers"},"Custom Markdown containers"),s(".")],-1),zt=t("h3",null,"Support for Includes",-1),Mt=t("p",null,[s("Markdown fragments can be added to "),t("code",null,"_pages/_include"),s(" - a special folder rendered with "),t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Includes.cshtml"},"Pages/Includes.cshtml"),s(" using an "),t("a",{href:"https://github.com/NetCoreTemplates/razor-ssg/blob/main/MyApp/Pages/Shared/_LayoutEmpty.cshtml"},"Empty Layout"),s(" which can be included in other Markdown and Razor Pages or fetched on demand with Ajax.")],-1),jt=t("p",null,[s("Markdown Fragments can be then included inside other markdown documents with the "),t("code",null,"::include"),s(" inline container, e.g:")],-1),Bt=t("div",{class:"pre"},[t("p",null,"::include vue/formatters.md::")],-1),Rt=t("p",null,[s("Where it will be replaced with the HTML rendered markdown contents of fragments maintained in "),t("code",null,"_pages/_include"),s(".")],-1),Pt=t("h3",null,"Include Markdown in Razor Pages",-1),Tt=t("p",null,[s("Markdown Fragments can also be included in Razor Pages using the custom "),t("code",null,"MarkdownTagHelper.cs"),s(),t("code",null,""),s(" tag:")],-1),Gt=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),s("markdown")]),s(),t("span",{class:"token attr-name"},"include"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),s("vue/formatters.md"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),s(` +`)])],-1),Nt=t("h3",null,"Inline Markdown in Razor Pages",-1),At=t("p",null,"Alternatively markdown can be rendered inline with:",-1),Ft=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),s("markdown")]),t("span",{class:"token punctuation"},">")]),s(` ## Using Formatters Your App and custom templates can also utilize @servicestack/vue's [built-in formatting functions](href="/vue/use-formatters). `),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),s(` -`)])],-1),Jt=t("h3",null,"Light and Dark Mode Query Params",-1),qt=t("p",null,[s("You can link to Dark and Light modes of your Razor SSG website with the "),t("code",null,"?light"),s(" and "),t("code",null,"?dark"),s(" query string params:")],-1),Et=t("ul",null,[t("li",null,[t("a",{href:"https://razor-ssg.web-templates.io/?dark"},"https://razor-ssg.web-templates.io/?dark")]),t("li",null,[t("a",{href:"https://razor-ssg.web-templates.io/?light"},"https://razor-ssg.web-templates.io/?light")])],-1),Ht=t("h3",null,"Blog Post Authors threads.net and Mastodon links",-1),Dt=t("p",null,[s("The social links for Blog Post Authors can now include "),t("a",{href:"https://threads.net"},"threads.net"),s(" and "),t("a",{href:"https://mastodon.social"},"mastodon.social"),s(" links, e.g:")],-1),Ut=t("pre",{class:"language-json"},[t("code",{class:"language-json"},[t("span",{class:"token punctuation"},"{"),s(` +`)])],-1),Lt=t("h3",null,"Light and Dark Mode Query Params",-1),$t=t("p",null,[s("You can link to Dark and Light modes of your Razor SSG website with the "),t("code",null,"?light"),s(" and "),t("code",null,"?dark"),s(" query string params:")],-1),Vt=t("ul",null,[t("li",null,[t("a",{href:"https://razor-ssg.web-templates.io/?dark"},"https://razor-ssg.web-templates.io/?dark")]),t("li",null,[t("a",{href:"https://razor-ssg.web-templates.io/?light"},"https://razor-ssg.web-templates.io/?light")])],-1),It=t("h3",null,"Blog Post Authors threads.net and Mastodon links",-1),Wt=t("p",null,[s("The social links for Blog Post Authors can now include "),t("a",{href:"https://threads.net"},"threads.net"),s(" and "),t("a",{href:"https://mastodon.social"},"mastodon.social"),s(" links, e.g:")],-1),Et=t("pre",{class:"language-json"},[t("code",{class:"language-json"},[t("span",{class:"token punctuation"},"{"),s(` `),t("span",{class:"token property"},'"AppConfig"'),t("span",{class:"token operator"},":"),s(),t("span",{class:"token punctuation"},"{"),s(` `),t("span",{class:"token property"},'"BlogImageUrl"'),t("span",{class:"token operator"},":"),s(),t("span",{class:"token string"},'"https://servicestack.net/img/logo.png"'),t("span",{class:"token punctuation"},","),s(` `),t("span",{class:"token property"},'"Authors"'),t("span",{class:"token operator"},":"),s(),t("span",{class:"token punctuation"},"["),s(` @@ -83,4 +83,4 @@ Your App and custom templates can also utilize @servicestack/vue's `),t("span",{class:"token punctuation"},"]"),s(` `),t("span",{class:"token punctuation"},"}"),s(` `),t("span",{class:"token punctuation"},"}"),s(` -`)])],-1),Qt=t("h2",null,"Feature Requests Welcome",-1),Zt=t("p",null,[s("Most of Razor SSG’s features are currently being driven by requirements from the new "),t("a",{href:"https://razor-ssg.web-templates.io/#showcase"},"Websites built with Razor SSG"),s(" 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:")],-1),Ot=t("div",{class:"text-indigo-600 text-3xl text-center"},[t("p",null,[t("a",{href:"https://servicestack.net/ideas"},"https://servicestack.net/ideas")])],-1),Yt=t("h3",null,"SSG or Dynamic Features",-1),Kt=t("p",null,[s("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 "),t("a",{href:"https://servicestack.net/creatorkit/"},"CreatorKit"),s(" - 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.")],-1),ns="New Blogging features in Razor SSG",es="Explore the new Blogging Features in Razor SSG",as=["razor","markdown","blog","dev"],os="https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000",rs="Lucy Bates",ls=[{property:"og:title",content:"New Blogging features in Razor SSG"},{name:"twitter:title",content:"New Blogging features in Razor SSG"},{property:"og:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}],cs={__name:"2023-08-23_razor-ssg-new-blog-features",setup(o,{expose:e}){const r={title:"New Blogging features in Razor SSG",summary:"Explore the new Blogging Features in Razor SSG",tags:["razor","markdown","blog","dev"],image:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000",author:"Lucy Bates",meta:[{property:"og:title",content:"New Blogging features in Razor SSG"},{name:"twitter:title",content:"New Blogging features in Razor SSG"},{property:"og:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]};return e({frontmatter:r}),C({title:"New Blogging features in Razor SSG",meta:[{property:"og:title",content:"New Blogging features in Razor SSG"},{name:"twitter:title",content:"New Blogging features in Razor SSG"},{property:"og:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]}),(l,g)=>{const w=b,_=f;return n(),h(_,{frontmatter:r},{default:z(()=>[t("div",D,[U,Q,Z,O,Y,K,X,tt,st,nt,et,at,ot,rt,lt,ct,t("div",it,[i(w,{template:"razor-ssg"})]),pt,ut,i(H,{files:{wwwroot:{posts:{_:[".mjs",".css"]}}}},null,8,["files"]),dt,ht,gt,kt,mt,wt,_t,ft,bt,i(M(B),{data:{labels:["10,000 Rows","100,000 Rows"],datasets:[{label:"SQLite Memory",backgroundColor:"rgba(201, 203, 207, 0.2)",borderColor:"rgb(201, 203, 207)",borderWidth:1,data:[17.066,166.747]},{label:"SQLite Disk",backgroundColor:"rgba(255, 99, 132, 0.2)",borderColor:"rgb(255, 99, 132)",borderWidth:1,data:[20.224,199.697]},{label:"PostgreSQL",backgroundColor:"rgba(153, 102, 255, 0.2)",borderColor:"rgb(153, 102, 255)",borderWidth:1,data:[14.389,115.645]},{label:"MySQL",backgroundColor:"rgba(54, 162, 235, 0.2)",borderColor:"rgb(54, 162, 235)",borderWidth:1,data:[64.389,310.966]},{label:"MySqlConnector",backgroundColor:"rgba(255, 159, 64, 0.2)",borderColor:"rgb(255, 159, 64)",borderWidth:1,data:[64.427,308.574]},{label:"SQL Server",backgroundColor:"rgba(255, 99, 132, 0.2)",borderColor:"rgb(255, 99, 132)",borderWidth:1,data:[89.821,835.181]}]}},null,8,["data"]),yt,vt,St,xt,Ct,zt,Mt,jt,Bt,Rt,Pt,Tt,Gt,Nt,At,Ft,Lt,$t,Vt,It,Wt,Jt,qt,Et,Ht,Dt,Ut,Qt,Zt,Ot,Yt,Kt])]),_:1})}}};export{rs as author,cs as default,os as image,ls as meta,es as summary,as as tags,ns as title}; +`)])],-1),Ht=t("h2",null,"Feature Requests Welcome",-1),Jt=t("p",null,[s("Most of Razor SSG’s features are currently being driven by requirements from the new "),t("a",{href:"https://razor-ssg.web-templates.io/#showcase"},"Websites built with Razor SSG"),s(" 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:")],-1),qt=t("div",{class:"text-indigo-600 text-3xl text-center"},[t("p",null,[t("a",{href:"https://servicestack.net/ideas"},"https://servicestack.net/ideas")])],-1),Dt=t("h3",null,"SSG or Dynamic Features",-1),Ut=t("p",null,[s("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 "),t("a",{href:"https://servicestack.net/creatorkit/"},"CreatorKit"),s(" - 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.")],-1),Kt="New Blogging features in Razor SSG",Xt="Explore the new Blogging Features in Razor SSG",ts=["razor","markdown","blog","dev"],ss="https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000",ns="Lucy Bates",es=[{property:"og:title",content:"New Blogging features in Razor SSG"},{name:"twitter:title",content:"New Blogging features in Razor SSG"},{property:"og:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}],as={__name:"2023-08-23_razor-ssg-new-blog-features",setup(d,{expose:a}){const r={title:"New Blogging features in Razor SSG",summary:"Explore the new Blogging Features in Razor SSG",tags:["razor","markdown","blog","dev"],image:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000",author:"Lucy Bates",meta:[{property:"og:title",content:"New Blogging features in Razor SSG"},{name:"twitter:title",content:"New Blogging features in Razor SSG"},{property:"og:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]};return a({frontmatter:r}),y({title:"New Blogging features in Razor SSG",meta:[{property:"og:title",content:"New Blogging features in Razor SSG"},{name:"twitter:title",content:"New Blogging features in Razor SSG"},{property:"og:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1486312338219-ce68d2c6f44d?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]}),(o,h)=>{const m=f,w=_;return n(),u(w,{frontmatter:r},{default:v(()=>[t("div",W,[E,H,J,q,D,U,Q,Z,O,Y,K,X,tt,st,nt,et,t("div",at,[c(m,{template:"razor-ssg"})]),ot,rt,c(I,{files:{wwwroot:{posts:{_:[".mjs",".css"]}}}},null,8,["files"]),lt,ct,it,pt,ut,dt,ht,gt,kt,c(S(x),{data:{labels:["10,000 Rows","100,000 Rows"],datasets:[{label:"SQLite Memory",backgroundColor:"rgba(201, 203, 207, 0.2)",borderColor:"rgb(201, 203, 207)",borderWidth:1,data:[17.066,166.747]},{label:"SQLite Disk",backgroundColor:"rgba(255, 99, 132, 0.2)",borderColor:"rgb(255, 99, 132)",borderWidth:1,data:[20.224,199.697]},{label:"PostgreSQL",backgroundColor:"rgba(153, 102, 255, 0.2)",borderColor:"rgb(153, 102, 255)",borderWidth:1,data:[14.389,115.645]},{label:"MySQL",backgroundColor:"rgba(54, 162, 235, 0.2)",borderColor:"rgb(54, 162, 235)",borderWidth:1,data:[64.389,310.966]},{label:"MySqlConnector",backgroundColor:"rgba(255, 159, 64, 0.2)",borderColor:"rgb(255, 159, 64)",borderWidth:1,data:[64.427,308.574]},{label:"SQL Server",backgroundColor:"rgba(255, 99, 132, 0.2)",borderColor:"rgb(255, 99, 132)",borderWidth:1,data:[89.821,835.181]}]}},null,8,["data"]),mt,wt,_t,ft,bt,yt,vt,St,xt,Ct,zt,Mt,jt,Bt,Rt,Pt,Tt,Gt,Nt,At,Ft,Lt,$t,Vt,It,Wt,Et,Ht,Jt,qt,Dt,Ut])]),_:1})}}};export{ns as author,as as default,ss as image,es as meta,Xt as summary,ts as tags,Kt as title}; diff --git a/assets/2023-11-20_net8-blazor-template-DZPACl7V.js b/assets/2023-11-20_net8-blazor-template-CCLPpx-4.js similarity index 97% rename from assets/2023-11-20_net8-blazor-template-DZPACl7V.js rename to assets/2023-11-20_net8-blazor-template-CCLPpx-4.js index ccece10..9f3e22c 100644 --- a/assets/2023-11-20_net8-blazor-template-DZPACl7V.js +++ b/assets/2023-11-20_net8-blazor-template-CCLPpx-4.js @@ -1 +1 @@ -import{_ as p}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{d,o as n,h as s,u as h,g as u,r as m,w as f,a as t,b as i,e}from"./index-BXwhoUEp.js";import{_ as w,I as g}from"./Templates.vue_vue_type_script_setup_true_lang-BcwdGCEk.js";const y=d({__name:"BlazorTemplate",setup(r){return(o,a)=>(n(),s(w,{templates:[h(g).blazor]},null,8,["templates"]))}}),b={class:"markdown-body"},v=t("p",null,[e("With the release of "),t("strong",null,".NET 8"),e(", we’re happy to announce ServiceStack’s new "),t("a",{href:"https://blazor.web-templates.io/"},"Blazor"),e(" Tailwind project template that takes advantage of .NET 8 Blazor’s new features that redefines modern Web Development in C#.")],-1),_=t("p",null,[e("In this video overview we’ll explore how the template, adopts Blazor’s familiar "),t("strong",null,"ASP.NET Core Identity"),e(" for its authentication, utilizes the modern "),t("a",{href:"https://tailwindcss.com"},"Tailwind CSS"),e(" framework for beautiful responsive design and adopts .NET’s best-practice "),t("a",{href:"https://learn.microsoft.com/en-us/dotnet/core/docker/publish-as-container"},"Docker Containerization"),e(" support for its built-in "),t("a",{href:"https://blazor.web-templates.io/deploy"},"GitHub Action Deployments"),e(" which enables a simple ready-made CI solution for deployment to any Linux Host via SSH and Docker compose.")],-1),T=t("p",null,[e("We’ll also discuss the project’s structure, usage of "),t("strong",null,"ASP.NET Core Identity"),e(" for Authorization and utilizing "),t("strong",null,"ServiceStack Blazor Components"),e(" for data handling, all integrated to maximize developer efficiency in building Web Applications.")],-1),x=t("div",{class:"not-prose mt-16 flex flex-col items-center"},[t("div",{class:"flex"},[t("svg",{class:"w-28 h-28 text-purple-500",xmlns:"http://www.w3.org/2000/svg",width:"24",height:"24",viewBox:"0 0 24 24"},[t("path",{fill:"currentColor",d:"M23.834 8.101a13.912 13.912 0 0 1-13.643 11.72a10.105 10.105 0 0 1-1.994-.12a6.111 6.111 0 0 1-5.082-5.761a5.934 5.934 0 0 1 11.867-.084c.025.983-.401 1.846-1.277 1.871c-.936 0-1.374-.668-1.374-1.567v-2.5a1.531 1.531 0 0 0-1.52-1.533H8.715a3.648 3.648 0 1 0 2.695 6.08l.073-.11l.074.121a2.58 2.58 0 0 0 2.2 1.048a2.909 2.909 0 0 0 2.695-3.04a7.912 7.912 0 0 0-.217-1.933a7.404 7.404 0 0 0-14.64 1.603a7.497 7.497 0 0 0 7.308 7.405s.549.05 1.167.035a15.803 15.803 0 0 0 8.475-2.528c.036-.025.072.025.048.061a12.44 12.44 0 0 1-9.69 3.963a8.744 8.744 0 0 1-8.9-8.972a9.049 9.049 0 0 1 3.635-7.247a8.863 8.863 0 0 1 5.229-1.726h2.813a7.915 7.915 0 0 0 5.839-2.578a.11.11 0 0 1 .059-.034a.112.112 0 0 1 .12.053a.113.113 0 0 1 .015.067a7.934 7.934 0 0 1-1.227 3.549a.107.107 0 0 0-.014.06a.11.11 0 0 0 .073.095a.109.109 0 0 0 .062.004a8.505 8.505 0 0 0 5.913-4.876a.155.155 0 0 1 .055-.053a.15.15 0 0 1 .147 0a.153.153 0 0 1 .054.053A10.779 10.779 0 0 1 23.834 8.1zM8.895 11.628a2.188 2.188 0 1 0 2.188 2.188v-2.042a.158.158 0 0 0-.15-.15Z"})])])],-1),z={class:"not-prose mt-4 px-4 sm:px-6"},k=t("div",{class:"text-center"},[t("h3",{id:"blazor-template",class:"text-4xl sm:text-5xl md:text-6xl tracking-tight font-extrabold text-gray-900"}," Blazor Tailwind Template ")],-1),S={class:"py-8 max-w-7xl mx-auto px-4 sm:px-6"},A={class:"not-prose relative bg-white dark:bg-black py-4"},B=t("div",{class:"mx-auto max-w-md px-4 text-center sm:max-w-3xl sm:px-6 lg:max-w-7xl lg:px-8"},[t("p",{class:"mt-2 text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-50 sm:text-4xl"},"Create a new Blazor Tailwind App"),t("p",{class:"mx-auto mt-5 max-w-prose text-xl text-gray-500"}," Create a new Blazor Tailwind project with your preferred project name: ")],-1),I=t("h2",null,"ASP.NET Core Identity Integration",-1),N=t("p",null,"In terms of security, the template integrates ASP.NET Core Identity, offering a structured approach to authentication, including support for email confirmation, two-factor authentication and GDPR compliance features.",-1),C=t("p",null,[e("Integrating ASP.NET Core Identity doesn’t complicate securing ServiceStack services which can still be secured with "),t("a",{href:"https://docs.servicestack.net/auth/authentication-and-authorization#declarative-validation-attributes"},"Declarative Validation Attributes"),e(" for role-based access control, e.g. using "),t("code",null,'[ValidateHasRole("Employee")]'),e(" directly on Request DTOs, bringing any existing knowledge and experience with ServiceStack Authentication to securing UIs and APIs with Identity Auth.")],-1),E=t("p",null,[e("The template includes a pre-baked solution for sending general and Identity Auth Emails with your configured SMTP Server in managed background workers with "),t("a",{href:"https://docs.servicestack.net/background-mq"},"Background MQ"),e(" and an enhanced version of the default ASP.NET Core Blazor Identity UI, with all pages upgraded to use a beautiful Tailwind CSS theme as well as integration with "),t("a",{href:"https://davidshimjs.github.io/qrcodejs/"},"qrcode.js"),e(" for providing a visual QR Code barcode which mobile phone users can easily scan to setup 2FA Authentication:")],-1),D=t("p",null,[t("img",{src:"https://servicestack.net/img/posts/net8-best-blazor/blazor-identityauth-qrcode.png",alt:""})],-1),U=t("h2",null,"Responsive and Interactive UIs with Tailwind CSS",-1),P=t("p",null,[e("With responsive UI out-of-the-box, thanks to Tailwind CSS, Developers can style their Blazor Apps with the modern popular utility-first CSS framework for creating beautiful, maintainable "),t("strong",null,"Responsive UIs"),e(" with "),t("strong",null,"DarkMode"),e(" support")],-1),j=t("p",null,"It also takes full advantage of Blazor’s static rendering for its Website layout for optimal performance and SEO, so only Pages and Components that require interactivity need to opt-in for Blazor Server Interactive rendering modes.",-1),R=t("h2",null,"ServiceStack.Blazor Tailwind Components",-1),H=t("p",null,[e("The "),t("a",{href:"https://blazor-gallery.jamstacks.net"},"ServiceStack.Blazor Components"),e(", updated for .NET 8 enables you to rapidly develop beautiful Blazor Apps integrated with Rich high-productivity UI Tailwind Components like "),t("a",{href:"https://blazor-gallery.servicestack.net/gallery/autoquerygrid"},"AutoQueryGrid"),e(" and "),t("a",{href:"https://blazor-gallery.servicestack.net/gallery/autoform"},"AutoForms"),e(" which interface with "),t("a",{href:"https://docs.servicestack.net/autoquery/"},"AutoQuery services"),e(" to provide a full CRUD data management UI with minimal effort.")],-1),L=t("h2",null,"Enhanced Containerization",-1),M=t("p",null,[e(".NET 8 simplifies Docker integration. Using "),t("code",null,"dotnet publish"),e(", developers can now automate the creation of Docker images that adhere to best security practices, such as running as a non-root user in containers that can utilize the built-in GitHub Actions to effortlessly deploy their containerized Blazor Apps with Docker and GitHub Registry via SSH to any Linux Server.")],-1),q=t("h2",null,"Other Template Features",-1),O=t("p",null,"Other features that enhances the default ASP.NET Blazor App templates with several modern, high-productivity features, include:",-1),Q=t("ul",null,[t("li",null,[t("a",{href:"https://learn.microsoft.com/ef/"},"Entity Framework"),e(" & "),t("a",{href:"https://docs.servicestack.net/ormlite/"},"OrmLite"),e(" - Choose the best ORM for each App feature, with a unified solution that sees "),t("a",{href:"https://docs.servicestack.net/ormlite/db-migrations"},"OrmLite’s DB Migrations"),e(" run both EF and OrmLite migrations, inc. Seed Data with a single command at Development or Deployment")]),t("li",null,[t("a",{href:"https://www.sqlite.org"},"SQLite Database"),e(" - Set up as the default database, it allows developers to start immediately without configuring a separate database server")]),t("li",null,[t("a",{href:"https://docs.servicestack.net/autoquery/"},"AutoQuery"),e(" - Rapidly developing data-driven APIs, UIs and CRUD Apps")]),t("li",null,[t("a",{href:"https://www.youtube.com/watch?v=tt0ytzVVjEY"},"Auto Admin Pages"),e(" - Quickly develop your back-office CRUD Admin UIs to manage your App’s Database tables at "),t("a",{href:"https://blazor.web-templates.io/admin"},"/admin")]),t("li",null,[t("a",{href:"https://docs.servicestack.net/razor-press/syntax"},"Markdown"),e(" - Maintain SEO-friendly documentation and content-rich pages like this one with just Markdown, beautifully styled with "),t("a",{href:"https://tailwindcss.com/docs/typography-plugin"},"@tailwindcss/typography")]),t("li",null,[t("a",{href:"https://servicestack.net/auto-ui"},"Built-in UIs"),e(" - Use ServiceStack’s Auto UIs to "),t("a",{href:"https://docs.servicestack.net/api-explorer"},"Explore your APIs"),e(" at "),t("a",{href:"https://blazor.web-templates.io/ui/"},"/ui"),e(" or Query your "),t("a",{href:"https://docs.servicestack.net/admin-ui-database"},"App’s Database Tables"),e(" at "),t("a",{href:"https://blazor.web-templates.io/admin-ui/database"},"/admin-ui/database")]),t("li",null,[t("a",{href:"https://youtu.be/66DgLHExC9E"},"Universal API Components"),e(" - Effortlessly create reusable, maximally performant universal Blazor API Components that works in Blazor Server and Web Assembly Interactivity modes")]),t("li",null,[t("a",{href:"https://docs.servicestack.net/physical-project-structure"},"Organized Project Structure"),e(" - Divided into AppHost, Service Interface, Service Model, and Tests projects to promote separation of concerns and maintainability.")])],-1),Z="New .NET 8 Blazor Tailwind Template",J="The new Blazor for .NET 8 template streamlines web UI development in C#.",K=["dotnet","blazor","templates"],X="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000",tt="Lucy Bates",et=[{property:"og:title",content:"New .NET 8 Blazor Tailwind Template"},{name:"twitter:title",content:"New .NET 8 Blazor Tailwind Template"},{property:"og:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}],at={__name:"2023-11-20_net8-blazor-template",setup(r,{expose:o}){const a={title:"New .NET 8 Blazor Tailwind Template",summary:"The new Blazor for .NET 8 template streamlines web UI development in C#.",tags:["dotnet","blazor","templates"],image:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000",author:"Lucy Bates",meta:[{property:"og:title",content:"New .NET 8 Blazor Tailwind Template"},{name:"twitter:title",content:"New .NET 8 Blazor Tailwind Template"},{property:"og:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]};return o({frontmatter:a}),u({title:"New .NET 8 Blazor Tailwind Template",meta:[{property:"og:title",content:"New .NET 8 Blazor Tailwind Template"},{name:"twitter:title",content:"New .NET 8 Blazor Tailwind Template"},{property:"og:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]}),(W,F)=>{const l=m("LiteYouTube"),c=p;return n(),s(c,{frontmatter:a},{default:f(()=>[t("div",b,[v,_,T,x,t("div",z,[k,t("div",S,[i(l,{id:"hqyozHSL0Nk",title:".NET 8 Blazor Tailwind Template"})])]),t("div",A,[B,i(y)]),I,N,C,E,D,U,P,j,R,H,L,M,q,O,Q])]),_:1})}}};export{tt as author,at as default,X as image,et as meta,J as summary,K as tags,Z as title}; +import{_ as p}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{d,o as n,h as s,u as h,i as u,r as m,w as f,a as t,b as i,e}from"./index-DWUkzBt0.js";import{_ as w,I as g}from"./Templates.vue_vue_type_script_setup_true_lang-BhUzPs-y.js";const y=d({__name:"BlazorTemplate",setup(r){return(o,a)=>(n(),s(w,{templates:[h(g).blazor]},null,8,["templates"]))}}),b={class:"markdown-body"},v=t("p",null,[e("With the release of "),t("strong",null,".NET 8"),e(", we’re happy to announce ServiceStack’s new "),t("a",{href:"https://blazor.web-templates.io/"},"Blazor"),e(" Tailwind project template that takes advantage of .NET 8 Blazor’s new features that redefines modern Web Development in C#.")],-1),_=t("p",null,[e("In this video overview we’ll explore how the template, adopts Blazor’s familiar "),t("strong",null,"ASP.NET Core Identity"),e(" for its authentication, utilizes the modern "),t("a",{href:"https://tailwindcss.com"},"Tailwind CSS"),e(" framework for beautiful responsive design and adopts .NET’s best-practice "),t("a",{href:"https://learn.microsoft.com/en-us/dotnet/core/docker/publish-as-container"},"Docker Containerization"),e(" support for its built-in "),t("a",{href:"https://blazor.web-templates.io/deploy"},"GitHub Action Deployments"),e(" which enables a simple ready-made CI solution for deployment to any Linux Host via SSH and Docker compose.")],-1),T=t("p",null,[e("We’ll also discuss the project’s structure, usage of "),t("strong",null,"ASP.NET Core Identity"),e(" for Authorization and utilizing "),t("strong",null,"ServiceStack Blazor Components"),e(" for data handling, all integrated to maximize developer efficiency in building Web Applications.")],-1),x=t("div",{class:"not-prose mt-16 flex flex-col items-center"},[t("div",{class:"flex"},[t("svg",{class:"w-28 h-28 text-purple-500",xmlns:"http://www.w3.org/2000/svg",width:"24",height:"24",viewBox:"0 0 24 24"},[t("path",{fill:"currentColor",d:"M23.834 8.101a13.912 13.912 0 0 1-13.643 11.72a10.105 10.105 0 0 1-1.994-.12a6.111 6.111 0 0 1-5.082-5.761a5.934 5.934 0 0 1 11.867-.084c.025.983-.401 1.846-1.277 1.871c-.936 0-1.374-.668-1.374-1.567v-2.5a1.531 1.531 0 0 0-1.52-1.533H8.715a3.648 3.648 0 1 0 2.695 6.08l.073-.11l.074.121a2.58 2.58 0 0 0 2.2 1.048a2.909 2.909 0 0 0 2.695-3.04a7.912 7.912 0 0 0-.217-1.933a7.404 7.404 0 0 0-14.64 1.603a7.497 7.497 0 0 0 7.308 7.405s.549.05 1.167.035a15.803 15.803 0 0 0 8.475-2.528c.036-.025.072.025.048.061a12.44 12.44 0 0 1-9.69 3.963a8.744 8.744 0 0 1-8.9-8.972a9.049 9.049 0 0 1 3.635-7.247a8.863 8.863 0 0 1 5.229-1.726h2.813a7.915 7.915 0 0 0 5.839-2.578a.11.11 0 0 1 .059-.034a.112.112 0 0 1 .12.053a.113.113 0 0 1 .015.067a7.934 7.934 0 0 1-1.227 3.549a.107.107 0 0 0-.014.06a.11.11 0 0 0 .073.095a.109.109 0 0 0 .062.004a8.505 8.505 0 0 0 5.913-4.876a.155.155 0 0 1 .055-.053a.15.15 0 0 1 .147 0a.153.153 0 0 1 .054.053A10.779 10.779 0 0 1 23.834 8.1zM8.895 11.628a2.188 2.188 0 1 0 2.188 2.188v-2.042a.158.158 0 0 0-.15-.15Z"})])])],-1),z={class:"not-prose mt-4 px-4 sm:px-6"},k=t("div",{class:"text-center"},[t("h3",{id:"blazor-template",class:"text-4xl sm:text-5xl md:text-6xl tracking-tight font-extrabold text-gray-900"}," Blazor Tailwind Template ")],-1),S={class:"py-8 max-w-7xl mx-auto px-4 sm:px-6"},A={class:"not-prose relative bg-white dark:bg-black py-4"},B=t("div",{class:"mx-auto max-w-md px-4 text-center sm:max-w-3xl sm:px-6 lg:max-w-7xl lg:px-8"},[t("p",{class:"mt-2 text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-50 sm:text-4xl"},"Create a new Blazor Tailwind App"),t("p",{class:"mx-auto mt-5 max-w-prose text-xl text-gray-500"}," Create a new Blazor Tailwind project with your preferred project name: ")],-1),I=t("h2",null,"ASP.NET Core Identity Integration",-1),N=t("p",null,"In terms of security, the template integrates ASP.NET Core Identity, offering a structured approach to authentication, including support for email confirmation, two-factor authentication and GDPR compliance features.",-1),C=t("p",null,[e("Integrating ASP.NET Core Identity doesn’t complicate securing ServiceStack services which can still be secured with "),t("a",{href:"https://docs.servicestack.net/auth/authentication-and-authorization#declarative-validation-attributes"},"Declarative Validation Attributes"),e(" for role-based access control, e.g. using "),t("code",null,'[ValidateHasRole("Employee")]'),e(" directly on Request DTOs, bringing any existing knowledge and experience with ServiceStack Authentication to securing UIs and APIs with Identity Auth.")],-1),E=t("p",null,[e("The template includes a pre-baked solution for sending general and Identity Auth Emails with your configured SMTP Server in managed background workers with "),t("a",{href:"https://docs.servicestack.net/background-mq"},"Background MQ"),e(" and an enhanced version of the default ASP.NET Core Blazor Identity UI, with all pages upgraded to use a beautiful Tailwind CSS theme as well as integration with "),t("a",{href:"https://davidshimjs.github.io/qrcodejs/"},"qrcode.js"),e(" for providing a visual QR Code barcode which mobile phone users can easily scan to setup 2FA Authentication:")],-1),D=t("p",null,[t("img",{src:"https://servicestack.net/img/posts/net8-best-blazor/blazor-identityauth-qrcode.png",alt:""})],-1),U=t("h2",null,"Responsive and Interactive UIs with Tailwind CSS",-1),P=t("p",null,[e("With responsive UI out-of-the-box, thanks to Tailwind CSS, Developers can style their Blazor Apps with the modern popular utility-first CSS framework for creating beautiful, maintainable "),t("strong",null,"Responsive UIs"),e(" with "),t("strong",null,"DarkMode"),e(" support")],-1),j=t("p",null,"It also takes full advantage of Blazor’s static rendering for its Website layout for optimal performance and SEO, so only Pages and Components that require interactivity need to opt-in for Blazor Server Interactive rendering modes.",-1),R=t("h2",null,"ServiceStack.Blazor Tailwind Components",-1),H=t("p",null,[e("The "),t("a",{href:"https://blazor-gallery.jamstacks.net"},"ServiceStack.Blazor Components"),e(", updated for .NET 8 enables you to rapidly develop beautiful Blazor Apps integrated with Rich high-productivity UI Tailwind Components like "),t("a",{href:"https://blazor-gallery.servicestack.net/gallery/autoquerygrid"},"AutoQueryGrid"),e(" and "),t("a",{href:"https://blazor-gallery.servicestack.net/gallery/autoform"},"AutoForms"),e(" which interface with "),t("a",{href:"https://docs.servicestack.net/autoquery/"},"AutoQuery services"),e(" to provide a full CRUD data management UI with minimal effort.")],-1),L=t("h2",null,"Enhanced Containerization",-1),M=t("p",null,[e(".NET 8 simplifies Docker integration. Using "),t("code",null,"dotnet publish"),e(", developers can now automate the creation of Docker images that adhere to best security practices, such as running as a non-root user in containers that can utilize the built-in GitHub Actions to effortlessly deploy their containerized Blazor Apps with Docker and GitHub Registry via SSH to any Linux Server.")],-1),q=t("h2",null,"Other Template Features",-1),O=t("p",null,"Other features that enhances the default ASP.NET Blazor App templates with several modern, high-productivity features, include:",-1),Q=t("ul",null,[t("li",null,[t("a",{href:"https://learn.microsoft.com/ef/"},"Entity Framework"),e(" & "),t("a",{href:"https://docs.servicestack.net/ormlite/"},"OrmLite"),e(" - Choose the best ORM for each App feature, with a unified solution that sees "),t("a",{href:"https://docs.servicestack.net/ormlite/db-migrations"},"OrmLite’s DB Migrations"),e(" run both EF and OrmLite migrations, inc. Seed Data with a single command at Development or Deployment")]),t("li",null,[t("a",{href:"https://www.sqlite.org"},"SQLite Database"),e(" - Set up as the default database, it allows developers to start immediately without configuring a separate database server")]),t("li",null,[t("a",{href:"https://docs.servicestack.net/autoquery/"},"AutoQuery"),e(" - Rapidly developing data-driven APIs, UIs and CRUD Apps")]),t("li",null,[t("a",{href:"https://www.youtube.com/watch?v=tt0ytzVVjEY"},"Auto Admin Pages"),e(" - Quickly develop your back-office CRUD Admin UIs to manage your App’s Database tables at "),t("a",{href:"https://blazor.web-templates.io/admin"},"/admin")]),t("li",null,[t("a",{href:"https://docs.servicestack.net/razor-press/syntax"},"Markdown"),e(" - Maintain SEO-friendly documentation and content-rich pages like this one with just Markdown, beautifully styled with "),t("a",{href:"https://tailwindcss.com/docs/typography-plugin"},"@tailwindcss/typography")]),t("li",null,[t("a",{href:"https://servicestack.net/auto-ui"},"Built-in UIs"),e(" - Use ServiceStack’s Auto UIs to "),t("a",{href:"https://docs.servicestack.net/api-explorer"},"Explore your APIs"),e(" at "),t("a",{href:"https://blazor.web-templates.io/ui/"},"/ui"),e(" or Query your "),t("a",{href:"https://docs.servicestack.net/admin-ui-database"},"App’s Database Tables"),e(" at "),t("a",{href:"https://blazor.web-templates.io/admin-ui/database"},"/admin-ui/database")]),t("li",null,[t("a",{href:"https://youtu.be/66DgLHExC9E"},"Universal API Components"),e(" - Effortlessly create reusable, maximally performant universal Blazor API Components that works in Blazor Server and Web Assembly Interactivity modes")]),t("li",null,[t("a",{href:"https://docs.servicestack.net/physical-project-structure"},"Organized Project Structure"),e(" - Divided into AppHost, Service Interface, Service Model, and Tests projects to promote separation of concerns and maintainability.")])],-1),Z="New .NET 8 Blazor Tailwind Template",J="The new Blazor for .NET 8 template streamlines web UI development in C#.",K=["dotnet","blazor","templates"],X="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000",tt="Lucy Bates",et=[{property:"og:title",content:"New .NET 8 Blazor Tailwind Template"},{name:"twitter:title",content:"New .NET 8 Blazor Tailwind Template"},{property:"og:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}],at={__name:"2023-11-20_net8-blazor-template",setup(r,{expose:o}){const a={title:"New .NET 8 Blazor Tailwind Template",summary:"The new Blazor for .NET 8 template streamlines web UI development in C#.",tags:["dotnet","blazor","templates"],image:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000",author:"Lucy Bates",meta:[{property:"og:title",content:"New .NET 8 Blazor Tailwind Template"},{name:"twitter:title",content:"New .NET 8 Blazor Tailwind Template"},{property:"og:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]};return o({frontmatter:a}),u({title:"New .NET 8 Blazor Tailwind Template",meta:[{property:"og:title",content:"New .NET 8 Blazor Tailwind Template"},{name:"twitter:title",content:"New .NET 8 Blazor Tailwind Template"},{property:"og:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:image",content:"https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?crop=entropy&fit=crop&h=1000&w=2000"},{name:"twitter:card",content:"summary_large_image"}]}),(W,F)=>{const l=m("LiteYouTube"),c=p;return n(),s(c,{frontmatter:a},{default:f(()=>[t("div",b,[v,_,T,x,t("div",z,[k,t("div",S,[i(l,{id:"hqyozHSL0Nk",title:".NET 8 Blazor Tailwind Template"})])]),t("div",A,[B,i(y)]),I,N,C,E,D,U,P,j,R,H,L,M,q,O,Q])]),_:1})}}};export{tt as author,at as default,X as image,et as meta,J as summary,K as tags,Z as title}; diff --git a/assets/2023-11-22_net8-best-blazor-khEMeRjt.js b/assets/2023-11-22_net8-best-blazor-C_kPuv0r.js similarity index 99% rename from assets/2023-11-22_net8-best-blazor-khEMeRjt.js rename to assets/2023-11-22_net8-best-blazor-C_kPuv0r.js index 802e31c..4fc4af4 100644 --- a/assets/2023-11-22_net8-best-blazor-khEMeRjt.js +++ b/assets/2023-11-22_net8-best-blazor-C_kPuv0r.js @@ -1,4 +1,4 @@ -import{_ as u}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CYqJwKJW.js";import{d as r,o as c,h as l,u as k,g as d,r as h,w as m,a as t,b as a,e as n}from"./index-BXwhoUEp.js";import{_ as g}from"./Counter.vue_vue_type_script_setup_true_lang-BwW-seaC.js";import{_ as f,I as w}from"./Templates.vue_vue_type_script_setup_true_lang-BcwdGCEk.js";const b=r({__name:"BlazorVueTemplate",setup(p){return(e,s)=>(c(),l(f,{templates:[k(w)["blazor-vue"]]},null,8,["templates"]))}}),v={class:"markdown-body"},y=t("p",null,[n("The best way to find out what’s new in .NET 8 Blazor is to watch the excellent "),t("a",{href:"https://www.youtube.com/watch?v=QD2-DwuOfKM"},"Full stack web UI with Blazor in .NET 8"),n(" presentation by Daniel Roth and Steve Sanderson, which covers how Blazor has become a Full Stack UI Web Technology for developing any kind of .NET Web App.")],-1),_=t("h2",null,"Your first .NET 8 Blazor App",-1),z=t("p",null,"You don’t get to appreciate what this means until you create your first .NET 8 Blazor App where you’ll be pleasantly surprised that Blazor Apps render fast, clean HTML without needing to load large Web Assembly assets needed for Blazor WebAssembly Apps or starting a stateful Web Socket connection required for Blazor Server Interactive Apps.",-1),B=t("p",null,[n("This is because the "),t("strong",null,"default rendering mode"),n(" for Blazor uses neither of these technologies, instead it returns to traditional Web App development where Blazor Pages now return clean, glorious HTML - courtesy of Blazor’s "),t("a",{href:"https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes"},"Static render mode"),n(".")],-1),x=t("p",null,[t("a",{href:"https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes"},[t("img",{src:"https://servicestack.net/img/posts/net8-best-blazor/blazor-ssr.png",alt:""})])],-1),C=t("h2",null,"Choose your compromise",-1),A=t("p",null,"Previously we were forced to choose upfront whether we wanted to build a Blazor Web Assembly App or a Blazor Server App and the compromises that came with them, which for public Internet Web Apps wasn’t even a choice as Blazor Server Apps perform poorly over high latency Internet connections.",-1),S=t("p",null,[n("This meant choosing Blazor Web Assembly Apps which required downloading a large Web Assembly runtime with users experiencing a long delay before the App was functional. To minimize this impact our Blazor WebAssembly Tailwind template included "),t("a",{href:"https://blazor-tailwind.jamstacks.net/docs/prerender"},"built-in prerendering"),n(" where as part of deployment would generate "),t("strong",null,"static .html pages"),n(" that were deployed with the Blazor Web Assembly front-end UI that can be hosted on CDN edge networks to further improve load times.")],-1),T=t("p",null,"Whilst this meant the App’s UI would be rendered immediately, it still wouldn’t be functional until the Web Assembly runtime was downloaded and initialized, which would flicker as the static UI was replaced with Blazor’s WASM rendered UI, then later Authenticated users would experience further delay and UI jank whilst the App signs in the Authenticated User. Whilst prerendering is an improvement over Blazor WASM’s default blank loading screen, it’s still not ideal for public facing Web Apps.",-1),q=t("h2",null,".NET 8 Blazor is a Game Changer",-1),I=t("p",null,[n("The situation has greatly improved in .NET 8 where your entire App no longer needs to be bound to a single Interactivity mode. Even better, Blazor’s default "),t("strong",null,"static rendering"),n(" mode results in the best UX where the Website Layout and important landing pages can be rendered instantly.")],-1),E=t("h3",null,"Interactive only when you need it",-1),W=t("p",null,[n("Only pages that need Blazor’s interactivity features can opt-in to whichever Blazor interactive rendering mode makes the most sense, either on a page-by-page or component basis, or by choosing "),t("code",null,"RenderMode.InteractiveAuto"),n(" which uses "),t("strong",null,"InteractiveWebAssembly"),n(" if the WASM runtime is loaded or "),t("strong",null,"InteractiveServer"),n(" if it isn’t.")],-1),j=t("h3",null,"Enhanced Navigation FTW",-1),M=t("p",null,[n("Ultimately I expect Blazor’s new "),t("strong",null,"Enhanced Navigation"),n(" is likely the feature that will deliver the biggest UX improvement users will experience given it’s enabled by default and gives traditional statically rendered Web Apps instant SPA-like navigation responsiveness where new pages are swapped in without needing to perform expensive full page reloads.")],-1),N=t("p",null,"It’s beauty lies in being able to do this as a mostly transparent detail without the traditional SPA complexity of needing to manage complex state or client-side routing. It’s a smart implementation that’s able to perform fine-grained DOM updates to only parts of pages that have changed, providing the ultimate UX of preserving page state, like populated form fields and scroll position, to deliver a fast and responsive UX that previously wasn’t attainable from the simplicity of a Server Rendered App.",-1),D=t("p",null,"Its implementation does pose some challenges in implementing certain features, but we’ll cover some approaches below we’ve used to overcome them below.",-1),L=t("h3",null,"Full Stack Web UI",-1),V=t("p",null,"Blazor’s static rendering with enhanced navigation and its opt-in flexibility makes .NET 8 Blazor a game changer, expanding it from a very niche set of use-cases that weren’t too adversely affected by its Interactivity mode downsides, to becoming a viable solution for developing any kind of .NET Web App, especially as it can also be utilized within existing ASP.NET MVC and Razor Pages Apps.",-1),P=t("h3",null,"Benefits over MVC and Razor Pages",-1),U=t("p",null,[n("In addition, Blazor’s superior component model allows building better encapsulated, more reusable and easier-to-use UI components which has enabled Blazor’s rich 3rd Party library ecosystem to flourish, that we ourselves utilize to develop the high productivity Tailwind Components in the "),t("a",{href:"https://blazor-gallery.servicestack.net"},"ServiceStack.Blazor"),n(" component library.")],-1),J=t("p",null,"So far there’s only upsides for .NET Web App development, the compromises only kick in when you need Blazor’s interactivity features, luckily these can now be scoped to just the Pages and Components that need them. But how often do we need them?",-1),O=t("h3",null,"When do you need Blazor’s Interactivity features?",-1),R=t("p",null,"It ultimately depends on what App your building, but a lot of Websites can happily display dynamic content, navigate quickly with enhanced navigation, fill out and submit forms - all in Blazor’s default static rendering mode.",-1),F=t("p",null,[n("Not even advanced features like "),t("strong",null,"Streaming Rendering"),n(" used in Blazor Template’s "),t("a",{href:"https://github.com/dotnet/aspnetcore/blob/v8.0.0-rc.2.23480.2/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor"},"Weather.razor"),n(" page require interactivity, as its progressive rendered UI updates are achieved in a single request without interactivity.")],-1),H=t("p",null,[n("In fact the only time "),t("code",null,"@rendermode InteractiveServer"),n(" is needed in the default Blazor template is in the "),t("a",{href:"https://github.com/dotnet/aspnetcore/blob/v8.0.0-rc.2.23480.2/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Counter.razor#L3"},"Counter.razor"),n(" page whose C# Event Handling require it.")],-1),G=t("p",null,"Ultimately some form of Interactivity is going to be required in order to add behavior or client-side functionality that runs after pages have been rendered, but you still have some options left before being forced to opt into an Interactive Blazor solution.",-1),Y=t("h3",null,"Interactive Feature Options",-1),$=t("p",null,[n("We can see some of these options utilized in the Blazor Template "),t("a",{href:"https://github.com/dotnet/aspnetcore/blob/v8.0.0-rc.2.23480.2/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor"},"NavMenu.razor"),n(" component which uses JavaScript "),t("code",null,"onclick"),n(" event handlers to add client-side behavior to simulate mouse clicks to toggle UI elements:")],-1),Q=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),n("div")]),n(),t("span",{class:"token attr-name"},"class"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),n("nav-scrollable"),t("span",{class:"token punctuation"},'"')]),n(),t("span",{class:"token special-attr"},[t("span",{class:"token attr-name"},"onclick"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),t("span",{class:"token value javascript language-javascript"},[n("document"),t("span",{class:"token punctuation"},"."),t("span",{class:"token function"},"querySelector"),t("span",{class:"token punctuation"},"("),t("span",{class:"token string"},"'.navbar-toggler'"),t("span",{class:"token punctuation"},")"),t("span",{class:"token punctuation"},"."),t("span",{class:"token function"},"click"),t("span",{class:"token punctuation"},"("),t("span",{class:"token punctuation"},")")]),t("span",{class:"token punctuation"},'"')])]),t("span",{class:"token punctuation"},">")]),n(` +import{_ as u}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{d as r,o as c,h as l,u as k,i as d,r as h,w as m,a as t,b as a,e as n}from"./index-DWUkzBt0.js";import{_ as g}from"./Counter.vue_vue_type_script_setup_true_lang-DcP0JNgA.js";import{_ as f,I as w}from"./Templates.vue_vue_type_script_setup_true_lang-BhUzPs-y.js";const b=r({__name:"BlazorVueTemplate",setup(p){return(e,s)=>(c(),l(f,{templates:[k(w)["blazor-vue"]]},null,8,["templates"]))}}),v={class:"markdown-body"},y=t("p",null,[n("The best way to find out what’s new in .NET 8 Blazor is to watch the excellent "),t("a",{href:"https://www.youtube.com/watch?v=QD2-DwuOfKM"},"Full stack web UI with Blazor in .NET 8"),n(" presentation by Daniel Roth and Steve Sanderson, which covers how Blazor has become a Full Stack UI Web Technology for developing any kind of .NET Web App.")],-1),_=t("h2",null,"Your first .NET 8 Blazor App",-1),z=t("p",null,"You don’t get to appreciate what this means until you create your first .NET 8 Blazor App where you’ll be pleasantly surprised that Blazor Apps render fast, clean HTML without needing to load large Web Assembly assets needed for Blazor WebAssembly Apps or starting a stateful Web Socket connection required for Blazor Server Interactive Apps.",-1),B=t("p",null,[n("This is because the "),t("strong",null,"default rendering mode"),n(" for Blazor uses neither of these technologies, instead it returns to traditional Web App development where Blazor Pages now return clean, glorious HTML - courtesy of Blazor’s "),t("a",{href:"https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes"},"Static render mode"),n(".")],-1),x=t("p",null,[t("a",{href:"https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes"},[t("img",{src:"https://servicestack.net/img/posts/net8-best-blazor/blazor-ssr.png",alt:""})])],-1),C=t("h2",null,"Choose your compromise",-1),A=t("p",null,"Previously we were forced to choose upfront whether we wanted to build a Blazor Web Assembly App or a Blazor Server App and the compromises that came with them, which for public Internet Web Apps wasn’t even a choice as Blazor Server Apps perform poorly over high latency Internet connections.",-1),S=t("p",null,[n("This meant choosing Blazor Web Assembly Apps which required downloading a large Web Assembly runtime with users experiencing a long delay before the App was functional. To minimize this impact our Blazor WebAssembly Tailwind template included "),t("a",{href:"https://blazor-tailwind.jamstacks.net/docs/prerender"},"built-in prerendering"),n(" where as part of deployment would generate "),t("strong",null,"static .html pages"),n(" that were deployed with the Blazor Web Assembly front-end UI that can be hosted on CDN edge networks to further improve load times.")],-1),T=t("p",null,"Whilst this meant the App’s UI would be rendered immediately, it still wouldn’t be functional until the Web Assembly runtime was downloaded and initialized, which would flicker as the static UI was replaced with Blazor’s WASM rendered UI, then later Authenticated users would experience further delay and UI jank whilst the App signs in the Authenticated User. Whilst prerendering is an improvement over Blazor WASM’s default blank loading screen, it’s still not ideal for public facing Web Apps.",-1),q=t("h2",null,".NET 8 Blazor is a Game Changer",-1),I=t("p",null,[n("The situation has greatly improved in .NET 8 where your entire App no longer needs to be bound to a single Interactivity mode. Even better, Blazor’s default "),t("strong",null,"static rendering"),n(" mode results in the best UX where the Website Layout and important landing pages can be rendered instantly.")],-1),E=t("h3",null,"Interactive only when you need it",-1),W=t("p",null,[n("Only pages that need Blazor’s interactivity features can opt-in to whichever Blazor interactive rendering mode makes the most sense, either on a page-by-page or component basis, or by choosing "),t("code",null,"RenderMode.InteractiveAuto"),n(" which uses "),t("strong",null,"InteractiveWebAssembly"),n(" if the WASM runtime is loaded or "),t("strong",null,"InteractiveServer"),n(" if it isn’t.")],-1),j=t("h3",null,"Enhanced Navigation FTW",-1),M=t("p",null,[n("Ultimately I expect Blazor’s new "),t("strong",null,"Enhanced Navigation"),n(" is likely the feature that will deliver the biggest UX improvement users will experience given it’s enabled by default and gives traditional statically rendered Web Apps instant SPA-like navigation responsiveness where new pages are swapped in without needing to perform expensive full page reloads.")],-1),N=t("p",null,"It’s beauty lies in being able to do this as a mostly transparent detail without the traditional SPA complexity of needing to manage complex state or client-side routing. It’s a smart implementation that’s able to perform fine-grained DOM updates to only parts of pages that have changed, providing the ultimate UX of preserving page state, like populated form fields and scroll position, to deliver a fast and responsive UX that previously wasn’t attainable from the simplicity of a Server Rendered App.",-1),D=t("p",null,"Its implementation does pose some challenges in implementing certain features, but we’ll cover some approaches below we’ve used to overcome them below.",-1),L=t("h3",null,"Full Stack Web UI",-1),V=t("p",null,"Blazor’s static rendering with enhanced navigation and its opt-in flexibility makes .NET 8 Blazor a game changer, expanding it from a very niche set of use-cases that weren’t too adversely affected by its Interactivity mode downsides, to becoming a viable solution for developing any kind of .NET Web App, especially as it can also be utilized within existing ASP.NET MVC and Razor Pages Apps.",-1),P=t("h3",null,"Benefits over MVC and Razor Pages",-1),U=t("p",null,[n("In addition, Blazor’s superior component model allows building better encapsulated, more reusable and easier-to-use UI components which has enabled Blazor’s rich 3rd Party library ecosystem to flourish, that we ourselves utilize to develop the high productivity Tailwind Components in the "),t("a",{href:"https://blazor-gallery.servicestack.net"},"ServiceStack.Blazor"),n(" component library.")],-1),J=t("p",null,"So far there’s only upsides for .NET Web App development, the compromises only kick in when you need Blazor’s interactivity features, luckily these can now be scoped to just the Pages and Components that need them. But how often do we need them?",-1),O=t("h3",null,"When do you need Blazor’s Interactivity features?",-1),R=t("p",null,"It ultimately depends on what App your building, but a lot of Websites can happily display dynamic content, navigate quickly with enhanced navigation, fill out and submit forms - all in Blazor’s default static rendering mode.",-1),F=t("p",null,[n("Not even advanced features like "),t("strong",null,"Streaming Rendering"),n(" used in Blazor Template’s "),t("a",{href:"https://github.com/dotnet/aspnetcore/blob/v8.0.0-rc.2.23480.2/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Weather.razor"},"Weather.razor"),n(" page require interactivity, as its progressive rendered UI updates are achieved in a single request without interactivity.")],-1),H=t("p",null,[n("In fact the only time "),t("code",null,"@rendermode InteractiveServer"),n(" is needed in the default Blazor template is in the "),t("a",{href:"https://github.com/dotnet/aspnetcore/blob/v8.0.0-rc.2.23480.2/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Pages/Counter.razor#L3"},"Counter.razor"),n(" page whose C# Event Handling require it.")],-1),G=t("p",null,"Ultimately some form of Interactivity is going to be required in order to add behavior or client-side functionality that runs after pages have been rendered, but you still have some options left before being forced to opt into an Interactive Blazor solution.",-1),Y=t("h3",null,"Interactive Feature Options",-1),$=t("p",null,[n("We can see some of these options utilized in the Blazor Template "),t("a",{href:"https://github.com/dotnet/aspnetcore/blob/v8.0.0-rc.2.23480.2/src/ProjectTemplates/Web.ProjectTemplates/content/BlazorWeb-CSharp/BlazorWeb-CSharp/Components/Layout/NavMenu.razor"},"NavMenu.razor"),n(" component which uses JavaScript "),t("code",null,"onclick"),n(" event handlers to add client-side behavior to simulate mouse clicks to toggle UI elements:")],-1),Q=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),n("div")]),n(),t("span",{class:"token attr-name"},"class"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),n("nav-scrollable"),t("span",{class:"token punctuation"},'"')]),n(),t("span",{class:"token special-attr"},[t("span",{class:"token attr-name"},"onclick"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),t("span",{class:"token value javascript language-javascript"},[n("document"),t("span",{class:"token punctuation"},"."),t("span",{class:"token function"},"querySelector"),t("span",{class:"token punctuation"},"("),t("span",{class:"token string"},"'.navbar-toggler'"),t("span",{class:"token punctuation"},")"),t("span",{class:"token punctuation"},"."),t("span",{class:"token function"},"click"),t("span",{class:"token punctuation"},"("),t("span",{class:"token punctuation"},")")]),t("span",{class:"token punctuation"},'"')])]),t("span",{class:"token punctuation"},">")]),n(` `)])],-1),X=t("p",null,"and submitting forms to Logout users:",-1),Z=t("pre",{class:"language-html"},[t("code",{class:"language-html"},[t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),n("LogoutForm")]),n(),t("span",{class:"token attr-name"},"id"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),n("logout-form"),t("span",{class:"token punctuation"},'"')]),n(),t("span",{class:"token punctuation"},"/>")]),n(` `),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),n("NavLink")]),n(),t("span",{class:"token attr-name"},"class"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),n("nav-link"),t("span",{class:"token punctuation"},'"')]),n(),t("span",{class:"token special-attr"},[t("span",{class:"token attr-name"},"onclick"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),t("span",{class:"token value javascript language-javascript"},[n("document"),t("span",{class:"token punctuation"},"."),t("span",{class:"token function"},"getElementById"),t("span",{class:"token punctuation"},"("),t("span",{class:"token string"},"'logout-form'"),t("span",{class:"token punctuation"},")"),t("span",{class:"token punctuation"},"."),t("span",{class:"token function"},"submit"),t("span",{class:"token punctuation"},"("),t("span",{class:"token punctuation"},")"),t("span",{class:"token punctuation"},";"),n(),t("span",{class:"token keyword"},"return"),n(),t("span",{class:"token boolean"},"false"),t("span",{class:"token punctuation"},";")]),t("span",{class:"token punctuation"},'"')])]),t("span",{class:"token punctuation"},">")]),n(` `),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"<"),n("span")]),n(),t("span",{class:"token attr-name"},"class"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),n("bi bi-arrow-bar-left"),t("span",{class:"token punctuation"},'"')]),n(),t("span",{class:"token attr-name"},"aria-hidden"),t("span",{class:"token attr-value"},[t("span",{class:"token punctuation attr-equals"},"="),t("span",{class:"token punctuation"},'"'),n("true"),t("span",{class:"token punctuation"},'"')]),t("span",{class:"token punctuation"},">")]),t("span",{class:"token tag"},[t("span",{class:"token tag"},[t("span",{class:"token punctuation"},"")]),n(` Logout diff --git a/assets/2024-02-28_markdown-components-in-vue-CRnyvCSj.js b/assets/2024-02-28_markdown-components-in-vue-CRnyvCSj.js new file mode 100644 index 0000000..73c0018 --- /dev/null +++ b/assets/2024-02-28_markdown-components-in-vue-CRnyvCSj.js @@ -0,0 +1,133 @@ +import{_ as l}from"./MarkdownPage.vue_vue_type_script_setup_true_lang-CE9iaOou.js";import{_ as u}from"./GettingStarted.vue_vue_type_script_setup_true_lang-C2rK_3Mh.js";import{i,r as k,o as r,h as g,w as m,a as n,b as a,u as d,e as s}from"./index-DWUkzBt0.js";import{_ as h,a as f,b as y}from"./HelloApi.vue_vue_type_script_setup_true_lang-sMvvqITG.js";import{_ as v}from"./Counter.vue_vue_type_script_setup_true_lang-DcP0JNgA.js";import{C as w}from"./ChartJs-3tDylpqN.js";const _={class:"markdown-body"},b=n("h2",null,"Vue Components in Markdown Pages",-1),x=n("p",null,[s("Thanks to the "),n("a",{href:"https://github.com/unplugin/unplugin-auto-import"},"unplugin-auto-import"),s(" plugin, App Components in "),n("a",{href:"https://github.com/ServiceStack/press-vue/tree/main/src/components"},"/src/components"),s(" are automatically imported and can immediately referenced in "),n("code",null,"*.md"),s(" Markdown pages using normal Vue SFC syntax, e.g:")],-1),C=n("pre",{class:"language-tsx"},[n("code",{class:"language-tsx"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),n("span",{class:"token class-name"},"GettingStarted")]),s(),n("span",{class:"token attr-name"},"template"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("press-vue"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token punctuation"},"/>")]),s(` +`)])],-1),S={class:"py-20 not-prose flex justify-center"},V=n("p",null,[s("Additional Global Components can be registered in "),n("a",{href:"https://github.com/ServiceStack/press-vue/blob/main/src/main.ts"},"main.ts"),s(" when creating the Vue App where you can register entire Component libraries, Aliases and TypeScript Vue components, e.g")],-1),M=n("pre",{class:"language-ts"},[n("code",{class:"language-ts"},[n("span",{class:"token keyword"},"import"),s(" ServiceStackVue "),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@servicestack/vue"'),s(` +`),n("span",{class:"token keyword"},"import"),s(),n("span",{class:"token punctuation"},"{"),s(" Icon "),n("span",{class:"token punctuation"},"}"),s(),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},"'@iconify/vue'"),s(` +`),n("span",{class:"token keyword"},"import"),s(" LiteYouTube "),n("span",{class:"token keyword"},"from"),s(),n("span",{class:"token string"},'"@/components/LiteYouTube"'),s(` + +app + `),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"use"),n("span",{class:"token punctuation"},"("),s("ServiceStackVue"),n("span",{class:"token punctuation"},")"),s(" "),n("span",{class:"token comment"},"// @servicestack/vue library"),s(` + `),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"component"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},"'Iconify'"),n("span",{class:"token punctuation"},","),s(" Icon"),n("span",{class:"token punctuation"},")"),s(" "),n("span",{class:"token comment"},"// Alias"),s(` + `),n("span",{class:"token punctuation"},"."),n("span",{class:"token function"},"component"),n("span",{class:"token punctuation"},"("),n("span",{class:"token string"},"'LiteYouTube'"),n("span",{class:"token punctuation"},","),s(" LiteYouTube"),n("span",{class:"token punctuation"},")"),s(),n("span",{class:"token comment"},"// .ts Vue Component"),s(` +`)])],-1),q=n("p",null,[s("Which can also be referenced inside "),n("code",null,"*.md"),s(" Markdown files without importing them:")],-1),H=n("pre",{class:"language-tsx"},[n("code",{class:"language-tsx"},[n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),s("div")]),s(),n("span",{class:"token attr-name"},"class"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("py-20 flex justify-evenly"),n("span",{class:"token punctuation"},'"')]),n("span",{class:"token punctuation"},">")]),n("span",{class:"token plain-text"},` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),n("span",{class:"token class-name"},"Iconify")]),s(),n("span",{class:"token attr-name"},"class"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("w-28 h-28"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"icon"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("logos:vue"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token punctuation"},"/>")]),n("span",{class:"token plain-text"},` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),n("span",{class:"token class-name"},"Iconify")]),s(),n("span",{class:"token attr-name"},"class"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("w-28 h-28"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"icon"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("logos:vitejs"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token punctuation"},"/>")]),n("span",{class:"token plain-text"},` + `),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"<"),n("span",{class:"token class-name"},"Iconify")]),s(),n("span",{class:"token attr-name"},"class"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("w-28 h-28"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token attr-name"},"icon"),n("span",{class:"token attr-value"},[n("span",{class:"token punctuation attr-equals"},"="),n("span",{class:"token punctuation"},'"'),s("logos:react"),n("span",{class:"token punctuation"},'"')]),s(),n("span",{class:"token punctuation"},"/>")]),n("span",{class:"token plain-text"},` +`),n("span",{class:"token tag"},[n("span",{class:"token tag"},[n("span",{class:"token punctuation"},"")]),s(` +`)])],-1),j={class:"py-20 flex justify-evenly"},L=n("h2",null,"Importing Vue Components",-1),A=n("p",null,[s("Any other components you want to use in the markdown pages will need to imported right at the top of the page using standard import syntax inside a "),n("code",null," - + +