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="app.css" />
|
||||||
<link rel="stylesheet" href="Blog.styles.css" />
|
<link rel="stylesheet" href="Blog.styles.css" />
|
||||||
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
|
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
|
||||||
<HeadOutlet @rendermode="InteractiveServer" />
|
<HeadOutlet />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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/anchor-js/anchor.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/clipboard@2.0.11/dist/clipboard.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>
|
<script src="_framework/blazor.web.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
<header class="container">
|
<header class="container">
|
||||||
<div class="title-container">
|
<div class="title-container">
|
||||||
<a href="/">
|
<NavLink href="/">
|
||||||
<h1>journal</h1>
|
<h1>journal</h1>
|
||||||
</a>
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="author-container">
|
<div class="author-container">
|
||||||
<span>by</span>
|
<span>by</span>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@rendermode InteractiveServer
|
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<Header />
|
<Header />
|
||||||
@@ -13,3 +12,10 @@
|
|||||||
<a href="" class="reload">Reload</a>
|
<a href="" class="reload">Reload</a>
|
||||||
<a class="dismiss">🗙</a>
|
<a class="dismiss">🗙</a>
|
||||||
</div>
|
</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;
|
right: 0.75rem;
|
||||||
top: 0.5rem;
|
top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#components-reconnect-modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code{
|
@code
|
||||||
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
private HttpContext? HttpContext { get; set; }
|
private HttpContext? HttpContext { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
@page "/{slug:nonfile}"
|
@page "/{slug:nonfile}"
|
||||||
@implements IAsyncDisposable
|
|
||||||
@inject IPostService PostService
|
@inject IPostService PostService
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@@ -13,11 +12,12 @@ else
|
|||||||
{
|
{
|
||||||
<PageTitle>@BlogPost.Title - journal</PageTitle>
|
<PageTitle>@BlogPost.Title - journal</PageTitle>
|
||||||
|
|
||||||
|
<PageScript Src="./Components/Pages/Post.razor.js" />
|
||||||
|
|
||||||
<HeadContent>
|
<HeadContent>
|
||||||
<meta name="description" content="@BlogPost.Lead">
|
<meta name="description" content="@BlogPost.Lead">
|
||||||
<link rel="stylesheet" href="markdown.css">
|
<link rel="stylesheet" href="markdown.css">
|
||||||
<link rel="stylesheet" href="prism.css">
|
<link rel="stylesheet" href="prism.css">
|
||||||
<script src="prism.js"></script>
|
|
||||||
</HeadContent>
|
</HeadContent>
|
||||||
|
|
||||||
<article>
|
<article>
|
||||||
@@ -31,11 +31,11 @@ else
|
|||||||
</article>
|
</article>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code
|
||||||
|
{
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Slug { get; set; } = string.Empty;
|
public string Slug { get; set; } = string.Empty;
|
||||||
private PostWithContent? BlogPost;
|
private PostWithContent? BlogPost;
|
||||||
private IJSObjectReference? module;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -49,32 +49,4 @@ else
|
|||||||
|
|
||||||
BlogPost = post;
|
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
|
* @summary Adds anchors to each heading in the post
|
||||||
* so that users can link directly to them.
|
* so that users can link directly to them.
|
||||||
*/
|
*/
|
||||||
export function addAnchors() {
|
function addAnchors() {
|
||||||
const selectors = [
|
const selectors = [
|
||||||
'.markdown-body h2',
|
'.markdown-body h2',
|
||||||
'.markdown-body h3',
|
'.markdown-body h3',
|
||||||
@@ -21,7 +49,7 @@ export function addAnchors() {
|
|||||||
* @summary Adds a button to each code block that
|
* @summary Adds a button to each code block that
|
||||||
* allows users to copy the code to their clipboard.
|
* allows users to copy the code to their clipboard.
|
||||||
*/
|
*/
|
||||||
export function addClipboard() {
|
function addClipboard() {
|
||||||
const codeBlocks = document.querySelectorAll('pre');
|
const codeBlocks = document.querySelectorAll('pre');
|
||||||
|
|
||||||
codeBlocks.forEach((block) => {
|
codeBlocks.forEach((block) => {
|
||||||
|
|||||||
@@ -18,20 +18,23 @@
|
|||||||
<section role="feed" class="container">
|
<section role="feed" class="container">
|
||||||
@foreach (var post in Posts)
|
@foreach (var post in Posts)
|
||||||
{
|
{
|
||||||
<a href="/@post.Slug">
|
<NavLink href="@GetLink(post)">
|
||||||
<article>
|
<article>
|
||||||
<h3 title="@post.Title" >@post.Title</h3>
|
<h3 title="@post.Title" >@post.Title</h3>
|
||||||
<p class="date">@post.PublishedAt.ToString("MMMM d, yyyy")</p>
|
<p class="date">@post.PublishedAt.ToString("MMMM d, yyyy")</p>
|
||||||
<p class="lead">@post.Lead</p>
|
<p class="lead">@post.Lead</p>
|
||||||
</article>
|
</article>
|
||||||
</a>
|
</NavLink>
|
||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code
|
||||||
|
{
|
||||||
private List<Post>? Posts;
|
private List<Post>? Posts;
|
||||||
|
|
||||||
|
private string GetLink(Post post) => $"/{post.Slug}";
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
Posts = await postService.GetPostsAsync();
|
Posts = await postService.GetPostsAsync();
|
||||||
|
|||||||
@@ -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 Microsoft.JSInterop
|
||||||
@using Blog
|
@using Blog
|
||||||
@using Blog.Components
|
@using Blog.Components
|
||||||
|
@using Blog.Components.Utils
|
||||||
|
|||||||
+2
-7
@@ -4,9 +4,7 @@ builder.Services.ConfigureOptions<FilePostServiceOptionsSetup>();
|
|||||||
builder.Services.AddSingleton<IFileSystem, FileSystem>();
|
builder.Services.AddSingleton<IFileSystem, FileSystem>();
|
||||||
builder.Services.AddScoped<IPostService, FilePostService>();
|
builder.Services.AddScoped<IPostService, FilePostService>();
|
||||||
|
|
||||||
builder.Services
|
builder.Services.AddRazorComponents();
|
||||||
.AddRazorComponents()
|
|
||||||
.AddInteractiveServerComponents();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
@@ -88,10 +86,7 @@ app
|
|||||||
.WithDisplayName("RSS Feed")
|
.WithDisplayName("RSS Feed")
|
||||||
.WithDescription("RSS feed for the blog");
|
.WithDescription("RSS feed for the blog");
|
||||||
|
|
||||||
app
|
app.MapRazorComponents<App>();
|
||||||
.MapRazorComponents<App>()
|
|
||||||
.AddInteractiveServerRenderMode();
|
|
||||||
|
|
||||||
|
|
||||||
app.Run();
|
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