{"id":18788614,"url":"https://github.com/jcormont/async-html-template","last_synced_at":"2026-04-25T23:36:15.035Z","repository":{"id":57185598,"uuid":"107640631","full_name":"jcormont/async-html-template","owner":"jcormont","description":"Simple async/await-able HTML template engine","archived":false,"fork":false,"pushed_at":"2017-11-04T00:33:03.000Z","size":23,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-10-08T15:24:58.530Z","etag":null,"topics":["async","asynchronous","await","express","expressjs","html","renderer","view-engine"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jcormont.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-10-20T06:31:54.000Z","updated_at":"2017-10-20T16:34:46.000Z","dependencies_parsed_at":"2022-09-06T04:02:06.773Z","dependency_job_id":null,"html_url":"https://github.com/jcormont/async-html-template","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/jcormont/async-html-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcormont%2Fasync-html-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcormont%2Fasync-html-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcormont%2Fasync-html-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcormont%2Fasync-html-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jcormont","download_url":"https://codeload.github.com/jcormont/async-html-template/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jcormont%2Fasync-html-template/sbom","scorecard":{"id":511771,"data":{"date":"2025-08-11","repo":{"name":"github.com/jcormont/async-html-template","commit":"818775b0645433e6ecae425f699b036ac3e6559e"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.8,"checks":[{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Code-Review","score":0,"reason":"Found 0/10 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":8,"reason":"2 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-wxhq-pm8v-cw75","Warn: Project is vulnerable to: GHSA-pfq8-rq6v-vf5m"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-20T00:43:22.442Z","repository_id":57185598,"created_at":"2025-08-20T00:43:22.442Z","updated_at":"2025-08-20T00:43:22.442Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32280979,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"ssl_error","status_checked_at":"2026-04-25T18:29:32.149Z","response_time":59,"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":["async","asynchronous","await","express","expressjs","html","renderer","view-engine"],"created_at":"2024-11-07T21:05:23.620Z","updated_at":"2026-04-25T23:36:15.017Z","avatar_url":"https://github.com/jcormont.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Async HTML Template Engine\n\nA simple asynchronous (using async/await) HTML template engine, for use with Express or as a standalone HTML generator.\n\n\u003e **Note:** this module is ONLY for use with NodeJS 8+ (for native async/await), and does NOT work in the browser.\n\n## Usage (Express)\n\nRegister the view engine as follows:\n\n```javascript\napp.engine(\"html\", require(\"async-html-template\").AsyncHtmlViewEngine);\napp.set(\"view engine\", \"html\");\n```\n\n(or, in TypeScript) --\n\n```typescript\nimport { AsyncHtmlViewEngine } from \"async-html-template\";\napp.engine(\"html\", AsyncHtmlViewEngine);\napp.set(\"view engine\", \"html\");\n```\n\nThen, use Express views to render content:\n\n```javascript\napp.get(\"/\", (req, res) =\u003e {\n    res.render(\"index\", { title: \"Hello, world!\" });\n})\n```\n\n## Standalone Usage\n\nYou can also use the template engine without Express.\n\nFor simple one-off transforms, use the exported `renderAsync` and `renderFileAsync` functions:\n\n* `renderAsync(templateText, context)` returns a Promise for rendered HTML based on given template string and context object.\n* `renderFileAsync(fileName, context)` returns a Promise for rendered HTML based on the template in the given file and with the given context object.\n\nThe exported `Template` class can be used directly as well. It encapsulates an asynchronously compiled function that efficiently generates HTML output for a given context object.\n\n```javascript\n// render a template from a file:\nlet fileTemplate = Template.fromFile(\"./index.html\");\nlet fileResult = await fileTemplate.renderAsync({ title: \"Hello, world!\" });\n\n// render a template from a string:\nlet str = `\u003ch1\u003e{{ title }}\u003c/h1\u003e`;\nlet strTemplate = new Template(str);\nlet strResult = await strTemplate.renderAsync({ title: \"Hello, world!\" });\n```\n\n## Optimization\n\nTemplates that are generated by `renderFileAsync` and `Template.fromFile` (but NOT the partials included by any of these templates) are automatically _cached_, so that files are generally only read and compiled only once.\n\nThe resulting HTML is never cached, even for the same context object.\n\nBy default, HTML output is minified and comments are removed (using the NPM package `html-minifier`). You can supply minification options to the `renderAsync` method of the `Template` class, _or_ use the exported `defaultHtmlMinifierOptions` object which contains default options.\n\n## Templates\n\nA template is basically just (partial) HTML, with added code that runs while rendering the template.\n\n### Expression Tags\n\nTags that look like `{{ ... }}` contain JavaScript expressions that are evaluated while rendering. For each tag, the result is HTML-encoded and inserted in place of the tag.\n\nAs part of the expression, you can use properties from the context object (passed in through Express `.render`, or any of the `Template` rendering methods), as if they are variables. Alternatively you can use the `context` object explicitly, e.g. if a property is masked by a local variable or if its name is a reserved word.\n\n```html\n\u003ch1\u003eHello, {{ firstName }}\u003c/h1\u003e\n\u003cp\u003eYour case number is {{ context.case.getNumber() }}\u003c/p\u003e\n```\n\nUse the `\u003ctemplate html=\"...\"\u003e` tag described below if you do not want the result of an expression to be HTML-encoded.\n\n### Template Script Code\n\nAny script code inside of a script tags that contains the `in-template` attribute is not copied to the output, but is run while rendering instead.\n\nYou can use properties from the context object in template script code as well.\n\n```html\n\u003cscript in-template\u003e\n    let result = [];\n    for (let i = 0; i \u003c firstName.length; i++) {\n        result.push(firstName.charCodeAt(i));\n    }\n\u003c/script\u003e\n\u003cp\u003eYour name in Unicode is {{ JSON.stringify(result) }}\u003c/p\u003e\n```\n\n### Loops\n\nWrap a part of your template in a `\u003ctemplate for=\"...\"\u003e` ... `\u003c/template\u003e` tag to add a for-loop around the wrapped area of your template, with exactly the same syntax as in JavaScript.\n\n```html\n\u003c!-- A traditional for-loop: --\u003e\n\u003cdiv\u003e\n    \u003ctemplate for=\"{{ var i = 1; i \u003c= 10; i++ }}\"\u003e\n        \u003cspan\u003e{{ i }}\u003c/span\u003e\n    \u003c/template\u003e\n\u003c/div\u003e\n\n\u003c!-- An object for-in loop: --\u003e\n\u003cdiv\u003e\n    \u003ctemplate for=\"{{ var property in person.attributes }}\"\u003e\n        \u003cspan\u003e\u003cb\u003e{{ property }}:\u003c/b\u003e {{ person.attributes[property] }}\u003c/span\u003e\n    \u003c/template\u003e\n\u003c/div\u003e\n\n\u003c!-- A modern for-of loop over an array: --\u003e\n\u003cdiv\u003e\n    \u003cscript in-template\u003e\n        var fib = [1, 1, 2, 3, 5, 8, 13, 21];\n    \u003c/script\u003e\n    \u003ctemplate for=\"{{ var n of fib }}\"\u003e\n        \u003cspan\u003e{{ n }}\u003c/span\u003e\n    \u003c/template\u003e\n\n    \u003c!-- or even: --\u003e\n    \u003ctemplate for=\"{{ var [index, n] of fib.entries() }}\"\u003e\n        \u003cspan\u003e{{ n }} ({{ index }})\u003c/span\u003e\n    \u003c/template\u003e\n\u003c/div\u003e\n```\n\nFor a while-loop, you can use the `\u003ctemplate while=\"...\"\u003e` tag.\n\n```html\n\u003cp\u003e\n    Fib in reverse is\n    \u003ctemplate while=\"{{ fib.length }}\"\u003e {{ fib.pop() }} \u003c/template\u003e\n\u003c/p\u003e\n```\n\n### Conditionals\n\nWrap a part of your template in a `\u003ctemplate if=\"...\"\u003e` ... `\u003c/template\u003e` tag to check if a condition is true _while rendering_, and skip rendering the wrapped area if not.\n\n```html\n\u003cdiv\u003e\n    \u003ctemplate if=\"{{ person.children \u0026\u0026 person.children.length }}\"\u003e\n        \u003ch2\u003eChildren\u003c/h2\u003e\n        \u003ctemplate for=\"{{ let child of person.children }}\"\u003e\n            \u003cp\u003eName: {{ child.name }}\u003c/p\u003e\n        \u003c/template\u003e\n    \u003c/template\u003e\n\u003c/div\u003e\n```\n\nOptionally, you can leave out the braces from the value of the `if` attribute (i.e. `if=\"expression\"`) for exactly the same result.\n\nYou can also combine conditionals with loops in the same `\u003ctemplate ...\u003e` tag.\n\n```html\n\u003cdiv\u003e\n    \u003ctemplate if=\"{{ person.hasChildren() }}\" for=\"{{ let child of person.getChildren() }}\"\u003e\n        \u003cp\u003eChild: {{ child.name }}\u003c/p\u003e\n    \u003c/template\u003e\n\u003c/div\u003e\n```\n\n### Literal HTML Output\n\nFor an expression that returns HTML which should be appended literally (instead of being HTML-encoded), you can use the `\u003ctemplate html=\"...\"\u003e` tag.\n\nThis tag does _not_ wrap the output in a surrounding tag.\n\n```html\n\u003cscript in-template\u003e\n    let myHtml = \"\u003cb\u003eHello\u003c/b\u003e\";\n\u003c/script\u003e\n\u003cp\u003eThis is HTML-encoded: {{ myHtml }}\u003c/p\u003e\n\u003cp\u003eThis is not: \u003ctemplate html=\"{{ myHtml }}\" /\u003e\u003c/p\u003e\n```\n\n### Reusable Templates (Mixin Functions)\n\nUsing the `\u003ctemplate define=\"...\"\u003e` tag, you can define a named template (sometimes called a 'mixin' function) that can be used multiple times throughout the rest of your template. This does in fact define an (asynchronous) function within the compiled template, which can be passed around and called using its identifier.\n\nTo make it easier to call functions defined in this way, you can use the `\u003ctemplate use=\"...\"\u003e` tag. If you include any content within this tag, the resulting HTML will be passed to the defined function in a `content` property (also available as `context.content`).\n\n```html\n\u003c!-- Define a reusable function: --\u003e\n\u003ctemplate define=\"timedDiv\"\u003e\n    \u003cdiv data-timestamp=\"{{ new Date().getTime() }}\"\u003e\n        \u003ctemplate html=\"{{ content }}\" /\u003e\n    \u003c/div\u003e\n\u003c/template\u003e\n\n\u003c!-- Call the function twice: --\u003e\n\u003ctemplate use=\"timedDiv\" /\u003e\n\u003cscript in-template\u003e\n    await new Promise(r =\u003e setTimeout(r, 1000));\n\u003c/script\u003e\n\u003ctemplate use=\"timedDiv\"\u003e\n    \u003cp\u003eOne second later.\u003c/p\u003e\n\u003c/template\u003e\n```\n\nAlternatively, you can pass in a different context object, using the `context=\"...\"` attribute. The result of the expression is used as the function's context.\n\n```html\n\u003ctemplate define=\"sayHi\"\u003eHello, {{ name }}!\u003c/template\u003e\n\n\u003ctemplate use=\"sayHi\" context=\"{{ {name: 'world'} }}\" /\u003e\n\u003c!-- this is the same as: --\u003e\n\u003ctemplate html=\"{{ await sayHi({ name: 'world' }) }}\" /\u003e\n```\n\n### Partial Includes\n\nYou can include (partial) HTML templates from other files using the `\u003ctemplate partial=\"...\"\u003e` tag. Any given file name is considered to be relative to the current file -- so this only works if the template is loaded from a file in the first place (i.e. not using the `renderAsync(templateText, context)` function, for example).\n\nThis tag can also be combined with conditionals or loops, which causes the partial template to be evaluated repeatedly and/or conditionally.\n\nIf you need the partial template to be rendered with a different context object, you can provide the `context=\"...\"` attribute. The result of the expression is used as the partial template's context, which can be different for each iteration of a combined loop (if any).\n\nIf you do not specify a context expression, a shallow clone of the current context object will be used as the context for the included partial, along with wrapped HTML content (if any) in the `content` property.\n\n```html\n\u003c!-- Always included, cloned context --\u003e\n\u003ctemplate partial=\"always.html\" /\u003e\n\n\u003c!-- Conditionally included, different context --\u003e\n\u003ctemplate if=\"{{ !!supervisor }}\" partial=\"super.html\" context=\"{{ supervisor.getData() }}\"\u003e\n\n\u003c!-- Loop with different context for each iteration --\u003e\n\u003ctemplate for=\"{{ let child of person.children }}\" partial=\"child.html\" context=\"{{ child }}\"\u003e\n```\n\nNote that any _variables_ set in the current template scope (i.e. using `let` or `var`) are not available to the partial template. You **can** set properties of the `context` object, which will also be available to partial templates with the same context:\n\n```html\n\u003ctemplate for=\"{{ let child of person.children }}\"\u003e\n    \u003cscript in-template\u003e\n        // add current child to context object\n        context.child = child;\n    \u003c/script\u003e\n    \u003ctemplate partial=\"child.html\"\u003e\n\u003c/template\u003e\n```\n\nJust like with template functions defined using `\u003ctemplate define=\"...\"\u003e`, you can pass contained content along to the partial template. The content will end up in the `content` property of the partial's context. For this reason, the `partial` attribute is also aliased as `wrap`, since it can be used to wrap sections into an outer 'template'.\n\nAny functions defined in your containing code up to the point of the `\u003ctemplate wrap=\"...\"\u003e` tag will also be passed to the partial template (unless you explicity specify another context object). These functions can be used by the partial template using the `\u003ctemplate use=\"...\"\u003e` tag.\n\n```html\n\u003c!-- inner.html --\u003e\n\u003ctemplate define=\"scripts\"\u003e\n    \u003cscript\u003econsole.log(\"foo\")\u003c/script\u003e\n\u003c/template\u003e\n\u003ctemplate wrap=\"outer.html\"\u003e\n    \u003ch1\u003eContent\u003c/h1\u003e\n    \u003cp\u003eLorem ipsum.\u003c/p\u003e\n\u003c/template\u003e\n\n\u003c!-- outer.html --\u003e\n\u003carticle\u003e\n    \u003ctemplate html=\"content\" /\u003e\n    \u003ctemplate use=\"context.scripts\" /\u003e\n    \u003ctemplate use=\"context.styles\" /\u003e \u003c!-- ignored --\u003e\n\u003c/article\u003e\n```\n\n## Asynchronous Code\n\nBecause of the asynchronous nature of this template engine, you can actually include `await` expressions in your template code. This is really helpful if your (view) model has `async` features, which can be consumed directly by the template, instead of having to `await` any data that is used inside of your template.\n\nGiven a model that looks like this:\n\n```typescript\nexport class WaitForIt {\n    static async getSomeDataAsync() {\n        // do something useful here, like loading data from a server...\n        await new Promise(res =\u003e setTimeout(res, 1000));\n        return { foo: [\"Bar\", \"Baz\", \"Quux\"] };\n    }\n}\n```\n\nYou can use `await` expressions for this model's results directly in a template:\n\n```html\n\u003cdiv\u003e\n    \u003c!-- Either in a script block: --\u003e\n    \u003cscript in-template\u003e\n        let data = await WaitForIt.getSomeDataAsync();\n    \u003c/script\u003e\n    \u003ctemplate for=\"let name of data.foo\"\u003e\n        \u003cspan\u003e{{ name }}\u003c/span\u003e\n    \u003c/template\u003e\n\n    \u003c!-- Or even directly in a for loop: --\u003e\n    \u003ctemplate for=\"let name of (await WaitForIt.getSomeDataAsync()).foo\"\u003e\n        \u003cspan\u003e{{ name }}\u003c/span\u003e\n    \u003c/template\u003e\n\u003c/div\u003e\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcormont%2Fasync-html-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjcormont%2Fasync-html-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjcormont%2Fasync-html-template/lists"}