diff --git a/docz/docs/03-demos/02-frontend/09-blazor.md b/docz/docs/03-demos/02-frontend/09-blazor.md
new file mode 100644
index 0000000..c5b1a30
--- /dev/null
+++ b/docz/docs/03-demos/02-frontend/09-blazor.md
@@ -0,0 +1,439 @@
+---
+title: Sheets in Blazor Sites
+sidebar_label: Blazor
+pagination_prev: demos/index
+pagination_next: demos/grid/index
+sidebar_position: 9
+---
+
+import current from '/version.js';
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+import CodeBlock from '@theme/CodeBlock';
+
+[Blazor](https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor) is a
+framework for building user interfaces using C#, HTML, JS and CSS.
+
+[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
+data from spreadsheets.
+
+This demo uses Blazor and SheetJS to process and generate spreadsheets. We'll
+explore how to load SheetJS in Razor components and compare common state models
+and data flow strategies.
+
+:::caution Blazor support is considered experimental.
+
+Great open source software grows with user tests and reports. Any issues should
+be reported to the Blazor project for further diagnosis.
+
+:::
+
+:::danger Telemetry
+
+**The `dotnet` command embeds telemetry.**
+
+The `DOTNET_CLI_TELEMETRY_OPTOUT` environment variable should be set to `1`.
+
+["Platform Configuration"](#platform-configuration) includes instructions for
+setting the environment variable on supported platforms.
+
+:::
+
+## Integration Details
+
+### Installation
+
+The SheetJS library can be loaded when the page is loaded or imported whenever
+the library functionality is used.
+
+#### Standalone Script
+
+The [SheetJS Standalone scripts](/docs/getting-started/installation/standalone)
+can be loaded in the root HTML page (typically `wwwroot/index.html`):
+
+{`\
+
+`}
+
+
+#### ECMAScript Module
+
+The SheetJS ECMAScript module script can be dynamically imported from functions.
+This ensures the library is only loaded when necessary. The following example
+loads the library and returns a Promise that resolves to the version string:
+
+{`\
+async function sheetjs_version(id) {
+ /* dynamically import the script in the event listener */
+ // highlight-next-line
+ const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs");
+\n\
+ /* use the library */
+ return XLSX.version;
+}`}
+
+
+### Calling JS from C#
+
+#### Setup
+
+The primary mechanism for invoking JS functions from Blazor is `IJSRuntime`[^1].
+It should be injected at the top of relevant Razor component scripts:
+
+```csharp title="Injecting IJSRuntime"
+@inject IJSRuntime JS
+```
+
+#### Fire and Forget
+
+When exporting a file with the SheetJS `writeFile` method[^2], browser APIs do
+not provide success or error feedback. As a result, this demo invokes functions
+using the `InvokeVoidAsync` static method[^3]:
+
+```csharp title="Invoking JS functions from C#"
+private async Task ExportDataset() {
+ await JS.InvokeVoidAsync("export_method", data);
+}
+```
+
+Methods are commonly bound to buttons in the Razor template using `@onclick`:
+
+```html title="Binding callback to a HTML button"
+
+```
+
+### State in Blazor
+
+The example [presidents sheet](https://docs.sheetjs.com/pres.xlsx) has one
+header row with "Name" and "Index" columns.
+
+![`pres.xlsx` data](pathname:///pres.png)
+
+#### C# Representation
+
+The natural C# representation of a single row is a class object:
+
+```csharp title="President class"
+public class President {
+ public string Name { get; set; }
+ public int Index { get; set; }
+}
+
+var PrezClinton = new President() { Name = "Bill Clinton", Index = 42 };
+```
+
+The entire dataset is typically stored in an array of class objects:
+
+```csharp title="President dataset"
+private President[] data;
+```
+
+#### Data Interchange
+
+`InvokeVoidAsync` can pass data from the C# state to a JS function:
+
+```csharp
+ await JS.InvokeVoidAsync("export_method", data);
+```
+
+Each row in the dataset will be passed as a separate argument to the JavaScript
+method, so the JavaScript code should collect the arguments:
+
+```js title="Collecting rows in a JS callback"
+/* NOTE: blazor spreads the C# array, so the ... spread syntax is required */
+async function export_method(...rows) {
+ /* display the array of objects */
+ console.log(rows);
+}
+```
+
+Each row is a simple JavaScript object.
+
+:::caution pass
+
+Blazor automatically spreads arrays. Each row is passed as a separate argument
+to the JavaScript method.
+
+The example method uses the JavaScript spread syntax to collect the arguments.
+
+:::
+
+#### Exporting Data
+
+With the collected array of objects, the SheetJS `json_to_sheet` method[^4] will
+generate a SheetJS worksheet[^5] from the dataset. After creating a workbook[^6]
+object with the `book_new` method[^7], the file is written with `writeFile`[^2]:
+
+{`\
+/* NOTE: blazor spreads the C# array, so the spread is required */
+async function export_method(...rows) {
+ const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs");
+ const ws = XLSX.utils.json_to_sheet(rows);
+ const wb = XLSX.utils.book_new(ws, "Data");
+ XLSX.writeFile(wb, "SheetJSBlazor.xlsx");
+}`}
+
+
+
+### HTML Tables
+
+When displaying datasets, Razor components typically generate HTML tables:
+
+```html title="Razor template from official starter"
+
+
+
Date
Temp. (C)
Temp. (F)
Summary
+
+
+ @foreach (var forecast in forecasts)
+ {
+
+
@forecast.Date.ToShortDateString()
+
@forecast.TemperatureC
+
@forecast.TemperatureF
+
@forecast.Summary
+
+ }
+
+
+```
+
+If it has an `id`, JS code on the frontend can find the table element using the
+`document.getElementById` DOM method. A SheetJS workbook object can be generated
+using the `table_to_book` method[^8] and exported with `writeFile`[^2]:
+
+{`\
+/* NOTE: blazor spreads the C# array, so the spread is required */
+async function export_method() {
+ const XLSX = await import("https://cdn.sheetjs.com/xlsx-${current}/package/xlsx.mjs");
+ const wb = XLSX.utils.table_to_book(document.getElementById("weather-table"));
+ XLSX.writeFile(wb, "SheetJSBlazor.xlsx");
+}`}
+
+
+This approach uses data that already exists in the document, so no additional
+data is passed from C# to JavaScript.
+
+## Complete Demo
+
+The Blazor + WASM starter app includes a "Weather" component that displays data
+from a C#-managed dataset. This demo uses SheetJS to export data in two ways:
+
+- "Export Dataset" will send row objects from the underlying C# data store to
+ the frontend. The SheetJS `json_to_sheet` method[^4] builds the worksheet.
+
+- "Export HTML Table" will scrape the table using the SheetJS `table_to_book`
+ method[^8]. No extra data will be sent to the frontend.
+
+:::note Tested Deployments
+
+This demo was tested in the following deployments:
+
+| Architecture | Date |
+|:-------------|:-----------|
+| `darwin-arm` | 2024-10-15 |
+
+:::
+
+### Platform Configuration
+
+0) Set the `DOTNET_CLI_TELEMETRY_OPTOUT` environment variable to `1`.
+
+
+ How to disable telemetry (click to hide)
+
+
+
+
+Add the following line to `.profile`, `.bashrc` and `.zshrc`:
+
+```bash title="(add to .profile , .bashrc , and .zshrc)"
+export DOTNET_CLI_TELEMETRY_OPTOUT=1
+```
+
+Close and restart the Terminal to load the changes.
+
+
+
+
+Type `env` in the search bar and select "Edit the system environment variables".
+
+In the new window, click the "Environment Variables..." button.
+
+In the new window, look for the "System variables" section and click "New..."
+
+Set the "Variable name" to `DOTNET_CLI_TELEMETRY_OPTOUT` and the value to `1`.
+
+Click "OK" in each window (3 windows) and restart your computer.
+
+
+
+
+
+
+1) Install .NET
+
+
+ Installation Notes (click to show)
+
+For macOS x64 and ARM64, install the `dotnet-sdk` Cask with Homebrew:
+
+```bash
+brew install --cask dotnet-sdk
+```
+
+For Steam Deck Holo and other Arch Linux x64 distributions, the `dotnet-sdk` and
+`dotnet-runtime` packages should be installed using `pacman`:
+
+```bash
+sudo pacman -Syu dotnet-sdk dotnet-runtime
+```
+
+https://dotnet.microsoft.com/en-us/download/dotnet/6.0 is the official source
+for Windows and ARM64 Linux versions.
+
+
+
+2) Open a new Terminal window in macOS or PowerShell window in Windows.
+
+### App Creation
+
+3) Create a new `blazorwasm` app:
+
+```bash
+dotnet new blazorwasm -o SheetJSBlazorWasm
+cd SheetJSBlazorWasm
+dotnet run
+```
+
+When the Blazor service runs, the terminal will display a URL:
+
+```text
+info: Microsoft.Hosting.Lifetime[14]
+// highlight-next-line
+ Now listening on: http://localhost:6969
+```
+
+4) In a new browser window, open the displayed URL from Step 3.
+
+5) Click the "Weather" link and confirm the page includes a data table.
+
+6) Stop the server (press CTRL+C in the terminal window).
+
+### SheetJS Integration
+
+7) Add the following script tag to `wwwroot/index.html` in the `HEAD` block:
+
+{`\
+`}
+
+
+8) Inject the `IJSRuntime` dependency near the top of `Pages/Weather.razor`:
+
+```csharp title="Pages/Weather.razor (add highlighted lines)"
+@page "/weather"
+@inject HttpClient Http
+// highlight-next-line
+@inject IJSRuntime JS
+```
+
+9) Add an ID to the `TABLE` element in `Pages/Weather.razor`:
+
+```html title="Pages/Weather.razor (add id to TABLE element)"
+{
+
+
+
+
+```
+
+10) Add callbacks to the `@code` section in `Pages/Weather.razor`:
+
+```csharp title="Pages/Weather.razor (add within the @code section)"
+ private async Task ExportDataset()
+ {
+ await JS.InvokeVoidAsync("export_dataset", forecasts);
+ }
+
+ private async Task ExportHTML()
+ {
+ await JS.InvokeVoidAsync("export_html", "weather-table");
+ }
+```
+
+11) Add Export buttons to the template in `Pages/Weather.razor`:
+
+```csharp title="Pages/Weather.razor (add highlighted lines)"
+
This component demonstrates fetching data from the server.
+
+
+
+
+
+```
+
+
+### Testing
+
+12) Launch the `dotnet` process again:
+
+```bash
+dotnet run
+```
+
+When the Blazor service runs, the terminal will display a URL:
+
+```text
+info: Microsoft.Hosting.Lifetime[14]
+ Now listening on: http://localhost:6969
+```
+
+13) In a new browser window, open the displayed URL from Step 12.
+
+14) Click the "Weather" link. The page should match the following screenshot:
+
+![SheetJSBlazorWasm with Exports](pathname:///blazor/weather.png)
+
+15) Click the "Export Dataset" button and save the generated file to
+`SheetJSBlazorDataset.xlsx`. Open the file in a spreadsheet editor and confirm
+the data matches the table. The column labels will differ since the underlying
+dataset uses different labels.
+
+![SheetJSBlazorDataset.xlsx](pathname:///blazor/dataset.png)
+
+16) Click the "Export HTML TABLE" button and save the generated file to
+`SheetJSBlazorHTML.xlsx`. Open the file in a spreadsheet editor and confirm the
+data matches the table. The column labels will match the HTML table.
+
+![SheetJSBlazorHTML.xlsx](pathname:///blazor/html.png)
+
+:::note pass
+
+It is somewhat curious that the official `dotnet` Blazor sample dataset marks
+`1 C` and `-13 C` as "freezing" but marks `-2 C` as "chilly". It stands to
+reason that `-2 C` should also be freezing.
+
+:::
+
+[^1]: See ["Microsoft.JSInterop.IJSRuntime"](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.ijsruntime) in the `dotnet` documentation.
+[^2]: See [`writeFile` in "Writing Files"](/docs/api/write-options)
+[^3]: See ["Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync"](https://learn.microsoft.com/en-us/dotnet/api/microsoft.jsinterop.jsruntimeextensions.invokevoidasync) in the `dotnet` documentation.
+[^4]: See [`json_to_sheet` in "Utilities"](/docs/api/utilities/array#array-of-objects-input)
+[^5]: See ["Sheet Objects"](/docs/csf/sheet)
+[^6]: See ["Workbook Object"](/docs/csf/book)
+[^7]: See [`book_new` in "Utilities"](/docs/api/utilities/wb)
+[^8]: See [`table_to_book` in "HTML" Utilities](/docs/api/utilities/html#create-new-sheet)
diff --git a/docz/docs/03-demos/02-frontend/09-legacy.md b/docz/docs/03-demos/02-frontend/18-legacy.md
similarity index 99%
rename from docz/docs/03-demos/02-frontend/09-legacy.md
rename to docz/docs/03-demos/02-frontend/18-legacy.md
index fd8e314..280f667 100644
--- a/docz/docs/03-demos/02-frontend/09-legacy.md
+++ b/docz/docs/03-demos/02-frontend/18-legacy.md
@@ -2,7 +2,7 @@
title: Legacy Frameworks
pagination_prev: demos/index
pagination_next: demos/grid/index
-sidebar_position: 9
+sidebar_position: 18
sidebar_custom_props:
skip: 1
---
diff --git a/docz/static/blazor/dataset.png b/docz/static/blazor/dataset.png
new file mode 100644
index 0000000..b219977
Binary files /dev/null and b/docz/static/blazor/dataset.png differ
diff --git a/docz/static/blazor/html.png b/docz/static/blazor/html.png
new file mode 100644
index 0000000..bdcf919
Binary files /dev/null and b/docz/static/blazor/html.png differ
diff --git a/docz/static/blazor/weather.png b/docz/static/blazor/weather.png
new file mode 100644
index 0000000..9446afc
Binary files /dev/null and b/docz/static/blazor/weather.png differ