Every year I ask readers if they're enjoying this newsletter. Then fail to use those responses 😅
This year I said heck it and built a /testimonials page that shows everyone's feedback raw from Typeform. Here's how.
The perfect feedback form
The best feedback comes from deep in your gut. It's something you feel to be true, but can't quite put into words.
To get great testimonials, you have to ask a specific series of questions.
- What was your hesitation in joining?
- What did you learn?
- What do you like most?
- What else do you like?
- Would you recommend to a friend? Why?
Straight from Sean D'Souza's Brain Audit
Add a few connecting words and you get beauties like this:
I’ve got 25 years of web dev experienced but I learned to feel more confident in my decisions and I love the no BS straight shooting opinionism. The newsletter usually bings a big smile to my face and I’d recommend it to everyone. It's a wealth of experience and knowledge being shared very openly and honestly, even the bad stuff.
And this:
I was worried the advice would be too generic or too tech specific. But I learned what value engineers add to an org and how to build a career as opposed to climbing the ladder. My favorite were the long term perspectives which do not focus only on one aspect of career like salary and roles. The Senior Mindset series in particular helps with gaining context around the industry. Going to use this to get promoted and be more comfortable about the value I add and asking for a raise.
For a newsletter I like to add demographics info. Helps to know if your writing lands with VPs in BigTech or individual contributors in tiny startups.
Just don't be like me and mess up the URL that adds everyone's email to the feedback. Can't followup to add faces and names to the good ones. Everything's anonymous 💩
A custom gatsby-typeform-source plugin
The goal is to write a Gatsby page query and bake Typeform data into your site at deploy time. Gatsby v4 offers more rendering options similar to NextJS, but my data changes once a year. Static is best.
Even re-fetching on every deploy is too much let's be honest.
When we're done, a query like this fetches every response to a pre-configured form.
query {
allTypeformResponse {
nodes {
answers {
field {
title
}
type
number
text
choice {
label
}
}
}
}
}
You can see the full code change, here. And a followup fix for rendering when I learned of a surprise in Typeform's API. More on that below.
Init with a starter
Quickest way to start a custom plugin is in your /plugins
directory. With a starter.
gatsby new plugins/gatsby-typeform-source https://github.com/gatsbyjs/gatsby-starter-plugin
This creates a basic file structure with gatsby-node.js
, gatsby-ssr.js
, a gatsby-browser.js
, and a package.json
file. The typical files you'll need to hook into Gatsby's machinery.
We'll do our work in gatsby-node.js
because we're creating new data nodes for the ... content mesh? ... whatever they're calling it these days. 🤷♀️
Fetch Typeform fields and responses
You'll need to yarn add @typeform/api-client
, the official Typeform SDK. It's a good SDK, I like it. Easy to use :)
The JSON for Typeform responses looks like this:
{
total_items: 156,
page_count: 1,
items: [
{
landing_id: 'idxliv8btjvwel0l6uj0qidxliv50j4q',
token: '...',
response_id: '...',
landed_at: '2022-01-22T02:45:14Z',
submitted_at: '2022-01-22T02:51:55Z',
metadata: { ... },
hidden: { email: '' },
calculated: { score: 0 },
answers: [
{
field: {
id: 'Sk4MM6YV3fyz',
ref: '0bbede4f-b734-4fde-8c52-58aa6b23175a',
type: 'opinion_scale'
},
type: 'number',
number: 5
},
{
field: {
id: 'kTdlVboYMQVH',
ref: '8fbe75b0-2caa-4523-a89b-566d4948d920',
type: 'long_text'
},
type: 'text',
text: "It was a while ago, I don't remember 😅 but usually it's because I don't read emails often so they
just fill up my inbox. I always read yours though!"
},
...
]
},
That means we'll need to fetch the form to get information about what questions were answered.
Like this:
// fetches responses for a specific typeform
function fetchResponses(token, formId) {
const typeformAPI = createClient({
token,
})
return Promise.all([
typeformAPI.forms.get({ uid: formId }),
typeformAPI.responses.list({
uid: formId,
pageSize: 1000,
}),
])
}
token
is your personal API token from Typeform settings. formId
is the ID of the form you want.
We create a new client then get the form and the list of responses. For simplicity we set pageSize: 1000
so we don't have to deal with pagination. 1000 is max though.
Promise.all
makes both requests in parallel. For a small performance boost.
Tie the data together
We'll need to bake the form questions into every response. That makes them easier to use via GraphQL – you can query the question as part of an answer. No custom code required.
That happens in Gatsby's sourceNodes
function:
// plugins/gatsby-typeform-source/gatsby-node.js
exports.sourceNodes = async (
{ actions, createContentDigest, createNodeId },
pluginOptions
) => {
// fetch data
const [{ fields }, { items }] = await fetchResponses(
pluginOptions.token,
pluginOptions.formId
)
// create map of form fields
const fieldMap = new Map(fields.map((field) => [field.id, field]))
// tie answers to fields and cleanup
for (const response of items) {
const data = {
answers: response.answers.map((answer) => ({
...answer,
field: fieldMap.get(answer.field.id),
})),
metadata: response.metadata,
submitted_at: response.submitted_at,
landed_at: response.landed_at
}
We use pluginOptions
to let the site configure the Typeform API token and formId
.
The fieldMap
maps every field.id
to its value. A Map()
can take an array of [key, value]
pairs to create itself. Handy :)
Then we iterate over all the responses to create a data object that:
- has all the answers
- ties each answer with its question
- contains metadata
- timestamps
- throws away response properties that didn't look useful
We'll feed that into Gatsby's createNode
to make data nodes.
Create nodes for Gatsby's machinery
Before you can create Gatsby data nodes, you need to define a new type. That way GraphQL understands the structure of your data and makes it easy to query.
We do that with createSchemaCustomization
// plugins/gatsby-typeform-source/gatsby-node.js
exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
createTypes(`
type TypeformResponse implements Node {
id: ID!
answers: [TypeformAnswer]
metadata: TypeformMetadata
submitted_at: String!
landed_at: String!
variables: String
}
type TypeformMetadata {
user_agent: String
platform: String
referer: String
network_id: String
browser: String
}
type TypeformField {
id: ID
title: String
type: String
}
type TypeformAnswer {
field: TypeformField
type: String
text: String
choice: TypeformChoice
number: Int
}
type TypeformChoice {
id: ID,
label: String
}
`)
}
It's a description of the shape of our data. We created a bunch of custom types and tied them together.
To use them, we add a call to createNode
after every const data = { ... }
.
// plugins/gatsby-typeform-source/gatsby-node.js
const { createNode } = actions
createNode({
id: createNodeId(`typeformResponse-${response.response_id}`),
parent: null,
children: [],
...data,
internal: {
type: "TypeformResponse",
contentDigest: createContentDigest(data),
},
})
This tells Gatsby to add a new node to the content mesh or whatever, creates an id
, and a contentDigest
so Gatsby can handle caching and optimize performance. I think it can skip page generation, if data stayed the same.
Using the custom Typeform source plugin
With that custom plugin, we can add Typeform responses to any page or component in Gatsby.
Configure the plugin
First you enable the plugin in gatsby-config.js
:
// gatsby-config.js
plugins: [
{
resolve: "gatsby-typeform-source",
options: {
// configured in .env and your hosting provider
token: process.env.TYPEFORM_TOKEN,
// hardcoded globally because I'm lazy
formId: "jLgVKKLf",
},
},
Would be nice if the formId
was a query param, but I needed this to build the /testimonials page. Not solve every problem ever ✌️
An important engineering skill.
I actually ask for a maximum time commitment. ... and still get replies from people that took 1-2days to do something that should take 1-2h to implement. Typically over-engineered. maybe even written using vi or emacs.
— Peter Kuhar (@pkuhar) January 24, 2022
Run the query
You can fetch Typeform data anywhere inside Gatsby. Mine is set up to create pages based on MDX.
// src/pages/testimonials.mdx
---
title: "Newsletter testimonials"
description: "What readers think of Swizec's newsletter"
---
import { graphql } from "gatsby"
import { TypeformResponse } from "../components/TypeformResponse.js"
export const pageQuery = graphql`
query {
allTypeformResponse {
nodes {
answers {
field {
title
}
type
number
text
choice {
label
}
}
}
}
}
`
About once a year I send a feedback poll to newsletter subscribers. Here are all their responses from 2021. You can judge for yourself 😊
<>
{props.data.allTypeformResponse.nodes.map(({ answers }) => (
<TypeformResponse answers={answers} />
))}
</>
Set a title and description, import a React component and Gatsby's graphql hook. Export a page query, add intro text, and iterate through the data to render responses.
Render the data
The tricky surprise was that Typeform's API skips questions that weren't answered. Saying answers[3]
can return answers to different questions for different people 🙃
A smol helper function solves that problem:
function getAnswer(question, answers) {
return answers.find((answer) => answer.field.title === question)
}
It finds the correct answer based on the question text.
The rest is about slapping together HTML components to show a testimonial. You can see the implementation on GitHub.
I think it came out great!
Think I should opensource the gatsby-typeform-source
plugin? Hit reply
Cheers,
~Swizec
Continue reading about How to add Typeform as a Gatsby source
Semantically similar articles hand-picked by GPT-4
- Using YouTube as a data source in Gatsbyjs
- Your first NextJS app – CodeWithSwiz
- Lessons from migrating a 14 year old blog with 1500 posts to Gatsby
- Build privacy-focused blazing fast tweet embeds – CodeWithSwiz 30
- Towards a Gatsby+Suspense proof-of-concept
Learned something new? Want to become a React expert?
Learning from tutorials is easy. You follow some steps, learn a smol lesson, and feel like you got dis 💪
Then comes the interview, a real world problem, or a question from the boss. Your mind goes blank. Shit, how does this work again ...
Happens to everyone. Building is harder than recognizing and the real world is a mess! Nothing fits in neat little boxes you learned about in tutorials.
That's where my emails come in – lessons from experience. Building production software :)
Leave your email and get the React email Series - a series of curated essays on building with React. Borne from experience, seasoned with over 12+ years of hands-on practice building the engineering side of growing SaaS companies.
Get Curated React Essays
Get a series of curated essays on React. Lessons and insights from building software for production. No bullshit.
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️