{"id":24083285,"url":"https://github.com/mitranim/papyre","last_synced_at":"2026-05-02T17:33:44.396Z","repository":{"id":57318760,"uuid":"122313396","full_name":"mitranim/papyre","owner":"mitranim","description":"Build tool for static websites. Bring your own rendering engine. Works well with React and Netlify CMS","archived":false,"fork":false,"pushed_at":"2018-04-09T19:43:37.000Z","size":88,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-02-24T06:44:39.465Z","etag":null,"topics":["build-tool","netlify-cms","react","static-site-generator","webpack"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mitranim.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-02-21T09:03:32.000Z","updated_at":"2023-08-01T09:10:20.000Z","dependencies_parsed_at":"2022-08-25T20:22:28.503Z","dependency_job_id":null,"html_url":"https://github.com/mitranim/papyre","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fpapyre","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fpapyre/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fpapyre/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mitranim%2Fpapyre/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mitranim","download_url":"https://codeload.github.com/mitranim/papyre/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":240959115,"owners_count":19884911,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["build-tool","netlify-cms","react","static-site-generator","webpack"],"created_at":"2025-01-09T23:56:17.403Z","updated_at":"2025-10-06T15:12:04.952Z","avatar_url":"https://github.com/mitranim.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Overview\n\nPapyre is a build tool for static sites. It handles watching, rebuilding, aggregating templates, parsing front matter; everything except actual rendering. Bring your own rendering engine (but it must be JS).\n\nPapyre is JS-centric. It requires a JS entry file that exports rendering functions, and uses those to render templates. It also uses Webpack to watch the dependency graph and transpile JS. You can add React with a few lines of code, and reuse code between static layouts, client-side bundle, Netlify CMS previews, or whatever. Check the `examples` directory.\n\nWorks well with React and Netlify CMS.\n\nNew and immature. Feedback and suggestions are welcome.\n\n## TOC\n\n* [Overview](#overview)\n* [Why](#why)\n* [Usage](#usage)\n* [API](#api)\n  * [Front Matter, Props, Render Functions](#front-matter-props-render-functions)\n  * [`build`](#buildwebpackconfig-ondone)\n  * [`buildP`](#buildpwebpackconfig)\n  * [`watch`](#watchwebpackconfig-ondone)\n  * [`writeEntries`](#writeentriesdir-entries)\n* [Misc](#misc)\n\n## Why\n\nStatic site generators tend to combine build tooling and an opinionated rendering engine. Also, React-based generators, like Gatsby, tend to have WAY too many concepts and API surface. Papyre is the missing link: a simple build tool that lets you bring your own rendering tool. You can add React, or whatever else, with just a few lines.\n\n## Usage\n\nInstall from NPM:\n\n```sh\nnpm i papyre\n```\n\nThis example uses React and involves three files: a build script, a markdown/HTML template, and a JS publics file with rendering functions.\n\n```\n╠═ build.js\n╚═ src\n   ╚═ templates\n      ╠═ index.md\n      ╚═ index.js\n```\n\nHere, `build.js` will be a standalone build script. Don't balk at the glue code; Papyre is not for useless 1-liner demos.\n\n```js\n'use strict'\n\nconst pt = require('path')\nconst papyre = require('papyre')\n\nconst webpackConfig = {\n  entry: pt.resolve('src/templates/index.js'),\n  module: {rules: [{\n    test: /\\.jsx?$/,\n    include: pt.resolve('src/templates'),\n    use: {loader: 'babel-loader'},\n  }]},\n}\n\nconst [_exec, _file, cmd] = process.argv\n\nif (cmd === 'build') {\n  papyre.build(webpackConfig, (err, result) =\u003e {\n    if (err) {\n      console.error(err)\n      process.exit(1)\n    }\n    else {\n      console.info(result.timing)\n      papyre.writeEntries('public', renameEntries(result.entries))\n    }\n  })\n}\nelse if (cmd === 'watch') {\n  papyre.watch(webpackConfig, (err, result) =\u003e {\n    if (err) {\n      console.error(err)\n    }\n    else {\n      console.info(result.timing)\n      papyre.writeEntries('public', renameEntries(result.entries))\n    }\n  })\n}\nelse {\n  throw Error(`Unrecognized or missing command: ${cmd}`)\n}\n\nfunction renameEntries(entries) {\n  for (const entry of entries) {\n    entry.path = entry.path.replace(/\\.md$/, '.html')\n  }\n  return entries\n}\n```\n\n`index.md` will be a template with a front matter. The latter also specifies which rendering function to use.\n\n```\n---\npapyre: {fn: html, layout: Index}\n---\n\n# Home\n\n**Hello world!**\n```\n\n`index.js` must export the rendering function `html` specified in the template. A rendering function receives a template with metadata and returns a string or a promise of a string. That's it. It could be making network calls on a meson uplink to the dark side of the Moon. Or it could use React:\n\n```js\nimport {createElement} from 'react'\nimport {renderToStaticMarkup} from 'react-dom/server'\n\nexport function html(props) {\n  const {layout} = Object(props.entry.papyre)\n  const Layout = exports[layout]\n  if (typeof Layout !== 'function') {\n    throw Error(`Expected to find layout function ${layout}, got ${Layout}`)\n  }\n  return `\u003c!doctype html\u003e${renderToStaticMarkup(\u003cLayout {...props} /\u003e)}`\n}\n\nexport function Index({entry, entries: __, tree: ___}) {\n  return (\n    \u003chtml\u003e\n      \u003chead\u003e\n        \u003ctitle\u003e{entry.title}\u003c/title\u003e\n      \u003c/head\u003e\n      \u003cbody\u003e\n        {entry.body}\n      \u003c/body\u003e\n    \u003c/html\u003e\n  )\n}\n```\n\nBuild once:\n\n```sh\nnode build build\n```\n\nWatch and rebuild:\n\n```sh\nnode build watch\n```\n\nThe `public` dir should now contain the output:\n\n```\n╚═ public\n   ╚═ index.html\n```\n\n## API\n\n### Front Matter, Props, Render Functions\n\nRendering is done by user-defined functions with the following signature:\n\n```\nProps -\u003e string | Promise\u003cstring\u003e\n```\n\nWhere props have the following shape:\n\n```js\ninterface Props {\n  entry: Entry\n  entries: [Entry]\n  tree: EntryTree\n}\n\ninterface Entry {\n  path: string\n  body: string\n  ...\n}\n\ninterface EntryTree {\n  [string]: Entry | EntryTree\n}\n```\n\n`entries` is the collection of the parsed templates. `tree` is the tree of all entries matching the folder structure, for convenient lookup. It's especially useful for rendering \"index\" pages that display multiple items, such as blog posts.\n\nTemplates typically look like this:\n\n```\n---\n(optional metadata in YAML format)\n---\n\n(body)\n```\n\nThe `--- ... ---` part is called [\"front matter\"](https://github.com/jxson/front-matter) and must be YAML.\n\nTemplates can also be JSON and YAML files. The top level data structure must be a dict:\n\n```json\n// json\n{\n  \"title\": \"Landing\",\n  \"description\": \"Company Website\"\n}\n```\n\n```yaml\n# yaml\ntitle: Landing\ndescription: Company Website\n```\n\nPapyre renders those and only those templates that specify a rendering function, which must be exported by your main JS file.\n\n```\n---\npapyre: {fn: myRenderingFunction}\n---\n```\n\n```js\nexport function myRenderingFunction(props) {\n  return props.entry.body\n}\n```\n\nEach template is parsed into an entry, which is the YAML front matter dict or the top-level data structure, with the remaining content added as `body`, plus the template's relative `path`.\n\n### Using the Tree\n\nSuppose you want to render a page with multiple elements, say, blog posts. Say we have this structure:\n\n```\n╚═ src\n   ╚═ templates\n      ╠═ index.js\n      ╠═ posts.md\n      ╚═ posts\n         ╠═ first.md\n         ╚═ second.md\n```\n\nSuppose `posts.md` looks like this:\n\n```\n---\npapyre: {fn: posts}\n---\n```\n\nAnd `first.md` and `second.md` look like this:\n\n```\n---\npapyre: {fn: post}\ntitle: Post Title\n---\n\n(body)\n```\n\nThen `posts` would receive the following tree, and could use it to render multiple posts:\n\n```js\nconst _tree = {\n  'index.js': {path: 'index.js', body: '(JS code)'},\n  'posts.md': {path: 'posts.md', body: ''},\n  'posts': {\n    'first.md': {path: 'posts/first.md', title: 'Post Title', body: '(body)'},\n    'second.md': {path: 'posts/second.md', title: 'Post Title', body: '(body)'},\n  }\n}\n\nfunction posts({tree}) {\n  // Should also sort these by date\n  return Object.values(tree.posts).map(post =\u003e (\n    `\u003cdiv\u003e${post.title}\u003c/div\u003e`\n  )).join('\\n')\n}\n\nfunction post({entry: {title}}) {\n  return `\u003cdiv\u003e${title}\u003c/div\u003e`\n}\n```\n\n### `build(webpackConfig, onDone)`\n\nRuns a single build cycle: compile JS, compile templates, trigger `onDone` when completed. See example in [Usage](#usage).\n\nThe config is used to create a new Webpack compiler instance, with modifications:\n  * store output in RAM\n  * don't bundle libraries\n  * compile for Node.js, without polyfills\n\nThe config must contain an `entry`, which must be a single string, a path to the JS file that exports rendering functions.\n\n```js\npapyre.build({entry: './src/templates/index.js'}, () =\u003e {})\n```\n\nThe template folder is assumed to be the entry file's directory; if the entry is `'./src/templates/index.js'`, the template folder is `'./src/templates'`.\n\n`onDone` receives either an error or the build result:\n\n```js\npapyre.build({entry: './src/templates/index.js'}, (err, result) =\u003e {\n  if (err) {\n    console.error(err)\n    process.exit(1)\n  }\n  else {\n    papyre.writeEntries('public', result.entries)\n  }\n})\n```\n\nThe result has the following shape:\n\n```js\ninterface Result {\n  entries: [Entry]\n  timing: string\n}\n\ninterface Entry {\n  path: string\n  body: string\n}\n```\n\nTo write the result to disk, use `writeEntries`, see below.\n\nWhen reusing a browser-oriented config, make sure to disable minification, i.e. `webpack.optimize.UglifyJsPlugin`, since it's expensive and pointless for a build-only bundle.\n\n### `buildP(webpackConfig)`\n\nSame as [`build`](#buildwebpackconfig-ondone), but instead of accepting a callback, returns a promise. Convenient with coroutines:\n\n```js\nasync function build() {\n  const result = await papyre.buildP(webpackConfig)\n  await papyre.writeEntries('...', result.entries)\n}\n```\n\n### `watch(webpackConfig, onDone)`\n\nAccepts the same configuration as `build`. Watches the templates directory and the dependency graph of the entry file. Triggers `onDone` on each rebuild. Returns a reference that can stop the watching:\n\n```js\nconst watch = papyre.watch({entry: './src/templates/index.js'}, () =\u003e {})\n\nwatch.deinit()\n```\n\n### `writeEntries(dir, entries)`\n\nWrites entries relative to `dir`, creating intermediary directories if necessary.\n\nThis:\n\n```js\nwriteEntries('public', [{path: 'index.html', body: ''}])\n```\n\nwill create this:\n\n```\n╚═ public\n   ╚═ index.html\n```\n\nShould be called in the `build` or `watch` callback; see the `examples` directory.\n\n## Misc\n\nI'm receptive to suggestions. If this library _almost_ satisfies you but needs changes, open an issue or chat me up. Contacts: https://mitranim.com/#contacts\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fpapyre","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmitranim%2Fpapyre","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmitranim%2Fpapyre/lists"}