{"id":13695652,"url":"https://github.com/splink/pagelets","last_synced_at":"2026-01-14T02:15:55.451Z","repository":{"id":44672620,"uuid":"69895304","full_name":"splink/pagelets","owner":"splink","description":"A module for the Play Framework to build highly modular applications","archived":false,"fork":false,"pushed_at":"2022-01-31T22:50:46.000Z","size":311,"stargazers_count":76,"open_issues_count":0,"forks_count":5,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-05-03T13:42:18.689Z","etag":null,"topics":["big-pipe","pagelets","playframework","scala","streaming"],"latest_commit_sha":null,"homepage":null,"language":"Scala","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/splink.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":"2016-10-03T17:48:28.000Z","updated_at":"2024-02-26T19:18:51.000Z","dependencies_parsed_at":"2022-08-23T17:01:04.761Z","dependency_job_id":null,"html_url":"https://github.com/splink/pagelets","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"purl":"pkg:github/splink/pagelets","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splink%2Fpagelets","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splink%2Fpagelets/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splink%2Fpagelets/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splink%2Fpagelets/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/splink","download_url":"https://codeload.github.com/splink/pagelets/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/splink%2Fpagelets/sbom","scorecard":{"id":841831,"data":{"date":"2025-08-11","repo":{"name":"github.com/splink/pagelets","commit":"2322de307ab17690eb52f09facf5e3f41133f228"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.5,"checks":[{"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":"Code-Review","score":0,"reason":"Found 0/21 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":"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":"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":"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":"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":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}},{"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: Apache License 2.0: 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":-1,"reason":"internal error: error during branchesHandler.setup: internal error: githubv4.Query: Resource not accessible by integration","details":null,"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":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 14 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}}]},"last_synced_at":"2025-08-23T20:39:02.652Z","repository_id":44672620,"created_at":"2025-08-23T20:39:02.652Z","updated_at":"2025-08-23T20:39:02.652Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28408711,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T01:52:23.358Z","status":"online","status_checked_at":"2026-01-14T02:00:06.678Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["big-pipe","pagelets","playframework","scala","streaming"],"created_at":"2024-08-02T18:00:31.807Z","updated_at":"2026-01-14T02:15:55.432Z","avatar_url":"https://github.com/splink.png","language":"Scala","readme":"[![Build Status](https://travis-ci.org/splink/pagelets.svg?branch=master)](https://travis-ci.org/splink/pagelets)\n\n# Pagelets\nA Module for the Play Framework to build modular applications in an elegant and concise manner.\n\nCheck out the [sample project](https://github.com/splink/pagelets-seed) to see a sample application based on Play Pagelets.  \n\n### Idea\nThe idea behind the Pagelets Module is to split a web page into small, composable units. Such a unit is called a pagelet. \nIn terms of the Play Framework a pagelet is just a simple Action[AnyContent]. That means that a pagelet is basically a (small) \nweb page. Pagelets can be arranged in a page tree. So, if a user requests a page, the page is constructed according to it's  page tree. It is also possible to serve any part of the tree down to a single pagelet individually. \nThe ordinary pagelet consists of a view, resources (JavaScript, Css), a controller action and a service to fetch data.  \n\n![Pagelets](docs//pagelets-tree-vis.png)\n\nPagelets are particularly useful if you want to serve tailor-made pages to your visitors. For instance you can easily \nserve a slightly different page to users from different countries (i18n), or perform A/B testing, or fine-tune the page \nbased on the user (logged-in, gender, other preferences, ...).\n\nPagelets comes in two flavours:\n*Async* and *Streaming*. *Async* composes the complete page on the server side and sends it back to the \nclient, as soon as all pagelets are complete. *Streaming* on the other hand, begins to send the page immediately to the\nclient and pagelets appear sequentially as soon as they complete. \n\n### Traits\n- **composable**: multiple pagelets can be composed into a page. A page is just a tree of pagelets. Any part of the pagelet tree can be served to the user. \n- **resilient**: if a pagelet fails, a fallback is served. Other pagelets are not affected by the failure of one or more pagelets. \n- **simple**: to create a pagelet is simple compared to a whole page, because of its limited scope. To compose a page from pagelets is simple.\n- **modular**: any pagelet can be easily swapped with another pagelet, removed or added to a page at runtime.\n\n\nPagelets are non invasive and not opinionated: You can stick to your code style and apply the patterns you prefer. Use your favorite dependency injection mechanism and template engine. You don't need to apply the goodness of pagelets everywhere, only employ pagelets where you need them. Pagelets also do not introduce additional dependencies to your project. \n\n### Quickstart\nTo get the idea how Pagelets look in code, read on and check out the [play pagelets seed project](https://github.com/splink/pagelets-seed) afterwards.\n\u003e The Pagelets Module depends on the Play Framework.\n\nAdd the following lines to your build.sbt file:\n\n#### Play 2.8 (Scala 2.12 | Scala 2.13)\n~~~scala\nlibraryDependencies += \"org.splink\" %% \"pagelets\" % \"0.0.11\n~~~\n\n##### For older Play/Scala versions:\n###### Play 2.5 (Scala 2.11)\n~~~scala\nlibraryDependencies += \"org.splink\" %% \"pagelets\" % \"0.0.3\n~~~\n\n###### Play 2.6 (Scala 2.11 | Scala 2.12)\n~~~scala\nlibraryDependencies += \"org.splink\" %% \"pagelets\" % \"0.0.8\n~~~\n\n~~~scala\nroutesImport += \"org.splink.pagelets.Binders._\"\n~~~\n\nIf you want to use streaming, you will also need:\n~~~scala\nTwirlKeys.templateFormats ++= Map(\"stream\" -\u003e \"org.splink.pagelets.twirl.HtmlStreamFormat\")\nTwirlKeys.templateImports ++= Vector(\"org.splink.pagelets.twirl.HtmlStream\", \"org.splink.pagelets.twirl.HtmlStreamFormat\")\n~~~\nthis adds streaming capabilities to the Twirl template engine. To use the streaming template format, you must name your\ntemplates *name.scala.stream* instead of *name.scala.html*\n\nNow add the following line to your application.conf file, to enable the Pagelets module:\n~~~\nplay.modules.enabled += org.splink.pagelets.pageletModule\n~~~\nCreate a standard Play controller and inject a *Pagelets* instance. In this example Guice is used as DI framework, but \nany DI mechanism works.\n~~~scala\n@Singleton\nclass HomeController @Inject()(pagelets: Pagelets)(implicit m: Materializer, e: Environment) extends InjectedController\n~~~\n\nBring pagelets into scope\n~~~scala\nimport pagelets._\n~~~\n\nTo use the Play's Twirl template engine, import TwirlConversions\n~~~scala\nimport org.splink.pagelets.twirl.TwirlCombiners._\n~~~\nTo use Streaming, additionally import HtmlStreamOps\n~~~scala\nimport org.splink.pagelets.twirl.HtmlStreamOps._\n~~~\n\nNow create the main template inside the *views* folder.\nName the file *wrapper.scala.html* or, if you want to use streaming, name it *wrapper.scala.stream*\n~~~scala\n @(resourceRoute: String =\u003e Call)(page: org.splink.pagelets.Page)\n \n \u003c!DOCTYPE html\u003e\n \u003chtml\u003e\n     \u003chead\u003e\n         \u003ctitle\u003e@page.head.title\u003c/title\u003e\n         \u003clink rel=\"stylesheet\" media=\"screen\" href='@routes.Assets.versioned(\"stylesheets/main.min.css\")'\u003e\n         \u003clink rel=\"shortcut icon\" type=\"image/png\" href=\"@routes.Assets.versioned(\"images/favicon.png\")\"\u003e\n \n         @page.head.metaTags.map { tag =\u003e\n           \u003cmeta name=\"@tag.name\" content=\"@tag.content\" /\u003e\n         }\n \n         @page.head.css.map { css =\u003e\n             \u003clink rel='stylesheet' media='screen' href='@{resourceRoute(css.toString).url}'\u003e\n         }\n \n         \u003cscript src=\"@routes.Assets.versioned(\"lib/jquery/jquery.min.js\")\"\u003e\u003c/script\u003e\n         @page.head.js.map { js =\u003e\n             \u003cscript src='@{resourceRoute(js.toString).url}'\u003e\u003c/script\u003e\n         }\n     \u003c/head\u003e\n     \u003cbody\u003e\n         @Html(page.body)\n \n         @page.js.map { js =\u003e\n             \u003cscript src='@{resourceRoute(js.toString).url}'\u003e\u003c/script\u003e\n         }\n     \u003c/body\u003e\n \u003c/html\u003e\n~~~\nThe main template receives a resource route which is needed to reference the JavaScript and Css resources for the page.\nThe template is also provided with a *Page* instance which contains all parts necessary to render the page: HTML body, \nJavaScript, Css and Meta Tags.\n\nCreate a simple pagelet template inside the views folder:\n~~~scala\n@(name: String)\n\u003cdiv\u003e@name\u003c/div\u003e\n~~~\n\nCreate a simple pagelet inside the controller:\n~~~scala\ndef pagelet(name: String)() = Action {\n  Ok(views.html.pagelet(name))\n}\n~~~\n\nDefine the page composition:\n~~~scala\ndef tree(r: RequestHeader) = {\n  val tree = Tree(\"root\".id, Seq(\n    Leaf(\"header\".id, pagelet(\"header\") _).withJavascript(Javascript(\"header.min.js\")).setMandatory(true),\n    Tree(\"content\".id, Seq(\n      Leaf(\"carousel\".id, pagelet(\"carousel\") _).withFallback(pagelet(\"Carousel\") _).withCss(Css(\"carousel.min.css\")),\n      Leaf(\"text\".id, pagelet(\"text\") _).withFallback(pagelet(\"Text\") _)\n    )),\n    Leaf(\"footer\".id, pagelet(\"footer\") _).withCss(Css(\"footer.min.css\"))\n  ))\n  \n  if(messagesApi.preferred(r).lang.language == \"de\") tree.skip(\"carousel\".id) else tree\n}\n~~~\nThere are 2 different kinds of pagelets: Leaf pagelets and Tree pagelets. A Leaf pagelet references an actual Action, while\na Tree pagelet combines its children into one. When a request arrives, the tree of pagelets is constructed. \nAll Leaf pagelets are executed in parallel and as soon a the children of a Tree pagelet complete, they are combined.\nThis process continues, until just the root pagelet remains.\n\nResources and fallbacks can be defined per pagelet. If a pagelet fails to render, its fallback pagelet is rendered. \nResources are assembled and combined by type and references are later provided to the main template.\n\nThe *skip* and *replace* operations are available on instances of *Tree*. They allow to change the tree at runtime. \nFor instance, the tree can be changed based on the language of an incoming request. Note that the resources for the skipped \npagelet are also excluded. If the request language is \"de\", the carousel pagelet and all its resource dependencies are \nleft out. \n\nIn this example the header pagelet is declared as mandatory, so if the header fails, the user is redirected to an (error) \npage. Note that Tree pagelets can't fail or depend on resources.\n\nThe carousel pagelet depends on *carousel.min.css* and the footer pagelet depends on *footer.min.css*. If the tree is \nconstructed, both *carousel.min.css* and *footer.min.css* are concatenated into one file whose name is the fingerprint of \nits contents. This sole Css file which consists of carousel and footer styles is then served under its fingerprint.\n\n \nNow add an index Action to the controller to render the complete page.\nIf you want to use *async*, add:\n~~~scala\ndef index = PageAction.async(routes.HomeController.errorPage)(_ =\u003e \"Page Title\", tree) { (request, page) =\u003e\n  views.html.wrapper(routes.HomeController.resourceFor)(page)\n}\n~~~\n\nIf you prefer to use *streaming*, add:\n~~~scala\ndef index = PageAction.stream(_ =\u003e \"Page Title\", tree) { (request, page) =\u003e\n  views.html.wrapper(routes.HomeController.resourceFor)(page)\n}\n~~~\n\nBoth flavours require the page title, the pagelet tree configuration and a function which receives the request and page \nas arguments. In the *async* case the function must return a *Writeable* and in the *streaming* case a \n*Source[Writeable,_]*. A *Writeable* is just a type class which is capable of transforming the wrapped class eventually to a \nHTTP response.\n*errorPage* is only required in the *async* case. It is called, if a mandatory pagelet and its fallback fail to render.\nThe *streaming* case can't redirect to another page in case some mandatory pagelet failed, because at the time, the\npagelet fails, parts of the page are already streaming to the client, thus it's too late.\n\n\nFinally add the route to conf/routes\n~~~scala\nGET  /                              controllers.HomeController.index\nGET  /resource/:fingerprint         controllers.HomeController.resourceFor(fingerprint: String)\n~~~\n\n### Details\n\n#### Advantages\n- Resilient: if one part of the page fails, the other pagelets remain unaffected. A fallback can be defined per pagelet. \nIf a fallback fails, the pagelet is simply left out. If a pagelet is declared as mandatory and its fallback also fails,\nthe request is redirected to a configurable error page.\n\n- Modular: a pagelet is an isolated and independent unit. Assets like JavaScript and Stylesheets are defined on a per pagelet\nbasis so a pagelet is completely autonomous. A pagelet can be easily reused on any page.\n\n- Flexible: a page can be composed with very little code, and the composition can be changed at runtime. Specific \npagelets can be replaced with others, removed or new pagelets can be added anywhere in the page with just a line of code.\nThis is quite handy to conduct A/B tests or to serve a different page based on the user properties like locale, user-role, ...\n\n- Simple: to create a pagelet is much simpler then to create a complete page, because the scope of a pagelet is small.\nThe composition of a page from pagelets is just a bit of configuration code and thus also simple. So all steps\nrequired to build a page are simple.\n\n- Logs: Detailed logs help to gain useful insights on the performance and to find bottlenecks quickly.\n\n- Performant #1: all pagelets in a page tree are executed in parallel, so splitting a page into paglets induces no \nperceptible overhead. \n\n- Performant #2: Resources are automatically concatenated and hashed as well as served with far future expiration dates. \nTherefore browsers need to make only few requests, and - as long as the resources haven't changed - can pull them from \nthe local cache.\n\n- Performant #3: A page can optionally be streamed which effectively reduces the time to first byte to milliseconds and\nenables the browser to start loading resources immediately.\n\n- Separation of concerns: by using pagelets, you automagically end up with a clean and flexible application design.\n\n\n#### Fallbacks\nEach pagelet can define a fallback. A fallback is just another pagelet. If the main pagelet fails, its fallback is executed.\nIf the fallback fails too, then the pagelet is simply left out. But if the pagelet was declared mandatory, then the\nrequest is redirected to another (error) page. If the main pagelet has no fallback and fails, it's left out - unless\nthe pagelet was declared mandatory.\n\n#### Resources\nAll resources declared by the pagelets of a page (JavaScript, Css) are de-duplicated, aggregated and combined during the construction of \nthe page. A hash is then computed for each combined resource type. Correspondingly, *script* and *link* tags which reference \nthe combined resources by their hash are injected into the page. These resources are served with far future expiration dates.\nSo, if the resources haven't changed, browsers can just pull them from the cache. As soon as the resources change, browsers\nare presented with a fresh hash value und thus fetch the new resources. This reduces the amount of requests a browser has \nto make to render a page to a bare minimum. This system also makes sure that only the resources which are actually needed \non a page are served.\n\n#### Cookies \u0026 Meta Tags\nEach pagelet can set Cookies and Meta Tags. Just as with the resources, Cookies and Meta Tags are de-duplicated, aggregated \nand combined during the construction of the page.\n\n#### Async vs. Streaming\n\n##### Async\nWhen a page is rendered in *async* mode, all pagelets are rendered and then assembled on the server into the final page. \nOnce complete, the complete page is sent to the client.\n\n##### Streaming\nIn *streaming* mode, the page is streamed immediately to the client. As soon the next pagelet is ready, the pagelet is\nstreamed to the client. This is repeated until all pagelets have been streamed.\nStreaming seems quite advantageous because the client receives the first parts of the page immediately. Within these first\nparts is the HTML head, which includes references to the JavaScript and Css which is needed to render the page. This means \nthat the browser can start loading external resources while the HTML is still streaming. This parallelization reduces the \noverall load time of the page. But even more perceptible is the extremely short amount of time until the first byte is \nreceived by the client, it takes only a few milliseconds. So from the perspective of the user, the page appears\nimmediately and completes rendering progressively.\n\nBut there are also downsides to the *streaming* approach:\n- HTTP Headers are sent first. As the headers contain the HTTP Status Code, the code is always set to 200/Ok, even though \nat the time the header is constructed, it is too early to safely assume that the page can be rendered correctly. So you \nneed to make sure that you have appropriate fallbacks in place in case a pagelet fails to render.\n- If a page is cached, the Cache will certainly cache a page with a status code of 200/Ok. This means that fallbacks might \nend up being cached.\n- Only non-httpOnly Cookies can be set. Each pagelet can set Cookies. But only when all pagelets are complete, the Cookies\nto set are all present. So, it's simply not possible to set the Cookies as usual via HTTP headers, because the HTTP headers\nare sent first to the client. So Cookies are set with a piece of Javascript code at the end of the Html body. As setting\nCookies relies on JavaScript, the Cookies can't be Http-only.\n\n##### When to choose streaming\nChoose the *streaming* if:\n- all users have JavaScript enabled or the page does not use Cookies\n- the page does not rely on httpOnly Cookies\n- pages are not cached or it's very unlikely that some pagelets fail or it does not matter if a fallback is cached\n\notherwise choose the caveat-free *async* mode.\n\n\n\n\u003e Big thanks to [brikis98](https://github.com/brikis98) who originally had the idea to port [Facebook's BigPipe](https://www.facebook.com/notes/facebook-engineering/bigpipe-pipelining-web-pages-for-high-performance/389414033919/) to Play and\ndid a lot of the groundwork with his [brilliant talks](https://www.youtube.com/watch?v=4b1XLka0UIw) and [ping-play repo](https://github.com/brikis98/ping-play)\n","funding_links":[],"categories":["Table of Contents","Web Frameworks","Libraries"],"sub_categories":["Web Frameworks","Others"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsplink%2Fpagelets","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsplink%2Fpagelets","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsplink%2Fpagelets/lists"}