Pages
For content-driven websites with low interactivity a multi-page approach is best suited.
Abandoning the use of frameworks means writing out those HTML pages from scratch. For this it is important to understand what a good minimal HTML page should look like.
An explanation of each element:
<!doctype html>
- The doctype is required to have the HTML parsed as HTML5 instead of an older version.
<html lang="en">
- The lang attribute is recommended to make sure browsers don't misdetect the language used in the page.
<head><title>
- The title will be used for the browser tab and when bookmarking, making it effectively non-optional.
<head><meta charset="utf-8">
- This is borderline unnecessary, but just to make sure a page is properly interpreted as UTF-8 this line should be included. Obviously the editor used to make the page should equally be set to UTF-8.
<head><meta name="viewport">
- The viewport meta is necessary to have mobile-friendly layout.
<head><link rel="stylesheet" href="index.css">
- By convention the stylesheet is loaded from
<head>
in a blocking way to ensure the page's markup does not have a flash of unstyled content. <body><noscript>
- Because web components don't work without JavaScript it is good practice to include a noscript warning to users that have JavaScript disabled. This warning only needs to be on pages with web components. If you don't want to show anything except the warning, see the template pattern below.
<body><header/main/footer>
- The page's markup should be organized using HTML landmarks. Landmarks when used properly help organize the page into logical blocks and make the page's structure accessible. Because they are standards-based, compatibility with present and future accessibility products is more likely.
<body><script type="module" src="index.js">
- The main JavaScript file is at the end, and will bootstrap the web components as explained on the components page.
Pages that should only show their contents if JavaScript is enabled can use this template pattern:
Semantics matter
The markup in the page should default to using semantic HTML, to improve accessibility and SEO. Web components should only be used in those cases where the level of complexity and interaction exceeds the capabilities of plain HTML markup.
Familiarize yourself with these aspects of semantic HTML:
- Landmarks
- As mentioned above, landmarks are the backbone of a page's structure and deliver good structure and accessibility by default.
- Elements
- Being familiar with HTML's set of built-in elements saves time, both in avoiding the need for custom elements and in easing implementation of the custom elements that are needed. When properly used HTML elements are accessibly by default.
- Forms
- HTML's built-in forms can implement many interactivity use cases when used to their full extent. Be aware of capabilities like rich input types, client-side validation and UI pseudo classes. When a suitable form input type for a use case cannot be found, consider using form-associated custom elements but be aware of the browser support for ElementInternals.
Favicons
There is one thing that you will probably want to add to the HTML that is not standards-based and that is a reference to a favicon:
- To keep it really simple, put a
favicon.ico
in the root path of the site and link to it from your HTML:<link rel="icon" href="favicon.ico">
- Consider SVG favicons, but be aware that Safari does not support them. Embed dark mode in the favicon SVG itself or use a generator like RealFaviconGenerator for more convience.
- Be aware that because favicons are not based on published web standards it is cumbersome to implement the de facto standard fully.
Project
A suggested project layout for a vanilla multi-page website:
- /
- The project root contains the files that will not be published, such as
README.md
,LICENSE
or.gitignore
. - /public
- The public folder is published as-is, without build steps. It is the whole website.
- /public/index.html
- The main landing page of the website, not particularly different from the other pages, except for its path.
- /public/index.[js/css]
-
The main stylesheet and javascript. These contain the shared styles and code for all pages.
index.js
loads and registers the web components used on all pages. By sharing these across multiple HTML pages unnecessary duplication and inconsistencies between pages can be avoided. - /public/pages/[name].html
-
All of the site's other pages, each including the same
index.js
andindex.css
, and ofcourse containing the content directly as markup in the HTML, leveraging the web components. - /public/components/[name]/
-
One folder per web component, containing a
[name].js
and[name].css
file. The.js
file is imported into theindex.js
file to register the web component. The.css
file is imported into the globalindex.css
or in the shadow DOM, as explained on the styling page. - /public/lib/
- For all external libraries used as dependencies. See below for how to add and use these dependencies.
- /public/styles/
- The global styles referenced from
index.css
, as explained on the styling page.
Configuration files for a smoother workflow in programmer's editors also belong in the project's root. Most of the development experience of a framework-based project is possible without a build step through editor extensions. See the Visual Studio Code setup for this site for an example.
Routing
The old-school routing approach of standard HTML pages and <a>
tags to link them together has the advantages of being easily indexed by search engines
and fully supporting browser history and bookmarking functionality out of the box. 😉
Dependencies
At some point you may want to pull in third-party libraries. Without npm and a bundler this is still possible.
Unpkg
To use libraries without a bundler they need to be prebuilt in either ESM or UMD format. These libraries can be obtained from unpkg.com:
- Browse to
unpkg.com/[library]/
(trailing slash matters), for example unpkg.com/microlight/ - Look for and download the library js file, which may be in a subfolder, like
dist
,esm
orumd
- Place the library file in the
lib/
folder
Alternatively, the library may be loaded directly from CDN.
UMD
The UMD module format is an older format for libraries loaded from script tag,
and it is the most widely supported, especially among older libraries.
It can be recognized by having typeof define === 'function' && define.amd
somewhere in the library JS.
To include it in your project:
- Include it in a script tag:
<script src="lib/microlight.js"></script>
- Obtain it off the window:
const { microlight } = window;
ESM
The ESM module format (also known as JavaScript modules) is the format specified by the ECMAScript standard, and newer or well-behaved libraries will typically provide an ESM build.
It can be recognized by the use of the export
keyword.
To include it in your project:
- Load it from CDN:
import('https://unpkg.com/web-vitals@4.2.2/dist/web-vitals.js').then((webVitals) => ...)
- Or load it from a local copy:
import webVitals from 'lib/web-vitals.js'
imports.js
To neatly organize libraries and separate them from the rest of the codebase, they can be loaded and exported from an imports.js
file.
For example, here is a page that uses a UMD build of Day.js and an ESM build of web-vitals:
The text is rendered by the <x-metrics>
component:
In the /lib
folder we find these files:
- web-vitals.js - the ESM build of web-vitals
- dayjs/
- dayjs.min.js - the UMD build of Day.js
- relativeTime.js - the UMD build of this Day.js plugin
- imports.js
Digging deeper into this last file we see how it bundles loading of third-party dependencies:
It imports the ESM library directly, but it pulls the UMD libraries off the Window
object.
These are loaded in the HTML.
Here is the combined example:
Regrettably not all libraries have a UMD or ESM build, but more and more do.
Import Maps
An alternative to the imports.js
approach are import maps.
These define a unique mapping between importable module name and corresponding library file in a special script tag in the HTML head.
This allows a more traditional module-based import syntax in the rest of the codebase.
The previous example adapted to use import maps:
Some things to take into account when using import maps:
-
Import maps can only map to ESM modules, so for UMD libraries a wrapper must be provided,
as with the
module.js
wrapper fordayjs
in this example. -
External import maps of the form
<script type="importmap" src="importmap.json">
are not yet supported in all browsers. This means the import map must be duplicated in every HTML page. -
The import map must be defined before the
index.js
script is loaded, preferably from the<head>
section. -
Import maps can be used to more easily load libraries from a
node_modules
folder or from a CDN. JSPM generator can be used to quickly create an import map for CDN dependencies. Do remain aware however that adding such external dependencies makes your vanilla codebase rely on the continued availability of that service.
Browser support
Vanilla web sites are supported in all modern browsers. But what does that mean?
- Everything on this site works in current versions of Safari, Chrome, Edge and Firefox.
- Everything on this site has 95% support or more on caniuse.com, with the exception of Import Maps (92%), Declarative Shadow DOM (89%), and CSS Nesting (88%), and it won't be long before those catch up.
- That in turn means you can safely rely on HTTP/2, HTML5 semantic elements, Custom Elements, Templates, Shadow DOM, MutationObserver, CustomEvent, FormData, and the Element.closest API.
- It's also safe to use JavaScript Modules, ECMAScript 6 / 2015, ECMAScript 8 / 2017 and ECMAScript 11 / 2020.
- In CSS you can rely on @import, variables, calc(), flexbox, grid, display: contents and so much more.
To keep up with new web standards, keep an eye on these projects:
Deploying
Any provider that can host static websites can be used for deployment.
An example using GitHub Pages:
- Upload the project as a repository on GitHub
- Go to Settings, Pages
- Source: GitHub Actions
- Static Website, Configure
- Scroll down to
path
, and change it to./public
- Commit changes...
- Go to the Actions page for the repository, wait for the site to deploy
Testing
Popular testing frameworks are all designed to run in build pipelines. However, a plain vanilla web site has no build. To test web components an old-fashioned approach can be used: testing in the browser using the Mocha framework.
For example, these are the live unit tests for the <x-tab-panel>
component used to present tabbed source code panels on this site:
And for ultimate coding inception, here is that tabpanel component showing the testing source code:
Some working notes on this approach:
-
The entire code for the unit tests, including the testing libraries, is isolated to a
public/tests/
subfolder. The tests will therefore be available live by adding/tests
to the deployed site's URL. If you don't want to deploy the tests on the live website, exclude the tests folder during the deploy step. - Mocha and Chai are used as test and assertion frameworks, because they work in-browser without a build step.
-
DOM Testing Library is used to more easily query the DOM.
The
imports-test.js
file configures it for vanilla use. -
An important limitation is that DOM Testing Library cannot query inside shadow roots.
To test something inside a shadow root it is necessary to first query for the containing web component,
get a handle to its
shadowRoot
property, and then query inside of that. - Web Components initialize asynchronously, which can make them tricky to test. Use the async methods of DOM Testing Library.
Example
This website is the example. Check out the project on GitHub.