diff --git a/gatsby-config.js b/gatsby-config.js index 59cd2f0..2b15c1e 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -63,7 +63,7 @@ module.exports = { { resolve: 'gatsby-remark-images', options: { - maxWidth: 960, + maxWidth: 1280, }, }, 'gatsby-remark-autolink-headers', // must precede prismjs! diff --git a/gatsby-node.js b/gatsby-node.js index e1485e2..60379bf 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -27,7 +27,8 @@ exports.onCreateNode = ({ node, getNode, actions }) => { getNode, basePath, }).replace(directory, ''); - const pathname = `/${baseURI}/${relativeFilePath}`.replace(multiSlashRE, '/'); + const slug = node.frontmatter && node.frontmatter.slug; + const pathname = `/${baseURI}/${slug || relativeFilePath}`.replace(multiSlashRE, '/'); // Add a field to each markdown node to indicate its source instance. This // is used by the gatsby-remark-prefix-relative-links plugin. @@ -75,10 +76,28 @@ exports.createPages = async ({ graphql, actions }) => { if (pathname === '/' && sourceInstanceName !== 'content') return; const component = path.resolve(template); createPage({ - path: node.fields.pathname, + path: pathname, component, // Context is available in page queries as GraphQL variables. context: { pathname, sourceInstanceName, template }, }); }); }; + +exports.createSchemaCustomization = ({ actions }) => { + const { createTypes } = actions + const typeDefs = ` + type MarkdownRemark implements Node { + frontmatter: Frontmatter + } + type Frontmatter { + author: String + canonical: String + # Date is the only required (non-nullable) field. + date: Date! @dateformat + slug: String + title: String + } + ` + createTypes(typeDefs) +} diff --git a/lib/navigation.js b/lib/navigation.js index 384b23f..a51f253 100644 --- a/lib/navigation.js +++ b/lib/navigation.js @@ -146,11 +146,11 @@ const insertChildNode = (nav, child, indices) => { }; const createSourceNav = (source) => { - const { title, config, baseURI } = source; + const { title, config, baseURI, page = null } = source; let nav = { title, key: baseURI, - page: null, + page, children: [], }; const configYaml = loadConfigYaml(config); diff --git a/lib/sources.js b/lib/sources.js index e930b22..e6accba 100644 --- a/lib/sources.js +++ b/lib/sources.js @@ -1,4 +1,5 @@ const path = require('path'); +const { assoc, compose, evolve, has, ifElse } = require('ramda'); const validateSource = (source = {}) => { const { name, config, baseURI, remote, local } = source; @@ -20,20 +21,16 @@ const validateSource = (source = {}) => { return isValid; }; -const prepareSource = git => source => { - if (!source.remote) { - return { - ...source, - basePath: source.local, - }; - } - const { config, directory = '', name } = source; - return { - ...source, - basePath: path.resolve(git, name, directory), - config: path.resolve(git, name, config), - }; -}; +const prepareSource = git => (s) => ifElse( + has('remote'), + compose( + evolve({ + config: c => path.resolve(git, s.name, c), + }), + assoc('basePath', path.resolve(git, s.name, s.directory || '')), + ), + assoc('basePath', s.local), +)(s); const prepareSources = (sources, git) => { return sources.filter(validateSource).map(prepareSource(git)); diff --git a/package-lock.json b/package-lock.json index e5cc5ef..eca5f42 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,10 +16,10 @@ "gatsby-plugin-image": "^1.14.0", "gatsby-plugin-manifest": "^3.14.0", "gatsby-plugin-react-helmet": "^4.14.0", - "gatsby-plugin-sharp": "^3.14.0", + "gatsby-plugin-sharp": "^3.14.3", "gatsby-plugin-sitemap": "^4.10.0", "gatsby-remark-autolink-headers": "^4.11.0", - "gatsby-remark-images": "^4.0.0", + "gatsby-remark-images": "^4.2.0", "gatsby-remark-prismjs": "^5.11.0", "gatsby-source-filesystem": "^3.14.0", "gatsby-source-git": "^1.1.0", diff --git a/package.json b/package.json index 9c66a60..8c6693b 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,10 @@ "gatsby-plugin-image": "^1.14.0", "gatsby-plugin-manifest": "^3.14.0", "gatsby-plugin-react-helmet": "^4.14.0", - "gatsby-plugin-sharp": "^3.14.0", + "gatsby-plugin-sharp": "^3.14.3", "gatsby-plugin-sitemap": "^4.10.0", "gatsby-remark-autolink-headers": "^4.11.0", - "gatsby-remark-images": "^4.0.0", + "gatsby-remark-images": "^4.2.0", "gatsby-remark-prismjs": "^5.11.0", "gatsby-source-filesystem": "^3.14.0", "gatsby-source-git": "^1.1.0", diff --git a/site-data.js b/site-data.js index 273eb57..85f8729 100644 --- a/site-data.js +++ b/site-data.js @@ -6,6 +6,7 @@ * designated `directory`. The default is `DOCS_TEMPLATE`. */ const DOCS_TEMPLATE = './src/templates/docs.js'; +const BLOG_TEMPLATE = './src/templates/blog.js'; module.exports = { siteMetadata: { @@ -57,5 +58,18 @@ module.exports = { branch: '1.x', directory: 'docs/', }, + { + name: 'blog', + title: 'Blog', + baseURI: '/blog', + page: { + title: 'Blog', + pathname: '/blog', + }, + remote: 'https://github.com/farmOS/farmOS-community-blog.git', + branch: 'main', + directory: 'posts/', + template: BLOG_TEMPLATE, + }, ], }; diff --git a/src/components/seo.js b/src/components/seo.js index 4c54313..e9158f3 100644 --- a/src/components/seo.js +++ b/src/components/seo.js @@ -4,7 +4,10 @@ import { Helmet } from "react-helmet" import { useLocation } from "@reach/router" import { useStaticQuery, graphql } from "gatsby" -const SEO = ({ title, description, image, article }) => { +const SEO = (props) => { + const { + article, canonical, description, image, title, + } = props; const { pathname, origin } = useLocation() const { site } = useStaticQuery(query) @@ -56,6 +59,8 @@ const SEO = ({ title, description, image, article }) => { )} {seo.image && } + + {canonical && } ) } @@ -67,6 +72,7 @@ SEO.propTypes = { description: PropTypes.string, image: PropTypes.string, article: PropTypes.bool, + canonical: PropTypes.string, } SEO.defaultProps = { @@ -74,6 +80,7 @@ SEO.defaultProps = { description: null, image: null, article: false, + canonical: null, } const query = graphql` diff --git a/src/pages/blog.js b/src/pages/blog.js new file mode 100644 index 0000000..aa8df23 --- /dev/null +++ b/src/pages/blog.js @@ -0,0 +1,108 @@ +import * as React from 'react'; +import { Link } from 'gatsby-material-ui-components'; +import { makeStyles } from '@material-ui/core/styles'; +import { Box, Typography } from '@material-ui/core'; +import Seo from '../components/seo'; +import theme from '../theme'; +import { graphql } from 'gatsby'; + +const DEFAULT_AUTHOR = 'the farmOS Community'; +const DEFAULT_TITLE = 'farmOS Community Blog'; + +const useStyles = makeStyles({ + main: { + '& h1': { + fontWeight: 300, + fontSize: '3rem', + lineHeight: 1.3, + letterSpacing: '-.01em', + margin: '0 0 1.25rem', + }, + }, + post: { + color: 'inherit', + textDecoration: 'none', + '& div': { paddingBottom: '2rem' }, + '& h3': { + color: theme.palette.primary.light, + fontSize: '2rem', + lineHeight: 1.3, + }, + '& h5': { + fontSize: '1rem', + lineHeight: 1.3, + '& span': { + fontWeight: 700, + }, + }, + '& p': { + color: theme.palette.text.hint, + }, + '&:hover': { + textDecoration: 'none', + '& h3': { color: theme.palette.warning.main }, + }, + }, +}); + +const BlogIndex = ({ data: { allMarkdownRemark } }) => { + const classes = useStyles(); + const posts = allMarkdownRemark.edges.map(({ node }, i) => { + const { + excerpt, fields: { pathname }, frontmatter, headings, + } = node; + const { author, date, title } = frontmatter; + const h1 = headings.find(({ depth }) => depth === 1); + return ( + + + + {title || h1?.value || DEFAULT_TITLE} + + + {date} by {author || DEFAULT_AUTHOR} + + {excerpt} + + + ); + }); + return ( + <> + + + + Community Blog + + {posts} + + + ); +}; + +export const query = graphql`query BlogIndex { + allMarkdownRemark( + filter: { fields: { template: { eq: "./src/templates/blog.js" } } } + sort: {fields: frontmatter___date, order: DESC} + ) { + edges { + node { + frontmatter { + author + date(formatString: "MMMM DD, YYYY") + title + } + excerpt + fields { + pathname + } + headings { + value + depth + } + } + } + } +}`; + +export default BlogIndex diff --git a/src/templates/blog.js b/src/templates/blog.js new file mode 100644 index 0000000..ede0d15 --- /dev/null +++ b/src/templates/blog.js @@ -0,0 +1,90 @@ +import React, { useEffect, useState } from 'react'; +import { graphql } from 'gatsby'; +import { makeStyles } from '@material-ui/core/styles'; +import { Box, Typography } from '@material-ui/core'; +import 'prismjs/themes/prism.css'; +import theme from '../theme'; +import Markdown from '../components/markdown'; +import Seo from '../components/seo'; + +const DEFAULT_AUTHOR = 'the farmOS Community'; +const DEFAULT_TITLE = 'farmOS Community Blog'; + +const useStyles = makeStyles({ + heading: { + margin: '0 0 1.25rem', + }, + headline: { + fontWeight: 300, + fontSize: '3rem', + lineHeight: 1.3, + }, + byline: { + fontSize: '1rem', + lineHeight: 1.3, + color: theme.palette.text.secondary, + }, + dateline: { + fontWeight: 700, + }, +}); + +export default function BlogTemplate({ data }) { + const classes = useStyles(); + const { markdownRemark: { frontmatter = {}, headings, html: initHtml } } = data; + const { canonical, date } = frontmatter; + let { author, title } = frontmatter; + if (!author) author = DEFAULT_AUTHOR; + const h1 = headings.find(({ depth }) => depth === 1); + if (!title && h1) title = h1.value; + if (!title && !h1) title = DEFAULT_TITLE; + const [html, setHtml] = useState(initHtml); + useEffect(() => { + if (h1) { + const div = document.createElement('div'); + div.innerHTML = html; + const el = div.querySelector(`#${h1.id}`); + if (el) { + el.remove(); + setHtml(div.outerHTML); + } + } + }, [h1, html]); + + return ( + <> + + + + + {title} + + + {date} by {author} + + + + + + ); +}; + +export const query = graphql` + query($pathname: String!) { + markdownRemark(fields: { pathname: { eq: $pathname } }) { + frontmatter { + author + canonical + date(formatString: "MMMM DD, YYYY") + slug + title + } + html + headings { + id + value + depth + } + } + } +`;