Nothing
#' Deploy an Application
#'
#' Deploy a [shiny][shiny::shiny-package] application, an
#' [RMarkdown][rmarkdown::rmarkdown-package] document, a plumber API, or HTML
#' content to a server.
#'
#' ## Deployment records
#'
#' When deploying an app, `deployApp()` will save a deployment record that
#' makes it easy to update the app on server from your local source code. This
#' generally means that you need to only need to supply important arguments
#' (e.g. `appName`, `appTitle`, `server`/`account`) on the first deploy, and
#' rsconnect will reuse the same settings on subsequent deploys.
#'
#' The metadata needs to make this work is stored in `{appDir}/rsconnect/`.
#' You should generally check these files into version control to ensure that
#' future you and other collaborators will publish to the same location.
#'
#' If you have lost this directory, all is not lost, as `deployApp()` will
#' attempt to rediscover existing deployments. This is easiest if you are
#' updating an app that you created, as you can just supply the `appName`
#' (and `server`/`account` if you have multiple accounts) and `deployApp()`
#' will find the existing application account. If you need to update an app
#' that was created by someone else (that you have write permission) for, you'll
#' instead need to supply the `appId`.
#'
#' @param appDir A directory containing an application (e.g. a Shiny app
#' or plumber API). Defaults to the current directory.
#' @param appFiles,appFileManifest Use `appFiles` to specify a
#' character vector of files to bundle in the app or `appFileManifest`
#' to provide a path to a file containing a list of such files. If neither
#' are supplied, will bundle all files in `appDir`, apart from standard
#' exclusions and files listed in a `.rscignore` file. See
#' [listDeploymentFiles()] for more details.
#' @param appPrimaryDoc If the application contains more than one document, this
#' parameter indicates the primary one, as a path relative to `appDir`. Can be
#' `NULL`, in which case the primary document is inferred from the contents
#' being deployed.
#' @param appSourceDoc `r lifecycle::badge("deprecated")` Please use
#' `recordDir` instead.
#' @param appName Application name, a string consisting of letters, numbers,
#' `_` and `-`. The application name is used to identify applications on a
#' server, so must be unique.
#'
#' If not specified, the first deployment will be automatically it from the
#' `appDir` for directory and website, and from the `appPrimaryDoc` for
#' document. On subsequent deploys, it will use the previously stored value.
#' @param appTitle Free-form descriptive title of application. Optional; if
#' supplied, will often be displayed in favor of the name. If ommitted,
#' on second and subsequent deploys, the title will be unchanged.
#' @param envVars A character vector giving the names of environment variables
#' whose values should be synchronised with the server (currently supported by
#' Connect only). The values of the environment variables are sent over an
#' encrypted connection and are not stored in the bundle, making this a safe
#' way to send private data to Connect.
#'
#' The names (not values) are stored in the deployment record so that future
#' deployments will automatically update their values. Other environment
#' variables on the server will not be affected. This means that removing an
#' environment variable from `envVars` will leave it unchanged on the server.
#' To remove it, either delete it using the Connect UI, or temporarily unset
#' it (with `Sys.unsetenv()` or similar) then re-deploy.
#'
#' Environment variables are set prior to deployment so that your code
#' can use them and the first deployment can still succeed. Note that means
#' that if the deployment fails, the values will still be updated.
#' @param appId Use this to deploy to an exact known application, ignoring all
#' existing deployment records and `appName`.
#'
#' You can use this to update an existing application that is missing a
#' deployment record. If you're re-deploying an application that you
#' created it's generally easier to use `appName`; `appId` is best reserved
#' for re-deploying apps created by someone else.
#'
#' You can find the `appId` in the following places:
#' * On shinyapps.io, it's the `id` listed on the applications page.
#' * For Posit Connect, it's `guid` from the info tab on the content page.
#' @param appMode Optional; the type of content being deployed.
#' Provide this option when the inferred type of content is incorrect. This
#' can happen, for example, when static HTML content includes a downloadable
#' Shiny application `app.R`. Accepted values include `"shiny"`, `"api"`,
#' `"rmd-static"`, `"rmd-shiny"`, `"quarto-static"`, `"quarto-shiny"`, and
#' `"static"`. The Posit Connect API Reference contains a full set of
#' available values. Not all servers support all types of content.
#' @param contentCategory Optional; classifies the kind of content being
#' deployed (e.g. `"plot"` or `"site"`).
#' @param account,server Uniquely identify a remote server with either your
#' user `account`, the `server` name, or both. If neither are supplied, and
#' there are multiple options, you'll be prompted to pick one.
#'
#' Use [accounts()] to see the full list of available options.
#' @param upload If `TRUE` (the default) then the application is uploaded from
#' the local system prior to deployment. If `FALSE` then it is re-deployed
#' using the last version that was uploaded. `FALSE` is only supported on
#' shinyapps.io; `TRUE` is required on Posit Connect.
#' @param recordDir Directory where deployment record is written. The default,
#' `NULL`, uses `appDir`, since this is usually where you want the deployment
#' data to be stored. This argument is typically only needed when deploying
#' a directory of static files since you want to store the record with the
#' code that generated those files, not the files themselves.
#' @param launch.browser If true, the system's default web browser will be
#' launched automatically after the app is started. Defaults to `TRUE` in
#' interactive sessions only. If a function is passed, it will be called
#' after the app is started, with the app URL as a paramter.
#' @param on.failure Function to be called if the deployment fails. If a
#' deployment log URL is available, it's passed as a parameter.
#' @param logLevel One of `"quiet"`, `"normal"` or `"verbose"`; indicates how
#' much logging to the console is to be performed. At `"quiet"` reports no
#' information; at `"verbose"`, a full diagnostic log is captured.
#' @param lint Lint the project before initiating deployment, to identify
#' potentially problematic code?
#' @param metadata Additional metadata fields to save with the deployment
#' record. These fields will be returned on subsequent calls to
#' [deployments()].
#'
#' Multi-value fields are recorded as comma-separated values and returned in
#' that form. Custom value serialization is the responsibility of the caller.
#' @param forceUpdate What should happen if there's no deployment record for
#' the app, but there's an app with the same name on the server? If `TRUE`,
#' will always update the previously-deployed app. If `FALSE`, will ask
#' the user what to do, or fail if not in an interactive context.
#'
#' Defaults to `TRUE` when called automatically by the IDE, and `FALSE`
#' otherwise. You can override the default by setting option
#' `rsconnect.force.update.apps`.
#' @param python Full path to a python binary for use by `reticulate`.
#' Required if `reticulate` is a dependency of the app being deployed.
#' If python = NULL, and RETICULATE_PYTHON or RETICULATE_PYTHON_FALLBACK is
#' set in the environment, its value will be used. The specified python binary
#' will be invoked to determine its version and to list the python packages
#' installed in the environment.
#' @param forceGeneratePythonEnvironment Optional. If an existing
#' `requirements.txt` file is found, it will be overwritten when this argument
#' is `TRUE`.
#' @param quarto Should the deployed content be built by quarto?
#' (`TRUE`, `FALSE`, or `NA`). The default, `NA`, will use quarto if
#' there are `.qmd` files in the bundle, or if there is a
#' `_quarto.yml` and `.Rmd` files.
#'
#' (This option is ignored and quarto will always be used if the
#' `metadata` contains `quarto_version` and `quarto_engines` fields.)
#' @param appVisibility One of `NULL`, `"private"`, or `"public"`; the
#' visibility of the deployment. When `NULL`, no change to visibility is
#' made. Currently has an effect only on deployments to shinyapps.io.
#' @param image Optional. The name of the image to use when building and
#' executing this content. If none is provided, Posit Connect will
#' attempt to choose an image based on the content requirements. You can
#' override the default by setting the environment variable `RSCONNECT_IMAGE`.
#' @param envManagement Optional. Should Posit Connect install R and Python
#' packages for this content? (`TRUE`, `FALSE`, or `NULL`).
#' The default, `NULL`, will not write any values to the bundle manifest,
#' and Connect will fall back to the application default environment
#' management strategy, or the server default if no application default
#' is defined.
#'
#' (This option is a shorthand flag which overwrites the values of both
#' `envManagementR` and `envManagementPy`.)
#' @param envManagementR Optional. Should Posit Connect install R packages
#' for this content? (`TRUE`, `FALSE`, or `NULL`). The default, `NULL`, will
#' not write any values to the bundle manifest, and Connect will fall back to
#' the application default R environment management strategy, or the server
#' default if no application default is defined.
#'
#' (This option is ignored when `envManagement` is non-`NULL`.)
#' @param envManagementPy Optional. Should Posit Connect install Python packages
#' for this content? (`TRUE`, `FALSE`, or `NULL`). The default, `NULL`, will
#' not write any values to the bundle manifest, and Connect will fall back to
#' the application default Python environment management strategy, or the
#' server default if no application default is defined.
#'
#' (This option is ignored when `envManagement` is non-`NULL`.)
#' @param space Optional. For Posit Cloud, the id of the space where the content
#' should be deployed. If none is provided, content will be deployed to the
#' deploying user's workspace or deployed to the same space in case of
#' redeploy.
#' @examples
#' \dontrun{
#'
#' # deploy the application in the current working dir
#' deployApp()
#'
#' # deploy an application in another directory
#' deployApp("~/projects/shiny/app1")
#'
#' # deploy using an alternative application name and title
#' deployApp("~/projects/shiny/app1", appName = "myapp",
#' appTitle = "My Application")
#'
#' # deploy specifying an explicit account name, then
#' # redeploy with no arguments (will automatically use
#' # the previously specified account)
#' deployApp(account = "jsmith")
#' deployApp()
#'
#' # deploy but don't launch a browser when completed
#' deployApp(launch.browser = FALSE)
#'
#' # deploy a Quarto website, using the quarto package to
#' # find the Quarto binary
#' deployApp("~/projects/quarto/site1")
#'
#' # deploy application with environment variables
#' # (e.g., `SECRET_PASSWORD=XYZ` is set via an ~/.Renviron file)
#' rsconnect::deployApp(envVars = c("SECRET_PASSWORD"))
#' }
#' @seealso [applications()], [terminateApp()], and [restartApp()]
#' @family Deployment functions
#' @export
deployApp <- function(appDir = getwd(),
appFiles = NULL,
appFileManifest = NULL,
appPrimaryDoc = NULL,
appSourceDoc = NULL,
appName = NULL,
appTitle = NULL,
envVars = NULL,
appId = NULL,
appMode = NULL,
contentCategory = NULL,
account = NULL,
server = NULL,
upload = TRUE,
recordDir = NULL,
launch.browser = getOption("rsconnect.launch.browser",
is_interactive()),
on.failure = NULL,
logLevel = c("normal", "quiet", "verbose"),
lint = TRUE,
metadata = list(),
forceUpdate = NULL,
python = NULL,
forceGeneratePythonEnvironment = FALSE,
quarto = NA,
appVisibility = NULL,
image = NULL,
envManagement = NULL,
envManagementR = NULL,
envManagementPy = NULL,
space = NULL
) {
check_string(appDir)
if (isStaticFile(appDir) && !dirExists(appDir)) {
lifecycle::deprecate_warn(
when = "1.0.0",
what = "deployApp(appDir = 'takes a directory, not a document,')",
with = "deployDoc()"
)
return(deployDoc(
appDir,
appName = appName,
appTitle = appTitle,
account = account,
server = server,
upload = upload,
recordDir = recordDir,
launch.browser = launch.browser,
logLevel = logLevel,
lint = lint
))
}
check_directory(appDir)
appDir <- normalizePath(appDir)
check_string(appName, allow_null = TRUE)
if (!is.null(appPrimaryDoc)) {
check_string(appPrimaryDoc)
if (!file.exists(file.path(appDir, appPrimaryDoc))) {
cli::cli_abort("`appPrimaryDoc` not found inside `appDir`")
}
}
if (!is.null(appSourceDoc)) {
# Used by IDE so can't deprecate
recordDir <- appSourceDoc
}
# set up logging helpers
logLevel <- match.arg(logLevel)
quiet <- identical(logLevel, "quiet")
verbose <- identical(logLevel, "verbose")
logger <- verboseLogger(verbose)
displayStatus <- displayStatus(quiet)
# run startup scripts to pick up any user options and establish pre/post deploy hooks
runStartupScripts(appDir, quiet = quiet, verbose = verbose)
# at verbose log level, turn on all tracing options implicitly for the
# duration of the call
if (verbose) {
old_verbose <- options(
rsconnect.http.trace = TRUE,
rsconnect.http.trace.json = TRUE,
rsconnect.error.trace = TRUE
)
defer(options(old_verbose))
}
# install error handler if requested
if (isTRUE(getOption("rsconnect.error.trace"))) {
old_error <- options(error = function(e) {
cat("----- Deployment error -----\n")
cat(geterrmessage(), "\n")
cat("----- Error stack trace -----\n")
traceback(x = sys.calls(), max.lines = 3)
})
defer(options(old_error))
}
# at verbose log level, generate header
if (verbose) {
logger("Deployment log started")
cat("Deploy command:", "\n", deparse(sys.call(1)), "\n\n")
cat("Session information: \n")
print(utils::sessionInfo())
}
# invoke pre-deploy hook if we have one
runDeploymentHook(appDir, "rsconnect.pre.deploy", verbose = verbose)
appFiles <- listDeploymentFiles(appDir, appFiles, appFileManifest)
if (isTRUE(lint)) {
lintResults <- lint(appDir, appFiles, appPrimaryDoc)
showLintResults(appDir, lintResults)
}
if (!quiet) {
cli::cli_rule("Preparing for deployment")
}
forceUpdate <- forceUpdate %||% getOption("rsconnect.force.update.apps") %||% fromIDE()
# determine the target deployment record and deploying account
recordPath <- findRecordPath(appDir, recordDir, appPrimaryDoc)
target <- findDeploymentTarget(
recordPath = recordPath,
appId = appId,
appName = appName,
appTitle = appTitle,
envVars = envVars,
account = account,
server = server,
forceUpdate = forceUpdate
)
accountDetails <- target$accountDetails
deployment <- target$deployment
if (is.null(deployment$appId)) {
dest <- accountLabel(accountDetails$name, accountDetails$server)
taskComplete(quiet, "Deploying {.val {deployment$name}} using {.val {dest}}")
} else {
dest <- accountLabel(accountDetails$name, accountDetails$server)
taskComplete(quiet, "Re-deploying {.val {deployment$name}} using {.val {dest}}")
}
# Run checks prior to first saveDeployment() to avoid errors that will always
# prevent a successful upload from generating a partial deployment
if (!isCloudServer(accountDetails$server) && identical(upload, FALSE)) {
# it is not possible to deploy to Connect without uploading
stop("Posit Connect does not support deploying without uploading. ",
"Specify upload=TRUE to upload and re-deploy your application.")
}
if (!isConnectServer(accountDetails$server) && length(envVars) > 1) {
cli::cli_abort("{.arg envVars} only supported for Posit Connect servers")
}
client <- clientForAccount(accountDetails)
if (verbose) {
showCookies(serverInfo(accountDetails$server)$url)
}
isShinyappsServer <- isShinyappsServer(accountDetails$server)
logger("Inferring App mode and parameters")
appMetadata <- appMetadata(
appDir = appDir,
appFiles = appFiles,
appPrimaryDoc = appPrimaryDoc,
quarto = quarto,
appMode = appMode,
contentCategory = contentCategory,
isShinyappsServer = isShinyappsServer,
metadata = metadata
)
if (is.null(deployment$appId)) {
taskStart(quiet, "Creating application on server...")
application <- client$createApplication(
deployment$name,
deployment$title,
"shiny",
accountDetails$accountId,
appMetadata$appMode,
contentCategory,
space
)
taskComplete(quiet, "Created application with id {.val {application$id}}")
} else {
taskStart(quiet, "Looking up application with id {.val {deployment$appId}}...")
application <- tryCatch(
{
application <- client$getApplication(deployment$appId, deployment$version)
taskComplete(quiet, "Found application {.url {application$url}}")
if (identical(application$type, "static")) {
application$application_id <- client$createRevision(application, contentCategory)
}
application
},
rsconnect_http_404 = function(err) {
application <- applicationDeleted(client, deployment, recordPath, appMetadata)
taskComplete(quiet, "Created application with id {.val {application$id}}")
application
}
)
}
saveDeployment(
recordPath,
deployment = deployment,
application = application,
metadata = metadata
)
# Change _visibility_ & set env vars before uploading contents
if (needsVisibilityChange(accountDetails$server, application, appVisibility)) {
taskStart(quiet, "Setting visibility to {appVisibility}...")
client$setApplicationProperty(
application$id,
"application.visibility",
appVisibility
)
taskComplete(quiet, "Visibility updated")
}
if (length(deployment$envVars) > 0) {
taskStart(quiet, "Updating environment variables {envVars}...")
client$setEnvVars(application$guid, deployment$envVars)
taskComplete(quiet, "Environment variables updated")
}
if (upload) {
python <- getPythonForTarget(python, accountDetails)
pythonConfig <- pythonConfigurator(python, forceGeneratePythonEnvironment)
taskStart(quiet, "Bundling {length(appFiles)} file{?s}: {.file {appFiles}}")
bundlePath <- bundleApp(
appName = deployment$name,
appDir = appDir,
appFiles = appFiles,
appMetadata = appMetadata,
quiet = quiet,
verbose = verbose,
pythonConfig = pythonConfig,
image = image,
envManagement = envManagement,
envManagementR = envManagementR,
envManagementPy = envManagementPy
)
size <- format(file_size(bundlePath), big.mark = ",")
taskComplete(quiet, "Created {size}b bundle")
# create, and upload the bundle
taskStart(quiet, "Uploading bundle...")
if (isCloudServer(accountDetails$server)) {
bundle <- uploadCloudBundle(client, application$application_id, bundlePath)
} else {
bundle <- client$uploadApplication(application$id, bundlePath)
}
taskComplete(quiet, "Uploaded bundle with id {.val {bundle$id}}")
saveDeployment(
recordPath,
deployment = deployment,
application = application,
bundleId = bundle$id,
metadata = metadata
)
} else {
# redeploy current bundle
bundle <- application$deployment$bundle
}
if (!quiet) {
cli::cli_rule("Deploying to server")
}
task <- client$deployApplication(application, bundle$id, space)
taskId <- if (is.null(task$task_id)) task$id else task$task_id
# wait for the deployment to complete (will raise an error if it can't)
response <- client$waitForTask(taskId, quiet)
if (!quiet) {
cli::cli_rule("Deployment complete")
}
# wait 1/10th of a second for any queued output get picked by RStudio
# before emitting the final status, to ensure it's the last line the user sees
Sys.sleep(0.10)
deploymentSucceeded <- is.null(response$code) || response$code == 0
if (!quiet) {
if (deploymentSucceeded) {
cli::cli_alert_success("Successfully deployed to {.url {application$url}}")
} else {
cli::cli_alert_danger("Deployment failed with error: {response$error}")
}
}
if (!quiet)
openURL(client, application, launch.browser, on.failure, deploymentSucceeded)
# invoke post-deploy hook if we have one
if (deploymentSucceeded) {
runDeploymentHook(appDir, "rsconnect.post.deploy", verbose = verbose)
}
logger("Deployment log finished")
invisible(deploymentSucceeded)
}
taskStart <- function(quiet, message, .envir = caller_env()) {
if (quiet) return()
cli::cli_alert_info(message, .envir = .envir)
}
taskComplete <- function(quiet, message, .envir = caller_env()) {
if (quiet) return()
cli::cli_alert_success(message, .envir = .envir)
}
findRecordPath <- function(appDir,
recordDir = NULL,
appPrimaryDoc = NULL) {
if (!is.null(recordDir)) {
recordDir
} else if (!is.null(appPrimaryDoc)) {
file.path(appDir, appPrimaryDoc)
} else {
appDir
}
}
# Need to set _before_ deploy
needsVisibilityChange <- function(server, application, appVisibility = NULL) {
if (is.null(appVisibility)) {
return(FALSE)
}
if (isConnectServer(server)) {
# Defaults to private visibility
return(FALSE)
}
if (!isShinyappsServer(server)) {
cli::cli_abort(c(
"Can't change cloud app visiblity from {.fun deployApp}.",
i = "Please change on posit.cloud instead."
))
}
cur <- application$deployment$properties$application.visibility
if (is.null(cur)) {
cur <- "public"
}
cur != appVisibility
}
runDeploymentHook <- function(appDir, option, verbose = FALSE) {
hook <- getOption(option)
if (!is.function(hook)) {
return()
}
if (verbose) {
cat("Invoking `", option, "` hook\n", sep = "")
}
hook(appDir)
}
applicationDeleted <- function(client, deployment, recordPath, appMetadata) {
header <- "Failed to find existing application on server; it's probably been deleted."
not_interactive <- c(
i = "Use {.fn forgetDeployment} to remove outdated record and try again.",
i = "Or use {.fn applications} to see other applications you have on the server."
)
prompt <- "What do you want to do?"
choices <- c(
"Give up and try again later",
"Delete existing deployment & create a new app"
)
cli_menu(header, prompt, choices, not_interactive = not_interactive, quit = 1)
# Must be option 2
path <- deploymentConfigFile(
recordPath,
deployment$name,
deployment$account,
deployment$server
)
unlink(path)
accountDetails <- accountInfo(deployment$account, deployment$server)
client$createApplication(
deployment$name,
deployment$title,
"shiny",
accountDetails$accountId,
appMetadata$appMode
)
}
# Does almost exactly the same work as writeManifest(), but called within
# deployApp() instead of being exposed to the user. Returns the path to the
# bundle directory, whereas writeManifest() returns nothing and deletes the
# bundle directory after writing the manifest.
bundleApp <- function(appName,
appDir,
appFiles,
appMetadata,
verbose = FALSE,
quiet = FALSE,
pythonConfig = NULL,
image = NULL,
envManagement = NULL,
envManagementR = NULL,
envManagementPy = NULL) {
logger <- verboseLogger(verbose)
# get application users (for non-document deployments)
users <- NULL
if (is.null(appMetadata$appPrimaryDoc)) {
users <- suppressWarnings(authorizedUsers(appDir))
}
# copy files to bundle dir to stage
logger("Bundling app dir")
bundleDir <- bundleAppDir(
appDir = appDir,
appFiles = appFiles,
appPrimaryDoc = appMetadata$appPrimaryDoc,
appMode = appMetadata$appMode
)
defer(unlink(bundleDir, recursive = TRUE))
# generate the manifest and write it into the bundle dir
logger("Generate manifest.json")
manifest <- createAppManifest(
appDir = bundleDir,
appMetadata = appMetadata,
users = users,
pythonConfig = pythonConfig,
retainPackratDirectory = TRUE,
image = image,
envManagement = envManagement,
envManagementR = envManagementR,
envManagementPy = envManagementPy,
verbose = verbose,
quiet = quiet
)
manifestJson <- toJSON(manifest)
manifestPath <- file.path(bundleDir, "manifest.json")
writeLines(manifestJson, manifestPath, useBytes = TRUE)
# create the bundle and return its path
logger("Compressing the bundle")
bundlePath <- tempfile("rsconnect-bundle", fileext = ".tar.gz")
writeBundle(bundleDir, bundlePath)
bundlePath
}
validURL <- function(url) {
!(is.null(url) || url == "")
}
openURL <- function(client, application, launch.browser, on.failure, deploymentSucceeded) {
# function to browse to a URL using user-supplied browser (config or final)
showURL <- function(url) {
if (isTRUE(launch.browser))
utils::browseURL(url)
else if (is.function(launch.browser))
launch.browser(url)
}
# Check to see if we should open config url or app url
if (!is.null(client$configureApplication)) {
config <- client$configureApplication(application$id)
url <- config$config_url
if (!deploymentSucceeded && validURL(config$logs_url)) {
# With 1.5.5+, Connect application configuration includes
# a logs URL to be shown on unsuccessful deployment.
url <- config$logs_url
}
if (validURL(url)) {
# Connect should always end up here, even on deployment failures
if (deploymentSucceeded) {
showURL(url)
} else if (is.function(on.failure)) {
on.failure(url)
}
}
} else if (deploymentSucceeded) {
# shinyapps.io should land here if things succeeded
showURL(application$url)
} else if (is.function(on.failure)) {
on.failure(NULL)
}
# or open no url if things failed
}
runStartupScripts <- function(appDir, quiet = FALSE, verbose = FALSE) {
scripts <- c(
# the site-wide startup script
file.path(R.home("etc"), "rsconnect.site"),
# the user startup script
path.expand("~/.rsconnect_profile"),
# a startup script specific to this application
file.path(appDir, ".rsconnect_profile")
)
scripts <- scripts[file.exists(scripts)]
# iterate over the startup scripts
for (script in scripts) {
taskStart(quiet, "Running {script}")
env <- new_environment(parent = globalenv())
source(script, verbose = verbose, local = env)
}
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.