A simpler react static site generator
> A simpler React static site generator.
https://priceless-euclid-d30b74.netlify.app/
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.
``shell`
npm install pequeno --save
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
2. Run Pequeno
`shell
npx pequeno
`3. Open your browser
and visit
http://localhost:8080You 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 --serveOr 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 --serveConfiguration
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.$3
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) => (
/news/${n.slug}/}>
{n.title}
))}
);
}
`You will get a pagination object with this data.
`js
{
page: 1,
total: 4,
items: [],
prev: null,
next: '/news/2/index.html'
}
`$3
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 }
}
`$3
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 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
`js
import React from 'react';
import styled from 'styled-components';const StyledButton = styled.button
;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 (
<>
>
);
}
`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 thats adds some css and js
Just add the
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
`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 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();
return $.html();
},
`Example site
See the
/example` folder for a complete website.Pequeno uses Esbuild for bundling, so it should be quite fast.
However performance optimizations are still missing.