# Discovering Quarto A series of 12 article(s), in reading order. # Running Quarto Markdown in Docker > Run Quarto Markdown in Docker for easy documentation and slideshow generation. Learn to build your own Docker image and render Markdown to PDF, HTML, and Reveal.js. --- ![Running Quarto Markdown in Docker](/img/v2/quarto.webp) **TL;DR** This article shows how to run Quarto (a Pandoc-based tool for converting Markdown into PDF, HTML, Word, ePub, or reveal.js slideshows) via Docker, either building a custom image from a `Dockerfile` or using a prebuilt `ghcr.io/quarto-dev/quarto` one. It walks through rendering the same Markdown file to PDF, HTML, and a multi-slide reveal.js presentation with `quarto render test.md --to `. [Quarto](https://quarto.org/) is a tool for producing PDF, Word document, HTML web pages, ePub files, slideshows and many, many more outputs based on a Markdown file. Using Quarto, you can render any markdown content to a new PDF f.i. Quarto supports a very large number of features, to which are added extensions from its community, making it a really practical tool for anyone wishing to produce documentation. Personally, I haven't used a Word-type word processor for several years; nor have I used PowerPoint — I don't even know when the last time was. And yet, I produce a great deal of documentation and slideshows. I write everything in Markdown and generate PDF or slideshows from the same content. Until recently, I'd been using [pandoc](https://pandoc.org/) but, having taken the time to look around Quarto, it's a hell of a lot more powerful. Like always on this blog, you will not install Quarto the old-fashioned way. You'll run it from a Docker image — and you don't even have to build that image yourself. ## Render your first PDF, without installing anything Create a temporary folder (`mkdir -p /tmp/docker-quarto && cd $_`), drop a Markdown file called `test.md` in it, and run: ```bash $ docker run -it --rm -v .:/input -w /input -u $(id -u):$(id -g) ghcr.io/quarto-dev/quarto:latest quarto render test.md --to pdf ``` ```bash $ docker run -it --rm -v .:/input -w /input -u $(id -u):$(id -g) cavo789/quarto quarto render test.md --to pdf pandoc to: latex output-file: test.tex standalone: true pdf-engine: xelatex variables: graphics: true tables: true default-image-extension: pdf metadata documentclass: scrartcl classoption: - DIV=11 - numbers=noendperiod papersize: letter header-includes: - \KOMAoption\{captions}\{tableheading} block-headings: true Rendering PDF running xelatex - 1 This is XeTeX, Version 3.141592653-2.6-0.999996 (TeX Live 2024) (preloaded format=xelatex) restricted \write18 enabled. entering extended mode running xelatex - 2 This is XeTeX, Version 3.141592653-2.6-0.999996 (TeX Live 2024) (preloaded format=xelatex) restricted \write18 enabled. entering extended mode Output created: test.pdf ``` A `test.pdf` file is now sitting next to your Markdown, owned by you, and it looks like this: ![Your PDF file](./images/pdf_version.webp) That image is the official Quarto one — nothing to build, nothing installed on your machine, and the container removes itself when the render is done. > **Docker CLI reminder:** > > As a reminder, the used Docker run command are (almost always the same): > > * `-it` to start Docker interactively, this will allow the script running in the container to ask you for some prompts f.i., > * `--rm` to ask Docker to kill and remove the container as soon as the script has been executed (otherwise you'll have a lot of exited but not removed Docker containers; you can check this by not using the `--rm` flag then running `docker container list` on the console), > * `-v .:/input` to share your current folder with a folder called `/input` in the Docker container, > * `-w /input` to tell Docker that the current directory, in the container, will be the `/input` folder, > * `-u $(id -u):$(id -g)` asks Docker to reuse your local credentials so when a file is updated/created in the container, the file will be owned by you, > * then the name of the Quarto Docker image, and, finally, > * `quarto render test.md --to pdf` i.e. the command line to start within the container. > **Hide non-essential information:** > > Add the `--log-level warning` CLI argument to Quarto to ask it to show only warning (and error) messages. Non-essential output will be hidden and you'll keep a clean console. ## The source file Here is the `test.md` used above: **/tmp/docker-quarto/test.md** ```markdown # What is Quarto? Explain like I'm five Imagine you want to write a story or a report, but instead of using a fancy computer program, you use plain text. That's kind of like Markdown, a simple language that lets you format your text without getting too complicated. Now, Quarto is like a super-powered writing tool that understands Markdown and can also help you write code in different languages, like R or Python. It's like having a helper in your writing process, making things easier and more fun. So, if you want to create documents, presentations, or even books, Quarto and Markdown can be your friends. They'll help you organize your thoughts, add cool features, and even share your work with the world. ``` **Please refer to the official documentation of [Quarto](https://quarto.org/) to get in-depth information about the Markdown it accepts.** The only thing that changes from one output format to the next is the `--to` argument. ## The same file, as HTML and as a slideshow Simply modify the `--to` argument and replace `pdf` by `html`: `docker run -it --rm -v .:/input -w /input -u $(id -u):$(id -g) ghcr.io/quarto-dev/quarto:latest quarto render test.md --to html --log-level warning` Now, you've a `test.html` file in your directory. For a slideshow, the `--to` argument should be set to `revealjs`: `docker run -it --rm -v .:/input -w /input -u $(id -u):$(id -g) ghcr.io/quarto-dev/quarto:latest quarto render test.md --to revealjs --log-level warning` Open the `test.html` file and you'll get this: ![Revealjs - one slide](./images/revealjs_version1.webp) Ok, you've just one slide now. Reopen the `test.md` file and you'll insert *slide breaks*. This can be done using the `----` syntax: **/tmp/docker-quarto/test.md** ```markdown # What is Quarto? Explain like I'm five Imagine you want to write a story or a report, but instead of using a fancy computer program, you use plain text. That's kind of like Markdown, a simple language that lets you format your text without getting too complicated. ---- Now, Quarto is like a super-powered writing tool that understands Markdown and can also help you write code in different languages, like R or Python. It's like having a helper in your writing process, making things easier and more fun. ---- So, if you want to create documents, presentations, or even books, Quarto and Markdown can be your friends. They'll help you organize your thoughts, add cool features, and even share your work with the world. ``` Rerun the `--to revealjs` command to regenerate the slideshow as a `test.html` file. > **Information:** > > Just run `docker run -d --name static-site -p 8080:80 -v .:/usr/local/apache2/htdocs/ httpd:alpine` then surf to `http://127.0.0.1:8080/test.html` to see your slideshow. Now your slideshow will have three slides (press `space` or arrow keys for navigation): ![Revealjs - slide 1](./images/revealjs_slide1.webp) ![Revealjs - slide 2](./images/revealjs_slide2.webp) ![Revealjs - slide 3](./images/revealjs_slide3.webp) > **Just deploy your slideshow online:** > > The nice thing now is that your slideshow is ready to be deployed on your remote server. Copy the html file and the associated folder (in our use case here, file `test.html` and folder `test_files`) to your FTP server f.i. and your website can be publicly accessed. Nice, isn't it? ## Build your own image (optional — skip it if the official one is enough) There are a number of prebuilt images on the Internet to suit your needs. You'll find them at . The so-called `Tier 0` image — the `ghcr.io/quarto-dev/quarto:latest` one used above — is suitable for generating html / revealjs output. You'll want your own image the day you need something it doesn't ship: a LaTeX package for a specific PDF layout, R or Python for executable code cells, a font, a Quarto extension baked in. Create a new file called `Dockerfile` (there is no extension) with this content: **Dockerfile** ```docker # cspell:disable # Version number of Quarto to download and use # See https://github.com/quarto-dev/quarto-cli/pkgs/container/quarto for existing versions ARG QUARTO_VERSION=1.10.18 ARG OS_USERNAME=quarto ARG UID=1000 ARG GID=1000 FROM ghcr.io/quarto-dev/quarto:${QUARTO_VERSION} # librsvg2-bin is to allow SVG conversion when rendering a PDF file # (will install the rsvg-view binary) RUN --mount=type=cache,target=/var/cache/apt,rw \ set -e -x && \ apt-get update -y \ && apt-get install -y --no-install-recommends \ ca-certificates \ git \ pandoc \ pandoc-citeproc \ curl \ gdebi-core \ librsvg2-bin \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* USER root # The input folder will be the one where we'll put our files before the conversion. # We should do this as root since it's a folder under / RUN set -e -x && \ mkdir -p /input # Should be done for the user; won't work if done for root # (quarto will say that "tinytex is not installed") ARG OS_USERNAME ARG UID ARG GID RUN set -e -x && \ groupadd -g ${GID} -o "${OS_USERNAME}" && \ useradd -l -m -u ${UID} -g ${GID} -o -s /bin/bash "${OS_USERNAME}" USER "${OS_USERNAME}" WORKDIR / # Install tools like tinytex to allow conversion to PDF RUN set -e -x && \ quarto install tool tinytex --update-path && \ ~/.TinyTeX/bin/x86_64-linux/tlmgr update --self --all && \ ~/.TinyTeX/bin/x86_64-linux/fmtutil-sys --all && \ # See https://github.com/rstudio/tinytex/issues/426 for explanation # Run tlmgr install for a few tinyText packages (needed for PDF conversion) ~/.TinyTeX/bin/x86_64-linux/tlmgr install fvextra footnotebackref pagecolor sourcesanspro sourcecodepro titling ``` This done, please run `docker build -t cavo789/quarto .` and after something like three minutes the first time, you'll get your own Docker image: ```bash $ docker build -t cavo789/quarto . [+] Building 183.3s (9/9) FINISHED docker:default => [internal] load build definition from Dockerfile 0.1s => => transferring dockerfile: 1.80kB 0.0s => [internal] load metadata for ghcr.io/quarto-dev/quarto:1.6.36 0.7s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [stage-0 1/6] FROM ghcr.io/quarto-dev/quarto:1.6.36@sha256:7c800f23602e532de8d2e35de8 19.6s => => resolve ghcr.io/quarto-dev/quarto:1.6.36@sha256:7c800f23602e532de8d2e35de838e9f5331 0.0s => => sha256:7c800f23602e532de8d2e35de838e9f53310af381daa34cf3d733c05fd7dec72 955B / 955B 0.0s => => sha256:a9220a0c1eb7e69d971f705b026870f03e511a0c6a95ae0d1140d45089c2 1.73kB / 1.73kB 0.0s => => sha256:7478e0ac0f23f94b2f27848fbcdf804a670fbf8d4bab26df842d40a10c 30.44MB / 30.44MB 3.2s => => sha256:14a397168681603ca2ef7ec883ab375f79cc39ed6abfc602de3d16d 126.00MB / 126.00MB 13.0s => => sha256:682f5bc1f33ef91c217708dbd0f9c287f896d05632b47cdeb1ced02 130.07MB / 130.07MB 16.1s => => extracting sha256:7478e0ac0f23f94b2f27848fbcdf804a670fbf8d4bab26df842d40a10cd33059 1.8s => => extracting sha256:14a397168681603ca2ef7ec883ab375f79cc39ed6abfc602de3d16dc40197b37 0.7s => => extracting sha256:682f5bc1f33ef91c217708dbd0f9c287f896d05632b47cdeb1ced02166e3aa8d 3.2s => [stage-0 2/6] RUN --mount=type=cache,target=/var/cache/apt,rw set -e -x && ap 79.5s => [stage-0 3/6] RUN set -e -x && mkdir -p /input 0.4s => [stage-0 4/6] RUN set -e -x && groupadd -g 1000 -o "quarto" && useradd -l -m - 0.6s => [stage-0 5/6] RUN set -e -x && quarto install tool tinytex --update-path && ~ 80.4s => exporting to image 1.9s => => exporting layers 1.9s => => writing image sha256:f7222fdd5856beb8fb209a609fc9271a0f8f3a586449bc5d90a6b338e54453 0.0s => => naming to docker.io/cavo789/quarto 0.0s ``` > **Choose your own name:** > > The previous instruction `docker build -t cavo789/quarto .` has created an image called `cavo789/quarto`. You can for sure choose a different name without any impact on the image. From then on, every command of this article works the same: just replace `ghcr.io/quarto-dev/quarto:latest` by `cavo789/quarto`. You can quickly check the size of your image; quite huge but except you're very low in memory / disk space; this is really not a big deal. ```bash $ docker image list | grep quarto cavo789/quarto latest fe1d20bd71a6 1 minute ago 1.55GB ``` ## Going further One Markdown file, three `--to` values, three completely different deliverables — and not a single application installed on your machine. That's the part I still find remarkable: the source of my PDF, my web page and my slideshow is the same text file, so they can never drift apart. Once you're comfortable running Quarto in a plain Docker container, two natural next steps are turning that setup into a proper VSCode [devcontainer](/blog/quarto-devcontainer) (open the project, everything is preinstalled and hot-reload works out of the box), and browsing [my favorite Quarto extensions](/blog/quarto-extensions) to enhance your documentation. For the slideshow side in depth, see [Level Up Your Presentations with Quarto, reveal.js, Decktape, Docker and DevContainers](/blog/running-revealjs-with-docker). --- # Quarto conditional display > Learn to use Quarto's conditional display features (content-visible, content-hidden) to control what content appears in different output formats (HTML, PDF, Word, slides) from a single Markdown file. --- ![Quarto conditional display](/img/v2/quarto.webp) **TL;DR** This article shows Quarto's `content-visible`/`content-hidden` conditional blocks (`::: {.content-visible when-format="html"}`), which let a single Markdown source render different content depending on the output format — useful for hiding wide tables from PDF, dropping lengthy code listings from slideshows, or adding a "Thank you / Questions?" closing slide only to the reveal.js output. Like me, you've definitely abandoned Microsoft Word, PowerPoint and friends and do everything in Markdown. Like me, you use the same markdown file to generate your documentation in `html`, `pdf`, `docx`, `pptx`, `revealjs`, ... as required, situation by situation. My personal use case is the following one: I've a single, sometimes big, `readme.md` file for the documentation of a tool I've created. In this document, I could display f.i. tables containing several columns that could be wide. This won't be a problem if I display the documentation as an HTML document, since we have a horizontal scrollbar in the browser to navigate from left to right. But when I generate a PDF, the table will *explode* the width of my PDF page and the result will be awful. Same idea: if my output is an HTML page, I can easily display a listing of a few dozen lines of my source code, for example, to illustrate a feature. On the other hand, if I display it as a slideshow, this will be counterproductive, since the slide will be huge and not very effective. And, if my goal is to generate a slideshow, the last part of my documentation will be a *Thank you for listening! Do you have any questions?* chapter. And that one should certainly not be part of the `docx` / `pdf` / `html` output. Just for the [`revealjs`](/blog/quarto-revealjs-tips) display (or [PowerPoint](/blog/quarto-powerpoint), if your audience insists). > **Docker image with Quarto:** > > If you don't have yet a Docker image with Quarto, read this article [Running Quarto Markdown in Docker](/blog/docker-quarto). ## content-visible when The syntax is very easy since we need to use the special tag `:::` to delimit an area in the file and then use a special filter called `content-visible`. Get in-depth syntax on the [official documentation](https://quarto.org/docs/authoring/conditional.html) Let's see in action; create a simple markdown file (`conditional-display.md`) on your disk and call quarto using `quarto render conditional-display.md --to xxx` and replace `xxx` by f.i. `docx`, `pdf`, `html`, `revealjs`, ... **conditional-display.md** ```markdown # Conditional display ::: {.content-visible when-format="html"} This paragraph will only appear in HTML. ::: ::: {.content-visible when-format="docx"} And this one only when the output format is Word ::: ::: {.content-visible when-format="revealjs"} I'm part of the slideshow ::: ``` When rendering the markdown sample here above to Word, we'll have the title and the paragraph `when-format="docx"`: ![Quarto conditional display - docx](./images/docx.webp) For `when-format="html"`, too, only the title and one paragraph: ![Quarto conditional display - html](./images/html.webp) For `when-format="revealjs"`, it's a little bit smarter since revealjs is an HTML document: ![Quarto conditional display - revealjs](./images/revealjs.webp) And since we don't have any paragraph only conditional ones, when converting to PDF, we just have the title. ![Quarto conditional display - pdf](./images/pdf.webp) The `when-format="xxx"` is thus working exactly as expected. > **`.content-hidden`:** > > The exact opposite can be done using `.content-hidden`, see [documentation](https://quarto.org/docs/authoring/conditional.html#content-hidden). --- # Use Quarto and create a PowerPoint slideshow > Discover how to convert your Markdown documentation into a PowerPoint (.pptx) slideshow or an online reveal.js presentation using Quarto. --- ![Use Quarto and create a PowerPoint slideshow](/img/v2/quarto.webp) **TL;DR** This article shows how to convert a Markdown file into a PowerPoint `.pptx` deck with `quarto render slides.md --to pptx`, where level-2 headings become slide titles and a `---` line starts a new section — noting the same source can instead be rendered to an online reveal.js slideshow with `--to revealjs`. Quarto can convert a markdown file to [a revealjs HTML slideshow](/blog/quarto-revealjs-tips) but can also create a `pptx` file that you can open and play in Microsoft PowerPoint. In this article, we'll create a `pptx` file from our markdown documentation. ## From Markdown to PowerPoint in one command Take a plain Markdown file called `slides.md` and run: ```bash $ quarto render slides.md --to pptx ``` Open the `.pptx` that comes out, and this is your first slide: ![PowerPoint - Slide 1](./images/pptx_slide_1.webp) The rule is worth remembering because it's the whole syntax: **every `##` heading becomes a slide title, and a `---` line starts a new slide**. Your document structure *is* your slide structure. ## The source file Here is the `slides.md` that produced the deck — the content is filler, the structure is what matters: **slides.md** ```markdown --- title: "My thesis in Latin" --- ## Chapter 1 Voluptatem minus labore architecto sed voluptas molestiae perferendis. Expedita magni facere. Ullam non non sint qui provident. ## Chapter 2 Iure repudiandae perferendis maiores dolorem consequuntur exercitationem suscipit. --- ### Chapter 2.1 Quis voluptate est quis in ea veniam qui incididunt ad cillum nostrud. ``` And the three remaining slides it generates: ![PowerPoint - Slide 2](./images/pptx_slide_2.webp) ![PowerPoint - Slide 3](./images/pptx_slide_3.webp) ![PowerPoint - Slide 4](./images/pptx_slide_4.webp) You'll find a lot of tips and tricks on the [official documentation page](https://quarto.org/docs/presentations/powerpoint.html). > **Docker image with Quarto:** > > If you don't have yet a Docker image with Quarto, read this article [Running Quarto Markdown in Docker](/blog/docker-quarto). ## Conclusion The nice thing here isn't that Quarto makes PowerPoint files — it's that your documentation and your slide deck become the same file. Update a chapter, re-render, and the presentation is up to date. No copy/paste into a slide editor, no two versions of the same content drifting apart. And the very same `slides.md` renders as an online slideshow: run `quarto render slides.md --to revealjs` instead, put the result on a web server, and everyone can consult your work from a browser. See [Quarto - revealjs tips](/blog/quarto-revealjs-tips) for what you can do with it. --- # Using variables from external file in your Quarto project > Stop hardcoding! Learn how to easily manage and reuse variables in your Quarto documentation using external YAML files and environment variables. --- ![Using variables from external file in your Quarto project](/img/v2/quarto.webp) **TL;DR** This article shows how to avoid hardcoding recurring values (IPs, paths, emails) in Quarto documentation by storing them in a `_variables.yml` file and referencing them with `{{< var xxx >}}` short codes, or via `{{< meta xxx >}}` for document metadata. It also covers reading values from an `.env` file with `{{< env xxx >}}`, loaded through `--env-file` when rendering Quarto in Docker. My use case is: I need to write some long, technical documentation where I need to provide some information like IP addresses of used servers, some paths to the application, configuration folders, ... The *normal way to do this* is to just put information directly in the documentation and to make sure to update every occurrence in case of changes during the lifecycle of the application. The best way is probably to use an external file where information is stored in a key-value form and, during the rendering process of Quarto, replace short codes by values. ## What Quarto variables do for you You write this in your Markdown source: ```markdown Version {{< var version >}} is a minor upgrade. Please contact us at {{< var email.info >}}. ``` And this is what Quarto renders: ![Using variables with Quarto](./images/variables.webp) The version number and the email address now live in one single file. Change them there, re-render, and every page of the documentation is up to date — including the twenty places you'd have forgotten. All it takes: a `_variables.yml` holding your key-values, a `_quarto.yml` (which may be empty), and the `{{< var >}}` / `{{< meta >}}` short codes in your text. ## Setting it up > **Docker image with Quarto:** > > If you don't have yet a Docker image with Quarto, read this article [Running Quarto Markdown in Docker](/blog/docker-quarto). You can find the official documentation on . Here is how to proceed: 1. First, it's mandatory, you should have a file called `_quarto.yml` in the same directory as the file (let's say `documentation.md`) you will convert using Quarto. *Note: that file can be empty (see for more information on this).* 2. Then you should create a file called `_variables.yml` with your key-values and, finally, 3. You need to have your markdown file. So, `_quarto.yml` can stay empty. Its presence is just to tell Quarto the markdown file is part of a project. Here is an example of what can be a `_variables.yml` content: **\_variables.yml** ```yaml version: 1.2 email: info: info@example.com support: support@example.com engine: jupyter: "[Jupyter](https://jupyter.org)" knitr: "[Knitr]()" ``` And here is a markdown example (file `documentation.md`): **documentation.md** ```markdown --- title: Testing of variables short code. --- {{< meta title >}} Version {{< var version >}} is a minor upgrade. Please contact us at {{< var email.info >}}. Quarto includes {{< var engine.jupyter >}} and {{< var engine.knitr >}} computation engines. ``` As you can see, the short code is something like `{{< meta xxx >}}` or `{{< var xxx >}}`. `meta` is for metadata of the document like its title and `var` to retrieve information from `_variables.yml`. Render it with `quarto preview documentation.md --to html` and you get the page shown at the beginning of this article. ## Environment variables You can also retrieve environment variables using `{{< env xxx >}}` but, there, you should first load these variables in case of need. For instance, you can have a `.env` file like this: **.env** ``` APPLICATION_NAME=My application name VERSION_NUMBER=1.2 ``` Then, before calling the Quarto rendering process, you should load the file. Since I'm using Docker, I do this like this: ```bash $ docker run --rm -it -v .:/input -w /input --env-file .env cavo789/quarto quarto preview documentation.md --to html ``` Here is the content of `documentation.md`: **documentation.md** ```markdown --- title: Testing of variables and env short codes. --- :::{.callout-tip} ## Environment variables {{< env APPLICATION_NAME >}} v.{{< env VERSION_NUMBER >}} ::: {{< meta title >}} Version {{< var version >}} is a minor upgrade. Please contact us at {{< var email.info >}}. Quarto includes {{< var engine.jupyter >}} and {{< var engine.knitr >}} computation engines. ``` I'm thus using `--env-file .env` in the `docker run` instruction so Docker will load my variables and make them available in the container. Quarto can then access them. ![Using environment variables](./images/environment.webp) This solution is even better if you have an application such as Laravel, and therefore already have such an .env file. As a result, you reuse the same values for both the application and the documentation. ## Conclusion Variables are the right tool when what repeats itself is a *value*: a version number, an IP, a support address, a path. They cost two files and pay for themselves the first time a server is renamed. When what repeats itself is a whole page structure rather than a value, variables are no longer enough — that's where [Using Mustache templating with Quarto](/blog/quarto-mustache) takes over. --- # Some tips and tricks for Quarto when rendering as a reveal.js slideshow > Elevate your Quarto presentations! Discover essential tips and tricks for rendering stunning reveal.js slideshows, including custom title slides, logo placement, CSS/JS customization, and advanced slide layouts. --- ![Some tips and tricks for Quarto when rendering as a reveal.js slideshow](/img/v2/quarto.webp) **TL;DR** This article is a grab bag of Quarto reveal.js tips: title slides, custom logo positioning with CSS/JS, background images and their `background-size` behavior, `slide-level` control, emoji, `r-fit-text` and `r-stretch` classes, fragment-based reveals, multi-column and quadrant layouts, custom short codes, inline and external CSS, hiding image captions, styling callouts, vertical/grid navigation, custom slide slugs, slide numbers, and speaker notes. The [Quarto reveal.js documentation](https://quarto.org/docs/reference/formats/presentations/revealjs.html) is the best place to find instructions for creating impeccable reveal.js presentations. In this article, we'll take a quick look at a few tips and tricks to make your reveal.js presentations even better. *For the complete workflow — writing, previewing and exporting a reveal.js slideshow as HTML and PDF from a devcontainer — see [Level Up Your Presentations with Quarto, reveal.js, Decktape, Docker and DevContainers](/blog/running-revealjs-with-docker).* > **Docker image with Quarto:** > > If you don't have yet a Docker image with Quarto, read this article [Running Quarto Markdown in Docker](/blog/docker-quarto). ## Working with slides ### Create a title slide The [title slide](https://quarto.org/docs/presentations/revealjs/advanced.html#title-slide) is the first one of your presentation. You can add such slide with a few yaml lines: **slides.md** ```markdown --- title: "My Slide Show" title-slide-attributes: data-background-image: ./img/background.jpg data-background-size: cover data-background-opacity: "0.9" --- # Slide 1 # Slide 2 # Slide 3 ``` The slideshow will have four slides, the first one will be the title-slide. ![Title slide](./images/title-slide.webp) ### Display a logo top left on the first slide, then bottom right The example below illustrates the need: on your first slide, you want to make your logo much more visible than on the other slides. To achieve this, on the first slide displayed, your logo will be displayed at the top left with a certain size, whereas when you change slides, the logo will be positioned at the bottom right and will be much smaller. Here is a visual example: ![Logo top left](./images/logo-top-left.webp) ![Logo bottom right](./images/logo-bottom-right.webp) To do this, we'll need to add a custom JavaScript and custom CSS. Create a new folder called `assets` and, in that folder, a file called `style.css` and a second called `custom.js`. **Project: assets** **style.css** ```css .reveal .slide-logo-bottom-right { bottom: 0 !important; left: unset !important; height: 100% !important; max-height: 2.2rem !important; right: 12px !important; } .slide-logo-max-size { bottom: unset !important; left: 12px; height: 100px !important; max-height: unset !important; right: unset !important; top: 5px; } .reveal .slide-logo { display: block; max-width: unset !important; position: fixed; width: 100x !important; } ``` **custom.js** ```javascript // The script below will ensure the logo is displayed top left // in his full size (probably on the first slide; title-slide) // then as soon as a different slide is displayed, the logo will // be displayed bottom right with a smaller size. function updateLogoSizePosition(event) { if (event.currentSlide.matches('#title-slide')) { var elements = document.querySelectorAll(".slide-logo"); [].forEach.call(elements, function(elem) { elem.classList.remove("slide-logo-bottom-right"); elem.classList.add("slide-logo-max-size"); }); } else { var elements = document.querySelectorAll(".slide-logo"); [].forEach.call(elements, function(elem) { elem.classList.add("slide-logo-bottom-right"); elem.classList.remove("slide-logo-max-size"); }); } }; window.addEventListener("load", (event) => { // Make sure the logo has his full size when the slideshow // is loaded (i.e. apply the slide-logo-max-size css class) var elements = document.querySelectorAll(".slide-logo"); [].forEach.call(elements, function(elem) { elem.classList.remove("slide-logo-bottom-right"); elem.classList.add("slide-logo-max-size"); }); Reveal.on("slidechanged", function(event) { updateLogoSizePosition(event); }); }); ``` Now, use them in your `slides.md` like this: **slides.md** ```markdown --- title: "My Slide Show" subtitle: "Playing with logo" author: "Christophe Avonture" date: last-modified lang: "en" format: revealjs: logo: images/my-logo.svg css: assets/style.css resources: images header-includes: | --- ``` As you can see above, the JavaScript code is injected in your presentation using the `header-includes` directive. For the stylesheet, the `css` directive is used (*there is no `js:` directive unfortunately*). *To run this example, run `quarto render slides.md --to revealjs`.* ### Adding a background image to a slide A background, a title and some style: **slides.md** ```markdown # Merry Christmas {background-image="images/winter.jpg" style="text-align: center; background-color: #cfb0f399; color: #210753;"} ``` *To run this example, run `quarto render slides.md --to revealjs`.* ![Adding a background image to a slide](./images/background-title-style.webp) #### Background-size contain or cover Sometimes the image display isn't what you'd like; this is the case when the image is too large and/or the slide too narrow. The illustration below shows this: the photo of the sloth is far too large and you can't see the whole thing. This can be corrected by using the attribute `background-size`. When not mentioned (as for the first slide below), the default value is `cover` (see to get more information). You can ask the browser to resize the image so it's fully displayed. This is the behavior when `background-size` is set to `contain`. **slides.md** ```markdown ## {background-image="images/2024.jpg"} ## {background-image="images/2024.jpg" background-size="contain"} ``` *To run this example, run `quarto render slides.md --to revealjs`.* The slide below is using `cover` and, no, not really attractive. ![Background-size is set to cover](./images/background-cover.webp) The second slide is using `contain` and, at least, we can see what the sloth was trying to tell us. ![Background-size is set to contain](./images/background-contain.webp) ### Slide-level The slide level is an important configuration item ([official documentation](https://pandoc.org/MANUAL.html#structuring-the-slide-show)). If we look at the markdown below, how many slides will we have? Two or three? The first is for the title, the second for `Technologies and tools`, but will `Apache` be a separate slide or not? **slides.md** ```markdown --- title: "Playing with slide-level" --- ## Technologies and tools ### Apache Our web applications are running under [Apache](https://httpd.apache.org/). We don't need to have Apache locally since we're using it as a Docker image. ``` In fact, it depends on the `slide-level` setting. By default, `slide-level` is equal to `2` and then, the result will be: ![Slide level 2](./images/slide-level-2.webp) We've two slides. On the second slide, we can see our `Apache` heading 3 as a text on `Technologies and tools`. If you want a slide to be created for each level 2 and level 3 (or higher) title, you need to set this with slide-level. **slides.md** ```markdown --- title: "Playing with slide-level" # highlight-next-line format: # highlight-next-line revealjs: # highlight-next-line slide-level: 3 --- ## Technologies and tools ### Apache Our web applications are running under [Apache](https://httpd.apache.org/). We don't need to have Apache locally since we're using it as a Docker image. ``` And the result will give: ![Slide level 3](./images/slide-level-3.webp) ### Using emoji It's simple, just add the following entry to your YAML front matter. ```yaml --- from: markdown+emoji --- ``` Then you can add emoji by just typing the code like `:wave:` or `:raised_hands:`. You can find a full list on . ### Big text The `r-fit-text` class ([official doc](https://quarto.org/docs/presentations/revealjs/advanced.html#fit-text)) will give the maximum size to your content i.e. **slides.md** ```markdown # ::: {.r-fit-text} Big Text ::: ``` ![Big text](./images/big-text.webp) ### Stretch The `r-stretch` class ([official doc](https://quarto.org/docs/presentations/revealjs/advanced.html#stretch)) is my preferred one. Consider the example below: **slides.md** ```markdown # Slide 1 Here is an image: ![Quarto slide with stretched image](image.webp){.r-stretch} Some text after the image. ``` This will generate this slide: ![Stretch 1](./images/stretch-1.webp) Adding a lot of more text / paragraphs will give this new slide: ![Stretch 2](./images/stretch-2.webp) The image height will be resized automatically so both text and images can be displayed on the same slide. If you have less text, the height of the image will be bigger, smaller otherwise. Without to have to manually resize the image on the disk. Very useful. ### Show an image on key press Using `::: {.fragment .fade-up}` to define a content area, you are asking reveal.js to show its content only the next key sequence. **slides.md** ```markdown # It's a candy dog ::: {style="font-size: 2em; color: #75AADB;"} Would you like to see a candy dog? ::: ::: {.fragment .fade-up} ![Candy dog reveal.js example](./images/dog.webp) ::: ``` *To run this example, run `quarto render slides.md --to revealjs`.* ![It's a candy dog](./images/candy-dog.gif) ### Creating columns You can use `columns` to divide your slide: **slides.md** ```markdown ## ::: columns ::: {.column width="70%"} ![Left column: slide example image](./images/image_1.webp) ::: ::: {.column width="30%"} ![Right column: top image](./images/image_2.webp) ![Right column: bottom image](./images/image_3.webp) ::: ::: ``` *To run this example, run `quarto render slides.md --to revealjs`.* ![Columns](./images/columns.webp) #### Four quadrants The example below comes from , a reveal.js slideshow made using Quarto. The idea is to split the slides in four parts and display content clockwise, starting with the top-left dial. **slides.md** ```markdown ::: columns ::: {.column width="5%"} ::: ::: {.column width="40%"} ::: bulletbox ::: {.fragment .fade-in-then-semi-out} ![Top-left quadrant example](./images/top-left.webp){width="450px"} ::: ::: ::: ::: {.column width="5%"} ::: ::: {.column width="40%"} ::: bulletbox ::: {.fragment .fade-in-then-semi-out} ![Top-right quadrant example](./images/top-right.webp){width="450px"} ::: ::: ::: ::: {.column width="5%"} ::: ::: ::: columns ::: {.column width="5%"} ::: ::: {.column width="40%"} ::: bulletbox ::: {.fragment .fade-in-then-semi-out} ![Bottom-left quadrant example](./images/bottom-left.webp){width="450px"} ::: ::: ::: ::: {.column width="5%"} ::: ::: {.column width="40%"} ::: bulletbox ::: {.fragment .fade-in-then-semi-out} ![Bottom-right quadrant example](./images/bottom-right.webp){width="450px"} ::: ::: ::: ::: {.column width="5%"} ::: ::: ``` *To run this example, run `quarto render slides.md --to revealjs`.* ![Four quadrants](./images/four-quadrants.gif) ### Creating our own short code, easily I like to be able to write `==Important text==` and, for a revealjs presentation, transform this text automatically to `Important text` so I can easily add a custom CSS to it. First, create a `assets/custom.js` if not yet present and copy/paste the following code: **assets/custom.js** ```javascript window.addEventListener("load", (event) => { // This function will search for short code like "==IpsoLorem==" // i.e. a portion of text between two equal sign and will replace it // to "IpsoLorem" so we can then use CSS to highlight // that portion. Reveal.on("slidechanged", function (event) { var re = /==([^=]*)==/; event.currentSlide.innerHTML = event.currentSlide.innerHTML.replace(new RegExp(re, "g"), "$1"); } ); }); ``` In your YAML front matter, add this: **slides.md** ```markdown --- format: revealjs: header-includes: | --- ``` And now, when the slideshow will be played, a custom JavaScript code will replace on-the-fly each fragment with `==xxxx==` and replace with `xxx`. ## CSS ### Using inline css You can apply styles to inline text by creating spans using `[]` to surround the text you want to style and `{}` to define the style you want to apply. **slides.md** ```markdown # Inline style To draw attention to a specific part of the text, you might want to make it [red]{style="color: red;"} with a [yellow background]{style="background-color: yellow;"}; [like this]{style="color: red; background-color: yellow;"}. ``` *To run this example, run `quarto render slides.md --to revealjs`.* This is correctly rendered in HTML: ![html](./images/html.webp) ### Use a custom css Use the YAML header block for this. **slides.md** ```markdown --- pagetitle: "Using my custom css" format: revealjs: theme: custom.css --- ## ![Mimikyu example image](./images/mimikyu.webp){style="width:400px;"} ``` *To run this example, run `quarto render slides.md --to revealjs`.* Now, create the `custom.css` file in the same folder as your markdown one and, for instance, set the slide background to yellow: **custom.css** ```css /*-- scss:defaults --*/ .reveal { background-color: rgb(224, 226, 98); } ``` ![Custom css](./images/custom-css.webp) ### Hide image caption By default, revealjs will display the caption of the image below it. You can hide it by using this stylesheet: **custom.css** ```css .reveal p.caption { display: none; } ``` ### Callout can be stylized A callout is a special div like below: **slides.md** ```markdown ## Customize your terminal :::{.callout-note} ## Notice Skip this part if you do not want to customize your terminal right now ::: As you can see on the image below, we can customize the terminal: ``` In revealjs, personally, I find that the rendering takes up too much space compared to the content of my slide. So I use css to hide the title: ![Callout title](./images/callout-title.webp) **custom.css** ```css .reveal .callout-title { display: none; } ``` ## Navigation ### Vertical slides There are three types of navigation: `linear` (default one), `vertical` or `grid` ([official doc](https://quarto.org/docs/presentations/revealjs/advanced.html#vertical-slides)). If you use vertical or grid navigation, you should structure your slides using level 1 headings for the horizontal axis and level 2 headings for the vertical axis. In a normal, `linear` navigation, revealjs will show each slide, whatever the key the user is pressing on his keyboard. In a `linear` navigation, as author, you are sure that your visitor will see each of them. In a `vertical` or `grid` navigation, it's just like you're showing the chapter's title and if the user press the `down` or `space` key, he will jump in the chapter (and this *vertically*). But if he is pressing the `right` key he'll skip it and go to the next chapter. The `navigation-mode` in the yaml header allows you to enable `vertical` mode, below an example: **slides.md** ```markdown --- title: "Presentation" format: revealjs: navigation-mode: vertical --- # Slide 1 ## Slide 1.1 # Slide 2 ## Slide 2.1 ## Slide 2.2 ## Slide 2.3 ``` ![Vertical navigation](./images/vertical.webp) > **Press esc key:** > > The `esc` key allow you to see the structure of your revealjs slideshow. ### Define a name for your slide By default, reveal.js will generate a slug from the title so you can reference the slide in a URL. Consider the following example: **slides.md** ```markdown ## Elit ad fugiat proident culpa sint Elit ad fugiat proident culpa sint qui id pariatur nostrud ullamco velit irure tempor culpa. Laborum occaecat ea tempor voluptate duis pariatur proident ea. Aute adipisicing ut ullamco exercitation nisi nulla sint. Reprehenderit tempor quis non cupidatat. ## Cillum do et commodo minim ullamco elit culpa Occaecat minim eu veniam laborumanim. Nostrud duis eiusmod ut amet velit commodo. Aliqua laborum cillum officia culpa quis duis enim cillum esse duis excepteur cillum do qui. Veniam dolore enim qui labore proident. Minim ipsum nisi Lorem cillum proident labore cupidatat aliqua pariatur. Nisi id magna incididunt et culpa. ``` *To run this example, run `quarto render slides.md --to revealjs`.* The generated URL for the first slide will be `http://[...]index.html#/elit-ad-fugiat-proident-culpa-sint` and, for the second slide, `http://[...]index.html#/cillum-do-et-commodo-minim-ullamco-elit-culpa`. To define the name yourself, just add `{#}` followed by the slug you desire. **slides.md** ```markdown ## Elit ad fugiat proident culpa sint {#intro} Elit ad fugiat proident culpa sint qui id pariatur nostrud ullamco velit irure tempor culpa. Laborum occaecat ea tempor voluptate duis pariatur proident ea. Aute adipisicing ut ullamco exercitation nisi nulla sint. Reprehenderit tempor quis non cupidatat. ## Cillum do et commodo minim ullamco elit culpa {#chapter1} Occaecat minim eu veniam laborumanim. Nostrud duis eiusmod ut amet velit commodo. Aliqua laborum cillum officia culpa quis duis enim cillum esse duis excepteur cillum do qui. Veniam dolore enim qui labore proident. Minim ipsum nisi Lorem cillum proident labore cupidatat aliqua pariatur. Nisi id magna incididunt et culpa. ``` ![Define a name for your slide](./images/set-slide-slug.webp) But, pay attention to the slide's menu (bottom left): perhaps it's also good to define a suitable name there. ![Also define the name in the menu](./images/set-name-in-menu.webp) To do this, set the `data-menu-title` attribute; f.i.: **slides.md** ```markdown ## Elit ad fugiat proident culpa sint {#intro data-menu-title="Introduction"} ## Cillum do et commodo minim ullamco elit culpa {#chapter1 data-menu-title="Chapter 1"} ``` ### Show slide number To only show the current slide number, you can use `slide-number: true` but, if you also want the total number of slides, you should use `slide-number: c/t` : **slides.md** ```markdown format: revealjs: slide-number: c/t ``` ![Slide / Total of slides](./images/slides_c_t.webp) You can retrieve more information in the [Quarto slide numbers documentation](https://quarto.org/docs/presentations/revealjs/presenting.html#slide-numbers). ## Misc ### Using speaker notes You can write messages in your presentation that will not be displayed when the slideshow is played, but only as a second screen when the presenter wishes. The example below illustrates this. The way to insert such presenter's notes is by using the `::: notes` block. **slides.md** ````markdown ## ::: columns ::: {.column width="70%"} ![Left column: speaker notes example image](./images/image_1.webp) ::: ::: {.column width="30%"} ![Right column: speaker notes top image](./images/image_2.webp) ![Right column: speaker notes bottom image](./images/image_3.webp) ::: ::: ::: notes On the left side, you have ... and, on the right side, the first image is .... The second image has been ... ::: ## ![Example slide image 4](./images/image_4.webp) ``` ```` *To run this example, run `quarto render slides.md --to revealjs`.* So, in the example above, we'll have two slides. By pressing the `s` key on the keyboard (s for speakers) a new window will be displayed. When you have two screens, on the first you'll display your presentation without the notes (left screen below) and on the second you'll display the notes (right screen below). If you share your screen through a tool like Teams or Zoom, same thing; you share the screen where the presentation is displayed (the first one) and keep the notes only for you. ![Speaker notes](./images/speaker-notes.webp) As you can see on the image above, the speaker notes are displaying valuable information as the elapsed time since the start of the presentation, the current time and the next slide to make your transition easier. --- # Quarto Callout Blocks > Discover the Quarto Callout Block syntax for creating attention-grabbing admonitions (Tips, Warnings, Cautions) in your reports. Includes a handy VSCode snippet. --- ![Quarto Callout Blocks](/img/v2/quarto.webp) **TL;DR** This article shows Quarto's callout-block syntax (`:::{.callout-caution}`, `:::{.callout-tip}`, etc.) for highlighted admonitions in PDF, HTML, and revealJS output — Quarto's equivalent of Docusaurus's `` — and provides a VSCode snippet that interactively prompts for the callout type, title, and body so you never need to remember the exact syntax. A callout (called *admonition* by [Docusaurus](https://docusaurus.io/docs/markdown-features/admonitions)) is a special syntax used to highlight a paragraph, f.i. a *Pay attention to...* or *Tip: Did you know that...* box. *On the Docusaurus side, I've covered that syntax in [Some tips and tricks when written articles for Docusaurus](/blog/docusaurus-articles-tips).* On this blog powered by Docusaurus, the syntax for *admonition* is ```markdown Never give your bank card code to a stranger. ``` and this is rendered like > **Pay attention to...:** > > Never give your bank card code to a stranger. And now a nice tip: > **Did you know that...:** > > By going to bed earlier, you'll get better quality sleep. Quarto implements this a little differently, so let's have a look... > **Docker image with Quarto:** > > If you don't have yet a Docker image with Quarto, read this article [Running Quarto Markdown in Docker](/blog/docker-quarto). ## Syntax Unfortunately, the syntax is not part of the standard Markdown language, so every tool actually has its own. For Quarto, the syntax is: ```markdown :::{.callout-caution} ## Pay attention to... Never give your bank card code to a stranger. ::: ``` And, that paragraph is converted like this in PDF: ![Callout Caution in PDF](./images/caution-pdf.webp) The Quarto syntax for a tip is: ```markdown :::{.callout-tip} ## Did you know that... By going to bed earlier, you'll get better quality sleep. ::: ``` ![Callout Tip in PDF](./images/tip-pdf.webp) Callouts are supported in PDF, HTML, revealJS and other formats. Get in-depth syntax on the [official documentation](https://quarto.org/docs/authoring/callouts.html). ## VSCode snippet If you're a VSCode user, don't make an effort to remember the correct syntax but use the *Snippet* feature of VSCode. Press `CTRL`-`SHIFT`-`P` to get access to the **Command Palette**. Start to type `Configure User Snippets`, validate and select `markdown.json` since our snippets should be available only when writing Markdown content. ![Creating a snippet](./images/create-snippets.webp) If this is your first snippet, the JSON file will be empty. Copy/paste the text below. Otherwise, just copy the `callout` node and paste it in your file to the correct location. **markdown.json** ```json { "callout": { "prefix": "quarto-callout", "body": [ ":::{.callout-${1|note,tip,warning,caution,important|}}", "## ${2:title}", "${3:body}", ":::" ], "description": "Add a callout paragraph to your markdown file" } } ``` Save and close the `markdown.json` file and go back to any markdown file (or create a new one). Now, by typing `quarto` and by pressing `CTRL`-`space`, VSCode will show the list of possibilities for that word and you'll retrieve your snippet. Select it and press `Enter` to validate. ![Call the snippet](./images/call-snippets.webp) And, as you can see on the image below, VSCode will ask you three things: 1. First the type of callout and you'll have a list of possible values; easy no? 2. Then, after having pressed the `tab` key, you'll be prompted to type a title and, finally, 3. The body of your paragraph. ![Using the snippet](./images/using-snippet.webp) VSCode will show you the snippet interactively and you can then, easily, create it without having to remember the correct syntax. ```markdown :::{.callout-caution} ## Pay attention to... Never give your bank card code to a stranger. ::: ``` --- # Quarto includes short code > Learn how to use the Quarto includes short code to break down long documents into multiple, organized files, making your writing process easier. --- ![Quarto includes short code](/img/v2/quarto.webp) **TL;DR** This article shows Quarto's `{{< include >}}` short code, which lets you split a long document into several smaller Markdown files and merge them at render time by referencing their relative paths from a main `.qmd` file. Using the Quarto `includes` short code, writing a long document can easily be broken down into several smaller ones. *`includes` splits a document into fixed chunks. When the repeated part is the same *structure* filled with different values, [Using Mustache templating with Quarto](/blog/quarto-mustache) and [Using variables from external file in your Quarto project](/blog/quarto-project-variables) are the right tools.* Imagine the following `main.qmd` file: **main.qmd** ```qmd # My thesis in Latin {{< include chapter1.md >}} {{< include chapter2.md >}} ``` Quarto will then merge the three files when you render `main.qmd`. > **Docker image with Quarto:** > > If you don't have yet a Docker image with Quarto, read this article [Running Quarto Markdown in Docker](/blog/docker-quarto). ## Includes tag The tag is simple, you just need to specify the relative path to the file to include. Get in-depth syntax on the [official documentation](https://quarto.org/docs/authoring/includes.html). ## Result Running `quarto render main.qmd --to html` on the `main.qmd` file shown above merges the included files into one document: ![Quarto includes](./images/includes.webp) Here's what's actually inside each included file. Here is some dummy content for `chapter1.md`: **chapter1.md** ```markdown ## Chapter 1 Voluptatem minus labore architecto sed voluptas molestiae perferendis. Sed voluptatem ut amet at blanditiis sunt et exercitationem quidem. Vel id impedit dicta omnis repudiandae iure. Expedita magni facere. Ullam non non sint qui provident. Ea beatae voluptatem pariatur. Et beatae error ea aut neque omnis pariatur rerum ut. Ad aut nobis magni aut est dicta adipisci est. Quo reiciendis eum aut rem. Dolores sit magni non. ``` And for `chapter2.md`: **chapter2.md** ```markdown ## Chapter 2 Iure repudiandae perferendis maiores dolorem consequuntur exercitationem suscipit. Animi voluptatem est quia. Quia id optio. Architecto ut ipsa voluptas minima voluptate accusamus architecto. Consequatur debitis et sunt eos quod qui unde aut. ``` Now, by running, f.i., `quarto render main.qmd --to html`, Quarto will merge these two files (and `main.qmd`) into the rendered page shown above. > **What is a `.qmd` file?:** > > For Quarto, the extension isn't very important: whether it's `.qmd` or `.md`, there's no difference in treatment. > > On the other hand, it's probably more interesting for us to use the `.qmd` extension when using a Quarto-specific syntax, as is the case here. --- # Quarto Inline style > Quickly apply inline CSS styles in Quarto for HTML documents using the []{} syntax. Highlight text instantly without writing a full CSS class. --- ![Quarto Inline style](/img/v2/quarto.webp) **TL;DR** This article shows Quarto's inline CSS span syntax `[text]{style="..."}` for quickly styling a bit of text (color, background, etc.) without defining a CSS class — noting it only renders in HTML output, and is silently ignored in Word or PDF. Don't want to take the time to create a CSS class and figure out how to integrate it into Quarto to quickly highlight part of your paragraph? Quarto supports an *inline css style* syntax for producing **HTML documents** (not working for, f.i., pdf or docx). *If you need the content itself, and not just its styling, to differ between formats, see [Quarto conditional display](/blog/quarto-conditional-display).* > **Syntax:** > > You can apply styles to inline text by creating spans using `[]` to surround the text you want to style and `{}` to define the style you want to apply. So to put a text like *red* in red, the syntax for Quarto is `[red]{style="color: red;"}`. The text enclosed between brackets will get the specified inline CSS style. ```markdown # Inline style To draw attention to a specific part of the text, you might want to make it [red]{style="color: red;"} with a [yellow background]{style="background-color: yellow;"}; [like this]{style="color: red; background-color: yellow;"}. ``` This is correctly rendered in HTML: ![html](./images/html.webp) > [source](https://mine-cetinkaya-rundel.github.io/quarto-tip-a-day/posts/11-spans/). > **Docker image with Quarto:** > > If you don't have yet a Docker image with Quarto, read this article [Running Quarto Markdown in Docker](/blog/docker-quarto). But just ignored in Word or PDF: ![docx](./images/docx.webp) ![pdf](./images/pdf.webp) --- # My favorite Quarto extensions > My favorite Quarto extensions - Learn about essential Quarto filters for partial content templating, Font Awesome icons, including external code files, and simple search-replace macros to enhance your documentation workflow. --- ![My favorite Quarto extensions](/img/v2/quarto.webp) **TL;DR** This article shares a curated list of the author's favorite Quarto extensions to enhance your documentation workflow. Discover useful filters for templating content with `quarto-partials`, embedding icons with `fontawesome`, including external code snippets, creating simple text-replacement macros with `search-replace`, and improving code block visibility in reveal.js presentations with `code-fullscreen`. I've been using Quarto for 18 months now to generate my documentation: I'm writing Markdown files (`.md`) and converting them to a Word document, PDF, static HTML site or [a revealjs slideshow](/blog/quarto-revealjs-tips). *If Quarto is new to you, [Running Quarto Markdown in Docker](/blog/docker-quarto) gets you a working setup in minutes.* Quarto is supporting external extensions (also called `plugins` elsewhere). Below a list of a few I'm using. ## gadenbuie/quarto-partials See When I come to this extension, my need was: I've coded more than 60 functionalities and I need to document them. Each will have the same look\&feel like a *Description* chapter, a *How to run?*, *How to configure?*, *Remarks* and so on. Sometimes the text will be exactly the same (like a generic text) and, in some cases, almost the same: the *How to configure?* chapter will have the same sentences except I've to replace some placeholders (like the name of the feature, the name of the configuration file, ...) In other terms: I need to be able to write some *page template* and *inject* specific content to it. I wish that my 60 functionalities pages look the same and if I need to change one global text, I have to do it in a central place. [Partial content](https://github.com/gadenbuie/quarto-partials/tree/main) is perfect here. Based on their documentation, it's similar to [Mustache](https://mustache.github.io/). ### Example Let's create a file called `_hello.md` with this content: **\_hello.md** ```markdown Hello, {{ name }}! ``` Then, in any file, we can do: **test.md** ```markdown {{< partial _hello.md name="John" >}} ``` Make sure to add lines below in your YAML front matter: **test.md** ```markdown filters: - quarto-partials ``` We can pass variables like here above but, more powerful, we can declare a `partial-data` key in our YAML frontmatter. If you want to go much further with this extension (conditional blocks, raw content, a real-world documentation use case), I've written a dedicated deep dive: [Using Mustache templating with Quarto](/blog/quarto-mustache). ## quarto-ext/fontawesome See Use Font Awesome icons in HTML and PDF documents. For instance, if you want a GitHub icon, you just need to put the `{{< fa brands github size=5x >}}` code in your file. Quite easy. Make sure to add lines below in your YAML front matter: **test.md** ```markdown filters: - fontawesome ``` ## quarto-ext/include-code-files > **Is this still needed?:** > > Not sure this addon is still needed: in one of my documentation, I've used it like this: > > ````markdown > ```{.yaml include="files/.gitlab-ci.yml"} > ``` > ```` > > It was working as expecting but during the generation of a `.docx` file (MS Word), the included file was missing. > > I've then use the native shortcode like below and, now, it works both for `.html` and `.docx`. > > ````markdown > ```yaml > {{< include files/.gitlab-ci.yml >}} > ``` > ```` See The objective is to be able to include external file by using the syntax below: ````markdown ```{.bash include="documentation/chapter_1/install.sh"} ``` ```` Make sure to add lines below in your YAML front matter: **test.md** ```markdown filters: - include-code-files ``` ## quarto-ext/search-replace See Quarto filter extension for simple search-replace macros. This extension allows you to search and replace when rendering documents. For instance, by putting the `+quarto` code in the frontmatter of the article or, better, in the `_quarto.yaml` global file, we can just write `+quarto` (a constant) in our document and let the replace action be done during the rendering of the documentation. **\_quarto.yaml** ```yaml search-replace: +quarto: "[Quarto](https://quarto.org)" ``` Make sure to add lines below in your YAML front matter: ```yaml filters: - search-replace ``` ### Example Below how to use it. It's simple; just write the name of your constant and during the rendering of your document, the constant will be replaced by the predefined value. Easy! ```markdown I'm using +quarto and I love it ``` ## shafayetShafee/code-fullscreen See This extension is for revealjs slideshow. If you have a very long block of code, revealjs will not display all lines and thus, the code will be truncated (like only the first 25 lines will be displayed). By adding the filter below to your YAML frontmatter, a small *Full screen* button will be displayed at the top right of the block code. **test.md** ```markdown filters: - code-fullscreen ``` --- # Using Mustache templating with Quarto > Master Mustache templating in Quarto! Discover how to use the quarto-partials extension to create structured, repetitive documentation pages for your static website project. --- ![Using Mustache templating with Quarto](/img/v2/mustache.webp) **TL;DR** This tutorial explains how to use Mustache-like templating in Quarto to efficiently create structured, repetitive documentation for a static website. By leveraging the `quarto-partials` extension, you can define reusable content snippets (partials) and include them in multiple pages. The guide covers setting up a project, passing variables from a document's YAML frontmatter to a partial, and using conditional logic to render content based on whether a variable is defined. This approach is ideal for documenting a large number of similar features with a consistent structure. I use Quarto to generate documentation; until now I've mainly generated `.docx` and `.pdf` files. Recently, I needed to generate a static website (HTML) that would allow my colleagues to consult the documentation for the latest software I've written. I had to describe nearly 80 functions: as many web pages to write, each with the same structure (a description chapter, a how to run chapter, a how to configure chapter, etc.). *This need eventually grew into the full pipeline described in [Quarto - How I Built a Self-Documenting Ecosystem for 50+ Projects](/blog/quarto-industrialisation).* And immediately, when you put it like that, you think of a CMS (content management system) such as Joomla for example, i.e. being able to define a standard page (a template) that will include the chapters and in each chapter, you could imagine injecting content that would be variables. Wouldn't it be silly to write each page by hand? And this is where [Mustache](https://mustache.github.io/)'s idea comes in. Mustache defines itself as a *Logic-less templates* framework. And, because I'm using Quarto for my documentation, I need an extension for using Mustache and, it's [Quarto-partials](https://github.com/gadenbuie/quarto-partials/tree/main) from Garrick Aden-Buie — one of the extensions I mention in [my favorite Quarto extensions](/blog/quarto-extensions), but it deserves this much deeper dive. Here's the idea in one picture: a Markdown page whose "How to run" chapter is injected from a shared template, with a variable filled in from the page's own frontmatter. ![Rendering the Contextual Canvas feature](./images/render_canvas.webp) The "Contextual Canvas" heading and description are typed directly in the page; the "How to run" paragraph below it comes from a template shared by every feature page, with `{{ command }}` replaced by the value declared in that page's frontmatter. Let's build it. ## Create some files Like always, let's create some files. Please create a temporary folder and jump in it: ```bash $ mkdir /tmp/partials && cd $_ ``` Create a file called `_quarto.yml` with this content: **\_quarto.yml** ```yaml project: type: website output-dir: . lang: en website: title: "Playing with Quarto partials" page-navigation: true bread-crumbs: true sidebar: style: "docked" toc-depth: 4 contents: auto filters: - partials ``` This file tells Quarto that we're about to create a website i.e. by running `quarto render` later on, we'll convert our pages written in Markdown as HTML pages. The two last lines should be present to load an external extension called `partials`; let's install it. Because we'll use Docker, the command is quite long: `docker run -it --rm -v .:/public -w /public -u $(id -u):$(id -g) ghcr.io/quarto-dev/quarto:latest quarto add gadenbuie/quarto-partials`. This will create a new folder called `_extensions` with partials in it. ## Let's discover the basics The idea behind Quarto-partials is to allow writing a page like the one below i.e. I'll describe my first fictive functionality and, in the `How to run` chapter, I'll inject the content of another page: **documentation/canvas.md** ```markdown --- title: Contextual Canvas --- ## Description Imagine you're working on a complex project – perhaps writing a research paper, designing a website, or planning a marketing campaign. You have various documents open, browser tabs scattered, and maybe even some handwritten notes nearby. It can feel like juggling multiple realities at once. *Contextual Canvas* aims to streamline this by creating a dynamic, visual workspace directly within your primary application. Instead of just seeing a static file or interface, you can summon the Canvas, which intelligently pulls in relevant information based on what you're currently working on ## How to run {{< partial ../_partials/run.md >}} ``` As you can see, I'm typing the description of my feature (called "Contextual Canvas" here) and then, I've a "How to run chapter". Instead of typing the how-to here, I'll include an external file (a *template*) called `../_partials/run.md`. That one will use the Quarto-partials extension so, I can put in `../_partials/run.md` some Mustache's feature like injecting a variable. Here is the template: **\_partials/run.md** ```markdown In order to run this action, please run `{{ command }}`. Existing flags: * `--debug`: output debug information, * `--no-color`: disable ANSI colors and * `--verbose`: enable verbose mode; showing more information on screen ``` Here comes Mustache in action: as you can see on the first line, the syntax `{{ command }}` will output the content of a variable called `command`. Of course, I need to declare it. Let's review my documentation and add what Quarto call a *frontmatter* YAML block: **documentation/canvas.md** ```markdown --- title: Contextual Canvas partial-data: command: "make canvas" --- ## Description Imagine you're working on a complex project – perhaps writing a research paper, designing a website, or planning a marketing campaign. You have various documents open, browser tabs scattered, and maybe even some handwritten notes nearby. It can feel like juggling multiple realities at once. *Contextual Canvas* aims to streamline this by creating a dynamic, visual workspace directly within your primary application. Instead of just seeing a static file or interface, you can summon the Canvas, which intelligently pulls in relevant information based on what you're currently working on ## How to run {{< partial ../_partials/run.md >}} ``` Time to create our website. Please run `docker run -it --rm -v .:/public -w /public -u $(id -u):$(id -g) ghcr.io/quarto-dev/quarto:latest quarto render` — the same rendering already teased at the top of this article. This will call the `render` command of Quarto. Quarto will then process the `_quarto.yml` file, see we want to produce a website and start HTML rendering. It'll take just one or two seconds. Now, by running `docker run -d --name partials -p 8080:80 -v .:/usr/local/apache2/htdocs/ httpd:alpine`, we'll create an Apache container accessible on port 8080. Finally, surf to `http://localhost:8080/documentation/canvas.html` and we'll see our website. And now, we're ready to copy/paste the file `documentation/canvas.md` to `documentation/feature_2.md` and just update the command variable. And do this again and again until we've documented all features. > Screenshot — http\://localhost:8080/documentation/canvas.html > > Our Canvas feature Let's prove the template is reusable. We'll create a new feature: **documentation/builder.md** ```markdown --- title: Intent-Driven Interface Builder partial-data: command: "make builder" --- ## Description Imagine the process of building user interfaces. Developers and designers often spend significant time translating conceptual designs and user stories into the specific widgets, layouts, and interactions of a software application. *Intent-Driven Interface Builder* aims to revolutionize this by allowing users to describe the intent of a UI element or section, rather than manually configuring every pixel and property. The *Intent-Driven Interface Builder* would then leverage AI and a vast library of pre-built, adaptable UI components and design patterns to automatically generate the corresponding interface elements. It would intelligently choose appropriate widgets, arrange them in a logical layout, and even suggest relevant styling based on the application's overall design language and best practices. ## How to run {{< partial ../_partials/run.md >}} ``` and render our site again by running again `docker run -it --rm -v .:/public -w /public -u $(id -u):$(id -g) ghcr.io/quarto-dev/quarto:latest quarto render`. > Screenshot — http\://localhost:8080/documentation/feature\_2.html > > Our second feature ### What Have We Just Done We've created a reusable template. Our document has two parts; a YAML section (the frontmatter) where we'll provide some variables. The second part, the Markdown one, is our basic template. We can copy that one for all our features f.i. ```markdown --- partial-data: command: "run canvas" --- ## How to run {{< partial ../_partials/run.md >}} ``` ## My use case In my own case, my documentation looks like this: **/documentation/php\_lint.md** ```markdown --- title: PHP linter categories: [linting, php] order: 1 partial-data: command: "lint-php" config_file: ".phplint.yml" config_url: "https://github.com/tengattack/phplint/blob/master/.phplint.yml" constant: "PHPLINT" type: "PHP" --- ## Description This is a brief description of the functionality. This text is hardcoded because it is different each time, from one feature to another. ## For what type of project {{< partial ../_partials/project_type.md >}} ## How to run {{< partial ../_partials/run.md >}} ## How to configure {{< partial ../_partials/configure/file.md >}} ## Attention points None. ## Remarks None. ``` ## Testing if a Variable Is Defined or Not (optional — skip if your templates don't need conditionals) Let's take a look at the `is_for.md` file. **/\_partials/project\_type.md** ```markdown {{#type}} This job only for **{{ type }}** project. {{/type}} {{^type}} This stage is for **all** type of projects. {{/type}} ``` There are two syntaxes used here: `{{#` and `{{^`. The first one will check the presence of a variable called `type` and if that one is defined, the block will be processed. The second one is called *Inverted section* and will check the absence of the variable so if `type` is not defined, that block will be parsed. If you look at my `php_lint.md` file, I do have a variable `type` defined in my `partial-data` section (the one used by Quarto-partials). **/documentation/php\_lint.md** ```markdown --- title: PHP linter categories: [linting, php] order: 1 partial-data: command: "lint-php" config_file: ".phplint.yml" config_url: "https://github.com/tengattack/phplint/blob/master/.phplint.yml" name: "PHP Linter" type: "PHP" --- ## Description This is a brief description of the functionality. This text is hardcoded because it is different each time, from one feature to another. ## For what type of project {{< partial ../_partials/project_type/is_for.md >}} ## How to run {{< partial ../_partials/run.md >}} ## How to configure {{< partial ../_partials/configure/file.md >}} ## Attention points None. ## Remarks None. ``` If I render my file using the command line `quarto render`, I'll then see `This job only for **PHP** project.` in my documentation. In case my feature was for all project types, I can just remove the `type: "PHP"` line from my frontmatter. In that case, I'll see `This stage is for **all** type of projects.`. Let's take a look at the next included file: **../\_partials/configure/file.md** ```markdown {{#config_file}} {{ name }} uses a settings file named .config/{{ config_file }} ([learn more]({{ config_url }})). {{/config_file}} {{^config_file}} There is no configuration file. {{/config_file}} ``` Same idea. If a `config_file` key is defined in the documentation frontmatter, we'll obtain `PHP Linter uses a settings file named .config/.phplint.yml ([Learn more](https://github.com/tengattack/phplint/blob/master/.phplint.yml)).`. If we remove the `config_file` line, we'll then get `There is no configuration file.`. ## Raw Contents (optional — skip unless a rendered value looks wrongly escaped) When displaying a variable, sometimes we need to disable the normal echo mode and use what Mustache calls `raw content`. In the example below, I've used a character `/` that will be escaped by Mustache. ```yaml partial-data: output: ".output/coverage" ``` ```markdown {{#output}} The report will be created into your project's directory in a folder called `{{ output }}`. {{/output}} ``` The HTML rendering here will return `.output/coverage` and not `.output/coverage` as expected. As we can see, Mustache has escaped our slash. So, we have to use the raw content mode and use the `{{& output }}` syntax instead. ```markdown {{#output}} The report will be created into your project's directory in a folder called `{{& output }}`. {{/output}} ``` ## Conclusion One `_quarto.yml` extension line, a couple of partial templates, and nearly 80 near-identical documentation pages stop being 80 copy-pasted Markdown files — they become one template plus one small frontmatter block per page. See [Quarto - How I Built a Self-Documenting Ecosystem for 50+ Projects](/blog/quarto-industrialisation) for how this grew into a full pipeline, and [my favorite Quarto extensions](/blog/quarto-extensions) for more building blocks like this one. --- # Make your Quarto project Devcontainer-Ready — No More Setup Headaches > Convert any Quarto project into a fully portable, VSCode-powered devcontainer in minutes. --- ![Make your Quarto project Devcontainer-Ready — No More Setup Headaches](/img/v2/devcontainer_quarto.webp) **TL;DR** This article details how to convert any Quarto project into a portable, VSCode-powered devcontainer, eliminating setup headaches. By adding three specific configuration files (`compose.yaml`, `Dockerfile`, `devcontainer.json`), users can create a fully equipped development environment. This setup includes pre-installed Quarto, configured VSCode extensions, and features like automatic preview and optional pre-commit hooks, simplifying documentation workflow. I love using Quarto and I've plenty of *documentation* projects on my disk. I'm writing user documentation, developer ones and, too, installation guide for almost every project I'm coding. If you've never run Quarto through Docker before, [Running Quarto Markdown in Docker](/blog/docker-quarto) is a good starting point; this article builds on that idea to give you a full VSCode devcontainer. I quickly came up with the idea of creating a Docker image so that I could develop my documentation in peace, as well as a devcontainer configuration for VSCode. Now, when I want to write my documentation, in less than a minute, I can create an environment entirely dedicated to writing documentation. Among other things, I install Quarto, install and configure a number of extensions for VSCode, and launch Quarto in preview mode: as soon as I save a change, Quarto regenerates the documentation on its own. Let's see how to do this; you'll see, it's so simple. *The same devcontainer, specialized for slideshows, is used in [Level Up Your Presentations with Quarto, reveal.js, Decktape, Docker and DevContainers](/blog/running-revealjs-with-docker); and taken to industrial scale in [Quarto - How I Built a Self-Documenting Ecosystem for 50+ Projects](/blog/quarto-industrialisation).* **Quick Jump:** [Let's get a simple Quarto project first](#lets-get-a-simple-quarto-project-first) | [We'll need to create three files](#well-need-to-create-three-files) ## Let's get a simple Quarto project first For sure, if you've one, just jump in your project. Here, for this blog post, we'll just reuse an existing one. By running the commands below in a console, you'll download just the `brand` folder from the `quarto-examples` project: ```bash $ cd /tmp git clone --filter=blob:none --sparse https://github.com/quarto-dev/quarto-examples cd quarto-examples git sparse-checkout set brand cd brand/brand-simple ``` Run `code .` to open the project in VSCode and you'll get this: ![Opening the brand-simple project in VSCode](./images/opening-in-vscode.webp) Here's where we're headed: open this same project as a devcontainer, run `quarto preview .`, and the site is already live — no Quarto install on your host required. ![Running quarto preview](./images/running_in_preview_mode.webp) ![The site is already running](./images/preview.webp) Crazy easy no? Let's build it, starting with three files. ## We'll need to create three files To get the best performance in terms of Docker image build speed and use of the Docker cache mechanism, we will need to create three files: The `.devcontainer/compose.yaml` file is necessary to tell VSCode to create our Docker image if it does not already exist, or to reuse it if it does. This file is therefore essential in terms of performance. The `.devcontainer/Dockerfile` file defines our Docker image: the binaries we need, user configuration, etc. The `.devcontainer/devcontainer.json` file is used by VSCode to understand and build our working environment. **Project: /tmp/quarto-examples/brand/brand-simple** **.devcontainer/compose.yaml** ```yaml --- # cspell:ignore tinytex services: quarto-doc-base: image: quarto-devcontainer:latest build: context: . dockerfile: Dockerfile args: # We use default values here. The actual UID/GID mapping is handled # by the VS Code Dev Container CLI at runtime, overriding these defaults # when the container starts via the user: vscode directive in the Dockerfile. USER_UID: 1000 USER_GID: 1000 # Flags you'll need to adjust depending on your project INSTALL_CHROMIUM: "true" INSTALL_CSPELL: "false" INSTALL_GRAPHVIZ: "true" INSTALL_PRECOMMIT_HOOKS: "false" user: vscode working_dir: /workspace volumes: - ..:/workspace:cached - ./history/.bash_history:/home/vscode/.bash_history:rw - type: bind source: ${HOME}/.quarto target: /home/vscode/.quarto - type: bind source: ${HOME}/.ssh/id_ed25519 target: /home/vscode/.ssh/id_ed25519 read_only: true # Base command to keep the container running indefinitely command: /bin/sh -c "while sleep 1000; do :; done" ``` **.devcontainer/Dockerfile** ```docker # syntax=docker/dockerfile:1.4 # cspell:disable FROM ghcr.io/quarto-dev/quarto:1.8.25 AS base ARG USERNAME=vscode ARG USER_UID=1000 ARG USER_GID=1000 ARG INSTALL_CHROMIUM=false ARG INSTALL_CSPELL=false ARG INSTALL_GRAPHVIZ=false ARG INSTALL_PRECOMMIT_HOOKS=false # Yaml tooling ARG YQ_VERSION=v4.42.1 # ---------- Stage 1: Install system dependencies ---------- # # openssh-client : used by git when pushing/pulling using git@ protocol # sudo : to easily install additional dependencies if needed # texlive-* : to render as PDF using "quarto render . --to pdf" # yamllint : to validate YAML frontmatter in .qmd files RUN --mount=type=cache,target=/var/cache/apt \ --mount=type=cache,target=/var/lib/apt \ set -eux; \ apt-get update && \ apt-get install -y --no-install-recommends \ ca-certificates \ curl \ git \ libnss3 libfontconfig1 libgbm1 \ libsecret-1-dev \ libx11-dev \ libxkbfile-dev \ openssh-client \ texlive-xetex texlive-fonts-recommended texlive-plain-generic \ unzip \ sudo \ yamllint \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Allow our user to run sudo without password RUN echo "${USERNAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers # Install yq (YAML validator) for validating YAML frontmatter RUN --mount=type=cache,target=/var/cache/yq \ set -eux; \ curl -L --fail --silent --show-error \ https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_amd64 \ -o /var/cache/yq/yq_linux_amd64; \ install -m 0755 /var/cache/yq/yq_linux_amd64 /usr/local/bin/yq # ---------- Stage 2: Extras - pre-commit, Node.js + cspell ---------- FROM base AS extras ARG INSTALL_CSPELL ARG INSTALL_GRAPHVIZ ARG INSTALL_PRECOMMIT_HOOKS ARG USERNAME # Install pre-commit (without installing hooks at build time) RUN --mount=type=cache,target=/root/.cache/pip \ if [ "$INSTALL_PRECOMMIT_HOOKS" = "true" ]; then \ set -eux; \ echo "Installing Python and pre-commit..." && \ apt-get update && \ apt-get install -y --no-install-recommends python3-pip && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* && \ pip3 install --no-cache-dir --upgrade pip && \ pip3 install --no-cache-dir pre-commit && \ pip cache purge || true; \ fi # Install Node.js + cspell (optional) RUN --mount=type=cache,target=/home/${USERNAME}/.npm \ if [ "${INSTALL_CSPELL}" = "true" ]; then \ set -eux; \ curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ apt-get install -y --no-install-recommends nodejs && \ npm config set cache /home/${USERNAME}/.npm && \ npm install -g cspell && \ npm cache clean --force && \ apt-get clean && \ rm -rf /var/lib/apt/lists/*; \ fi # Install Graphviz for Quarto (dot diagrams support) RUN --mount=type=cache,target=/var/cache/apt \ --mount=type=cache,target=/var/lib/apt \ if [ "${INSTALL_GRAPHVIZ}" = "true" ]; then \ set -eux; \ apt-get update \ && apt-get install -y --no-install-recommends \ libcairo2 \ libpango-1.0-0 \ graphviz \ && apt-get clean \ && rm -rf /var/lib/apt/lists/*; \ fi # ---------- Stage 3: Finalization ---------- FROM extras AS final ARG USERNAME ARG USER_UID ARG USER_GID ARG INSTALL_CHROMIUM # ---------- User and permissions -------------------------------------------- RUN set -eux && \ USER_UID=${USER_UID:-1000} && \ USER_GID=${USER_GID:-1000} && \ groupadd -g "${USER_GID}" "${USERNAME}" && \ useradd -m -u "${USER_UID}" -g "${USER_GID}" -s /bin/bash "${USERNAME}" && \ chown -R "${USERNAME}":"${USERNAME}" "/home/${USERNAME}" # Create .ssh directory and set permissions for the non-root user RUN set -eux && \ mkdir -p /home/${USERNAME}/.ssh && \ chown -R "${USERNAME}":"${USERNAME}" "/home/${USERNAME}/.ssh" && \ chmod 700 /home/${USERNAME}/.ssh # Switch to non-root user for Chromium and Quarto tools USER ${USERNAME} # Install Quarto Chromium tool (optional) RUN --mount=type=cache,target=/home/${USERNAME}/.quarto/tools \ if [ "${INSTALL_CHROMIUM}" = "true" ]; then \ set -eux; \ echo "Installing Quarto's Chromium tool..." && \ quarto install tool chromium; \ fi # ---------- Shell and history setup ----------------------------------------- # Configure bash to use this file RUN echo "export HISTFILE=/home/${USERNAME}/.bash_history" >> "/home/${USERNAME}/.bashrc" && \ echo 'export HISTSIZE=10000' >> "/home/${USERNAME}/.bashrc" && \ echo 'export HISTFILESIZE=10000' >> "/home/${USERNAME}/.bashrc" && \ echo 'shopt -s histappend' >> "/home/${USERNAME}/.bashrc" && \ echo "alias render_graphviz='find . -name \"flow.dot\" -exec sh -c '\''for f; do dir=\$(dirname \"\$f\"); dot -Tpng \"\$f\" -o \"\$dir/flow.png\"; done'\'' sh {} +'" >> "/home/${USERNAME}/.bashrc" # Set up the shell prompt RUN set -eux && \ echo "PS1='\n\e[0;33m🐳 \e[0;36m\$(whoami)\e[0m \w # '" >> "/home/${USERNAME}/.bashrc" # Welcome message and tips RUN set -eux && \ cat <> "/home/${USERNAME}/.bashrc" echo -e "\n📘 Welcome to the Quarto Docs Dev Container!" echo -e "" echo -e "Below are some useful commands:" echo -e "" echo -e " \e[1mQuarto Commands\e[0m" echo -e " ───────────────" echo -e " ✨ \e[32mquarto preview .\e[0m Start a hot-reloading preview in your browser." echo -e " ✨ \e[32mquarto render .\e[0m Build the documentation (based on _quarto.yml)." echo -e " ✨ \e[32mquarto render . --profile docx --to docx\e[0m Build a .docx file (requires _quarto-docx.yml)." EOF # Append conditional commands depending on optional tools RUN set -eux && \ if [ "${INSTALL_CSPELL}" = "true" ] || [ "${INSTALL_PRECOMMIT_HOOKS}" = "true" ]; then \ echo -e 'echo -e ""' >> "/home/${USERNAME}/.bashrc"; \ echo -e 'echo -e " \e[1mData quality checks\e[0m"' >> "/home/${USERNAME}/.bashrc"; \ echo -e 'echo -e " ───────────────────"' >> "/home/${USERNAME}/.bashrc"; \ fi && \ if [ "${INSTALL_CSPELL}" = "true" ]; then \ echo 'echo -e " ✍️ \e[32mcspell lint . --config .vscode/cspell.json\e[0m Run spell checks.\n"' >> "/home/${USERNAME}/.bashrc"; \ fi && \ if [ "${INSTALL_PRECOMMIT_HOOKS}" = "true" ]; then \ echo 'echo -e " ⚙️ \e[32mpre-commit run --all-files\e[0m Run code quality checks on all files.\n"' >> "/home/${USERNAME}/.bashrc"; \ fi # Tools RUN set -eux && \ if [ "${INSTALL_GRAPHVIZ}" = "true" ]; then \ echo 'echo -e ""' >> "/home/${USERNAME}/.bashrc"; \ echo 'echo -e " \e[1mTools\e[0m"' >> "/home/${USERNAME}/.bashrc"; \ echo 'echo -e " ─────"' >> "/home/${USERNAME}/.bashrc"; \ echo 'echo -e " 📊 \e[32mrender_graphviz\e[0m Scan your documentation, process .dot files and generate .png ones.\n"' >> "/home/${USERNAME}/.bashrc"; \ fi ``` **.devcontainer/devcontainer.json** ```json { "name": "Quarto Docs Project", "dockerComposeFile": "compose.yaml", "service": "quarto-doc-base", "workspaceFolder": "/workspace", "remoteUser": "vscode", "containerEnv": { "GIT_SSH_COMMAND": "ssh -i /home/vscode/.ssh/id_ed25519 -o 'UserKnownHostsFile=/dev/null'" }, "initializeCommand": "mkdir -p .devcontainer/history && touch .devcontainer/history/.bash_history && echo '.bash_history' > .devcontainer/history/.gitignore", "postAttachCommand": "if which pre-commit >/dev/null 2>&1; then pre-commit install || true; fi", "forwardPorts": [ 8000 ], "customizations": { "vscode": { "settings": { "[dockerfile]": { "files.eol": "\n", "editor.defaultFormatter": "ms-azuretools.vscode-docker" }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" }, "[jsonc]": { "editor.defaultFormatter": "vscode.json-language-features", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[markdown]": { "editor.defaultFormatter": "yzhang.markdown-all-in-one", "editor.wordWrap": "wordWrapColumn", "editor.wordWrapColumn": 80, "editor.wrappingIndent": "indent" }, "[yaml]": { "editor.defaultFormatter": "redhat.vscode-yaml" }, "cSpell.language": "en,fr,nl", "cSpell.words": [ "Quarto", "devcontainer", "frontmatter", "Pandoc", "Gitlens" ], "editor.formatOnSave": true, "editor.guides.bracketPairs": "active", "editor.guides.bracketPairsHorizontal": "active", "editor.guides.highlightActiveIndentation": true, "editor.guides.indentation": true, "editor.multiCursorModifier": "ctrlCmd", "editor.renderWhitespace": "all", "editor.rulers": [ 120, 70 ], "editor.stickyScroll.enabled": true, "editor.tabCompletion": "on", "editor.tabSize": 4, "editor.wordBasedSuggestionsMode": "allDocuments", "editor.wordWrapColumn": 120, "explorer.compactFolders": false, "explorer.confirmDelete": true, "explorer.confirmDragAndDrop": true, "extensions.autoCheckUpdates": false, "files.associations": { "resources/*.json": "jsonc" }, "files.autoSave": "onFocusChange", "files.defaultLanguage": "${activeEditorLanguage}", "files.eol": "\n", "files.exclude": { "**/.cache": true, "**/.git": true, "**/.quarto": true, "**/*.html": true, "**/readme_files/**": true, "**/site_libs/**": true }, "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "maptz.regionfolder": { "[dockerfile]": { "foldEnd": "[\\S]*\\#[\\s]*endregion", "foldEndRegex": "[\\S]*\\#[\\s]*endregion[\\s]*(.*)$", "foldStart": "[\\S]*\\#[\\s]*region [NAME]", "foldStartRegex": "[\\S]*\\#[\\s]*region[\\s]*(.*)$" } }, "markdown.extension.toc.levels": "2..6", "markdown.extension.tableFormatter.enabled": true, "markdownlint.config": { "MD033": false, "MD036": false }, "quarto.preview.renderOnSave": true, "telemetry.telemetryLevel": "off", "terminal.integrated.profiles.linux": { "bash": { "path": "/bin/bash", "icon": "terminal-bash" } }, "terminal.integrated.defaultProfile.linux": "bash", "terminal.integrated.fontFamily": "MesloLGS NF", "terminal.integrated.tabs.defaultLocation": "editorGroup", "yaml.schemas": { "https://json.schemastore.org/json-schema-v7.json": [ "_quarto.yml", "_quarto-*.yml" ], "https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml" }, "window.title": "${dirty}${rootName} ${separator} [DevContainer]", "workbench.colorTheme": "Visual Studio Light" }, "extensions": [ "DavidAnson.vscode-markdownlint", "esbenp.prettier-vscode", "gitkraken.gitlens", "maptz.regionfolder", "mde.select-highlight-minimap", "mikestead.dotenv", "ms-vscode.makefile-tools", "quarto.quarto", "redhat.vscode-yaml", "sirtori.indenticator", "streetsidesoftware.code-spell-checker-dutch", "streetsidesoftware.code-spell-checker-french", "streetsidesoftware.code-spell-checker", "yzhang.markdown-all-in-one" ] } } } ``` > **Information:** > > Strictly speaking, we don't need the `.devcontainer/compose.yaml` file but this is the only way to build the Docker image **once** and reuse it across projects. > > By opening a Devcontainer, even if the `Dockerfile` is strictly the same across your documentation projects, the context will be different (**project1**, **project2**, ...) and VSCode will rebuild the image for that context. To avoid this, we need to build the image and **give it a name**; this can only be done using a `.devcontainer/compose.yaml` file. ## Extra Information (optional — skip if the defaults work for you) ### Installation of sudo During the creation of the Docker image, we also install the `sudo` command and allow our `vscode` user to run `sudo su root` in the devcontainer without having to fill in a password. The reason here is that, sometimes, Quarto will complain about a missing dependency (like when we are exporting to PDF and there is a missing library). To allow the developer to quickly add the dependency and do some tests without having to rebuild the container again and again, `sudo` is installed. ## Build Arguments (optional — read only the ones you need) ### Installation of Chromium If you pay attention to `.devcontainer/compose.yaml` file, you'll see an argument called `INSTALL_CHROMIUM`. Depending on your project, you'll need it or not. Indeed, under certain circumstances, when rendering your documentation to Word (i.e. by running f.i. `quarto render . --profile docx --to docx`), Quarto can ask you to install Chromium. To avoid doing this every time, simply open the `.devcontainer/devcontainer.json` file, search for `INSTALL_CHROMIUM` and initialize it to `true`. Note: if you change the `.devcontainer/Dockerfile` code or the `.devcontainer/devcontainer.json` file, you'll need to rebuild the container as explained here below. > **Make sure you need it:** > > Before installing Chromium, make sure you need it i.e. first render your documentation without it and see if Quarto complains about Chromium. This is because Chromium requires a lot of dependencies and it will make your Docker image size much bigger. ### Installation of Code Spell Checker The same way, you have a variable called `INSTALL_CSPELL`. Initialize it to `true` if you want to install the Code-spell check tool (requires Node.js). ### Installation of GraphViz The same way, you have a variable called `INSTALL_GRAPHVIZ`. Initialize it to `true` if you want to install the GraphViz converter. This is needed when, in your `.qmd` file, you have a graph like this one: ```markdown digraph flow { rankdir = LR; A [shape=box, label="Source"]; B [shape=ellipse, label="Processor"]; C [shape=box, label="Destination"]; A -> B; B -> C; } ``` > **GraphViz:** > > If you have a Graphviz graph and didn't install GraphViz, you'll get an error like this: `ERROR: AssertionError: Error occurred during cleanup: TypeError: Child process has already terminated`. ### Installation of pre-commit-hooks The third variable is `INSTALL_PRECOMMIT_HOOKS` and, if your documentation has its own `.git` folder, it'll be a good idea to initialize the variable to `true` so, when committing your changes, a few data quality controls / formatting tools will be applied. ## Opening our project as a Devcontainer Right now, just press `F1` (or `CTRL`+`SHIFT`+`P`) and select **Dev Containers: Rebuild and Reopen in Container**. If you don't have that command, please make sure to install the `ms-vscode-remote.remote-containers` extension from Microsoft. > **The remote container of Microsoft:** > > You can install the extension from the console by running `code --install-extension ms-vscode-remote.remote-containers` or by opening this page: [Marketplace - Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers). So, once you've fired the **Dev Containers: Rebuild and Reopen in Container** command, VSCode will close your current session and open a new one. The very first time, VSCode will have to make some initializations (like building the Docker image and downloading extensions so it'll be slower). After a few seconds, you'll get a screen like this: ![The Devcontainer has been opened](./images/devcontainer.webp) 1. See bottom left, you have, in the status bar, the text "Dev Container: xxx" (followed by the name of our container as configured in the `devcontainer.json` file). 2. In the **Terminal window** (if you don't see it press `CTRL`+`ù`), you'll see a cheatsheet with main commands like `quarto preview .` to run the site with hot reload. So, still in the Terminal, please type `quarto preview .` then press `Enter` — you'll get the same output already teased at the top of this article. In theory, your browser will be automatically fired and the site opened, showing the same result already shown earlier. If not, just follow the link by pressing `ALT` and clicking on the link. Crazy easy no? ## What have we done? At this stage, what have we done? We retrieved a Quarto website project and made sure to “dockerize” it. Let's see the `_quarto.yml` file of the project we've just cloned (you'll find more information on the official Quarto site: [Creating a website](https://quarto.org/docs/websites/)): **\_quarto.yml** ```yaml project: type: website website: title: "Simple Brand Example" navbar: title: "Demo CO" tools: - icon: github href: https://github.com/quarto-dev/quarto-examples/tree/main/brand/brand-simple ``` We've added three files to the project in a special folder called `.devcontainer`. That folder will tell VSCode that we would like to use a Docker container when working on that project. The container will be based on a custom Docker image (as coded in `.devcontainer/Dockerfile`). The image will reuse the official Quarto image. So, without having to install Quarto on our computer, we'll be able to use all features of Quarto. Finally, we have to *switch to the devcontainer* i.e. just after we've opened VSCode, we have to run a special command to reopen the project as a devcontainer. That command is, the first time, **Dev Containers: Rebuild and Reopen in Container** but as soon as the custom image has been created once, we can simply use **Dev Containers: Reopen in Container**. ## Writing our documentation So far, we've already fired `quarto preview .` in a terminal. The documentation was rendered and our website is already displayed (on `http://localhost:7519` or any other port). Let's check the hot reload feature: please edit the `index.qmd` file you'll retrieve in the root folder and change something. For instance, change the word `Overview` to `Preface` and save. ![The synchronization is already enabled](./images/synchronization-enabled.webp) As you can see on the image here above, I just need to make a change, save my file and wait less than one second and the website is updated automatically. Nothing to do (I don't need to refresh the page). Easy no? ## Using the terminal Go to the **Terminal window** (if you don't see it press `CTRL`+`ù`). Look now at the right part: there is a `Configuring...` terminal but too a `bash workspace` (the name of our project). If you don't see that one, look just above `Configuring...` and click on the `+` button. ![The terminal](./images/terminal.webp) As you can see, you'll see a few *instructions*. These instructions were coded in the final part of the `.devcontainer/Dockerfile` file. They are there just to help the first time user. So, if your objective is to render the final documentation, you can always see *Oh yes, I simply need to run `quarto render .`* ## What is pre-commit? In this project, I've planned the installation of [pre-commit](https://pre-commit.com/). This is an optional tool that will run some validation controls on your project before you push it to your versioning system (like GitHub or GitLab). *I've dedicated a full article to it: [Git - pre-commit-hooks](/blog/git-precommit).* By adding `pre-commit` in the devcontainer, you'll get the privilege to never again commit files with some fault like f.i. formatting issues in your Markdown content. If you like this idea, please create the `.pre-commit-config.yaml` file in your project's root folder (so not in the `.devcontainer` folder but its parent folder). Just copy/paste the content below: **.pre-commit-config.yaml** ```yaml # Pre-commit configuration for Quarto projects exclude: "^.build/|^.git/|^.quarto/" repos: # --- Universal hooks for clean files --- - repo: https://github.com/pre-commit/pre-commit-hooks rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict - id: check-json - id: check-merge-conflict - id: check-yaml - id: detect-private-key - id: end-of-file-fixer - id: trailing-whitespace # Markdown linter - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.45.0 hooks: - id: markdownlint args: - "--config=.config/.markdownlint.yaml" # Checks all of the hyperlinks in a markdown text to determine if they are alive or dead - repo: https://github.com/tcort/markdown-link-check rev: v3.13.7 hooks: - id: markdown-link-check name: Check Markdown Links entry: markdown-link-check language: node types: [markdown] # The hook below hook extracts the YAML front matter from each .qmd # file and validates its syntax using yq. # It helps catch formatting errors (e.g. missing colons, invalid keys) # before committing. # Only the YAML block between the first pair of --- lines is checked. - repo: local hooks: - id: validate-qmd-yaml name: Validate YAML front matter in .qmd entry: bash -c 'awk "/^---/{f=!f;next}f" "$1" | yq eval "." - > /dev/null' language: system pass_filenames: true args: ["{}"] files: \.qmd$ # --- Spelling --- # Requires to install cspell in the Docker file as a binary. This implies to install NPM tool # The .vscode/cspell.json file is shared with VSCode - repo: local hooks: - id: cspell name: Spell check with cspell entry: cspell language: system args: - "--config=.vscode/cspell.json" - "--no-progress" - "--no-summary" exclude: ^.*_extensions/.* ``` Once this file is in place, when you fire the `git commit` command from the terminal (`CTRL`+`ù`), `git` will first run `pre-commit` to run *hooks*. In the `.pre-commit-config.yaml` file above, we've defined two hooks: the first one to make generic checks on files, then a second one to ensure there are no linting errors with `.md` files (just check the official repo for more info). These hooks are fired every time you commit files (and just on the committed files; not the entire project). You can also force hooks to run without committing by running `pre-commit run --all-files` in the console. This command will check all files. `pre-commit` is a valuable tool for ensuring compliance with established industry quality standards. ## Conclusion With just three files (`.devcontainer/compose.yaml`, `.devcontainer/Dockerfile` and `.devcontainer/devcontainer.json`), we've converted a simple project to a Devcontainer where Quarto is preinstalled, extensions installed and configured and some features like the automatic preview is running. Cool no? --- # Quarto - How I Built a Self-Documenting Ecosystem for 50+ Projects > A deep dive into how I engineered a fully automated documentation ecosystem using Quarto and Docker, transforming documentation from a chore into a high-performance build pipeline. --- ![Quarto - How I Built a Self-Documenting Ecosystem for 50+ Projects](/img/v2/quarto-industrialisation.webp) **TL;DR** This blog post explores how I built a self-documenting ecosystem for over 50 projects using Quarto and Docker, automating the documentation process and ensuring it stays up-to-date with the codebase. As a developer, I like writing code and documentation, but I need to be honest: it's difficult to keep documentation up-to-date. The moment you write it, it becomes outdated. This is a problem I've faced repeatedly throughout my career. I wanted to find a way to automate the documentation process, ensuring that it always reflects the current state of the codebase. You build a beautiful API, you write a manual, and then… you change your code. The structure of your answer is quite different, the error message is no longer the same, you've published a v1.1 while your documentation was still about v1.0, and many things like that. What can I do to try to solve this problem? Can we automate the documentation process? Can we make it self-updating? Can we make it so that when I change my code, my documentation changes too? The short answer is: yes, we can. And that's what I did with [Quarto](https://quarto.org/). Let's see how we can industrialize our documentation process with Quarto and Docker, creating a self-documenting ecosystem that scales across multiple projects. **Quick Jump:** [The "Wow" Moment](#-the-wow-moment) | [Installation — The Wow Lab](#installation--the-wow-lab-build-your-first-doc-engine-in-5-minutes) ## 🚀 The "Wow" Moment Once the lab is running (covered under Installation below), open the folder in VS Code, click **"Reopen in Container"**, then in the terminal type: `./scripts/preview.sh`. You'll get something like this: ![Preview Script](./images/devcontainer.webp) Do a `Ctrl + Click` on the URL (`http://localhost:`4242), and it will open your browser with the rendered Quarto document. You should see the Mermaid diagrams generated by our Python scripts. > Screenshot — http\://localhost:4242 > > ![Automated Infrastructure Map](./images/automated_infrastructure_map.webp) Great no? But where does this diagram come from? It's generated by the `render_project_arch()` function in our `project_viz.py` script. That script looks at our `compose.yaml` and our `Dockerfile` to understand the structure of our project and generates a Mermaid diagram based on that. Let's look at the output of our `file_stats.py` script. It analyzes the files in our project and generates a report that is rendered directly in our Quarto document. ![File Stats](./images/file_stats.webp) This is in real-time. If you add a new file to the project, save it, and refresh the browser, you'll see the new file appear in the stats and the architecture diagram update accordingly. The `my_feature.py` script generates a simple Mermaid diagram based on a list of items. You can change the list in the script, save it, and see the diagram update in real-time. ![My Feature](./images/my_feature.webp) And last but not least, the `call_api.py` script can be used to call a live API during the render process. This means you can pull real data from your production or staging environment and include it in your documentation, all while keeping your credentials secure with the Secrets Resolver. ![Call API](./images/call_api.webp) ## Why It Works Most people think Doc-as-Code is just "saving Markdown in Git." It's not. It's about **runtime execution**. * A Quarto document isn't static Markdown — a `{python}` code block embedded in the document actually **executes during render**, not before it. * `output: asis` tells Quarto to treat whatever that Python function prints as raw Markdown (or Mermaid) instead of literal text — so a function that returns a diagram becomes a rendered diagram, not a code listing. * Because the diagram is generated from the *live* `compose.yaml` and `Dockerfile`, changing the infrastructure and re-rendering updates the diagram automatically — no manual redraw, ever. ## Installation — The "Wow" Lab: Build Your First Doc-Engine in 5 Minutes Here is a concrete, ready-to-use setup. This Dockerfile creates an environment where Quarto can "talk" to custom Python scripts to generate Mermaid diagrams on the fly. *The Python-to-Mermaid part alone is detailed in [Documentation as Code - Transform Your Infrastructure into Beautiful Diagrams with Python and Mermaid](/blog/docker-python-mermaid); here we plug it into Quarto.* ### 1. The Orchestrator (compose.yaml) **compose.yaml** ```yaml services: docs: build: . volumes: - .:/workspace ports: - "4242:4242" environment: - PYTHONPATH=/workspace/scripts command: > bash -c "chmod +x /workspace/scripts/preview.sh && sleep infinity" ``` ### 2. The Engine (`Dockerfile`) We install Quarto and configure the `PYTHONPATH` so our document can import our custom "Feature" library from anywhere. **Dockerfile** ```docker # Use a clean Ubuntu base FROM ubuntu:22.04 # Install Dependencies & Quarto RUN apt-get update && apt-get install -y --no-install-recommends curl python3 python3-pip && \ rm -rf /var/lib/apt/lists/* RUN curl -L -o quarto.tar.gz https://github.com/quarto-dev/quarto-cli/releases/download/v1.4.550/quarto-1.4.550-linux-amd64.tar.gz \ && mkdir -p /opt/quarto && tar -xzf quarto.tar.gz -C /opt/quarto --strip-components=1 \ && ln -s /opt/quarto/bin/quarto /usr/local/bin/quarto WORKDIR /workspace # Disable Python bytecode generation to keep image layers lean ENV PYTHONDONTWRITEBYTECODE=1 # Force Python stdout and stderr streams to be unbuffered for immediate logging ENV PYTHONUNBUFFERED=1 # Install Python requirements for Quarto (Jupyter & YAML) RUN pip3 install --no-cache-dir jupyter PyYAML ``` ### 3. The Experience (.devcontainer/devcontainer.json) This is where the magic happens. We tell VS Code which extensions to install and how to behave. *[Make your Quarto project Devcontainer-Ready — No More Setup Headaches](/blog/quarto-devcontainer) goes through that file line by line.* **.devcontainer/devcontainer.json** ```json { "name": "WritingDoc Lab", "dockerComposeFile": "../compose.yaml", "service": "docs", "workspaceFolder": "/workspace", "customizations": { "vscode": { "extensions": [ "quarto.quarto", "ms-python.python", "yzhang.markdown-all-in-one", "tomoyukim.vscode-mermaid-editor" ], "settings": { "files.exclude": { "index_files/": true, "index.html": true }, "quarto.renderOnSave": true, "editor.formatOnSave": true } } } } ``` ### 4. Our Python scripts We'll use a few, simple, Python scripts to generate content for our documentation. For example, `my_feature.py` contains a simple function that generates a Mermaid diagram based on a list of items, `file_stats.py` analyzes the project files and generates a report, and `project_viz.py` creates a visual representation of the project structure. **scripts/call\_api.py** ```python #!/usr/bin/env python3 """ Fetch 10 fake users from RandomUser API and display the result as a Markdown table. """ from typing import Any, Dict, List import requests API_URL: str = "https://randomuser.me/api/" def fetch_users() -> List[Dict[str, Any]]: """Call RandomUser API and return a list of users.""" params: Dict[str, Any] = {"results": 10} response: requests.Response = requests.get( API_URL, params=params, timeout=10 ) response.raise_for_status() data: Dict[str, Any] = response.json() return data["results"] def format_markdown_table(users: List[Dict[str, Any]]) -> str: """Convert users data into a Markdown table.""" headers = ["Name", "Email", "Phone", "City", "Country"] lines = [] lines.append("| " + " | ".join(headers) + " |") lines.append("| " + " | ".join(["---"] * len(headers)) + " |") for user in users: name = f"{user['name']['first']} {user['name']['last']}" email = user["email"] phone = user["phone"] city = user["location"]["city"] country = user["location"]["country"] lines.append(f"| {name} | {email} | {phone} | {city} | {country} |") return "\n".join(lines) def main() -> None: """Main program.""" users = fetch_users() markdown_table = format_markdown_table(users) print(markdown_table) if __name__ == "__main__": main() ``` **scripts/file\_stats.py** ````python import os from collections import Counter from pathlib import Path def render_extension_pie(directory="."): """Scans the directory and generates a Mermaid pie chart of file types.""" p = Path(directory) ignored_folders = {'.git', '.quarto', '__pycache__', 'node_modules'} extensions = [] for f in p.rglob('*'): if f.is_file() and not any(part in ignored_folders for part in f.parts): ext = f.suffix.lower() if f.suffix else "No extension" extensions.append(ext) stats = Counter(extensions) mermaid = ["pie title Project File Distribution"] for ext, count in stats.items(): label = ext.replace('.', '').upper() if ext.startswith('.') else ext mermaid.append(f' "{label}" : {count}') print("\n```{mermaid}") print("\n".join(mermaid)) print("```\n") ```` **scripts/my\_feature.py** ````python def generate_flow(nodes): """Generates a Mermaid flowchart from a list of names.""" mermaid = ["flowchart LR"] for i, name in enumerate(nodes): mermaid.append(f' node{i}["{name}"]') if i > 0: mermaid.append(f" node{i-1} --> node{i}") # We output "asis" Markdown so Quarto interprets it as a block print("\n```{mermaid}\n") print("\n".join(mermaid)) print("\n```\n") ```` **scripts/project\_viz.py** ````python import yaml import re from pathlib import Path def render_project_arch(): compose_path = Path("compose.yaml") docker_path = Path("Dockerfile") parent_image = "Unknown" volume_mapping = "Unknown" exposed_port = "Unknown" cmd = "Unknown" if docker_path.exists(): content = docker_path.read_text() from_match = re.search(r"FROM\s+(.+)", content) if from_match: parent_image = from_match.group(1).strip() if compose_path.exists(): with open(compose_path, 'r') as f: data = yaml.safe_load(f) svc = data.get('services', {}).get('docs', {}) volumes = svc.get('volumes', ['?']) volume_mapping = volumes[0].split(":")[0] if ":" in volumes[0] else volumes[0] ports = svc.get('ports', ['?']) exposed_port = str(ports[0]) cmd = svc.get('command', 'sleep infinity') mermaid = [ "flowchart TD", " classDef host fill:#f9f9f9,stroke:#333,stroke-dasharray: 5 5", " classDef container fill:#e1f5fe,stroke:#01579b,stroke-width:2px", " classDef image fill:#fff3e0,stroke:#e65100,stroke-width:1px", "", ' HostFolder["📁 Local Folder"]', ' UserBrowser["🌐 Web Browser"]', f' BaseImage(["📦 Parent Image: {parent_image}"]):::image', ' subgraph WritingDoc ["🚀 Documentation Container"]', ' QuartoEngine{"Quarto Engine"}', ' PythonEngine[["🐍 Python Helpers"]]', ' end', ' class WritingDoc container', "", ' HostFolder -- "Mount" --> WritingDoc', ' BaseImage -- "Extends" --> WritingDoc', ' WritingDoc -- "HTTP" --> UserBrowser' ] print("\n```{mermaid}") print("\n".join(mermaid)) print("```\n") ```` ### 5. The Document (`index.qmd`) This is where we write our Quarto document. Instead of static Markdown, we can execute Python code directly in our document to generate dynamic content. For example, we can call our `my_feature.py` script to generate a Mermaid diagram based on the current state of our code. **index.qmd** ````qmd --- title: "Project Self-Analysis" --- ## 🛠️ Automated Infrastructure Map ```{python} #| echo: false #| output: asis from project_viz import render_project_arch render_project_arch() ``` ## 📈 File System Analytics The following chart analyzes the physical structure of this DevContainer. It counts every file in our workspace by its extension. ```{python} #| echo: false #| output: asis from file_stats import render_extension_pie render_extension_pie(".") ``` ## 📊 Business Logic Flow ```{python} #| echo: false #| output: asis from my_feature import generate_flow generate_flow(["Load Balancer", "API Gateway", "Auth Service", "Database"]) ``` ## 🧑 Call API Call RandomUser API and return a list of 10 fake users. ```{python} #| echo: false #| output: asis from call_api import main main() ``` ```` See the three `{python}` blocks in the document? This is where the magic happens. That's where we execute our Python code. The output of that code is directly rendered in the document. When Quarto sees the block below, it will execute the `render_project_arch()` function from our `project_viz.py` script (that feature has to be present in the `PYTHONPATH`). This function will generate a Mermaid diagram based on the current state of our project, and Quarto will render it directly in the document. ````markdown ```{python} #| echo: false #| output: asis from project_viz import render_project_arch render_project_arch() ``` ```` The `output: asis` option tells Quarto to treat the output as raw Markdown. This means that if our Python script outputs a Mermaid diagram, Quarto will render it as a diagram instead of just displaying the code. ### The full project Here is the full project structure. You can copy this setup and run it on your machine to see how it works in practice. The easiest way to do this is to click on the `Generate install script` button then on the `Copy` button. Start a terminal and paste the command. It will create a `/tmp/quarto_industrialisation` folder with all the files and the Docker setup ready to go. **Project: /tmp/quarto\_industrialisation** > **Note:** > > Now, start vscode by running `code .` and select **Reopen in Container** when asked. In the terminal, run `./scripts/preview.sh` and open the link in your browser. Change the list in `my_feature.py` and see the diagram update in real-time. **.devcontainer/devcontainer.json** ```json { "name": "WritingDoc Lab", "dockerComposeFile": "../compose.yaml", "service": "docs", "workspaceFolder": "/workspace", "customizations": { "vscode": { "extensions": [ "quarto.quarto", "ms-python.python", "yzhang.markdown-all-in-one", "tomoyukim.vscode-mermaid-editor" ], "settings": { "files.exclude": { "index_files/": true, "index.html": true }, "quarto.renderOnSave": true, "editor.formatOnSave": true } } } } ``` **scripts/call\_api.py** ```python #!/usr/bin/env python3 """ Fetch 10 fake users from RandomUser API and display the result as a Markdown table. """ from typing import Any, Dict, List import requests API_URL: str = "https://randomuser.me/api/" def fetch_users() -> List[Dict[str, Any]]: """Call RandomUser API and return a list of users.""" params: Dict[str, Any] = {"results": 10} response: requests.Response = requests.get( API_URL, params=params, timeout=10 ) response.raise_for_status() data: Dict[str, Any] = response.json() return data["results"] def format_markdown_table(users: List[Dict[str, Any]]) -> str: """Convert users data into a Markdown table.""" headers = ["Name", "Email", "Phone", "City", "Country"] lines = [] lines.append("| " + " | ".join(headers) + " |") lines.append("| " + " | ".join(["---"] * len(headers)) + " |") for user in users: name = f"{user['name']['first']} {user['name']['last']}" email = user["email"] phone = user["phone"] city = user["location"]["city"] country = user["location"]["country"] lines.append(f"| {name} | {email} | {phone} | {city} | {country} |") return "\n".join(lines) def main() -> None: """Main program.""" users = fetch_users() markdown_table = format_markdown_table(users) print(markdown_table) if __name__ == "__main__": main() ``` **scripts/file\_stats.py** ````python import os from collections import Counter from pathlib import Path def render_extension_pie(directory="."): """Scans the directory and generates a Mermaid pie chart of file types.""" p = Path(directory) ignored_folders = {'.git', '.quarto', '__pycache__', 'node_modules'} extensions = [] for f in p.rglob('*'): if f.is_file() and not any(part in ignored_folders for part in f.parts): ext = f.suffix.lower() if f.suffix else "No extension" extensions.append(ext) stats = Counter(extensions) mermaid = ["pie title Project File Distribution"] for ext, count in stats.items(): label = ext.replace('.', '').upper() if ext.startswith('.') else ext mermaid.append(f' "{label}" : {count}') print("\n```{mermaid}") print("\n".join(mermaid)) print("```\n") ```` **scripts/my\_feature.py** ````python def generate_flow(nodes): """Generates a Mermaid flowchart from a list of names.""" mermaid = ["flowchart LR"] for i, name in enumerate(nodes): mermaid.append(f' node{i}["{name}"]') if i > 0: mermaid.append(f" node{i-1} --> node{i}") # We output "asis" Markdown so Quarto interprets it as a block print("\n```{mermaid}\n") print("\n".join(mermaid)) print("\n```\n") ```` **scripts/project\_viz.py** ````python import yaml import re from pathlib import Path def render_project_arch(): compose_path = Path("compose.yaml") docker_path = Path("Dockerfile") parent_image = "Unknown" volume_mapping = "Unknown" exposed_port = "Unknown" cmd = "Unknown" if docker_path.exists(): content = docker_path.read_text() from_match = re.search(r"FROM\s+(.+)", content) if from_match: parent_image = from_match.group(1).strip() if compose_path.exists(): with open(compose_path, 'r') as f: data = yaml.safe_load(f) svc = data.get('services', {}).get('docs', {}) volumes = svc.get('volumes', ['?']) volume_mapping = volumes[0].split(":")[0] if ":" in volumes[0] else volumes[0] ports = svc.get('ports', ['?']) exposed_port = str(ports[0]) cmd = svc.get('command', 'sleep infinity') mermaid = [ "flowchart TD", " classDef host fill:#f9f9f9,stroke:#333,stroke-dasharray: 5 5", " classDef container fill:#e1f5fe,stroke:#01579b,stroke-width:2px", " classDef image fill:#fff3e0,stroke:#e65100,stroke-width:1px", "", ' HostFolder["📁 Local Folder"]', ' UserBrowser["🌐 Web Browser"]', f' BaseImage(["📦 Parent Image: {parent_image}"]):::image', ' subgraph WritingDoc ["🚀 Documentation Container"]', ' QuartoEngine{"Quarto Engine"}', ' PythonEngine[["🐍 Python Helpers"]]', ' end', ' class WritingDoc container', "", ' HostFolder -- "Mount" --> WritingDoc', ' BaseImage -- "Extends" --> WritingDoc', ' WritingDoc -- "HTTP" --> UserBrowser' ] print("\n```{mermaid}") print("\n".join(mermaid)) print("```\n") ```` **scripts/preview\.sh** ```bash #!/bin/bash set -euo pipefail echo -e "\n\e[1;34m🚀 WritingDoc Engine is starting...\e[0m" echo -e "\e[1;32m👉 Click to view your live docs: http://localhost:4242\e[0m\n" # Run Quarto in quiet mode so the 0.0.0.0 binding notice doesn't clutter the output quarto preview index.qmd --port 4242 --host 0.0.0.0 --no-browser ``` **compose.yaml** ```yaml services: docs: build: . volumes: - .:/workspace ports: - "4242:4242" environment: - PYTHONPATH=/workspace/scripts command: > bash -c "chmod +x /workspace/scripts/preview.sh && sleep infinity" ``` **Dockerfile** ```docker # Use a clean Ubuntu base FROM ubuntu:22.04 # Install Dependencies & Quarto RUN apt-get update && apt-get install -y --no-install-recommends curl python3 python3-pip && \ rm -rf /var/lib/apt/lists/* RUN curl -L -o quarto.tar.gz https://github.com/quarto-dev/quarto-cli/releases/download/v1.4.550/quarto-1.4.550-linux-amd64.tar.gz \ && mkdir -p /opt/quarto && tar -xzf quarto.tar.gz -C /opt/quarto --strip-components=1 \ && ln -s /opt/quarto/bin/quarto /usr/local/bin/quarto WORKDIR /workspace # Disable Python bytecode generation to keep image layers lean ENV PYTHONDONTWRITEBYTECODE=1 # Force Python stdout and stderr streams to be unbuffered for immediate logging ENV PYTHONUNBUFFERED=1 # Install Python requirements for Quarto (Jupyter & YAML) RUN pip3 install --no-cache-dir jupyter PyYAML ``` **index.qmd** ````qmd --- title: "Project Self-Analysis" --- ## 🛠️ Automated Infrastructure Map ```{python} #| echo: false #| output: asis from project_viz import render_project_arch render_project_arch() ``` ## 📈 File System Analytics The following chart analyzes the physical structure of this DevContainer. It counts every file in our workspace by its extension. ```{python} #| echo: false #| output: asis from file_stats import render_extension_pie render_extension_pie(".") ``` ## 📊 Business Logic Flow ```{python} #| echo: false #| output: asis from my_feature import generate_flow generate_flow(["Load Balancer", "API Gateway", "Auth Service", "Database"]) ``` ## 🧑 Call API Call RandomUser API and return a list of 10 fake users. ```{python} #| echo: false #| output: asis from call_api import main main() ``` ```` ## My real use case In my 50-project ecosystem, I took this "Lab" concept and turned it into an industrial-strength **WritingDoc Base Image**. Most documentation setups start with a "minimal" image. That's a mistake. "Minimal" means "slow startup." I went the opposite way. My image is a 2.5GB beast, but it follows a **"Zero-Wait" philosophy**. I baked everything in: Quarto, Python, Node.js, Mermaid-CLI, and every linter known to man. When a writer opens a project in a VS Code DevContainer, they don't wait for `apt-get` or `pip install`. The environment is alive and ready the moment the window opens. If you want the beginner-friendly version of this idea, see [Make your Quarto project Devcontainer-Ready](/blog/quarto-devcontainer) — this ecosystem is that same principle taken to 50 projects. ### Doc-as-Code: The Python AST Magic The real "Wow" factor started when I stopped copy-pasting code into Markdown. I wrote a suite of Python "Features" that use **AST (Abstract Syntax Tree)** analysis to read my source code. Actually, I've more than 40 different "Features" that can analyze my codebase, extract information, and generate documentation automatically. ![Having more than 40 different "Features" that can analyze my codebase, extract information, and generate documentation automatically.](./images/having_more_than_40_features.webp) A few examples of these features: #### 🧠 Python AST Semantic Analyzers Perform Abstract Syntax Tree analysis on source code to extract logic *without executing it*. Automatically generates UML diagrams, API tables, and data dictionaries that stay in sync with the code. #### 🗄️ Live Database Introspection Connects to a live PostgreSQL instance to generate real-time ERD diagrams (Mermaid), functional domain flows, and complete data dictionaries with data previews. #### 🎨 Format-Aware Mermaid Engine Detects the render target (HTML → SVG, PDF/Word → PNG via Puppeteer at 300 DPI). Includes a fingerprint-based caching system to skip unchanged diagrams. #### 🔐 Secure Authenticated API Orchestrator Pulls live data from protected environments during the render. Handles the full OAuth2/Bearer handshake and uses a **Secrets Resolver** (`env:` prefix) so credentials never touch Git. #### 🖥️ Terminal High-Fidelity Capture Captures real CLI execution into a vector SVG image, preserving ANSI colors, ASCII borders, and layout integrity across HTML, PDF, and Word outputs. #### ⚙️ GitLab CI/CD Infrastructure Mapper Reconstructs the effective pipeline by resolving recursive `includes` and `extends`. Generates a visual architecture of CI/CD stages with human-readable job descriptions. #### 🐳 Docker Ecosystem Visualizers Maps image inheritance chains, service topology (ports, networks, volumes), and multi-stage build sequences into clean, understandable diagrams. #### 📋 "Invisible" TODO Dashboard Scans hidden HTML comments (``) across the project to generate a consolidated internal backlog — invisible in the final customer-facing PDF or Word export. #### ✨ Dynamic Workspace Assistant Automatically manages the Quarto sidebar based on file structure and provides real-time VS Code IntelliSense snippets for every variable and helper script in the project. #### 📊 Smart Multi-Format Data Viewer A polymorphic renderer for CSV, JSON, and XML. Supports horizontal comparison tables and vertical detail views, with deep node selection (XPath/Dot-notation) and TTL caching. #### 🏗️ Language-Aware Project Scaffolder Uses a subset-tagging system (`file.python.fastapi.qmd`) to instantly generate a complete, tailored documentation structure based on the specific technologies in the project. ### Visualizing the Invisible: Mermaid & Caching Quarto is able to render Mermaid.js while rendering a documentation as a website, but it will not work for offline outputs like Word or PDF. I solved this by building an **Advanced Mermaid Renderer**. In my setup, the renderer is format-aware. If you're viewing the HTML preview, it serves sharp SVGs. If you're building a `.docx` for a business stakeholder, it automatically spins up a headless Chromium instance (via Puppeteer), renders the diagram at 300 DPI, and embeds a high-res PNG. And because I hate waiting, I implemented a **Content-Aware Caching System**. Every diagram and every API snapshot is fingerprinted. If the source hasn't changed, the build skips the heavy lifting and pulls the image from the cache. My build times dropped from minutes to seconds. ### The "Living" Documentation: Authenticated API Calls I wanted my documentation to be a window into the live system. So, I built a feature that can perform **real REST API calls during the render process**. It handles the full OAuth2 handshake: it hits the authentication server, gets a Bearer token, fetches the latest data, and masks the secrets before displaying a "Terminal Trace" in the documentation. Worried about security? I built a custom **Secrets Resolver**. By using an `env:` prefix in my Quarto files, I can pull credentials from a local `.secrets` file that is never committed to Git. The framework resolves them at runtime and masks them in every log file. It's enterprise-grade security for Markdown. ### The "Invisible" Assistant: Background Daemons & Snippets I realized that even with all this automation, writers still had to remember variable names or script signatures. To fix this, I created the **WritingDoc Assistant**. It's a Python **daemon** that runs in the background of the container. It watches my `_variables.yml` and my `features/` directory. The second I save a change, the daemon wakes up and generates **VS Code IntelliSense Snippets** on the fly. * I type `v-` and I get a list of every variable in the project with its current value. * I type `feature-` and I get a list of every automation script with its full documentation. * I type `r-` and it has already scanned all my files to offer me cross-references to every heading and figure in the project. It's like having a dedicated librarian living inside my editor, watching my every move to make my life easier. The daemon is started in my Devcontainer using the `nohup` command, ensuring it runs in the background without blocking the main process. It continuously monitors for changes and updates the snippets in real-time, providing an always up-to-date coding experience. It's really a game-changer for productivity. ### Centralization at Scale Scaling to 50 projects meant I couldn't have 50 different configurations. I centralized everything: * **`_variables.yml`**: The "Brain" of the project. Every name, every link, and every setting is here. It's the single source of truth. * **`update_chapters.py`**: A script I wrote that maintains the Quarto sidebar. It respects folder numbering and ignores "draft" folders, ensuring a perfect table of contents every time. * **The Glossary**: A single YAML file that generates an alphabetical, anchor-linked glossary across the entire site. * **Global References**: A central file for all external URLs, so I never have to hunt for a link twice. ### The Result Today, when I start a new project, I run a single command: `scaffold`. In seconds, I have a complete structure tailored for my stack. The documentation isn't a static monument to the past anymore. It's a living, breathing reflection of the code. It's fast, it's secure, and it's beautiful. This is the power of **engineered documentation**. We've moved beyond writing; we're now compiling our knowledge. ## 🎖️ A Final Word: Why Quarto Changes Everything None of this would be possible without the visionary work of the developers behind Quarto. The Quarto team had the intelligence and the foresight to build something different: a bridge between narrative and execution. By allowing us to execute real code (Python, R, Julia) directly within the document lifecycle, they didn't just give us a better version of [Pandoc](https://pandoc.org/). They gave us an extensible documentation engine. It is this specific capability—the ability to run a script, analyze an API, or scan a filesystem during the build—that allowed me to build the "WritingDoc" ecosystem. To the Quarto developers: **Thank you for your extraordinary work** and for providing the world with an open-source tool that finally empowers technical writers to behave like software engineers. You've turned the "chore" of documentation into a high-fidelity engineering discipline. ---