From 106b2faaf1957409d761df77afd7602b0ae6e3b4 Mon Sep 17 00:00:00 2001 From: Chi Vinh Le Date: Wed, 14 Mar 2018 14:25:00 +0100 Subject: [PATCH 01/10] Extract css --- package.json | 1 + views/admin.ejs | 1 + views/embed/stream.ejs | 1 + views/login.ejs | 1 + webpack.config.js | 16 +++++++++++----- yarn.lock | 11 ++++++++++- 6 files changed, 25 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 924df52d6..29becde3e 100644 --- a/package.json +++ b/package.json @@ -213,6 +213,7 @@ "enzyme-adapter-react-15": "^1.0.0", "eslint": "^4.5.0", "eslint-plugin-mocha": "^4.11.0", + "extract-text-webpack-plugin": "^3.0.2", "husky": "^0.14.3", "identity-obj-proxy": "^3.0.0", "ip": "^1.1.5", diff --git a/views/admin.ejs b/views/admin.ejs index 939da0dcf..36356f797 100644 --- a/views/admin.ejs +++ b/views/admin.ejs @@ -5,6 +5,7 @@ Talk - Coral Admin + + + + + + <%- include partials/head %> diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs index 50faaf5c2..0ca249176 100644 --- a/views/embed/stream.ejs +++ b/views/embed/stream.ejs @@ -2,6 +2,8 @@ + + <%- include ../partials/head %> diff --git a/views/login.ejs b/views/login.ejs index 9ae282f65..7af820a9c 100644 --- a/views/login.ejs +++ b/views/login.ejs @@ -2,6 +2,8 @@ + + <%- include partials/head %> diff --git a/views/partials/head.ejs b/views/partials/head.ejs index 555f5689e..e6cdb97d2 100644 --- a/views/partials/head.ejs +++ b/views/partials/head.ejs @@ -12,11 +12,9 @@ - - <%_ if (locals.customCssUrl) { _%> <%_ } _%> <%- include data %> - \ No newline at end of file + From c27def3167bfdd5dbc8bb9a3d5ddde5b3cd47198 Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Thu, 15 Mar 2018 12:46:02 -0600 Subject: [PATCH 09/10] Asset Manager Tutorial --- docs/_config.yml | 2 + docs/source/integrating/asset-management.md | 314 ++++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 docs/source/integrating/asset-management.md diff --git a/docs/_config.yml b/docs/_config.yml index fe7146988..2a6fa2725 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -100,6 +100,8 @@ sidebar: children: - title: Authentication url: /integrating/authentication/ + - title: Asset Managment + url: /integrating/asset-management/ - title: Configuring the Comment Stream url: /integrating/configuring-comment-stream/ - title: Configuring the Admin diff --git a/docs/source/integrating/asset-management.md b/docs/source/integrating/asset-management.md new file mode 100644 index 000000000..a4888f4c5 --- /dev/null +++ b/docs/source/integrating/asset-management.md @@ -0,0 +1,314 @@ +--- +title: Asset Management +permalink: /integrating/asset-management/ +--- + +One of the most frequent questions that we get asked by organizations trying to +integrate Talk is: _How do we hook our CMS up to Talk so that articles are in +sync?_ + +This guide is designed to explain the steps to take your base installation of +Talk and configure it to allow only assets pushed into it from your CMS, and +keep your URL/title in sync. We won't cover here how to install the plugin, as +it is covered in our [Plugins Overview](/talk/plugins/). + +## Why do we need to create a plugin? + +By default, Talk will use "Lazy Asset Creation" to dynamically generate Assets +in Talk in order to make it easier for lighter installations. In order to have +more strict control over this flow, we will create a plugin that will: + +1. Disable "Lazy Asset Creation" by [Overriding a Resolver](#Overriding-a-Resolver). +2. Create Assets from our CMS by [Creating a New Asset Route](#Creating-a-New-Asset-Route). +3. Facilitate updates from our CMS to keep Talk in sync by [Creating an Asset Update Route](#Creating-an-Asset-Update-Route). + +We will then modify our embed so that we can [Target the Asset](#Target-the-Asset). + +But first we should grab our basic plugin structure: + +```sh +# clone our example repo (that comes with all the code below!) +git clone https://github.com/coralproject/talk-plugin-asset-manager-example.git + +# checkout the step-1 tag that starts us off with the basic file structure of +# the plugin. +git checkout step-1 +``` + +## Overriding a Resolver + +First we'll replace the content of the `resolver.js` file with the following: + +```js +// We'll need to modify the behavior of how assets are +// "resolved" in Talk, so we override the base asset resolver +// for the RootQuery type. +module.exports = { + RootQuery: { + asset: async (root, args, ctx) => { + // We'll grab the id of the asset being requested + // such that we'll be able to lookup the asset. + const { id } = args; + if (!id) { + // If the ID isn't provided, we don't want to do + // anything. + return null; + } + + // A mouthful for sure, but we need to use the loader + // that is available on the graph context in order to + // lookup the asset by ID. + const asset = await ctx.loaders.Assets.getByID.load(id); + if (!asset) { + // If the asset can't be found, we don't want to do + // anything. + return null; + } + + // Send the asset back. + return asset; + }, + }, +}; +``` + +This serves to override the default asset resolver. You can of course, override +any other field in the schema to perform whatever action your business needs +require, including adding additional resolvers! You can refer to our +[GraphQL API Docs](/talk/reference/graphql/) to see what other fields you can +override. + +Without this, Talk will continue to use the "Lazy Asset Creation" to handle +resolving the `asset` edge, which is what we want to change. + +_Note, you can also get to this point by running `git checkout step-2`!_ + +## Creating a New Asset Route + +In order to create Assets now, we have to get our CMS to push those into Talk, +the easiest way to do this is by creating a custom route. We won't cover +specific CMS integrations, but will assume that there is some type of webhook +system you are able to utilize that will trigger when a new article is created. + +We'll replace the contents of the `router.js` file with the following: + +```js +// This file we'll create routes that will facilitate asset creation and +// updates. + +const authz = require('middleware/authorization'); + +module.exports = router => { + // We'll respond to a POST request on the following route where the request + // must have a valid ADMIN access token. + router.post( + '/api/v1/plugin/asset-manager-example', + authz.needed('ADMIN'), + async (req, res, next) => { + // Get the graph context from the request. + const { context } = req; + + // Grab from the graph context, the AssetModel that we can use to create + // the new Asset. Lots of object destructuring here, but this lets us keep + // the important business logic cleaner. + const { connectors: { models: { Assets } } } = context; + + try { + // Now we can create the asset that was passed to us in the body of the + // request as JSON. Check the schema of the Asset model by looking at: + // https://github.com/coralproject/talk/blob/master/models/asset.js + await Assets.create(req.body); + + // Let your webhook callback know we got it! + return res.status(204).end(); + } catch (err) { + return next(err); + } + } + ); +}; +``` + +This request handler when mounted on Talk will allow your CMS to send a POST +request to `${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example` with the +Asset as a JSON payload. In order to protect the endpoint from abuse, we add the +authorization middleware. This middleware essentially says, _you must be an +admin to hit this route_. We need to generate a token that can be used by your +CMS using the Talk cli tool: + +```sh +# find or create an admin user that can be used as the basis for the token +./bin/cli users list + +# create a token for the user with the given id +./bin/cli token create ${USER_ID} cms-token +``` + +You can attach the generated token to the request a few ways: + +1. HTTP Header: + + curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example \ + -XPOST \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + --data "${ASSET_JSON}" + +2. Query Parameter: + + curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example?access_token=${TOKEN} + -XPOST \ + -H "Content-Type: application/json" \ + --data "${ASSET_JSON}" + +Where `${ASSET_JSON}` is the JSON for your Asset matching the +[AssetSchema](https://github.com/coralproject/talk/blob/master/models/asset.js). + +_Note, you can also get to this point by running `git checkout step-3`!_ + +## Creating an Asset Update Route + +Now imagine the situation where you decide that you want to change the url slug +of the page, or update the title, now Talk is out of sync! Let's fix that. + +Update your `router.js` to the following: + +```js +// This file we'll create routes that will facilitate asset creation and +// updates. + +const authz = require('middleware/authorization'); + +module.exports = router => { + // We'll respond to a POST request on the following route where the request + // must have a valid ADMIN access token. + router.post( + '/api/v1/plugin/asset-manager-example', + authz.needed('ADMIN'), + async (req, res, next) => { + // Get the graph context from the request. + const { context } = req; + + // Grab from the graph context, the AssetModel that we can use to create + // the new Asset. Lots of object destructuring here, but this lets us keep + // the important business logic cleaner. + const { connectors: { models: { Assets } } } = context; + + try { + // Now we can create the asset that was passed to us in the body of the + // request as JSON. Check the schema of the Asset model by looking at: + // https://github.com/coralproject/talk/blob/master/models/asset.js + await Assets.create(req.body); + + // Let your webhook callback know we got it! + return res.status(204).end(); + } catch (err) { + return next(err); + } + } + ); + + // We'll respond to a PUT request on the following route where the request + // must also have a valid ADMIN access token. + router.put( + '/api/v1/plugin/asset-manager-example/:id', + authz.needed('ADMIN'), + async (req, res, next) => { + // Get the graph context from the request. + const { context } = req; + + // Grab from the graph context, the AssetModel that we can use to update + // the Asset. Lots of object destructuring here, but this lets us keep + // the important business logic cleaner. + const { connectors: { models: { Assets } } } = context; + + try { + // Now we can lookup the asset we're updating and apply out updates to + // the model atomically. + const asset = await Assets.findOneAndUpdate( + { id: req.params.id }, + req.body, + { + // We want to validate the model being updated. + runValidators: true, + } + ); + if (!asset) { + // The asset indicated by the ID wasn't found, let the webhook know! + return res.status(404).end(); + } + + // Let your webhook callback know we got it! + return res.status(204).end(); + } catch (err) { + return next(err); + } + } + ); +}; +``` + +As you can see from the previous step of [Creating a New Asset Route](#Creating-a-New-Asset-Route) +, we have added the new `PUT` route to the router. This is a simple addition +that allows your CMS to call into Talk when the asset has updated it's title, +it's url (or really anything in the [AssetSchema](https://github.com/coralproject/talk/blob/master/models/asset.js)) to keep the Talk Admin and links up to date. + +Following the previous example, you can issue the request as follows: + +```sh +curl ${TALK_ROOT_URL}/api/v1/plugin/asset-manager-example/${ASSET_ID} \ + -XPUT \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + --data "${ASSET_JSON}" +``` + +The difference from the previous curl example, is that this one changes the +method from a `POST` to a `PUT`, and we add the `${ASSET_ID}` to the end of the +url. + +_Note, you can also get to this point by running `git checkout step-4`!_ + +## Target the Asset + +Now that we have a way to create and update Assets, we now need a way to +reference it. One of the most important fields in the Asset model, is the `id`. +This `id` can be one generated from your CMS, or some other system, but must +be kept consistent. + +When you install Talk, and visit the admin panel, we can see under +`/admin/configure` in the tab for Tech Settings, an embed snippet: + +```html +
+ +``` + +We'll modify this to the following: + +```html +
+ +``` + +Adding the `asset_id` parameter to the render function will accomplish a very +important task. It will provide Talk with the specific ID of the asset to +associate with the displayed page. This is important because even if you update +the URL in the future, the embed will still reference the correct Asset. The +`${ASSET_ID}` should be replaced by your CMS with the correct Asset id using +your desired scripting/templating tools. + +At this point, you should have a fully built Talk plugin that can be paired with +some work on your CMS to create a fully integrated asset management pipeline! + +To view the fully completed source code, visit +https://github.com/coralproject/talk-plugin-asset-manager-example. \ No newline at end of file From 58c5026fb7359fcec660e5f3d68c99a78ed06bcd Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Mon, 19 Mar 2018 13:00:41 -0600 Subject: [PATCH 10/10] some minor fixes --- middleware/staticTemplate.js | 47 +++++++++++++++++++++--------------- webpack.config.js | 13 ++++++++-- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/middleware/staticTemplate.js b/middleware/staticTemplate.js index 1d79f0011..b7d01feb4 100644 --- a/middleware/staticTemplate.js +++ b/middleware/staticTemplate.js @@ -1,8 +1,7 @@ const SettingsService = require('../services/settings'); const fs = require('fs'); const path = require('path'); -const merge = require('lodash/merge'); -const memoize = require('lodash/memoize'); +const { merge } = require('lodash'); const { BASE_URL, @@ -38,36 +37,44 @@ const attachStaticLocals = locals => { } }; +// MANIFESTS are all the manifests accessible by Talk. +const MANIFESTS = ['../dist/manifest.json', '../dist/manifest.embed.json']; + +// getManifest will retrieve the manifest files and parse the JSON. function getManifest() { return merge( {}, - ...['../dist/manifest.json', '../dist/manifest.embed.json'] - .map(f => fs.readFileSync(path.resolve(__dirname, f), 'utf8')) - .map(JSON.parse) + ...MANIFESTS.map(f => + fs.readFileSync(path.resolve(__dirname, f), 'utf8') + ).map(JSON.parse) ); } -const getManifestMemoized = memoize(getManifest); - -if (process.env.NODE_ENV === 'production') { - // Crash early if file does not exists. - getManifestMemoized(); -} - -function resolve(key) { +/** + * resolve is a function that can be used in templates to resolve an asset from + * the manifest. In production, the manifest is cached. + */ +const resolve = (() => { if (process.env.NODE_ENV === 'production') { - return `${STATIC_URL}static/${getManifestMemoized()[key]}`; - } else { - // In dev mode, we are more forgiving and we always load the - // newest version of the manifest. + // In production, we should attempt to load the manifest early. + const manifest = getManifest(); + + return key => `${STATIC_URL}static/${manifest[key]}`; + } + + // In dev mode, we are more forgiving and we always load the + // newest version of the manifest. + return key => { try { - return `${STATIC_URL}static/${getManifest()[key]}`; + const manifest = getManifest(); + + return `${STATIC_URL}static/${manifest[key]}`; } catch (err) { console.warn(err); return ''; } - } -} + }; +})(); module.exports = async (req, res, next) => { try { diff --git a/webpack.config.js b/webpack.config.js index 0f2287fa9..184107491 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -93,7 +93,15 @@ const config = { use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: [ - 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', + { + loader: 'css-loader', + options: { + minimize: true, + modules: true, + importLoaders: 1, + localIdentName: '[name]__[local]___[hash:base64:5]', + }, + }, 'postcss-loader', ], }), @@ -104,7 +112,8 @@ const config = { test: /\.(jpg|png|gif|svg)$/, }, { - loader: 'url-loader?limit=100000', + loader: 'url-loader', + options: { limit: 100000 }, test: /\.woff$/, }, {