New-project menu: split into Quarto and LaTeX blank/example options
Build and Deploy Verso / deploy (push) Successful in 7m44s

Replace the generic "Blank project" / "Example project" entries with four
flavour-specific ones in both the New-project dropdown and the welcome-screen
dropdown:

- Blank Quarto project   -> empty main.qmd (format: typst)
- Blank LaTeX project    -> empty main.tex
- Example Quarto project -> a Reveal.js presentation showcasing images, math,
  a table, code and incremental lists (new template
  project_files/example-project-quarto/)
- Example LaTeX project  -> the existing LaTeX example

Backend: ProjectController.newProject now dispatches the `template` value
(blank_quarto/blank_latex/example_quarto/example_latex, plus the legacy
'example'/'none') to createBasicProject(flavour) / createExampleProject(flavour).
_createRootDoc takes a root-doc name so each flavour gets the right extension —
this also fixes the LaTeX example, whose root doc was wrongly created as
main.qmd, back to main.tex (matching the acceptance test). Signatures stay
backward compatible (flavour defaults: blank=quarto, example=latex).

Also refresh the README: Verso now runs Quarto and LaTeX side by side
(engine chosen by root-file extension), not Quarto instead of LaTeX.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-01 12:30:07 +00:00
co-authored by Claude Opus 4.8
parent 3e10d1c4ee
commit 2a9c4cfe81
11 changed files with 340 additions and 48 deletions
+19 -7
View File
@@ -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_`).
@@ -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,
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

@@ -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%"}
![A friendly frog](frog.jpg)
:::
::::
## 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.
@@ -192,24 +192,48 @@ function NewProjectButton({
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'blank_project',
dropdownMenuEvent: 'blank-project',
modalVariant: 'blank_quarto',
dropdownMenuEvent: 'blank-project-quarto',
})
}
>
{t('blank_project')}
{t('blank_quarto_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'example_project',
dropdownMenuEvent: 'example-project',
modalVariant: 'blank_latex',
dropdownMenuEvent: 'blank-project-latex',
})
}
>
{t('example_project')}
{t('blank_latex_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'example_quarto',
dropdownMenuEvent: 'example-project-quarto',
})
}
>
{t('example_quarto_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
onClick={e =>
handleModalMenuClick(e, {
modalVariant: 'example_latex',
dropdownMenuEvent: 'example-project-latex',
})
}
>
{t('example_latex_project')}
</DropdownItem>
</li>
<li role="none">
@@ -5,9 +5,14 @@ import { Tag } from '../../../../../../app/src/Features/Tags/types'
type BlankProjectModalProps = {
onHide: () => void
initialTags?: Tag[]
template?: string
}
function BlankProjectModal({ onHide, initialTags }: BlankProjectModalProps) {
function BlankProjectModal({
onHide,
initialTags,
template = 'blank_quarto',
}: BlankProjectModalProps) {
return (
<OLModal
show
@@ -16,7 +21,11 @@ function BlankProjectModal({ onHide, initialTags }: BlankProjectModalProps) {
id="blank-project-modal"
backdrop="static"
>
<ModalContentNewProjectForm onCancel={onHide} initialTags={initialTags} />
<ModalContentNewProjectForm
onCancel={onHide}
template={template}
initialTags={initialTags}
/>
</OLModal>
)
}
@@ -5,11 +5,13 @@ import { Tag } from '../../../../../../app/src/Features/Tags/types'
type ExampleProjectModalProps = {
onHide: () => void
initialTags?: Tag[]
template?: string
}
function ExampleProjectModal({
onHide,
initialTags,
template = 'example_latex',
}: ExampleProjectModalProps) {
return (
<OLModal
@@ -21,7 +23,7 @@ function ExampleProjectModal({
>
<ModalContentNewProjectForm
onCancel={onHide}
template="example"
template={template}
initialTags={initialTags}
/>
</OLModal>
@@ -11,8 +11,10 @@ const UploadProjectModal = lazy(() => import('./upload-project-modal'))
const ImportDocumentModal = lazy(() => import('./import-document-modal'))
export type NewProjectButtonModalVariant =
| 'blank_project'
| 'example_project'
| 'blank_quarto'
| 'blank_latex'
| 'example_quarto'
| 'example_latex'
| 'upload_project'
| 'import_from_github'
| 'import_docx'
@@ -50,10 +52,38 @@ function NewProjectButtonModal({
)
switch (modal) {
case 'blank_project':
return <BlankProjectModal onHide={onHide} initialTags={initialTags} />
case 'example_project':
return <ExampleProjectModal onHide={onHide} initialTags={initialTags} />
case 'blank_quarto':
return (
<BlankProjectModal
onHide={onHide}
initialTags={initialTags}
template="blank_quarto"
/>
)
case 'blank_latex':
return (
<BlankProjectModal
onHide={onHide}
initialTags={initialTags}
template="blank_latex"
/>
)
case 'example_quarto':
return (
<ExampleProjectModal
onHide={onHide}
initialTags={initialTags}
template="example_quarto"
/>
)
case 'example_latex':
return (
<ExampleProjectModal
onHide={onHide}
initialTags={initialTags}
template="example_latex"
/>
)
case 'upload_project':
return (
<Suspense fallback={<FullSizeLoadingSpinner delay={500} />}>
@@ -113,22 +113,56 @@ function WelcomeMessageCreateNewProjectDropdown({
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(e, 'blank_project', 'blank-project')
handleDropdownItemClick(
e,
'blank_quarto',
'blank-project-quarto'
)
}
tabIndex={-1}
>
{t('blank_project')}
{t('blank_quarto_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(e, 'example_project', 'example-project')
handleDropdownItemClick(e, 'blank_latex', 'blank-project-latex')
}
tabIndex={-1}
>
{t('example_project')}
{t('blank_latex_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(
e,
'example_quarto',
'example-project-quarto'
)
}
tabIndex={-1}
>
{t('example_quarto_project')}
</DropdownItem>
</li>
<li role="none">
<DropdownItem
as="button"
onClick={e =>
handleDropdownItemClick(
e,
'example_latex',
'example-project-latex'
)
}
tabIndex={-1}
>
{t('example_latex_project')}
</DropdownItem>
</li>
<li role="none">
+4
View File
@@ -292,7 +292,9 @@
"billing": "Billing",
"billing_period_sentence_case": "Billing period",
"binary_history_error": "Preview not available for this file type",
"blank_latex_project": "Blank LaTeX project",
"blank_project": "Blank project",
"blank_quarto_project": "Blank Quarto project",
"blocked_filename": "This file name is blocked.",
"blog": "Blog",
"bold": "Bold",
@@ -837,7 +839,9 @@
"everything_in_group_standard_plus": "Everything in Standard group, plus…",
"everything_in_standard_plus": "Everything in Standard, plus…",
"example": "Example",
"example_latex_project": "Example LaTeX project",
"example_project": "Example project",
"example_quarto_project": "Example Quarto project",
"examples": "Examples",
"examples_lowercase": "examples",
"examples_to_help_you_learn": "Examples to help you learn how to use powerful LaTeX packages and techniques.",