Expand my Community achievements bar.

Introducing Adobe LLM Optimizer: Own your brand’s presence in AI-Powered search and discovery

Recommended way for users to save articles/pages to profile

Avatar

Level 1

Hi all,

 

I want to allow logged-in users to save pages to their user account, so they can access them from their user profile page. This would function similarly to the "Reading List" on nytimes.com or "Saved Stories" on washingtonpost.com.

 

Are there existing AEM add-ons or plugins that facilitate this, or are there other recommended solutions?

 

Thanks!

Topics

Topics help categorize Community content and increase your ability to discover relevant content.

5 Replies

Avatar

Community Advisor

Hi @idyllopus 

AEM OOTB does not provide bookmarking features for end users.

However you can build a custom solution(A servlet) to save bookmark page to user profile/CUG user.

Or you can build something outside AEM, A database which allow to store and retrieve bookmarks and an API to interact.

Arun Patidar

AEM LinksLinkedIn

Avatar

Level 1

Thanks @arunpatidar -- are there existing tools you would recommend that handle similar tasks?

Avatar

Community Advisor

Hi @AmitVishwakarma 

In our case we did not used the AEM to store the dada because of multi publish setup and then syncing would be needed. So we created a spingboot app and integrated directly from frontend. 

 

but you can rely on AWS Lambda/Adobe IO to run a simple program which does read/write user's bookmark, saved into your database.

Arun Patidar

AEM LinksLinkedIn

Avatar

Community Advisor

Hi @idyllopus,

As @arunpatidar said saving personalized content is not available OOTB in AEM, but it can be implemented with a few common patterns.

I would recommend to go with Custom User Content Storage in JCR or External DB

1. Store saved pages under /home/users/<user-id>/... in JCR
  • AEM lets you store user-specific data under the user’s home folder in the repository.

  • Example:
    /home/users/s/santosh/save-list/page1
    /home/users/s/santosh/save-list/page2

  • You can create a custom servlet or Sling model to:

    • Save a page reference (eg. cq:Page path, title, thumbnail)

    • Retrieve it for the user's profile page

  • Requires login via AEM’s Adobe Granite OAuth or SAML/Auth handler.

2. Use an external storage (MongoDB, Firebase, etc.)
  • This is useful if you're on AEM as a Cloud Service and want to avoid storing user-generated data in JCR (not recommended for scale).

  • Create an API microservice to handle save/retrieve actions.

  • Can use Adobe I/O Runtime or your own serverless backend.

Now coming back to - Are there existing add-ons?

I remember - AEM used to have Communities (social features like user profiles, blogs, etc.) but they’re deprecated.


Santosh Sai

AEM BlogsLinkedIn


Avatar

Community Advisor

Hi @idyllopus ,

You can achieve this by storing user-specific data in:

  - JCR under /home/users/<uid> (good for AEM Sites / enterprise intranets)

  - External DB/Cloud (recommended for Cloud Service / scale)

Below is a proven JCR-based working approach for AEM 6.5.

1. Structure for Saved Pages

Each user in AEM has a node under /home/users/.

We'll store saved bookmarks like:

/home/users/a/amit-user/save-list/page-<hash>
    - jcr:title: "Page Title"
    - pagePath: "/content/mysite/en/news/article-1"
    - savedAt: "2025-06-13T12:33:55"

2. Bookmark Save/Remove Servlet

@Component(service = Servlet.class,
           property = {
               "sling.servlet.methods=POST",
               "sling.servlet.paths=/bin/user/bookmark"
           })
public class BookmarkServlet extends SlingAllMethodsServlet {

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
        String action = request.getParameter("action"); // "add" or "remove"
        String pagePath = request.getParameter("pagePath");

        Session session = request.getResourceResolver().adaptTo(Session.class);
        String userId = session.getUserID();

        String bookmarkNodeName = "page-" + Integer.toHexString(pagePath.hashCode());

        try {
            Node userNode = session.getNode("/home/users/" + userId.substring(0, 1) + "/" + userId);
            Node saveListNode = userNode.hasNode("save-list")
                    ? userNode.getNode("save-list")
                    : userNode.addNode("save-list", "nt:unstructured");

            if ("add".equals(action)) {
                Node bookmarkNode = saveListNode.hasNode(bookmarkNodeName)
                        ? saveListNode.getNode(bookmarkNodeName)
                        : saveListNode.addNode(bookmarkNodeName, "nt:unstructured");

                bookmarkNode.setProperty("pagePath", pagePath);
                bookmarkNode.setProperty("savedAt", Calendar.getInstance());
                bookmarkNode.setProperty("jcr:title", request.getParameter("title"));
            } else if ("remove".equals(action) && saveListNode.hasNode(bookmarkNodeName)) {
                saveListNode.getNode(bookmarkNodeName).remove();
            }

            session.save();
            response.setStatus(200);
        } catch (Exception e) {
            response.setStatus(500);
            response.getWriter().write("Error: " + e.getMessage());
        }
    }
}

3. Front-End Button (HTL + JS)

HTL

<button class="bookmark-btn" data-path="${currentPage.path}" data-title="${currentPage.title}">
    Save to My List
</button>

JS

document.querySelectorAll('.bookmark-btn').forEach(button => {
    button.addEventListener('click', () => {
        const pagePath = button.dataset.path;
        const title = button.dataset.title;

        fetch('/bin/user/bookmark', {
            method: 'POST',
            body: new URLSearchParams({
                action: 'add',
                pagePath,
                title
            }),
            headers: {
                'Content-Type': 'application/x-www-form-urlencoded'
            }
        }).then(resp => {
            if (resp.ok) {
                alert("Saved!");
            }
        });
    });
});

4. Read Saved Pages for Profile Page

Create a Sling Model or use JCR query to fetch all child nodes under:

/home/users/<first-letter>/<user-id>/save-list

Each node contains:

  - pagePath

  - jcr:title

  - savedAt

Render them as links on the user profile page.

Note:

Only allow logged-in users (validate session).

Protect /bin/user/bookmark servlet via Apache Sling Referrer Filter or CSRF filter.

Set rep:policy ACL to allow users to modify their own save-list.

Use hash of page path to uniquely name saved nodes.

For AEM Cloud Service, use Adobe I/O + Firestore/DynamoDB instead.

Regards,
Amit