keystonejs / keystone-classic

Node.js CMS and web app framework
http://v4.keystonejs.com
MIT License
14.63k stars 2.21k forks source link

Content Management #67

Closed JedWatson closed 8 years ago

JedWatson commented 11 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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 commented 10 years ago

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! :smile: )

alexzaporozhets commented 10 years ago

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

talon commented 10 years ago

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 commented 10 years ago

@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:


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 commented 10 years ago

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

spbarber commented 10 years ago

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

talon commented 10 years ago

Update on this please!

ignlg commented 10 years ago

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 commented 10 years ago

@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 commented 10 years ago

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

frederikprijck commented 10 years ago

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 https://github.com/keystonejs/keystone/issues/322 , However there's no estimated timing their aswell.

marekgoczol commented 9 years ago

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

jack-guy commented 9 years ago

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

itzaks commented 9 years ago

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 commented 9 years ago

:+1:

creynders commented 9 years ago

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 commented 9 years ago

@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 commented 9 years ago

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

rclark72 commented 9 years ago

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 commented 9 years ago

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 :grin:

Mentioum commented 9 years ago

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 commented 8 years ago

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 :

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

charleslouis commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

Yes indeed @ignlg !! Thank you very much !

Mentioum commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

Thanks @Mentioum and @snowkeeper !

Mentioum commented 8 years ago

Ah good to know @snowkeeper !

mxstbr commented 8 years ago

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