diff --git a/README.md b/README.md
index 94d4ffc182..6b965926af 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,15 @@
# Verso
-**A collaborative real-time editor for Quarto presentations and documents.**
+**A collaborative real-time editor for Quarto and LaTeX presentations and documents.**
-Verso is a fork of [Overleaf](https://github.com/overleaf/overleaf) adapted to compile [Quarto](https://quarto.org) documents instead of LaTeX. It keeps Overleaf's real-time collaboration infrastructure while replacing the LaTeX/CLSI compiler with Quarto, enabling collaborative editing of `.qmd` files with instant PDF (via Typst) or HTML (RevealJS) preview.
+Verso is a fork of [Overleaf](https://github.com/overleaf/overleaf) that adds first-class [Quarto](https://quarto.org) support alongside Overleaf's existing LaTeX toolchain. It keeps Overleaf's real-time collaboration infrastructure and runs **two compilers side by side**: `.qmd` files are built with Quarto (PDF via Typst, or HTML via RevealJS), while `.tex` files still compile with `latexmk`/TeX Live. The engine is selected automatically from the root file's extension, so Quarto and LaTeX projects coexist.
---
## Features
- **Real-time collaboration** — multiple users editing the same `.qmd` file simultaneously, powered by Overleaf's operational-transformation engine
-- **Quarto compilation** — documents are compiled with Quarto on every save
+- **Dual compiler** — `.qmd` files build with Quarto; `.tex` files build with `latexmk`/TeX Live. The runner is chosen by the root file's extension, so LaTeX and Quarto projects live side by side
- **Two output formats** — set `format: typst` in the YAML frontmatter for a PDF preview, or `format: revealjs` for an interactive HTML presentation rendered in an iframe
- **Document outline** — headings (`#`, `##`, `###`) are extracted and shown in the sidebar outline panel
- **Math, code, tables** — standard Quarto/Pandoc Markdown features all work
@@ -44,7 +44,7 @@ Then open `http://localhost` in your browser. On first run, visit `/launchpad` t
### Build from source
```bash
-# Build the base image (installs Quarto)
+# Build the base image (installs Quarto + TeX Live)
cd server-ce
make build-base
@@ -56,7 +56,7 @@ The two Dockerfiles are:
| File | Purpose |
|------|---------|
-| `server-ce/Dockerfile-base` | Base OS image — installs system deps and Quarto |
+| `server-ce/Dockerfile-base` | Base OS image — installs system deps, Quarto, and a TeX Live (`latexmk`) toolchain |
| `server-ce/Dockerfile` | Application image — installs Node services, compiles frontend |
## Architecture
@@ -70,7 +70,7 @@ browser ──→ nginx:80
└── /project/*/output/* → clsi-nginx:8080 (compiled output files)
web → document-updater → Redis pub/sub → real-time → browser
-web → CLSI (quarto render) → output files → nginx → browser
+web → CLSI (quarto render / latexmk) → output files → nginx → browser
```
Key services:
@@ -80,7 +80,7 @@ Key services:
| `web` | HTTP API, React frontend, auth, project management |
| `real-time` | WebSocket layer, live cursor and edit sync |
| `document-updater` | Operational transformation, Redis pub/sub |
-| `clsi` | Quarto compiler — runs `quarto render` and serves output |
+| `clsi` | Compiler — runs `quarto render` (`.qmd`) or `latexmk` (`.tex`) and serves output |
| `docstore` | Document text storage (MongoDB) |
| `filestore` | Binary file storage (S3 or local) |
| `project-history` | Change history and version tracking |
@@ -111,6 +111,18 @@ For PDF output, change `format: revealjs` to `format: typst`.
> **Note on display math**: put `$$...$$` on a single line.
> Multi-line display math blocks can cause YAML parse errors in some Quarto versions.
+## Writing a LaTeX document
+
+LaTeX still works exactly as in Overleaf. A project whose root file is a `.tex`
+file (e.g. `main.tex`) compiles with `latexmk`/TeX Live instead of Quarto — the
+engine is chosen from the root file's extension, no setting required. The
+**Example LaTeX project** in the *New project* menu is a ready-made starting
+point; **Blank LaTeX project** gives you an empty `main.tex`.
+
+> The bundled TeX Live is a minimal `scheme-basic` install. Documents needing
+> extra packages may not build out of the box yet — see `server-ce/Dockerfile-base`
+> for how to switch to a fuller TeX Live scheme.
+
## Environment variables
Verso inherits all of Overleaf's environment variables (prefixed `OVERLEAF_`).
diff --git a/services/web/app/src/Features/Project/ProjectController.mjs b/services/web/app/src/Features/Project/ProjectController.mjs
index 955c2aea23..36e9bdbb57 100644
--- a/services/web/app/src/Features/Project/ProjectController.mjs
+++ b/services/web/app/src/Features/Project/ProjectController.mjs
@@ -323,12 +323,44 @@ const _ProjectController = {
req.body.projectName != null ? req.body.projectName.trim() : undefined
const { template } = req.body
- const project = await (template === 'example'
- ? ProjectCreationHandler.promises.createExampleProject(
+ // The "template" selects flavour (Quarto vs LaTeX) and kind (blank vs
+ // example). 'example'/'none' are kept for backwards compatibility.
+ let project
+ switch (template) {
+ case 'example':
+ case 'example_latex':
+ project = await ProjectCreationHandler.promises.createExampleProject(
userId,
- projectName
+ projectName,
+ {},
+ 'latex'
)
- : ProjectCreationHandler.promises.createBasicProject(userId, projectName))
+ break
+ case 'example_quarto':
+ project = await ProjectCreationHandler.promises.createExampleProject(
+ userId,
+ projectName,
+ {},
+ 'quarto'
+ )
+ break
+ case 'blank_latex':
+ project = await ProjectCreationHandler.promises.createBasicProject(
+ userId,
+ projectName,
+ 'latex'
+ )
+ break
+ case 'blank_quarto':
+ case 'none':
+ default:
+ project = await ProjectCreationHandler.promises.createBasicProject(
+ userId,
+ projectName,
+ 'quarto'
+ )
+ break
+ }
ProjectAuditLogHandler.addEntryIfManagedInBackground(
project._id,
diff --git a/services/web/app/src/Features/Project/ProjectCreationHandler.mjs b/services/web/app/src/Features/Project/ProjectCreationHandler.mjs
index 94b9f68381..e11d433434 100644
--- a/services/web/app/src/Features/Project/ProjectCreationHandler.mjs
+++ b/services/web/app/src/Features/Project/ProjectCreationHandler.mjs
@@ -85,11 +85,19 @@ async function createProjectFromSnippet(ownerId, projectName, docLines) {
return project
}
-async function createBasicProject(ownerId, projectName) {
+async function createBasicProject(ownerId, projectName, flavour = 'quarto') {
const project = await _createBlankProject(ownerId, projectName)
- const docLines = await _buildTemplate('mainbasic.qmd', ownerId, projectName)
- await _createRootDoc(project, ownerId, docLines)
+ // Verso compiles .qmd with Quarto and .tex with latexmk; the root file's
+ // extension selects the runner (see CompileManager._isQuartoFile in CLSI),
+ // so a blank project's flavour is just a choice of template + root name.
+ const { templateName, rootDocName } =
+ flavour === 'latex'
+ ? { templateName: 'mainbasic.tex', rootDocName: 'main.tex' }
+ : { templateName: 'mainbasic.qmd', rootDocName: 'main.qmd' }
+
+ const docLines = await _buildTemplate(templateName, ownerId, projectName)
+ await _createRootDoc(project, ownerId, docLines, rootDocName)
AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
projectId: project._id,
@@ -163,20 +171,29 @@ async function populateClsiCacheForExampleProject(
return projectId
}
-async function createExampleProject(ownerId, projectName, attributes = {}) {
+async function createExampleProject(
+ ownerId,
+ projectName,
+ attributes = {},
+ flavour = 'latex'
+) {
const project = await _createBlankProject(ownerId, projectName, attributes)
- const { fileEntries, docEntries } = await _addExampleProjectFiles(
- ownerId,
- projectName,
- project
- )
- await populateClsiCacheForExampleProject(
- ownerId,
- project,
- fileEntries,
- docEntries
- )
+ const { fileEntries, docEntries } =
+ flavour === 'quarto'
+ ? await _addQuartoExampleProjectFiles(ownerId, projectName, project)
+ : await _addExampleProjectFiles(ownerId, projectName, project)
+
+ if (flavour !== 'quarto') {
+ // clsi-cache warming keys on a single static example; only do it for the
+ // long-standing LaTeX example to avoid the "content is not static" guard.
+ await populateClsiCacheForExampleProject(
+ ownerId,
+ project,
+ fileEntries,
+ docEntries
+ )
+ }
AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', {
projectId: project._id,
@@ -191,7 +208,12 @@ async function _addExampleProjectFiles(ownerId, projectName, project) {
ownerId,
projectName
)
- const rootDoc = await _createRootDoc(project, ownerId, mainDocLines)
+ const rootDoc = await _createRootDoc(
+ project,
+ ownerId,
+ mainDocLines,
+ 'main.tex'
+ )
const bibDocLines = await _buildTemplate(
`${templateProjectDir}/sample.bib`,
@@ -229,6 +251,40 @@ async function _addExampleProjectFiles(ownerId, projectName, project) {
}
}
+async function _addQuartoExampleProjectFiles(ownerId, projectName, project) {
+ const mainDocLines = await _buildTemplate(
+ 'example-project-quarto/main.qmd',
+ ownerId,
+ projectName
+ )
+ const rootDoc = await _createRootDoc(
+ project,
+ ownerId,
+ mainDocLines,
+ 'main.qmd'
+ )
+
+ const imagePath = path.join(
+ import.meta.dirname,
+ '/../../../templates/project_files/example-project-quarto/frog.jpg'
+ )
+ const { fileRef } = await ProjectEntityUpdateHandler.promises.addFile(
+ project._id,
+ project.rootFolder[0]._id,
+ 'frog.jpg',
+ imagePath,
+ null,
+ ownerId,
+ null
+ )
+ return {
+ fileEntries: [{ path: fileRef.name, file: fileRef }],
+ docEntries: [
+ { path: 'main.qmd', doc: rootDoc, docLines: mainDocLines.join('\n') },
+ ],
+ }
+}
+
async function _createBlankProject(
ownerId,
projectName,
@@ -299,12 +355,17 @@ async function _createBlankProject(
return project
}
-async function _createRootDoc(project, ownerId, docLines) {
+async function _createRootDoc(
+ project,
+ ownerId,
+ docLines,
+ rootDocName = 'main.qmd'
+) {
try {
const { doc } = await ProjectEntityUpdateHandler.promises.addDoc(
project._id,
project.rootFolder[0]._id,
- 'main.qmd',
+ rootDocName,
docLines,
ownerId,
null
diff --git a/services/web/app/templates/project_files/example-project-quarto/frog.jpg b/services/web/app/templates/project_files/example-project-quarto/frog.jpg
new file mode 100644
index 0000000000..5b889ef3cf
Binary files /dev/null and b/services/web/app/templates/project_files/example-project-quarto/frog.jpg differ
diff --git a/services/web/app/templates/project_files/example-project-quarto/main.qmd b/services/web/app/templates/project_files/example-project-quarto/main.qmd
new file mode 100644
index 0000000000..e1c5a9621d
--- /dev/null
+++ b/services/web/app/templates/project_files/example-project-quarto/main.qmd
@@ -0,0 +1,84 @@
+---
+title: "<%= project_name %>"
+author: "<%= user.first_name %> <%= user.last_name %>"
+date: "<%= month %> <%= year %>"
+format:
+ revealjs:
+ theme: simple
+ slide-number: true
+ chalkboard: true
+---
+
+## Welcome to Verso
+
+This is an example **presentation** written in [Quarto](https://quarto.org)
+and rendered to interactive HTML slides with Reveal.js.
+
+- Write your slides in plain Markdown
+- Mix in math, code, images and tables
+- Hit **Recompile** to see your changes instantly
+
+::: aside
+Use the arrow keys to navigate, and press `f` for fullscreen.
+:::
+
+## Mathematics
+
+Inline math such as $E = mc^2$ renders inline, and display equations are
+centered automatically:
+
+$$
+\int_{-\infty}^{\infty} e^{-x^2}\,dx = \sqrt{\pi}
+$$
+
+. . .
+
+You can reveal content step by step by separating it with `. . .`.
+
+## Two columns
+
+:::: {.columns}
+
+::: {.column width="55%"}
+### Figures sit nicely beside text
+
+Fenced columns let you place an explanation next to a figure — perfect for
+walking an audience through a diagram.
+:::
+
+::: {.column width="45%"}
+
+:::
+
+::::
+
+## Tables
+
+| Engine | Output | Best for |
+|--------|:------:|-----------------------|
+| Typst | PDF | Fast, modern layout |
+| Reveal | HTML | Interactive slides |
+| LaTeX | PDF | Classic STEM articles |
+
+: Quarto can target several output formats from one source. {.striped .hover}
+
+## Code
+
+Code blocks are syntax-highlighted out of the box:
+
+```python
+def greet(name):
+ return f"Hello, {name}!"
+
+print(greet("Verso"))
+```
+
+## Incremental lists {.incremental}
+
+- Quarto turns Markdown into beautiful documents
+- The same source can become a PDF, a website or these slides
+- Now make this deck your own — edit `main.qmd`!
+
+## Thank you {.center}
+
+Questions? Start editing and explore what Quarto can do.
diff --git a/services/web/frontend/js/features/project-list/components/new-project-button.tsx b/services/web/frontend/js/features/project-list/components/new-project-button.tsx
index 6f34d3049e..46af4164de 100644
--- a/services/web/frontend/js/features/project-list/components/new-project-button.tsx
+++ b/services/web/frontend/js/features/project-list/components/new-project-button.tsx
@@ -192,24 +192,48 @@ function NewProjectButton({