rstudio / flexdashboard

Easy interactive dashboards for R
https://pkgs.rstudio.com/flexdashboard/
Other
813 stars 300 forks source link

Compatibility with Plausible analytics #448

Closed gvelasq closed 8 months ago

gvelasq commented 8 months ago

Thank you for the great package; flexdashboard powers an R-Ladies YouTube dashboard that @ivelasq and I created and featured in her blog. We recently added Plausible analytics to the dashboard and noticed that the answer to a related issue provided here doesn't work for Plausible:

---
title: ""
output: 
  flexdashboard::flex_dashboard:
    includes:
      in_header: plausible.html
---

The reason is because Plausible requires their analytics script to be added to the header verbatim, however during the rmarkdown::render() process a malformed script is added to the header when using the YAML above.

The reprexes below show the following:

  1. Current flexdashboard behavior with includes: followed by in_header:: a malformed script is added to the header.
  2. Current flexdashboard behavior with pandoc_args: followed by include-in-header:: a malformed script is added to the body. It's unclear to me why this adds the script so far down the page.
  3. Desired flexdashboard behavior requested in this issue: I added the Plausible script verbatim by modifying the rendered dashboard .html using the shell command sed. This could be implemented in flexdashboard in a way that retains the old behavior when files are passed to includes:, by adding an option for direct text input that interpolates text verbatim into the rendered .html (see 4. below).
  4. pkgdown has already implemented the desired behavior in https://github.com/r-lib/pkgdown/pull/1852 using the whisker R package and mustache templates. In this case, only direct text input can be passed to the pkgdown template (files cannot be passed).
# 0. setup ----
library(flexdashboard)
library(fs)
library(glue)
library(pkgdown)
suppressPackageStartupMessages(library(rmarkdown))
library(usethis)

find_script <- function(path) {
  grep("plausible", readLines(path))
}

# 1. flexdashboard: current behavior ----
# includes/in_header adds a malformed script in the header
dom <- "ivelasq.github.io/rladies-video-feed"
src <- "https://plausible.io/js/script.js"
script <- glue('<script defer data-domain="{dom}" src="{src}"></script>')
cat(script, file = "./plausible.html", sep = "\n")
yaml1_part1 <- '---\ntitle: "reprex1"\noutput:\n  flexdashboard::flex_dashboard:'
yaml1_part2 <- '    includes:\n      in_header: plausible.html\n---'
cat(yaml1_part1, yaml1_part2, file = "./reprex1.Rmd", sep = "\n")
render("./reprex1.Rmd")
#> processing file: reprex1.Rmd
#> output file: reprex1.knit.md
#> /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/pandoc +RTS -K512m -RTS reprex1.knit.md --to html4 --from markdown+autolink_bare_uris+tex_math_single_backslash --output reprex1.html --lua-filter /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/rmarkdown/rmarkdown/lua/pagebreak.lua --lua-filter /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/rmarkdown/rmarkdown/lua/latex-div.lua --embed-resources --standalone --variable bs3=TRUE --standalone --section-divs --template /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/flexdashboard/www/flex_dashboard/default.html --variable theme=cosmo --mathjax --variable 'mathjax-url=https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML' --include-in-header /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/rmarkdown-str1ae46a6cd1c5.html --highlight-style pygments --include-in-header plausible.html --include-in-header /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/file1ae4e401319html --include-before-body /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/file1ae410a5d59e.html --include-after-body /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/file1ae4776b809b.html
#> 
#> Output created: reprex1.html
loc1 <- "./reprex1.html"
find_script(loc1) # line number in reprex1.html
#> [1] 137
out1 <- scan(loc1, character(), n = 1, sep = "\n", skip = find_script(loc1) - 1)
out1
#> [1] "<script>!function(){\"use strict\";var a=window.location,r=window.document,o=r.currentScript,l=o.getAttribute(\"data-api\")||new URL(o.src).origin+\"/api/event\";function s(t,e){t&&console.warn(\"Ignoring Event: \"+t),e&&e.callback&&e.callback()}function t(t,e){if(/^localhost$|^127(\\.[0-9]+){0,2}\\.[0-9]+$|^\\[::1?\\]$/.test(a.hostname)||\"file:\"===a.protocol)return s(\"localhost\",e);if(window._phantom||window.__nightmare||window.navigator.webdriver||window.Cypress)return s(null,e);try{if(\"true\"===window.localStorage.plausible_ignore)return s(\"localStorage flag\",e)}catch(t){}var n={},i=(n.n=t,n.u=a.href,n.d=o.getAttribute(\"data-domain\"),n.r=r.referrer||null,e&&e.meta&&(n.m=JSON.stringify(e.meta)),e&&e.props&&(n.p=e.props),new XMLHttpRequest);i.open(\"POST\",l,!0),i.setRequestHeader(\"Content-Type\",\"text/plain\"),i.send(JSON.stringify(n)),i.onreadystatechange=function(){4===i.readyState&&e&&e.callback&&e.callback()}}var e=window.plausible&&window.plausible.q||[];window.plausible=t;for(var n,i=0;i<e.length;i++)t.apply(this,e[i]);function p(){n!==a.pathname&&(n=a.pathname,t(\"pageview\"))}var c,w=window.history;w.pushState&&(c=w.pushState,w.pushState=function(){c.apply(this,arguments),p()},window.addEventListener(\"popstate\",p)),\"prerender\"===r.visibilityState?r.addEventListener(\"visibilitychange\",function(){n||\"visible\"!==r.visibilityState||p()}):p()}();</script>"

# 2. flexdashboard: current behavior ----
# pandoc_args/include-in-header adds a malformed script in the body
yaml2_part1 <- '---\ntitle: "reprex2"\noutput:\n  flexdashboard::flex_dashboard:'
yaml2_part2 <- '    pandoc_args:\n      include-in-header: plausible.html\n---'
cat(yaml2_part1, yaml2_part2, file = "./reprex2.Rmd", sep = "\n")
render("./reprex2.Rmd")
#> processing file: reprex2.Rmd
#> output file: reprex2.knit.md
#> /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/pandoc +RTS -K512m -RTS reprex2.knit.md --to html4 --from markdown+autolink_bare_uris+tex_math_single_backslash --output reprex2.html --lua-filter /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/rmarkdown/rmarkdown/lua/pagebreak.lua --lua-filter /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/rmarkdown/rmarkdown/lua/latex-div.lua --embed-resources --standalone plausible.html --variable bs3=TRUE --standalone --section-divs --template /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/flexdashboard/www/flex_dashboard/default.html --variable theme=cosmo --mathjax --variable 'mathjax-url=https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML' --include-in-header /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/rmarkdown-str1ae432c87d5d.html --highlight-style pygments --include-in-header /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/file1ae489e7eb7html --include-before-body /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/file1ae45d9124cc.html --include-after-body /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/file1ae46406fd11.html
#> 
#> Output created: reprex2.html
loc2 <- "./reprex2.html"
find_script(loc2) # line number in reprex2.html
#> [1] 1933
out2 <- scan(loc2, character(), n = 1, sep = "\n", skip = find_script(loc2) - 1)
out2
#> [1] "<script>!function(){\"use strict\";var a=window.location,r=window.document,o=r.currentScript,l=o.getAttribute(\"data-api\")||new URL(o.src).origin+\"/api/event\";function s(t,e){t&&console.warn(\"Ignoring Event: \"+t),e&&e.callback&&e.callback()}function t(t,e){if(/^localhost$|^127(\\.[0-9]+){0,2}\\.[0-9]+$|^\\[::1?\\]$/.test(a.hostname)||\"file:\"===a.protocol)return s(\"localhost\",e);if(window._phantom||window.__nightmare||window.navigator.webdriver||window.Cypress)return s(null,e);try{if(\"true\"===window.localStorage.plausible_ignore)return s(\"localStorage flag\",e)}catch(t){}var n={},i=(n.n=t,n.u=a.href,n.d=o.getAttribute(\"data-domain\"),n.r=r.referrer||null,e&&e.meta&&(n.m=JSON.stringify(e.meta)),e&&e.props&&(n.p=e.props),new XMLHttpRequest);i.open(\"POST\",l,!0),i.setRequestHeader(\"Content-Type\",\"text/plain\"),i.send(JSON.stringify(n)),i.onreadystatechange=function(){4===i.readyState&&e&&e.callback&&e.callback()}}var e=window.plausible&&window.plausible.q||[];window.plausible=t;for(var n,i=0;i<e.length;i++)t.apply(this,e[i]);function p(){n!==a.pathname&&(n=a.pathname,t(\"pageview\"))}var c,w=window.history;w.pushState&&(c=w.pushState,w.pushState=function(){c.apply(this,arguments),p()},window.addEventListener(\"popstate\",p)),\"prerender\"===r.visibilityState?r.addEventListener(\"visibilitychange\",function(){n||\"visible\"!==r.visibilityState||p()}):p()}();</script>"

# 3. flexdashboard: desired behavior ----
# command line modification after rendering adds the intact script in the header
yaml3 <- '---\ntitle: "reprex3"\noutput:\n  flexdashboard::flex_dashboard\n---'
cat(yaml3, file = "./reprex3.Rmd", sep = "\n")
render("./reprex3.Rmd")
#> processing file: reprex3.Rmd
#> output file: reprex3.knit.md
#> /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/pandoc +RTS -K512m -RTS reprex3.knit.md --to html4 --from markdown+autolink_bare_uris+tex_math_single_backslash --output reprex3.html --lua-filter /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/rmarkdown/rmarkdown/lua/pagebreak.lua --lua-filter /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/rmarkdown/rmarkdown/lua/latex-div.lua --embed-resources --standalone --variable bs3=TRUE --standalone --section-divs --template /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/flexdashboard/www/flex_dashboard/default.html --variable theme=cosmo --mathjax --variable 'mathjax-url=https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML' --include-in-header /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/rmarkdown-str1ae46d09a65.html --highlight-style pygments --include-in-header /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/file1ae469486061html --include-before-body /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/file1ae4eafb647.html --include-after-body /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//Rtmp3Uuyuh/file1ae431e7f6d9.html
#> 
#> Output created: reprex3.html
system('sed -i "" "s/<\\/head>/<script defer data-domain=\\"ivelasq.github.io\\/rladies-video-feed\\" src=\\"https:\\/\\/plausible.io\\/js\\/script.js\\"><\\/script>\\n&/" reprex3.html')
loc3 <- "./reprex3.html"
find_script(loc3) # line number in reprex3.html
#> [1] 219
out3 <- scan(loc3, character(), n = 1, sep = "\n", skip = find_script(loc3) - 1)
out3
#> [1] "<script defer data-domain=\"ivelasq.github.io/rladies-video-feed\" src=\"https://plausible.io/js/script.js\"></script>"

# 4. pkgdown: desired behavior ----
# includes/in_header direct text input adds the intact script in the header
create_package(".", check_name = FALSE, open = FALSE)
#> ✔ Setting active project to
#> '/private/var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T/RtmpCUI9FE/reprex-1a0b62bd36df-fishy-coral'
#> ✔ Creating 'R/'
#> ✔ Writing 'DESCRIPTION'
#> Package: reprex-1a0b62bd36df-fishy-coral
#> Title: What the Package Does (One Line, Title Case)
#> Version: 0.0.0.9000
#> Authors@R (parsed):
#>     * First Last <first.last@example.com> [aut, cre] (YOUR-ORCID-ID)
#> Description: What the package does (one paragraph).
#> License: `use_mit_license()`, `use_gpl3_license()` or friends to pick a
#>     license
#> Encoding: UTF-8
#> Roxygen: list(markdown = TRUE)
#> RoxygenNote: 7.3.1
#> ✔ Writing 'NAMESPACE'
#> ✔ Setting active project to '<no active project>'
use_pkgdown()
#> ✔ Setting active project to '/private/var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T/RtmpCUI9FE/reprex-1a0b62bd36df-fishy-coral'
#> ✔ Adding '^_pkgdown\\.yml$', '^docs$', '^pkgdown$' to '.Rbuildignore'
#> ✔ Adding 'docs' to '.gitignore'
#> ✔ Writing '_pkgdown.yml'
#> • Edit '_pkgdown.yml'
init_site()
#> -- Initialising site -----------------------------------------------------------
#> Copying '../../../../../../../../Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/pkgdown/BS5/assets/link.svg' to 'link.svg'
#> Copying '../../../../../../../../Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/pkgdown/BS5/assets/pkgdown.js' to 'pkgdown.js'
yaml4 <- 'url: ~\ntemplate:\n  bootstrap: 3\n  includes:\n    in_header: |'
cat(yaml4, glue("      {script}"), file = "./_pkgdown.yml", sep = "\n")
build_home()
#> -- Building home ---------------------------------------------------------------
#> Writing 'authors.html'
#> Writing '404.html'
loc4 <- "./docs/index.html"
find_script(loc4) # line number in index.html
#> [1] 19
out4 <- scan(loc4, character(), n = 1, sep = "\n", skip = find_script(loc4) - 1)
out4
#> [1] "<![endif]--><script defer data-domain=\"ivelasq.github.io/rladies-video-feed\" src=\"https://plausible.io/js/script.js\"></script>"

Created on 2024-03-07 with reprex v2.1.0

gadenbuie commented 8 months ago

The difference in the two scripts, namely

<script>!function(){\"use strict\"; ...

vs

<script defer data-domain=\"ivelasq.github.io/rladies-video-feed\" ...

is the result of pandoc's --embed-resource flag (docs). This feature is enabled by default in most rmarkdown settings via the self_contained = TRUE argument (or self_contained: true yaml). When this is turned on, pandoc embeds all CSS and JavaScript into the final .html output file so that you can share or deploy a single file rather than a directory of files.

You can turn this off for this specific script by adding data-external="1" to the <script> tag, e.g. <script defer data-external=1 data-domain="..." ...>, or you can turn it off globally for the flexdashboard document:

---
title: ""
output: 
  flexdashboard::flex_dashboard:
    self_contained: false
    includes:
      in_header: plausible.html
---
gvelasq commented 8 months ago

Thanks very much, @gadenbuie. I had tried self_contained: false which had worked but had also removed many of the desirable features of the dashboard. The data-external=1 fix works great (reprex below)! Thanks for pointing it out in the Pandoc documentation.

library(flexdashboard)
library(glue)
suppressPackageStartupMessages(library(rmarkdown))
library(usethis)

find_script <- function(path) {
  grep("plausible", readLines(path))
}

dom <- "ivelasq.github.io/rladies-video-feed"
src <- "https://plausible.io/js/script.js"
script <- glue('<script defer data-external=1 data-domain="{dom}" src="{src}"></script>')
cat(script, file = "./plausible.html", sep = "\n")

yaml_part1 <- '---\ntitle: "reprex1"\noutput:\n  flexdashboard::flex_dashboard:'
yaml_part2 <- '    includes:\n      in_header: plausible.html\n---'
cat(yaml_part1, yaml_part2, file = "./reprex.Rmd", sep = "\n")
render("./reprex.Rmd")
#> processing file: reprex.Rmd
#> output file: reprex.knit.md
#> /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/pandoc +RTS -K512m -RTS reprex.knit.md --to html4 --from markdown+autolink_bare_uris+tex_math_single_backslash --output reprex.html --lua-filter /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/rmarkdown/rmarkdown/lua/pagebreak.lua --lua-filter /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/rmarkdown/rmarkdown/lua/latex-div.lua --embed-resources --standalone --variable bs3=TRUE --standalone --section-divs --template /Library/Frameworks/R.framework/Versions/4.3-x86_64/Resources/library/flexdashboard/www/flex_dashboard/default.html --variable theme=cosmo --mathjax --variable 'mathjax-url=https://mathjax.rstudio.com/latest/MathJax.js?config=TeX-AMS-MML_HTMLorMML' --include-in-header /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//RtmpIqDwWY/rmarkdown-str7324a6142e3.html --highlight-style pygments --include-in-header plausible.html --include-in-header /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//RtmpIqDwWY/file732376e6b3bhtml --include-before-body /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//RtmpIqDwWY/file73232420aeb.html --include-after-body /var/folders/qf/yt27gctx33x4pvc255t0w9zc0000gp/T//RtmpIqDwWY/file732ddae414.html
#> 
#> Output created: reprex.html

loc <- "./reprex.html"
find_script(loc) # line number in reprex.html
#> [1] 137
out <- scan(loc, character(), n = 1, sep = "\n", skip = find_script(loc) - 1)
out
#> [1] "<script defer data-external=\"1\" data-domain=\"ivelasq.github.io/rladies-video-feed\" src=\"https://plausible.io/js/script.js\"></script>"

Created on 2024-03-09 with reprex v2.1.0