When working with Salesforce Experience Cloud implementations, there is a common requirement to render a custom list of files rather than using the default UI component, which improves the user experience and allows site users to retrieve relevant files more efficiently.
For example, Salesforce returns the list of files as is, but sometimes there is a requirement to organize them.
For instance, if you share specific files with partners and customers annually, grouping them by year allows site members to find and download files for a particular year easily.
Additionally, what if you want to render the list of downloadable attached files right in the screen flow for external users?
In this post, I share an example of such a case.
Specifically, how to query the documents attached to an Account record, render an organized list split by years, and implement a download functionality for the external users in the community.
Fetching the data
First of all, how does Salesforce store files?
It's essentially a set of ContentDocument, ContentDocumentLink, and ContentVersion objects, where ContentDocument serves as a container for ContentVersion, and ContentDocumentLink acts as a middle object to link documents and other objects, such as users, account records, and so on.
Note: In Salesforce, you can have multiple versions of the same file. Although the default UI doesn't provide any user-friendly ways to handle and manage these, it is possible to implement custom solutions that work with versioning. For instance, fetching specific versions, previewing, and comparing them.
The second concern is sharing. What files can the external users access?
File Sharing and Visibility in Experience Cloud
Here is a good outline of file visibility and sharing in Experience Cloud sites.
Below are the most essential excerpts for our use case.
Regardless of the Experience Cloud site, users see files:
- they own
- shared with them directly
- shared to an Experience Cloud site they're a member of
- shared with a group that they can access
- that they can access within a library
- posted to a record they have access to, and when visibility on that file allows them to access it
- content added from Salesforce CMS
For example, it means that a site user can access record files only if they have access to the record, and the files' visibility allows site users to access them.
Otherwise, for example, you can rely on the programmatic sharing.
Given our scenario, let's create a simple Apex class to fetch the files attached to the record.
public with sharing class DocumentsController {
@AuraEnabled(cacheable=true)
public static List<ContentVersion> getDocuments(String recordId) {
List<ContentDocumentLink> contentDocumentLinks = [
SELECT ContentDocumentId
FROM ContentDocumentLink
WHERE LinkedEntityId = :recordId
WITH USER_MODE
];
Set<Id> ids = new Set<Id>();
for (ContentDocumentLink document : contentDocumentLinks) {
ids.add(document.ContentDocumentId);
}
return [
SELECT Id, Title, ContentDocumentId, ContentSize, FileType, VersionNumber, FileExtension, CreatedDate
FROM ContentVersion
WHERE ContentDocumentId IN: ids AND IsLatest = true
WITH USER_MODE
];
}
}
Note: I have returned a list of the latest versions in this class so that I can compare their creation dates. Otherwise, it's enough to return content document links directly, as you can download the file using its ContentDocumentId.
Related Files Lightning Web Component
Let's discuss the component architecture for rendering files split by years.
In this case, I will create two LWCs:
- A container that is responsible for fetching the data and transforming the state.
- A stateless component that accepts and renders the list of files.
Using this approach, you can separate the logic and keep the components reusable.
One of the things often overlooked is keeping both data fetching, business logic, and representation in the same component, which makes the architecture rigid.
With a suggested approach, a stateless component doesn't care about the logic, so you can reuse it in different contexts as soon as you pass a list of files into it.
Account Documents Container Component
I want to keep the main component as simple as possible and rely on the Lightning Data Services whenever possible.
The logic is straightforward. We take the ID of the current user, fetch their record to obtain the account ID, and use it to fetch the related documents via the class we created earlier.
Additionally, I have a getter that processes a flat list of files, splits them by the year they were created, and returns a new list of objects with the "year" and "documents" properties, allowing us to render them separately.
import { LightningElement, wire } from "lwc";
import userId from "@salesforce/user/Id";
import { getRecord, getFieldValue } from "lightning/uiRecordApi";
import ACCOUNT_ID from "@salesforce/schema/User.AccountId";
import getDocuments from "@salesforce/apex/DocumentsController.getDocuments";
export default class AccountDocuments extends LightningElement {
@wire(getRecord, { recordId: userId, fields: [ACCOUNT_ID] })
user;
@wire(getDocuments, { recordId: "$accountId" })
documents;
get accountId() {
return getFieldValue(this.user?.data, ACCOUNT_ID);
}
get documentsByYear() {
if (!this.documents?.data) {
return [];
}
return (
Object.entries(
this.documents?.data?.reduce((acc, doc) => {
const year = new Date(doc.CreatedDate).getFullYear();
if (!acc[year]) {
acc[year] = [];
}
acc[year].push(doc);
return acc;
}, {}) ?? {}
)?.map(([year, documents]) => ({ year, documents })) ?? []
);
}
}
Given that we have documentsByYear
, we can render the groups of documents in the template.
I mentioned that we will create a stateless component containing the documents themselves.
Let's call it just documents
and use it in a template. I will share its implementation in the next section.
<template>
<template for:each="{documentsByYear}" for:item="yearGroup">
<div key="{yearGroup.year}">
<h2 class="slds-text-heading_small slds-var-m-top_small">
{yearGroup.year}
</h2>
<c-documents documents="{yearGroup.documents}"></c-documents>
</div>
</template>
</template>
Depending on where you want to display the related files component, you can define the targets in its metadata file.
In my example, I want to expose the component to the Experience Cloud site and screen flows. Thus, I set the isExposed
to true and add required targets.
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Account Documents</masterLabel>
<targets>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
<target>lightning__FlowScreen</target>
</targets>
</LightningComponentBundle>
Documents Stateless Component and File Download
We are almost done with the implementation. The only bit left is to render the actual files and implement the download functionality.
Let's create the documents component that accepts files via the documents
attribute, renders them in a loop, and downloads the file on click.
Back in the day, I remember constructing the download URL, something similar to:
get downloadUrl() {
return `/sfc/servlet.shepherd/version/download/${contentDocumentId}`;
}
Fortunately, now we have a better way at our disposal.
It's a generateUrl
function from the file download module for LWC.
It allows you to generate the download URL for the files in LWR Experience Cloud sites.
Note: The feature is currently in a release preview and not yet generally available. Also, it's only available for the LWR sites.
Here is how the component controller looks:
import { LightningElement, api } from "lwc";
import { generateUrl } from "lightning/fileDownload";
export default class Documents extends LightningElement {
@api documents;
handleDownload(event) {
window.open(generateUrl(event.currentTarget.dataset.id), "_blank");
}
}
In the template, we render each file in the lightning layout grid as a button with the download icon.
We use the document name as a label and bind a content document ID to the dataset attributes, as generateUrl
accepts either ContentDocument, ContentVersion, Attachment, or Document ID.
Note: Generally, I'm not a fan of using the dataset attributes and would probably refactor the architecture.
Here is an example preview of the rendered files component.

Conclusion
In conclusion, customizing the presentation of files can enhance the end-user experience, particularly for external users, by allowing them to access relevant documents efficiently.
In this post, I described an approach to combine Apex and LWC to implement a custom solution that renders related files grouped by year.
I briefly mentioned the sharing and visibility for external users, as well as a new upcoming feature to generate download URLs in the context of Experience Cloud sites.
The given scenario is relatively narrow, but I hope it effectively demonstrates the main idea, and you can use it as a foundation to fulfill your requirements.

Nikita Verkhoshintcev
Senior Salesforce Technical Architect & Developer
I'm a senior Salesforce technical architect and developer, specializing in Experience Cloud, managed packages, and custom implementations with AWS and Heroku. I have extensive front-end engineering experience and have worked as an independent contractor since 2016. My goal is to build highly interactive, efficient, and reliable systems within the Salesforce platform. Typically, companies contact me when a complex implementation is required. I'm always open to collaboration, so please don't hesitate to reach out!