Git Product home page Git Product logo

fastify-html's Introduction

fastify-html

Generate html in the most natural Fastify way, using template tags, layouts and the plugin system.

Template expressions are escaped by default unless they are prefixed with !.

Install

npm i fastify fastify-html

Usage

import fastify from 'fastify'
import fastifyHtml from 'fastify-html'

const app = fastify()
await app.register(fastifyHtml)

app.addLayout(function (inner, reply) {
  return app.html`
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <script src="https://unpkg.com/[email protected]"></script>
      </head>
      <body>
        <!-- Prefix expressions with ! if they contain safe HTML -->
        !${inner}
      </body>
    </html>
  `
}, { skipOnHeader: 'hx-request' })

app.get('/', async (req, reply) => {
  const name = req.query.name || 'World'
  return reply.html`<h1>Hello ${name}</h1>`
})

app.get('/complex-response/:page', async (req, reply) => {
  const name = req.query.name || 'World'
  const userInfo = await getInfo(name) || {}
  const demand = req.query.demand
  
  return reply.html`
      <div>
        Welcome, ${name}.
        <br /><br />

        User information:
        <br />

        <!-- Don't forget to prefix expressions that contain other html tags -->
        !${Object.keys(userInfo).map(
          (key) => app.html`
            ${key}: <b>${userInfo[key]}</b>
            <br />
          `
        )}
        <br />

        !${
          demand
            ? app.html`
              <p>Your demand: ${demand}</p>
            `
            : ""
        }
      </div>
  `
})

await app.listen({ port: 3000 })

Plugins

Encapsulation is supported and respected for layouts, meaning that addLayout calls will be not exposed to the parent plugin, like the following:

import fastify from 'fastify'
import fastifyHtml from 'fastify-html'

const app = fastify()
await app.register(fastifyHtml)

app.addLayout(function (inner, reply) {
  return app.html`
    <!DOCTYPE html>
    <html lang="en">
      <body>
        !${inner}
      </body>
    </html>
  `
})

app.get('/', async (req, reply) => {
  const name = req.query.name || 'World'
  strictEqual(reply.html`<h1>Hello ${name}</h1>`, reply)
  return reply
})

app.register(async function (app) {
  app.addLayout(function (inner) {
    return app.html`
      <i>
        !${inner}
      </i>
    `
  })

  app.get('/nested', async (req, reply) => {
    const name = req.query.name || 'World'
    return reply.html`<h1>Nested ${name}</h1>`
  })
})

await app.listen({ port: 3000 })

License

MIT

fastify-html's People

Contributors

mcollina avatar gurgunday avatar dependabot[bot] avatar

Stargazers

克里の小跟班 avatar Daniel Weck avatar Robert McGuinness avatar  avatar anravel avatar Tim Siewert avatar Vittor avatar Luis Mauro avatar Luis Ramos avatar Andrew Chou avatar Ralph Greaves avatar Thibaut Nguyen avatar Jeremiah John Macariola avatar Kaleb Ercanbrack avatar Diego Betto avatar Vlad Sirenko avatar Cameron Flint avatar Anton Guz avatar Tu Nguyen avatar Matt Burton avatar Evan Shortiss avatar Jonas Galvez avatar David Meir-Levy avatar Kamil Chmielewski avatar Marco Damiani avatar Paul Fischer avatar  avatar Bohdan avatar Jacopo Patroclo Martinelli avatar Hyeseong Kim avatar danny avatar Kye Hohenberger avatar  avatar

Watchers

 avatar Kye Hohenberger avatar Vlad Sirenko avatar  avatar  avatar

Forkers

gurgunday

fastify-html's Issues

proposal to replace the default html tag

Hey, just wanted to ask if you'd be down to integrating my html tag to fastify-html

For very short strings it is 10x faster, for long strings it can be more than 700x faster

It also supports html escaping by default – to indicate that an expression is safe to render raw, it must be prefixed with !

I think people who are using React & Co. just to send basic html to users are making it much harder than necessary

Random benchmark results (html2 is common-tags)

html1 with safe input x 29,247,213 ops/sec ±0.79% (98 runs sampled)
html1 with safe input x 22,540,186 ops/sec ±0.09% (91 runs sampled)
html1 with safe input x 32,791,098 ops/sec ±0.10% (96 runs sampled)
html1 with safe input x 10,819,679 ops/sec ±0.62% (99 runs sampled)
html1 with safe input x 32,523,957 ops/sec ±0.43% (98 runs sampled)
html2 with safe input x 41,971 ops/sec ±0.14% (98 runs sampled)
html2 with safe input x 2,225,232 ops/sec ±0.20% (99 runs sampled)
html2 with safe input x 3,117,999 ops/sec ±0.42% (96 runs sampled)
html2 with safe input x 1,874,689 ops/sec ±0.23% (94 runs sampled)
html2 with safe input x 3,090,945 ops/sec ±0.51% (97 runs sampled)

Benchmark

import benchmark from "benchmark";
import { html as html1 } from "./html.js";
import { html as html2 } from "common-tags";

// Define input data
const dangerousOutputWithHTMLEscapeChars = "<script>".repeat(1000);
const safeOutput = "This is safe content!".repeat(1000);

// Create benchmark suite
const suite = new benchmark.Suite();

// Add tests to the suite
suite
  .add("html1 with safe input", () => {
    html1`<div>!${safeOutput}</div>`;
  })
  .add("html1 with safe input", () => {
    html1`<div>!${123321}</div>`;
  })
  .add("html1 with safe input", () => {
    html1`<div>!${undefined}</div>`;
  })
  .add("html1 with safe input", () => {
    html1`<div>!${[null, 123, undefined]}</div>`;
  })
  .add("html1 with safe input", () => {
    html1`<div>!${""}</div>`;
  })
  .add("html2 with safe input", () => {
    html2`<div>!${safeOutput}</div>`;
  })
  .add("html2 with safe input", () => {
    html2`<div>!${123321}</div>`;
  })
  .add("html2 with safe input", () => {
    html2`<div>!${undefined}</div>`;
  })
  .add("html2 with safe input", () => {
    html2`<div>!${[null, 123, undefined]}</div>`;
  })
  .add("html2 with safe input", () => {
    html2`<div>!${""}</div>`;
  })

  .on("cycle", function (event) {
    console.log(String(event.target));
  })
  .on("complete", function () {
    console.log("Fastest is " + this.filter("fastest").map("name"));
  })

  // Run the benchmark
  .run({ async: true });

A complex example

Here's an example where the html tag can completely replace React's rendering model while natively supporting Prettier formatting, Tailwind completions, and all the other goodies

// layout.js
const Layout = (it, View) => {
  return html`<!doctype html>
    <html
      lang="${it.user ? "" : "en"}"
      data-theme="${it.user?.data.theme || "blackHEYHEY"}"
      class="mx-auto max-w-[1440px] overflow-auto scroll-smooth bg-cover bg-center md:bg-fixed"
    >
      <head>
        !${Meta(it)}
        <link rel="stylesheet" href="/p/assets/css/style.min.css" />
        <link
          rel="icon"
          sizes="192x192"
          href="/p/assets/img/lolo-192x192.png"
        />
        <link
          rel="apple-touch-icon"
          sizes="192x192"
          href="/p/assets/img/lolo-192x192.png"
        />
        !${it.scripts?.map((script) => {
          return html`
            <script
              fetchpriority="low"
              type="module"
              src="/p/assets/js/${script}"
            ></script>
          `;
        })}
        <script defer src="/p/assets/misc/js/quicklink.min.js"></script>
        !${it.pageBackground &&
        it.user?.data.profileBG &&
        it.user.data.profileBGLastUploadDate &&
        it.user.data.isPro
          ? html`
              <style>
                html {
                  background-image: url("${process.env
                    .S3_CDN_URI}/userAssets/${it.user.id}/profileBG.webp");
                }

                body {
                  backdrop-filter: blur(
                    ${it.user.data.profileBGBlur ?? "10"}px
                  );
                  -webkit-backdrop-filter: blur(
                    ${it.user.data.profileBGBlur ?? "10"}px
                  );
                }
              </style>
            `
          : ""}
        !${it.user?.data.theme === "custom"
          ? html`
              <style>
                [data-theme=custom] {
                ${it.user.getThemeCustomResult()}
                }
              </style>
            `
          : ""}
      </head>
      <body class="min-h-screen overflow-hidden">
        <header>
          <nav class="navbar">
            <div class="navbar-start">
              <a href="/" class="btn btn-ghost text-lg font-black capitalize">
                HEYHEY
              </a>
            </div>
            <div class="navbar-end">
              !${it.sessionAccount
                ? html`
                    <a
                      href="/p/account/profile"
                      class="btn btn-square btn-ghost"
                    >
                      <svg
                        xmlns="http://www.w3.org/2000/svg"
                        width="24"
                        height="24"
                        class="fill-current"
                      >
                        <title>Settings Icon</title>
                        <path
                          d="m2.344 15.271 2 3.46a1 1 0 0 0 1.366.365l1.396-.806c.58.457 1.221.832 1.895 1.112V21a1 1 0 0 0 1 1h4a1 1 0 0 0 1-1v-1.598a8.094 8.094 0 0 0 1.895-1.112l1.396.806c.477.275 1.091.11 1.366-.365l2-3.46a1.004 1.004 0 0 0-.365-1.366l-1.372-.793a7.683 7.683 0 0 0-.002-2.224l1.372-.793c.476-.275.641-.89.365-1.366l-2-3.46a1 1 0 0 0-1.366-.365l-1.396.806A8.034 8.034 0 0 0 15 4.598V3a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v1.598A8.094 8.094 0 0 0 7.105 5.71L5.71 4.904a.999.999 0 0 0-1.366.365l-2 3.46a1.004 1.004 0 0 0 .365 1.366l1.372.793a7.683 7.683 0 0 0 0 2.224l-1.372.793c-.476.275-.641.89-.365 1.366zM12 8c2.206 0 4 1.794 4 4s-1.794 4-4 4-4-1.794-4-4 1.794-4 4-4z"
                        ></path>
                      </svg>
                    </a>
                    <a
                      href="/${it.sessionAccount.id}"
                      class="btn btn-square btn-ghost"
                    >
                      <svg
                        xmlns="http://www.w3.org/2000/svg"
                        width="24"
                        height="24"
                        class="fill-current"
                      >
                        <title>Home Icon</title>
                        <path
                          d="m21.743 12.331-9-10c-.379-.422-1.107-.422-1.486 0l-9 10a.998.998 0 0 0-.17 1.076c.16.361.518.593.913.593h2v7a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-4h4v4a1 1 0 0 0 1 1h3a1 1 0 0 0 1-1v-7h2a.998.998 0 0 0 .743-1.669z"
                        ></path>
                      </svg>
                    </a>
                  `
                : html`
                    <a href="/p/login" class="btn btn-square btn-ghost">
                      <svg
                        xmlns="http://www.w3.org/2000/svg"
                        width="24"
                        height="24"
                        viewBox="0 0 24 24"
                        class="fill-current"
                      >
                        <title>Login Icon</title>
                        <path
                          d="M12 2C6.579 2 2 6.579 2 12s4.579 10 10 10 10-4.579 10-10S17.421 2 12 2zm0 5c1.727 0 3 1.272 3 3s-1.273 3-3 3c-1.726 0-3-1.272-3-3s1.274-3 3-3zm-5.106 9.772c.897-1.32 2.393-2.2 4.106-2.2h2c1.714 0 3.209.88 4.106 2.2C15.828 18.14 14.015 19 12 19s-3.828-.86-5.106-2.228z"
                        ></path>
                      </svg>
                    </a>
                  `}
            </div>
          </nav>
        </header>
        !${View(it)}
        !${it.user
          ? ""
          : html`
              <footer class="footer footer-center rounded p-16">
                <div class="grid grid-flow-col gap-5">
                  <a href="mailto:hey@heyhey,to" class="link-hover link">
                    Contact
                  </a>
                  <a href="/p/privacy" class="link-hover link">Privacy</a>
                  <a href="/p/terms" class="link-hover link">Terms</a>
                </div>
                <div>
                  <p>&copy; GOODMEN VENTURES, Inc.</p>
                </div>
              </footer>
            `}
      </body>
    </html>`;
};

HTML streaming

gurgunday/ghtml@c06823f added the htmlGenerator function, we can use it to stream templates as they get processed

I will probably send a PR in the following days, but just putting it up here so that if anyone is interested, they can give it a shot

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.