Navigate back to the homepage

Building a Portfolio/Resume Site with Gatsby, Part 2 - Customizing your site and merging your changes

Gavin Johnson
July 27th, 2020 · 4 min read

This post is the second in a series and was originally published on Dev.to.


Quick recap. My personal website sucks. I’m updating it using Gatsby and the Novela theme by narative, because it’s popular, and I’m basic.

FYI, I’m using a Mac, so there may be some Mac-specific commands.


Alright, part 2, customizing your site. Part 1 was very entry-level, very basic. Part 2 is much more involved. Part 2 is where you make your site your own. Let's not waste any more time. Let's get started.

Update your site metadata

  • In Terminal, cd to your website directory
  • git checkout -b initial_customizations

Your terminal output should be something like the following.

1Switched to a new branch 'initial_customizations'
  • Open gatsby-config.js in your code editor (I use Visual Studio Code)
    • Update your site metadata and save. Here’s mine.
1module.exports = {
2 siteMetadata: {
3 title: `thtmnisamnstr`,
4 name: `thtmnisamnstr`,
5 siteUrl: `https://thtmnisamnstr.com/`,
6 description: `Gavin Johnson's personal site. Resume, blog posts, tech product marketing stuff, maybe some tech-y stuff too.`,
7 hero: {
8 heading: `Hi. I'm Gavin. My handle is usually thtmnisamnstr. This is my site.`,
9 maxWidth: 652,
10 },
11 social: [
12 {
13 name: `twitter`,
14 url: `https://twitter.com/gavinjtech`,
15 },
16 {
17 name: `devto`,
18 url: `https://dev.to/thtmnisamnstr`,
19 },
20 {
21 name: `github`,
22 url: `https://github.com/thtmnisamnstr`,
23 },
24 {
25 name: `linkedin`,
26 url: `https://www.linkedin.com/in/gavin-johnson/`,
27 },
28 {
29 name: `instagram`,
30 url: `https://www.instagram.com/thtmnisamnstr`,
31 },
32 ],
33 },
34 plugins: [
35 {
36 resolve: "@narative/gatsby-theme-novela",
37 options: {
38 contentPosts: "content/posts",
39 contentAuthors: "content/authors",
40 basePath: "/",
41 authorsPage: true,
42 sources: {
43 local: true,
44 },
45 },
46 },
47 {
48 resolve: `gatsby-plugin-manifest`,
49 options: {
50 name: `Novela by Narative`,
51 short_name: `Novela`,
52 start_url: `/`,
53 background_color: `#fff`,
54 theme_color: `#fff`,
55 display: `standalone`,
56 icon: `src/assets/favicon.png`,
57 },
58 },
59 {
60 resolve: `gatsby-plugin-netlify-cms`,
61 options: {
62 },
63 },
64 ],
65};

I also changed the favicon,././src/assets/favicon.png

Add your author info

  • Add your author info for your author page
    • Create a new file under ./content/authors/
      • Name it authors.yml and add your name, bio, avatar, featured, social parameters, and save. Here’s mine.
1- name: Gavin Johnson
2 bio: |
3 Gavin is a programmer-turned-marketer with experience in IT, consulting, and marketing.
4 He has been at New Relic since 2018, focusing his time and efforts on helping New Relic
5 be more open and telling New Relic's open source story. Gavin started his career as a
6 programmer and sys admin prior to attending business school at the University of Southern
7 California. Following b-school, he worked for Deloitte Digital as a Senior Consultant and
8 Manager, designing and delivering custom applications for clients. He then moved to AT&T,
9 working as a Lead Product Marketing Manager for their digital video products. Gavin is an
10 Oregon native but has made Los Angeles his home since 2009. He is a happily married,
11 Brazilian Jiu Jitsu purple belt, film photographer, homebrewer, and former marathoner.
12 shortBio: |
13 Open Source marketing person at New Relic. Ex-AT&T marketer. Ex-Deloitte consultant. Ex-sys
14 admin. (Sometimes)Ex-developer.
15 avatar: ./avatars/gavin-johnson.jpg
16 social:
17 - url: https://twitter.com/gavinjtech
18 - url: https://dev.to/thtmnisamnstr
19 - url: https://github.com/thtmnisamnstr
20 - url: https://www.linkedin.com/in/gavin-johnson/
21 - url: https://www.instagram.com/thtmnisamnstr
22 featured: true
  • Create a new folder under ./content/authors/. Name it avatars.
  • Add your avatar image to ./content/authors/avatars/. Name it [your-name].jpg. Here’s mine.

Alt Text

  • Update the existing example blog post to use your author info
    • Open ./content/posts/2020-01-01-my-first-post-using-novela-by-narative/index.md
      • Change the author parameter to [your name] and save. Here’s the first few lines of mine.
1---
2title: My first post using Novela by Narative
3author: Gavin Johnson
4date: 2019-04-30
5hero: ./images/hero.jpg
6excerpt: With the growing community interest in Gatsby, we hope to create more resources that make it easier for anyone to grasp the power of this incredible tool.
7---
  • Delete the example author files
    • Delete ./content/authors/authors/ and all files under it
  • I’m changing the site logo to simple text by shadowing the Logo component.
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/components/Logo/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/components/Logo/Logo.tsx' './src/@narative/gatsby-theme-novela/components/Logo/Logo.tsx'
    • Open ./src/@narative/gatsby-theme-novela/components/Logo/Logo.tsx
    • Update your SVG info and save. Here’s mine.
1import React from "react";
2import styled from "@emotion/styled";
3
4import mediaqueries from "@styles/media";
5
6import { Icon } from '@types';
7
8const Logo: Icon = ({ fill = "white" }) => {
9 return (
10 <LogoContainer>
11 <svg
12 width="192"
13 height="23"
14 viewBox="0 0 192 23"
15 fill="none"
16 xmlns="http://www.w3.org/2000/svg"
17 className="Logo__Desktop"
18 >
19 {/* Gavin 20200719: Changed SVG logo to text */}
20 <text x="0" y="11" dx fontFamily="Helvetica Neue" fontWeight="bold" textAnchor="start" fill={fill}>
21 thtmnisamnstr
22 </text>
23 <defs>
24 <clipPath id="clip0">
25 <rect width="191.156" height="23" fill="white" />
26 </clipPath>
27 </defs>
28 </svg>
29
30 <svg
31 width="18"
32 height="23"
33 viewBox="0 0 18 23"
34 fill="none"
35 xmlns="http://www.w3.org/2000/svg"
36 className="Logo__Mobile"
37 >
38 <text x="0" y="11" dx fontFamily="Helvetica Neue" fontWeight="bold" textAnchor="start" fill={fill}>
39 thtmnisamnstr
40 </text>
41 </svg>
42 </LogoContainer>
43 );
44};
45
46export default Logo;
47
48const LogoContainer = styled.div`
49 .Logo__Mobile {
50 display: none;
51 }
52
53 ${mediaqueries.tablet`
54 .Logo__Desktop {
55 display: none;
56 }
57
58 .Logo__Mobile{
59 display: block;
60 }
61 `}
62`;
  • Run gatsby develop to see what your site looks like. Here’s mine.
    Alt Text

I don’t love how big the bio text is. I want a short bio. So I’mma add a new key to the Authors YAML, shortBio, and put that in the Featured Author section.

  • Add shortBio to the authors YAML
    • Open /content/authors/authors.yml
    • Add a short bio. Here’s my authors file.
1- name: Gavin Johnson
2 bio: |
3 Gavin is a programmer-turned-marketer with experience in IT, consulting, and marketing.
4 He has been at New Relic since 2018, focusing his time and efforts on helping New Relic
5 be more open and telling New Relic's open source story. Gavin started his career as a
6 programmer and sys admin prior to attending business school at the University of Southern
7 California. Following b-school, he worked for Deloitte Digital as a Senior Consultant and
8 Manager, designing and delivering custom applications for clients. He then moved to AT&T,
9 working as a Lead Product Marketing Manager for their digital video products. Gavin is an
10 Oregon native but has made Los Angeles his home since 2009. He is a happily married,
11 Brazilian Jiu Jitsu purple belt, film photographer, homebrewer, and former marathoner.
12 shortBio: |
13 Open Source marketing person at New Relic. Ex-AT&T marketer. Ex-Deloitte consultant. Ex-sys
14 admin. (Sometimes)Ex-developer.
15 avatar: ./avatars/gavin-johnson.jpg
16 social:
17 - url: https://twitter.com/gavinjtech
18 - url: https://dev.to/thtmnisamnstr
19 - url: https://github.com/thtmnisamnstr
20 - url: https://www.linkedin.com/in/gavin-johnson/
21 - url: https://www.instagram.com/thtmnisamnstr
22 featured: true
  • Shadow the Bio component, Bio.tsx
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/components/Bio/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/components/Bio/Bio.tsx' './src/@narative/gatsby-theme-novela/components/Bio/Bio.tsx'
    • Open ./src/@narative/gatsby-theme-novela/components/Bio/Bio.tsx
    • Change author.bio to author.shortBio and save. Here’s mine.
1import React from 'react';
2import { Link } from 'gatsby';
3import styled from '@emotion/styled';
4
5import Image from '@components/Image';
6import { IAuthor } from '@types';
7
8const Bio: React.FC<IAuthor> = ({ author }) => {
9 return (
10 <BioContainer>
11 <BioAvatar
12 as={author.authorsPage ? Link : 'div'}
13 to={author.slug}
14 data-a11y="false"
15 aria-label="Author's bio"
16 >
17 <BioAvatarInner>
18 <RoundedImage src={author.avatar.medium} />
19 </BioAvatarInner>
20 </BioAvatar>
21 {/* Gavin, 20200719: Changed to shortBio */}
22 <BioText dangerouslySetInnerHTML={{ __html: author.shortBio }} />
23 </BioContainer>
24 );
25};
26
27export default Bio;
28
29const BioContainer = styled.div`
30 display: flex;
31 align-items: center;
32 position: relative;
33 left: -10px;
34`;
35
36const BioAvatar = styled.div`
37 display: block;
38 position: relative;
39 height: 40px;
40 width: 40px;
41 border-radius: 50%;
42 background: rgba(0, 0, 0, 0.25);
43 margin-right: 16px;
44 margin: 10px 26px 10px 10px;
45
46 &::after {
47 content: '';
48 position: absolute;
49 left: -5px;
50 top: -5px;
51 width: 50px;
52 height: 50px;
53 border-radius: 50%;
54 border: 1px solid rgba(0, 0, 0, 0.25);
55 }
56
57 &[data-a11y='true']:focus::after {
58 content: '';
59 position: absolute;
60 left: -5px;
61 top: -5px;
62 width: 50px;
63 height: 50px;
64 border: 2px solid ${p => p.theme.colors.accent};
65 }
66`;
67
68const RoundedImage = styled(Image)`
69 border-radius: 50%;
70`;
71
72const BioAvatarInner = styled.div`
73 height: 40px;
74 width: 40px;
75 border-radius: 50%;
76 background: rgba(0, 0, 0, 0.25);
77 margin-right: 16px;
78 overflow: hidden;
79`;
80
81const BioText = styled.p`
82 max-width: 430px;
83 font-size: 14px;
84 line-height: 1.45;
85 color: ${p => p.theme.colors.grey};
86
87 a {
88 color: ${p => p.theme.colors.grey};
89 text-decoration: underline;
90 }
91`;
  • Shadow the IAuthor interface in index.d.ts
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/types/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/types/index.d.ts' './src/@narative/gatsby-theme-novela/types/index.d.ts'
    • Open ./src/@narative/gatsby-theme-novela/types/index.d.ts
    • Add shortBio to your IAuthor interface and save. Here’s mine.
1import React from "react";
2
3export interface IPaginator {
4 pageCount: number;
5 index: number;
6 pathPrefix: string;
7}
8
9interface IGatsbyImage {
10 src: string;
11 base64?: string;
12 srcWebp?: string;
13 srcSet?: string;
14 srcSetWebp?: string;
15 tracedSVG?: string;
16}
17
18interface IGatsbyImageFluid extends IGatsbyImage {
19 maxHeight: number;
20 maxWidth: number;
21}
22
23interface IGatsbyImageFixed extends IGatsbyImage {
24 height: number;
25 width: number;
26}
27
28export interface IAuthor {
29 authorsPage?: boolean;
30 featured?: boolean;
31 name: string;
32 slug: string;
33 bio: string;
34 // Gavin, 20200719: Added shortBio
35 shortBio: string;
36 avatar: {
37 image: IGatsbyImageFluid;
38 full: IGatsbyImageFluid;
39 };
40}
41
42export interface IArticle {
43 slug: string;
44 authors: IAuthor[];
45 excerpt: string;
46 body: string;
47 id: string;
48 hero: {
49 full: IGatsbyImageFluid;
50 preview: IGatsbyImageFluid;
51 regular: IGatsbyImageFluid;
52 seo: string;
53 };
54 timeToRead: number;
55 date: string;
56 secret: boolean;
57}
58
59interface IArticleQuery {
60 edges: {
61 node: IArticle;
62 }[];
63}
64
65export interface IProgress {
66 height: number;
67 offset: number;
68 title: string;
69 mode: string;
70 onClose?: () => void;
71}
72
73export type Icon = React.FC<{
74 fill: string
75}>
76
77export type Template = React.FC<{
78 pageContext: {
79 article: IArticle;
80 authors: IAuthor[];
81 mailchimp: boolean;
82 next: IArticle[];
83 };
84 location: Location;
85}>
  • The correct thing to do next would be to shadow data.query.js and add the shortBio field, but that isn’t working for some reason. So let’s do it directly in the Novela theme module.
    • Open ./node_modules/@narative/gatsby-theme-novela/src/gatsby/data/data.query.js
    • Add shortBio to your author queries and save. Here’s mine.
1/* eslint-disable */
2
3// https://github.com/gatsbyjs/gatsby/blob/master/packages/gatsby-transformer-sharp/src/fragments.js
4
5const GatsbyFluid_withWebp = `
6 base64
7 aspectRatio
8 src
9 srcSet
10 srcWebp
11 srcSetWebp
12 sizes
13`;
14
15module.exports.local = {
16 articles: `{
17 articles: allArticle(
18 sort: { fields: [date, title], order: DESC }
19 limit: 1000
20 ) {
21 edges {
22 node {
23 id
24 slug
25 secret
26 title
27 author
28 date(formatString: "MMMM Do, YYYY")
29 dateForSEO: date
30 timeToRead
31 excerpt
32 canonical_url
33 subscription
34 body
35 hero {
36 full: childImageSharp {
37 fluid(maxWidth: 944, quality: 100) {
38 ${GatsbyFluid_withWebp}
39 }
40 }
41 regular: childImageSharp {
42 fluid(maxWidth: 653, quality: 100) {
43 ${GatsbyFluid_withWebp}
44 }
45 }
46 narrow: childImageSharp {
47 fluid(maxWidth: 457, quality: 100) {
48 ${GatsbyFluid_withWebp}
49 }
50 }
51 seo: childImageSharp {
52 fixed(width: 1200, quality: 100) {
53 src
54 }
55 }
56 }
57 }
58 }
59 }
60 }`,
61 authors: `{
62 authors: allAuthor {
63 edges {
64 node {
65 authorsPage
66 bio
67 shortBio
68 id
69 name
70 featured
71 social {
72 url
73 }
74 slug
75 avatar {
76 small: childImageSharp {
77 fluid(maxWidth: 50, quality: 100) {
78 ${GatsbyFluid_withWebp}
79 }
80 }
81 medium: childImageSharp {
82 fluid(maxWidth: 100, quality: 100) {
83 ${GatsbyFluid_withWebp}
84 }
85 }
86 large: childImageSharp {
87 fluid(maxWidth: 328, quality: 100) {
88 ${GatsbyFluid_withWebp}
89 }
90 }
91 }
92 }
93 }
94 }
95 }`,
96};
97
98module.exports.contentful = {
99 articles: `{
100 articles: allContentfulArticle(sort: {fields: [date, title], order: DESC}, limit: 1000) {
101 edges {
102 node {
103 body {
104 childMdx {
105 body
106 timeToRead
107 }
108 }
109 excerpt
110 title
111 slug
112 secret
113 date(formatString: "MMMM Do, YYYY")
114 dateForSEO: date
115 hero {
116 full: fluid(maxWidth: 944, quality: 100) {
117 ${GatsbyFluid_withWebp}
118 }
119 regular: fluid(maxWidth: 653, quality: 100) {
120 ${GatsbyFluid_withWebp}
121 }
122 narrow: fluid(maxWidth: 457, quality: 100) {
123 ${GatsbyFluid_withWebp}
124 }
125 seo: fixed(width: 1200, quality: 100) {
126 src
127 }
128 }
129 id
130 author {
131 name
132 }
133 }
134 }
135 }
136 }
137 `,
138 authors: `{
139 authors: allContentfulAuthor {
140 edges {
141 node {
142 avatar {
143 small: fluid(maxWidth: 50, quality: 100) {
144 ${GatsbyFluid_withWebp}
145 }
146 medium: fluid(maxWidth: 100, quality: 100) {
147 ${GatsbyFluid_withWebp}
148 }
149 large: fluid(maxWidth: 328, quality: 100) {
150 ${GatsbyFluid_withWebp}
151 }
152 }
153 fields {
154 authorsPage
155 slug
156 }
157 bio
158 shortBio
159 id
160 name
161 social
162 featured
163 }
164 }
165 }
166 }`,
167};
  • Because data.query.js couldn’t be shadowed, add the Novela theme under node_modules to your git repo
    • Open ./.gitignore
    • Update to include the Novela theme folder in your repo and save. Here’s mine.
1# Logs
2logs
3*.log
4npm-debug.log*
5yarn-debug.log*
6yarn-error.log*
7
8# Runtime data
9pids
10*.pid
11*.seed
12*.pid.lock
13
14# Directory for instrumented libs generated by jscoverage/JSCover
15lib-cov
16
17# Coverage directory used by tools like istanbul
18coverage
19
20# nyc test coverage
21.nyc_output
22
23# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24.grunt
25
26# Bower dependency directory (https://bower.io/)
27bower_components
28
29# node-waf configuration
30.lock-wscript
31
32# Compiled binary addons (http://nodejs.org/api/addons.html)
33build/Release
34
35# Dependency directories
36jspm_packages/
37# Gavin, 20200719: Including the Novela theme directory under node_modules
38node_modules/*
39!node_modules/@narative/
40node_modules/@narative/*
41!node_modules/@narative/gatsby-theme-novela/
42
43# Typescript v1 declaration files
44typings/
45
46# Optional npm cache directory
47.npm
48
49# Optional eslint cache
50.eslintcache
51
52# Optional REPL history
53.node_repl_history
54
55# Output of 'npm pack'
56*.tgz
57
58# dotenv environment variables file
59.env
60
61# gatsby files
62.cache/
63public
64
65# Mac files
66.DS_Store
67
68# Yarn
69yarn-error.log
70.pnp/
71.pnp.js
72# Yarn Integrity file
73.yarn-integrity
74
75.netlify/
  • Run gatsby develop to see what your site looks like. Here’s mine.

    Alt Text

  • Click on the author avatar to go to the Author page. Note that it still uses the standard bio field. This is what I was aiming for. Here’s mine.

    Alt Text

Add a resume page and navigation

I also want a resume page as well. I could go and create a new content type, update the query to include it, add it to createPages, …, or I could create a static page, but I just want a resume I can write in markdown and direct-link to. Plus, I’m trying to learn Gatsby anyway, so I’m going to make a resume page by making a secret post that includes a slug.

  • Create a secret post for the resume page
    • mkdir -p './content/posts/SECRET-20200719-Resume/images/'
    • touch './content/posts/SECRET-20200719-Resume/index.md'
    • Open ./content/posts/SECRET-20200719-Resume/index.md
    • Write your resume and save make sure you set secret: true and slug: [first]-[last]-resume. Here’s mine.
1---
2title: Resume
3author: Gavin Johnson
4date: 2020-07-19
5hero:
6secret: true
7slug: gavin-johnson-resume
8---
9
10*Experienced marketer and strategist with a record of approaching business questions through an analytical lens and driving action through that analysis.*
11* *Data analysis and data driven decision making*
12* *Marketing strategy and content development*
13* *Executive presentation and public speaking*
14* *Technical background, both as a dev and dev lead*
15
16## Experience
17
18### New Relic *-- Los Angeles, CA (Remote)*
19#### Principal Product Marketing Manager, Open Source *-- May 2020-Present*
20Led go-to-market strategy and execution for [New Relic Open Source](https://opensource.newrelic.com) and for [New Relic's open source instrumentation](https://blog.newrelic.com/product-news/introducing-open-source-agents-and-projects/).
21#### Principal Solutions Marketing Manager, Digital Customer Experience (now Digital Experience Management) *-- November 2018-May 2020*
22Built the strategy for New Relic’s Digital Customer (DCX) solution and executed on it. Led creation of DCX Marketing and Sales content. Integrated DCX into New Relic’s campaigns and tactics. Represented New Relic as an external speaker.
23* Built New Relic’s DCX solution strategy. Analyzed internal and external content developed for and tactics previously used by other New Relic solutions. Performed competitive analysis. Developed a DCX content and tactics roadmap and calendar.
24* Wrote New Relic’s DCX solution positioning and messaging. Described how we wanted the DCX solution perceived by the market, who our targeted personas were, and wrote taglines, copy blocks, and email templates for use by Marketing and Sales.
25* Created DCX solution content for Sales. Collaborated with the solution consulting group from Sales to build industry specific DCX content (e.g. pitch decks, solution guide decks, industry primers, discovery questions, objection handling, etc.). Targeted four industries: Media, Retail, FinServ, and Travel.
26* Integrated DCX solution into Marketing campaigns and tactics. Worked cross-functionally with demand generation, content and creative, digital marketing, and field marketing. Led creation of and co-wrote content (e.g. articles, solution sheets, white papers, etc.) and landing pages. Developed an event response campaign and cadence.
27* Represented New Relic as an external speaker. Presented on the importance of DCX for modern, digital businesses and connecting technical application performance to business outcomes. Presented at New Relic’s FutureStack Los Angeles 2019, WP Engine Summit 2019, and Gartner IT Symposium Xpo 2019.
28
29### AT&T Mobility & Entertainment *-- El Segundo, CA*
30#### Lead Product Marketing Manager, Video Lifecycle Strategy *-- June 2017-November 2018*
31Built analytical models and business cases for strategic project analysis. Managed enablement projects for product go-to-market, ensuring ancillary systems and processes worked correctly for customers.
32* Built the analytical model and business case that got Netflix on AT&T TV’s upcoming streaming box. Built analytical scenario model comparing multiple levels of integration with Netflix. Collaborated across teams in Marketing and Corporate Strategy to align on assumptions, key takeaways, and to build the executive presentation deck.
33* Developed product sunsetting strategy for AT&T’s IPTV service. Built a yearly profitability model for the IPTV service. Used existing subscriber and revenue as well as churn and churn acceleration projections to project revenue 5 years forward. Collaborated with engineering to build a 5-year IPTV cost projection model.
34* Managed product enablement projects. Insured customer-impacting issues did not occur because of internal, cross-team coordination issues. (e.g. Got a customer authentication feature for AT&T TV’s upcoming streaming box in the product backlog, engaged a 3rd party to provide at-home delivery for AT&T TV’s upcoming streaming box, etc.).
35* Built profitability models, business cases, and recommendations for VP and SVP-initiated innovation proposals (e.g. TV service for Cricket Mobile customers, a hyperlocal video service, etc.).
36
37### Deloitte Digital / Deloitte Consulting (*Los Angeles, CA*)
38#### Manager, Consulting *-- August 2016-June 2017*
39Trusted partner to clients, consulting at Fox for ~2 years. Led teams to create, expand, and maintain custom-built business applications for a diverse set of companies, from startup (Human Longevity Inc.) to Fortune 10 (AT&T).
40* Led expansion and maintenance of Fox Theatrical’s global release planning application. Managed two different teams of onshore and offshore consultants and developers through two major expansion projects. Led a cross-functional teams of marketing users, IT leadership, and our consultants through the entire SDLC for each project.
41* Served as a trusted client partner. Traveled with my primary client at Fox to Europe and Asia to introduce Global Marketing to major updates of the global release planning application and promote application use in non-Domestic regions. Also used these trips to gather first-hand feedback on the application that informed future features and fixes.
42* Recognized with “Extraordinary Moments” award from company leadership for being a company-wide top performer at my level multiple years consecutively.
43#### Senior Consultant *-- July 2014-August 2016*
44* Managed tech consulting projects for a variety of companies. Led teams of business analysts, consultants, and developers through project design and delivery (e.g. Led a team through development of a digital marketing transformation roadmap for a major grocery wholesaler, led a team through the deployment of a tailored Salesforce CRM application for a biotech startup, etc.).
45#### Summer Associate (*June 2013-August 2013*)
46* Defined program management processes and quality management plan for a multi-project, customer service enhancement program at T-Mobile. Established PMO reporting templates and cadence.
47
48### National Technical Systems *-- Calabasas, CA*
49#### Microsoft Dynamics ERP Administrator *-- June 2009-July 2012*
50Led a cross-company ERP implementation serving 600+ business users at 20+ facilities. Managed 2 internal developers and 7 consultants through design, development, and delivery of system customizations.
51* Led cross-functional team of business users from finance, sales, marketing, operations, and IT; developers; and consultants to consolidate fragmented CRM, sales, and financial systems into a single ERP system.
52* Owned yearly project CapEx budget for the ERP system. Worked with CIO to build yearly CapEx requests. Managed hardware purchases, software licensing, 3rd party add-on licensing, and consulting costs.
53
54### Timber Products Company *-- Springfield, OR*
55#### Computer Programmer / Analyst *-- June 2007-June 2009*
56Collaborated with business users from inventory management, sales, logistics, accounting, and finance to design, development, and delivery of enhancements to the company’s Microsoft Dynamics ERP system.
57
58### ViewPlus Technologies *-- Corvallis, OR*
59#### Product Development Assistant *-- November 2004-June 2007*
60Developed testing protocols for and tested Braille printer drivers and firmware for release. Developed a text-to-Braille application for release.
61
62## Education
63
64### University of Southern California
65#### MBA, Masters of Business Administration *-- Graduated May 2014*
66* Beta Gamma Sigma, Business Honor Society.
67* Program Lead, Marshall Case Competition Program.
68 * Trained fellow MBA candidates on how to compete in case competitions, and coached case competition teams. Program achieved 89% participation rate for the class of 2015.
69* Asia-Pacific Economic Cooperation (APEC), Business Advisory Council Research Project. Foreign Direct Investment across APEC: Impediments and Opportunities for Improvement.
70 * Large, 12-person, MBA research project sponsored by and presented to APEC.
71 * Led data analysis team. Sourced and sanitized data, built SQL database, developed data analysis methodology, and led team of 3 MBA candidates though creation of country data analysis slides.
72* Director of Technology Consulting, Marshall Consulting & Strategy Club.
73* Business of Entertainment Graduate Certificate from the USC School of Cinematic Arts.
74
75### Oregon State University
76#### BS, Computer Science *-- Graduated June 2007*
77* Cum Laude.
78* Upsilon Pi Epsilon, Computer Science Honor Society.
79Other

I also want to change the default behavior of posts to not have a hero image if one isn’t defined, like in the resume post.

  • Shadow the Article.Hero.tsx section component
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/sections/article/'
    • cp -R './node_modules/@narative/gatsby-theme-novela/src/sections/article/' './src/@narative/gatsby-theme-novela/sections/article/'

Yes, you have to shadow all the damn files in the folder even though you really only want to change one.”

  • Open ‘./src/@narative/gatsby-theme-novela/sections/article/Article.Hero.tsx’
  • Update to not render hero image if none is specified and save. Here’s mine.
1import React from 'react';
2import styled from '@emotion/styled';
3
4import Headings from '@components/Headings';
5import Image, { ImagePlaceholder } from '@components/Image';
6
7import mediaqueries from '@styles/media';
8import { IArticle, IAuthor } from '@types';
9
10import ArticleAuthors from './Article.Authors';
11
12interface ArticleHeroProps {
13 article: IArticle;
14 authors: IAuthor[];
15}
16
17const ArticleHero: React.FC<ArticleHeroProps> = ({ article, authors }) => {
18 const hasCoAUthors = authors.length > 1;
19 const hasHeroImage =
20 article.hero &&
21 Object.keys(article.hero.full).length !== 0 &&
22 article.hero.full.constructor === Object;
23
24 return (
25 <Hero>
26 <Header>
27 <HeroHeading>{article.title}</HeroHeading>
28 <HeroSubtitle hasCoAUthors={hasCoAUthors}>
29 <ArticleAuthors authors={authors} />
30 <ArticleMeta hasCoAUthors={hasCoAUthors}>
31 {article.date} · {article.timeToRead} min read
32 </ArticleMeta>
33 </HeroSubtitle>
34 </Header>
35 <HeroImage id="ArticleImage__Hero">
36 {hasHeroImage && (
37 <Image src={article.hero.full} />
38 )}
39 </HeroImage>
40 </Hero>
41 );
42};
43
44export default ArticleHero;
45
46const Hero = styled.div`
47 ${p => mediaqueries.phablet`
48 &::before {
49 content: "";
50 width: 100%;
51 height: 20px;
52 background: ${p.theme.colors.primary};
53 position: absolute;
54 left: 0;
55 top: 0;
56 transition: ${p.theme.colorModeTransition};
57 }
58
59 &::after {
60 content: "";
61 width: 100%;
62 height: 10px;
63 background: ${p.theme.colors.background};
64 position: absolute;
65 left: 0;
66 top: 10px;
67 border-top-left-radius: 25px;
68 border-top-right-radius: 25px;
69 transition: ${p.theme.colorModeTransition};
70 }
71 `}
72`;
73
74const ArticleMeta = styled.div<{ hasCoAUthors: boolean }>`
75 margin-left: ${p => (p.hasCoAUthors ? '10px' : '0')};
76
77 ${mediaqueries.phablet`
78 margin-left: 0;
79 `}
80`;
81
82/* Gavin 20200722: Reduced bottom margin on the Header because it looked too
83 big w/ no hero */
84const Header = styled.header`
85 position: relative;
86 z-index: 10;
87 margin:50px auto 120px;
88 padding-left: 68px;
89 max-width: 749px;
90
91 ${mediaqueries.desktop`
92 padding-left: 53px;
93 max-width: calc(507px + 53px);
94 margin: 50px auto 70px;
95 `}
96
97 ${mediaqueries.tablet`
98 padding-left: 0;
99 margin: 50px auto 70px;
100 max-width: 480px;
101 `}
102
103 ${mediaqueries.phablet`
104 margin: 50px auto 180px;
105 padding: 0 40px;
106 `}
107
108 @media screen and (max-height: 700px) {
109 margin: 50px auto;
110 }
111`;
112
113const HeroHeading = styled(Headings.h1)`
114 font-size: 48px;
115 font-family: ${p => p.theme.fonts.serif};
116 margin-bottom: 25px;
117 font-weight: bold;
118 line-height: 1.32;
119
120 ${mediaqueries.tablet`
121 margin-bottom: 20px;
122 font-size: 36px;
123 `}
124
125 ${mediaqueries.phablet`
126 font-size: 32px;
127 `}
128`;
129
130const HeroSubtitle = styled.div<{ hasCoAUthors: boolean }>`
131 position: relative;
132 display: flex;
133 font-size: 18px;
134 color: ${p => p.theme.colors.grey};
135
136 ${p => mediaqueries.phablet`
137 font-size: 14px;
138 flex-direction: column;
139
140 ${p.hasCoAUthors &&
141 `
142 &::before {
143 content: '';
144 position: absolute;
145 left: -20px;
146 right: -20px;
147 top: -10px;
148 bottom: -10px;
149 border: 1px solid ${p.theme.colors.horizontalRule};
150 opacity: 0.5;
151 border-radius: 5px;
152 }
153 `}
154
155
156 strong {
157 display: block;
158 font-weight: 500;
159 margin-bottom: 5px;
160 }
161 `}
162`;
163
164/* Gavin 20200724: Changed from 'height: 220px' to 'max-width: 100%' for phablet */
165const HeroImage = styled.div`
166 position: relative;
167 z-index: 1;
168 width: 100%;
169 max-width: 944px;
170 overflow: hidden;
171 margin: 0 auto;
172 box-shadow: 0 30px 60px -10px rgba(0, 0, 0, 0.2),
173 0 18px 36px -18px rgba(0, 0, 0, 0.22);
174
175 ${mediaqueries.tablet`
176 max-width: 100%;
177 `}
178
179 ${mediaqueries.phablet`
180 margin: 0 auto;
181 max-width: 100%;
182
183 & > div {
184 max-width: 100%;
185 }
186`}
187`;

Now I need to create a link to the resume in the navigation. I’m throwing in a Home L1 as well, because it looks weird without one.

  • Shadow Navigation.Header.tsx component
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/components/Navigation/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/components/Navigation/Navigation.Header.tsx' './src/@narative/gatsby-theme-novela/components/Navigation/Navigation.Header.tsx'
    • Open ./src/@narative/gatsby-theme-novela/components/Navigation/Navigation.Header.tsx
    • Hardcode and style (I know, I’m lazy) the navigation links and save. Here’s mine.
1import React, { useState, useEffect } from "react";
2import styled from "@emotion/styled";
3import { Link, navigate, graphql, useStaticQuery } from "gatsby";
4import { useColorMode } from "theme-ui";
5
6import Section from "@components/Section";
7import Logo from "@components/Logo";
8
9import Icons from "@icons";
10import mediaqueries from "@styles/media";
11import {
12 copyToClipboard,
13 getWindowDimensions,
14 getBreakpointFromTheme,
15} from "@utils";
16
17const siteQuery = graphql`
18 {
19 sitePlugin(name: { eq: "@narative/gatsby-theme-novela" }) {
20 pluginOptions {
21 rootPath
22 basePath
23 }
24 }
25 }
26`;
27
28const DarkModeToggle: React.FC<{}> = () => {
29 const [colorMode, setColorMode] = useColorMode();
30 const isDark = colorMode === `dark`;
31
32 function toggleColorMode(event) {
33 event.preventDefault();
34 setColorMode(isDark ? `light` : `dark`);
35 }
36
37 return (
38 <IconWrapper
39 isDark={isDark}
40 onClick={toggleColorMode}
41 data-a11y="false"
42 aria-label={isDark ? "Activate light mode" : "Activate dark mode"}
43 title={isDark ? "Activate light mode" : "Activate dark mode"}
44 >
45 <MoonOrSun isDark={isDark} />
46 <MoonMask isDark={isDark} />
47 </IconWrapper>
48 );
49};
50
51const SharePageButton: React.FC<{}> = () => {
52 const [hasCopied, setHasCopied] = useState<boolean>(false);
53 const [colorMode] = useColorMode();
54 const isDark = colorMode === `dark`;
55 const fill = isDark ? "#fff" : "#000";
56
57 function copyToClipboardOnClick() {
58 if (hasCopied) return;
59
60 copyToClipboard(window.location.href);
61 setHasCopied(true);
62
63 setTimeout(() => {
64 setHasCopied(false);
65 }, 1000);
66 }
67
68 return (
69 <IconWrapper
70 isDark={isDark}
71 onClick={copyToClipboardOnClick}
72 data-a11y="false"
73 aria-label="Copy URL to clipboard"
74 title="Copy URL to clipboard"
75 >
76 <Icons.Link fill={fill} />
77 <ToolTip isDark={isDark} hasCopied={hasCopied}>
78 Copied
79 </ToolTip>
80 </IconWrapper>
81 );
82};
83
84const NavigationHeader: React.FC<{}> = () => {
85 const [showBackArrow, setShowBackArrow] = useState<boolean>(false);
86 const [previousPath, setPreviousPath] = useState<string>("/");
87 const { sitePlugin } = useStaticQuery(siteQuery);
88
89 const [colorMode] = useColorMode();
90 const fill = colorMode === "dark" ? "#fff" : "#000";
91 const { rootPath, basePath } = sitePlugin.pluginOptions;
92
93 useEffect(() => {
94 const { width } = getWindowDimensions();
95 const phablet = getBreakpointFromTheme("phablet");
96
97 const prev = localStorage.getItem("previousPath");
98 const previousPathWasHomepage =
99 prev === (rootPath || basePath) || (prev && prev.includes("/page/"));
100 const currentPathIsHomepage =
101 location.pathname === (rootPath || basePath) || location.pathname.includes("/page/");
102
103 setShowBackArrow(
104 previousPathWasHomepage && !currentPathIsHomepage && width <= phablet,
105 );
106 setPreviousPath(prev);
107 }, []);
108
109 return (
110 <Section>
111 <NavContainer>
112 <LogoLink
113 to={rootPath || basePath}
114 data-a11y="false"
115 title="Navigate back to the homepage"
116 aria-label="Navigate back to the homepage"
117 back={showBackArrow ? "true" : "false"}
118 >
119 {showBackArrow && (
120 <BackArrowIconContainer>
121 <Icons.ChevronLeft fill={fill} />
122 </BackArrowIconContainer>
123 )}
124 <Logo fill={fill} />
125 <Hidden>Navigate back to the homepage</Hidden>
126 {/* Gavin 20200719: Lazy ass navigation links */}
127 <div style={{marginLeft: '25px', marginRight: '25px'}}><Link to="/" style={{fontWeight: 'bolder', color: fill}}>Home</Link></div>
128 <div style={{marginLeft: '25px', marginRight: '25px'}}><Link to="/gavin-johnson-resume" style={{fontWeight: 'bolder', color: fill}}>Resume</Link></div>
129 </LogoLink>
130 <NavControls>
131 {showBackArrow ? (
132 <button
133 onClick={() => navigate(previousPath)}
134 title="Navigate back to the homepage"
135 aria-label="Navigate back to the homepage"
136 >
137 <Icons.Ex fill={fill} />
138 </button>
139 ) : (
140 <>
141 <SharePageButton />
142 <DarkModeToggle />
143 </>
144 )}
145 </NavControls>
146 </NavContainer>
147 </Section>
148 );
149};
150
151export default NavigationHeader;
152
153const BackArrowIconContainer = styled.div`
154 transition: 0.2s transform var(--ease-out-quad);
155 opacity: 0;
156 padding-right: 30px;
157 animation: fadein 0.3s linear forwards;
158
159 @keyframes fadein {
160 to {
161 opacity: 1;
162 }
163 }
164
165 ${mediaqueries.desktop_medium`
166 display: none;
167 `}
168`;
169
170const NavContainer = styled.div`
171 position: relative;
172 z-index: 100;
173 padding-top: 100px;
174 display: flex;
175 justify-content: space-between;
176
177 ${mediaqueries.desktop_medium`
178 padding-top: 50px;
179 `};
180
181 @media screen and (max-height: 800px) {
182 padding-top: 50px;
183 }
184`;
185
186const LogoLink = styled(Link)<{ back: string }>`
187 position: relative;
188 display: flex;
189 align-items: center;
190 left: ${p => (p.back === "true" ? "-54px" : 0)};
191
192 ${mediaqueries.desktop_medium`
193 left: 0
194 `}
195
196 &[data-a11y="true"]:focus::after {
197 content: "";
198 position: absolute;
199 left: -10%;
200 top: -30%;
201 width: 120%;
202 height: 160%;
203 border: 2px solid ${p => p.theme.colors.accent};
204 background: rgba(255, 255, 255, 0.01);
205 border-radius: 5px;
206 }
207
208 &:hover {
209 ${BackArrowIconContainer} {
210 transform: translateX(-3px);
211 }
212 }
213`;
214
215const NavControls = styled.div`
216 position: relative;
217 display: flex;
218 align-items: center;
219
220 ${mediaqueries.phablet`
221 right: -5px;
222 `}
223`;
224
225const ToolTip = styled.div<{ isDark: boolean; hasCopied: boolean }>`
226 position: absolute;
227 padding: 4px 13px;
228 background: ${p => (p.isDark ? "#000" : "rgba(0,0,0,0.1)")};
229 color: ${p => (p.isDark ? "#fff" : "#000")};
230 border-radius: 5px;
231 font-size: 14px;
232 top: -35px;
233 opacity: ${p => (p.hasCopied ? 1 : 0)};
234 transform: ${p => (p.hasCopied ? "translateY(-3px)" : "none")};
235 transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
236
237 &::after {
238 content: "";
239 position: absolute;
240 left: 0;
241 right: 0;
242 bottom: -6px;
243 margin: 0 auto;
244 width: 0;
245 height: 0;
246 border-left: 6px solid transparent;
247 border-right: 6px solid transparent;
248 border-top: 6px solid ${p => (p.isDark ? "#000" : "rgba(0,0,0,0.1)")};
249 }
250`;
251
252const IconWrapper = styled.button<{ isDark: boolean }>`
253 opacity: 0.5;
254 position: relative;
255 border-radius: 5px;
256 width: 40px;
257 height: 25px;
258 display: flex;
259 align-items: center;
260 justify-content: center;
261 transition: opacity 0.3s ease;
262 margin-left: 30px;
263
264 &:hover {
265 opacity: 1;
266 }
267
268 &[data-a11y="true"]:focus::after {
269 content: "";
270 position: absolute;
271 left: 0;
272 top: -30%;
273 width: 100%;
274 height: 160%;
275 border: 2px solid ${p => p.theme.colors.accent};
276 background: rgba(255, 255, 255, 0.01);
277 border-radius: 5px;
278 }
279
280 ${mediaqueries.tablet`
281 display: inline-flex;
282 transform: scale(0.708);
283 margin-left: 10px;
284
285
286 &:hover {
287 opacity: 0.5;
288 }
289 `}
290`;
291
292// This is based off a codepen! Much appreciated to: https://codepen.io/aaroniker/pen/KGpXZo
293const MoonOrSun = styled.div<{ isDark: boolean }>`
294 position: relative;
295 width: 24px;
296 height: 24px;
297 border-radius: 50%;
298 border: ${p => (p.isDark ? "4px" : "2px")} solid
299 ${p => p.theme.colors.primary};
300 background: ${p => p.theme.colors.primary};
301 transform: scale(${p => (p.isDark ? 0.55 : 1)});
302 transition: all 0.45s ease;
303 overflow: ${p => (p.isDark ? "visible" : "hidden")};
304
305 &::before {
306 content: "";
307 position: absolute;
308 right: -9px;
309 top: -9px;
310 height: 24px;
311 width: 24px;
312 border: 2px solid ${p => p.theme.colors.primary};
313 border-radius: 50%;
314 transform: translate(${p => (p.isDark ? "14px, -14px" : "0, 0")});
315 opacity: ${p => (p.isDark ? 0 : 1)};
316 transition: transform 0.45s ease;
317 }
318
319 &::after {
320 content: "";
321 width: 8px;
322 height: 8px;
323 border-radius: 50%;
324 margin: -4px 0 0 -4px;
325 position: absolute;
326 top: 50%;
327 left: 50%;
328 box-shadow: 0 -23px 0 ${p => p.theme.colors.primary},
329 0 23px 0 ${p => p.theme.colors.primary},
330 23px 0 0 ${p => p.theme.colors.primary},
331 -23px 0 0 ${p => p.theme.colors.primary},
332 15px 15px 0 ${p => p.theme.colors.primary},
333 -15px 15px 0 ${p => p.theme.colors.primary},
334 15px -15px 0 ${p => p.theme.colors.primary},
335 -15px -15px 0 ${p => p.theme.colors.primary};
336 transform: scale(${p => (p.isDark ? 1 : 0)});
337 transition: all 0.35s ease;
338
339 ${p => mediaqueries.tablet`
340 transform: scale(${p.isDark ? 0.92 : 0});
341 `}
342 }
343`;
344
345const MoonMask = styled.div<{ isDark: boolean }>`
346 position: absolute;
347 right: -1px;
348 top: -8px;
349 height: 24px;
350 width: 24px;
351 border-radius: 50%;
352 border: 0;
353 background: ${p => p.theme.colors.background};
354 transform: translate(${p => (p.isDark ? "14px, -14px" : "0, 0")});
355 opacity: ${p => (p.isDark ? 0 : 1)};
356 transition: ${p => p.theme.colorModeTransition}, transform 0.45s ease;
357`;
358
359const Hidden = styled.span`
360 position: absolute;
361 display: inline-block;
362 opacity: 0;
363 width: 0px;
364 height: 0px;
365 visibility: hidden;
366 overflow: hidden;
367`;
  • Run gatsby develop to see what your site looks like. Here’s mine.

    Alt Text

  • Click on the Resume L1 menu item to go to the Resume page. Here’s mine.

    Alt Text

Now that we’ve gotten rid of the hero image, there is a gap between the author info and the resume body that I don’t like. So let’s fix that.

  • Shadow the article.template.tsx template
    • In your root directory, mkdir -p './src/@narative/gatsby-theme-novela/templates/'
    • cp './node_modules/@narative/gatsby-theme-novela/src/templates/article.template.tsx' './src/@narative/gatsby-theme-novela/templates/article.template.tsx'
    • Open ‘./src/@narative/gatsby-theme-novela/templates/article.template.tsx’
    • Update the top padding on ArticleBody and save. Here’s mine.
1import React, { useRef, useState, useEffect } from "react";
2import styled from "@emotion/styled";
3import throttle from "lodash/throttle";
4import { graphql, useStaticQuery } from "gatsby";
5
6import Layout from "@components/Layout";
7import MDXRenderer from "@components/MDX";
8import Progress from "@components/Progress";
9import Section from "@components/Section";
10import Subscription from "@components/Subscription";
11
12import mediaqueries from "@styles/media";
13import { debounce } from "@utils";
14
15import ArticleAside from "../sections/article/Article.Aside";
16import ArticleHero from "../sections/article/Article.Hero";
17import ArticleControls from "../sections/article/Article.Controls";
18import ArticlesNext from "../sections/article/Article.Next";
19import ArticleSEO from "../sections/article/Article.SEO";
20import ArticleShare from "../sections/article/Article.Share";
21
22import { Template } from "@types";
23
24const siteQuery = graphql`
25 {
26 allSite {
27 edges {
28 node {
29 siteMetadata {
30 name
31 }
32 }
33 }
34 }
35 }
36`;
37
38const Article: Template = ({ pageContext, location }) => {
39 const contentSectionRef = useRef<HTMLElement>(null);
40
41 const [hasCalculated, setHasCalculated] = useState<boolean>(false);
42 const [contentHeight, setContentHeight] = useState<number>(0);
43
44 const results = useStaticQuery(siteQuery);
45 const name = results.allSite.edges[0].node.siteMetadata.name;
46
47 const { article, authors, mailchimp, next } = pageContext;
48
49 useEffect(() => {
50 const calculateBodySize = throttle(() => {
51 const contentSection = contentSectionRef.current;
52
53 if (!contentSection) return;
54
55 /**
56 * If we haven't checked the content's height before,
57 * we want to add listeners to the content area's
58 * imagery to recheck when it's loaded
59 */
60 if (!hasCalculated) {
61 const debouncedCalculation = debounce(calculateBodySize);
62 const $imgs = contentSection.querySelectorAll("img");
63
64 $imgs.forEach($img => {
65 // If the image hasn't finished loading then add a listener
66 if (!$img.complete) $img.onload = debouncedCalculation;
67 });
68
69 // Prevent rerun of the listener attachment
70 setHasCalculated(true);
71 }
72
73 // Set the height and offset of the content area
74 setContentHeight(contentSection.getBoundingClientRect().height);
75 }, 20);
76
77 calculateBodySize();
78 window.addEventListener("resize", calculateBodySize);
79
80 return () => window.removeEventListener("resize", calculateBodySize);
81 }, []);
82
83 return (
84 <Layout>
85 <ArticleSEO article={article} authors={authors} location={location} />
86 <ArticleHero article={article} authors={authors} />
87 <ArticleAside contentHeight={contentHeight}>
88 <Progress contentHeight={contentHeight} />
89 </ArticleAside>
90 <MobileControls>
91 <ArticleControls />
92 </MobileControls>
93 <ArticleBody ref={contentSectionRef}>
94 <MDXRenderer content={article.body}>
95 <ArticleShare />
96 </MDXRenderer>
97 </ArticleBody>
98 {mailchimp && article.subscription && <Subscription />}
99 {next.length > 0 && (
100 <NextArticle narrow>
101 <FooterNext>More articles from {name}</FooterNext>
102 <ArticlesNext articles={next} />
103 <FooterSpacer />
104 </NextArticle>
105 )}
106 </Layout>
107 );
108};
109
110export default Article;
111
112const MobileControls = styled.div`
113 position: relative;
114 padding-top: 60px;
115 transition: background 0.2s linear;
116 text-align: center;
117
118 ${mediaqueries.tablet_up`
119 display: none;
120 `}
121`;
122
123/* Gavin 20200722: Reduced top padding on the ArticleBody because it looked too
124 big w/ no hero */
125const ArticleBody = styled.article`
126 position: relative;
127 padding: 50px 0 35px;
128 padding-left: 68px;
129 transition: background 0.2s linear;
130
131 ${mediaqueries.desktop`
132 padding-left: 53px;
133 `}
134
135 ${mediaqueries.tablet`
136 padding: 25px 0 80px;
137 `}
138
139 ${mediaqueries.phablet`
140 padding: 20px 0;
141 `}
142`;
143
144const NextArticle = styled(Section)`
145 display: block;
146`;
147
148const FooterNext = styled.h3`
149 position: relative;
150 opacity: 0.25;
151 margin-bottom: 100px;
152 font-weight: 400;
153 color: ${p => p.theme.colors.primary};
154
155 ${mediaqueries.tablet`
156 margin-bottom: 60px;
157 `}
158
159 &::after {
160 content: '';
161 position: absolute;
162 background: ${p => p.theme.colors.grey};
163 width: ${(910 / 1140) * 100}%;
164 height: 1px;
165 right: 0;
166 top: 11px;
167
168 ${mediaqueries.tablet`
169 width: ${(600 / 1140) * 100}%;
170 `}
171
172 ${mediaqueries.phablet`
173 width: ${(400 / 1140) * 100}%;
174 `}
175
176 ${mediaqueries.phone`
177 width: 90px
178 `}
179 }
180`;
181
182const FooterSpacer = styled.div`
183 margin-bottom: 65px;
184`;
  • Open ‘./src/@narative/gatsby-theme-novela/templates/article.template.tsx’
    • Reduce the bottom margin on Header and save. Here’s mine.
1import React from 'react';
2import styled from '@emotion/styled';
3
4import Headings from '@components/Headings';
5import Image, { ImagePlaceholder } from '@components/Image';
6
7import mediaqueries from '@styles/media';
8import { IArticle, IAuthor } from '@types';
9
10import ArticleAuthors from './Article.Authors';
11
12interface ArticleHeroProps {
13 article: IArticle;
14 authors: IAuthor[];
15}
16
17const ArticleHero: React.FC<ArticleHeroProps> = ({ article, authors }) => {
18 const hasCoAUthors = authors.length > 1;
19 const hasHeroImage =
20 article.hero &&
21 Object.keys(article.hero.full).length !== 0 &&
22 article.hero.full.constructor === Object;
23
24 return (
25 <Hero>
26 <Header>
27 <HeroHeading>{article.title}</HeroHeading>
28 <HeroSubtitle hasCoAUthors={hasCoAUthors}>
29 <ArticleAuthors authors={authors} />
30 <ArticleMeta hasCoAUthors={hasCoAUthors}>
31 {article.date} · {article.timeToRead} min read
32 </ArticleMeta>
33 </HeroSubtitle>
34 </Header>
35 <HeroImage id="ArticleImage__Hero">
36 {hasHeroImage && (
37 <Image src={article.hero.full} />
38 )}
39 </HeroImage>
40 </Hero>
41 );
42};
43
44export default ArticleHero;
45
46const Hero = styled.div`
47 ${p => mediaqueries.phablet`
48 &::before {
49 content: "";
50 width: 100%;
51 height: 20px;
52 background: ${p.theme.colors.primary};
53 position: absolute;
54 left: 0;
55 top: 0;
56 transition: ${p.theme.colorModeTransition};
57 }
58
59 &::after {
60 content: "";
61 width: 100%;
62 height: 10px;
63 background: ${p.theme.colors.background};
64 position: absolute;
65 left: 0;
66 top: 10px;
67 border-top-left-radius: 25px;
68 border-top-right-radius: 25px;
69 transition: ${p.theme.colorModeTransition};
70 }
71 `}
72`;
73
74const ArticleMeta = styled.div<{ hasCoAUthors: boolean }>`
75 margin-left: ${p => (p.hasCoAUthors ? '10px' : '0')};
76
77 ${mediaqueries.phablet`
78 margin-left: 0;
79 `}
80`;
81
82/* Gavin 20200722: Reduced bottom margin on the Header because it looked too
83 big w/ no hero */
84const Header = styled.header`
85 position: relative;
86 z-index: 10;
87 margin:50px auto 120px;
88 padding-left: 68px;
89 max-width: 749px;
90
91 ${mediaqueries.desktop`
92 padding-left: 53px;
93 max-width: calc(507px + 53px);
94 margin: 50px auto 70px;
95 `}
96
97 ${mediaqueries.tablet`
98 padding-left: 0;
99 margin: 50px auto 70px;
100 max-width: 480px;
101 `}
102
103 ${mediaqueries.phablet`
104 margin: 50px auto 180px;
105 padding: 0 40px;
106 `}
107
108 @media screen and (max-height: 700px) {
109 margin: 50px auto;
110 }
111`;
112
113const HeroHeading = styled(Headings.h1)`
114 font-size: 48px;
115 font-family: ${p => p.theme.fonts.serif};
116 margin-bottom: 25px;
117 font-weight: bold;
118 line-height: 1.32;
119
120 ${mediaqueries.tablet`
121 margin-bottom: 20px;
122 font-size: 36px;
123 `}
124
125 ${mediaqueries.phablet`
126 font-size: 32px;
127 `}
128`;
129
130const HeroSubtitle = styled.div<{ hasCoAUthors: boolean }>`
131 position: relative;
132 display: flex;
133 font-size: 18px;
134 color: ${p => p.theme.colors.grey};
135
136 ${p => mediaqueries.phablet`
137 font-size: 14px;
138 flex-direction: column;
139
140 ${p.hasCoAUthors &&
141 `
142 &::before {
143 content: '';
144 position: absolute;
145 left: -20px;
146 right: -20px;
147 top: -10px;
148 bottom: -10px;
149 border: 1px solid ${p.theme.colors.horizontalRule};
150 opacity: 0.5;
151 border-radius: 5px;
152 }
153 `}
154
155
156 strong {
157 display: block;
158 font-weight: 500;
159 margin-bottom: 5px;
160 }
161 `}
162`;
163
164const HeroImage = styled.div`
165 position: relative;
166 z-index: 1;
167 width: 100%;
168 max-width: 944px;
169 overflow: hidden;
170 margin: 0 auto;
171 box-shadow: 0 30px 60px -10px rgba(0, 0, 0, 0.2),
172 0 18px 36px -18px rgba(0, 0, 0, 0.22);
173
174 ${mediaqueries.tablet`
175 max-width: 100%;
176 `}
177
178 ${mediaqueries.phablet`
179 margin: 0 auto;
180 width: calc(100vw - 40px);
181 height: 220px;
182
183 & > div {
184 height: 220px;
185 }
186`}
187`;
  • Run gatsby develop and click on the Resume L1 menu item to go to the Resume page. Here’s mine.
    Alt Text

Much better. Now let’s commit your changes to your repo.

Merge your changes into your repo

  • Add all new files
    • git add --all
  • Commit your changes
    • git commit -a -m "[your commit message]"
  • Push your branch to your repo on GitHub
    • git push -u origin initial_customizations

Boom! Your branch and changes have been saved to your repo. Now create a pull request on GitHub and merge your changes into the main branch.

If you got an “Authentication failed…” error even though you entered your GitHub username and password correctly, you may have to create and use a personal access token. Read this Medium blog for details on how to do that.

  • In your repo on GitHub, click on the “Pull Requests” tab
    Alt Text
  • Click on the “New pull request” button
    Alt Text
  • Click the “Compare” dropdown and select the initial_customizations branch
    Alt Text
  • Compare your changes
  • When you are good with all of the change made, click the “Create pull request” button
    Alt Text
  • Write your PR notes and click the “Create pull request” button
    Alt Text
  • Click the “Merge pull request” button
    Alt Text
  • Click the “Confirm merge” button
    Alt Text
    You should see “Pull request successfully merged and closed.”
  • In Terminal, git checkout main

Boom! Changes have been merged, and you are back on the main branch, ready to move forward.

That’s it for today. I’ll post tutorial next week on how to deploy your site to Firebase and maybe how to automate deploys with GitHub Actions. We’ll see what I have time to get around to.

More articles from thtmnisamnstr

Building a Portfolio/Resume Site with Gatsby, Part 1 - Setting up your environment and creating a local, themed site

My personal website sucks. So I'm going to update it using Gatsby.

July 20th, 2020 · 2 min read

Adapting Code and Culture: How New Relic Went Open Source

To become a community-friendly, open source partner, we needed to transform how we create our instrumentation. Here’s how we did it.

October 16th, 2020 · 9 min read
© 2020 thtmnisamnstr
Link to $https://twitter.com/gavinjtechLink to $https://dev.to/thtmnisamnstrLink to $https://github.com/thtmnisamnstrLink to $https://www.linkedin.com/in/gavin-johnson/Link to $https://www.instagram.com/thtmnisamnstr