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

Hide draft posts from /build_production, but include in /build_local #221

Closed
dirkolbrich opened this issue May 29, 2018 · 13 comments
Closed

Comments

@dirkolbrich
Copy link

Is there a way to hide/exclude a .md file/post from the posts collection when compiling to build_production, but have it included in build_local?

The YAML of the post.md file would be:

---
extends: _layouts.post
title: post title
author: me
status: draft  // <-- this would change to status: published when ready, could also be draft: true/false
date: "2018-05-29"
---

lots of text.

I'm struggling with the collections helper methods. Thanks for the help.

[Proposal] Maybe add a "How to" section to the docs?

@damiani
Copy link
Contributor

damiani commented May 30, 2018

Hmm, there's not a built-in way to specifically exclude a collection item from a particular environment. One possible approach, which would involve a change to Jigsaw, would be extending the events feature (#189) to add a new beforeFile event that is fired before each file is processed...this would allow the user to inspect the file variables and reject draft files in production. Feel free to open a PR, or leave this issue open and I'll work on coding it up soon.

@cossssmin
Copy link

cossssmin commented Jun 3, 2018

If a collection is being used here, wouldn't it be possible to remove the collection items in the afterCollections event? I imagine one could ->map() over it and reject any items that have a status key with a value of draft, based on the environment?

Just an idea - haven't tested it - but since afterCollections is fired before files are built, this should work.

@dirkolbrich
Copy link
Author

@hellocosmin thanks, this seems to be the right direction. So far I've got in bootstrap.php:

$events->afterCollections(function ($jigsaw) {
    // are we in production?
    if ($jigsaw->getConfig("production") == false) {
        return;
    }
    // removing draft posts from the posts collection
    $publicPosts = $jigsaw->getCollection("posts")->filter(function ($post) {
        return $post->draft == false && $post->status !== "draft";
    });

    $jigsaw->setCollection("posts", $publicPosts);
});

The $publicPosts collection includes only the published post. As intended.

But the last line is not working, due to PHP Fatal error: Uncaught Error: Call to undefined method TightenCo\Jigsaw\Jigsaw::setCollection(). There does not seem to be a public helper method to alter or replace the collection within the $events loop.

@damiani maybe a new feature? ;-)

Another idea would be to transform the collection directly with transform() :

 $jigsaw->getCollection("posts")->transform(function ($post) {
     if ( $post->draft == false && $post->status !== "draft") {
         unset($post);
     }
});

But the specific $post item is not unset, when inspecting the resulting collection. Any ideas?

@AlanHolmes
Copy link

Just been testing this, cause I need something similar, and this appears to work:

$events->afterCollections(function ($jigsaw) {
    if ($jigsaw->getConfig("production") == false) {
        return;
    }

    $jigsaw->getCollection('posts')->each(function ($post, $key) use ($jigsaw) {
        if ($post->publish !== true) {
            $jigsaw->getCollection('blog')->forget($key);
        }
    });
});

Tested on my site, and without this, the posts collection has 7 entires, and with it, it has 6 entires (I have one post that is set to publish = false

However, this does seem to generate this message (strangely not every time, but most)

Notice: Trying to get property of non-object in vendor/tightenco/jigsaw/src/Handlers/MarkdownHandler.php on line 4

Though it doesn't appear to have any effect on the output, still needs looking into further.

@dirkolbrich
Copy link
Author

Cool, your solution works in altering the collection. Thank you.

$events->afterCollections(function ($jigsaw) {
    // are we in production?
    if ($jigsaw->getConfig("production") == false) {
        return;
    }

    // removing draft posts from the posts collection
    $jigsaw->getCollection('posts')->each(function ($post, $key) use ($jigsaw) {
        if ($post->draft == true or $post->status == "draft") {
            $jigsaw->getCollection('posts')->forget($key);
        }
    });
});

But that gives me the same notice too:
PHP Notice: Trying to get property 'extends' of non-object in {my-projekt}/vendor/tightenco/jigsaw/src/Handlers/MarkdownHandler.php on line 45

I think, altering the collection after it is build is a dead end, as the removal of a single item does not alter the reference from the remaining items to the removed items. This only creates links to non existing items.

I think @damiani's approach is more feasible as to not consider rejected files when building the collection.

@damiani
Copy link
Contributor

damiani commented Jun 4, 2018

Turns out this is pretty complicated using events, for exactly the reasons you've run into—for collection items, drafts would need to be removed from both the collection item array and the built files; those are processed at different times during a build, so a single beforeFile event wouldn't be able to affect the collection item in both places.

I'm wondering now if there's a better way to solve this need. Similar to what Jekyll does, we could allow users to store draft collection items in a _drafts directory within the collection's directory:

├── _posts
|   ├── first-post.md
|   ├── second-post.md
|   └── _drafts
|       ├── draft-post.md
|       └── other-draft-post.md

...then, we could add a --drafts option to the jigsaw build command:

jigsaw build --drafts

If --drafts is specified, Jigsaw can build those files as if they were normal collection items; otherwise, those files will get ignored automatically.

@dirkolbrich
Copy link
Author

Agreed, the more I think about it, this makes the most sense from the user side.

  • clear folder structure with draft posts vs. a gazillion files where you don't know, which one is set to draft status - just move a file and it's public (could even be automated by date for scheduled posts?)
  • this folder floats on top of your posts directory - easy visible
  • clear switch between local and production build
  • could be set via a config.php and config.production.php with a drafts => true/false; option
  • the heck, could even be set by collection:
'collections' => [
        'people' => [
            'drafts' => true,
        ],
        'posts' => [
            'drafts' => false,
        ],
    ],

@cossssmin
Copy link

+1 for the solution proposed by @damiani.

My only issue with this is that, just like with --pretty=false, you have no control over it when running one of the npm scripts, as the command to jigsaw build for these sits in tasks/build.js.

I've had to do a workaround for this in all my Jigsaw projects. If you ever need to send args to jigsaw build in that JS file, here's my approach.

Create a PHP script that gets the merged config, based on build environment:

// tasks/php/config

#!/usr/bin/env php
<?php

/*
|--------------------------------------------------------------------------
| Site Config
|--------------------------------------------------------------------------
|
| Return the environment-specific site configuration as JSON.
| This is called in `tasks/build.js`, to store the config in a var
| allowing us to pass arguments otherwise unavailable
| in AfterWebpack(), to `jigsaw build`.
|
*/

$env = getopt('e:');
$config_dev = require_once('config.php');

if($env) {

    $file = 'config.' . $env['e'] . '.php';

    if(file_exists($file)) {

        $config_env = require_once($file);
        $out = array_merge($config_dev, $config_env);

        print json_encode($out);
        exit();

    }

}

print json_encode($config_dev);

exit();

Get that config in tasks/build.js:

// tasks/build.js

let execSync = require('child_process').execSync;

let config = JSON.parse(execSync('php ./tasks/php/config -e' + env));

// We now have the (merged) config in that variable, so we can do this for drafts and pretty URLs:

jigsaw: new AfterWebpack(() => {
    
    let prettyURLs = config.pretty == false ? ' --pretty=false ' : '';
    let buildDrafts = config.buildDrafts == true ? ' --drafts=true ' : '';

    command.get(bin.path() + ' build ' + env + prettyURLs + buildDrafts, (error, stdout, stderr) => {
        ...
    });
});

Of course, the above assumes you have a 'buildDrafts' key in one of your configs.

Hope this helps - looking forward to this feature :)

@dirkolbrich dirkolbrich changed the title [Question] Hide draft posts from /build_production, but include in /build_local [Feature] Hide draft posts from /build_production, but include in /build_local Jun 9, 2018
@dirkolbrich dirkolbrich changed the title [Feature] Hide draft posts from /build_production, but include in /build_local [Feature Request] Hide draft posts from /build_production, but include in /build_local Jun 9, 2018
@damiani damiani changed the title [Feature Request] Hide draft posts from /build_production, but include in /build_local Hide draft posts from /build_production, but include in /build_local Oct 2, 2018
@rumansaleem
Copy link

I'd be happy to take up this issue and submit a PR. But, I am new to open source, it would be helpful if someone could point me in the right direction. Thanks 🙂

@camuthig
Copy link
Contributor

I am also looking for some ability to have drafts within a blog. As noted in #285 the idea of "drafts" is very specific to blogs, so not necessarily something that belongs in the core of Jigsaw. I prefer keeping my drafts in the same folder as the rest of my content and using the date and a draft key in each file to determine if they should be published, though. With that in mind my thought is that it could work out well if there was some "filter" functionality built into the collections instead. I did a little testing of this locally and came to the conclusion that a couple of things would be needed.

  1. Defining a filter key in the collection in config.php, much like you would define sort . This would be a callable that accepts a item and returns a boolean.
  2. Updating Collection::loadItems to apply that filter to the items found
  3. Updating the CollectionItemHandler::handle to verify that $pageData->page is not null after calling $pageData->setPageVariableToCollectionItem before calling the underlying handle function. If it is null, the function would return immediately without processing the file

The first two parts ensure the item is not added to the site data for the collection. The third item keeps the Trying to get property 'extends' of non-object in ... warning from showing, since the site builder currently processes all files in a collection, regardless of if they have page data or not. It might be nice to still optionally log that the given page has no data, in case someone is building a listener that is causing errors, though.

@damiani
Copy link
Contributor

damiani commented Oct 3, 2019

@camuthig I like this solution, would you PR it?

@camuthig
Copy link
Contributor

camuthig commented Oct 8, 2019

Sorry, @damiani , I pushed that PR but now realize that I don't know if GitHub would alert you of it or not. #379 should lay the groundwork for applying a filter to collections.

@damiani
Copy link
Contributor

damiani commented Oct 11, 2019

Thanks for the great solution!

@damiani damiani closed this as completed Oct 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants