Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content Management #67

Closed
JedWatson opened this issue Nov 11, 2013 · 35 comments

Comments

Projects
None yet
@JedWatson
Copy link
Member

commented Nov 11, 2013

It would be great to be able to define a data structure for storing content, where pages would be a single representation instead of a collection.

The structure would be defined in a similar syntax to models:

keystone.set(‘content', {
    home: {
        title: String,
        headerImage: Types.CloudinaryImage,
        intro: { Types.Html, wysiwyg: true }
    },
    about: {
        title: { type: String, default: 'About' },
        content: { Types.Html, wysiwyg: true }
    }
});

… then there would be an easy way to retrieve content for a particular page, something like this:

keystone.getContent(‘about’, callback);

Page content would be stored on a page-per-document basis, in a collection called app_content.

@ryanlewis

This comment has been minimized.

Copy link

commented Feb 24, 2014

Been evaluating some CMS platforms and see the management of 'static' content (Home, About etc.) as a key feature. Whats the current status of this?

@JedWatson

This comment has been minimized.

Copy link
Member Author

commented Feb 24, 2014

It's been on hold for a bit (we've been flat out) but should be picked up again soon. There's a project we're working on that really wants it.

I'm hesitant to make promises about timing on new features but this is close to the top of the list, focus-wise. I agree it's a key feature :)

What kind of timeline are you on?

@ryanlewis

This comment has been minimized.

Copy link

commented Feb 24, 2014

Looking at delivery in May so would need to be able to allow the client content entry by mid April at the latest.

Have you guys got a roadmap?

@JedWatson

This comment has been minimized.

Copy link
Member Author

commented Feb 24, 2014

Drafted one last night, actually. Will post it up here soon and link to the issues we've got planned for upcoming releases.

For content management, I don't think April would be a push at all, if you guys are adopting Keystone I'd be happy to work with you to make sure it all works out.

In other news, just realised your company has an office in Sydney about four blocks from mine... small world :)

@jdarling

This comment has been minimized.

Copy link
Contributor

commented Feb 25, 2014

This is great news Jed. I was just talking with others the other day about Keystone and they asked how it handled "static" content.

The other thing they asked was how it handled dynamic content inside a static page. Basically, on the home screen you have some introductory text, then maybe some images, and a section that reports back the recent news items (say top 3 that exist). Something to think about.

@ryanlewis

This comment has been minimized.

Copy link

commented Mar 3, 2014

Hi Jed

Gone with another CMS for now purely for maturity and an established feature-set. Definitely have you Keystone earmarked for the future however - definitely think this project is heading in the right direction.

Would love the opportunity to hear your plans in the future regarding this feature, and can stick in some 2 cents (and contribute, given the constraints of parenthood! 😄 )

@alexzaporozhets

This comment has been minimized.

Copy link

commented May 7, 2014

Hello, any updates on this issue? It is really important feature.
At least provide tutorial how to create it appropriate way for keystone

@talon

This comment has been minimized.

Copy link
Contributor

commented May 12, 2014

I'm looking into working on this issue. I see you already have some of the groundwork under lib/content but not sure how to dive in.

@talon

This comment has been minimized.

Copy link
Contributor

commented May 14, 2014

@alexzaporozhets
There needs to be formal documentation about this, probably when it is more complete however this is what I have discovered.

You can add this to your layout/base.jade or however suits you:

if user && user.canAccessKeystone
  script(src='/keystone/js/content/editor.js')

that script will scan the page for all elements with a data-ks-editable={"type": "list"} and parse the json/use the data to display a button that's action depends on what the type of ks-editable is.

here's a schema of what is currently expected in the JSON:

{
  type: 'list', // can be list, content or error (not sure what error is?)
  path: '', // (required for type.list) keystone prefixed path to the list page
  plural: '', // (required for type.list) pluralized list name
  singular: '', // (required for type.list) singular list name
  id: '' // (optional for type.list) specific list item to link to
}

this line is the one that particularly pertains to this issue. It is subjective what this case exactly should do but I think it should replace the data-ks-editable element with a live content editor (based on the content field type just like the admin UI). I think this is started here

You can study this folder to figure more out but the content api in its current state roughly works like such:

var keystone = require('keystone')
  , Types = keystone.content.Types
  , Home = keystone.content.Page('Home');

Home.add({
  title: {type: Types.Text, required: true},
  about: {type: Types.Text, required: true}
})

// FETCH
// =====
keystone.content.store('home', {title: 'Welcome', about: 'such content' }, function(err) {
  if (err) console.log(err);
});

keystone.content.fetch('Home', function(err, content) {
  console.log(content) // [Object Object]
});

A lot of this still needs to be coded out. So don't get too ambitious with using it in the wild yet.

Some things I'm still trying to figure out:

  • why there needs to be separate field types for content
  • if/how to add these contents to the Admin UI to be edited there.

Sorry if any of this is unclear. I wrote it all mostly as a reference for myself as I am going to start hacking on this issue tonight or tomorrow night

@itzaks

This comment has been minimized.

Copy link
Contributor

commented Jun 19, 2014

Any updates here? :–) I'm happy to help.

@spbarber

This comment has been minimized.

Copy link

commented Jun 27, 2014

Has this been put on hold for a while or will it be implemented in the near future?

@talon

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2014

Update on this please!

@ignlg

This comment has been minimized.

Copy link

commented Aug 14, 2014

About the Admin UI part, it seems better to wait for the React rewrite. So we don't code it twice in a short period of time and we can take advantage of the React's (potential) UI flexibility too.

@JedWatson

This comment has been minimized.

Copy link
Member Author

commented Aug 14, 2014

@ignlg is correct, to build a really cool content management interface in Keystone we want the React UI up and running first. I probably should have listed this issue in #503 as one of the things that will unblock.

I've also been uneasy about the duplication of field types for content, especially since we'll be implementing a proper API for plugging in custom field types, so getting that in place is also important.

We're also going to end up with the ability to customize the Admin UI, which this needs to fit in with.

Sorry to anyone who's been waiting on this, but we really want to make sure we get it right, and have the right pieces in place first.

@jdarling

This comment has been minimized.

Copy link
Contributor

commented Aug 14, 2014

Better to take your time and get it right than to rush something out the door and piss everyone off.

@frederikprijck

This comment has been minimized.

Copy link

commented Nov 1, 2014

Any update about the timing for the static content management feature? Not that I want to be impatiantly, just checking in for an update since this thread has been inactive for 2,5 months!

I see the update being mentioned in #322 , However there's no estimated timing their aswell.

@marekgoczol

This comment has been minimized.

Copy link

commented Jan 21, 2015

it's been open for over a year now, I would appreciate any timing update on that.

@morenoh149 morenoh149 added this to the 0.3.x milestone Jan 21, 2015

@JedWatson JedWatson modified the milestones: Backlog, 0.3.x Jul 20, 2015

@jack-guy

This comment has been minimized.

Copy link

commented Aug 13, 2015

So this isn't being targeted for a release in 0.3?

@itzaks

This comment has been minimized.

Copy link
Contributor

commented Aug 16, 2015

Hm that's too bad. Still checking up on this issue now and then for updates. I'm eager to help if there's something I can do.

@acontreras89

This comment has been minimized.

Copy link

commented Sep 15, 2015

👍

@creynders

This comment has been minimized.

Copy link
Contributor

commented Sep 15, 2015

TBH I don't really see the need for this (at least not for static page generation). You can achieve "static" pages, by creating a Pages collection and creating a view which resolves a url parameter to determine what "page" needs to be shown.
See

All you need to do is create a Page document, fill out the slug field. Then if you surf to //<host>/content/<slug> it'll show whatever you filled out content wise. Obviously if you need multiple page types, i.e. with various fields you'd need multiple collections. But that would be the same with the above approach.

@acontreras89

This comment has been minimized.

Copy link

commented Sep 15, 2015

@creynders, as you can see in the original comment, the idea is to be able to manage (maybe completely) different pages from the same "collection". For now, you either create a different collection for each different layout or you limit yourself (and the people who'll be working with the CMS later on) to generic fields (like having a couple of WYSIWYG editors to compose the whole page.)

On the other hand, I can't help it but feel like this feature would make a huge difference when it comes to internationalization.

@creynders

This comment has been minimized.

Copy link
Contributor

commented Sep 15, 2015

@acontreras89 ah yes, didn't read it thoroughly enough I realise! Ok, nvm my other post!

@lojack

This comment has been minimized.

Copy link
Contributor

commented Sep 15, 2015

I wrote about an approach that accomplishes this using list inheritance a little while ago. The approach is a bit different than what @JedWatson proposed. But you can read about it @ http://rob.codes/creating-a-page-router-in-keystonejs/

If enough people find this useful I should be able to pull this functionality out into its own library or integrate it with keystone.

@acontreras89

This comment has been minimized.

Copy link

commented Sep 15, 2015

thanks for sharing your experience, @lojack
Your approach is somewhat similar to what we were talking

create a different collection for each different layout

Of course we can work around the lack of centralized static content management, but having this functionality would likely make things easier 😁

@Mentioum

This comment has been minimized.

Copy link

commented Sep 20, 2015

My way allows pages to be created within the same collection? I believe it works the same way as @creynders mentioned except instead of using different collections for different templates I just load different partials within the same handlebars template from a select dropdown in the Page model.

screenshot from 2015-09-20 10 58 24

I just created a collection which allows the user to change the slugs.

Then if I go to through that collection before I do other routes which might match the same pattern checking against the requested URL for a match. If I find a match I parse the item, if I don't i continue down the other routes.

Each 'page' in that collection then has a Select dropdown for the template they want to use for that page which is populated on startup from a template folders contents. It then shows the correct fields for that template using 'dependsOn:'

The handlebars template then has a switch statement in it which loads a different partials depending on template options and other page settings.

Works fine IMO. I'd like a more efficient way of checking perhaps but tbh its going to be just fine unless you've got hundreds of custom pages.

If anyone is looking for an example of more detail please do get in touch.

@charleslouis

This comment has been minimized.

Copy link

commented Nov 30, 2015

Hey there !

Thanks for sharing @Mentioum and for offering a more detailed example !

We're looking into this solution as well as discussed here : https://groups.google.com/forum/#!topic/keystonejs/yIqUNaD_H30 since we just figured out keystone doens't offer a single representation for single pages instead of a collection.

Would you mind putting together a Gist example or screen captures to illustrate how you achieve this ?
Mainly :

  • how do we implement dependsOn in the model and create the dropdown ?
  • how to populate the template list from the /templates folder ?

I'll start digging this meanwhile, but that would be awesome !

@charleslouis

This comment has been minimized.

Copy link

commented Dec 1, 2015

Based on @Mentioum solution, here is how we are handleling this at the moment :
Disclaimer : this solution is not advanced/better, but the code example might help some people.

1 - We've duplicated Post.js model to create a Page.js model

2- created a dropdown with templates
template: { type: Types.Select, options: 'page, about, team, contact, portfolio', default: 'page'},

3- display someField if the selected template is 'about'
someField: { type: String, dependsOn: { template: 'about' } },

and it works :)

Now, I'd like to have an OR operator in dependsOn: { template: 'about' }, so that I can do something like dependsOn: { template: 'about' OR 'legal' } or dependsOn: { template: 'about || legal' }

Any idea on how to make this happen ?

Thanks !

Full code example for Page.js here : https://gist.github.com/charleslouis/ff5cc15ce7d2f3aee59d

@ignlg

This comment has been minimized.

Copy link

commented Dec 1, 2015

What about allowing functions?

dependsOn: { template: checkTemplate }

That way it's easy to be extremely flexible:

function checkTemplate(template) {
  return (template === 'about' || template === 'legal');
}

It's the easiest way to allow any behaviour, complex or not.

@charleslouis

This comment has been minimized.

Copy link

commented Dec 1, 2015

Yes indeed @ignlg !! Thank you very much !

@Mentioum

This comment has been minimized.

Copy link

commented Jan 6, 2016

Apologies for disappearing, been busy with work.

@charleslouis and @ignlg The dependsOn object takes an array or a list of comma separated variables (String) as well (I believe) as just a single value so I believe so you can have multiple template values show the same fields if you choose to. I like the idea of being able to use a function a lot though @ignlg

I actually called my model 'SpecialPages' and ended up storing all sorts of stuff in there:

Model

var keystone = require('keystone');
var Types = keystone.Field.Types;

/**
 * Special Page Model
 * ==================
 */
var Pages = [
  'About',
  'Home',
  'Facilities',
  'Gallery',
  'Team',
  'FAQ',
  'Contact',
  'Book a Tour',
  'Help'
];

var SpecialPage = new keystone.List('SpecialPage', {
  map: {name: 'title'},
  plural: 'SpecialPages'
});


SpecialPage.add({
  active: {type: Types.Boolean },
  title: { type: String, required: true, intial: true},
  page: {type: Types.Select, options: Pages, note: 'Choose which page this custom data is for.  Make sure there is only one SpecialPage per SpecialPage type Active.' },
  home: {
    testimonies: {type: Types.Relationship, ref: 'Testimony', many: true, dependsOn: {page:'Home'}},
    segments: {type: Types.Relationship, ref: 'Segment', many: true, dependsOn: {page: 'Home'}},
    carouselSegments: {type: Types.Relationship, ref: 'Segment', many: true, dependsOn: {page: 'Home'}},
    videoUrl: {type: Types.Url, note: 'This will be the video URL for the home page.', dependsOn: {page: 'Home'}},
    videoTitle: {type: Types.Text, note: 'This will be the title under the video at the top of the page.', dependsOn: {page: 'Home'}},
    videoText: {type: Types.Text, note: 'This will be the small text under the video at the top of the page.', dependsOn: {page: 'Home'}},
    videoImage: {type: Types.Relationship, ref: 'Image', note: 'This image will show on mobile intead of the video.', dependsOn: {page: 'Home'}}
  },
  facilities:{
    introduction:{type: Types.Html, wysiwyg: true, dependsOn: {page: 'Facilities'}},
    bannerImage: {type: Types.Relationship, ref: 'Image', note: 'The image which appears at the top of the page.', dependsOn: {page: 'Facilities'}},
    bannerText: {type: Types.Text, note: 'This text will appear overlaying the banner image.', dependsOn: {page: 'Facilities'} }
  },
  team:{
    introduction:{type: Types.Html, wysiwyg: true, dependsOn: {page: 'Team'}},
    bannerImage: {type: Types.Relationship, ref: 'Image', note: 'The image which appears at the top of the page.', dependsOn: {page: 'Team'}},
    bannerText: {type: Types.Text, note: 'This text will appear overlaying the banner image.', dependsOn: {page: 'Team'} }
  },
  help:{
    introduction:{type: Types.Html, wysiwyg: true, dependsOn: {page: 'Help'}},
    bannerImage: {type: Types.Relationship, ref: 'Image', note: 'The image which appears at the top of the page.', dependsOn: {page: 'Help'}},
    bannerText: {type: Types.Text, note: 'This text will appear overlaying the banner image.', dependsOn: {page: 'Help'}},
    faq: {
      thumbnailTitle: {type: Types.Text, note: 'This is the title which will appear on the FAQ thumbnail Text ', dependsOn:{page: 'Help'}},
      thumbnailText: {type: Types.Text, note: 'This is the text which will appear on the FAQ thumbnail Text ', dependsOn:{page: 'Help'}},
      thumbnailImage: {type: Types.Relationship, ref: 'Image', note: 'This image will be used for the thumnail linking to the FAQ page.', dependsOn:{page: 'Help'}},
    }
  },
  gallery: {
    introduction: {type: Types.Html, wysiwyg: true, dependsOn: {page: 'Gallery'}},
    bannerImage: {type: Types.Relationship, ref: 'Image', note: 'The image which appears at the top of the page.', dependsOn: {page: 'Gallery'}},
    bannerText: {type: Types.Text, note: 'This text will appear overlaying the banner image.', dependsOn: {page: 'Gallery'}},
  },
  faq: {
    introduction:{type: Types.Html, wysiwyg: true, dependsOn: {page: 'FAQ'}},
    bannerImage: {type: Types.Relationship, ref: 'Image', note: 'The image which appears at the top of the page.', dependsOn: {page: 'FAQ'}},
    bannerText: {type: Types.Text, note: 'This text will appear overlaying the banner image.', dependsOn: {page: 'FAQ'}},
  },
  about:{
    introduction:{type: Types.Html, wysiwyg: true, dependsOn: {page: 'About'}},
    bannerImage: {type: Types.Relationship, ref: 'Image', note: 'The image which appears at the top of the page.', dependsOn: {page: 'About'}},
    bannerText: {type: Types.Text, note: 'This text will appear overlaying the banner image.', dependsOn: {page: 'About'}},
  },
  contact:{
    introduction:{type: Types.Html, wysiwyg: true, dependsOn: {page: 'Contact'}},
    bannerImage: {type: Types.Relationship, ref: 'Image', note: 'The image which appears at the top of the page.', dependsOn: {page: 'Contact'}},
    bannerText: {type: Types.Text, note: 'This text will appear overlaying the banner image.', dependsOn: {page: 'Contact'}},
  },
  tour:{
    introduction:{type: Types.Html, wysiwyg: true, dependsOn: {page: 'Book a Tour'}},
    successText:{type: Types.Html, wysiwyg: true, dependsOn: {page: 'Book a Tour'}},
    bannerImage: {type: Types.Relationship, ref: 'Image', note: 'The image which appears at the top of the page.', dependsOn: {page: 'Book a Tour'}},
    bannerText: {type: Types.Text, note: 'This text will appear overlaying the banner image.', dependsOn: {page: 'Book a Tour'}},
  },
  meta: {type: Types.Relationship, ref:'Meta'},
});

SpecialPage.defaultColumns = 'title, page, active';
SpecialPage.register();

Route Controller for Home Page

view.on('init', function(next) {
        keystone.list('SpecialPage').model.findOne()
        .where('page', 'Home')
        .where('active', true)
        .populate('meta')
        .deepPopulate(deepPaths, {
            populate:{
                'home.testimonies': {
                    options:{
                        sort: 'sortOrder'
                    }
                },
                'home.segments': {
                    options:{
                        sort: 'sortOrder'
                    }
                }
            }
        })
        .exec(function(err, page){
            if(err){
             console.log(err); 
             return next(err);
            } else {
                locals.data.page = page;
                next(err);
            }
        });
    });

The reason I ended up allowing people to make multiple Special Pages rather than just having one to cover all of these pages around the site is that it allows the end user to create several 'Home' pages and then switch between them with the 'active' variable in the model. I'd recommend having a pre-save hook checking to make sure any of the same type aren't active to ensure you only have 1 home page active at a time.

@snowkeeper

This comment has been minimized.

Copy link
Member

commented Jan 7, 2016

We recently updated dependsOn to accept mongoose style expression operators. I have not updated the docs yet. For more info you can checkout the README for expression-match. It has a Keystone example collection.

@charleslouis

This comment has been minimized.

Copy link

commented Jan 7, 2016

Thanks @Mentioum and @snowkeeper !

@Mentioum

This comment has been minimized.

Copy link

commented Jan 7, 2016

Ah good to know @snowkeeper !

@mxstbr mxstbr removed the new feature label Apr 29, 2016

@mxstbr

This comment has been minimized.

Copy link
Member

commented Apr 29, 2016

We're closing all feature requests to keep the issue tracker unpolluted. From now on submit feature requests on productpains.com!

@mxstbr mxstbr closed this Apr 29, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.