fix: switch to SSR only

- not actually making use of interactive server
  functionality
- don't need to deal with websocket disconnect
  when server is restarted
- can get necessary functionality using static
  rendering plus executing js on client
  when loaded
- added page script component to handle
  loading collocated scripts for pages that
  also work with enhanced navigation
This commit is contained in:
Stevan Freeborn
2024-04-13 19:09:03 -05:00
parent 279f80fc97
commit 66bef092f3
12 changed files with 179 additions and 50 deletions
+3 -2
View File
@@ -8,13 +8,14 @@
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="Blog.styles.css" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<HeadOutlet @rendermode="InteractiveServer" />
<HeadOutlet />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<Routes />
<script src="https://cdn.jsdelivr.net/npm/anchor-js/anchor.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.11/dist/clipboard.min.js"></script>
<script src="prism.js"></script>
<script src="_framework/blazor.web.js"></script>
</body>
+2 -2
View File
@@ -2,9 +2,9 @@
<header class="container">
<div class="title-container">
<a href="/">
<NavLink href="/">
<h1>journal</h1>
</a>
</NavLink>
</div>
<div class="author-container">
<span>by</span>
+7 -1
View File
@@ -1,5 +1,4 @@
@inherits LayoutComponentBase
@rendermode InteractiveServer
<div class="container">
<Header />
@@ -13,3 +12,10 @@
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<div id="components-reconnect-modal">
<div>
<h3>Connection lost</h3>
<p>Attempting to reconnect...</p>
</div>
</div>
@@ -32,3 +32,11 @@ main {
right: 0.75rem;
top: 0.5rem;
}
#components-reconnect-modal {
display: none;
position: fixed;
bottom: 0;
right: 0;
padding: 0.5rem;
}
+2 -1
View File
@@ -15,7 +15,8 @@
}
</div>
@code{
@code
{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
+4 -32
View File
@@ -1,5 +1,4 @@
@page "/{slug:nonfile}"
@implements IAsyncDisposable
@inject IPostService PostService
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@@ -13,11 +12,12 @@ else
{
<PageTitle>@BlogPost.Title - journal</PageTitle>
<PageScript Src="./Components/Pages/Post.razor.js" />
<HeadContent>
<meta name="description" content="@BlogPost.Lead">
<link rel="stylesheet" href="markdown.css">
<link rel="stylesheet" href="prism.css">
<script src="prism.js"></script>
</HeadContent>
<article>
@@ -31,11 +31,11 @@ else
</article>
}
@code {
@code
{
[Parameter]
public string Slug { get; set; } = string.Empty;
private PostWithContent? BlogPost;
private IJSObjectReference? module;
protected override async Task OnInitializedAsync()
{
@@ -49,32 +49,4 @@ else
BlogPost = post;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./Components/Pages/Post.razor.js");
await module.InvokeVoidAsync("addAnchors");
await module.InvokeVoidAsync("addClipboard");
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
try
{
if (module is not null)
{
await module.DisposeAsync();
}
}
catch (Exception ex) when (ex is JSDisconnectedException)
{
}
catch (Exception ex)
{
Logger.LogError(ex, "Error disposing of the module");
}
}
}
+30 -2
View File
@@ -1,9 +1,37 @@
let initialized = false;
export function onLoad() {
if (initialized) {
return;
}
init();
}
export function onUpdate() {
if (initialized) {
return;
}
init();
}
export function onDispose() {
initialized = false;
}
function init(){
addAnchors();
addClipboard();
Prism.highlightAll();
initialized = true;
}
/**
* @summary Adds anchors to each heading in the post
* so that users can link directly to them.
*/
export function addAnchors() {
function addAnchors() {
const selectors = [
'.markdown-body h2',
'.markdown-body h3',
@@ -21,7 +49,7 @@ export function addAnchors() {
* @summary Adds a button to each code block that
* allows users to copy the code to their clipboard.
*/
export function addClipboard() {
function addClipboard() {
const codeBlocks = document.querySelectorAll('pre');
codeBlocks.forEach((block) => {
+6 -3
View File
@@ -18,19 +18,22 @@
<section role="feed" class="container">
@foreach (var post in Posts)
{
<a href="/@post.Slug">
<NavLink href="@GetLink(post)">
<article>
<h3 title="@post.Title" >@post.Title</h3>
<p class="date">@post.PublishedAt.ToString("MMMM d, yyyy")</p>
<p class="lead">@post.Lead</p>
</article>
</a>
</NavLink>
}
</section>
}
@code {
@code
{
private List<Post>? Posts;
private string GetLink(Post post) => $"/{post.Slug}";
protected override async Task OnInitializedAsync()
{
@@ -0,0 +1,19 @@
@namespace Blog.Components.Utils
@*
custom web-component defined in:
Blog.lib.module.js
Manages loading colocated scripts
for pages and invoking expected
lifecycle methods to support
enhanced page navigation.
*@
<page-script src="@Src"></page-script>
@code
{
[Parameter]
[EditorRequired]
public string Src { get; set; } = default!;
}
+1
View File
@@ -8,3 +8,4 @@
@using Microsoft.JSInterop
@using Blog
@using Blog.Components
@using Blog.Components.Utils
+2 -7
View File
@@ -4,9 +4,7 @@ builder.Services.ConfigureOptions<FilePostServiceOptionsSetup>();
builder.Services.AddSingleton<IFileSystem, FileSystem>();
builder.Services.AddScoped<IPostService, FilePostService>();
builder.Services
.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddRazorComponents();
var app = builder.Build();
@@ -88,10 +86,7 @@ app
.WithDisplayName("RSS Feed")
.WithDescription("RSS feed for the blog");
app
.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.MapRazorComponents<App>();
app.Run();
+95
View File
@@ -0,0 +1,95 @@
const pageScriptInfoBySrc = new Map();
function registerPageScriptElement(src) {
if (!src) {
throw new Error('Must provide a non-empty value for the "src" attribute.');
}
let pageScriptInfo = pageScriptInfoBySrc.get(src);
if (pageScriptInfo) {
pageScriptInfo.referenceCount++;
return;
}
pageScriptInfo = { referenceCount: 1, module: null };
pageScriptInfoBySrc.set(src, pageScriptInfo);
initializePageScriptModule(src, pageScriptInfo);
}
function unregisterPageScriptElement(src) {
if (!src) {
return;
}
const pageScriptInfo = pageScriptInfoBySrc.get(src);
if (!pageScriptInfo) {
return;
}
pageScriptInfo.referenceCount--;
}
async function initializePageScriptModule(src, pageScriptInfo) {
// If the path is relative, normalize it by by making it an absolute URL
// with document's the base HREF.
if (src.startsWith("./")) {
src = new URL(src.substr(2), document.baseURI).toString();
}
const module = await import(src);
if (pageScriptInfo.referenceCount <= 0) {
// All page-script elements with the same 'src' were
// unregistered while we were loading the module.
return;
}
pageScriptInfo.module = module;
module.onLoad?.();
module.onUpdate?.();
}
function onEnhancedLoad() {
// Start by invoking 'onDispose' on any modules that are no longer referenced.
for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) {
if (referenceCount <= 0) {
module?.onDispose?.();
pageScriptInfoBySrc.delete(src);
}
}
// Then invoke 'onUpdate' on the remaining modules.
for (const { module } of pageScriptInfoBySrc.values()) {
module?.onUpdate?.();
}
}
export function afterWebStarted(blazor) {
customElements.define(
"page-script",
class extends HTMLElement {
static observedAttributes = ["src"];
// We use attributeChangedCallback instead of connectedCallback
// because a page-script element might get reused between enhanced
// navigations.
attributeChangedCallback(name, oldValue, newValue) {
if (name !== "src") {
return;
}
this.src = newValue;
unregisterPageScriptElement(oldValue);
registerPageScriptElement(newValue);
}
disconnectedCallback() {
unregisterPageScriptElement(this.src);
}
}
);
blazor.addEventListener("enhancedload", onEnhancedLoad);
}