Skip to content

refactor: create share card from buffer #309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions gatsby-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ const {
} = require('./gatsby/create-pages/create-career-pages');
const { createFilePath } = require('gatsby-source-filesystem');

exports.onCreateNode = (gatsbyCreateNodeArgs) => {
createPreviewCards(gatsbyCreateNodeArgs);
exports.onCreateNode = async (gatsbyCreateNodeArgs) => {
await createPreviewCards(gatsbyCreateNodeArgs);

const { node, actions, getNode } = gatsbyCreateNodeArgs;
const { createNodeField } = actions;
Expand All @@ -27,6 +27,36 @@ exports.onCreateNode = (gatsbyCreateNodeArgs) => {
}
};

/**
* We should use `@link` and link the given foreign key field to the actual node.
* The before known foreign Key `___NODE` notation is deprecated, that's why we need the custom and explicit schema.
*
* You can't replace fields.socialCard hence the creation of the node in the parent.
* This is syntactic sugar and as as an alternative can use
* `schema.buildObjectType` and then resolve the reference manually through `context.nodeModel.getNodeById`
*
* Deprecation Note:
* https://www.gatsbyjs.com/docs/reference/release-notes/migrating-from-v3-to-v4/#___node-convention-is-deprecated
*
* How to "schema additions"
* https://www.gatsbyjs.com/docs/how-to/plugins-and-themes/creating-a-source-plugin/#create-foreign-key-relationships-between-data
*/

exports.createSchemaCustomization = ({ actions, schema }) => {
const { createTypes } = actions;

const typeDefs = [
`type MarkdownRemark implements Node {
socialCardFile: File @link(from: "fields.socialCardFileId")
}`,
`type SyPersonioJob implements Node {
socialCardFile: File @link(from: "fields.socialCardFileId")
}`,
];

createTypes(typeDefs);
};

exports.createPages = async (createPagesArgs) => {
await createCareerPages(createPagesArgs);
await createBlogPosts(createPagesArgs);
Expand Down
71 changes: 32 additions & 39 deletions gatsby/create-node/create-preview-cards.js
Original file line number Diff line number Diff line change
@@ -1,65 +1,58 @@
const slugify = require('slugify');
const path = require('path');
const { siteMetadata } = require('../../gatsby-config');
const {
generateCard,
generateCardToBuffer,
} = require('../util/preview-card-generator/generate-card');

const { createFileNodeFromBuffer } = require('gatsby-source-filesystem');

/**
* Create a preview card file and put the url to the Gatsby store to be able
* to link it in the page header.
*/
const createPreviewMarkdown = ({ node, _, actions }) => {
const { createNodeField } = actions;
const post = node.frontmatter;
const createPreviewCard = async (
title,
{ node, _, actions, getCache, createNodeId },
) => {
const { createNode, createNodeField } = actions;

// we need the title in order to generate anything
if (!post.title) {
if (!title) {
return;
}

// we create a file name from the path (which is the page slug and more unique) but if none is available take the title
const fileName = `social-card---${slugify(post.path || post.title, {
lower: true,
})}.jpg`;
const outputFile = path.join('public', fileName);
const imagePath = `/${fileName}`;

generateCard({ title: post.title }, outputFile).then(() => {
createNodeField({
node,
name: `socialCard`,
value: imagePath,
});
const buffer = await generateCardToBuffer({ title });
/**
* The util function `createFileNodeFromBuffer` from the official gatsby source plugin `gatsby-source-filesystem`
* creates a file node from a given file buffer. The value of `parentNodeId` creates the necessary relationship
* between the original node and the actual file node so it's not garbage collected.
*
* The actual foreign key relationship is resolved through `createSchemaCustomization`
* in gatsby-node.js for all node types the `createPreviewCard` is invoked for.
*/
const fileNode = await createFileNodeFromBuffer({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function creates AND links the node to the parent, right? A comment above would make this more clear, as I was just wondering at which place the actual connection between the Markdown/SyJob and the file is happening.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add the following comment to make things clear:

The util function createFileNodeFromBuffer from the official gatsby source plugin gatsby-source-filesystem
creates a file node from a given file buffer. The value of parentNodeId creates the necessary relationship
between the original node and the actual file node so it's not garbage collected.

The actual foreign key relationship is resolved through createSchemaCustomization
in gatsby-node.js for all node types the createPreviewCard is invoked for.

Ideally we create a plugin "create social cards" or such that would bundle this invocation (tied to onCreateNodde) and the fk lookup which is tied to createSchemaCustomization. Feels like a random split while it's technically required. That's our next small local plugin we can extract (and configure) 🙏

name: 'social-card',
buffer,
getCache,
createNode,
createNodeId,
parentNodeId: node.id,
});
};

const createPreviewJob = ({ node, _, actions }) => {
const { createNodeField } = actions;

const fileName = `social-card---career-${slugify(node.name, {
lower: true,
})}.jpg`;

const outputFile = path.join('public', fileName);
const imagePath = `/${fileName}`;

generateCard({ title: node.name }, outputFile).then(() => {
if (fileNode) {
createNodeField({
node,
name: 'socialCard',
value: imagePath,
name: `socialCardFileId`,
value: fileNode.id,
});
});
}
};

const createPreviewCards = ({ node, _, actions }) => {
const createPreviewCards = async ({ node, ...rest }) => {
if (node.internal.type === 'SyPersonioJob') {
createPreviewJob({ node, _, actions });
await createPreviewCard(node.name, { node, ...rest });
}

if (node.internal.type === 'MarkdownRemark') {
createPreviewMarkdown({ node, _, actions });
await createPreviewCard(node.frontmatter.title, { node, ...rest });
}
};

Expand Down
10 changes: 3 additions & 7 deletions gatsby/util/preview-card-generator/generate-card.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
const fs = require('fs');
const path = require('path');
const { createCanvas, loadImage, registerFont } = require('canvas');

Expand Down Expand Up @@ -63,7 +62,7 @@ const wrapTextFactory = (context) => (text, x, y, maxWidth, lineHeight) => {
* by composing a background with a title
* and an optional author.
*/
async function generateCard({ title, author }, file) {
async function generateCardToBuffer({ title, author }) {
const canvas = createCanvas(CARD_WIDTH, CARD_HEIGHT);

const context = canvas.getContext('2d');
Expand Down Expand Up @@ -95,12 +94,9 @@ async function generateCard({ title, author }, file) {
);
}

const buffer = canvas.toBuffer('image/jpeg');
fs.writeFileSync(file, buffer);

return file;
return canvas.toBuffer('image/jpeg');
}

module.exports = {
generateCard,
generateCardToBuffer,
};
23 changes: 15 additions & 8 deletions src/pages/career.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import SEO from '../components/layout/seo';
import { graphql } from 'gatsby';
import { useTranslation } from 'gatsby-plugin-react-i18next';
import { CareerPage } from '../components/pages/career/career-page';
import { LocalesQuery, SyPersonioJob } from '../types';
import {
LocalesQuery,
PlainFixedImageSharpSource,
SyPersonioJob,
} from '../types';
import { IGatsbyImageData } from 'gatsby-plugin-image';

interface CareerMarkdownQuery {
htmlAst: string;
fields: {
socialCard: string;
};
socialCardFile: PlainFixedImageSharpSource;
}

interface CareerProps {
Expand All @@ -27,12 +29,13 @@ interface CareerProps {

const Career = (props: CareerProps) => {
const { t } = useTranslation();
const socialCard = props.data.markdownRemark?.fields?.socialCard;
const socialCardPath =
props.data.markdownRemark.socialCardFile.childImageSharp.fixed.src;

return (
<>
<SEO
shareImagePath={socialCard}
shareImagePath={socialCardPath}
title={`${t('career.title')} | Satellytes`}
description={t('career.seo.description')}
location={props.location}
Expand Down Expand Up @@ -81,8 +84,12 @@ export const CareerPageQuery = graphql`
fileAbsolutePath: { regex: "/(pages/career)/" }
frontmatter: { language: { eq: $language } }
) {
fields {
socialCard
socialCardFile {
childImageSharp {
fixed(width: 1440, height: 760) {
src
}
}
}
}
}
Expand Down
14 changes: 9 additions & 5 deletions src/templates/career-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ interface CareerPageProps {
const CareerPage: React.FC<CareerPageProps> = (props): JSX.Element => {
const { pageContext } = props;
const position = props.data.syPersonioJob;
const socialCardImage = position.fields.socialCard;
const socialCardPath = position.socialCardFile.childImageSharp.fixed.src;
const { t } = useTranslation();

return (
<>
<SEO
shareImagePath={socialCardImage}
shareImagePath={socialCardPath}
title={t('career.seo.title-detail', {
name: position.name,
})}
Expand All @@ -51,9 +51,6 @@ const CareerPage: React.FC<CareerPageProps> = (props): JSX.Element => {
export const CareerDetailsPageQuery = graphql`
query ($language: String!, $id: String!) {
syPersonioJob(id: { eq: $id }) {
fields {
socialCard
}
id
lang
jobId
Expand All @@ -66,6 +63,13 @@ export const CareerDetailsPageQuery = graphql`
descriptionHtml
description
}
socialCardFile {
childImageSharp {
fixed(width: 1440, height: 760) {
src
}
}
}
}
locales: allLocale(filter: { language: { eq: $language } }) {
Expand Down
22 changes: 11 additions & 11 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@ export interface SyPersonioJob {
slug: string;
sections: SyPersonioJobSection[];

// now owned by the source plugin, added via `onCreateNode`
fields: {
socialCard: string;
};
// added via `onCreateNode`
socialCardFile: PlainFixedImageSharpSource;
}

export interface SyTeamMember {
Expand All @@ -52,6 +50,14 @@ export interface SyTeamMember {
image: IGatsbyImageData;
}

export interface PlainFixedImageSharpSource {
childImageSharp: {
fixed: {
src: string;
};
};
}

export interface BlogPostMarkdown {
excerpt: string;
htmlAst;
Expand All @@ -61,13 +67,7 @@ export interface BlogPostMarkdown {
};
};
frontmatter: {
shareImage: {
childImageSharp: {
fixed: {
src: string;
};
};
};
shareImage: PlainFixedImageSharpSource;
attribution: {
creator: string;
source: string;
Expand Down