Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/signalkuppe/pequeno

A simpler react static site generator
https://github.com/signalkuppe/pequeno

react ssg static-site-generator styled-components

Last synced: 2 months ago
JSON representation

A simpler react static site generator

Awesome Lists containing this project

README

        

# Pequeño

> A simpler [React](https://reactjs.org/) static site generator.

## Demo

[https://priceless-euclid-d30b74.netlify.app/](https://priceless-euclid-d30b74.netlify.app/)

## Why

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

Framework like [Gatsby](https://www.gatsbyjs.com/), [Astro](https://astro.build/) or [NextJs](https://nextjs.org/) 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

```shell
npm install pequeno --save
```

## Quick start

1. Create some pages in src/pages, giving them a permalink like this

```js
import React from 'react';

export const permalink = '/index.html';

export default function Index() {
return

Empty page
;
}
```

2. Run Pequeno

```shell
npx pequeno
```

3. 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

```js
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**

```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.

```js
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.

```js
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 (


    {news.map((n, i) => (


  • {n.title}




  • ))}

);
}
```

You will get a pagination object with this data.

```js
{
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

```js
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

{news.title}

;
}
```

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

```js
{
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.

```js
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](https://styled-components.com/) for styling. but you can also use plain css if you want. If you are using styled components [babel-plugin-styled-components](https://styled-components.com/docs/tooling#babel-plugin) is included.

**component usage**

```js
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 ;
}
```

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

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

export default function TestButton({ children }) {
return (
<>
{children}
{client}
>
);
}
```

**index.client.js**

```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](https://github.com/signalkuppe/fisarmonica) thats adds some css and js
Just add the `` component in your code like this

```js
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}

>
);
}
```

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**

```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

```js

```

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

```js
{client}
```

## Html strings

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

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

export default function TestHtml() {
return {myHtmlString};
}
```

## Svgs

Svg imports are included, so you can do this

```js
import TestSvg from '../public/img/TestSvg.svg';
export default function SvgTest() {
return ;
}
```

## 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.

```js
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

```js
{
markup: `....`, // the page markup
styles: `...`, // 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](https://github.com/cheeriojs/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.

```js
processHtml: function ($, data, config) {
// data: { route: { name, ...}}
$('head').append(`

if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js');
});
}
`);
return $.html();
},
```

## Example site

See the `/example` folder for a complete website.
[signalkuppe.com](https://www.signalkuppe.com/) is built with Pequeño.

## Performance

Pequeno uses [Esbuild](https://esbuild.github.io/) for bundling, so it should be quite fast.
However performance optimizations are still missing.