Coder Social home page Coder Social logo

kitajchuk / clutch Goto Github PK

View Code? Open in Web Editor NEW
6.0 1.0 0.0 2.71 MB

A Developer framework for Prismic.io static web apps.

License: Creative Commons Zero v1.0 Universal

JavaScript 61.37% HTML 5.70% SCSS 32.93%
babel eslint jest jsdocs mobx prismic react webpack

clutch's People

Contributors

kitajchuk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

clutch's Issues

Dynamic sitemap.xml generator

The current static generator works but doesn't account for the dynamic content. Migrating this static generator to a dynamic endpoint will ensure new documents created in the CMS get added to the sitemap.xml endpoint for services like Google Search Console.

Handle template render error with 500.html

Use this for the template:

<%
const error = locals.context.get( 'error' );
%>
<div class="cms -wrap -pad">
    <h2>Page Error</h2>
<pre class="-expt -spot">
<%- error.toString().replace( /\</g, '&lt;' ).replace( /\>/g, '&gt;' ) %>
</pre>
</div>

Update context: getPageTitle

getPageTitle () {
        const page = this.get( "page" );;
        const site = this.get( "site" );
        const navi = this.get( "navi" );
        const items = this.get( "items" );
        let item = this.get( "item" );
        let title = site.data.title;
        let navItem = null;

        if ( typeof title === "object" ) {
            title = prismicDOM.RichText.asText( title );
        }

        // Supports collection mapping to content-type
        if ( items ) {
            item = navi.items.find(( nav ) => {
                return (nav.uid === page);
            });

            if ( item ) {
                item.data = {
                    title: item.title
                };
            }

        } else if ( item && (typeof item.data.title === "object") ) {
            item.data.title = prismicDOM.RichText.asText( item.data.title );
        }

        title = (item ? `${item.data.title}${title}` : title);

        return title;
    }

Implement: Homepage query for when home is NOT is Site>Navigation

const getDataForPage = function ( req, listener ) {
    return new Promise(( resolve, reject ) => {
        const data = {
            item: null,
            items: null
        };
        const doQuery = function ( type, uid ) {
            let query = [];
            const navi = getNavi( type );
            const form = getForm( req, cache.api, type );
            const isHeadlessHome = (type === core.config.homepage);
            const isNaviNoForm = (navi && !cache.api.data.forms[ type ]);
            const done = function ( json ) {
                if ( !json.results.length ) {
                    // Static page with no CMS data attached to it...
                    if ( core.template.cache.pages.indexOf( `${type}.html` ) !== -1 ) {
                        resolve( data );

                    } else {
                        reject( `Prismic has no data for the content-type "${type}".` );
                    }

                } else {
                    // all
                    data.items = json.results;

                    // uid
                    if ( uid || isNaviNoForm || isHeadlessHome ) {
                        data.item = getDoc( (isHeadlessHome ? core.config.homepage : (isNaviNoForm ? navi.uid : uid)), json.results );

                        if ( !data.item ) {
                            reject( `The document with UID "${isNaviNoForm ? navi.uid : uid}" could not be found by Prismic.` );
                        }
                    }

                    resolve( data );
                }
            };
            const fail = function ( error ) {
                reject( error );
            };

            // query: type?
            if ( isHeadlessHome ) {
                query.push( prismic.Predicates.at( "document.type", "page" ) );
                query.push( prismic.Predicates.at( "my.page.uid", core.config.homepage ) );

            } else if ( isNaviNoForm ) {
                query.push( prismic.Predicates.at( "document.type", navi.type ) );
                query.push( prismic.Predicates.at( "document.id", navi.id ) );

            } else if ( !cache.api.data.forms[ type ] ) {
                // Only if type? is NOT a search form collection
                query.push( prismic.Predicates.at( "document.type", type ) );
            }

            // @hook: query
            if ( listener && listener.handlers.query ) {
                query = listener.handlers.query( prismic, cache.api, query, cache, req );
            }

            // query: promise?
            if ( query instanceof Promise ) {
                query.then( done ).catch( fail );

            } else {
                // query?
                if ( query.length ) {
                    form.query( query );
                }

                // @hook: orderings
                if ( listener && listener.handlers.orderings ) {
                    listener.handlers.orderings( prismic, cache.api, form, cache, req );
                }

                // @hook: fetchLinks
                if ( listener && listener.handlers.fetchLinks ) {
                    listener.handlers.fetchLinks( prismic, cache.api, form, cache, req );
                }

                // @hook: pagination
                if ( listener && listener.handlers.pagination ) {
                    listener.handlers.pagination( prismic, cache.api, form, cache, req );
                }

                // submit
                form.submit().then( done ).catch( fail );
            }
        };

        if ( !req.params.type ) {
            resolve( data );

        } else {
            doQuery( req.params.type, req.params.uid );
        }
    });
};

Update `state` mixin

This is a ProperJS/app implementation:

@mixin state( $module, $state, $glue: "-" ) {
    .is-#{$module}#{$glue}#{$state} & {
        @content;
    }
}

Add pagination to the context for api and pages

// Pagination information
            // console.log( json );
            data.pagination = {
                page: json.page,
                results_per_page: json.results_per_page,
                results_size: json.results_size,
                total_results_size: json.total_results_size,
                total_pages: json.total_pages,
                next_page: json.next_page,
                prev_page: json.prev_page
            }

Only source CDN for production environment

In conjunction with ONLY syncing S3 for master deployments, this will ensure that feature code for staging 1) always serves locally from the staging environment and is reliable for feature testing and 2) NEVER deploys to S3 and possibly overrides the static code deployed and serving to production.

Add DNS prefetch to layout template

Something like this:

<meta http-equiv="x-dns-prefetch-control" content="on" />
<link rel="dns-prefetch" href="//fpdl.vimeocdn.com" />
<link rel="dns-prefetch" href="//{project-repo}.cdn.prismic.io" />
<link rel="dns-prefetch" href="//prismic-io.s3.amazonaws.com" />
<link rel="dns-prefetch" href="//www.google-analytics.com" />

Implement: CSRF protection for POST handling

Require valid CSRF token on :POST requests
Expose CSRF token on each request to context for template render with consolidate

Use token in templates like this:

"use strict";



const express = require( "express" );
const expressApp = express();
const compression = require( "compression" );
const cookieParser = require( "cookie-parser" );
const bodyParser = require( "body-parser" );
const lager = require( "properjs-lager" );
const csurf = require( "csurf" );
const listeners = {};
const core = {
    query: require( "./query" ),
    config: require( "../../clutch.config" ),
    content: require( "./content" ),
    template: require( "./template" )
};
const ContextObject = require( "../class/ContextObject" );
const checkCSRF = csurf({
    cookie: true
});
let isSiteUpdate = false;



/**
 *
 * Configure Express Middleware.
 *
 */
expressApp.use( cookieParser() );
expressApp.use( bodyParser.json() );
expressApp.use( bodyParser.urlencoded({
    extended: true
}));
expressApp.use( compression( core.config.compression ) );
expressApp.use( express.static( core.config.template.staticDir, {
    maxAge: core.config.static.maxAge
}));



/**
 *
 * Configure Express Routes.
 *
 */
const setRoutes = () => {
    // SYSTEM
    expressApp.get( "/preview", getPreview );
    expressApp.post( "/webhook", postWebhook );
    expressApp.get( "/sitemap.xml", getSitemap );
    expressApp.get( "/authorizations", checkAuthToken, getAuthorizations );
    expressApp.get( "/authorizations/:app", checkAuthToken, getAuthorizationForApp );
    expressApp.get( "/taxonomy", checkCSRF, setReq, getTaxonomyPage );

    // AUTHORIZATIONS
    core.config.authorizations.apps.forEach(( app ) => {
        require( `../auth/${app}` ).init( expressApp, checkCSRF );
    });

    // API => JSON
    expressApp.get( "/api/:type", setReq, getApi );
    expressApp.get( "/api/:type/:uid", setReq, getApi );

    // URI => HTML
    expressApp.get( "/", checkCSRF, setReq, getPage );
    expressApp.get( "/:type", checkCSRF, setReq, getPage );
    expressApp.get( "/:type/:uid", checkCSRF, setReq, getPage );
};




/**
 *
 * Request handling.
 *
 */
const setReq = ( req, res, next ) => {
    req.params.type = req.params.type || core.config.homepage;

    next();
};
const getKey = ( type ) => {
    const key = type;

    return key || core.config.homepage;
};



/**
 *
 * :GET API
 *
 */
const getApi = ( req, res ) => {
    const key = getKey( req.params.type );

    core.query.getApi( req, res, listeners[ key ] ).then(( result ) => {
        if ( req.query.format === "html" ) {
            res.status( 200 ).send( result );

        } else {
            res.status( 200 ).json( result );
        }
    });
};



/**
 *
 * :GET Pages
 *
 */
const getPage = ( req, res ) => {
    const key = getKey( req.params.type );

    core.content.getPage( req, res, listeners[ key ] ).then(( callback ) => {
        // Handshake callback :-P
        callback(( status, html ) => {
            res.status( status ).send( html );
        });
    });
};



/**
 *
 * :GET  Prismic stuff
 * :POST Prismic stuff
 *
 */
const getPreview = ( req, res ) => {
    core.query.getPreview( req, res ).then(( url ) => {
        res.redirect( url );
    });
};
const postWebhook = ( req, res ) => {
    // Skip if update is in progress, Skip if invalid secret was sent
    if ( !isSiteUpdate && req.body.secret === core.config.api.secret ) {
        isSiteUpdate = true;

        // Re-Fetch Site JSON
        core.query.getSite().then(() => {
            isSiteUpdate = false;
        });
    }

    // Always resolve with a 200 and some text
    res.status( 200 ).send( "success" );
};
const getSitemap = ( req, res ) => {
    const sitemap = require( `../generators/${core.config.api.adapter}.sitemap` );

    sitemap.generate().then(( xml ) => {
        res.set( "Content-Type", "text/xml" ).status( 200 ).send( xml );
    });

};



/**
 *
 * :GET CSRF
 *
 */
const getCSRF = ( req, res ) => {
    res.status( 200 ).json({
        csrf: req.csrfToken()
    });
};



/**
 *
 * Middleware checks
 *
 */
const checkOrigin = ( req, res, next ) => {
    // No origin means not CORS :-)
    if ( !req.headers.origin ) {
        next();

    } else {
        res.status( 200 ).json({
            error: "Invalid origin for request"
        });
    }
};
const checkAuthToken = ( req, res, next ) => {
    if ( req.query.token === core.config.authorizations.token ) {
        next();

    } else {
        res.redirect( "/" );
    }
};



/**
 *
 * :GET Authorizations
 *
 */
const getAuthorizations = ( req, res ) => {
    req.params.type = "authorizations";
    core.content.getPage( req, res, listeners.authorizations ).then(( callback ) => {
        // Handshake callback :-P
        callback(( status, html ) => {
            res.status( status ).send( html );
        });
    });
};
const getAuthorizationForApp = ( req, res ) => {
    const app = core.config.authorizations.apps.find(( app ) => {
        return (app === req.params.app);
    });

    require( `../auth/${app}` ).auth( req, res );
};



/**
 *
 * :GET Taxonomies
 *
 */
const getTaxonomyPage = ( req, res ) => {
    req.params.type = "taxonomy";
    core.content.getPage( req, res, listeners.taxonomy ).then(( callback ) => {
        // Handshake callback :-P
        callback(( status, html ) => {
            res.status( status ).send( html );
        });
    });
};



/**
 *
 * Router API.
 *
 */
module.exports = {
    /**
     *
     * Handle router subscribe.
     *
     */
    on ( type, handlers ) {
        const key = getKey( type );

        // One handler per route
        if ( !listeners[ key ] ) {
            listeners[ key ] = {
                type: type,
                handlers: handlers
            };
        }
    },


    /**
     *
     * Start the Express {app}.
     *
     */
    init () {
        // Init routes
        setRoutes();

        // Fetch ./template/pages listing
        core.template.getPages().then(() => {
            // Fetch Site JSON
            core.query.getSite().then(() => {
                expressApp.listen( core.config.express.port );

                lager.server( `Clutch Express server started` );
                lager.server( `Clutch access URL — http://localhost:${core.config.browser.port}` );
            });
        });
    }
};

Only sync S3 for master deployments

By ensuring that sandbox and staging ALWAYS source static from the local web app we can ONLY sync S3 when merging new code into master. This will ensure that feature work on the dev branch will only operate on the staging environment and will NOT override the static code served to the production environment.

clutch-static

  • Sandbox development doesn't change at all.
  • Addition of static build/dist.
  • Refactor into pure dev server + static deploy on Netlify

Address loader-utils warning

(node:7763) DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic, see https://github.com/webpack/loader-utils/issues/56 parseQuery() will be replaced with getOptions() in the next major version of loader-utils.

Implement: Add pagination hook for router + pagination data for context

// @hook: pagination
if ( listener && listener.handlers.pagination ) {
    listener.handlers.pagination( prismic, cache.api, form, cache, req );
}

// Pagination information
data.pagination = {
    page: json.page,
    results_per_page: json.results_per_page,
    results_size: json.results_size,
    total_results_size: json.total_results_size,
    total_pages: json.total_pages,
    next_page: json.next_page,
    prev_page: json.prev_page
}

Update context: getUrl

same as preview link resolver

getUrl ( doc ) {
     const type = (config.generate.mappings[ doc.type ] || doc.type);
     const resolvedUrl = doc.uid === config.homepage ? "/" : ((type === "page") ? `/${doc.uid}/` : `/${type}/${doc.uid}/`);

    return resolvedUrl;
}

Authorizations

Create better third-party authorization workflow with middleware and an authorizations listing page.

Better prismic previews link resolver

const linkResolver = function ( doc ) {
    const type = (core.config.generate.mappings[ doc.type ] || doc.type);

    return (type === "page") ? `/${doc.uid}/` : `/${type}/${doc.uid}/`;
};

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.