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:
@@ -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,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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -15,7 +15,8 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
@code{
|
||||
@code
|
||||
{
|
||||
[CascadingParameter]
|
||||
private HttpContext? HttpContext { get; set; }
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -8,3 +8,4 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using Blog
|
||||
@using Blog.Components
|
||||
@using Blog.Components.Utils
|
||||
|
||||
+2
-7
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user