Never Post Memes

    5sg

    0.0.8 • Public • Published

    Stupid Simple Svelte Static Site Generator (5SG)

    Introduction

    5sg stands for stupid simple svelte static site generator. It's a static site generator (SSG) in the making which focuses on ease of development, simplicity of structure, and speed of delivery. It takes in markdown and svelte, and outputs html. I had planned on changing the name, mostly because French google mostly turns up 5th week pregnancy results (5ème semaine de grossesse), but 🤷‍♀️.

    Big ideas

    Simple build process

    1. Install 5sg using npm install -S 5sg or yarn add 5sg
    2. Put your content files (*.md and .svelte) in <PROJECT_ROOT>/src/content/.
    3. Pick your adventure
      • Static build: run 5sg to build to the <PROJECT_ROOT>/public directory
      • Development: run 5sg --serve to build to the <PROJECT_ROOT>/public directory and serve on http://localhost:3221

    Installation using a template

    You can install a 5sg template using degit

    For a basic starter site use the template at https://github.com/cborchert/5sg-basic-template

    npm install -g degit
    degit cborchert/5sg-basic-template my-5sg-site
    cd my-5sg-site
    npm install
    npm run dev
    

    For more complicated a blog site use the blog template at https://github.com/cborchert/5sg-blog-template

    npm install -g degit
    degit cborchert/5sg-blog-template my-5sg-blog
    cd my-5sg-blog
    npm install
    npm run dev
    

    Intuitive, file-based routing

    src/content/foo/bar.(md|svelte) generates public/foo/bar.html

    Small files and partial hydration

    All generated html is feather-weight and the client loads no javascript unless needed.

    All images are processed to use modern formats where possible.

    Customization and automation

    Customize everything from config.js

    • Your sitemap and webmanifest are taken care of for you
    • You can easily add dynamically rendered pages such as a blog feed and category pages
    • You can apply custom layouts to your pages, either defined by the content path or the layout entry in the content's frontmatter

    Dynamic pages

    If you're building a blog, you'll probably want a blogfeed. 5sg provides a way to build dynamic pages using your content.

    Access your content data at every inpoint

    Using the special deriveProps export, every layout and top level .svelte file has access to the meta data of every other file.This means that you can easily create navigation between sibling blog posts, for example.

    More nitty-gritty

    Project structure

    <PROJECT_ROOT>/
    ├─ .5sg/ # generated by .5sg you can ignore
    ├─ public/ # the output of the 5sg build process
    ├─ src/
    │  ├─ content/ # This is where your content lives! src/content/foo/bar.(md|svelte) generates public/foo/bar.html
    │  ├─ static/ # Unprocessed content. All files are copied to public/static/
    │  ├─ <YOUR CUSTOM FILES AND FOLDER>
    ├─ .gitignore
    ├─ config.js # Optional config file
    ├─ package.json
    

    I recommend structuring your <PROJECT_ROOT>/src directory like this, but you do what you want.

    <PROJECT_ROOT>/src/
    ├─ content/ # This is where your content lives! src/content/foo/bar.(md|svelte) generates public/foo/bar.html
    ├─ static/ # Unprocessed content. All files are copied to public/static/
    ├─ components / # your svelte components
    ├─ layouts/ # your page-level layout components
    ├─ dynamicPages/ # your components for dynamically rendered pages
    

    Recommended .gitignore

    node_modules/
    public/
    .5sg/
    

    Recommended package.json scripts

    {
       // ... the rest of your package.json
       "scripts": {
        "build": "5sg",
        "dev": "5sg --serve",
        // ...your test scripts etc. here
      },
    }
    

    Partial Hydration

    All svelte components are rendered to static html, and, by default, that's where the story ends.

    However if you need the component to be hydrated (i.e. interactive), you can use the custom <Hydrate /> component from 5sg. Hydrate accepts two props:

    • component: the component to hydrate
    • and props: the component's props

    Example:

    <script>
      import Hydrate from "5sg/Hydrate";
      import Count from "../components/Count.svelte";
    </script>
    
    <h1>Hello, World!</h1>
    <Count name="non-hydrated, non-interactive counter 😢" />
    <Hydrate component={Count} props={{ name: "hydrated counter 🤯" }} />

    Note that the rendered component will be placed in a <div> which may have layout implications.

    Custom markdown preprocessors

    By default, markdown files are processed using remark and the following plugins:

    remark-highlight.js: to process code fences (you need to add the appropriate global for highlighting to work), remark-gfm: to add github style markdown transformations and remark-gemoji: to transform emojis

    If you want to change this, simply define the remarkPlugins property as an array of plugins in config.js

    // config.js
    import gemoji from 'remark-gemoji';
    import footnotes from 'remark-footnotes';
    import highlight from 'remark-highlight.js';
    import gfm from 'remark-gfm';
    
    export default {
      // ...other config
      remarkPlugins: [highlight, gfm, gemoji, footnotes],
    };

    if you need to pass options to the plugin you can do so by passing an tuple: [plugin, options]:

    import gemoji from 'remark-gemoji';
    import highlight from 'remark-highlight.js';
    import gfm from 'remark-gfm';
    import customPluginWithOptions from './plugin.js';
    
    export default {
      remarkPlugins: [highlight, gfm, gemoji, [customPluginWithOptions, { foo: 'bar' }]],
    };

    Syntax highlighting

    Although we're using remark-highlight.js by default to enable syntax highlighting in code fences, you need to include one of their themes. There's an explorer here, and you can use a cdn to include the styles (see the highlight.js usage page), or download one of the styles from their repo and include it yourself.

    Custom Layouts

    By default, content is thrown into a plain old html wrapper. In order to give it some style, you'll need to be able to assign it a layout. A layout is simply a svelte file which contains a <slot /> that the transformed content is injected into.

    For example, the markdown content

    # Hello [world](http://www.example.com) !

    plus the layout

    <div>
      <nav><a href="/">Home</a></nav>
      <main>
        <slot />
      </main>
    </div>
    
    <svelte:head>
      <!-- import global css -->
      <link rel="stylesheet" href="/static/styles/global.css" />
      <!-- highlight.js theme for highlighting code blocks (for blogs and documentation sites, etc.) -->
      <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/styles/default.min.css">
    </svelte:head>
    
    <style>
      main {
        width: 1024px;
        margin: 20px auto;
      }
    </style>
    

    would result in an html file kindof like this

    <html>
      <head>
        <link rel="stylesheet" href="/static/styles/global.css" />
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/styles/default.min.css" />
        <style>
          main {
            width: 1024px;
            margin: 20px auto;
          }
        </style>
      </head>
      <body>
        <div>
          <nav><a href="/">Home</a></nav>
          <main>
            <h1>Hello <a href="http://www.example.com">world</a></h1>
          </main>
        </div>
      </body>
    </html>

    You can define the layouts in the config.js file

    // config.js
    export default {
      // ...other config
      layouts: { blog: `src/layouts/Blog.svelte`, _: `src/layouts/Page.svelte` },
    };

    a markdown file will use the _ layout by default, and it will use any other layout based on one of two things:

    1. If its directory relative to src/content matches the layout name. For example, by default all files in src/content/blog will use the blog layout.
    2. If the frontmatter property layout matches the layout name.

    Of note:

    • If the frontmatter property is defined, it supercedes the directory-based layout
    • If the frontmatter property layout === false, no layout will be used.
    • The layout name is case insensitive
    • Layouts are not applied to svelte components by default. You can just import the component and use it in your svelte component.

    Markdown Frontmatter in Layouts

    Layouts receive all properties declared in a markdown file's frontmatter as an object prop called metadata.

    Example:

    ---
    title: qui eius qui quisquam!
    date: 2021-01-01T20:52:15.045Z
    tags:
      - perferendis
      - foo
      - bar
    layout: false
    ---
    
    # Hello world
    <script>
      export let metadata = {};
      const { title, date, tags, layout } = metadata;
      // title: string === "qui eius qui quisquam!"
      // date: string === "2021-01-01T20:52:15.045Z"
      // tags: string[] === ["perferendis", "foo", "bar"]
      // layout: boolean === false
      // note, if we had had `layout: blog`, then layout would be a string "blog"
    </script>
    
    <slot />
    

    Layout and svelte file deriveProps

    Top-level svelte files (i.e. .svelte files in the content folder, layout files, and dynamic page files) have access to the meta data of all content nodes in the project. For the moment the way to access this data is a bit convoluted, and it was done this way as a way to get around atrociously large files in the build process. There may be a better way, and this is one of those things that, we might expect to change in a v1 release.

    Here's how it works:

    You can export a function called deriveProps from the context="module" script of your page/layout file which takes in all the content node data, and transforms it into props to be injected into the component.

    Here's a basic reference of deriveProps:

    /**
     * @typedef {Object} NodeMetaEntry
     * @property {Object} metadata the exported frontmatter
     * @property {string} publicPath the publish path with extension
     */
    
    /**
     * @typedef {Object} ContentNode a single block of content in the nodeMap
     * @property {string} facadeModuleId the path of the input file
     * @property {string} fileName the path relative to .5sg/build/bundled for the component
     * @property {string} name the publish path / slug
     * @property {string} publicPath the publish path with extension
     * @property {boolean} isDynamic if true, the ContentNode was created dynamically rather than from a file
     */
    
    /**
     *
     * @param {Object} context the context of the current content node
     * @param {Object<string, NodeMetaEntry>} context.nodeMeta all the content node information, where the key is the path of the content node and the value is the content node meta information
     * @param {ContentNode} context.nodeData the content node information of the current node
     * @returns {Object} the props to be injected into the component
     */
    function deriveProps(context) {
      const { nodeMeta = {}, nodeData = {} } = context;
    
      return {
        //... the injected derived props
      };
    }

    A basic example

    <script context="module">
      export const deriveProps = ({ nodeMeta = {} }) => {
        const numberOfContentNodes = Object.keys(nodeMeta).length;
        return {
          numberOfContentNodes,
        }
      }
    </script>
    
    <script>
      // injected from deriveProps
      export let numberOfContentNodes;
    </script>
    
    <h1>There are {numberOfContentNodes} in this project</h1>

    A more complicated example

    <script context="module">
      // layouts/Blog.svelte
    
      export const deriveProps = ({ nodeMeta = {}, nodeData = { name: "" } }) => {
        // create sibling pages
    
        // get an array containing only blog nodes, sorted by date
        const blogPages = Object.values(nodeMeta)
          // get all the content nodes in the src/content/blog/ directory
          .filter((node) => node.publicPath.startsWith("blog/"))
          // sort by date
          .sort((a, b) => {
            const dateA = (a.metadata && a.metadata.date) || "";
            const dateB = (b.metadata && b.metadata.date) || "";
            // newest first
            return dateA > dateB ? -1 : 1;
          });
    
        // get the current node's position in the array
        const currentPath = `${nodeData.publicPath}`;
        const currentIndex = blogPages.findIndex(
          (node) => node.publicPath === currentPath;
        );
    
        // get the siblings
    
        // the previous or false
        const prevPost = currentIndex > 0 && blogPages[currentIndex - 1];
    
        // the next or false
        const nextPost =
          currentIndex < blogPages.length - 1 && blogPages[currentIndex + 1];
    
        // these will be injected into the component
        return {
          nextPost,
          prevPost,
        };
      };
    </script>
    
    <script>
      // these props are injected thanks to deriveProps above
      export let nextPost;
      export let prevPost;
    </script>
    
    <article>
      <slot />
      <footer>
        <nav>
          <ul class="sibling-navigation">
            <li>
              {#if prevPost}
                <a href="/{prevPost.publicPath}">← {prevPost.metadata.title}</a>
              {/if}
            </li>
            <li>
              {#if nextPost}
                <a href="/{nextPost.publicPath}">{nextPost.metadata.title} →</a>
              {/if}
            </li>
          </ul>
        </nav>
      </footer>
    </article>

    Site Meta and SEO

    The site meta data from config.js is injected into each top-level svelte component (layout, content page, and dynamically rendered page) as the prop siteMeta.

    For example, if in config.js you have

    export default {
      siteMeta: {
        name: "My 5sg site!",
      }
    }
    

    then in the template Page.svelte or in the content file src/content/index.svelte you could have

    <script>
      export let siteMeta = {};
      const { name } = siteMeta;
    </script>
    
    <h1>Welcome to {name}</h1>

    Additionally, the following siteMeta values are used to create a site.webmanifest file:

    name,
    short_name,
    description,
    icons,
    theme_color,
    background_color,
    display,
    

    see the web.dev guide on manifests for more information.

    Dynamically built pages using config.getDynamicNodes

    In addition to pages rendered based on existing .svelte or .md files, you can create pages dynamically using the getDynamicNodes property of the config object exported by config.js.

    getDynamicNodes is a function which receives an array of all non-dynamic node metaData and which must return an array of dynamic page nodes to build.

    /**
     * @typedef {Object} NodeMetaEntry
     * @property {Object=} metadata the extracted metadata from the frontmatter (md) or the named export `metadata` from the svelte context="module" script tag
     * @property {string} publicPath the final html path
     */
    
    /**
     * @typedef {Object} RenderablePage
     * @property {Object} props the props to render the component with
     * @property {string} slug the identifier of the page to be rendered (use .dynamic as the extension)
     * @property {string} component the path to the rendering component from the project root
     */
    
    /**
     * Given the nodeMeta, returns the information necessary to render some dynamic pages
     * @param {Array<NodeMetaEntry>} nodes
     * @returns {Array<RenderablePage>}
     */
    const getDynamicNodes = (nodes = []) => [];

    We could create a simple page like this

    //config.js
    
    export default {
      getDynamicNodes: () => [
        // will create a page at path/to/customPage.html using the CustomPage svelte file injected with the props {foo: "bar" }
        {
          props: { foo: 'bar' },
          component: 'src/pages/CustomPage.svelte',
          slug: 'path/to/customPage.dynamic',
        },
      ],
    };

    This could be useful, for example, for creating a blogfeed

    //config.js
    
    export default {
      getDynamicNodes: (nodes = []) => [
        {
          props: { blogPosts: nodes.filter(({ publicPath }) => publicPath.startsWith('/blog')) },
          component: 'src/pages/BlogFeed.svelte',
          slug: 'blog/index.dynamic',
        },
      ],
    };

    While the example above is possible, it doesn't hold any real advantage over simply using deriveProps.

    What would be more useful, for example, is using getDynamicNodes to create a paginated blog feed, where each page contains 10 posts. Here's a somewhat naïve implemenation:

    //config.js
    
    export default {
      getDynamicNodes: (nodes = []) => {
        const pages = [];
        const blogPosts = nodes.filter(({ publicPath }) => publicPath.startsWith('/blog'));
    
        let totalBlogPages = 1;
        let posts = [];
    
        blogPosts.forEach((post, i) => {
          posts.push(post);
          // every 10 posts, create a new page
          // also create a new page if we're at the end of the array
          if (posts.length === 10 || i === blogPosts.length - 1) {
            pages.push({
              props: { blogPosts: [...posts], currentPage: totalBlogPages, totalNumberOfPosts: blogPosts.length },
              component: 'src/pages/BlogFeed.svelte',
              slug: `blog/${totalBlogPages}.dynamic`,
            });
            // if we're not on the last post, set up the next batch
            if (i < blogPosts.length - 1) {
              posts = [];
              totalBlogPages++;
            }
          }
        });
    
        // additional props to make pagination easier
        pages.forEach((page, i) => {
          page.props.totalBlogPages = totalBlogPages;
          page.props.nextBlogPageSlug = i === totalBlogPages ? undefined : pages[i + 1].props.slug;
          page.props.prevBlogPageSlug = i > 1 ? pages[i - 1].props.slug : undefined;
        });
    
        // return the pages to be created
        return pages;
      },
    };

    In order to help with what we think will be relatively recurrent operations when creating dynamic pages, we've included some helper functions which can be imported from 5sg/helpers

    getDynamicSlugFromName

    Doc:

    /**
     * Formats a name to a dynamic slug which can be universally recognized
     * @param {string} name the page name
     * @returns {string} the dynamic slug
     */

    Example:

    import { getDynamicSlugFromName } from '5sg/helpers';
    
    const slug = getDynamicSlugFromName('this/is/my/name');
    // slug === 'this/is/my/name.dynamic';

    paginateNodeCollection

    Doc:

    /**
     * Given an array of nodes, returns an array paginated nodes to be rendered
     * @param {Array<NodeMetaEntry>} nodes the collection of nodes
     * @param {object} config the pagination config
     * @param {number=} config.perPage the number of nodes to put on a single page 10
     * @param {(i:number)=>string=} config.slugify a function to transform the page number into the slug/path/unique key of the page i => i
     * @param {string=} config.component the component to render each page
     * @returns {Array<RenderablePage>} the paginated node collection
     */

    Example:

    import { paginateNodeCollection } from '5sg/helpers';
    
    const pages = paginateNodeCollection(
      [
        { metadata: { a: 1 }, publicPath: 'test1.html' },
        { metadata: { a: 2 }, publicPath: 'test2.html' },
        { metadata: { a: 3 }, publicPath: 'test3.html' },
        { metadata: { a: 4 }, publicPath: 'test4.html' },
        { metadata: { a: 5 }, publicPath: 'test5.html' },
      ],
      {
        perPage: 2,
        slugify: (i) => `test/page-${i + 1}.dynamic`,
        component: 'path/to/MyComponent.svelte',
      },
    );
    
    // Result
    const result = [
      {
        props: {
          nodes: [
            { metadata: { a: 1 }, publicPath: 'test1.html' },
            { metadata: { a: 2 }, publicPath: 'test2.html' },
          ],
          pageNumber: 0,
          numPages: 3,
          pageSlugs: ['test/page-1.dynamic', 'test/page-2.dynamic', 'test/page-3.dynamic'],
        },
        slug: 'test/page-1.dynamic',
        component: 'path/to/MyComponent.svelte',
      },
      {
        props: {
          nodes: [
            { metadata: { a: 3 }, publicPath: 'test3.html' },
            { metadata: { a: 4 }, publicPath: 'test4.html' },
          ],
          pageNumber: 1,
          numPages: 3,
          pageSlugs: ['test/page-1.dynamic', 'test/page-2.dynamic', 'test/page-3.dynamic'],
        },
        slug: 'test/page-2.dynamic',
        component: 'path/to/MyComponent.svelte',
      },
      {
        props: {
          nodes: [{ metadata: { a: 5 }, publicPath: 'test5.html' }],
          pageNumber: 2,
          numPages: 3,
          pageSlugs: ['test/page-1.dynamic', 'test/page-2.dynamic', 'test/page-3.dynamic'],
        },
        slug: 'test/page-3.dynamic',
        component: 'path/to/MyComponent.svelte',
      },
    ];

    sortByNodeDate

    Docs:

    /**
     * a sort function to sort by date
     * @param {NodeMetaEntry} a
     * @param {NodeMetaEntry} b
     * @returns {-1|1} the sort order
     */

    Example:

    const nodes = [
      { metadata: { date: '2021-01-03', a: 1 }, publicPath: 'test.html' },
      { metadata: { date: '2021-03-03', a: 2 }, publicPath: 'test2.html' },
      { metadata: { date: '2020-05-30', a: 2 }, publicPath: 'test3.html' },
    ].sort(sortByNodeDate);
    
    // Result
    const result = [
      { metadata: { date: '2021-03-03', a: 2 }, publicPath: 'test2.html' },
      { metadata: { date: '2021-01-03', a: 1 }, publicPath: 'test.html' },
      { metadata: { date: '2020-05-30', a: 2 }, publicPath: 'test3.html' },
    ];

    filterByNodePath

    Docs:

    /**
     * Creates a function to filter the nodes by their public path
     * @param {string} dir the path to filter by
     * @returns {(NodeMetaEntry)=>boolean}
     */

    Example:

    const nodes = [
      { metadata: { a: 1 }, publicPath: 'blog/test.html' },
      { metadata: { a: 2 }, publicPath: 'other/test2.html' },
      { metadata: { a: 3 }, publicPath: 'blog/test3.html' },
    ].filter(filterByNodePath('blog/'));
    
    // Result
    const result = [
      { metadata: { a: 1 }, publicPath: 'blog/test.html' },
      { metadata: { a: 3 }, publicPath: 'blog/test3.html' },
    ];

    filterByNodeFrontmatter

    Docs:

    /**
     * Creates a function to filter the nodes by their frontmatter
     * Returns true if the given key equals the given value OR if the given key contains the given value (if an array)
     * @param {string} key the frontmatter entry key
     * @param {any} val the frontmatter entry value to test against
     * @returns {(NodeMetaEntry)=>boolean}
     */

    Example:

    // get all nodes with metadata.tags including 'bacon
    
    const nodes = [
      { metadata: { tags: ['bacon', 'eggs', 'cheese'] }, publicPath: 'blog/test.html' },
      { metadata: { tags: ['cheese'] }, publicPath: 'other/test2.html' },
      { metadata: { tags: ['eggs', 'cheese'] }, publicPath: 'blog/test3.html' },
      { metadata: { tags: 'bacon' }, publicPath: 'blog/test4.html' },
    ].filter(filterByNodeFrontmatter('tags', 'bacon'));
    
    // Result
    const result = [
      { metadata: { tags: ['bacon', 'eggs', 'cheese'] }, publicPath: 'blog/test.html' },
      { metadata: { tags: 'bacon' }, publicPath: 'blog/test4.html' },
    ];

    getFrontmatterTerms

    Docs:

    /**
     * Gathers all the existing values of a given frontmatter entry on a node collection
     * @param {Array<NodeMetaEntry>} nodes the collection of nodes
     * @param {string} key the frontmatter entry key to collect the values of
     * @param {(any)=>any} transform the function to apply to each term (for example a=>a.toLowerCase())
     * @returns {Array}
     */

    Example:

    const nodes = [
      { metadata: { foo: ['A', 'b', 'c'] } },
      { metadata: { foo: 'd' } },
      { metadata: { foo: ['e', 'C'] } },
      { metadata: { bar: ['lol'] } },
    ];
    const terms = getFrontmatterTerms(nodes, 'foo', (a) => a.toLowerCase());
    
    // result
    const result = ['a', 'b', 'c', 'd', 'e'];

    groupByFrontmatterTerms

    Docs:

    /**
     * Groups a node collection by the values in a given frontmatter entry
     * @param {Array<NodeMetaEntry>} nodes the collection of nodes
     * @param {string} key the frontmatter entry key to collect the values of
     * @param {(any)=>any} transform the function to apply to each term (for example a=>a.toLowerCase())
     * @returns {Object<string, Array<NodeMetaEntry>>} the grouped nodes
     */

    Example:

    const node1 = { metadata: { foo: ['A', 'b', 'c'] } };
    const node2 = { metadata: { foo: 'd' } };
    const node3 = { metadata: { foo: ['e', 'C'] } };
    const node4 = { metadata: { bar: ['lol'] } };
    const nodes = [node1, node2, node3, node4];
    
    const groupedNodes = groupByFrontmatterTerms(nodes, 'foo', (a) => a.toLowerCase());
    
    // result
    const result = {
      a: [node1],
      b: [node1],
      c: [node1, node3],
      d: [node2],
      e: [node3],
    };

    For an example of how all of this can be used together take a look at the getDynamicNodes in the blog template: https://github.com/cborchert/5sg-blog-template/blob/main/config.js

    Image processing

    All .jpg image files which are not in the static folder will be transformed into images which are at most 800w by 400h. We add .avif and .webp file versions, and then we transform all image tags into picture tags with sources.

    This will likely be refined before v1, and it will be customizable.

    The static folder

    The src/static folder is copied directly to public/static without any transformations.

    Questions and answers

    What's the deal with the tsconfig file ?

    This project doesn't use Typescript, yet, mostly because I wanted to avoid a build step. But I nonetheless wanted to make sure that I had a way to implement type-safety. I'm using a weird mash up of js-doc style type declarations along with a ts-config file so that my text editor and linter can catch type errors. It's hacky, but what about this project ISN'T ?

    Inspirations

    5sg was inspired by Gatsby, ElderJS, 11ty, Grav, and MDSvex. I did extensive research on partial hydration after the version 0 was finished, and would like to thank the developers of ElderJS and 7ty for their implementations which made the most sense to me.

    Future plans

    Check the project v1 release candidate. Once I have a v1, I truly doubt that I'll do much more work on this other than bugfixes. Hopefully sveltekit gets to a point (and it seems to be rapidly becoming the case), where this project will become obsolete.

    Also, the documentation needs A LOT of work. Sorry for anyone who got this far and has no idea what's going on. No promises, but if there's enough demand, I will probably go through and make a few tutorials and/or clean up the documentation

    How fast is it?

    In my test project which contains 100 images, 994 static pages / posts (md/svelte), and dynamic blogfeed, category, and tags pages resulting in a total of 1124 page total built, the first build takes 30 seconds on my macbook pro, and 3.5 additional seconds when a file is modified.

    The experience is pretty subjective, but it seems more or less consistent with what I've seen elsewhere: it's quick.

    First build

    building
    bundling: 10.612s
    pruning: 0.007ms
    nodeMap: 0.948ms
    import: 901.282ms
    dynamic: 234.995ms
    render: 1.113s
    hydrationBundle: 131.178ms
    publish: 773.428ms
    transform: 16.434s
    static: 11.309ms
    sitemap: 1.579ms
    manifest: 0.201ms
    build: 30.216s
    

    After a modification

    building
    bundling: 2.555s
    pruning: 28.73ms
    nodeMap: 3.191ms
    import: 255.307ms
    dynamic: 84.215ms
    render: 180.369ms
    hydrationBundle: 61.854ms
    publish: 246.609ms
    transform: 28.427ms
    static: 17.949ms
    sitemap: 1.734ms
    manifest: 0.211ms
    build: 3.471s
    

    Keywords

    none

    Install

    npm i 5sg

    DownloadsWeekly Downloads

    4

    Version

    0.0.8

    License

    MIT

    Unpacked Size

    91.4 kB

    Total Files

    34

    Last publish

    Collaborators

    • cmborchert