Building a Purple Experience
Bundles
12 min
please follow this documentation of how to setup bundles in the hub there are 2 ways to configure and display bundles in experience using the content body component (app + web) this configuration uses a single content body component and does not support swiping between items its ux is mainly intended for the usage in websites to implement this approach, you need to configure two views view 1 path — prefix/\ contentslug view 2 path — prefix/\ bundleslug (note prefix can be replaced with any static path segment ) both views should include the content body component the only difference between them lies in the property filters one uses \ contentslug, and the other uses \ bundleslug swiper of posts (only app) to leverage swipe functionality, we use a swiper of content body components (also referred to as the post swiper ) since the content body component can render both posts and bundles, this setup allows users to swipe through individual posts within a bundle steps to implement create a view and a function swiper configuration set the swiper's content type to content body component you can configure additional functionality just like with any swiper by default, the effect is set to "slide" handling bundle data for the swiper the bundle content type must be converted to be used in swiper data sources a bundle is structured as an array of objects where each item has a post key with its associated post data to convert this into a simple array of posts, use the custom server side function getbundlelist define it in your custom server ts getbundlelist (bundle) => { return bundle contents map(content => content post) }, use a custom data source to supply this flattened data to the swiper data source via url resolver the actual content data for the swiper should be fetched through the url resolver read the next section to see, how tracking setup to enable proper analytics and engagement tracking, make sure the postview field is enabled url resolver setup properly configure the url resolver for displaying the swiping bundle, two parts must be implemented a datatopathresolver function this determines the url structure based on the content type and device type example it's important to include a path parameter (like the bundle slug) so that the content can be resolved later via urltoviewresolvers b urltoviewresolvers array this allows the system to retrieve the correct bundle data based on the slug include the flag includebundledcontent in the query to fetch the bundle along with its posts / data to path resolver maps content to available urls this function is used by opencontent actions to generate urls for navigation / async function datatopathresolver({ content, taxonomy, collection, publication, dataresolver }) { // handle content (posts, articles, etc ) if (content) { if (content contenttype === 'bundle' && dataresolver contextservice viewcontext device type !== 'desktop') { return {paths \[`readbundle/${content properties slug}`]}; } } } / url to view resolvers handle incoming urls and map them to views / const urltoviewresolvers = \[ { pathpattern '/readbundle/\ bundleslug', async viewresolver({ match, resolveddata, dataresolver }) { const bundleslug = match params bundleslug; // check if post slug exists in resolved data if (!resolveddata\[bundleslug] || !resolveddata\[bundleslug] contents length) { return { notfound true }; } // get the content const contentmatch = resolveddata\[bundleslug] contents\[0]; try { const content = await dataresolver findcontentbyid(contentmatch id, { includebundledcontent true }); // return the appropriate view for the content return { viewname 'readbundle', viewcontext { content content, } }; } catch (error) { console error('error resolving preview content ', error); return { notfound true }; } } }, ]; // export the configuration export default { urltoviewresolvers, datatopathresolver }; display ads or other dependent components to display dynamic or contextual content—such as ads, ctas, or related modules —based on the currently visible post use the swiper id param (or another custom param configured in the swiper) the swiper id updates in real time as the user swipes, matching the id of the current post you can use this id to conditionally render specific content components (e g , ad components) example usage getadid (swiperid) => { const adlist= \['bundlead1', 'bundlead2', 'bundlead3'] return adlist\[math floor(math random() adlist length)]; } styling buttons to change the position of the nav buttons, you can override it in css as follows app // app css selectors media (max width 767px) { // default is absolute pxp swiper swiper button { position fixed !important; } // default is 10px swiper button next { right 0px !important; } // default is 10px swiper button prev { left 0px !important; } } web (default css from experience) it can be overwritten in via scss // default web css selectors bundle button prev { left 0; right auto; } bundle button next { right 0; left auto; } bundle button prev, bundle button next { position absolute; top 50%; width 27px; height 44px; margin top 22px; z index 10; cursor pointer; background { size 27px 44px; position center; repeat no repeat; } } configure both bundle behaviors simultaneuosly to support both behaviors within the same app (i e , displaying the same view using the content body on the web and swiping bundles in the app), we need to apply both configurations simultaneously this involves creating three views two for the content body method and one for the swiping bundle method additionally, the url resolver must be updated to support both approaches below is an example implementation of the updated url resolver / data to path resolver maps content to available urls this function is used by opencontent actions to generate urls for navigation / async function datatopathresolver({ content, taxonomy, collection, publication, dataresolver }) { // check if content is provided if (content) { // get the device type from the view context (e g , 'desktop', 'mobile') const { device type } = dataresolver contextservice viewcontext; // handle non desktop (mobile/tablet) devices if (device type !== 'desktop') { // if the content is a bundle, route to a bundle reader path if (content contenttype === 'bundle') { return { paths \[`readbundle/${content properties slug}`] }; } // if it's a post not part of a bundle, use a direct path if (content bundleid == null) { return { paths \[`read/${content properties slug}`] }; } // if it's a post in a bundle, use the bundle reader with swiper id return { paths \[`readbundle/${content bundleid}?swiper id=${content id}`] }; } // desktop device logic const isbundle = content contenttype === 'bundle'; const issingledpost = content contenttype === 'post' && content bundleid == null; // if the content is a bundle or a single post (not part of any bundle), use its slug if (isbundle || issingledpost) { return { paths \[`read/${content properties slug}`] }; } // if it's a post inside a bundle, resolve the bundle slug and create a nested path if (content contenttype === 'post') { const bundle = await dataresolver findcontentbyid(content bundleid); const bundleslug = bundle properties slug; return { paths \[`read/${bundleslug}/${content properties slug}`] }; } } } / url to view resolvers handle incoming urls and map them to views / const urltoviewresolvers = \[ { // define the url pattern to match (e g , /readbundle/my bundle slug) pathpattern '/readbundle/\ bundleslug', // function to resolve the view when the url matches async viewresolver({ match, resolveddata, dataresolver }) { // extract the bundle slug or id from the url const bundleslugorid = match params bundleslugorid; // attempt to get the content id from resolved data, fallback to using the bundleslugorid directly // in this case bundleslugorid is the bundle id const contentmatchid = resolveddata\[bundleslugorid]? contents\[0]? id || bundleslugorid; try { // fetch the content by id, including any bundled content const content = await dataresolver findcontentbyid(contentmatchid, { includebundledcontent true }); // return the matched view and inject the content into the view context return { viewname 'readbundle', viewcontext { content content } }; } catch (error) { // handle errors and fallback to a notfound response console error('error resolving preview content ', error); return { notfound true }; } } } ]; // export the configuration export default { urltoviewresolvers, datatopathresolver }; view path structure the view paths follow this structure read/\ contentslug read/\ bundleslug/\ postslug readbundle (note read is used as a prefix in all routes ) we generally have 3 use cases 1\ opening a bundle in this case, we must provide the bundle slug to the datatopathresolver this is essential to query the bundle content and include it in the view context 2\ opening a single post for single posts that are not part of a bundle , we always redirect to read/\ bundleslug/\ postslug this ensures a consistent path structure, even if swiping isn't required 3\ opening a post within a bundle (swiping bundle case) when using bundle swiper we pass the bundle id as a path parameter we pass the post id as a query parameter (swiper id by default) the swiper id helps determine which post should be displayed inside the bundle view for the content body case , we need to retrieve the bundle slug in the datatopathresolver to correctly construct the view path important remarks a post can be associated with multiple bundles therefore, when opening a post, the expected behavior needs to be clearly defined below are the possible options open the post directly , without reference to any bundle open the post within the bundle that matches the bundleid associated with the post open a separate view or popup listing all possible bundles, allowing the user to choose display a small section within the component (e g , content or search) that lists all related bundles, letting the user select one to open currently, option 2 used in this documentation