Skip to content

Working with external files

The spreadsheet is your project's single source of truth for structured data: taxonomy, traits, bibliographic references, and configuration all live there. But a spreadsheet cannot practically hold everything. A high-resolution photo, a multi-page field guide text, a .bib file managed in a reference manager, or a custom SVG distribution map all belong somewhere else. That somewhere is the usercontent/ folder on your server, and your spreadsheet points to those files rather than containing them.

This page explains how that pointing works, which file types belong in usercontent/, and how to keep the two in sync through the compilation process.

The usercontent/ folder

usercontent/ is a directory at the root of your NaturaList deployment, on the same server as the app (see Installation and deployment). The compiler has access to it when you upload and process your spreadsheet, and the published app reads from it at runtime for media files.

Think of it as the asset store that accompanies your spreadsheet. The spreadsheet tells the app what exists and how to interpret it; usercontent/ holds the actual files content that cannot live in a spreadsheet cell.

A few subfolders have reserved roles and must be named exactly as shown if you use the corresponding features:

FolderPurpose
usercontent/data/Holds the compiled checklist.json; managed by the app or your manual updates
usercontent/identity/Holds manifest.json, favicon.png, and credentials.php - customise before first use
usercontent/online_search_icons/Icons for Search online links
usercontent/keys/Images used in Single-access keys

Beyond those, you are free to organise additional files however suits your project. A typical layout in a more complex project might look like:

usercontent/
├── data/                  ← [! reserved] managed by the app
├── identity/              ← [! reserved] app identity files
├── keys/                  ← [! reserved] identification key images
├── online_search_icons/   ← [! reserved] icons for Search Online links
├── audio/                 ← [user-added] sound recordings
├── docs/                  ← [user-added] longer per-taxon markdown files
├── maps/                  ← [user-added] SVG distribution maps (comes shipped-in with world.svg for convenience)
├── specimen-scans/        ← [user-added] scans of specimen sheets
├── taxa-field-photos/     ← [user-added] taxon photos
├── about.md               ← [user-added] About section text
├── eml.xml                ← [user-added] hand-crafted EML file for DwC-A export
└── references.bib         ← [user-added] BibTeX bibliography

How paths resolve

All relative paths you enter in the spreadsheet - image sources, map sources, sound sources, and F: directive paths - are resolved relative to usercontent/. A value of img/litoria.jpg in a template becomes usercontent/img/litoria.jpg on the server.

External URLs are the exception. If a path starts with https://, it is used as-is and no usercontent/ prefix is added. This lets you reference images or other assets hosted externally (e.g. on a CDN or your existing online database), provided those hosts have appropriate CORS settings.

Files in usercontent/ are connected to spreadsheet data through two distinct mechanisms, each suited to a different type of content.

Templates - for media and binary files

Media columns (image, sound, mapregions, and the map data types) never store the file itself in a cell. Instead, a cell holds an identifier - a short filename, a code, or any value that can be used to construct a path - and the Template column in Custom data definition assembles the full file path using Handlebars syntax.

For example, if your photo column stores bare filenames like rosa_canina_001, a Template of usercontent/img/{{ value }}.jpg resolves it to usercontent/img/rosa_canina_001.jpg. The cell stays clean and short; the naming convention is declared once in the template, not repeated thousands of times in your data.

This pattern has a practical consequence: renaming a folder or changing a file extension is a one-cell edit in the Template column, not a bulk find-and-replace across your data.

For fixed references - for instance, when every row uses the same SVG map - a static template string with no placeholder is valid: maps/europe.svg.

The F: directive - for text content

Some content is simply impractical to author inside a spreadsheet cell. A long species description with rich Markdown formatting, a full BibTeX bibliography maintained in a reference manager, a hand-crafted EML metadata block, or an About section that someone outside your team needs to edit periodically - these belong in dedicated files, not in cells. The F: directive is the bridge: write F: followed by a relative path inside usercontent/, and the compiler fetches that file at compile time and bakes its content in as if you had typed it into the cell yourself.

F:about.md
F:docs/species_notes/litoria.md
F:references.bib

The F: prefix is case-sensitive and must appear at the very start of the cell value with no leading spaces.

Where the F: directive applies depends on which table and column you're working in. Not every cell accepts it - only those where the underlying content type is text-based:

TableColumn / contextAccepted file extensions
data sheetAny column declared as markdown or text data type in Custom data definition.md, .markdown, .txt
BibliographyBibTeX entries column.bib, .txt
DwC archiveValue source column, when using the eml:precomposed directive.xml
CustomizationAbout section.md, .markdown, .txt

For .md files loaded into Data sheet cells, the compiler also rewrites any relative image paths they contain. If the file is at usercontent/docs/litoria.md and it references frog_photo.jpg, the image resolves to usercontent/docs/frog_photo.jpg. Images referenced by absolute URLs (https://...) or root-relative paths (/img/...) are left unchanged. These rewritten paths are automatically added to the list of assets to precache for offline use.

Because content is fetched at compile time, the published app has no runtime dependency on the source files - users receive the compiled result, not the files themselves. This has one important consequence: editing a file after publication has no visible effect until you recompile and republish.

When to reach for an F: directive:

  • Per-taxon descriptions - if individual taxa have long, richly formatted field notes, one file per taxon keeps authoring and version history clean. Pair it with a consistent naming convention (e.g. docs/lutra_lutra.md) and the pattern scales naturally.
  • Reusable content blocks - the same file can be referenced from as many rows as needed. Typical cases: a handful of standard survey protocols attached to the species they were used for, expedition or collector-team descriptions shared across all specimens from the same field trip, or habitat characterisations that apply identically to dozens of taxa. The compiler caches fetched files within a single compilation run, so there is no cost to reusing a file extensively. An update to the file is reflected everywhere on the next recompile, with no rows drifting out of sync.
  • Content maintained outside the spreadsheet - if your About text is lengthy, contains complex formatting, or is maintained by someone who is not comfortable editing spreadsheet cells, externalising it to usercontent/about.md keeps the spreadsheet clean and authorship flexible. For multilingual projects, use e.g. about.en.md and about.fr.md to manage language variants.
  • Bibliography and EML files - for BibTeX content managed in a reference manager, or EML metadata assembled by hand or exported from another tool, the F: directive lets you drop the file into usercontent/ and point to it without ever pasting its contents into a cell.

Paths and file naming

Whether you are writing a template expression or an F: directive, paths follow the same conventions: they must be relative - no leading /, no ./, no .. traversal - and use only forward slashes as separators. Path segments can contain letters, digits, underscores, hyphens, tildes, and dots.

For filenames on disk, keep things lowercase and avoid spaces (use underscores or hyphens instead). The reason this matters more than it might seem is that Linux web servers are case-sensitive: Litoria.jpg and litoria.jpg are different files, and a case mismatch will produce a broken asset with no obvious error message to point you at the cause.

File type reference

The table below is a cheat-sheet with suggestions for different kinds of external files.

Content typeMechanismWhere to configureSuggested folder
Taxon photosTemplate in Custom data definitionTemplate column on the image-type rowusercontent/img/
Sound recordingsTemplate in Custom data definitionTemplate column on the sound-type rowusercontent/audio/
SVG choropleth mapsTemplate in Custom data definitionTemplate column on the mapregions-type rowusercontent/maps/
Identification key imagesFilename in Single-access keysImages column (filename only, no keys/ path prefix)usercontent/keys/
Search Online iconsFilename in Search onlineIcon column (filename only, no online_search_icons/ path prefix)usercontent/online_search_icons/
Long Markdown text (About, per-taxon notes)F: directiveAny Markdown-accepting cellusercontent/ or usercontent/docs/
BibTeX bibliographyF: directive in BibliographyBibTeX entries columnusercontent/
Custom DwC-A EML metadataPath in DwC archiveeml:fromFile directiveusercontent/dwc/

Compile-time vs. runtime resolution

Not all files are fetched at the same moment, and understanding the difference matters for keeping your published app consistent.

Fetched at compile time - text and bibliography files. When you upload your spreadsheet, the compiler fetches every file referenced by an F: directive and bakes its content into checklist.json to ship them to the user with the rest of your published data. Once compiled, the published app has no dependency on those source files. Changing them after publishing has no effect on users. To reflect an update to a .md or .bib file, recompile and republish.

Fetched at runtime - media files. Images, sound files, SVG maps, and icons are not embedded in checklist.json. The app requests them from your server as users navigate. This means media files must remain in place and accessible at the same paths after publishing. Renaming or moving them without recompiling will produce broken media in the live app.

Precaching for offline use

The app automatically precaches all referenced local media files - images, sounds, SVG maps, and online search icons - in the browser's cache when the project is first loaded. This enables full offline use for users who have visited the app at least once.

At compile time, the compiler performs a HEAD request to each known asset to verify it exists. Files that return a 404 are reported as errors in the Review step, so you can catch typos in Template values or missing uploads before going live.

One consequence of caching is worth keeping in mind: if you replace a media file on the server with a different version under the same filename - a cropped photo, a corrected map - users who already have the app cached will continue seeing the old cached version. The straightforward fix is to treat the filename as part of the version: rename the updated file rather than overwrite it (e.g. litoria_aurea_v2.jpg instead of litoria_aurea.jpg), update the reference in your data cell or Template, and recompile. The old file can be left in place or removed - cached users will fetch the new one as soon as the updated app loads.

Two settings in Customization control precaching behaviour:

  • Precache max file size - maximum size in MB of a single file to include in the precache.
  • Precache max total size - maximum combined size in MB of all precached assets.

Large media files

If you use high-resolution photos, consider providing web-optimised versions - compressed JPEG or WebP - rather than raw camera files. This reduces precache size substantially and speeds up the first load for users on slow connections.

Working with the EML metadata file

If you export a Darwin Core Archive (via DwC archive), the compiler auto-generates an eml.xml metadata document from the fields configured in that table. For simple projects this may be sufficient. In practice you may need more control over the EML - for example, to include additional elements not supported by the table-based configuration - you can provide a hand-authored or reference-manager-exported eml.xml file. Upload it to usercontent/ (e.g., usercontent/dwc/eml.xml) and set the eml:fromFile directive in DwC archive to that path. The compiler will use your file instead of the auto-generated one.

Offline capability and PWA storage

NaturaList is a Progressive Web App and can operate fully offline once its assets and data are cached by the service worker. The app requests persistent storage from the browser on first load. If granted, cached data will not be automatically evicted under storage pressure. The current storage status (persistent / not persistent) is shown at the bottom of the side menu.

On Firefox and iOS Safari, persistent storage is not always granted; users on those platforms may occasionally see cached content cleared by the operating system. There is nothing you can do as an author to prevent this - it is a browser-level decision - but informing your users of this behaviour in your About text can help manage expectations.

Practical tips

Organise before you scale. Folder structure is easiest to define at the start. Moving files later means updating Template values and potentially broken paths in interim compilations. Pick a layout that makes sense for your project's volume and stick to it.

Use templates to decouple data from file paths. Store only a meaningful identifier in the data cell - a species code, a specimen number, a short slug - and let the template construct the path. This keeps your data portable and the file layout changeable without touching individual rows.

Reuse .md files freely. The compiler caches fetched files within a single compilation run, so referencing the same .md file from multiple cells (e.g. a shared methodology section across many taxa) is efficient and generates only one network request.

Validate paths during the Review step. The compiler performs HEAD requests to all known media paths at compile time and flags missing or unreachable files as errors. If you see path warnings during review, check for typos in your Template values or F: directives, and confirm the corresponding files exist in usercontent/ before publishing.

NaturaList documentation