diff --git a/gatsby-node.js b/gatsby-node.js index 7688abe..92bd55f 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -1,3 +1,4 @@ +const _ = require('lodash') const path = require('path') const { createFilePath } = require('gatsby-source-filesystem') @@ -14,6 +15,7 @@ exports.createPages = ({ boundActionCreators, graphql }) => { slug } frontmatter { + tags templateKey } } @@ -26,10 +28,13 @@ exports.createPages = ({ boundActionCreators, graphql }) => { return Promise.reject(result.errors) } - result.data.allMarkdownRemark.edges.forEach(edge => { + const posts = result.data.allMarkdownRemark.edges + + posts.forEach(edge => { const id = edge.node.id createPage({ path: edge.node.fields.slug, + tags: edge.node.frontmatter.tags, component: path.resolve( `src/templates/${String(edge.node.frontmatter.templateKey)}.js` ), @@ -39,6 +44,30 @@ exports.createPages = ({ boundActionCreators, graphql }) => { }, }) }) + + // Tag pages: + let tags = [] + // Iterate through each post, putting all found tags into `tags` + posts.forEach(edge => { + if (_.get(edge, `node.frontmatter.tags`)) { + tags = tags.concat(edge.node.frontmatter.tags) + } + }) + // Eliminate duplicate tags + tags = _.uniq(tags) + + // Make tag pages + tags.forEach(tag => { + const tagPath = `/tags/${_.kebabCase(tag)}/` + + createPage({ + path: tagPath, + component: path.resolve(`src/templates/tags.js`), + context: { + tag, + }, + }) + }) }) } diff --git a/package.json b/package.json index 92fa01b..47e06cc 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "gatsby-starter-netlify-cms", "description": "Example Gatsby, and Netlify CMS project", - "version": "1.1.0", + "version": "1.1.1", "author": "Austin Green", "dependencies": { "bulma": "^0.6.0", @@ -15,6 +15,8 @@ "gatsby-source-filesystem": "^1.5.23", "gatsby-transformer-remark": "^1.7.33", "gatsby-transformer-sharp": "^1.6.21", + "lodash": "^4.17.5", + "lodash-webpack-plugin": "^0.11.4", "prop-types": "^15.6.0", "react": "^16.2.0", "react-helmet": "^5.2.0" diff --git a/src/cms/preview-templates/AboutPagePreview.js b/src/cms/preview-templates/AboutPagePreview.js index 71b9d32..413a6da 100644 --- a/src/cms/preview-templates/AboutPagePreview.js +++ b/src/cms/preview-templates/AboutPagePreview.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import { AboutPageTemplate } from '../../templates/about-page' const AboutPagePreview = ({ entry, widgetFor }) => ( @@ -8,4 +9,11 @@ const AboutPagePreview = ({ entry, widgetFor }) => ( /> ) +AboutPagePreview.propTypes = { + entry: PropTypes.shape({ + getIn: PropTypes.func, + }), + widgetFor: PropTypes.func, +} + export default AboutPagePreview diff --git a/src/cms/preview-templates/BlogPostPreview.js b/src/cms/preview-templates/BlogPostPreview.js index f3be58b..7677323 100644 --- a/src/cms/preview-templates/BlogPostPreview.js +++ b/src/cms/preview-templates/BlogPostPreview.js @@ -1,12 +1,21 @@ import React from 'react' +import PropTypes from 'prop-types' import { BlogPostTemplate } from '../../templates/blog-post' const BlogPostPreview = ({ entry, widgetFor }) => ( ) +BlogPostPreview.propTypes = { + entry: PropTypes.shape({ + getIn: PropTypes.func, + }), + widgetFor: PropTypes.func, +} + export default BlogPostPreview diff --git a/src/cms/preview-templates/ProductPagePreview.js b/src/cms/preview-templates/ProductPagePreview.js index 0173650..7970041 100644 --- a/src/cms/preview-templates/ProductPagePreview.js +++ b/src/cms/preview-templates/ProductPagePreview.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import { ProductPageTemplate } from '../../templates/product-page' const ProductPagePreview = ({ entry, getAsset }) => { @@ -45,4 +46,11 @@ const ProductPagePreview = ({ entry, getAsset }) => { ) } +ProductPagePreview.propTypes = { + entry: PropTypes.shape({ + getIn: PropTypes.func, + }), + getAsset: PropTypes.func, +} + export default ProductPagePreview diff --git a/src/components/Content.js b/src/components/Content.js index 24ee52e..289e571 100644 --- a/src/components/Content.js +++ b/src/components/Content.js @@ -1,8 +1,19 @@ import React from 'react' +import PropTypes from 'prop-types' -export default ({ content, className }) => ( -
{content}
-) export const HTMLContent = ({ content, className }) => (
) + +const Content = ({ content, className }) => ( +
{content}
+) + +Content.propTypes = { + content: PropTypes.string, + className: PropTypes.string, +} + +HTMLContent.propTypes = Content.propTypes + +export default Content diff --git a/src/components/Features.js b/src/components/Features.js index 1b0778c..15c19b2 100644 --- a/src/components/Features.js +++ b/src/components/Features.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' const FeatureGrid = ({ gridItems }) => (
@@ -15,4 +16,13 @@ const FeatureGrid = ({ gridItems }) => (
) +FeatureGrid.propTypes = { + gridItems: PropTypes.arrayOf( + PropTypes.shape({ + image: PropTypes.string, + text: PropTypes.string, + }) + ), +} + export default FeatureGrid diff --git a/src/components/Pricing.js b/src/components/Pricing.js index df49b26..6677a65 100644 --- a/src/components/Pricing.js +++ b/src/components/Pricing.js @@ -1,6 +1,7 @@ import React from 'react' +import PropTypes from 'prop-types' -export default ({ data }) => ( +const Pricing = ({ data }) => (
{data.map(price => (
@@ -24,3 +25,16 @@ export default ({ data }) => ( ))}
) + +Pricing.propTypes = { + data: PropTypes.arrayOf( + PropTypes.shape({ + plan: PropTypes.string, + price: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + description: PropTypes.string, + items: PropTypes.array, + }) + ), +} + +export default Pricing diff --git a/src/components/Testimonials.js b/src/components/Testimonials.js index 171303d..05b15c4 100644 --- a/src/components/Testimonials.js +++ b/src/components/Testimonials.js @@ -1,6 +1,7 @@ import React from 'react' +import PropTypes from 'prop-types' -export default ({ testimonials }) => ( +const Testimonials = ({ testimonials }) => (
{testimonials.map(testimonial => (
@@ -13,3 +14,14 @@ export default ({ testimonials }) => ( ))}
) + +Testimonials.propTypes = { + testimonials: PropTypes.arrayOf( + PropTypes.shape({ + quote: PropTypes.string, + author: PropTypes.string, + }) + ), +} + +export default Testimonials diff --git a/src/layouts/all.sass b/src/layouts/all.sass index 32e9e81..9e87d7b 100644 --- a/src/layouts/all.sass +++ b/src/layouts/all.sass @@ -7,6 +7,21 @@ $kaldi-red-invert: #fff $primary: $kaldi-red $primary-invert: $kaldi-red-invert +.content .taglist + list-style: none + margin-bottom: 0 + margin-left: 0 + margin-right: 1.5rem + margin-top: 1.5rem + display: flex + flex-wrap: wrap + justify-content: left + align-items: center + li + padding: 0 2rem 1rem 0 + margin-bottom: 1.5rem + margin-top: 0 + // Helper Classes .full-width-image-container width: 100vw diff --git a/src/pages/blog/2016-12-17-making-sense-of-the-scaas-new-flavor-wheel.md b/src/pages/blog/2016-12-17-making-sense-of-the-scaas-new-flavor-wheel.md index 5d3036a..ca4b004 100644 --- a/src/pages/blog/2016-12-17-making-sense-of-the-scaas-new-flavor-wheel.md +++ b/src/pages/blog/2016-12-17-making-sense-of-the-scaas-new-flavor-wheel.md @@ -3,6 +3,9 @@ templateKey: blog-post title: Making sense of the SCAA’s new Flavor Wheel date: 2016-12-17T15:04:10.000Z description: The Coffee Taster’s Flavor Wheel, the official resource used by coffee tasters, has been revised for the first time this year. +tags: + - flavor + - tasting --- ![flavor wheel](/img/flavor_wheel.jpg) diff --git a/src/pages/blog/2017-01-04-a-beginners-guide-to-brewing-with-chemex.md b/src/pages/blog/2017-01-04-a-beginners-guide-to-brewing-with-chemex.md index 8889c48..f06dd8a 100644 --- a/src/pages/blog/2017-01-04-a-beginners-guide-to-brewing-with-chemex.md +++ b/src/pages/blog/2017-01-04-a-beginners-guide-to-brewing-with-chemex.md @@ -3,6 +3,9 @@ templateKey: blog-post title: A beginners’ guide to brewing with Chemex date: 2017-01-04T15:04:10.000Z description: Brewing with a Chemex probably seems like a complicated, time-consuming ordeal, but once you get used to the process, it becomes a soothing ritual that's worth the effort every time. +tags: + - brewing + - chemex --- ![chemex](/img/chemex.jpg) diff --git a/src/pages/blog/2017-01-04-just-in-small-batch-of-jamaican-blue-mountain-in-store-next-week.md b/src/pages/blog/2017-01-04-just-in-small-batch-of-jamaican-blue-mountain-in-store-next-week.md index 9007f16..9f899b0 100644 --- a/src/pages/blog/2017-01-04-just-in-small-batch-of-jamaican-blue-mountain-in-store-next-week.md +++ b/src/pages/blog/2017-01-04-just-in-small-batch-of-jamaican-blue-mountain-in-store-next-week.md @@ -5,6 +5,11 @@ date: 2017-01-04T15:04:10.000Z description: >- We’re proud to announce that we’ll be offering a small batch of Jamaica Blue Mountain coffee beans in our store next week. +tags: + - jamaica + - green beans + - flavor + - tasting --- We expect the shipment of a limited quantity of green beans next Monday. We’ll be offering the roasted beans from Tuesday, but quantities are limited, so be quick. diff --git a/src/pages/index.js b/src/pages/index.js index f519327..512870e 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import Link from 'gatsby-link' export default class IndexPage extends React.Component { @@ -43,6 +44,14 @@ export default class IndexPage extends React.Component { } } +IndexPage.propTypes = { + data: PropTypes.shape({ + allMarkdownRemark: PropTypes.shape({ + edges: PropTypes.array, + }), + }), +} + export const pageQuery = graphql` query IndexQuery { allMarkdownRemark(sort: { order: DESC, fields: [frontmatter___date] }) { diff --git a/src/pages/tags/index.js b/src/pages/tags/index.js new file mode 100644 index 0000000..06580a7 --- /dev/null +++ b/src/pages/tags/index.js @@ -0,0 +1,49 @@ +import React from 'react' +import { kebabCase } from 'lodash' +import Helmet from 'react-helmet' +import Link from 'gatsby-link' + +const TagsPage = ({ + data: { allMarkdownRemark: { group }, site: { siteMetadata: { title } } }, +}) => ( +
+ +
+
+
+

Tags

+
    + {group.map(tag => ( +
  • + + {tag.fieldValue} ({tag.totalCount}) + +
  • + ))} +
+
+
+
+
+) + +export default TagsPage + +export const tagPageQuery = graphql` + query TagsQuery { + site { + siteMetadata { + title + } + } + allMarkdownRemark(limit: 1000) { + group(field: frontmatter___tags) { + fieldValue + totalCount + } + } + } +` diff --git a/src/templates/about-page.js b/src/templates/about-page.js index b36f822..a662a62 100644 --- a/src/templates/about-page.js +++ b/src/templates/about-page.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import Content, { HTMLContent } from '../components/Content' export const AboutPageTemplate = ({ title, content, contentComponent }) => { @@ -22,7 +23,13 @@ export const AboutPageTemplate = ({ title, content, contentComponent }) => { ) } -export default ({ data }) => { +AboutPageTemplate.propTypes = { + title: PropTypes.string.isRequired, + content: PropTypes.string, + contentComponent: PropTypes.func, +} + +const AboutPage = ({ data }) => { const { markdownRemark: post } = data return ( @@ -34,6 +41,12 @@ export default ({ data }) => { ) } +AboutPage.propTypes = { + data: PropTypes.object.isRequired, +} + +export default AboutPage + export const aboutPageQuery = graphql` query AboutPage($id: String!) { markdownRemark(id: { eq: $id }) { diff --git a/src/templates/blog-post.js b/src/templates/blog-post.js index f90af05..8437026 100644 --- a/src/templates/blog-post.js +++ b/src/templates/blog-post.js @@ -1,11 +1,15 @@ import React from 'react' +import PropTypes from 'prop-types' +import { kebabCase } from 'lodash' import Helmet from 'react-helmet' +import Link from 'gatsby-link' import Content, { HTMLContent } from '../components/Content' export const BlogPostTemplate = ({ content, contentComponent, description, + tags, title, helmet, }) => { @@ -22,6 +26,18 @@ export const BlogPostTemplate = ({

{description}

+ {tags && tags.length ? ( +
+

Tags

+
    + {tags.map(tag => ( +
  • + {tag} +
  • + ))} +
+
+ ) : null}
@@ -29,20 +45,37 @@ export const BlogPostTemplate = ({ ) } -export default props => { - const { markdownRemark: post } = props.data +BlogPostTemplate.propTypes = { + content: PropTypes.string.isRequired, + contentComponent: PropTypes.func, + description: PropTypes.string, + title: PropTypes.string, + helmet: PropTypes.instanceOf(Helmet), +} + +const BlogPost = ({ data }) => { + const { markdownRemark: post } = data return ( } + helmet={} + tags={post.frontmatter.tags} title={post.frontmatter.title} /> ) } +BlogPost.propTypes = { + data: PropTypes.shape({ + markdownRemark: PropTypes.object, + }), +} + +export default BlogPost + export const pageQuery = graphql` query BlogPostByID($id: String!) { markdownRemark(id: { eq: $id }) { @@ -52,6 +85,7 @@ export const pageQuery = graphql` date(formatString: "MMMM DD, YYYY") title description + tags } } } diff --git a/src/templates/product-page.js b/src/templates/product-page.js index 3e5ac9d..d556fbb 100644 --- a/src/templates/product-page.js +++ b/src/templates/product-page.js @@ -1,4 +1,5 @@ import React from 'react' +import PropTypes from 'prop-types' import Features from '../components/Features' import Testimonials from '../components/Testimonials' import Pricing from '../components/Pricing' @@ -104,7 +105,31 @@ export const ProductPageTemplate = ({ ) -export default ({ data }) => { +ProductPageTemplate.propTypes = { + image: PropTypes.string, + title: PropTypes.string, + heading: PropTypes.string, + description: PropTypes.string, + intro: PropTypes.shape({ + blurbs: PropTypes.array, + }), + main: PropTypes.shape({ + heading: PropTypes.string, + description: PropTypes.string, + image1: PropTypes.object, + image2: PropTypes.object, + image3: PropTypes.object, + }), + testimonials: PropTypes.array, + fullImage: PropTypes.string, + pricing: PropTypes.shape({ + heading: PropTypes.string, + description: PropTypes.string, + plans: PropTypes.array, + }), +} + +const ProductPage = ({ data }) => { const { frontmatter } = data.markdownRemark return ( @@ -122,6 +147,16 @@ export default ({ data }) => { ) } +ProductPage.propTypes = { + data: PropTypes.shape({ + markdownRemark: PropTypes.shape({ + frontmatter: PropTypes.object, + }), + }), +} + +export default ProductPage + export const productPageQuery = graphql` query ProductPage($id: String!) { markdownRemark(id: { eq: $id }) { diff --git a/src/templates/tags.js b/src/templates/tags.js new file mode 100644 index 0000000..0879a7b --- /dev/null +++ b/src/templates/tags.js @@ -0,0 +1,71 @@ +import React from 'react' +import Helmet from 'react-helmet' +import Link from 'gatsby-link' + +class TagRoute extends React.Component { + render() { + const posts = this.props.data.allMarkdownRemark.edges + const postLinks = posts.map(post => ( +
  • + +

    {post.node.frontmatter.title}

    + +
  • + )) + const tag = this.props.pathContext.tag + const title = this.props.data.site.siteMetadata.title + const totalCount = this.props.data.allMarkdownRemark.totalCount + const tagHeader = `${totalCount} post${ + totalCount === 1 ? '' : 's' + } tagged with “${tag}”` + + return ( +
    + +
    +
    +
    +

    {tagHeader}

    +
      {postLinks}
    +

    + Browse all tags +

    +
    +
    +
    +
    + ) + } +} + +export default TagRoute + +export const tagPageQuery = graphql` + query TagPage($tag: String) { + site { + siteMetadata { + title + } + } + allMarkdownRemark( + limit: 1000 + sort: { fields: [frontmatter___date], order: DESC } + filter: { frontmatter: { tags: { in: [$tag] } } } + ) { + totalCount + edges { + node { + fields { + slug + } + frontmatter { + title + } + } + } + } + } +` diff --git a/static/admin/config.yml b/static/admin/config.yml index 35d0a2b..020c0db 100644 --- a/static/admin/config.yml +++ b/static/admin/config.yml @@ -17,6 +17,7 @@ collections: - {label: "Publish Date", name: "date", widget: "datetime"} - {label: "Description", name: "description", widget: "text"} - {label: "Body", name: "body", widget: "markdown"} + - {label: "Tags", name: "tags", widget: "list"} - name: "pages" label: "Pages" diff --git a/yarn.lock b/yarn.lock index 16707c1..2ceb48f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5761,6 +5761,12 @@ lodash-id@^0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/lodash-id/-/lodash-id-0.14.0.tgz#baf48934e543a1b5d6346f8c84698b1a8c803896" +lodash-webpack-plugin@^0.11.4: + version "0.11.4" + resolved "https://registry.yarnpkg.com/lodash-webpack-plugin/-/lodash-webpack-plugin-0.11.4.tgz#6c3ecba3d4b8d24b53940b63542715c5ed3c4ac5" + dependencies: + lodash "^4.17.4" + lodash._basecopy@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" @@ -5965,7 +5971,7 @@ lodash@3.10.1: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@4, lodash@^4.0.0, lodash@^4.1.0, lodash@^4.12.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.4: +lodash@4, lodash@^4.0.0, lodash@^4.1.0, lodash@^4.12.0, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.6.1, lodash@~4.17.4: version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"