Documentation

§Play 2.4 Migration Guide

This is a guide for migrating from Play 2.3 to Play 2.4. If you need to migrate from an earlier version of Play then you must first follow the Play 2.3 Migration Guide.

As well as the information contained on this page, there are is more detailed migration information for some topics:

§Java 8 support

The support for Java 6 and Java 7 was dropped and Play 2.4 now requires Java 8. This decision was made based on the fact that Java 7 reached its End-of-Life in April 2015. Also, Java 8 enables clean APIs and has better support for functional programming style. If you try to use Play 2.4 with Java 6/7, you will get an error like below:

java.lang.UnsupportedClassVersionError: play/runsupport/classloader/ApplicationClassLoaderProvider : Unsupported major.minor version 52.0

A java.lang.UnsupportedClassVersionError means that reading a Java class file with an older version of Java than the class file was compiled with is unsupported.

Note: Scala 2.10 does not have full support to all Java 8 language features, like static methods on interfaces. If your project has Java code using these new features present in Java 8, upgrade to use Scala 2.11.6+. See sbt docs to learn how to set scalaVersion to your project.

§Build changes

The following steps need to be taken to update your sbt build before you can load/run a Play project in sbt.

§Play upgrade

Update the Play version number in project/plugins.sbt to upgrade Play:

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.4.0") // Set to latest version of Play 2.4

§sbt upgrade

Play 2.4 now requires a minimum of sbt 0.13.8. Update your project/build.properties so that it reads:

sbt.version=0.13.8

§Specs2 support in a separate module

If you were previously using Play’s specs2 support, you now need to explicitly add a dependency on that to your project. Additionally, specs2 now requires scalaz-stream which isn’t available on maven central or any other repositories that sbt uses by default, so you need to add the scalaz-stream repository as a resolver:

libraryDependencies += specs2 % Test

resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"

If you are using a .scala build file, you will need to add the following import import play.sbt.PlayImport._

§Database evolutions support in a separate module

Support for database evolutions used to be included with both Play JDBC and JPA support. That’s no longer the case. Therefore, if you are using evolutions, you now need to add an explicit dependency to evolutions in your project’s build:

libraryDependencies += evolutions

While, if you are not using evolutions, you can now safely remove evolutionplugin=disabled from your application.conf.

The configuration keys have changed, so instead of evolutionplugin=disabled you now need to use play.evolutions.enabled=false (See Evolutions configuration)

If you are using Play Slick module (with or without evolutions), things have changed quite a bit, so make sure to read the Play Slick migration guide.

§IDEs: Eclipse and IntelliJ IDEA

Play no longer includes the sbteclipse or sbt-idea plugins, which enables users to upgrade IDE support independently of Play.

Eclipse support can be setup with as little as one extra line to import the plugin. See the documentation for details.

IntelliJ is now able to import sbt projects natively, so we recommend using that instead. Alternatively, the sbt-idea plugin can be manually installed and used, instructions can be found here.

§Play SBT plugin API

All classes in the SBT plugin are now in the package play.sbt, this is particularly pertinent if using .scala files to configure your build. You will need to import identifiers from play.sbt.PlayImport to use play provided configuration elements.

§playWatchService renamed

The SBT setting key playWatchService has been renamed to fileWatchService.

Also the corresponding class has changed. To set the FileWatchService to poll every two seconds, use it like this:

PlayKeys.fileWatchService := play.runsupport.FileWatchService.sbt(2000)

§Play Slick dependency

Play Slick module has gone through a major restyle to support Slick 3.0. For a smooth upgrade, read the Play Slick migration guide.

§Ebean dependency

Ebean has been pulled out into an external project, to allow it to have a lifecycle independent of Play’s own lifecycle. The Ebean bytecode enhancement functionality has also been extracted out of the Play sbt plugin into its own plugin.

To migrate an existing Play project that uses Ebean to use the new external Ebean plugin, remove javaEbean from your libraryDependencies in build.sbt, and add the following to project/plugins.sbt:

addSbtPlugin("com.typesafe.sbt" % "sbt-play-ebean" % "1.0.0")

After that, enable Ebean plugin for your project:

lazy val myProject = (project in file("."))
  .enablePlugins(PlayJava, PlayEbean)

And finally, configure Ebean mapped classes as a list instead of a comma separated string (which is still supported but was deprecated):

ebean.default = ["models.*"]
ebean.orders = ["models.Order", "models.OrderItem"]

Additionally, Ebean has been upgraded to 4.5.x, which pulls in a few of the features that Play previously added itself, including the Model class. Consequently, the Play Model class has been deprecated, in favour of using com.avaje.ebean.Model.

§Bytecode enhancement

Play's bytecode enhancement, which generates getters and setters for Java properties, has been pulled out of the core of Play into a separately managed project that can have its own lifecycle. To enable it, add the following to your project/plugins.sbt file:

addSbtPlugin("com.typesafe.sbt" % "sbt-play-enhancer" % "1.1.0")

§Dependency Injection

Play now, out of the box, uses dependency injection provided by Guice. This is part of a long term strategy to remove global state out of Play, which we hope to complete in the Play 3.0 release. Moving any application from depending on global state to being entirely global state free is a big task, one that can be very disruptive if it is done all at once. For this reason, the approach we’ve taken in Play is to spread the change over a number of releases, allowing end users to gradually migrate their code so that it doesn’t depend on global state, rather than forcing it all at once.

As much as practical, we have ensured that the APIs provided in Play 2.4 are source compatible with Play 2.3. This means, in many situations, there are two ways of doing things, a way that depends on global state, and a way that doesn’t. We’ve updated the documentation to reflect the new dependency injection approach of doing things - in cases where you still want to use the old APIs and see documentation about them, in general, the Play 2.3 documentation is still relevant.

It’s important that you read the documentation about dependency injection in Play before proceeding with migrating to Play 2.4. There are some decisions to make up front. Out of the box we provide and encourage the use of Guice for dependency injection, but many other dependency injection tools and techniques, including compile time dependency injection techniques in Scala are possible. You can read about dependency injection in Java or Scala.

§Routing

One of the most disruptive changes with regards to dependency injection is we now support the generation of two styles of routers. The first is the existing static style, this is largely unchanged from the Play 2.3 router. It is a Scala singleton object, and assumes that all the actions that it invokes are either Scala singleton objects, or Java static methods. The second is a dependency injected router, which is a class that declares its dependencies in its constructor. To illustrate the difference between these two routers, consider the following routes file:

GET   /               controllers.Application.index
POST  /save           controllers.Application.save
GET   /assets/*file   controllers.Assets.versioned(path = "/public", file: Asset)

The static routes generator will generate a router that very roughly (pseudo code) looks like this:

object Routes extends GeneratedRouter {
  def routes = {
    case ("GET", "/") => controllers.Application.index
    case ("POST", "/save") => controllers.Application.save
    case ("GET", "/assets/:file") => controllers.Assets.versioned("/public", file)
  }
}

Meanwhile the injected routes generator will generate a router that very roughly looks like this:

class Routes(application: controllers.Application, assets: controllers.Assets) extends GeneratedRouter {
  def routes = {
    case ("GET", "/") => application.index
    case ("POST", "/save") => application.save
    case ("GET", "/assets/:file") => assets.versioned("/public", file)
  }
}

The default is to use the static routes generator. You must use this if you are not ready to migrate all of your Java actions to be non static methods, or your Scala actions to be classes. In most cases, this is quite straightforward to do, in Java it requires deleting the static keyword, in Scala it requires changing the word object to class. The static router still supports the @ operator, which will tell it to look up the action from a runtime Injector, you may find this useful if you are in a transitional period where some of your actions are static and some are injected.

If you wish to switch to the injected generator, add the following to your build settings in build.sbt:

routesGenerator := InjectedRoutesGenerator

By default Play will automatically handle the wiring of this router for you using Guice, but depending in the DI approach you’re taking, you may be able to customise it.

The injected routes generator also supports the @ operator on routes, but it has a slightly different meaning (since everything is injected), if you prefix a controller with @, instead of that controller being directly injected, a JSR 330 Provider for that controller will be injected. This can be used, for example, to eliminate circular dependency issues, or if you want a new action instantiated per request.

In addition, Play now, by default, generates the router in the router package, instead of at the root package. This is to aid with dependency injection, so if needed it can be manually created or bound, since classes in the root package can’t usually be referenced.

§Dependency Injected Components

While Play 2.4 won’t force you to use the dependency injected versions of components, we do encourage you to start switching to them. The following tables show old static APIs that use global state and new injected APIs that you should be switching to:

§Scala

Old API New API Comments
Lang Langs
Messages MessagesApi Using one of the preferred methods, you can get a Messages instance.
DB DBApi or better, Database You can get a particular database using the @NamedDatabase annotation.
Cache CacheApi or better You can get a particular cache using the @NamedCache annotation.
Cached object Cached instance Use an injected instance instead of the companion object. You can use the @NamedCache annotation.
Akka N/A No longer needed, just declare a dependency on ActorSystem
WS WSClient
Crypto Crypto
GlobalSettings HttpErrorHandler, HttpRequestHandler, and HttpFilters Read the details in the GlobalSettings section below.

§Java

Old API New API Comments
Lang Langs Instances of Lang objects are still fine to use
Messages MessagesApi Using one of the preferred methods, you can get a Messages instance, and you can then use at to get messages for that lang.
DB DBApi or better, Database You can get a particular database using the @NamedDatabase annotation.
JPA JPAApi
Cache CacheApi You can get a particular cache using the @NamedCache annotation.
Akka N/A No longer needed, just declare a dependency on ActorSystem
WS WSClient
Crypto Crypto The old static methods have been removed, an instance can statically be accessed using play.Play.application().injector().instanceOf(Crypto.class)
GlobalSettings HttpErrorHandler, HttpRequestHandler, and HttpFilters Read the details in the GlobalSettings section below.

§GlobalSettings

If you are keen to use dependency injection, we are recommending that you move out of your GlobalSettings implementation class as much code as possible. Read the `GlobalSettings` migration documentation for the gory details.

§Plugin deprecated

Both Java play.Plugin and Scala play.api.Plugin types have been deprecated. Read migrating Plugin to Module to update your code to use play.api.inject.Module.

§Configuration changes

Play 2.4 now uses reference.conf to document and specify defaults for all properties. You can easily find these by going here and searching for files called reference.conf.

Additionally, Play has now better namespaced a large number of its configuration properties. The old configuration paths will generally still work, but a deprecation warning will be output at runtime if you use them. Here is a summary of the changed keys:

Old key New key
application.secret play.crypto.secret
application.context play.http.context
session.* play.http.session.*
flash.* play.http.flash.*
application.router play.http.router
application.langs play.i18n.langs
application.lang.cookie play.i18n.langCookieName
parsers.text.maxLength play.http.parser.maxMemoryBuffer
csrf play.filters.csrf
evolutions.* play.evolutions.*
applyEvolutions.<db> play.evolutions.db.<db>.autoApply
ws play.ws

§Akka configuration

Play 2.4 now has just one actor system. Before, the internal actor system was configured under play.akka and the Akka plugin was configured under akka. The new combined actor system is configured under akka. There is no actor system configuration under play.akka anymore. However, several Play specific settings are still given under the play.akka prefix.

If you want to change how the actor system is configured, you can set play.akka.config = "my-akka", where my-akka is your chosen configuration prefix.

See the Java or Scala Akka page for more information.

§Thread pool configuration

Previously the two actor systems had slightly different thread pool configuration. Now that there is only one actor system, the configuration has been merged. We’ve also added a LIFO (stack-based) scheduling rule which should improve performance in most Play applications.

The following settings are the new defaults in Play 2.4. They’ve been shown to have good performance in our testing, but every application is different so you may need to tweak them or revert them to the Play 2.3 settings. You can do that by overriding any of these values in your application.conf. Here are the new settings:

akka {
  actor {
    default-dispatcher {
      fork-join-executor {
        parallelism-factor = 1.0
        parallelism-max = 24
        task-peeking-mode = LIFO
      }
    }
  }
}

In particular, you might want to try the default Akka settings:

akka {
  actor {
    default-dispatcher {
      fork-join-executor {
        parallelism-factor = 3.0
        parallelism-max = 64
        task-peeking-mode = FIFO
      }
    }
  }
}

See the thread pool configuration section for more information.

§HttpRequestHandler

The HttpRequestHandler that Play uses by default delegates to the legacy GlobalSettings methods. If you’re not using GlobalSettings in your application then you can increase performance slightly by changing the handler. You can do that by adding the following to your settings:

play.http.requestHandler = "play.http.DefaultHttpRequestHandler"

§Logging

Logging is now configured solely via logback configuration files.

§JDBC connection pool

The default JDBC connection pool is now provided by HikariCP, instead of BoneCP.

To switch back to BoneCP, you can set the play.db.pool property in application.conf:

play.db.pool = bonecp

The full range of configuration options available to the Play connection pools can be found in the Play JDBC reference.conf.

You may run into the following exception:

Caused by: java.sql.SQLException: JDBC4 Connection.isValid() method not supported, connection test query must be configured

This occurs if your JDBC-Drivers do not support Connection.isValid(). The fastest and recommended fix is to make sure you use the latest version of your JDBC-Driver. If upgrading to the latest version does not help, you may specify the connectionTestQuery in your application.conf like this

#specify a connectionTestQuery. Only do this if upgrading the JDBC-Driver does not help
db.default.hikaricp.connectionTestQuery="SELECT TRUE"

More information on this can be found on the HikariCP Github Page

§Body Parsers

The default body parser is now play.api.mvc.BodyParsers.parse.default. It is similar to anyContent parser, except that it only parses the bodies of PATCH, POST, and PUT requests. To parse bodies for requests of other methods, explicitly pass the anyContent parser to Action.

def foo = Action(play.api.mvc.BodyParsers.parse.anyContent) { request =>
  Ok(request.body.asText)
}

§Maximum body length

For both Scala and Java, there have been some small but important changes to the way the configured maximum body lengths are handled and applied.

A new property, play.http.parser.maxDiskBuffer, specifies the maximum length of any body that is parsed by a parser that may buffer to disk. This includes the raw body parser and the multipart/form-data parser. By default this is 10MB.

In the case of the multipart/form-data parser, the aggregate length of all of the text data parts is limited by the configured play.http.parser.maxMemoryBuffer value, which defaults to 100KB.

In all cases, when one of the max length parsing properties is exceeded, a 413 response is returned. This includes Java actions who have explicitly overridden the maxLength property on the BodyParser.Of annotation - previously it was up to the Java action to check the RequestBody.isMaxSizeExceeded flag if a custom max length was configured, this flag has now been deprecated.

Additionally, Java actions may now declare a BodyParser.Of.maxLength value that is greater than the configured max length.

§JSON API changes

The semantics of JSON lookups have changed slightly. JsUndefined has been removed from the JsValue type hierarchy and all lookups of the form jsv \ foo or jsv(bar) have been moved to JsLookup. They now return a JsLookupResult instead of a JsValue.

If you have code of the form

val v: JsValue = json \ "foo" \ "bar"

the following code is equivalent, if you know the property exists:

val v: JsValue = (json \ "foo" \ "bar").get

If you don’t know the property exists, we recommend using pattern matching or the methods on JsLookupResult to safely handle the JsUndefined case, e.g.

val vOpt: Option[JsValue] = (json \ "foo" \ "bar").toOption

§JsLookup

All JSON traversal methods have been moved to the JsLookup class, which is implicitly applied to all values of type JsValue or JsLookupResult. In addition to the apply, \, and \\ methods, the head, tail, and last methods have been added for JSON arrays. All methods except \\ return a JsLookupResult, a wrapper for JsValue that helps with handling undefined values.

The methods as[A], asOpt[A], validate[A] also exist on JsLookup, so code like the below should require no source changes:

val foo: Option[FooBar] = (json \ "foo" \ "bar").asOpt[FooBar]
val bar: JsResult[Baz] = (json \ "baz").validate[Baz]

As a result of these changes, your code can now assume that all values of type JsValue are serializable to JSON.

§Reading Options

OptionReads is no longer available by default in 2.4. If you have code of the form jsv.validate[Option[A]], you’ll need to rewrite it in one of the following ways:

  1. To map both JsNull and an undefined lookup result to None, use jsv.validateOpt[A]. This is typically what you want.
  2. To map all validation errors to None, use JsSuccess(jsv.asOpt[A]). This was what reading an Option did in 2.3.
  3. To map JsNull to None and validate the value if it exists, use jsv.validate(Reads.optionWithNull[A]). If the value does not exist the result will be a JsError.

When using the functional combinators to construct your Reads, you will most likely want to replace

(JsPath \ "property").read[Option[String]]

with

(JsPath \ "property").readNullable[String]

This does the same as (1) above. The same goes for Writes with JsPath.writeNullable and Format with JsPath.formatNullable. Note that readNullable will return a JsError if a node could not be found before the last node, instead of JsSuccess(None).

Finally, you may run into problems creating a serializer for a type that requires a Reads for Option such as Seq[Option[String]]. In this case, you just need to create a reads for Option[String] and make sure it is in scope:

implicit val optionStringReads: Reads[Option[String]] = Reads.optionWithNull[String]

§Testing changes

FakeRequest has been replaced by RequestBuilder.

The reverse ref router used in Java tests has been removed. Any call to Helpers.call that was passed a ref router can be replaced by a call to Helpers.route which takes either a standard reverse router reference or a RequestBuilder.

§Java TimeoutExceptions

If you use the Java API, the F.Promise class now throws unchecked F.PromiseTimeoutExceptions instead of Java’s checked TimeoutExceptions. The TimeoutExceptionss which were previously used were not properly declared with the throws keyword. Rather than changing the API to use the throws keyword, which would mean users would have to declare throws on their methods, the exception was changed to a new unchecked type instead. See #1227 for more information.

Old API New API Comments
TimeoutException F.PromiseTimeoutException

§WS client

WSRequestHolder has been renamed to WSRequest in Scala and Java. The previous WSRequest class has been removed out as it was only used internally to WS for OAuth functionality.

WS has upgraded from AsyncHttpClient 1.8.x to 1.9.x, which includes a number of breaking changes if using or configuring that library directly. Please see the AsyncHttpClient Migration Guide for more details. The upgrade to AsyncHttpClient 1.9.x enables Server Name Indication (SNI) in HTTPS – this solves a number of problems with HTTPS based CDNs such as Cloudflare which depend heavily on SNI.

Configuration settings for WS have changed:

Because of the AsyncHttpClient 1.9.x upgrade, several settings no longer have the same names that they did previously in the 1.8.x version AsyncHttpClientConfig.Builder. To reduce confusion, here is the map from WS settings to 1.9.x AsyncHttpClientConfig.Builder:

WS has changed the OAuth signature calculator from Signpost to AsyncHttpClient’s OAuthCalculator. Signpost is still used to retrieve the request token and access tokens. This should not require any application level changes, but is worth noting in case of unexpected OAuth failures.

Due to the recent spate of TLS vulnerabilities, there has been more activity to deprecate insecure HTTPS configurations. Per RFC 7465, RC4 cipher suites have added to the list of deprecated ciphers, and are not available by default. They may be explicitly enabled as cipher suites using the play.ws.ssl.enabledCiphers and play.ws.ssl.loose.allowWeakCiphers settings. Please also consider reviewing RFC 7525 for the IETF recommended configuration of TLS.

§Crypto APIs

Play 2.4’s AES encryption now uses initialization vectors to randomize each encryption. The Play encryption format has been changed to add support for initialization vectors.

The full name of the new AES transformation used by Play 2.4 is AES/CTR/NoPadding. The old transformation was AES/ECB/PKCS5Padding. The CTR mode is much more secure than the ECB mode. As before, you can override Play’s encryption transformation by setting the play.crypto.aes.transformation configuration option. In Play 2.4, any transformation supported by your JRE can be used, including transformations that use an initialization vector.

Play 2.4 uses a new encryption format, but it can read data encrypted by earlier versions of Play. However, earlier versions of Play will not be able to read data encrypted by Play 2.4. If your Play 2.4 application needs to produce data in the old format then you may want to copy the algorithm from the Play 2.3 Crypto code.

The table below shows the encryption formats supported by different versions of Play. Old format is used by older versions of Play. New format I is used by Play 2.4 if the configured cipher doesn’t use an initialization vector. New format II is used when an initialization vector is needed.

Format Encoding Play 2.3 Play 2.4
Old format hex(cipher(plaintext)) writes reads reads
New format I “1-” + base64(cipher(plaintext)) writes reads
New format II “2-” + base64(iv + cipher(plaintext, iv)) writes reads

Usage of the Java Crypto API remains the same even though the output is different:

import play.libs.Crypto;

String enc = Crypto.encryptAES(orig);
String dec = Crypto.decryptAES(enc);

Usage of the Scala Crypto API is also the same:

import play.api.libs.Crypto

val enc = Crypto.encryptAES(orig)
val dec = Crypto.decryptAES(enc)

§Anorm

The new Anorm version includes various fixes and improvements. Read here to find out more.

§HTTP server configuration

Advanced Netty configuration options, that is, options prefixed with http.netty.option, must now use the prefix play.server.netty.option instead.

§I18n

The configuration key to specify the languages that your application supports changed from application.langs to play.i18n.langs. Also, it is now a list instead of a comma separated string. Per instance:

play.i18n.langs = [ "en", "en-US", "fr" ]

§Scala

You now need to have an implicit Messages value instead of just Lang in order to use the i18n API. The Messages type aggregates a Lang and a MessagesApi.

This means that you should change your templates to take an implicit Messages parameter instead of Lang:

@(form: Form[Login])(implicit messages: Messages)
...

From you controllers you can get such an implicit Messages value by mixing the play.api.i18n.I18nSupport trait in your controller that gives you an implicit Messages value as long as there is a RequestHeader value in the implicit scope. The I18nSupport trait has an abstract member def messagesApi: MessagesApi so your code will typically look like the following:

import javax.inject.Inject
import play.api.i18n.{MessagesApi, I18nSupport}
import play.api.mvc.Controller

class MyController @Inject() (val messagesApi: MessagesApi)
  extends Controller with I18nSupport {

}

A simpler migration path is also supported if you want your controller to be still use static controller objects rather than injected classes with the I18nSupport trait. After modifying your templates to take an implicit Messages parameter, as described above, add the following import to your controllers:

import play.api.i18n.Messages.Implicits._

This import brings you an implicit Messages value as long as there are a Lang and an Application in the implicit scope (thankfully controllers already provide the Lang and you can get the currently running application by importing play.api.Play.current).

§Java

The API should be backward compatible with your code using Play 2.3 so there is no migration step. Nevertheless, note that you have to start your Play application before using the Java i18n API. That should always be the case when you run your project, however your test code may not always start your application. Please refer to the corresponding documentation page to know how to start your application before running your tests.

§Distribution

Previously, Play added all the resources to the the conf directory in the distribution, but didn’t add the conf directory to the classpath. Now Play adds the conf directory to the classpath by default.

This can be turned off by setting PlayKeys.externalizeResources := false, which will cause no conf directory to be created in the distribution, and it will not be on the classpath. The contents of the applications conf directory will still be on the classpath by virtue of the fact that it’s included in the applications jar file.

Please note that if you’re using the Java Persistence API (JPA) you will need to set this option to false in order to deploy your application. Otherwise you will get an error message: Entity X is not mapped. This is not required for local development.

§Changes in Debian Package creation

The sbt-native-packager has been upgraded. Due to this, the following adjustments might be necessary:
* The syntax of the /etc/default/$appname file has changed from being a simple list of command line parameters to being a shell script that gets sourced by the start/stop scripts, allowing you to set environment variables.
* The equivalent to the old syntax of the default file is an application.ini file in your archive’s conf folder.
* The default-file gets sourced by SystemV Init scripts only - Upstart ignores this file right now. To change your build to create SystemV compatible packages, add this to your build.sbt:

import com.typesafe.sbt.packager.archetypes.ServerLoader.{SystemV, Upstart}

serverLoading in Debian := SystemV

§Miscellaneous

§No more OrderedExecutionContext

The mysterious OrderedExecutionContext had been retained in Play for several versions in order to support legacy applications. It was rarely used and has now been removed. If you still need the OrderedExecutionContext for some reason, you can create your own implementation based on the Play 2.3 source. If you haven’t heard of this class, then there’s nothing you need to do.

§Subproject Assets

Any assets in sub projects are now by default placed into /lib/[subproject] to allow files with the same name in the root project / different subprojects without causing them to interfere with each other.

To get the asset routing to work correctly in your app, you’ll need to change:

GET     /assets/*file               controllers.myModule.Assets.at(path="/public", file)

to this

GET     /assets/*file               controllers.myModule.Assets.at(path="/public/lib/myModule", file)

Next: Removing `GlobalSettings`


Found an error in this documentation? The source code for this page can be found here. After reading the documentation guidelines, please feel free to contribute a pull request.