{"id":15601159,"url":"https://github.com/hacknlove/html-rewriter-plus","last_synced_at":"2026-02-14T15:02:56.093Z","repository":{"id":255192775,"uuid":"848783265","full_name":"hacknlove/html-rewriter-plus","owner":"hacknlove","description":null,"archived":false,"fork":false,"pushed_at":"2025-01-15T17:15:06.000Z","size":360,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-24T03:54:34.751Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hacknlove.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-08-28T11:59:33.000Z","updated_at":"2025-01-15T17:15:07.000Z","dependencies_parsed_at":null,"dependency_job_id":"595245d3-94f0-4ab4-96de-405ae7c0fa83","html_url":"https://github.com/hacknlove/html-rewriter-plus","commit_stats":null,"previous_names":["hacknlove/html-rewritter-plus","hacknlove/html-rewriter-plus"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/hacknlove/html-rewriter-plus","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacknlove%2Fhtml-rewriter-plus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacknlove%2Fhtml-rewriter-plus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacknlove%2Fhtml-rewriter-plus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacknlove%2Fhtml-rewriter-plus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hacknlove","download_url":"https://codeload.github.com/hacknlove/html-rewriter-plus/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hacknlove%2Fhtml-rewriter-plus/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29447768,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-14T14:10:32.461Z","status":"ssl_error","status_checked_at":"2026-02-14T14:09:49.945Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":[],"created_at":"2024-10-03T02:18:56.150Z","updated_at":"2026-02-14T15:02:56.076Z","avatar_url":"https://github.com/hacknlove.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# HTMLRewriter+\n\nStatic generated sites with dynamic content injected on the fly, and they stream to the client.\n\n## Install\n\n```bash\nnpm install html-rewriter-plus\n```\n\n## How it works\n\nUse any static site generator to generate your site.\n\nServe it with cloudflare pages.\n\nAdd some `data-ssr-*` attributes to your HTML elements.\n\nUse cloudflare page functions to fetch the data asynchronously, and render the HTML, injecting the data into the elements on the fly.\n\n## Directives\n\nUse any static site generator to generate your site.\n\n### Conditional rendering\n\n**Element:** any\n**Attribute:** `data-ssr-if`\n**Value:** A path to a value in the data object.\n\n```html\n\u003c!-- public/some-template.html --\u003e\n\u003cdialog data-ssr-if=\"page.hasSpoiler\" class=\"full-screen\"\u003e\n    Are you sure you want to read this? It contains spoilers.\n  \u003cform method=\"dialog\"\u003e\n    \u003cbutton\u003eYes\u003c/button\u003e\n  \u003c/form\u003e\n\u003c/dialog\u003e\n```\n\n**Note:** Expressions are not allowed in the value, only paths.\nAny logical operation should be done in the function, not in the template.\n\n### Map data to attributes\n\n**Element:** any\n**Attribute:** `data-ssr-map`\n**Value:** A comma-separated list of `path:attribute` pairs; `innerHTML` and `innerText` can be used to set the inner content of the element.\n\n```html\n\u003c!-- public/some-template.html --\u003e\n \u003ch1 data-ssr-map=\"post.title:innerText,post.\"\u003e\n  default content\n\u003c/h1\u003e\n```\n\n**Note:** Static content will be overwritten by the mapped data.\n\n### Map data to css variables\n\n**Element:** `style`\n**Attribute:** `data-ssr-css-vars`\n**Value:** A comma-separated list of `path:variable` pairs.\n\n```html\n\u003c!-- public/some-template.html --\u003e\n\u003cstyle data-ssr-css-vars=\"theme.backgroundColor:background-color,theme.color:color\"\u003e\u003c/style\u003e\n```\n\n**Note:** The css variables be set to the root element of the document.\n\n### Define template \n**Element:** `template`\n**Attribute:** `data-ssr-name`\n**Value:** The name of the template\n\nIt defines the templated named `name` so it can be used by other directives.\n\ncurrently, only the `for each` directive uses templates\n\n```html\n\u003c!-- public/some-template.html--\u003e\n\u003ctemplate data-ssr-name=\"bookCard\"\u003e\n  \u003cdiv class=\"catd\"\u003e\n    \u003ch2 data-ssr-map=\"book.title:innetText\" /\u003e\n    \u003ch3 data-ssr-map=\"book.author:innerHtml\" /\u003e\n    \u003cimg data-ssr-map=\"book.cover:src\"\u003e\n    \u003cdiv class=\"description\" data-ssr-map=\"book.description:imnerHTML\" /\u003e\n  \u003c/div\u003e\n\u003c/div\u003e\n```\n\n**Note:** Templates must be set before they are used\n\n**Note:** Templates can be set using the API\n\n### For Each\n**Element:** `template`\n**Attribute:** `data-ssr-for`\n**Value:** The name of the field to be used to hold the array elements\n**Attribute:** `data-ssr-in`\n**Value:** The name of the field where the array lies\n**Attribute:** `data-ssr-reder-template`\n**Value:** The name of the template to render\n\nIt renders the said template for each item of the array, storing it with the specified name\n\n```html\n\u003c!-- public/some-template.html --\u003e\n\u003ctemplate data-ssr-for=\"book\" data-ssr-in=\"books\" data-ssr-render-template=\"bookCard\" /\u003e\n```\n\n\n\n\n## Cloudflare Pages functions\n\nFollow the cloudflare pages functions documentation to deal with routes.\n\nUse `onRequestFactory` to make the route render the template.\n\n```javascript\n// functions/some-path.js\nimport { onRequestFactory } from \"html-rewriter-plus\";\n\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  data: {\n    ...,\n  },\n});\n```\n\n### All options\n\n* `template`: the path to the template file.\n* `data`: the data object that will be used to render the template.\n* `middlewares`: an array of functions that will be called in order before the data functions are called.\n* `flags`: an object with flags that will be used to set classes in the body element.\n* `postwares`: an array of functions that will be called in order after the data functions are called, and the response starts to be created.\n* `clientSideData`: an object with data that will be available in the client-side, at `window.data`.\n* `rules`: an array of extra rules that will be used to rewrite the response.\n* `templates`: an object of templates, as strings, promises, or functions\n\n### template\nIt's the template or the route to the template file, within the static generated site, that will be used to render the page.\nIt can be also a promise of the template, or a function that returns the template as a string or as `Response`, (or as a promise that resolves to a string or a `Response`).\n\nIf the string starts by `/` it will be considered a path to get the template.\n\nIf the string starts by `//`, `http://`, or `https://`, it will be considered a URL to get the template.\n\nOtherwise, it will be considered the template itself.\n\n**Note:** If the template is falsy, the template will be the same as the path.\n\n### data\nIt's the data object that will be used to render the template.\n\nYou can set static data, or promises that will be awaited just before the field is used.\n\nYou can also set a function that will be called with the cloudflare context object, and the object that was passed to `onRequestFactory`.\n\n```javascript\n\n// functions/user/[userId].js\nimport { onRequestFactory } from \"html-rewriter-plus\";\n\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  data: {\n    meaningOfLife: 42,\n    \n    // This will be called at startup time\n    todo: fetch('https://jsonplaceholder.typicode.com/todos/1').then(response =\u003e response.json()), \n    \n    // This will be called for each request\n    user: getUser\n  },\n});\n\n// ctx is not used in this example, but it's available\nfunction getUser(ctx) {\n  const userId = ctx.cfContext.params.userId;\n  return fetch(`https://jsonplaceholder.typicode.com/users/${userId}`).then(response =\u003e response.json());\n}\n```\n\n**Note:** The data functions will be called at the very beginning of the request, but same as the set promises, they won't be awaited until the field is used.\n\n### middlewares\n\nYou can define middlewares that will be executed and awaited in order before the data functions are called.\n\n**Uses:**\n\nReturn something different than the template.\n\n```javascript\n// functions/some-path.js\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  middlewares: [\n    (ctx) =\u003e {\n      if (!cfContext.request.headers.get('authorization')) {\n        // Redirect to login page\n        return new Response(null, {\n          status: 302,\n          headers: {\n            'Location': '/login'\n        });\n      },\n    }\n  ],\n  data: {\n    post: getPost\n  },\n});\n```\n\nRender a different template.\n```javascript\n// functions/some-path.js\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  middlewares: [\n    (ctx) =\u003e {\n      if (!ctx.cfContext.request.headers.get('authorization')) {\n        ctx.template = \"/login-required\";\n        ctx.data = {\n          redirect: ctx.cfContext.request.url\n        }\n      }\n    },\n  ],\n  data: {\n    post: getPost\n  },\n});\n```\n\n\nSet values in the context object, to be used by the data functions.\n\n```javascript\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  middlewares: [\n    (ctx) =\u003e {\n      ctx.lang = ctx.cfContext.request.headers.get('accept-language');\n    },\n  ],\n  data: {\n    post: getPost\n  },\n});\n\nfunction getPost(ctx) {\n  const lang = ctx.lang;\n  // fetch the post using the lang\n}\n```\n\nAnother option is to use the data itself, but remember to place the required field before it's used by other fields.\n\n```javascript\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  middlewares: [\n    (ctx) =\u003e {\n      ctx.lang = ctx.cfContext.request.headers.get('accept-language');\n    },\n  ],\n  data: {\n    lang: ctx =\u003e ctx.cfContext.request.headers.get('accept-language'),\n    post: getPost\n  },\n});\n\nfunction getPost(ctx) {\n  const lang = ctx.data.lang;\n  // fetch the post using the lang\n}\n```\n\nRewrite the request, to serve a different file.\n\n```javascript\n// functions/some-path.js\n\nimport otherPath from './other-path.js';\n\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  middlewares: [\n    (cfContext) =\u003e {\n      if (someCondition) {\n        return otherPath.onRequestGet(cfContext);\n      }\n    },\n  ],\n  data: {\n    post: getPost\n  },\n});\n```\n\n### Flags\n\nFlags work like data, but they are used to set classes in the body element.\n\nYou can set static flags, or promises that will be awaited just before the flag is used.\n\nYou can also set a function that will be called with the cloudflare context object, and the object that was passed to `onRequestFactory`.\n\nThen when the body element is rendered, the flags will be awaited and used to set classes.\n\n```javascript\n// functions/some-path.js\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  flags: {\n    'is-iphone': ctx =\u003e ctx.cfContext.request.headers.get('user-agent').includes('iPhone'),\n  },\n  data: {\n    post: getPost\n  },\n});\n\n// \u003cbody class=\"is-iphone\"\u003e\n```\n\n### postwares\n\nPostwares are functions that will be called in order after the data functions are called, and the response starts to be created.\n\nYou can use them to modify the response, to add cookies, or to change the headers, for instance.\n\n```javascript\n// functions/some-path.js\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  postwares: [\n    (ctx, response) =\u003e {\n      response.headers.set('Cache-Control', 'public, max-age=3600');\n    },\n  ],\n  data: {\n    post: getPost\n  },\n});\n```\n\nYou can also use a postware to deal with optimistic fallbacks\n\n```javascript\n// functions/some-path.js\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  postwares: [\n    (ctx, response) =\u003e {\n      if (!(await ctx.data.getPost)) {\n        return new Response('Not found', { status: 404 });\n      }\n    },\n  ],\n  data: {\n    post: getPost\n  },\n});\n```\n\n**Note:** But awaiting the data in the postware will block the response to be streamed to the client until the data is available. As soon as the data is available, the streaming will start sending all the HTML until the point on the html where the field is used; which is better than awaiting for the whole html to be rendered; but still it's much better to not await the data in the postware, and let the response be streamed to the client from the first byte.\n\n### clientSideData\n\nIt works pretty much like the `data` field, but it's not used to render the template. The data will be available in the client-side, at `window.data`.\n\nIf you set a function, it will be called with the cloudflare context object, and the object that was passed to `onRequestFactory`.\n\nAny promise will be awaited just before the data is injected into the stream, after the HTML is sent to the client.\n\n\n### Deferred Dynamic Head elements\n\nAny head element that needs to access data, will be automatically deferred to the end of the head element, so the browser gets all the static resources first, like stylesheets, scripts, and preloads, without awaiting for the asynchronous data to be available.\n\n### Client-side data\n\nYou might need dynamic data that is not available at build time.\n\nIn this case, you use the `clientSideData` field in the `onRequestFactory` options.\n\n```javascript\n// functions/some-path.js\n\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  clientSideData: {\n    foo: 'bar',\n  },\n,\n});\n```\n### Rules\n\nYou can set extra rules that will be used to rewrite the response.\n\nIt's an array of functions that accept the rewriter and the ctx.\n\n```javascript\n// functions/some-path.js\nexport const onRequestGet = onRequestFactory({\n  template: \"/some-template\",\n  rules: [\n    (rewriter, ctx) =\u003e {\n      rewriter.on('img:not([alt])', (element) =\u003e {\n        element.setAttribute('alt', '');\n      });\n    },\n  ],\n});\n```\n\n### Templates\nit's an object whose keys are the names of the templates and whose values are the templates that can be used by other directives to render data.\n\nThe values are dealt with the same way as the `template` field.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhacknlove%2Fhtml-rewriter-plus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhacknlove%2Fhtml-rewriter-plus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhacknlove%2Fhtml-rewriter-plus/lists"}