2
0
Fork
You've already forked cms
0

Feature: support relative paths for uploaded files #69

Merged
esroyo merged 1 commit from main into main 2026年02月24日 23:31:20 +01:00
esroyo commented 2026年02月14日 11:47:33 +01:00 (Migrated from github.com)
Copy link

Hi @oscarotero,

I've found Lume CMS incredibly useful for my use case—it’s simple yet rich enough to be powerful. Thank you for sharing it :)

One feature I felt was missing is the ability to use relative paths for asset linking. I prefer relative paths because they make documents more portable and easier to render in different contexts (like GitHub previews), which is how I typically manage my Astro projects.

I've added an option to enable this behavior and would love to contribute it back. I'm open to discussing this further if you think it would benefit the broader audience. Obviously this would a completely optional new feature that you would need to opt-in for.

What’s new?

  • A new boolean option, preferRelativePaths, added to CmsOptions—and passed along to UploadOption for convenience.
  • When enabled:
    • File links in the upload modal (opened for editing/creating a document) are shown as paths relative to the current document.
    • When inspecting or editing a file field (type file), relative paths are correctly resolved.
    • New uploads store their field value as a relative path.

How it works

  • When opening upload modals, a parent URL parameter is now included, containing the current document’s directory. This parameter is kept during navigation inside the modal, via the referrer ([0] for more detail).
  • On upload, field values are set as relative or absolute according to the chosen option.
  • The document template injects documentPathDir (the document’s directory) alongside the fields definition throughout the component hierarchy. This allows the correct handling of paths in the UI and logic, and attaching the parent parameter on modals.
  • When rendering the upload list/edit template, the file data includes a new relativePath property. This property is filled only when preferRelativePath is enabled and the document reference is available. The logic here is "best effort."

[0] Note on Implementation:
I considered several options for preserving the parent parameter during modal navigation (such as the Navigation API), but encountered issues intercepting POST-method uploads and wanted to avoid intrusive changes to the server-side URL logic. I opted for a simpler approach that inherits the parameter from the referrer, though I recognize this is a slightly more "fragile" solution.


Let me know if you have any questions, suggestions, or a preferred approach for achieving this. Thanks again for your time and effort! ❤️


  • Update the PR descritpion (when code is ready)
Hi @oscarotero, I've found Lume CMS incredibly useful for my use case—it’s simple yet rich enough to be powerful. Thank you for sharing it :) One feature I felt was missing is the ability to use _relative paths_ for asset linking. I prefer relative paths because they make documents more portable and easier to render in different contexts (like GitHub previews), which is how I typically manage my Astro projects. I've added an option to enable this behavior and would love to contribute it back. I'm open to discussing this further if you think it would benefit the broader audience. Obviously this would a completely optional new feature that you would need to opt-in for. ### What’s new? - A new boolean option, `preferRelativePaths`, added to `CmsOptions`—and passed along to `UploadOption` for convenience. - When enabled: - File links in the upload modal (opened for editing/creating a document) are shown as paths relative to the current document. - When inspecting or editing a file field (type `file`), relative paths are correctly resolved. - New uploads store their field value as a relative path. ### How it works - When opening upload modals, a `parent` URL parameter is now included, containing the current document’s directory. This parameter is kept during navigation inside the modal, via the referrer ([0] for more detail). - On upload, field values are set as relative or absolute according to the chosen option. - The document template injects `documentPathDir` (the document’s directory) alongside the `fields` definition throughout the component hierarchy. This allows the correct handling of paths in the UI and logic, and attaching the `parent` parameter on modals. - When rendering the upload list/edit template, the file data includes a new `relativePath` property. This property is filled only when `preferRelativePath` is enabled and the document reference is available. The logic here is "best effort." --- [0] Note on Implementation: I considered several options for preserving the `parent` parameter during modal navigation (such as the Navigation API), but encountered issues intercepting POST-method uploads and wanted to avoid intrusive changes to the server-side URL logic. I opted for a simpler approach that inherits the parameter from the referrer, though I recognize this is a slightly more "fragile" solution. --- Let me know if you have any questions, suggestions, or a preferred approach for achieving this. Thanks again for your time and effort! ❤️ --- - [ ] Update the PR descritpion (when code is ready)

Hi and thanks for your contribution!
This is not really needed for Lume sites, since the Relative URLs plugin can do it automatically.
I don't know if there's a similar feature for other SSG like Astro.

You made a lot of changes here and I'm not sure if they are really needed.
Maybe configuring it as a global setting of the CMS is not a good idea and it's simpler if this can be managed at field level.

Maybe the file field could have a option to convert automatically all values to relatives. This would allow to paste any value (absolute or relative) and the value would be saved as relative (even if it's displayed as absolute in the CMS).

{
 name: "image",
 type: "file",
 upload: "src:images",
 relative: true
}
Hi and thanks for your contribution! This is not really needed for Lume sites, since the [Relative URLs plugin](https://lume.land/plugins/relative_urls/) can do it automatically. I don't know if there's a similar feature for other SSG like Astro. You made a lot of changes here and I'm not sure if they are really needed. Maybe configuring it as a global setting of the CMS is not a good idea and it's simpler if this can be managed at field level. Maybe the `file` field could have a option to convert automatically all values to relatives. This would allow to paste any value (absolute or relative) and the value would be saved as relative (even if it's displayed as absolute in the CMS). ```js { name: "image", type: "file", upload: "src:images", relative: true } ```
esroyo commented 2026年02月14日 17:57:29 +01:00 (Migrated from github.com)
Copy link

@oscarotero thanks for the quick response; I perfectly understand the concerns about the amount of changes.

Having a relative option in the file type is interesting. Just to be sure, you are proposing that the conversion from absolute to relative paths happens exactly when storing the data, right? I would also need to parse and convert the "content" of the markdowns too, but sounds definitively doable.

The only loose end I'm left with is to make the file "inspection" option (the magnifying glass icon click) work with relative values later on. For that, I would still need to reverse to absolute path before opening the modal. That is a bit of a challenge. May be the conversion could always be done in the read/write operations? As a consequence, the CMS UI user would not be aware if it unless they see the source of the document.
Could I take advantage of the transform hook for that? 🤔

Let me know what do you think. Thanks!

@oscarotero thanks for the quick response; I perfectly understand the concerns about the amount of changes. Having a `relative` option in the `file` type is interesting. Just to be sure, you are proposing that the conversion from absolute to relative paths happens exactly when storing the data, right? I would also need to parse and convert the "content" of the markdowns too, but sounds definitively doable. The only loose end I'm left with is to make the file "inspection" option (the magnifying glass icon click) work with relative values later on. For that, I would still need to reverse to absolute path before opening the modal. That is a bit of a challenge. May be the conversion could _always_ be done in the read/write operations? As a consequence, the CMS UI user would not be aware if it unless they see the source of the document. Could I take advantage of the `transform` hook for that? :thinking: Let me know what do you think. Thanks!

My idea is to make this conversion transparent for the user, since it's a decision of the developer that configure the cms. The end user should see always absolute paths and only in the moment of saving the value, it's converted to relative.

All fields have a applyChanges function, responsive to apply the changes to the final object that will be saved in the store.
Take a look to the number field for a simple example:

  • The value is converted to a number
  • If the result is a valid number (not a NaN), it's saved in the data object using the name of the field as the key.
  • If the value is not valid but is required, it saves 0 as default
  • Otherwhise, remove the value from the object (equivalent to set it as undefined).

The function for the file field is a bit more complex since the frontend send an object with two values: the previous value (string) and the new file (File) but and the end, once the file is stored, it applies the final path to the data object.

So the idea is to make the conversion there, in the moment of saving the data. The document argument is where the final data will be stored, so you can know the path of the document with document.name.

My idea is to make this conversion transparent for the user, since it's a decision of the developer that configure the cms. The end user should see always absolute paths and only in the moment of saving the value, it's converted to relative. All fields have a `applyChanges` function, responsive to apply the changes to the final object that will be saved in the store. Take a look [to the `number` field](https://github.com/lumeland/cms/blob/main/fields/number.ts#L38) for a simple example: - The value is converted to a number - If the result is a valid number (not a `NaN`), it's saved in the `data` object using the name of the field as the key. - If the value is not valid but is required, it saves `0` as default - Otherwhise, remove the value from the object (equivalent to set it as `undefined`). The [function for the `file` field](https://github.com/lumeland/cms/blob/main/fields/file.ts#L49) is a bit more complex since the frontend send an object with two values: the previous value (string) and the new file (`File`) but and the end, once the file is stored, it applies the final path to the `data` object. So the idea is to make the conversion there, in the moment of saving the data. The `document` argument is where the final data will be stored, so you can know the path of the document with `document.name`.
esroyo commented 2026年02月15日 18:55:11 +01:00 (Migrated from github.com)
Copy link

@oscarotero thanks for the advice, sounds good. I'll give it a try and let you know 👍

@oscarotero thanks for the advice, sounds good. I'll give it a try and let you know 👍
esroyo commented 2026年02月18日 23:27:37 +01:00 (Migrated from github.com)
Copy link

@oscarotero, thanks for the fantastic advice! The approach of converting to relative on applyChanges and reverting back to absolute on init is much leaner and achieves the goal with a much smaller footprint overall.

Whenever you have a moment, please take a look at the current diff and let me know what you think. Thanks so much!

@oscarotero, thanks for the fantastic advice! The approach of converting to relative on `applyChanges` and reverting back to absolute on `init` is much leaner and achieves the goal with a much smaller footprint overall. Whenever you have a moment, please take a look at the current diff and let me know what you think. Thanks so much!

Did you use AI to create this PR?

Did you use AI to create this PR?
esroyo commented 2026年02月21日 17:23:23 +01:00 (Migrated from github.com)
Copy link

Did you use AI to create this PR?

Yes, limitedly. I used Google Gemini (web chat) to assist me in the creation of the RegExps found in replacePaths (and the test suite for those text replacements).
In any case wiring and decisions are my own, so if something is flawed in the PR, AI is not to blame but just me.

> Did you use AI to create this PR? Yes, limitedly. I used Google Gemini (web chat) to assist me in the creation of the RegExps found in `replacePaths` (and the test suite for those text replacements). In any case wiring and decisions are my own, so if something is flawed in the PR, AI is not to blame but just me.
@ -63,3 +65,3 @@
if ("fields" in json) {
json.fields = await Promise.all(
json.fields.map((f) => prepareField(f, content, data)),
json.fields.map((f) => prepareField(f, content, data?.[field.name], document)),

The Document argument shouldn't be optional. So this signature should be changed to:

export async function prepareField(
 field: Lume.CMS.ResolvedField,
 content: CMSContent,
 data: Data | undefined,
 document: Document,
)
The `Document` argument shouldn't be optional. So this signature should be changed to: ```ts export async function prepareField( field: Lume.CMS.ResolvedField, content: CMSContent, data: Data | undefined, document: Document, ) ```
@ -6,7 +6,8 @@
},

Don't use specifiers from esm.sh.

 "@std/assert": "jsr:@std/assert@1.0.18"
Don't use specifiers from esm.sh. ```suggestion "@std/assert": "jsr:@std/assert@1.0.18" ```
@ -13,6 +13,12 @@ interface FileField extends InputField<ResolvedFileField> {

The option name should be more descriptive, for example relativePath.

The option name should be more descriptive, for example `relativePath`.
@ -22,6 +24,12 @@ interface MarkdownField extends InputField<ResolvedMarkdownField> {
* The value can contain the {$} placeholder that will be replaced by the currently selected text.

The option name should be more descriptive, for example relativePath.

The option name should be more descriptive, for example `relativePath`.
@ -11,6 +13,12 @@ interface RichTextField extends InputField<ResolvedRichTextField> {
* If it's false, no uploads are used.

The option name should be more descriptive, for example relativePath.

The option name should be more descriptive, for example `relativePath`.

If the init function will receive the Document instance, it shouldn't be optional. So the signature must be similar to prepareField:

init(
 field: T,
 content: CMSContent,
 data: Data | undefined,
 document: Document,
)
If the `init` function will receive the `Document` instance, it shouldn't be optional. So the signature must be similar to prepareField: ```ts init( field: T, content: CMSContent, data: Data | undefined, document: Document, ) ```
esroyo (Migrated from github.com) reviewed 2026年02月22日 23:14:49 +01:00
@ -63,3 +65,3 @@
if ("fields" in json) {
json.fields = await Promise.all(
json.fields.map((f) => prepareField(f, content, data)),
json.fields.map((f) => prepareField(f, content, data?.[field.name], document)),
esroyo (Migrated from github.com) commented 2026年02月22日 23:14:48 +01:00
Copy link

@oscarotero I'm having difficulties with it, as there are sites where the document does not exists, like when creating a collection. Any thoughts? Thanks

@oscarotero I'm having difficulties with it, as there are sites where the document does not exists, like [when creating a collection](https://github.com/lumeland/cms/blob/main/core/routes/collection.ts#L85). Any thoughts? Thanks
@ -63,3 +65,3 @@
if ("fields" in json) {
json.fields = await Promise.all(
json.fields.map((f) => prepareField(f, content, data)),
json.fields.map((f) => prepareField(f, content, data?.[field.name], document)),

Oh, you're right.
Okay, let's leave it as is.

Oh, you're right. Okay, let's leave it as is.

Thank you!

Thank you!
esroyo commented 2026年02月28日 20:17:45 +01:00 (Migrated from github.com)
Copy link

@oscarotero Thank you so much for accepting the change so quickly 🙏 ❤️
I have submitted an update for the documentation as well https://github.com/lumeland/lume.land/pull/243

@oscarotero Thank you so much for accepting the change so quickly :pray: :heart: I have submitted an update for the documentation as well https://github.com/lumeland/lume.land/pull/243
Sign in to join this conversation.
No reviewers
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lume/cms!69
Reference in a new issue
lume/cms
No description provided.
Delete branch "main"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?