Ausnahme gefangen: SSL certificate problem: certificate is not yet valid 📌 Server Side Rendering a Blog with Web Components

🏠 Team IT Security News

TSecurity.de ist eine Online-Plattform, die sich auf die Bereitstellung von Informationen,alle 15 Minuten neuste Nachrichten, Bildungsressourcen und Dienstleistungen rund um das Thema IT-Sicherheit spezialisiert hat.
Ob es sich um aktuelle Nachrichten, Fachartikel, Blogbeiträge, Webinare, Tutorials, oder Tipps & Tricks handelt, TSecurity.de bietet seinen Nutzern einen umfassenden Überblick über die wichtigsten Aspekte der IT-Sicherheit in einer sich ständig verändernden digitalen Welt.

16.12.2023 - TIP: Wer den Cookie Consent Banner akzeptiert, kann z.B. von Englisch nach Deutsch übersetzen, erst Englisch auswählen dann wieder Deutsch!

Google Android Playstore Download Button für Team IT Security



📚 Server Side Rendering a Blog with Web Components


💡 Newskategorie: Programmierung
🔗 Quelle: dev.to

This blog post supports a Youtube Livestream scheduled for Wednesday 4/19 at 12pm EST / 9am PST. You can watch the livestream here on Youtube.

Introduction

It has never been easier to server side render a website. Years ago it took server side technologies like PHP and Java, requiring web developers to learn another language besides JavaScript. With the introduction of Node.js, web developers got a JavaScript runtime on the server and tools such as Express that could handle HTTP requests and return HTML to the client. Meta-frameworks for server side rendering sprang up that supported popular libraries and frameworks like React, Angular, Vue, or Svelte.

Meta-frameworks had to consider React a first-class citizen because of Virtual DOM, an abstraction around DOM that enables the library to "diff" changes and update the view. Virtual DOM promised efficiency, but what we got was a toolchain that made web development more difficult to support Virtual DOM. Tooling had to be created and maintained to support rendering Virtual DOM server-side, then "hydrating" the view client-side. The nasty side-effect of so much tooling being that web developers have to maintain many dependencies, which districts developers from developing features and implementing bug fixes.

Hydration became part of the vocabulary of several web developers in recent years. Hydration being the process of using client-side JavaScript to add state and interactivity to server-rendered HTML. React popularized many of the concepts web developers think of when we say "server-side rendering" today.

What if I told you there was a way to server-side render HTML with way less tooling? Would that be enticing?

If you like what you are reading in this blog post consider purchasing a copy of my book Fullstack Web Components available at newline.co. Details following this post.

In this guide, I'll demonstrate how to server-side render autonomous custom elements using Express middleware with an open source package developed by Google. @lit-labs/ssr is library package under active development by the team that maintains Lit, a popular library for developing custom elements. @lit-labs/ssr is part of the Lit Labs family of experimental packages. Even though the package is in "experimental" status, the core offering is quite stable for use with "vanilla" custom elements.

You can render custom elements today server-side with @lit-labs/ssr by binding the render function exported by the package to Express middleware. @lit-labs/ssr supports rendering custom elements that extend from LitElement first and foremost, although LitElement is based off web standards, the class is extended from HTMLElement. @lit-labs/ssr relies on another web standard named Declarative Shadow DOM.

Declarative Shadow DOM

Declarative Shadow DOM is the web specification what makes server-side rendering custom elements possible. Prior to this standard, the only way to develop custom elements was imperatively. You had to define a class and inside of the constructor construct a shadow root. In the below example, we have a custom element named AppCard that imperatively constructs a shadow root. The benefit of a shadow root is that we gain encapsulation. CSS and HTML defined in the context of the shadow root can't leak out into the rest of the page.

class AppCard extends HTMLElement {
  constructor() {
    super();
    if (!this.shadowRoot) {
      const shadowRoot = this.attachShadow({ mode: 'open' });
      const template = document.createElement('template');
      template.innerHTML = `
      <style>
      ${styles}
      </style>
      <header>
        <slot name="header"></slot>
      </header>
      <section>
        <slot name="content"></slot>
      </section>
      <footer>
        <slot name="footer"></slot>
      </footer>
      `;
      shadowRoot.appendChild(template.content.cloneNode(true));
    }
  }
}

customElements.define('app-card', AppCard);

Declarative Shadow DOM allows you to define the same template for the component declaratively. Here is an example of the same shadow root for AppCard defined with Declarative Shadow DOM.

<app-card>
  <template shadowrootmode="open"> 
    <style>
      ${styles}
    </style>
    <header>
      <slot name="header"></slot>
    </header>
    <section>
      <slot name="content"></slot>
    </section>
    <footer>
      <slot name="footer"></slot>
    </footer> 
  </template>
  <img slot="header" src="${thumbnail}" alt="${alt}" />
  <h2 slot="header">${headline}</h2>
  <p slot="content">${content}</p>
  <a href="/post/${link}" slot="footer">Read Post</a>
</app-card>

Declarative Shadow DOM introduced the shadowrootmode attribute to HTML templates. This attribute is detected my the HTML parser and applied as the shadow root of the parent element, in this example <app-card>. The above example uses template slots to dynamically inject content into a custom element template. With Declarative Shadow DOM, any HTML defined outside of the template is considered "Light DOM" that can be projected through the slot to "Shadow DOM". The usage of ${} syntax is merely for the example. This isn't some kind of data-binding technique. Since the template is now defined declaratively, you can reduce the definition to a string. ES2015 template strings are well suited for this purpose. In the demo, we'll use template strings to define each component's template declaratively using the Declarative Shadow DOM standard.

But wait? If the component's template is reduced to a string how do you inject interactivity into the component client-side? You still have to define the component imperatively for the client, but since Shadow DOM is already instantiated because the browser already parsed the Declarative Shadow DOM template, you no longer need to instantiate the template. You may still imperatively instantiate Shadow DOM if the shadow root doesn't already exist.

class AppCard extends HTMLElement {
  constructor() {
    super();
    if (!this.shadowRoot) {
      // instantiate the template imperatively
      // if a shadow root doesn't exist 
    }
    ...

Optionally, you can hydrate the component client-side differently when a shadow root is detected (because the component was server-side rendered).

class AppCard extends HTMLElement {
  constructor() {
    super();
    if (this.shadowRoot) {
      // bind event listeners here
      // or handle other client-side interactivity
    }
    ...

Custom elements must be registered regardless if their templates are defined imperatively or declaratively.

customElements.define('app-card', AppCard);

The architects in the crowd may notice one flaw with this approach. Won't the same template need to defined twice? Once for Declarative Shadow DOM and a second time for the declaration in the custom element's constructor. Sure, but we can mitigate this by using ES2015 template strings. By implementing the template through composition, we can inject the template typically defined imperatively into the other defined declaratively. We'll make sure to reuse partial templates in each component developed in this workshop.

What You Will Build

In this workshop, you'll server-side render four custom elements necessary to display a blog:

  • AppCard displays a card
  • AppHeader displays the site header
  • MainView displays the site header and several cards
  • PostView displays the site header and a blog post

The main view of the blog displays a header and cards

Acceptance Criteria

The blog should have two routes. One that displays a list of the latest posts, another that displays the content of a single post.

  • When the user visits http://localhost:4444 the user should view the site header and several cards (MainView).

  • When the user visits http://localhost:4444/post/:slug the user should view the site header and blog post content (PostView). The route includes a variable "slug" which is dynamically supported by the blog post.

Project Structure

The workspace is a monorepo consisting of 4 project directories:

  • client: Custom elements rendered client and server-side
  • server: Express server that handles server-side rendering
  • shim: Custom shim for browser specifications not found in Node.js, provided by Lit
  • style: Global styles for the blog site

Lerna and Nx handle building the project, while nodemon handles watching for changes and rebuilding the project.

The project is mainly coded with TypeScript.

For the workshop, you'll focus primarily on a single file found at /packages/server/src/middleware/ssr.ts. This file contains the middleware that handles server-side rendering. You'll also edit custom elements found in packages/client/src/. Each file includes some boilerplate to get your started.

Architecture

In this workshop you'll develop methods for asynchronously rendering Declarative Shadow DOM templates. Each view is mapped to a Route. Both Route listed in the acceptance criteria have been defined in the starter code in this file: packages/client/src/routes.ts.

export const routes: Routes = [
  {
    path: "/",
    component: "main",
    tag: "main-view",
    template: mainTemplate,
  },
  {
    pathMatch: /\/post\/([a-zA-Z0-9-]*)/,
    component: "post",
    tag: "post-view",
    template: postTemplate,
  },
];

We need a static definition of the routes somewhere. Putting the definition in an Array exported from a file is an opinion. Some meta-frameworks obfuscate this definition with the name of directories parsed at build or runtime. In the middleware, we'll reference this Array to check if a route exists at the path the user is requesting the route. Above the routes are defined with an identifier: path. path: "/", matches the root, i.e. http://localhost:4444. The second example uses pathMatch instead. The route used to display each post is dynamic, it should display a blog post by slug. Each route also corresponds to a template, which we'll define in each "view" file as a Declarative Shadow DOM template string, or more accurately, a function that returns a template string. An example of a template is below.

export const mainTemplate = () => `<style>
    ${styles}
  </style>
  <div class="container">
  <!-- put content here -->
  </div>`;

During the workshop, we'll display a static route, but soon after make each view dependent on API requests. The JSON returned from the API endpoints will be used as a model for the view. We'll export a named function from each view file that fetches the data and returns the model. An example of this is below.

function fetchModel(): Promise<DataModel> {
  return Promise.all([
    fetch("http://localhost:4444/api/meta"),
    fetch("http://localhost:4444/api/posts"),
  ])
    .then((responses) => Promise.all(responses.map((res) => res.json())))
    .then((jsonResponses) => {
      const meta = jsonResponses[0];
      const posts = jsonResponses[1].posts;
      return {
        meta,
        posts,
      };
    });
}

In the above example, two calls to fetch request the metadata for the site and an Array of recent blog posts from two different API endpoints. The responses are mapped to the model needed to display the view.

A description of each API endpoint is below, but first let's look at the flow of information.

Markdown -> JSON w/ embedded Markdown -> HTML -> Declarative Shadow DOM Template

Blog posts are stored in markdown format in the directory packages/server/data/posts/. Two API endpoints (/api/posts and /api/post/:slug) fetch the markdown from each file and return that markdown in the format of a Post. The type definition of a Post is as-follows:

export type Post = {
  content: string;
  slug: string;
  title: string;
  thumbnail: string;
  author: string;
  excerpt: string;
};

Another endpoint handles metadata for the entire page. The interface for the data returned by this endpoint is simplified for the workshop and could be expanded for SEO purposes.

export type Meta = {
  author: string;
  title: string;
};

During the workshop, you'll rely on three local API endpoints to fetch the metadata of the site and the data associated with each blog post. A description of each endpoint is below.

http://localhost:4444/api/meta returns the metadata necessary to display the site header in JSON format.

http://localhost:4444/api/post/:slug returns a single blog post by it's "slug", a string delineated by - in JSON format.

http://localhost:4444/api/posts returns an array of blog posts in JSON format.

You will make requests to these endpoints during the workshop and use the JSON responses to populate the content for each Declarative Shadow DOM template. In each file that requires data, a type definition for Meta and Post schema is already provided in packages/server/src/db/index.ts. These type definitions are imported into relevant files to ease in development.

In addition to the three provided local endpoints, you'll be making a request to the Github API to parse the markdown returned from the included blog post files. This API is necessary because it provides the simplest way to parse code snippets found in markdown files and convert them to HTML.

Getting Started

You'll need a GitHub account. You can use GitHub to sign-in to StackBlitz and later, you'll need GitHub to generate a token.

If you don't already have a GitHub account, signup for Github here.

If you want to follow along with this tutorial, fork the StackBlitz or Github repo.

When using Stackblitz, the development environment will immediately install dependencies and load an embedded browser. VS Code is available in browser for development. Stackblitz works best in Google Chrome.

If using Github, fork the repository and clone the repo locally. Run npm install and npm run dev. Begin coding in your IDE of choice.

Important Note About Tokens

During the workshop, we'll be using the Github API known as Octokit to generate client-side HTML from Markdown for each blog post. If you're using Stackblitz, an API token is provided for the workshop but will be revoked soon after. If you've cloned the repo or the token is revoked, login to GitHub and generate a new token on Github for use in the workshop.

But be like me, never store tokens statically in code. The only reason the token is injected this way for the workshop is because it was the easiest method to start making requests with Octokit.

Important Note About Support

StackBlitz works best in Google Chrome. If you are following along with StackBlitz, make sure you are using Google Chrome.

Examples included in this workshop will work in every mainstream browser except Firefox. Although Mozilla has signaled support for implementing Declarative Shadow DOM, the browser vender has yet to provide support in a stable release. A ponyfill is available to address the lack of support in Firefox. Support for Declarative Shadow DOM is subject to change in Firefox. I have faith the standard will become cross-browser compatible in the near future because Mozilla recently changed it's position on Declarative Shadow DOM. I've been using the ponyfill for over a year without issues in browsers that lack support for the standard.

Server Side Rendering Custom Elements with @lit-labs/ssr

Let's get started coding, shall we? The first task to work on is declaring a Declarative Shadow DOM template for the component named MainView found in packages/client/src/view/main/index.ts. This view will ultimately replace the boilerplate that is currently displaying "Web Component Blog Starter" in the browser at the path http://localhost:4444/.

Supporting Declarative Shadow DOM in MainView

Open packages/client/src/view/main/index.ts in your IDE and find the Shadow DOM template that is currently defined imperatively in the constructor of the MainView class. We're going to declare this template instead as an ES2015 template string returned by a function named shadowTemplate. Cut and paste the current template into the template string returned by shadowTemplate.

const shadowTemplate = () => html`<style>
    ${styles}
  </style>
  <div class="post-container">
    <app-header></app-header>
  </div>`;

For your convenience, html is imported into this file, which you can use to tag the template literal. If you opted for GitHub and cloned the repo locally, you can enable this VSCode extension that offers syntax highlighting of HTML and CSS inside tagged template literals. That's really the only purpose of html, although you could exploit this function and parse the template string in different ways if you wanted.

Update the imperative logic to call shadowTemplate which should return the same String as before.

template.innerHTML = shadowTemplate();

In an effort to reuse the template, we can pass it to the next function we'll declare named template. the format is the same, although this time the template normally declared imperatively is encapsulated by the HTML tag associated with the component (<main-view>) and a HTML template with the attribute shadowrootmode set to open. Inject the shadowTemplate() we defined earlier inside of the <template> tags.

const template = () => html`<main-view>
  <template shadowrootmode="open">
    ${shadowTemplate()}
  </template>
  </main-view>`;

Congratulations! You just declared your first Declarative Shadow DOM template. Some benefits of this approach is that we can reuse the typical shadow root template declared imperatively and since we're using function, inject arguments that can be passed to the ES2015 template string. This will come in handy when we want to populate the template with data returned from API endpoints.

Finally, be sure to export the template from the file. More on why we're exporting the MainView class and template function later.

export { MainView, template };

This is a good start, but we'll run into an issue if we keep the code as-is. While it is possible to reuse template partials, the above example is kind of an exaggeration. You'll rarely be able to inject the entire shadow root into the Declarative Shadow DOM template like we did, especially when there are child custom elements that also need to be rendered server-side with Declarative Shadow DOM. In the above example, <app-header> can't currently be rendered on the server because we haven't declared that component's template with Declarative Shadow DOM. Let's do that now.

Supporting Declarative Shadow DOM in Header

To declare a new template to server-side render the AppHeader component, open packages/client/src/component/header/Header.ts.

Just like in the last example, cut and paste the template that is defined imperatively in the AppHeader constructor into a new function named shadowTemplate that returns the same string. This time, inject a single argument into the function that deconstructs an Object with two properties: styles and title and pass those to the template.

const shadowTemplate = ({ styles, title }) => html`
  <style>
    ${styles}
  </style>
  <h1>${title}</h1>
`;

Call shadowTemplate with the first argument, setting to two properties that are currently found in the file: styles and title

template.innerHTML = shadowTemplate({ styles, title });

For low level components, it's quite alright to define arguments in this way that later populate the template. It will offer some flexibility later on when we need to map the response of an API endpoint to what the template function expects. To declare the Declarative Shadow DOM template, define a new function named template, this time encapsulating the call to shadowTemplate with the <app-header> tag and HTML template.

const template = ({ styles, title }) => html`<app-header>
  <template shadowrootmode="open">
  ${shadowTemplate({ styles, title })}
  </template>
</app-header>`;

We can reuse the shadow root template used declaratively in this case because AppHeader is a leaf node, that is to say, it has no direct descendants that need to also be rendered server-side with Declarative Shadow DOM.

Finally, export all the necessary parts that can be reused in MainView.

export { styles, title, template, AppHeader };

Updating MainView with the Declarative Header

Open packages/client/src/view/main/index.ts again and import the parts from Header.js needed to render the template this time renamed as appHeaderTemplate. Notice how styles as appHeaderStyles is used to not conflict with this local styles declaration in packages/client/src/view/main/index.ts.

import {
  styles as appHeaderStyles,
  template as appHeaderTemplate,
  title,
  AppHeader,
} from '../../component/header/Header.js';

Update the template function, replacing the shadowTemplate call with appHeaderTemplate, being sure to pass in the styles and title.

const template = () => html`<main-view>
  <template shadowrootmode="open">
    ${appHeaderTemplate({ styles: appHeaderStyles, title})}
  </template>
  </main-view>`;

Keen observers may notice an opportunity here. We don't have to always use the styles and title prescribed in Header.ts, but use the call to appHeaderTemplate as a means to override styling or set the title dynamically. We'll do the latter in a later section of this workshop.

Supporting Declarative Shadow DOM in PostView

Before we code the Express middleware needed to server-side render these Declarative Shadow DOM templates we've declared so far, we have some housekeeping to do. The second view component defined in packages/client/src/view/post/index.ts also needs a function named template exported from the file. This component is responsible for displaying a single blog post.

Notice a pattern that is forming? Patterns are very helpful when server-side rendering components. If each component reliably exports the same named template, we can ensure the middleware reliably interprets each Declarative Shadow DOM template.

Open packages/client/src/view/post/index.ts. Cut and copy the template declared imperatively into a function named shadowTemplate, just like you did in the last two components.

const shadowTemplate = () => html`<style>
    ${styles}
  </style>
  <div class="post-container">
    <app-header></app-header>
    <div class="post-content">
      <h2>Author: </h2>
      <footer>
        <a href="/">👈 Back</a>
      </footer>
    </div>
  </div>`;

Call shadowTemplate() in the constructor of PostView.

template.innerHTML = shadowTemplate();

Declare a new function named template and make sure to encapsulate the shadowTemplate with Declarative Shadow DOM.

const template = () => html`<post-view>
  <template shadowrootmode="open">
    ${shadowTemplate()}
  </template>
</post-view>`;

Export template from packages/client/src/view/post/index.ts.

export { PostView, template };

We'll return to this file later, but for now that should be enough to display a bit of text and a link that navigates the user back to MainView. Onto the middleware...

Express Middleware

@lit-labs/ssr is an npm package distributed by Lit at Google for server-side rendering Lit templates and components. The package is for use in the context of Node.js and can be used in conjunction with Express middleware. Express is a popular HTTP server for Node.js that is largely based on middleware. Express middleware are functions that intercept HTTP requests and allow you to handle HTTP responses. We'll code an Express middleware that handles requests to http://localhost:4444 and http://localhost:4444/post/ and calls a function exported as ssr. The algorithm in that function is what you'll be working on.

First, it's good to know in packages/server/src/index.ts the middleware you'll be working with is imported and set on two routes.

import ssr from "./middleware/ssr.js";
...
app.get("/", ssr);
app.get("/post/:slug", ssr);

When the user visits http://localhost:4444/ or http://localhost:4444/post/, the middleware is activated. The notation :slug in the second route for the post view denotes the middleware should expect a path segment named "slug" that will now appear on the request params passed into the middleware function.

If you've never coded Node.js, have no fear. We'll go step-by-step like in the pervious sections to code the Express middleware that will serve each view's Declarative Shadow DOM template.

Open packages/server/src/middleware/ssr.ts to get started coding the Express middleware. Browse the file to acquaint yourself with the available import and declared const.

Notice how different files from the monorepo are injected into the HTML declared in renderApp. Relative paths are used here with the readFileSync to find the path of the global styles file named style.css and then minify the styles (for increased performance). The minification could be turned off by dynamically setting the minifyCSS with the env variable, which is used to determine if the code is running in "development" or "production" environments.

const stylePath = (filename) =>
  resolve(`${process.cwd()}../../style/${filename}`);
const readStyleFile = (filename) =>
  readFileSync(stylePath(filename)).toString();

const styles = await minify(readStyleFile('style.css'), {
  minifyCSS: env === "production",
  removeComments: true,
  collapseWhitespace: true,
});

In the above example, the global styles of the blog are read from a file in the monorepo and then minified depending on the environment.

The middleware function is found at the bottom of the file. Currently, the middleware calls renderApp() and responds to the HTTP request by calling res.status(200), which is the HTTP response code for "success" and then .send() with the ES2015 template string returned from renderApp.

export default async (req, res) => {
  const ssrResult = renderApp();
  res.status(200).send(ssrResult);
};

Select the boilerplate shown in the below image and remove the <script>.

Image description

Insert a new string in the template to reveal a change in the browser window. In the below example, we inject "Hello World" into the template. This will be temporary, as the renderApp function will become dynamic soon.

function renderApp() {
  return `<!DOCTYPE html>
  <html lang="en">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Web Components Blog">
        <style rel="stylesheet" type="text/css">${styles}</style>
    </head>
    <body> 
      <div id="root">${'Hello World'}</div>
  </body></html>`;
}

Handling Routes in Middleware

Since this middleware will handle multiple routes in Express, we need a way to detect if a route should be served. HTML should only be served if a route is declared. In packages/client/src/routes.js each route is declared in an Array. You can import this Array directly from the file, ensuring to use the file extension ".js". This is how imports are handled with ES2015 modules in Node.js, which mirrors the way browsers expect imports to be declared. Eventhough we're coding in TypeScript files, ".js" is still used in the path.

import { routes } from '../../../client/src/routes.js';

Each Route is typed as follows. You can reference this type to code an algorithm that returns the Route based on the HTTP request. You may also rely on your IDE intellisense.

export type Route = {
  component: string;
  path?: string;
  pathMatch?: RegExp;
  tag: string;
  template: (data?: any) => string;
  title?: string;
  params?: any;
};

In the middleware function, write an algorithm that handles two different use cases:

  1. If the route declares a "path", match the route exactly to the current originalUrl on the HTTP request.
  2. If the your declares a "pathMatch", which is a way to match routes by RegExp, call test on the RegExp to determine if the regular expression matches the originalUrl on the HTTP request.

When you are finished, log the matched route. It should log the Route in your Terminal. An edge case should be accounted for when the user visits http://localhost:4444 instead of http://localhost:4444/ and the path is declared as /, there should still be a match.

export default async (req, res) => {
  let route = routes.find((r) => {
    // handle base url
    if (
      (r.path === '/' && req.originalUrl == '') ||
      (r.path && r.path === req.originalUrl)
    ) {
      return r;
    }
    // handle other routes
    if (r.pathMatch?.test(req.originalUrl)) {
      return r;
    }
  });

  console.log(route);
  ...

If there isn't a match, Array.prototype.find will return undefined, so account for this by redirecting the user to a "404" route. We won't actually work on this route now, but for bonus points you could later server-side render a 404 page.

  if (route === undefined) {
    res.redirect(301, '/404');
    return;
  }

Next, add the current HTTP request params to the Route. This will be necessary for the single post view which has a single param :slug which can now be accessed via route.params.slug.

  route = {
    ...route,
    params: req.params,
  };

Now that you have a matched route, you should have access to the route's template stored on the Route, but we first have to "build" the route in development like it were built in production. When the routes are built in the client package, each route is deployed to packages/client/dist as a separate JavaScript bundle. We can simulate this in the development environment by using rebuild programmatically to build the JavaScript bundle that matches each route.

First, define a new function that returns the path to the either the view's source in the src directory or the bundle in the dist directory of the client package. We'll need both paths because during development we'll build each view from the src directory to dist.

const clientPath = (directory: 'src' | 'dist', route: any) => {
  return resolve(
`${process.cwd()}../../client/${directory}/view/${route.component}/index.js`
  );
};

Use the new clientPath function in the context of the middleware to build the JavaScript for the route, calling esbuild.build with the path to the source file mapped to entryPoints and the outfile mapped to the path to the distributed bundle in the dist directory. This will simulate how the bundles are distributed for production, but utilize esbuild for development which should be fast and efficient upon each request in the development environment.

 if (env === 'development') {
    await esbuild.build({
      entryPoints: [clientPath('src', route)],
      outfile: clientPath('dist', route),
      format: 'esm',
      minify: env !== 'development',
      bundle: true,
    });
}

Rendering the Template

To render the Declarative Shadow DOM template exported from each bundle, we need to dynamically import the JavaScript bundle. Reuse the clientPath function, this time in the context of a dynamic import statement to set a new const named module. This will give us access to whatever is exported from the view's JavaScript bundle.

  const module = await import(clientPath('dist', route));

Declare another const named compiledTemplate that calls template function exported from module. template returns the Declarative Shadow DOM template we defined earlier in packages/client/src/view/main/index.ts.

  const compiledTemplate = module.template();

Before we pass the String returned by the template function to another function named render exported from @lit-labs/ssr, we need to sanitize the HTML. render will ultimately be the function that parses and streams the Declarative Shadow DOM template for the HTML response. We could also provide a custom sanitization algorithm, but whatever the outcome we need to pass the template through the unsafeHTML function exported from @lit-labs/ssr. You could think of this function like dangerouslySetInnerHTML from React. The render function exported from @lit-labs/ssr expects any HTML template to be sanitized through unsafeHTML prior to calling render.

Make a new function named sanitizeTemplate which should be marked async because unsafeHTML itself is asynchronous. html in this example is exported from the lit package, it's the function used by the Lit library for handling HTML templates. It's very similar in nature to the html we tagged the template literals with earlier in the client code, but works well within in the Lit ecosystem.

export const sanitizeTemplate = async (template) => {
  return await html`${unsafeHTML(template)}`;
};

In the middleware function exported in ssr.ts make a new const and set it to a call to sanitizeTemplate, passing in the compiledTemplate. Finally, pass template to renderApp.

const template = await sanitizeTemplate(compiledTemplate);
const ssrResult = renderApp(template);

Add a first argument to renderApp appropriately named template. Replace the "Hello World" from earlier with a call to render, passing in the template. render is the function exported from @lit-labs/ssr that is responsible for rendering the Declarative Shadow DOM template for the entire view. There is some logic render for handling components built with Lit, but it can also accept vanilla Declarative Shadow DOM templates.

function renderApp(template) {
  return `<!DOCTYPE html>
  <html lang="en">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Web Components Blog">
        <style rel="stylesheet" type="text/css">${styles}</style>
    </head>
    <body> 
      <div id="root">${render(template)}</div>
  </body></html>`;
}

If you use intellisense to reveal more about render, you'll learn render accepts any template that can be parsed my lit-html, another package in the Lit ecosystem. You'll also find out the function returns a "string iterator". The thing to take away here is that render works with vanilla custom elements and custom elements built with Lit.

Image description

By now you'll notice the template doesn't render correctly. Instead [object Generator] is printed in the browser. What is going on here? The hint comes from what render returns: a "string iterator". Generator was introduced in ES2015 and it's an often under appreciated, yet powerful aspect of the JavaScript language, although Lit is using Generators here to support streaming the value over the request/response lifecycle of a HTTP request, for instance.

Image description

Handling The Generator

An easy way to support the output of render in out code is to convert renderApp to a Generator. That is rather easy. Generator can be defined using the function* syntax, which defines a function that returns a Generator. Calling a Generator does not immediately execute the algorithm defined in the function* body. You must define yield expressions, that specify a value returned by the iterator. Generator are just one kind of Iterator. yield* delegates something to another Generator, in our example render.

Convert renderApp to a Generator function, breaking up the HTML document into logical yield or yield* expressions, like in the below example.

function* renderApp(template) {
  yield `<!DOCTYPE html>
  <html lang="en">
    <head>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Web Components Blog">
        <style rel="stylesheet" type="text/css">${styles}</style>
    </head>
    <body> 
      <div id="root">`;
  yield* render(template);
  yield `</div>
  </body></html>`;
}

Once you're complete, the output in the browser window should change. Rather anticlimactic, huh?

Image description

There is a hint for the reason this is happening in the documentation for render. That function "streams" the result and we haven't yet handled the stream. Before we do, update the bottom of the middleware function, being sure to await the result of renderApp, which is now asynchronous due to being a Generator function.

  const ssrResult = await renderApp(template);

Streaming the Result

Streams in Node.js can be expressed as Buffer. Ultimately what we need to do is convert the stream to a String that can be sent to the client in the HTTP response as an HTML document. Buffer can be converted to a String by calling toString, which also accepts utf-8 formatting as an argument. UTF-8 is necessary because it is defined as the default character encoding for HTML.

Streams are asynchronous. We can use a for combined with an await to push each "chunk" of the stream to a Buffer, then combine all the Buffer using Buffer.concat and converting the result to a UTF-8 encoded String. Make a new function named streamToString that does that, giving it an argument named stream and make the function return the String.

async function streamToString(stream) {
  const chunks = [];
  for await (let chunk of stream) {
    chunks.push(Buffer.from(chunk));
  }
  return Buffer.concat(chunks).toString('utf-8');
}

The above seems like it would just accept our stream returned from renderApp, but we first should pass the stream through Readable, a utility in Node.js that allows us to read the stream. Make another async function named renderStream.

async function renderStream(stream) {
  return await streamToString(Readable.from(stream));
}

Finally, in the middleware function make another variable named stream and call renderStream, passing in the ssrResult. Then, call res.send with the stream.

 let stream = await renderStream(ssrResult);
 res.status(200).send(stream);

Voilà! The title defined in the Declarative Shadow DOM template is now visible in the browser.

Image description

If you inspect the HTML you'll notice artifacts left over from the render function, comments that Lit left behind. Declarative Shadow DOM template has successfully been server-side rendered.

The hard part is over. We'll return to the middleware to enhance parts of it later, but a large majority of the work is done. The middleware you just coded can now be used to render Declarative Shadow DOM templates.

Let's add some content to the view. Next, you'll update the Card custom element to export a Declarative Shadow DOM template, then import the template into the MainView custom element. Then, you'll populate the view with cards that display metadata about each blog post.

Updating Card with Declarative Shadow DOM

In this section you'll learn how to handle HTML template slots with Delcarative Shadow DOM templates after working on the most complex template yet, a reusable card that only accept content through HTML template slots.

To update the Card open packages/client/src/component/card/Card.ts. Just like in previous examples, cut and paste the template that is declared imperatively in the constructor to a template string returned by a new function named shadowTemplate. Add a single argument to this function that allows you to pass in the styling for the template. Notice how the header, content, footer layout of each Card accepts a named HTML template slot.

const shadowTemplate = ({ styles }) => html`
  <style>
    ${styles}
  </style>
  <header>
    <slot name="header"></slot>
  </header>
  <section>
    <slot name="content"></slot>
  </section>
  <footer>
    <slot name="footer"></slot>
  </footer>
`;

Set the innerHTML in the custom element constructor to the new shadowTemplate function.

template.innerHTML = shadowTemplate({ styles });

Link in the example of Header, Card is a leaf-node, so we can reuse the shadowTemplate wholesale within the Declarative Shadow DOM template. Make a new function named template that passes in a single argument and encapsulate the shadowTemplate with the proper syntax.

const template = ({ styles }) => html`<app-card>
  <template shadowrootmode="open"> 
  ${shadowTemplate({ styles })}
  </template>
</app-card>`;

Cards should display a thumbnail, the blog post title, a short description, and link. Declare new properties on the first argument of the template function that account for content, headline, link, and thumbnail, in addition to styles.

We can still project content into the slots in a Declarative Shadow DOM template. Shadow DOM is contained within the shadowTemplate function. Whatever elements are places outside of the <template> are considered Light DOM and can be projected through the slots using the slot attribute. For each of the properties on the model, make a new element. A new <img> tag with a slot attribute set to header will project into the custom element's <header>. Set the src attribute to an interpolated ${thumbnail} and the alt to an interpolated ${content}, to describe the image for screen readers. Additionally define a <h2> and <p>. If any part of the template becomes too complex, you can wrap the entire template partial in string interpolation. This is the case with setting the href attribute on the <a> tag, which displays a "Read Post" link for the user.

const template = ({
  content,
  headline,
  link,
  thumbnail,
  styles,
}) => html`<app-card>
  <template shadowrootmode="open"> ${shadowTemplate({ styles })} </template>
  <img slot="header" src="${thumbnail}" alt="${content}" />
  <h2 slot="header">${headline}</h2>
  <p slot="content">${content}</p>
  ${html`<a href="/post/${link}" slot="footer">Read Post</a>`}
</app-card>`;

Since each element utilizes a named HTML template slot that matches a slot found in Shadow DOM for this custom element, each element will be project into Shadow DOM. If two elements share a named slot, they will be projected in the order they are defined.

Finally, export styles and template from the file.

export { styles, template, AppCard };

We'll switch our attention now to packages/client/src/view/main/index.ts where we need to update the Declarative Shadow DOM template to accept the new template exported from Card.ts.

Fetching the Data Model

Views in modern websites are rarely static. Usually a view must be populated with content from a database. In our project, data is stored in markdown files for each blog post. Either way, a REST API could be used to fetch data from an endpoint and populate the view with content. In this next section, we'll develop a pattern for fetching data from a REST API and integrating the resulting JSON with the middleware you coded in a pervious section.

You'll inject the data from two endpoints into the MainView Declarative Shadow DOM template. JSON returned from 'http://localhost:4444/api/meta can be used to render AppHeader, while the Array of Post returned from http://localhost:4444/api/posts will be used to render a list of AppCard.

Open packages/client/src/view/main/index.ts to begin coding this section.

Just like we did with template, we are standardizing a pattern. This time for declaring asynchronous API calls that fetch data for the purpose of injecting that data into each Declarative Shadow DOM template. Essentially, we're talking about the "model" for the "view", so we'll call the new function "fetchModel".

This function should return a Promise. With TypeScript, we can strictly type define the Promise and match that definition with the first argument of template. Eventually this data model will get passed to template, enabling us to hydrate the view with content.

function fetchModel(): Promise<DataModel> {}
const template = (data: DataModel) => string;

This may seem disconnected at first, because nowhere in index.ts do these two function work together. In the Express middleware we have access to exports from the bundle, so if we export fetchModel, we can also call the function in the context of the middleware and pass the result to template, which expects the same DataModel. This is why we need a standardized pattern.

An implementation of fetchModel for MainView would be as follows. Use Promise.all to call each endpoint with fetch, then map each response to the JSON returned from the endpoints, and finally call then again to map the resulting JSON to the schema expected in DataModel. The JSON response from http://localhost:4444/api/meta will be used to populate AppHeader, while http://localhost:4444/api/posts will be used to populate content for a list of AppCard.

function fetchModel(): Promise<DataModel> {
  return Promise.all([
    fetch('http://localhost:4444/api/meta'),
    fetch('http://localhost:4444/api/posts'),
  ])
    .then((responses) => Promise.all(responses.map((res) => res.json())))
    .then((jsonResponses) => {
      const meta = jsonResponses[0];
      const posts = jsonResponses[1].posts;
      return {
        meta,
        posts,
      };
    });
}

Update template with a new argument named data and type define it as DataModel. Update the appHeaderTemplate title with the title accessed by data.meta.title.

const template = (data: DataModel) => html`<main-view>
  <template shadowrootmode="open">
    ${appHeaderTemplate({ styles: appHeaderStyles, title: data.meta.title })}
  </template>
  </main-view>`;

We still need to integrate the cards that display a preview of each blog post. Import AppCard, styles and template from Card.js, being sure to rename imports where appropriate so they don't clash with any local variables.

import {
  styles as appCardStyles,
  template as appCardTemplate,
  AppCard,
} from '../../component/card/Card.js';

Integrate appCardTemplate into the template function, mapping the Array found at data.posts to the model necessary to display each Card. You need to convert the Array to a String, so call join with an empty String as the argument to convert the Array to a String. Optionally, use the utility named joinTemplates imported into the file instead of calling join directly. Also inject the styles for each Card here.

const template = (data: DataModel) => html`<main-view>
  <template shadowrootmode="open">
    <style>
      ${styles}
    </style>
    <div class="post-container">
    ${appHeaderTemplate({ styles: appHeaderStyles, title: data.meta.title })}
    ${data.posts
      .map(
        (post) =>
          `${appCardTemplate({
            styles: appCardStyles,
            headline: post.title,
            content: post.excerpt,
            thumbnail: post.thumbnail,
            link: post.slug,
          })}`
      )
      .join('')}
     </div>
  </template>
  </main-view>`;`;

Export all of the following from index.ts.

export { template, fetchModel, AppCard, AppHeader, MainView };

Handling fetchModel in Middleware

To integrate the fetchModel exported from the bundle into the middleware, open packages/server/src/middleware/ssr.ts.

At the top of the middleware function declare a new variable named fetchedData.

export default async (req, res) => {
  let fetchedData;

After the line where we import the bundle, check if the ES2015 module exports fetchModel with a conditional expression. If truthy, set fetchedData to the result of module.fetchModel() using an await because fetchModel returns a Promise. We don't necessarily want to make fetchModel required for any hypothetical static layouts.

Anticipating the next route that displays a single post, pass in the route to fetchModel. Our existing implementation will effectively ignore this argument, but we'll need information on the route.params in the next example. Finally, pass fetchedData into the call for module.template.

  const module = await import(clientPath('dist', route));

  if (module.fetchModel) {
    fetchedData = await module.fetchModel(route);
  }

  const compiledTemplate = module.template(fetchedData);

You should now be able to view a server-side rendered header and list of cards at http://localhost:4444. This view is rendered entirely server-side. Network requests are made server-side, the Declarative Shadow DOM template is constructed and then iterated upon by @lit-labs/ssr render Generator. Next, you'll take what you've learned and render a single post view using the same methods.

Rendering A Single Post

If you click on any "Read More" links you'll be greeted with a rather unimpressive layout. A blank "Author:" field and a hand pointing "Back" should greet you. This is the result of the boilerplate we worked on earlier.

Image description

Upon navigation to this view, you should notice the Terminal update with a different Route if you still have the console.log enabled.

Image description

In this section we're going to populate the single post view with the content of a blog post. Each blog post is written in markdown and stored in the packages/server/data/posts directory. Each post can be accessed via a REST API endpoint at http://localhost:4444/api/post/:slug. If there is a post with a matching "slug", the endpoint returns the blog post in JSON, with the markdown found on the JSON response.

Open packages/client/src/view/post/index.ts to begin coding against the single post view.

import {
  styles as appHeaderStyles,
  template as appHeaderTemplate,
  AppHeader,
} from '../../component/header/Header.js';
  const template = (data: DataModel) => html`<post-view>
  <template shadowrootmode="open">
    <style>
      ${styles}
    </style>
    <div class="post-container">
      ${appHeaderTemplate({ styles: appHeaderStyles, title: data.post.title })}
      <div class="post-content">
        <h2>Author: ${data.post.author}</h2>
        ${data.html}
        <footer>
          <a href="/">👈 Back</a>
        </footer>
      </div>
    </div>
  </template>
</post-view>`;

We can largely reuse the fetchModel function from the main view in single post view. Copy and paste the function from main/index.ts to post/index.ts, modifying it to accept a new argument. Remember when we passed the route to fetchModel in the middleware? This is why. We need the slug property found on route.params to make the request to http://localhost:4444/api/post/:slug. Modify the fetchModel function until you get a working example, like below.

After we receive the markdown, we need to convert the markdown into useable HTML. The GitHub API is provided in this file to help with this purpose. The blog posts contain code snippets and Octokit is a convenient utility for parsing the markdown and converting it into HTML. Set a new const named request with the response from both local API endpoints and then await another HTTP request to the OctoKit API by calling octokit.request. We want to make a POST request to the /markdown endpoint, passing in the request.post.content to the text property on the body of the request, while making sure to specific the API version in the headers.


function fetchModel({ params }): Promise<any> {
  const res = async () => {
    const request = await Promise.all([
      fetch('http://localhost:4444/api/meta'),
      fetch(`http://localhost:4444/api/post/${params['slug']}`),
    ])
      .then((responses) => Promise.all(responses.map((res) => res.json())))
      .then((jsonResponses) => {
        return {
          meta: jsonResponses[0],
          post: jsonResponses[1].post,
        };
      });

    const postContentTemplate = await octokit.request('POST /markdown', {
      text: request.post.content,
      headers: {
        'X-GitHub-Api-Version': '2022-11-28',
      },
    });
    return {
      ...request,
      html: postContentTemplate.data,
    };
  };
  return res();
}

postContentTemplate will return the converted Markdown into HTML via the data property. Return that in the fetchModel function, along with the meta and post on a new property named html. When you are finished, export all the relevant parts for the middleware.

export { template, fetchModel, AppHeader, PostView };

If you refresh or navigate back to /, then click "Read More" on any card, you now should be able to view a single blog post. You just completed your second server-side rendered view with Declarative Shadow DOM! This is the last view of the workshop. Remaining sections will cover additional content for handling SEO, hydration, and more.

Handling Metadata for SEO

In this particular scenario, a lot can be gleaned from the blog post content for SEO. Each markdown file has a header which contains metadata that could be used for SEO purposes.

---
title: "Form-associated custom elements FTW!"
slug: form-associated-custom-elements-ftw
thumbnail: /assets/form-associated.jpg
author: Myself
excerpt: Form-associated custom elements is a web specification that allows engineers to code custom form controls that report value and validity to `HTMLFormElement`...
---

This header gets transformed by the http://localhost:4444/api/post API endpoint into JSON using the matter package. You can review for yourself in packages/server/src/route/post.ts.

You could expand upon this content, delivering relevant metadata for JSON-LD or setting <meta> tags in the HTML document. For the purposes of this workshop, we'll only set one aspect of the document.head, the page <title>, but you can extrapolate on this further and modify renderApp to project specifications.

Open packages/server/src/middleware/ssr.ts to get started for coding for this section and navigate to the middleware function. Some point before the multiple await, we can ass relevant SEO metadata to the Route. If we wanted to override the route.title with the title of each blog post, we could do that by setting the property with the value returned from fetchedData.meta.title.

  route.title = route.title ? route.title : fetchedData.meta.title;

Pass route into the renderApp function.

  const ssrResult = await renderApp(template, route);

Set the <title> tag with the route.title.

function* renderApp(template, route) {
  yield `<!DOCTYPE html>
  <html lang="en">
    <head>
        <title>${route.title}</title>
   ...

Wherever you derive SEO metadata from could vary per project, but in our case we can store this metadata on each markdown file. Since we have repeatable patterns like Route and fetchModel, we can reliably deliver metadata to each HTML document server-side rendered in the middleware.

Hydration

So far all the coding you've done has been completely server-side. Any JavaScript that ran happened on the server and each Declarative Shadow DOM template was parsed by the browser. But that's it. If there's any other JavaScript in the custom element that needs to run in the browser, like binding a callback to a click listener, that isn't happening yet. We need to hydrate the custom elements in a particular view client side.

There's an easy and performant fix. We could host the bundle for each route locally and add a script tag to the HTML document that makes the request, but a more performant solution would be to inline the JavaScript, eliminating the network request entirely.

readFileSync is a Node.js utility that allows us to read the bundle from a file and convert the content to a String. Set the String to a new const named script.

  const module = await import(clientPath('dist', route));
  const script = await readFileSync(clientPath('dist', route)).toString();

Pass the script to the renderApp function in a third argument.

const ssrResult = await renderApp(template, route, script);

Add a <script> tag to the end of the HTML document, setting the content with the String named script.

function* renderApp(route, template, script) {
  yield `<!DOCTYPE html>
  <html lang="en">
    <head>
        <title>${route.title}</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta name="description" content="Web Components Blog">
        <style rel="stylesheet" type="text/css">${styles}</style>
    </head>
    <body> 
      <div id="root">`;
  yield* render(template);
  yield `</div>
    <script type="module">${script}</script>
  </body></html>`;
}

We can test hydration is now available in AppHeader. Open
packages/client/src/component/header/Header.ts.

Add an else statement to the conditional already defined in the constructor. This conditional if (!this.shadowRoot) is checking if a shadow root doesn't exist and if so, imperatively declares a new shadow root. Since the custom element has a template declared with Declarative Shadow DOM, the content inside the if won't run, so we need an else if we hope to run additional logic, client-side only. Modify the <h1> that already exists (because it was server-side rendered) by appending Hydrated to the element innerText.

class AppHeader extends HTMLElement {
  constructor() {
    super();
    if (!this.shadowRoot) {
      const shadowRoot = this.attachShadow({ mode: 'open' });
      const template = document.createElement('template');
      template.innerHTML = shadowTemplate({ styles, title });
      shadowRoot.appendChild(template.content.cloneNode(true));
    } else {
      const title = this.shadowRoot.querySelector('h1').innerText;
      this.shadowRoot.querySelector('h1').innerText = `${title} Hydrated`;
    }
  }
}

Remove the else once your test is complete. This section demoed how to hydrate custom elements client-side, but how does the server understand any of this JavaScript? Some of you maybe wondering how did the server interpret class AppHeader extends HTMLElement when HTMLElement doesn't exist in Node.js. This final section is an explainer of how this works with @lit-labs/ssr. Coding is complete. Congratulations! You've finished the workshop.

Shimming Browser Spec

Lit shims browser specifications it needs to run on the server via a function exported from @lit-labs/ssr/lib/dom-shim.js named installWindowOnGlobal. If all your project relies on is autonomous custom elements extended from HTMLElement you can get by just with calling this function prior to anything else in Node.js.

A custom implementation of the shim is found at packages/shim/src/index.ts. I used this custom shim for the chapters in the book Fullstack Web Components because the views we server-side render in the book contain Customized built-in elements. Lit was thoughtful enough to allow engineers like myself to extend the shim with other mocks necessary to server-side render browser specifications. Use this file as an example of how you could shim browser spec in your server-side rendered project.

installShimOnGlobal is exported from this package in the monorepo and imported into packages/server/src/index.ts where it's called before any other code.

import { installShimOnGlobal } from "../../shim/dist/index.js";

installShimOnGlobal();

It's necessary to shim browser specifications before any other code runs in Node.js. That's why the shim is executed first.

Conclusion

In this workshop, you server-side rendered a blog using Declarative Shadow DOM and the @lit-labs/ssr package with Express middleware. You took autonomous custom elements that declared a shadow root imperatively and made reusable templates that also support Declarative Shadow DOM. Declarative Shadow DOM is a standard that allows web developers to encapsulate styling and template with a special HTML template, declaratively. The "special" part of the HTML template is the element must use the shadowrootmode attribute.

You learned how to use the render Generator exported from @lit-labs/ssr to server-side render template partials in a HTML document. We only rendered one template, but you could extrapolate on what you learned to render multiple template partials with Lit.

The usage of a static configuration for each Route, exporting template and fetchModel from each view bundle were opinions. This is just one way of working. The main takeaway is for server-side rendering, you should standardize portions of the system to streamline development and ease with integration. The patterns we defined could be templated for codegen. By exporting template and fetchModel reliably, we can ensure the middleware interprets Declarative Shadow DOM equally reliably.

We didn't cover the entire build system in this workshop. Under the hood, nodemon was watching for changes while esbuild is responsible for building for production. If you build for production, you will find the output is 100% minified and scores 100% for performance in Lighthouse. Someone could streamline development further with Vite, the main benefit one would gain is hot module reloading.

We also didn't cover how to use @lit-labs/ssr with LitElement and that was on purpose. Lit is a wonderful library. Use it at your discretion. I find it much more interesting that Lit, because it is built from browser specifications, can operate with browser spec like Declarative Shadow DOM without coding directly with Lit. I hope you find this interesting as well. There is another aspect to this I would like to highlight.

When I started in web development in the 90s, I appreciated the egalitarian nature of HTML and JavaScript. Anyone could code a website, like anyone could read and write a book. Over time front end web development has gotten way too complicated. Frameworks and libraries that sought to simplify developer experience did so at the cost of user experience, while also making the barrier to entry far greater for anyone wanting to learn how to code a site. I demonstrated how to server-side render a website using only browser standards and leveraging a single function from a library. Anyone can learn how to code Declarative Shadow DOM, just like decades ago anyone could learn how to code HTML. I hope you found it easy to accomplish server-side rendering with Declarative Shadow DOM and can't wait to see what you build.

Comment below with a link so we can see those 100% performance scores in Lighthouse.

If you liked this tutorial, you will find many more like it in my book about Web Components. Details below.

Fullstack Web Components

Are you looking to code Web Components now, but don't know where to get started? I wrote a book titled Fullstack Web Components, a hands-on guide to coding UI libraries and web applications with custom elements. In Fullstack Web Components, you'll...

  • Code several components using autonomous, customized built-in, and form-associated custom elements, Shadow DOM, HTML templates, CSS variables, and Declarative Shadow DOM
  • Develop a micro-library with TypeScript decorators that streamlines UI component development
  • Learn best practices for maintaining a UI library of Web Components with Storybook
  • Code an application using Web Components and TypeScript

Fullstack Web Components is available now at newline.co

Fullstack Web Components Book Cover

...



📌 Server-Side Rendering (SSR) vs. Client-Side Rendering (CSR): The Fascinating World of Page Rendering


📈 63.19 Punkte

📌 Server Side Rendering a Blog with Web Components


📈 51.26 Punkte

📌 Understanding Server-Side Rendering (SSR) vs. Client-Side Rendering (CSR)


📈 49.05 Punkte

📌 Server-Side Rendering v/s Client-Side Rendering


📈 49.05 Punkte

📌 What is The Difference Between Server Side Rendering (SSR) and React Server Components?


📈 41.18 Punkte

📌 Server Components Vs. Server-side Rendering


📈 41.18 Punkte

📌 Rendering Patterns for Web Apps – Server-Side, Client-Side, and SSG Explained


📈 38.83 Punkte

📌 Next.js Tutorial: Building a Complete Web App from Scratch with React, Server-Side Rendering, and SEO-Friendly URLs


📈 30.13 Punkte

📌 CVE-2023-37908 | XWiki Rendering XHTML Rendering cross site scripting (GHSA-663w-2xp3-5739)


📈 28.27 Punkte

📌 Why we Server Side Render Web Components


📈 27.59 Punkte

📌 Lightning Web Components: Custom Nested Components


📈 27.11 Punkte

📌 Avoid Common Pitfalls with Next.js Server-Side Rendering


📈 26.21 Punkte

📌 Server-Side Rendering with AWS Amplify


📈 26.21 Punkte

📌 The Challenges and Pitfalls of Server Side Rendering


📈 26.21 Punkte

📌 Microsoft Windows up to Server 2019 Client Side Rendering Print Provider privileges management


📈 26.21 Punkte

📌 A detailed guide on how to implement Server-side Rendering (SSR) in a NextJs Application


📈 26.21 Punkte

📌 Unlocking the Power of Next.js Pre-Rendering with Server-Side Props


📈 26.21 Punkte

📌 Server Side Rendering in JavaScript – SSR vs CSR Explained


📈 26.21 Punkte

📌 SERVER SIDE RENDERING WITH REACT AND REDUX


📈 26.21 Punkte

📌 Why Next.js is the Best Choice for Server-Side Rendering with React


📈 26.21 Punkte

📌 Server-Side Rendering (SSR) in React


📈 26.21 Punkte

📌 Building Server-Side Rendering (SSR) Applications with Deno and JSX


📈 26.21 Punkte

📌 What is Server Side Rendering (SSR) and Static Site Generation (SSG)?


📈 26.21 Punkte

📌 Angular: mehr Server-Side-Rendering und Performance mit Wiz


📈 26.21 Punkte

📌 Understanding the Differences: Server Side Rendering (SSR) vs Static Site Generation (SSG) in Next.js


📈 26.21 Punkte

📌 Is server side rendering always good?


📈 26.21 Punkte

📌 Implementing Angular Server-Side Rendering (SSR) AKA Angular Universal


📈 26.21 Punkte

📌 Rendering NativeScript Angular Templates and Components into images


📈 25.73 Punkte

📌 React Server-Side Components without Next.js


📈 23.67 Punkte

📌 Medium CVE-2017-18604: Sitebuilder dynamic components project Sitebuilder dynamic components


📈 23.19 Punkte

📌 Typescript for React Components (or How To Write Components in React The Right Way)


📈 23.19 Punkte

📌 React Components 101: Building Reusable Components


📈 23.19 Punkte

📌 Converting React Class Components to Functional Components: A Checklist and Example


📈 23.19 Punkte











matomo