When you use files, and not attachments, you might notice that files, whose names contain a / (slash), are stored with a modified name, viz. only the part that follows the /

For example, file abc/def.pdf will be stored as def.pdf (and no, not inside directory abc).

As long as you use Classic, that does not have to be a big deal. For example, in quotes, Classic will remember the original full file name and will use that name when sending files/attachments via e-mail.

But in Lightning it's different: Lightning will send the file with the modified name.

Why/when is this a big deal? For companies in the tyre industry. Quotes for tyres will often use the measures of the tyres and those measures will always contain a / So the PDF in the e-mail will have a strangely looking name. You might say, this is splitting hairs, but it worked OK in Classic and the transition to Lightning will make us look less professional (and looks matter, don't they?)

Simply replacing the / by a - or + looks funny. So, any tips, workarounds for this?

1 Answer 1


I have a solution for my particular problem, Quote PDFs for Quotes with names containing a /

I have done this with a trigger:

trigger ContentDocumentLinkTrigger on ContentDocumentLink(after insert) {

and of course its handler:

public with sharing class ContentDocumentLinkTriggerHandler {

    public static void ProcessOnInsert(List<ContentDocumentLink> newList) {
        Set<Id> quoteIds = new Set<Id>();
        Map<Id, Id> mapQuoteIdToCDId = new Map<Id, Id>();

        for (ContentDocumentLink cdl : newList) {
            String lEId = cdl.LinkedEntityId;
            String objectPrefix = lEId == null ? '' : lEId.left(3);
            if (objectPrefix == '0Q0') {
                mapQuoteIdToCDId.put(lEId, cdl.ContentDocumentId);
        Map<Id, Quote> mapQuotes = new Map<Id, Quote>([SELECT Id, Name FROM Quote WHERE Id IN :quoteIds]);

        Set<Id> contDocIds = new Set<Id>();
        Map<Id, String> cdIdToQuoteName = new Map<Id, String>();

        for (Id quoteId : quoteIds) {
            if (mapQuotes.containsKey(quoteId)) {
                Quote q = mapQuotes.get(quoteId);
                if (q.Name.contains('/')) {
                    Id contentDocumentId = mapQuoteIdToCDId.get(quoteId);
                    cdIdToQuoteName.put(contentDocumentId, q.Name);

        // Modify document/file names if necessary.
        Map<Id, ContentDocument> mapContentDocuments = new Map<Id, ContentDocument>([SELECT Id, Title, FileExtension FROM ContentDocument WHERE Id IN :contDocIds]);
        List<ContentDocument> cdsToUpdate = new List<ContentDocument>();
        for (ContentDocumentLink cdl : newList) {
            if (mapContentDocuments.containsKey(cdl.ContentDocumentId)) {
                ContentDocument objCntDoc = mapContentDocuments.get(cdl.ContentDocumentId);
                // Apply full Quote name with / replaced by -.
                // objCntDoc.Title contains part of the Quote name + a version.
                // For example Quote "abc/offerte" will have a file with Title "offerte_V1".
                Integer startOfVersionInfo = objCntDoc.Title.lastIndexOf('_V');
                String versionInfo = startOfVersionInfo >= 0 ? objCntDoc.Title.substring(startOfVersionInfo) : '';
                String strFilename = cdIdToQuoteName.get(cdl.ContentDocumentId).replace('/', '-') + versionInfo + '.' + objCntDoc.FileExtension;
                objCntDoc.Title = strFilename;
        if (cdsToUpdate.size() > 0) {
            update cdsToUpdate;

I wish it could be done in a simpler way, but this works for me.

Kudos to tushar who came up with the basis for this solution here.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .