Skip to content

Instantly share code, notes, and snippets.

@pngwn

pngwn/ssg.md

Last active May 12, 2021
Embed
What would you like to do?
A Simple Svelte SSG.

The Simplest Svelte Static Site Generator

Assuming you don't want to statically export a Sapper app, most of the parts to build a simple SSG for Svelte already exist. The only thing that is missing is the tooling ('only').

However, you don't need a lot to get things going: just a couple of rollup builds and a config file will get you most of the way there. Just some glue.

What follows is a bunch of rambling, half thought out thoughts on how I would probably go about this. Most of the stuff discussed here is stuff I've actually done or half done or am in the process of doing with varying degrees of success. It is something I'll be spending more time on in the future. There are other things I have done, want to do, or think would be a good idea that are not listed here as they don't fall into the scope of a simple SSG.

Dislaimer: This is how I would build an SSG, this isn't the only way, but I like this approach as there are a bunch of compile-time optimisations you can perform. I am biased towards doing as much as possible at build time, rather than runtime (it is a disease, I cannot help it), especially for smaller or content-based sites with content that changes infrequently.

Config

You will probably need a config file; you could probably also use a declarative (XML-based) router, or even the file-system (like Sapper, Next). Just some way to work out which components belong to which routes and a way to read them in.

This could be achieved in a variety of ways, it just depends on your needs and preferences.

Components

Svelte already provides the two necessary compilation outputs: SSR and DOM. The DOM components are your regular Svelte components to add interactivity. SSR components are plain functions that take in some props and spit out an object of HTML, head and css — everything you need to create a web page.

The beauty of the SSR components is that they are just functions that return strings. There is no component tree to wrangle; no server is required. It is straightforward and very fast.

When you compile, you need two builds: an SSR build and a DOM build. The SSR build is needed to create each HTML file, the DOM output is inserted into the HTML files that you build. The DOM build should be set to hydratable: true so that the static HTML can be hydrated correctly. You will probably want to emitCss as well to handle the component styles, I'll come to that later.

The way you get the correct files to build will depend on how the config is set up, the way I've done it in the past is with a plain js object although there are other ways to achieve the same:

const routes = {
  '/': './Home.svelte',
  '/about': './About.svelte
}

You will want to pull this map in, compiling all of the route files (as chunks) and keeping track of what route they belong to. A simple one-off rollup plugin will cover most of this. Watchers are a bit out of scope here but it is simple enough: you'll want to trigger rebuilds when either these route files or the route config change. Although something being simple and getting it to work consistently are different things entirely.

Content

If this site has content, you will need some way of getting this content and building your site from it. This is the kind of thing that could live on its own in md files (mdsvex if you want superpowers) which could later be parsed and passed as a string, to be rendered with Svelte's {@html x} syntax or preprocessed into an actual Svelte component which can be rendered as it is.

CSS (optional-ish)

You need to write the CSS to file. Creating single CSS files from multiple components isn't as simple as it first seems because you don't want to duplicate your CSS. Additionally, some components will be required in multiple routes, meaning those CSS files need to be written out separately to save on network requests and unnecessary downloads for the user.

Providing your emitCss in rollup-plugin-svelte and handle the CSS files that come from svelte components, Rollup does provide all of the information needed for this. However, you will need to look through the bundle output (the map of chunks, not the generated code) to work out which files belong to which components and then look back at which components belong to which routes to do this properly. Webpack might be able to deal with this more easily; Rollup doesn't care much for CSS.

You'll need to keep a reference of which CSS files belong to which routes for the routing step.

HTML

Building the static HTML is relatively simple: you can require the SSR js file, pass it some props and write the returned content (html, style, css) to a file. You will want to put the DOM js file that corresponds to that route in this HTML file too. Call new Component() with the hydrate option set to true and you should be good. Write these file to the correct folder based on their route.

If you are going down the preprocessing route with your content (essential if using mdsvex), then you will need to work out where these files need to go in the site hierarchy (route them, maybe in a config file), and use those locations just as with the more 'static' routes. These files would be statically generated just like the static route files (at this point they basically are static route files) and then written to disk in the correct location.

If you aren't preprocessing and just want to pass in strings of HTML to be rendered by Svelte's {@html x} syntax, then you can use a layout component exporting an html prop. This function would be called once for each parsed markdown file passing in the props each time. You still need to write this file to the correct location as above. This option is probably going to be faster since there will be considerably less compilation.

A hybrid approach is something I have been playing with recently: not every blog post will be interactive and require mdsvex but it is nice to have it when you need it. Using the same layout file but only preprocessing and compiling as a Svelte component when necessary could save a huge amount of time building the site. Switching between slots and props when needed seems possible, but the ergonomics aren't ideal.

Routing

You need a router. I think routers that work at compile time to generate the necessary code are clever and fun. You can build the configuration dynamically from both your map of routes, JS chunks and CSS files. This way you can do something like:

function handleCss(css) {
  return new Promise((res, rej) => {
    const link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = '/' + css;

    link.onload = () => res();
    link.onerror = rej;

    document.head.appendChild(link);
  });
}

await Promise.all([
  import(my_js_chunk),
  handle_css('my_css_file.css')
]);

// mount it or whatever

Since we are waiting for the CSS before rendering the next route, we eliminate the dreaded flash of unstyled content.

There are other approaches to routing as well, I just like this one.

Other stuff

The list could go on, this is the gist of what I think a half decent SSG might look like but this only covers the basics. In addition to the essential router, there is an endless array of runtime stuff you could add to deliver a better user-experience. Lazy-loaded images and route prefetching (probably both with intersection observers) are just two popular examples, but there are others.

In the compiler world, automatic image optimisation strikes me as some obvious low-hanging fruit, but there is pretty much nothing you can't do. Depends on the app but if you can do it at compile time then your users will thank you. There is also an entire category of tooling aimed at DX that could be bolted on as well.

tl;dr

  • Route config:
    • Either via a config file or some other means, for example, this could be dictated by the file-system (like next/sapper).
  • Content:
    • a directory of markdown files at its simplest, multiple directories would probably require additional information in the build configuration (config file again?)
  • The build - Part 1:
    • Input determined by the route config (however that is expressed)
    • Generate DOM components with hydratable: true,
    • Generate SSR components
    • Content needs to be parsed if it is vanilla md, or given the same treatment as static route files if you are using mdsvex (DOM + SSR components generated).
    • CSS needs to be handled as part of the build if this is desired. Static files for runtime route changes, inlined for SSR builds (critical styles).
    • A map of routes to css and js files needs to be kept for later in the build process.
  • The build - Part 2:
    • Using the route map, create the HTML from the SSR components
    • Inject the DOM component output into the generated HTML file (as linked scripts).
    • Use the route map to generate the correct router configuration with both the JS chunks and the CSS chunk. This could be pure runtime if you didn't do anything special with the CSS.
    • Write everything to file using the route map as your guide.
  • The Runtime:
    • At the very least, you need a router to load in all that lovely JS and CSS.
@sw-yx

This comment has been minimized.

Copy link

@sw-yx sw-yx commented Sep 13, 2019

notes from pngwn today: sapper export is way too slow.

  • has to build, start up puppeteer, crawl the DOM etc.
  • Not exactly a fast process, doesn't really take much advantage of svelte's super fast ssr.
  • There is also pretty much zero opportunity to fire up multiple instances and do stuff in parallel either because its just using the DOM. Would probably be slow anyway because puppeteer.
@nickreese

This comment has been minimized.

Copy link

@nickreese nickreese commented Aug 24, 2020

Dropping this link here for those looking for another solution other than Sapper.

https://github.com/Elderjs/elderjs

If you end up on this gist Elder.js may be a fit for your needs if you don’t want to roll your own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment