Turn a set of .qmd files into a navigable, branded website. Then use freeze to reuse computed results and render the site to a publishable folder. Publishing online is optional. Everything uses base-R datasets::penguins (R ≥ 4.5).
Before you start: work through the Setup page if you haven’t. Its 00-check-setup.R confirms R, Quarto, and the packages this lab uses.
Starting point. Open the day2-projects/ folder from the exercises download in your editor, or make it your working directory in R (see the Setup page). It holds four content pages that already render on their own but are not yet a project:
index.qmd — a home page
analysis.qmd — a penguins analysis with a figure and a table
reports/methods.qmd and reports/data-notes.qmd — two short supporting pages, in their own folder because you will treat that folder differently later
Render one page to confirm it works: quarto render analysis.qmd creates analysis.html next to its source. A finished reference is in solutions/day2/. You can also open the finished website, which shows the site after Part 2.
1 Website Challenge
Goal: turn the day2-projects/ pages into a navigable, branded website that renders locally to a _site/ folder.
Complete the four core steps below. The listing is optional. Each step includes a checkpoint, a hint, and a solution.
Use solutions/day2/ to compare your work or complete missed steps.
1.1 Make it a project
Goal. Make Quarto treat day2-projects/ as one project, so that a single command builds every page in it into an output folder.
Create a _quarto.yml in day2-projects/, alongside reports/. Declare the folder as a website project, and send the built pages to a _site folder. Then build the whole project from inside day2-projects/, without naming a file.
(_site is already the default output folder for a website. Set it explicitly so the publishable folder is visible in the configuration.)
Note You should see
One command, run with no file argument, builds all four pages at once, and the HTML is written to a new day2-projects/_site/ folder instead of next to each .qmd. Before this step, rendering analysis.qmd put analysis.html right beside it.
TipHint
Quarto walks up from the file being rendered and stops at the nearest_quarto.yml. That file marks the project root. Putting _quarto.yml in day2-projects/ makes that folder the project root.
quarto render index.qmd still builds just the one page.
NoteSolution
Create day2-projects/_quarto.yml:
day2-projects/_quarto.yml
project:type: websiteoutput-dir: _site
Then, from inside day2-projects/:
quarto render
1.2 Build the navigation
Goal. Give the site a title and a top navigation bar, so a reader can move between the home and analysis pages without editing the URL.
Add index.qmd (Home) and analysis.qmd (Analysis) to the navigation. Give the site the title Penguin Lab (the home page already carries that title, but the site does not), and apply the ready-made cosmo theme.
A second top-level key, website:, sits next to project: and configures the site itself. Complete this block:
From this step on, preview the site with quarto preview from inside day2-projects/. On Day 1, quarto preview served one document. From the project root, it serves the whole site and re-renders a page when you save it.
Note You should see
The preview opens the home page in your browser. A bar across the top carries the site title, two entries, Home and Analysis, and a magnifier (the built-in site search, on by default). Clicking Analysis moves you to the other page, with its body-mass figure and table of species means.
TipHint
The theme has nothing to do with navigation. Where do format options live in a .qmd header, and what would the same key look like one level up, in _quarto.yml?
_quarto.yml takes a format: key exactly as a document header does, and what you put there applies to every page in the project. theme: is an HTML format option, so it sits under format: html:.
navbar: also takes a right: list if you want entries on the other side.
Leave it running: it re-renders and refreshes as you save. quarto render still works when you only want the files.
TipTry the other menu: a sidebar built for you
Swap the navbar: block for this, re-render, and compare:
sidebar:contents: auto
contents: auto builds the menu from the files it finds, so you do not need to list every page. It is the right choice for a site that grows. A navbar is the right choice when you want to control exactly what appears and in what order. Put the navbar back before you carry on, or keep both.
NoteLinking pages and what does not carry across
The Day-1 rules now meet a multi-page site.
index.qmd already links to the analysis with a plain Markdown link, [analysis](analysis.qmd). That is deliberate. Across pages there is no auto-numbering (that is a book feature, not a website one), so a plain link is how you connect website pages.
Withinanalysis.qmd, yesterday’s rule still holds: @tbl-means resolves to Table 1 because the reference and table share one page.
1.3 Brand it
Goal. Give the site one shared palette and typeface from a _brand.yml. Yesterday it sat next to your report. Here it sits at the project root, where it covers every page.
Start by creating this file and rendering the site:
day2-projects/_brand.yml
color:palette:teal:"#4C979F"primary: tealtypography:fonts:-family: Albert Sanssource: googlebase:family: Albert Sans
Then look at theme:. It takes a list of layers. When two layers set the same option, the later layer wins. The position of brand determines whether the navbar uses the brand teal or cosmo blue.
Note You should see
After adding _brand.yml, the font changes but the navbar and links remain cosmo blue. This is expected. After correcting the theme order, the navbar and links become teal.
The incorrect order renders without an error, but the navbar remains blue. Check the navbar color, not the log.
TipHint
Compare three parts of the rendered site: the body font, the navbar, and the links. The font changes, but the navbar and links remain blue.
A bare theme: cosmo expands to [brand, cosmo]. Quarto applies cosmo’s palette after the brand palette, so the site uses cosmo blue.
The fonts apply in either order, which can make the incorrect order look successful.
NoteSolution
Put brandlast in the list:
day2-projects/_quarto.yml
format:html:theme:[cosmo, brand]
Dropping theme: altogether works too: with no named theme, the brand layer supplies the theme.
1.4 Scope options to one folder
Goal. Fold the code on the two pages in reports/, and only there, behind a toggle labeled Show the code. The home and analysis pages keep showing their code as before.
You have not looked at day2-projects/reports/ yet. It holds two short pages, methods.qmd and data-notes.qmd, each with a small code cell.
A _metadata.yml file applies options to every document in its folder. Put one in reports/ to configure both pages without editing either file. It takes the same document options you would write in _quarto.yml. The difference is where they apply.
You need two options. code-fold gives you the toggle, and code-summary sets its label. The default label is Code, and the checkpoint asks for Show the code.
Neither page is in the navbar. Use the site search (the magnifier) to open them.
Note You should see
On the two reports/ pages, each code cell is collapsed behind a Show the code toggle that expands when clicked. On index.qmd and analysis.qmd, nothing changes. You did not edit any of the four .qmd files to get this.
TipHint
Quarto merges reports/_metadata.yml on top of the project defaults, so you write only what differs. Its options apply to reports/ and its subfolders.
NoteSolution
Create day2-projects/reports/_metadata.yml:
day2-projects/reports/_metadata.yml
code-fold:truecode-summary:"Show the code"
Re-render the project. Nothing else changes.
1.5 Index the reports (optional)
Only start this after the four core steps work.
Goal. Put a table of the reports/ pages on the home page, built by Quarto rather than typed by you, so that adding a page there shows up without editing index.qmd.
A listing: key in the YAML header of index.qmd builds the table, and contents: names the folder it indexes. Where the table appears on the page is a separate choice.
If quarto preview is still running, stop it (Ctrl + C in the terminal) before you re-render. Otherwise, the running preview may rebuild the page and leave you checking a different result from the one produced by quarto render.
Note You should see
A table on the home page, under a Reports heading you add, with one row per page in reports/, showing each title and its description (both pages already carry a description: in their YAML header). The table sits directly under that heading, not at the foot of the page. Add a third page to reports/, re-render, and it appears in the table on its own.
TipHint
Give the listing an id:, then place the result where you want it on the page with an empty div carrying that id:
::: {#reports}:::
Without the id: and the div, the listing renders at the bottom of the page.
Goal: make the project’s builds reproducible, bring your own work onto the site, and render it all to a publishable _site/ folder.
NoteStarting point
Continue in the day2-projects/ folder from the Website Challenge. If you didn’t finish it, open the ready-made solutions/day2/ project. It already contains the completed Day-2 configuration, including freeze: auto and the my-report.qmd navbar entry. Inspect those settings and run the checkpoints instead of adding them again. In the steps below, replace day2-projects/ with solutions/day2/.
If quarto preview is still running, stop it (Ctrl + C in the terminal). Run each render below yourself. To check a page after a render, open the built file (for example _site/analysis.html) directly in your browser and reload it.
Core tasks: prove that freeze: auto works, add your Day-1 report to the site, then complete the guided freeze: true experiment. We will compare cache and freeze together. Publishing and the dashboard are optional.
Goal. Right now every quarto render re-runs all the R on every page, even the pages you have not touched. Make Quarto reuse the previous results for a page whose source has not changed. Then prove that it worked.
Execution options for the whole project sit under a top-level execute: key, alongside project: and website:. Complete it:
day2-projects/_quarto.yml
execute:freeze: ...
Then add a value that changes on every execution, so the page shows whether the code ran again.
Note You should see
Render twice with no edits in between. On the second render, the R on your pages does not run again, and you can show it: something on the rendered page that would certainly be different if the code had run, and is not. Then edit the code on one page only and render again. Only that page runs.
TipHint
What could a cell print that is guaranteed to differ between two runs a minute apart?
Set freeze: auto to re-execute a document only when its source changes.
For the proof: print the current time. If the value on the page is identical across two renders, the code that produced it did not run.
NoteSolution
In day2-projects/_quarto.yml:
day2-projects/_quarto.yml
execute:freeze: auto
For the visible check, add a cell to analysis.qmd:
cat(format(Sys.time()))
Then, from inside day2-projects/:
quarto render # runs the code, writes _freeze/ and _site/quarto render # no edits: cells are skipped, and the printed time is unchanged
Now change the plot code in analysis.qmd and render again. Only that page runs, and its timestamp changes.
NoteWhy keep _freeze/?
The results now live in _freeze/, inside your project. Keep that folder. For documents with current frozen results, the next project render reads those results instead of re-running the analysis. This also lets another machine build those pages without running their R code. _freeze/ is part of the project, not a temporary folder to delete.
2.2 Discuss together: cache versus freeze
Goal. You have now met two things that both avoid re-running code. Explain how they differ and when to use each one.
The slides introduced cache earlier today. It is a cell option (#| cache: true) that stores the results from that cell in a knitr cache folder beside the document. You have just used freeze: auto. Both skip work. They are not the same mechanism, and only one of them solves the problem you just solved.
Note You should be able to explain
cache
freeze
Scope
A cell within a document
A document within a project
Results
Local cache directory
_freeze/
Commit the results?
Normally no
Yes
TipHint
Where does each one write its results, and would that location normally be committed? Those answers explain their different purposes.
cache is knitr’s chunk cache. It works inside one document, keyed on the chunk, and avoids re-running an expensive chunk while you iterate on that document. Its cache directory is local and is not normally committed or published.
freeze is a project policy. It stores each document’s rendered results under _freeze/, which you do commit, so a later build (including one on a machine with no R) reuses them.
Use cache for a slow chunk you are iterating on. Use freeze to make the project buildable without re-running the analysis. freeze: true goes further than auto: a project build reuses a page’s stored results even after you edit it, so you decide when to refresh by rendering that page on its own.
2.3 Put your Day-1 report on the site
This is a core task.
Goal. Take the report you wrote yesterday and make it a page of this website.
On Day 1 you created my-report.qmd in day1-intro/. Copy it into day2-projects/, together with references.bib and apa.csl (the report cites through them), then add it to the project. If you do not have that file, take solutions/day1/penguins-report.qmd from your download instead.
Note You should see
Your Day-1 report opens as a page of the site, reachable from the navigation, styled like the rest of it. Its prose and code are untouched: the YAML header is the only part of the file you edited.
TipHint
Your Day-1 document may still declare its own format:. Inside a project, where does format: come from now, and what happens if a page insists on its own?
Copy the .qmd into day2-projects/, together with references.bib and apa.csl from day1-intro/ (the YAML header points at them by relative path), add an entry for it in the navbar: list, and re-render.
A page in a project inherits format: from _quarto.yml. If your file still carries the format: typst from the Day-1 exercise, it keeps building a PDF instead of a page. Drop that key and it builds as HTML like its neighbors. (The included penguins-report.qmd declares format: html, which is the same key to drop.)
NoteSolution
Copy your my-report.qmd into day2-projects/, together with references.bib and apa.csl from day1-intro/ (the report cites through them). Drop any format: block from its YAML header, and add it to the navbar:
day2-projects/_quarto.yml
website:navbar:left:-href: index.qmdtext: Home-href: analysis.qmdtext: Analysis-href: my-report.qmdtext: My report
solutions/day2/my-report.qmd shows the finished page.
2.4 Guided experiment: freeze: true
This step completes the core tasks. It is guided: run the commands as written.
Goal. See what freeze: true changes compared to auto: the build reuses a page’s stored results even after you edit that page.
This step needs a stored result to reuse. With freeze: auto still set, run quarto render once from inside day2-projects/ before you continue.
Note You should see
Check the label and timestamp after each render:
Project render: both remain unchanged because the build reuses _freeze/.
Single-file render: both update because the page executes.
Final project render: the updated values remain because the build reuses the refreshed result.
First, change freeze from auto to true in _quarto.yml:
day2-projects/_quarto.yml
execute:freeze:true
Then, in analysis.qmd, make a visible edit to the plot code: change the y-axis label to y = "Body mass in grams".
Stop quarto preview first if it is running (Ctrl + C). A watching preview re-runs the page when you save and refreshes _freeze/, so the first render below would show your edit.
Now run the three renders from inside day2-projects/, and reload _site/analysis.html in your browser after each one:
quarto render # whole project: your edit does not appearquarto render analysis.qmd # one file: the page refreshesquarto render # whole project: the refreshed page stays
With freeze: true, the workflow is: edit a page, render that file, then build the project without re-running it. Switch back to freeze: auto before you continue and keep that setting for the rest of the lab. Use true when project builds must reuse stored results until you deliberately refresh a page.
2.5 Put it online (optional)
Goal. Take the _site/ folder you have built and put it on a URL you can open on your phone.
_site/is the core deliverable. Any static host will serve it: an internal server, your department’s pages, or a hosting service. Publishing here is optional. If you made a free Posit Connect Cloud account before the workshop, one command puts the site online.
NoteThe command
From inside your project folder:
quarto publish posit-connect-cloud
No repository is needed. Connect Cloud takes the locally rendered files as they are.
If you skip this step, the complete site remains in _site/. Nothing downstream depends on publishing.
Note You should see
A browser opens once to sign you in. Then the contents of _site/ upload, and Connect Cloud returns a URL for the site.
2.6 Optional: inspect a dashboard
This worked example shows how the same analysis becomes a dashboard. Open the finished dashboard, then compare it with solutions/day2/dashboard.qmd. You do not need to build it during the workshop.
Look for four ideas:
format: dashboard changes the output.
A ## heading starts a row and a ### heading starts a column.
#| content: valuebox creates a single-number panel from a cell whose result is one number (for example nrow(penguins)).
{.tabset} turns the cells in a column into tabs.
The overall shape is:
---title: "Penguins dashboard"format: dashboard---## Row {height="20%"}(two cells, each with `#| content: valuebox`)## Row {height="80%"}### Column(the boxplot cell, with a `#| title:`)### Column {.tabset}(the scatter cell and the table cell: each becomes a tab)
After looking at it, you can optionally copy analysis.qmd to dashboard.qmd and reproduce one part of the layout.
NoteFor later: package versions
renv::snapshot() writes an renv.lock that pins your package versions. That is the second half of reproducibility: freeze preserves the results, renv.lock preserves the environment that produced them.
3 Troubleshooting
Tip If rendering fails or the output looks wrong
Rendered, but can’t find the HTML? Before you add _quarto.yml, a single-page render writes the HTML next to its source in day2-projects/. Once _quarto.yml is present, a project render collects all pages under day2-projects/_site/. Look in those two places. The earlier analysis.html and analysis_files/ are removed from beside the sources by the project render, so finding them gone is expected.
YAML is indentation-sensitive. If rendering fails before any code runs, check _quarto.yml for stray spaces, tabs, or unmatched quotes.
“No project” / pages don’t link? The _quarto.yml must be inside day2-projects/, and href: paths are relative to the source folder, not_site/.
Brand not applied? Two causes. The file must be in day2-projects/ (the project root), and brand must come last in the theme: list: theme: [cosmo, brand] gives you the palette, while theme: cosmo on its own keeps cosmo’s colors and only the fonts change. Re-render after either fix. For plots, the palette needs library(brand.yml) + theme_brand_ggplot2(); _brand.yml alone does not apply the palette to ggplot figures.
Render fails with “references.bib not found”? Your Day-1 report points at references.bib and apa.csl by relative path. Copy both files from day1-intro/ into day2-projects/, next to the report, and render again.
A cross-reference shows ?@…? The cell label must start fig-/tbl-/eq- and be unique. Website cross-references resolve only within one page.
Freeze didn’t skip the cell? Under freeze: auto it skips only when the source is unchanged since the last build: any edit (prose or code) re-runs that page. Delete _freeze/ to force a clean rebuild.
A page won’t update? This is expected with freeze: true: a project render reuses the stored results even after an edit. Render that file directly, or switch back to freeze: auto.
A page still updates under freeze: true? The project has no stored result for it yet. A page with no _freeze/ entry executes on the next render. Render once with freeze: auto, then retry.
Missing package?install.packages("<name>") (or renv::restore()) and render again.
Session
Session info
sessionInfo()
R version 4.6.1 (2026-06-24 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 26200)
Matrix products: default
LAPACK version 3.12.1
locale:
[1] C
system code page: 65001
time zone: Europe/Paris
tzcode source: internal
attached base packages:
[1] stats graphics grDevices datasets utils methods base
other attached packages:
[1] ggokabeito_0.1.0 ggplot2_4.0.3 dplyr_1.2.1
loaded via a namespace (and not attached):
[1] vctrs_0.7.3 cli_3.6.6 knitr_1.51 rlang_1.3.0
[5] xfun_0.59 renv_1.2.3 generics_0.1.4 S7_0.2.2
[9] jsonlite_2.0.0 glue_1.8.1 htmltools_0.5.9 scales_1.4.0
[13] rmarkdown_2.31 grid_4.6.1 evaluate_1.0.5 tibble_3.3.1
[17] fastmap_1.2.0 yaml_2.3.12 lifecycle_1.0.5 compiler_4.6.1
[21] RColorBrewer_1.1-3 htmlwidgets_1.6.4 pkgconfig_2.0.3 farver_2.1.2
[25] digest_0.6.39 R6_2.6.1 tidyselect_1.2.1 pillar_1.11.1
[29] magrittr_2.0.5 withr_3.0.3 tools_4.6.1 gtable_0.3.6