pequeno

0.2.1 • Public • Published

Pequeño

A simpler React static site generator.

Demo

https://priceless-euclid-d30b74.netlify.app/

Why

Jsx emerged as the leading template engine, due to its great developer experience.

Framework like Gatsby, Astro or NextJs are great, but I wanted something lighter, dependency-free, using only vanilla js on the client. It's best suited for small blogs, if you want to build something more complex you'll probably need one of the above tools.

Installation

npm install pequeno --save

Quick start

  1. Create some pages in src/pages, giving them a permalink like this
import React from 'react';

export const permalink = '/index.html';

export default function Index() {
    return <div>Empty page</div>;
}
  1. Run Pequeno
npx pequeno
  1. Open your browser

and visit http://localhost:8080

You should see your basic index page

Cli options

you can run the pequeno command with these options

  • --verbose for verbose output
  • --clean cleans the destination folder
  • --serve fires a server that watches for changes
  • --path builds only the specified path (--page=/news/index.html)
  • --page builds only the specified page (--page=news-item)
  • --data fetches only the specified data file (--data=news)
  • --dataParam pass an optional param to the data function (--dataParam=32)
  • --noAfterBuild prevents the afterBuild function to run (see below)
  • --noProcessHtml prevents the processHtml function to run (see below)
  • --noData skips the data fetch step
  • --noCopy skips the copy defined in the config object
  • --noPublicCopy skips the copy of publicDir
  • --example builds the example site.

Page and path options (together with --data) are useful during development to speed up page refresh or during build if you want to write only a specified page or path.

For example if you want to develop a specific news page maybe fetching just a single item from an api, you can run

npx pequeno --page=news-item --path=/news/news-1-slug/index.html --data=news --dataParam=news-1-slug --noAfterBuild --noProcessHtml --serve

Or if you want to develop a single page that doesn’t need any data/libs/files you can be quicker with

npx pequeno --page=test --noData --noCopy --noPublicCopy --serve

Configuration

Just place a .pequeno.js file in the root of you project to override the default settings

module.exports = {
    // where to place bundled pages
    cacheDir: '.cache',
    // destination folder
    outputDir: '.site',
    // source folder
    srcDir: 'src',
    // where to search for data (relative to srcDir)
    dataDir: 'data',
    // public directory that will be copied to outputDir (relative to srcDir)
    publicDir: 'public',
    // where to look for pages (relative to srcDir)
    pagesDir: 'pages',
    // an object that tells what to copy (key) and where (value)
    // useful to copy external libs to the destination folder
    copy: {
        'node_modules/node_lib/index.js': 'libs/node_lib/index.js',
    },
    // and async function to be run after the build (see below)
    afterBuild: async function () {},
};

NB: All files and folders in the publicDir folder will be copied to the destination folder

Data

You can provide data at build time by creating files into the dataDir folder. Each data file should export a promise.

For example:

data/news.js

module.exports = function () {
    const { config } = pequeno;
    return new Promise((resolve) => {
        fetch('https://my.custom.endpoint')
            .then((response) => response.json())
            .then((data) => {
                // eg: you can use the config object to output a file during data fetch
                fs.outputJsonSync(
                    path.join(
                        process.cwd(),
                        config.outputDir,
                        '_data',
                        'computed-json.json',
                    ),
                    data.map((item) => _.pick(item, ['category', 'title'])),
                );
                resolve(data);
            });
    });
};

Now you have a news collection available in your templates. In Every data promise function you can access the pequeno instance. So, for example, you can get the config object.

Derived collections

You can export different functions from the same data file to create derived collections from the main one.

For example if you want to create a page for each photo in a news item you can export a photos function like this.

module.exports.photos = function (news) {
    return _.flatten(_.map(news, 'photos'));
};

Non default exports receive the main collection as the first argument. Non default exports should be synchronous.

Pagination

You can paginate your data creating lists of content. Just export a paginate object giving the collection name in the data prop. For example you can create pages that lists chunks of 10 news in this way.

export const paginate = {
    data: 'news',
    size: 10,
};

export const permalink = function (data) {
    const { page } = data.pagination;
    if (page === 1) {
        return `/news/index.html`;
    } else {
        return `/news/${page}/index.html`;
    }
};

export default function News({ pagination, route }) {
    const news = pagination.items;
    return (
        <ul>
            {news.map((n, i) => (
                <li key={i}>
                    <Link underline href={`/news/${n.slug}/`}>
                        <h2>{n.title}</h2>
                    </Link>
                </li>
            ))}
        </ul>
    );
}

You will get a pagination object with this data.

{
    page: 1,
    total: 4,
    items: [],
    prev: null,
    next: '/news/2/index.html'
}

Programmatically create pages from data.

Just use a size of 1 in the pagination export and you'll get a page for each news

export const paginate = {
    data: 'news',
    size: 1,
};

export const permalink = function (data) {
    const news = data.pagination.items[0];
    return `/news/${news.slug}/index.html`;
};

export default function News({ pagination, route }) {
    const news = pagination.items[0];
    return <h1>{news.title}</h1>;
}

In this case, the pagination object will contain also the prev and the next item payload

{
    prevItem: { ...props }, // an object containing the item payload
    nextItem: { ...props }
}

Grouping items

You can generate list of grouped content by adding a groupBy prop to the pagination object. The groupBy prop must match an existing prop of your item object.

export const paginate = {
    data: 'news',
    size: 8,
    groupBy: 'category',
};

export const CategoryNewsPageLink = function (page, group) {
    group = group.toLowerCase();
    if (page === 1) {
        return `/news/${group}/index.html`;
    } else {
        return `/news/${group}/${page}/index.html`;
    }
};

export const permalink = function (data) {
    const { page, group } = data.pagination;
    return CategoryNewsPageLink(page, group);
};

The pagination object will contain the group prop. Grouping is limited to string props.

Styling

Pequeno integrates Styled Components for styling. but you can also use plain css if you want. If you are using styled components babel-plugin-styled-components is included.

component usage

import React from 'react';
import styled from 'styled-components';

const StyledButton = styled.button`
    background: ${props => props.primary ? var(--color-primary) : var(--color-secondary)}
`;

export default function MyButton({ primary, ...props }) {
    return <StyledButton primary={primary} {...props} />;
}

See styled components docs for detailed usage.

Dealing with client-side js

You can use a classic approach, or use the built-in Script component to add js in a more "component way" like this

import React from 'react';
import { Script } from 'pequeno';
import client from './index.client.js';

export default function TestButton({ children }) {
    return (
        <>
            <button className="js-test-button">{children}</button>
            <Script>{client}</Script>
        </>
    );
}

index.client.js

testButton.addEventListener('click', function () {
    alert('You clicked the test button');
});

Say you have a component that need some vanilla client-side js logic and maybe an external library, like an accordion thats adds some css and js Just add the <Script> component in your code like this

import React, { Fragment } from 'react';
import { Script } from 'pequeno';
import client from './index.client.js';

export default function Accordion({ items, ...props }) {
    return (
        <>
            <dl {...props}>
                {items.map((item, i) => (
                    <Fragment key={i}>
                        <dt>
                            <button>{item.title}</button>
                        </dt>
                        <dd>{item.description}</dd>
                    </Fragment>
                ))}
            </dl>
            <Script
                libs={[
                    {
                        where: 'head',
                        tag: '<script src="/libs/fisarmonica/fisarmonica.js" />',
                    },
                    {
                        where: 'head',
                        tag: '<link rel="stylesheet" href="/libs/fisarmonica/fisarmonica.css" />',
                    },
                ]}
                vars={[
                    {
                        name: 'accordion_selector',
                        value: `.${props.className} `,
                    },
                ]}
            >
                {client}
            </Script>
        </>
    );
}

The Script components has a libs prop where you can pass any external library you wish to use (proviously copied with the copy property in the config file). You can specify the tag and also where to append it (head/body)

Then in index.client.js

var colorPrimary = getComputedStyle(document.documentElement).getPropertyValue(
    '--color-primary',
);

var fisarmonica = new Fisarmonica({
    selector: accordion_selector,
    theme: {
        fisarmonicaBorderColor: colorPrimary,
        fisarmonicaBorderColorFocus: colorPrimary,
        fisarmonicaInnerBorderColorFocus: colorPrimary,
        fisarmonicaButtonBackgroundFocus: colorPrimary,
        fisarmonicaButtonColor: colorPrimary,
        fisarmonicaButtonColorFocus: 'white',
        fisarmonicaArrowColor: colorPrimary,
        fisarmonicaArrowColorFocus: 'white',
        fisarmonicaPanelBackground: 'white',
    },
});

And finally use it anywhere

<Accordion items={accordionData} className="js-accordion" />

Notice that we used accordion_selector variable, passed by our Script tag withe the vars props and made available to the DOM. At build time, the builder will extract all the libs and code used and place them in the document (code will be inserted before the closing of the body tag).

You can also insert inline scripts with the inline prop like this

<Script inline>{client}</Script>

Html strings

You can use the built-in Html component to output html strings.

import React from 'react';
import { Html } from 'pequeno';
import { myHtmlString } from './example-data';

export default function TestHtml() {
    return <Html>{myHtmlString}</Html>;
}

Svgs

Svg imports are included, so you can do this

import TestSvg from '../public/img/TestSvg.svg';
export default function SvgTest() {
    return <TestSvg width="20em" />;
}

After build

Using the afterBuild config prop you can execute async code after the website has been built. The afterBuild function receives the renderedPages argument, which contains all the pages created with all the data including the markup.

For example you can create a sitemap.

afterBuild: async function (renderedPages) {
    // create a sitemap
    const sitemapLinks = renderedPages.map((page) => ({
        url: page.data.route.href,
        changefreq: 'daily',
        priority: 0.3,
    }));
    const stream = new SitemapStream({
        hostname: 'https://priceless-euclid-d30b74.netlify.app',
    });
    const data = await streamToPromise(
        Readable.from(sitemapLinks).pipe(stream),
    );
    await fs.writeFile(
        path.join(pequeno.config.outputDir, 'sitemap.xml'),
        data.toString(),
        'utf8',
    );

    // move service worker
    await fs.copy(
        path.join(pequeno.baseDir, 'service-worker.js'),
        path.join(pequeno.config.outputDir, 'service-worker.js'),
    );
}

Each rendered page contains the following props

{
    markup: `<html>....</html>`, // the page markup
    styles: `<style>...</style>`, // the extracted page styles (if using styled components)
    data: {
        route: {
            name: 'news-item',
            href: `/news/news-1-slug/index.html`,
            pagination: {}
        }
    },
}

Process generated html

Using the processHtml config prop you can alter the generated html during the build process. The processHtml function receives the cheerio dom instance, the page data and the pequeno settings. For example you can inject a script in the head. You can use data to manipulate html only for some spacific pages. Be sure to return the html() method of the cheerio object. The processHtml function should be synchronous.

processHtml: function ($, data, config) {
    // data: { route: { name, ...}}
    $('head').append(`
    <script>
        if ('serviceWorker' in navigator) {
            window.addEventListener('load', () => {
                navigator.serviceWorker.register('/service-worker.js');
            });
        }
    </script>`);
    return $.html();
},

Example site

See the /example folder for a complete website. signalkuppe.com is built with Pequeño.

Performance

Pequeno uses Esbuild for bundling, so it should be quite fast. However performance optimizations are still missing.

Package Sidebar

Install

npm i pequeno

Weekly Downloads

1

Version

0.2.1

License

ISC

Unpacked Size

297 kB

Total Files

76

Last publish

Collaborators

  • signalkuppe