You can now easily build a browser extension with Blazor!
Demo
How to use this package
Important for v0.*.*:
This package is still in pre-release stage so the versioning does not comply with semantic versioning. Feature and bug fix increments the patch version and breaking change increments the minor version. So be sure to check the release note before upgrading between minor version.
This package imports two other packages, which are:
- WebExtensions.Net – Provides interop for WebExtensions standard API.
- JsBind.Net – Provides advanced JavaScript interop features used by WebExtensions.Net.
- Blazor.BrowserExtension.Build (in this repository) – Adds build target and tasks to the project.
Samples/References
Sample projects are available in the repository Blazor.BrowserExtension.Samples.
You can also refer to the following projects for real life extensions:
- Blazor Edge New Tab – Published for Chrome and Edge.
Or check out the GitHub dependency graph for more repositories.
Create new project
- Run
dotnet new --install Blazor.BrowserExtension.Template
. - Run
dotnet new browserext --name
to initialize a new project with the template. - Change the working directory into the newly created project directory.
- Run
dotnet build
to build the project.
If you are using Visual Studio, you can do all these from the UI after installing the template NuGet package, like how I did in the demo above (once you have enabled showing .Net Core templates in the New project dialog).
Setup existing project
- Install NuGet package
Blazor.BrowserExtension
. - Add
under thetrue
node in your.csproj
project file to automatically setup the project files to be compatible for building into browser extension. - Build the project.
Manual Setting Up
You can setup the project manually as well, if for some reason you encounter any problem with the bootstrapping step above.
- Add a new file
manifest.json
under thewwwroot
folder. An example of minimalmanifest.json
file:{ "manifest_version": 2, "name": "My Blazor Extension", "description": "My browser extension built with Blazor WebAssembly", "version": "0.1", "background": { "page": "background.html", "persistent": true }, "content_security_policy": "script-src 'self' 'unsafe-eval' 'wasm-eval' 'sha256-v8v3RKRPmN4odZ1CWM5gw80QKPCCWMcpNeOmimNL2AA='; object-src 'self'", "web_accessible_resources": [ "framework/*", "content/*" ] }
- Add the following to the
.csproj
file to make sure that all the files underwwwroot
will always be copied to the output.
“><ItemGroup> <None Include="wwwroot***" CopyToOutputDirectory="Always" /> ItemGroup>
- In
wwwroot/index.html
replace the script tagwith
- In
Pages/Index.razor
replace the first line@page "/"
with the following lines:@page "/index.html" @inherits Blazor.BrowserExtension.Pages.IndexPage
- Add a
Background.razor
file underPages
folder (Right click on thePages
folder and select Add → Razor Component), with the following content:@page "/background.html" @inherits Blazor.BrowserExtension.Pages.BackgroundPage @using WebExtensions.Net.Tabs @code { protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); // this opens index.html in the extension as a new tab when the background page is loaded var extensionUrl = await WebExtensions.Runtime.GetURL("index.html"); await WebExtensions.Tabs.Create(new CreateProperties() { url = extensionUrl }); } }
- Add the following into
Program.cs
file.public static async Task Main(string[] args) { ... builder.Services.AddBrowserExtensionServices(); ... }
If you are targeting .Net 5.0 you will need to add replace the original
HttpClient
service registration withPre-initialization script (app.js)A custom script can be run before the initialization of the Blazor application.
This is particularly useful for content scripts because we need to inject aDIV
element as the container for the Blazor application before it is initialized.To do so, create a file named
app.js
under the directorywwwroot
in your project.
The file will automatically be detected during the build and theapp.js
file will be executed every time the Blazor application is going to be initialized.Change initialization behaviour
Using an
app.js
, you can set the following properties in theBlazorBrowserExtension
global object to change the initialization behaviour.Property Name Description ImportBrowserPolyfill Set to false
to disable importing of the browser polyfill script.
Default:true
StartBlazorBrowserExtension Set to false
to prevent auto initialization of Blazor. UseBlazorBrowserExtension.BrowserExtension.InitializeAsync
to initialize manually.
Default:true
Example:
globalThis.BlazorBrowserExtension.ImportBrowserPolyfill = false; globalThis.BlazorBrowserExtension.StartBlazorBrowserExtension = false; globalThis.BlazorBrowserExtension.BrowserExtension.InitializeAsync("Production");
Build and load extension
Google Chrome
- Launch the Extensions page ( ⋮ → More tools → Extensions).
- Switch on
Developer mode
. - Click on the
Load unpacked
button, then navigate to%ProjectDir%binDebugnet5.0
and select the folderbrowserextension
.
Microsoft Edge
- Launch the Extensions page ( ⋮ → Extensions).
- Click on the ☰ and switch on
Developer mode
. - Click on the button with the title
Load unpacked
, then navigate to%ProjectDir%binDebugnet5.0
and select the folderbrowserextension
.
Mozilla Firefox
- Navigate to the URL about:debugging#/runtime/this-firefox
- Click on
Load Temporary Add-on...
, then navigate to%ProjectDir%binDebugnet5.0browserextension
and select any file in the directory.
Debugging locally in IIS Express or Kestrel
- Start the Blazor project directly from Visual Studio or
dotnet run
. - Once the application is loaded, use the Blazor debugging hotkey Shift+Alt+D to launch the debugging console.
At the moment, debugging when the application is loaded as an extension in the browser is not possible.
This is because debugging requires a NodeJs debugging proxy launched by the DevServer, which is not available when loaded as extension in the browser.Browser Extension features
Add a browser action popup page
Add the following to the
manifest.json
"browser_action": { "default_popup": "popup.html" }
Add a
Popup.razor
Razor component underPages
folder with the following content.My popup page”>@page "/popup.html" @inherits Blazor.BrowserExtension.Pages.BasePage <h1>My popup pageh1>
Add an extension options page
Add the following to the
manifest.json
"options_ui": { "page": "options.html", "open_in_tab": true }
Add a
Options.razor
Razor component underPages
folder with the following content.My options page”>@page "/options.html" @inherits Blazor.BrowserExtension.Pages.BasePage <h1>My options pageh1>
Add a content script
Add the following to the
manifest.json
"content_scripts": [ { "matches": [ "*://*/*" ], "js": [ "content/Blazor.BrowserExtension/ContentScript.js" ] } ], ... "web_accessible_resources": [ ... "app.js" ],
Add a
ContentScript.razor
Razor component underPages
folder with the following content.My content script”>@page "/contentscript.html" @inherits Blazor.BrowserExtension.Pages.BasePage <h1>My content scripth1>
Additional changes are required for content scripts to not have conflict of the element ID of the Blazor root component with any other elements in any pages it is injected in.
First of all, decide on a unique ID to be used for the application
DIV
container, such asMy_Unique_Extension_App_Id
(this ID is used in the steps below, replace with your own unique ID).
It should be unique enough that it does not collide with any existing element in the pages where the content scripts are going to be injected.- In
index.html
change the IDapp
of lineLoading...My_Unique_Extension_App_Id
. - In
Program.cs
change the ID#app
of linebuilder.RootComponents.Add
to("#app"); #My_Unique_Extension_App_Id
. - Add a new file named
app.js
under the directorywwwroot
(Skip this if it has been created already). - Add the following code in the
app.js
file to inject a newDIV
element with the matching ID into the current page.if (globalThis.BlazorBrowserExtension.BrowserExtension.Mode === globalThis.BlazorBrowserExtension.Modes.ContentScript) { const appDiv = document.createElement("div"); appDiv.id = "My_Unique_Extension_App_Id"; document.body.appendChild(appDiv); }
In
App.razor
, add the followingif
statement to opt out of routing only for content scripts.Browser Extension APIWhen you inherit from the
BasePage
, a few properties are injected for you to consume.
This includes the WebExtensions API and Logger.
The WebExtensions API is provided by the package WebExtensions.Net, and you can consume the API in a page:Get index page URL@indexPageUrl
@code {
string indexPageUrl = null;
async Task GetIndexPageUrl()
{
indexPageUrl = await WebExtensions.Runtime.GetURL(“index.html”);
}
}”>@inherits Blazor.BrowserExtension.Pages.BasePage; <button @onclick="GetIndexPageUrl">Get index page URLbutton> <p>@indexPageUrlp> @code { string indexPageUrl = null; async Task GetIndexPageUrl() { indexPageUrl = await WebExtensions.Runtime.GetURL("index.html"); } }
How does routing work
Default routing
The default routing when using this package is physical file routing.
When building the project,- all the razor components are processed to get a list of all the physical file routes
- the routing entry file (default is
index.html
, seeBrowserExtensionRoutingEntryFile
below) is copied to the output directory based on the list of physical file routes
For example, if the
Background.razor
contains@page "/background.html"
and theOptions.razor
contains@page "/options.html"
, when the project is built or published, the fileindex.html
will be copied/duplicated to the output directory with the namebackground.html
andoptions.html
.This is especially useful for browser extensions because the browsers only serve static files, and the presence of these physical files supports the routing when the extension page is reloaded.
Virtual path routing
Routing with virtual path is also supported, however not encouraged due to the requirements.
Virtual path routing means that the routes do not have a corresponding physical file, for example
/background
or/options
.
What this means is that when the user tries to reload the page, the browser will return a page not found (404) error.To overcome this, the background page intercept
Read More