kitajchuk / clutch Goto Github PK
View Code? Open in Web Editor NEWA Developer framework for Prismic.io static web apps.
License: Creative Commons Zero v1.0 Universal
A Developer framework for Prismic.io static web apps.
License: Creative Commons Zero v1.0 Universal
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.
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, '<' ).replace( /\>/g, '>' ) %>
</pre>
</div>
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;
}
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 );
}
});
};
This is a ProperJS/app
implementation:
@mixin state( $module, $state, $glue: "-" ) {
.is-#{$module}#{$glue}#{$state} & {
@content;
}
}
This is a ProperJS/app update.
This is will happen in ProperJS/app
// 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
}
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.
This includes new JSON model, updates to prismic adapter JS and updates to navi HTML 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" />
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}` );
});
});
}
};
Example: The way I've been doing Vimeo auth for Clutch apps.
const apiOptions = (core.config.api.token ? { accessToken: core.config.api.token } : null);
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.
I never use Contentful, so eff it ya...?
See info here: https://trello.com/c/27Zj3cHo/369-circle-ci-migration-to-v2
Replaces forever and nodemon.
It stops building webpack after awhile — possibly to do with the BrowserSync proxy?
(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.
// @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
}
In done() method add api: core.query.cache.api
Update this line to look like this:
const sassFontPath = (config.aws.cdnOn && config.env.production) ? `${config.aws.cdn}/fonts/` : "/fonts/";
This is will happen in ProperJS/app
Current hooks:
This is a ProperJS/app update...
Use prismic-dom in templates and ContextObject helpers.
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;
}
Create better third-party authorization workflow with middleware and an authorizations listing page.
Load site JSON on startup.
Listen to webhooks, update site JSON if its edited?
-i ./static -i ./template -i ./source
Move authorizations to /clutch/authorizations/
.
Source all templates from root of /template/clutch/
.
const linkResolver = function ( doc ) {
const type = (core.config.generate.mappings[ doc.type ] || doc.type);
return (type === "page") ? `/${doc.uid}/` : `/${type}/${doc.uid}/`;
};
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.