1
0
mirror of https://github.com/kremalicious/blog.git synced 2024-06-25 18:56:25 +02:00

Merge pull request #63 from kremalicious/feature/related-posts

related posts based on tags & pagination
This commit is contained in:
Matthias Kretschmann 2018-09-29 21:35:47 +02:00 committed by GitHub
commit 0497764f2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 384 additions and 106 deletions

View File

@ -16,16 +16,38 @@
## Table of Contents
- [🎉 Features](#-features)
- [🎆 EXIF extraction](#-exif-extraction)
- [🕸 Related Posts](#-related-posts)
- [🏆 SEO component](#-seo-component)
- [📈 Matomo (formerly Piwik) analytics tracking](#-matomo-formerly-piwik-analytics-tracking)
- [gatsby-redirect-from](#-gatsby-redirect-from)
- [💎 Importing SVG assets](#-importing-svg-assets)
- [🍬 Typekit component](#-typekit-component)
- [✨ Development](#-development)
- [🔮 Linting](#-linting)
- [🎈 Add a new project](#-add-a-new-project)
- [🚚 Deployment](#-deployment)
- [🏛 Licenses](#-licenses)
- [Posts](#-posts)
- [Photos & images](#-photos-images)
---
## 🎉 Features
The whole [blog](https://kremalicious.com) is a React-based Single Page App built with [Gatsby v2](https://www.gatsbyjs.org).
### EXIF extraction
### 🎆 EXIF extraction
...
### 🕸 Related Posts
Under each post a list of related posts is displayed which are based on the tags of the currently viewed post. Also allows loading more related posts in place.
If you want to know how, have a look at the respective component under [`src/components/molecules/Pagination.jsx`](src/components/molecules/Pagination.jsx)
### 🏆 SEO component
Includes a SEO component which automatically switches all required `meta` tags for search engines, Twitter Cards, and Facebook OpenGraph tags based on the browsed route/page.

View File

@ -10,10 +10,10 @@ updated: 2018-07-11 00:52:46+02:00
featured: true
tags:
- goodies
- tutorial
- tor
- macos
- goodies
coinhive: true
---

View File

@ -2,7 +2,6 @@ const path = require('path')
const fs = require('fs')
const yaml = require('js-yaml')
const { createFilePath } = require('gatsby-source-filesystem')
const { paginate } = require('gatsby-awesome-pagination')
const fastExif = require('fast-exif')
const Fraction = require('fraction.js')
const dms2dec = require('dms2dec')
@ -172,12 +171,13 @@ exports.createPages = ({ graphql, actions }) => {
}
const posts = result.data.allMarkdownRemark.edges
const numPages = Math.ceil(posts.length / itemsPerPage)
// Generate posts & posts index
generateContent(createPage, posts)
generateContent(createPage, posts, numPages)
// Generate Tag Pages
generateTagPages(createPage, posts)
generateTagPages(createPage, posts, numPages)
// create manual redirects
redirects.forEach(({ f, t }) => {
@ -196,7 +196,7 @@ exports.createPages = ({ graphql, actions }) => {
const postsTemplate = path.resolve('src/templates/Posts.jsx')
const generateContent = (createPage, posts) => {
const generateContent = (createPage, posts, numPages) => {
const postTemplate = path.resolve('src/templates/Post.jsx')
// Create Post pages
@ -210,69 +210,49 @@ const generateContent = (createPage, posts) => {
})
})
// Create paginated front page
paginate({
createPage,
items: posts,
itemsPerPage: itemsPerPage,
pathPrefix: '/',
component: postsTemplate
// Create paginated Blog index pages
Array.from({ length: numPages }).forEach((_, i) => {
createPage({
path: i === 0 ? '/' : `/page/${i + 1}`,
component: postsTemplate,
context: {
limit: itemsPerPage,
skip: i * itemsPerPage,
numPages,
currentPageNumber: i + 1,
prevPage: i - 1,
nextPage: i + 2
}
})
})
}
const generateTagPages = (createPage, posts) => {
const tagSet = new Set()
const tagMap = new Map()
posts.forEach(post => {
if (post.node.frontmatter.tags) {
post.node.frontmatter.tags.forEach(tag => {
tagSet.add(tag)
const array = tagMap.has(tag) ? tagMap.get(tag) : []
array.push(post)
tagMap.set(tag, array)
})
}
})
const tagList = Array.from(tagSet)
const tagList = arrayReducer(posts, 'tags')
tagList.forEach(tag => {
if (tag === 'goodies') return
// Create tag pages
createPage({
path: `/tag/${tag}/`,
path: `/tags/${tag}/`,
component: postsTemplate,
context: { tag }
})
})
// Object.keys(posts).forEach(tagName => {
// const pageSize = 5
// const pagesSum = Math.ceil(posts[tagName].length / pageSize)
// for (let page = 1; page <= pagesSum; page++) {
// createPage({
// path:
// page === 1
// ? `/tag/${tagName.toLowerCase()}`
// : `/tag/${tagName.toLowerCase()}/page/${page}`,
// component: postsTemplate,
// context: {
// posts: paginate(posts[tagName], pageSize, page),
// tag: tagName,
// pagesSum,
// page
// }
// })
// }
// })
}
// function paginate(array, page_size, page_number) {
// return array
// .slice(0)
// .slice((page_number - 1) * page_size, page_number * page_size)
// }
// https://www.adamjberkowitz.com/tags-and-categories-in-gatsby-js/
const arrayReducer = (postsArray, type) => {
return (postsArray = postsArray
.map(({ node }) => {
return node.frontmatter[type]
})
.reduce((a, b) => {
return a.concat(b)
}, [])
.filter((type, index, array) => {
return array.indexOf(type) === index
})
.sort())
}

View File

@ -28,7 +28,6 @@
"fast-exif": "^1.0.1",
"fraction.js": "^4.0.9",
"gatsby": "^2.0.12",
"gatsby-awesome-pagination": "^0.3.1",
"gatsby-image": "^2.0.12",
"gatsby-plugin-catch-links": "^2.0.3",
"gatsby-plugin-favicon": "^3.1.4",

View File

@ -29,4 +29,12 @@ export const imageSizeDefault = graphql`
}
`
export const imageSizeThumb = graphql`
fragment ImageFluidThumb on ImageSharp {
fluid(maxWidth: 200, maxHeight: 85, quality: 85, cropFocus: CENTER) {
...GatsbyImageSharpFluid_withWebp_noBase64
}
}
`
export default Image

View File

@ -27,15 +27,10 @@
@include ellipsis();
width: 100%;
text-align: center;
color: $text-color;
font-family: $font-family-base;
font-size: $font-size-small;
padding: ($spacer/4) 0;
max-width: 70%;
margin: -($spacer) auto $spacer auto;
@media (min-width: $screen-sm) {
max-width: 50%;
}
margin-top: -($spacer);
margin-bottom: $spacer;
}

View File

@ -16,9 +16,7 @@ const query = graphql`
title
image {
childImageSharp {
fluid(maxWidth: 300, maxHeight: 130, cropFocus: CENTER) {
...GatsbyImageSharpFluid_withWebp_noBase64
}
...ImageFluidThumb
}
}
}

View File

@ -3,25 +3,61 @@ import PropTypes from 'prop-types'
import { Link } from 'gatsby'
import styles from './Pagination.module.scss'
const Pagination = ({ pageContext }) => {
const { previousPagePath, nextPagePath } = pageContext
const PageNumber = ({ i, current }) => (
<Link
className={current ? styles.current : styles.number}
to={i === 0 ? '' : `/page/${i + 1}`}
>
{i + 1}
</Link>
)
PageNumber.propTypes = {
i: PropTypes.number.isRequired,
current: PropTypes.bool
}
const PrevNext = ({ prevPagePath, nextPagePath }) => {
const link = prevPagePath || nextPagePath
const rel = prevPagePath ? 'prev' : 'next'
const title = prevPagePath ? 'Newer Posts' : 'Older Posts'
return (
<div className={styles.pagination}>
{nextPagePath ? (
<Link className={styles.paginationLink} to={nextPagePath}>
&laquo; Older Posts
</Link>
) : null}
{previousPagePath ? (
<Link className={styles.paginationLink} to={previousPagePath}>
Newer Posts &raquo;
</Link>
) : null}
</div>
<Link to={link} rel={rel} title={title}>
{prevPagePath ? '←' : '→'}
</Link>
)
}
PrevNext.propTypes = {
prevPagePath: PropTypes.string,
nextPagePath: PropTypes.string
}
const Pagination = ({ pageContext }) => {
const { currentPageNumber, numPages, prevPage, nextPage } = pageContext
const isFirst = currentPageNumber === 1
const isLast = currentPageNumber === numPages
const prevPagePath = currentPageNumber === 2 ? '/' : `/page/${prevPage}`
const nextPagePath = `/page/${nextPage}`
return nextPage > 1 ? (
<div className={styles.pagination}>
<div>{!isFirst && <PrevNext prevPagePath={prevPagePath} />}</div>
<div>
{Array.from({ length: numPages }, (_, i) => (
<PageNumber
key={`pagination-number${i + 1}`}
i={i}
current={currentPageNumber === i + 1}
/>
))}
</div>
<div>{!isLast && <PrevNext nextPagePath={nextPagePath} />}</div>
</div>
) : null
}
Pagination.propTypes = {
pageContext: PropTypes.object.isRequired
}

View File

@ -3,18 +3,41 @@
.pagination {
display: flex;
margin-top: $spacer * 2;
margin-bottom: $spacer * 2;
}
margin-bottom: $spacer;
justify-content: center;
.paginationLink {
flex: 1 1 50%;
display: block;
> div {
&:first-child {
margin-right: $spacer;
}
&:last-child {
text-align: right;
}
&:only-child {
text-align: left;
&:last-child {
margin-left: $spacer;
text-align: right;
}
}
}
.number {
text-align: center;
width: 2rem;
height: 2rem;
line-height: 1.7;
display: inline-block;
border-radius: 50%;
border: 1px solid transparent;
&:hover,
&:focus {
background: rgba(255, 255, 255, .3);
border-color: darken($brand-grey-dimmed, 5%);
}
}
.current {
composes: number;
cursor: default;
pointer-events: none;
border: 1px solid darken($brand-grey-dimmed, 5%);
color: $brand-grey-light;
}

View File

@ -3,17 +3,14 @@ import PropTypes from 'prop-types'
import { Link } from 'gatsby'
import Time from 'react-time'
import slugify from 'slugify'
import PostLinkActions from '../atoms/PostLinkActions'
import styles from './PostMeta.module.scss'
const PostMeta = ({ post, meta }) => {
const { author, updated, tags, type, linkurl } = post.frontmatter
const { date, slug } = post.fields
const { author, updated, tags, type } = post.frontmatter
const { date } = post.fields
return (
<footer className={styles.entryMeta}>
{type === 'link' && <PostLinkActions slug={slug} linkurl={linkurl} />}
<div className={styles.byline}>
<span className={styles.by}>by</span>
<a className="fn" rel="author" href={meta.author.uri}>
@ -45,7 +42,7 @@ const PostMeta = ({ post, meta }) => {
{tags && (
<div className={styles.tags}>
{tags.map(tag => {
const to = tag === 'goodies' ? '/goodies' : `/tag/${slugify(tag)}/`
const to = tag === 'goodies' ? '/goodies' : `/tags/${slugify(tag)}/`
return (
<Link key={tag} className={styles.tag} to={to}>

View File

@ -0,0 +1,120 @@
import React, { Fragment, PureComponent } from 'react'
import PropTypes from 'prop-types'
import { Link, graphql, StaticQuery } from 'gatsby'
import Image from '../atoms/Image'
import styles from './RelatedPosts.module.scss'
const query = graphql`
query {
allMarkdownRemark(sort: { order: DESC, fields: [fields___date] }) {
edges {
node {
id
frontmatter {
title
type
linkurl
tags
image {
childImageSharp {
...ImageFluidThumb
}
}
}
fields {
slug
date(formatString: "MMMM DD, YYYY")
}
}
}
}
}
`
const postsWithDataFilter = (postsArray, key, valuesToFind) => {
const newArray = postsArray.filter(post => {
const frontmatterKey = post.node.frontmatter[key]
if (
frontmatterKey !== null &&
frontmatterKey.some(r => valuesToFind.includes(r))
) {
return post
}
})
return newArray
}
const PostItem = ({ post }) => {
return (
<li>
<Link to={post.node.fields.slug}>
{post.node.frontmatter.image ? (
<Fragment>
<Image
fluid={post.node.frontmatter.image.childImageSharp.fluid}
alt={post.node.frontmatter.title}
/>
<h4 className={styles.postTitle}>{post.node.frontmatter.title}</h4>
</Fragment>
) : (
<div className={styles.empty}>
<h4 className={styles.postTitle}>{post.node.frontmatter.title}</h4>
</div>
)}
</Link>
</li>
)
}
PostItem.propTypes = {
post: PropTypes.object.isRequired
}
class RelatedPosts extends PureComponent {
shufflePosts = () => {
this.forceUpdate()
}
render() {
return (
<StaticQuery
query={query}
render={data => {
const posts = data.allMarkdownRemark.edges
const filteredPosts = postsWithDataFilter(
posts,
'tags',
this.props.tags
)
return (
<aside className={styles.relatedPosts}>
<h1 className={styles.title}>Related Posts</h1>
<ul>
{filteredPosts
.sort(() => 0.5 - Math.random())
.slice(0, 6)
.map(post => (
<PostItem key={post.node.id} post={post} />
))}
</ul>
<button
className={`${styles.button} btn`}
onClick={this.shufflePosts}
>
More Related Posts
</button>
</aside>
)
}}
/>
)
}
}
RelatedPosts.propTypes = {
tags: PropTypes.array.isRequired
}
export default RelatedPosts

View File

@ -0,0 +1,88 @@
@import 'variables';
@import 'mixins';
.empty {
height: 100%;
min-height: 80px;
display: flex;
align-items: center;
padding: $spacer / 4;
h4 {
margin-top: 0;
}
}
.relatedPosts {
margin-top: -($spacer * 2);
margin-bottom: $spacer;
ul {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
padding: 0;
margin: 0;
}
li {
display: block;
flex: 0 0 48%;
margin-bottom: $spacer;
@media (min-width: $screen-sm) {
flex-basis: 31%;
}
&::before {
display: none;
}
}
a {
display: block;
> div {
@include media-frame;
}
&:hover,
&:focus {
> div {
border-color: $link-color;
}
h4 {
color: $link-color;
}
}
}
}
.title {
@include heading-band;
font-size: $font-size-h3;
}
.postTitle {
display: inline-block;
margin-top: $spacer / 4;
margin-bottom: 0;
font-size: $font-size-small;
line-height: $line-height-small;
color: $brand-grey-light;
padding-left: .2rem;
padding-right: .2rem;
transition: color .2s ease-out;
@media (min-width: $screen-md) {
font-size: $font-size-base;
}
}
.button {
margin: auto;
display: block;
margin-top: $spacer / 2;
}

View File

@ -202,7 +202,6 @@ picture {
height: auto;
margin: 0 auto;
display: block;
border-radius: $border-radius;
}
img {
@ -323,14 +322,14 @@ cite {
blockquote,
blockquote > p {
font-style: italic;
color: $brand-grey;
color: $brand-grey-light;
}
// stylelint-enable no-descending-specificity
blockquote {
margin: 0 0 $spacer;
position: relative;
padding-left: $spacer * 1.5;
padding-left: $spacer * 1.25;
// quotation marks
&::before {

View File

@ -8,16 +8,26 @@ import PostTitle from '../components/atoms/PostTitle'
import PostLead from '../components/atoms/PostLead'
import PostContent from '../components/atoms/PostContent'
import PostActions from '../components/atoms/PostActions'
import PostLinkActions from '../components/atoms/PostLinkActions'
import SEO from '../components/atoms/SEO'
import Coinhive from '../components/atoms/Coinhive'
import PostMeta from '../components/molecules/PostMeta'
import Exif from '../components/atoms/Exif'
import RelatedPosts from '../components/molecules/RelatedPosts'
import styles from './Post.module.scss'
const Post = ({ data, location }) => {
const { markdownRemark: post } = data
const { contentYaml: meta } = data
const { title, image, type, linkurl, style, coinhive } = post.frontmatter
const {
title,
image,
type,
linkurl,
style,
coinhive,
tags
} = post.frontmatter
const { slug } = post.fields
return (
@ -37,9 +47,12 @@ const Post = ({ data, location }) => {
)}
{image && image.fields && <Exif exif={image.fields.exif} />}
<PostContent post={post} />
{type === 'link' && <PostLinkActions slug={slug} linkurl={linkurl} />}
<PostActions slug={slug} url={meta.url} />
<PostMeta post={post} meta={meta} />
</article>
{type === 'post' && <RelatedPosts tags={tags} />}
</Layout>
{coinhive && <Coinhive />}
</Fragment>

View File

@ -15,7 +15,7 @@ import styles from './Posts.module.scss'
const Posts = ({ data, location, pageContext }) => {
const edges = data.allMarkdownRemark.edges
const { tag, previousPagePath, humanPageNumber, numberOfPages } = pageContext
const { tag, currentPageNumber, numPages } = pageContext
const PostsList = edges.map(({ node }) => {
const { type, linkurl, title, image } = node.frontmatter
@ -58,11 +58,11 @@ const Posts = ({ data, location, pageContext }) => {
<Layout location={location}>
<SEO />
{location.pathname === '/' && <Featured />}
{tag && <h1 className={styles.archiveTitle}>{tag}</h1>}
{previousPagePath && (
{tag && <h1 className={styles.archiveTitle}>#{tag}</h1>}
{currentPageNumber > 1 && (
<h1
className={styles.archiveTitle}
>{`Page ${humanPageNumber} / ${numberOfPages}`}</h1>
>{`Page ${currentPageNumber} / ${numPages}`}</h1>
)}
{PostsList}
<Pagination pageContext={pageContext} />