├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── LICENSE ├── README.md ├── components ├── Card.js ├── Footer.js ├── Image.js ├── LayoutWrapper.js ├── Link.js ├── MDXComponents.js ├── MobileNav.js ├── NewsletterForm.js ├── PageTitle.js ├── Pagination.js ├── Pre.js ├── SEO.js ├── ScrollTopAndComment.js ├── SectionContainer.js ├── TOCInline.js ├── Tag.js ├── ThemeSwitch.js ├── Tweet.js ├── Tweets.js ├── analytics │ ├── GoogleAnalytics.js │ ├── Plausible.js │ ├── SimpleAnalytics.js │ └── index.js ├── comments │ ├── Disqus.js │ ├── Giscus.js │ ├── Utterances.js │ └── index.js └── social-icons │ ├── facebook.svg │ ├── github.svg │ ├── index.js │ ├── linkedin.svg │ ├── mail.svg │ ├── twitter.svg │ └── youtube.svg ├── css ├── prism.css └── tailwind.css ├── data ├── authors │ ├── default.md │ └── sparrowhawk.md ├── blog │ ├── 2021.mdx │ ├── 2022.mdx │ ├── build-or-sell.mdx │ ├── building-a-community.mdx │ ├── devrel.mdx │ ├── leading-devrel.mdx │ ├── naval-almanack.mdx │ ├── scrum-master.mdx │ ├── standout-dev-review.mdx │ ├── startup-journey.mdx │ ├── talent-stack.mdx │ ├── time.mdx │ ├── twitter-strategy.mdx │ └── vercel.mdx ├── h.svg ├── headerNavLinks.js ├── logo.svg ├── projectsData.js └── siteMetadata.js ├── jsconfig.json ├── layouts ├── AuthorLayout.js ├── ListLayout.js ├── PostLayout.js └── PostSimple.js ├── lib ├── generate-rss.js ├── mdx.js ├── remark-code-title.js ├── remark-img-to-jsx.js ├── remark-toc-headings.js ├── tags.js ├── twitter.js └── utils │ ├── files.js │ ├── formatDate.js │ ├── htmlEscaper.js │ └── kebabCase.js ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 404.js ├── [...slug].js ├── _app.js ├── _document.js ├── about.js ├── api │ └── getTweets.js ├── blog.js ├── index.js ├── page │ └── [page].js ├── tags.js ├── tags │ └── [tag].js └── tweets.js ├── postcss.config.js ├── prettier.config.js ├── public └── static │ ├── favicons │ ├── android-chrome-96x96.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── h.png │ ├── h2.png │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest │ └── images │ ├── Eisenhower.jpeg │ ├── aaa.png │ ├── avatar.png │ ├── canada │ ├── lake.jpg │ ├── maple.jpg │ ├── mountains.jpg │ └── toronto.jpg │ ├── curl.jpeg │ ├── devrel.jpeg │ ├── divio.png │ ├── email_usg.png │ ├── fig_DM.png │ ├── google.png │ ├── image.jfif │ ├── jack.jpeg │ ├── journey.png │ ├── leading-devrel.png │ ├── logo.png │ ├── me_compressed.png │ ├── ocean.jpeg │ ├── og-image.png │ ├── old-og-image.png │ ├── org.png │ ├── org_chart.PNG │ ├── pillars-of-devrel.jpeg │ ├── sparrowhawk-avatar.jpg │ ├── time-machine.jpg │ ├── twitter-alternative.jpeg │ ├── twitter-card.png │ ├── twitter-cool.jpeg │ ├── twitter-strategy.jpeg │ ├── usg_actual_chart.png │ ├── usg_org_chart.png │ ├── usg_twitter.png │ └── yearinreview.png ├── scripts ├── compose.js └── generate-sitemap.js └── tailwind.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | amd: true, 6 | node: true, 7 | es6: true, 8 | }, 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:jsx-a11y/recommended', 12 | 'plugin:prettier/recommended', 13 | 'next', 14 | 'next/core-web-vitals', 15 | ], 16 | rules: { 17 | 'prettier/prettier': 'error', 18 | 'react/react-in-jsx-scope': 'off', 19 | 'jsx-a11y/anchor-is-valid': [ 20 | 'error', 21 | { 22 | components: ['Link'], 23 | specialLink: ['hrefLeft', 'hrefRight'], 24 | aspects: ['invalidHref', 'preferButton'], 25 | }, 26 | ], 27 | 'react/prop-types': 0, 28 | 'no-unused-vars': 0, 29 | 'react/no-unescaped-entities': 0, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ## Source: https://github.com/alexkaratarakis/gitattributes 2 | ## Modified * text=auto to * text eol=lf to force LF endings. 3 | 4 | ## GITATTRIBUTES FOR WEB PROJECTS 5 | # 6 | # These settings are for any web project. 7 | # 8 | # Details per file setting: 9 | # text These files should be normalized (i.e. convert CRLF to LF). 10 | # binary These files are binary and should be left untouched. 11 | # 12 | # Note that binary is a macro for -text -diff. 13 | ###################################################################### 14 | 15 | # Auto detect 16 | ## Force LF line endings automatically for files detected as 17 | ## text and leave all files detected as binary untouched. 18 | ## This will handle all files NOT defined below. 19 | * text eol=lf 20 | 21 | # Source code 22 | *.bash text eol=lf 23 | *.bat text eol=crlf 24 | *.cmd text eol=crlf 25 | *.coffee text 26 | *.css text 27 | *.htm text diff=html 28 | *.html text diff=html 29 | *.inc text 30 | *.ini text 31 | *.js text 32 | *.json text 33 | *.jsx text 34 | *.less text 35 | *.ls text 36 | *.map text -diff 37 | *.od text 38 | *.onlydata text 39 | *.php text diff=php 40 | *.pl text 41 | *.ps1 text eol=crlf 42 | *.py text diff=python 43 | *.rb text diff=ruby 44 | *.sass text 45 | *.scm text 46 | *.scss text diff=css 47 | *.sh text eol=lf 48 | *.sql text 49 | *.styl text 50 | *.tag text 51 | *.ts text 52 | *.tsx text 53 | *.xml text 54 | *.xhtml text diff=html 55 | 56 | # Docker 57 | Dockerfile text 58 | 59 | # Documentation 60 | *.ipynb text 61 | *.markdown text 62 | *.md text 63 | *.mdwn text 64 | *.mdown text 65 | *.mkd text 66 | *.mkdn text 67 | *.mdtxt text 68 | *.mdtext text 69 | *.txt text 70 | AUTHORS text 71 | CHANGELOG text 72 | CHANGES text 73 | CONTRIBUTING text 74 | COPYING text 75 | copyright text 76 | *COPYRIGHT* text 77 | INSTALL text 78 | license text 79 | LICENSE text 80 | NEWS text 81 | readme text 82 | *README* text 83 | TODO text 84 | 85 | # Templates 86 | *.dot text 87 | *.ejs text 88 | *.haml text 89 | *.handlebars text 90 | *.hbs text 91 | *.hbt text 92 | *.jade text 93 | *.latte text 94 | *.mustache text 95 | *.njk text 96 | *.phtml text 97 | *.tmpl text 98 | *.tpl text 99 | *.twig text 100 | *.vue text 101 | 102 | # Configs 103 | *.cnf text 104 | *.conf text 105 | *.config text 106 | .editorconfig text 107 | .env text 108 | .gitattributes text 109 | .gitconfig text 110 | .htaccess text 111 | *.lock text -diff 112 | package-lock.json text -diff 113 | *.toml text 114 | *.yaml text 115 | *.yml text 116 | browserslist text 117 | Makefile text 118 | makefile text 119 | 120 | # Heroku 121 | Procfile text 122 | 123 | # Graphics 124 | *.ai binary 125 | *.bmp binary 126 | *.eps binary 127 | *.gif binary 128 | *.gifv binary 129 | *.ico binary 130 | *.jng binary 131 | *.jp2 binary 132 | *.jpg binary 133 | *.jpeg binary 134 | *.jpx binary 135 | *.jxr binary 136 | *.pdf binary 137 | *.png binary 138 | *.psb binary 139 | *.psd binary 140 | # SVG treated as an asset (binary) by default. 141 | *.svg text 142 | # If you want to treat it as binary, 143 | # use the following line instead. 144 | # *.svg binary 145 | *.svgz binary 146 | *.tif binary 147 | *.tiff binary 148 | *.wbmp binary 149 | *.webp binary 150 | 151 | # Audio 152 | *.kar binary 153 | *.m4a binary 154 | *.mid binary 155 | *.midi binary 156 | *.mp3 binary 157 | *.ogg binary 158 | *.ra binary 159 | 160 | # Video 161 | *.3gpp binary 162 | *.3gp binary 163 | *.as binary 164 | *.asf binary 165 | *.asx binary 166 | *.fla binary 167 | *.flv binary 168 | *.m4v binary 169 | *.mng binary 170 | *.mov binary 171 | *.mp4 binary 172 | *.mpeg binary 173 | *.mpg binary 174 | *.ogv binary 175 | *.swc binary 176 | *.swf binary 177 | *.webm binary 178 | 179 | # Archives 180 | *.7z binary 181 | *.gz binary 182 | *.jar binary 183 | *.rar binary 184 | *.tar binary 185 | *.zip binary 186 | 187 | # Fonts 188 | *.ttf binary 189 | *.eot binary 190 | *.otf binary 191 | *.woff binary 192 | *.woff2 binary 193 | 194 | # Executables 195 | *.exe binary 196 | *.pyc binary 197 | 198 | # RC files (like .babelrc or .eslintrc) 199 | *.*rc text 200 | 201 | # Ignore files (like .npmignore or .gitignore) 202 | *.*ignore text 203 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | public/sitemap.xml 15 | .vercel 16 | 17 | # production 18 | /build 19 | *.xml 20 | # rss feed 21 | /public/feed.xml 22 | 23 | # misc 24 | .DS_Store 25 | 26 | # debug 27 | *.log 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # local env files 33 | .env 34 | .env.local 35 | .env.development.local 36 | .env.test.local 37 | .env.production.local 38 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Timothy Lin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My blog 2 | 3 | Credit to the [Next.js Tailwind starter repo](https://github.com/timlrx/tailwind-nextjs-starter-blog) for the initial design and code. 4 | -------------------------------------------------------------------------------- /components/Card.js: -------------------------------------------------------------------------------- 1 | import Image from './Image' 2 | import Link from './Link' 3 | 4 | const Card = ({ title, description, imgSrc, href }) => ( 5 |
6 |
7 | {href ? ( 8 | 9 | {title} 16 | 17 | ) : ( 18 | {title} 25 | )} 26 |
27 |

28 | {href ? ( 29 | 30 | {title} 31 | 32 | ) : ( 33 | title 34 | )} 35 |

36 |

{description}

37 | {href && ( 38 | 43 | Learn more → 44 | 45 | )} 46 |
47 |
48 |
49 | ) 50 | 51 | export default Card 52 | -------------------------------------------------------------------------------- /components/Footer.js: -------------------------------------------------------------------------------- 1 | import siteMetadata from '@/data/siteMetadata' 2 | import SocialIcon from '@/components/social-icons' 3 | 4 | export default function Footer() { 5 | return ( 6 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /components/Image.js: -------------------------------------------------------------------------------- 1 | import NextImage from 'next/image' 2 | 3 | // eslint-disable-next-line jsx-a11y/alt-text 4 | const Image = ({ ...rest }) => 5 | 6 | export default Image 7 | -------------------------------------------------------------------------------- /components/LayoutWrapper.js: -------------------------------------------------------------------------------- 1 | import siteMetadata from '@/data/siteMetadata' 2 | import headerNavLinks from '@/data/headerNavLinks' 3 | import Logo from '@/data/h.svg' 4 | import Link from './Link' 5 | import SectionContainer from './SectionContainer' 6 | import Footer from './Footer' 7 | import MobileNav from './MobileNav' 8 | import ThemeSwitch from './ThemeSwitch' 9 | 10 | const LayoutWrapper = ({ children }) => { 11 | return ( 12 | 13 |
14 |
15 |
16 | 17 |
18 |
19 | 20 |
21 | {typeof siteMetadata.headerTitle === 'string' ? ( 22 |
23 | {siteMetadata.headerTitle} 24 |
25 | ) : ( 26 | siteMetadata.headerTitle 27 | )} 28 |
29 | 30 |
31 |
32 |
33 | {headerNavLinks.map((link) => ( 34 | 39 | {link.title} 40 | 41 | ))} 42 |
43 | 44 | 45 |
46 |
47 |
{children}
48 |
49 |
50 |
51 | ) 52 | } 53 | 54 | export default LayoutWrapper 55 | -------------------------------------------------------------------------------- /components/Link.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-has-content */ 2 | import Link from 'next/link' 3 | 4 | const CustomLink = ({ href, ...rest }) => { 5 | const isInternalLink = href && href.startsWith('/') 6 | const isAnchorLink = href && href.startsWith('#') 7 | 8 | if (isInternalLink) { 9 | return ( 10 | 11 | 12 | 13 | ) 14 | } 15 | 16 | if (isAnchorLink) { 17 | return 18 | } 19 | 20 | return 21 | } 22 | 23 | export default CustomLink 24 | -------------------------------------------------------------------------------- /components/MDXComponents.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import { useMemo } from 'react' 3 | import { getMDXComponent } from 'mdx-bundler/client' 4 | import Image from './Image' 5 | import CustomLink from './Link' 6 | import TOCInline from './TOCInline' 7 | import Pre from './Pre' 8 | import { BlogNewsletterForm } from './NewsletterForm' 9 | import Tweets from './Tweets' 10 | 11 | export const MDXComponents = { 12 | Tweets, 13 | Image, 14 | TOCInline, 15 | a: CustomLink, 16 | pre: Pre, 17 | BlogNewsletterForm: BlogNewsletterForm, 18 | wrapper: ({ components, layout, ...rest }) => { 19 | const Layout = require(`../layouts/${layout}`).default 20 | return 21 | }, 22 | } 23 | 24 | export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => { 25 | const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource]) 26 | 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /components/MobileNav.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import Link from './Link' 3 | import headerNavLinks from '@/data/headerNavLinks' 4 | 5 | const MobileNav = () => { 6 | const [navShow, setNavShow] = useState(false) 7 | 8 | const onToggleNav = () => { 9 | setNavShow((status) => { 10 | if (status) { 11 | document.body.style.overflow = 'auto' 12 | } else { 13 | // Prevent scrolling 14 | document.body.style.overflow = 'hidden' 15 | } 16 | return !status 17 | }) 18 | } 19 | 20 | return ( 21 |
22 | 49 |
54 | 60 | 73 |
74 |
75 | ) 76 | } 77 | 78 | export default MobileNav 79 | -------------------------------------------------------------------------------- /components/NewsletterForm.js: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react' 2 | 3 | import siteMetadata from '@/data/siteMetadata' 4 | 5 | const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => { 6 | const inputEl = useRef(null) 7 | const [error, setError] = useState(false) 8 | const [message, setMessage] = useState('') 9 | const [subscribed, setSubscribed] = useState(false) 10 | 11 | const subscribe = async (e) => { 12 | e.preventDefault() 13 | 14 | const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, { 15 | body: JSON.stringify({ 16 | email: inputEl.current.value, 17 | }), 18 | headers: { 19 | 'Content-Type': 'application/json', 20 | }, 21 | method: 'POST', 22 | }) 23 | 24 | const { error } = await res.json() 25 | if (error) { 26 | setError(true) 27 | setMessage('Your e-mail adress is invalid or you are already subscribed!') 28 | return 29 | } 30 | 31 | inputEl.current.value = '' 32 | setError(false) 33 | setSubscribed(true) 34 | setMessage('Successfully! 🎉 You are now subscribed.') 35 | } 36 | 37 | return ( 38 |
39 |
{title}
40 |
41 |
42 | 45 | 56 |
57 |
58 | 67 |
68 |
69 | {error && ( 70 |
{message}
71 | )} 72 |
73 | ) 74 | } 75 | 76 | export default NewsletterForm 77 | 78 | export const BlogNewsletterForm = ({ title }) => ( 79 |
80 |
81 | 82 |
83 |
84 | ) 85 | -------------------------------------------------------------------------------- /components/PageTitle.js: -------------------------------------------------------------------------------- 1 | export default function PageTitle({ children }) { 2 | return ( 3 |

4 | {children} 5 |

6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /components/Pagination.js: -------------------------------------------------------------------------------- 1 | import Link from '@/components/Link' 2 | 3 | export default function Pagination({ totalPages, currentPage }) { 4 | const prevPage = parseInt(currentPage) - 1 > 0 5 | const nextPage = parseInt(currentPage) + 1 <= parseInt(totalPages) 6 | 7 | return ( 8 |
9 | 34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /components/Pre.js: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react' 2 | 3 | const Pre = (props) => { 4 | const textInput = useRef(null) 5 | const [hovered, setHovered] = useState(false) 6 | const [copied, setCopied] = useState(false) 7 | 8 | const onEnter = () => { 9 | setHovered(true) 10 | } 11 | const onExit = () => { 12 | setHovered(false) 13 | setCopied(false) 14 | } 15 | const onCopy = () => { 16 | setCopied(true) 17 | navigator.clipboard.writeText(textInput.current.textContent) 18 | setTimeout(() => { 19 | setCopied(false) 20 | }, 2000) 21 | } 22 | 23 | return ( 24 |
25 | {hovered && ( 26 | 64 | )} 65 | 66 |
{props.children}
67 |
68 | ) 69 | } 70 | 71 | export default Pre 72 | -------------------------------------------------------------------------------- /components/SEO.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import { useRouter } from 'next/router' 3 | import siteMetadata from '@/data/siteMetadata' 4 | 5 | const CommonSEO = ({ title, description, ogType, ogImage, twImage }) => { 6 | const router = useRouter() 7 | return ( 8 | 9 | {title} 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {ogImage.constructor.name === 'Array' ? ( 18 | ogImage.map(({ url }) => ) 19 | ) : ( 20 | 21 | )} 22 | 23 | 24 | 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | export const PageSEO = ({ title, description }) => { 32 | const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner 33 | const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner 34 | return ( 35 | 42 | ) 43 | } 44 | 45 | export const TagSEO = ({ title, description }) => { 46 | const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner 47 | const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner 48 | const router = useRouter() 49 | return ( 50 | <> 51 | 58 | 59 | 65 | 66 | 67 | ) 68 | } 69 | 70 | export const BlogSEO = ({ authorDetails, title, summary, date, lastmod, url, images = [] }) => { 71 | const router = useRouter() 72 | const publishedAt = new Date(date).toISOString() 73 | const modifiedAt = new Date(lastmod || date).toISOString() 74 | let imagesArr = 75 | images.length === 0 76 | ? [siteMetadata.socialBanner] 77 | : typeof images === 'string' 78 | ? [images] 79 | : images 80 | 81 | const featuredImages = imagesArr.map((img) => { 82 | return { 83 | '@type': 'ImageObject', 84 | url: `${siteMetadata.siteUrl}${img}`, 85 | } 86 | }) 87 | 88 | let authorList 89 | if (authorDetails) { 90 | authorList = authorDetails.map((author) => { 91 | return { 92 | '@type': 'Person', 93 | name: author.name, 94 | } 95 | }) 96 | } else { 97 | authorList = { 98 | '@type': 'Person', 99 | name: siteMetadata.author, 100 | } 101 | } 102 | 103 | const structuredData = { 104 | '@context': 'https://schema.org', 105 | '@type': 'Article', 106 | mainEntityOfPage: { 107 | '@type': 'WebPage', 108 | '@id': url, 109 | }, 110 | headline: title, 111 | image: featuredImages, 112 | datePublished: publishedAt, 113 | dateModified: modifiedAt, 114 | author: authorList, 115 | publisher: { 116 | '@type': 'Organization', 117 | name: siteMetadata.author, 118 | logo: { 119 | '@type': 'ImageObject', 120 | url: `${siteMetadata.siteUrl}${siteMetadata.siteLogo}`, 121 | }, 122 | }, 123 | description: summary, 124 | } 125 | 126 | const twImageUrl = featuredImages[0].url 127 | 128 | return ( 129 | <> 130 | 137 | 138 | {date && } 139 | {lastmod && } 140 | 141 | 23 | 24 | ) 25 | } 26 | 27 | export default GAScript 28 | 29 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events 30 | export const logEvent = (action, category, label, value) => { 31 | window.gtag?.('event', action, { 32 | event_category: category, 33 | event_label: label, 34 | value: value, 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /components/analytics/Plausible.js: -------------------------------------------------------------------------------- 1 | import Script from 'next/script' 2 | 3 | import siteMetadata from '@/data/siteMetadata' 4 | 5 | const PlausibleScript = () => { 6 | return ( 7 | <> 8 | 18 | 19 | ) 20 | } 21 | 22 | export default PlausibleScript 23 | 24 | // https://plausible.io/docs/custom-event-goals 25 | export const logEvent = (eventName, ...rest) => { 26 | return window.plausible?.(eventName, ...rest) 27 | } 28 | -------------------------------------------------------------------------------- /components/analytics/SimpleAnalytics.js: -------------------------------------------------------------------------------- 1 | import Script from 'next/script' 2 | 3 | const SimpleAnalyticsScript = () => { 4 | return ( 5 | <> 6 | 11 |