From 358d884bd43b68789d262974ad910eab3577406f Mon Sep 17 00:00:00 2001 From: Ivan Li Date: Sun, 17 Jul 2022 21:40:41 +0800 Subject: [PATCH] Initial commit --- .env.example | 29 ++ .eslintignore | 1 + .eslintrc.js | 17 + .gitattributes | 202 ++++++++ .github/FUNDING.yml | 3 + .github/ISSUE_TEMPLATE/bug_report.md | 37 ++ .github/ISSUE_TEMPLATE/feature_request.md | 20 + .gitignore | 36 ++ .husky/.gitignore | 1 + .husky/pre-commit | 4 + LICENSE | 21 + README.md | 250 ++++++++++ components/Card.js | 56 +++ components/ClientReload.js | 23 + components/Footer.js | 32 ++ components/Image.js | 6 + components/LayoutWrapper.js | 54 +++ components/Link.js | 23 + components/MDXComponents.js | 26 + components/MobileNav.js | 85 ++++ components/NewsletterForm.js | 84 ++++ components/PageTitle.js | 7 + components/Pagination.js | 36 ++ components/Pre.js | 71 +++ components/SEO.js | 163 +++++++ components/ScrollTopAndComment.js | 61 +++ components/SectionContainer.js | 3 + components/TOCInline.js | 64 +++ components/Tag.js | 14 + components/ThemeSwitch.js | 38 ++ components/analytics/GoogleAnalytics.js | 36 ++ components/analytics/Plausible.js | 27 ++ components/analytics/Posthog.js | 18 + components/analytics/SimpleAnalytics.js | 25 + components/analytics/Umami.js | 18 + components/analytics/index.js | 22 + components/comments/Disqus.js | 37 ++ components/comments/Giscus.js | 72 +++ components/comments/Utterances.js | 52 ++ components/comments/index.js | 39 ++ components/social-icons/facebook.svg | 1 + components/social-icons/github.svg | 1 + components/social-icons/index.js | 40 ++ components/social-icons/linkedin.svg | 1 + components/social-icons/mail.svg | 4 + components/social-icons/twitter.svg | 1 + components/social-icons/youtube.svg | 1 + css/prism.css | 140 ++++++ css/tailwind.css | 25 + data/authors/default.md | 16 + data/authors/sparrowhawk.md | 12 + data/blog/code-sample.md | 38 ++ data/blog/deriving-ols-estimator.mdx | 141 ++++++ data/blog/github-markdown-guide.mdx | 198 ++++++++ data/blog/guide-to-using-images-in-nextjs.mdx | 76 +++ ...troducing-tailwind-nextjs-starter-blog.mdx | 214 +++++++++ data/blog/my-fancy-title.md | 10 + ...ng-multi-part-posts-with-nested-routing.md | 30 ++ data/blog/new-features-in-v1.mdx | 451 ++++++++++++++++++ data/blog/pictures-of-canada.mdx | 82 ++++ data/blog/the-time-machine.mdx | 238 +++++++++ data/headerNavLinks.js | 8 + data/logo.svg | 3 + data/projectsData.js | 20 + data/references-data.bib | 33 ++ data/siteMetadata.js | 85 ++++ jsconfig.json | 12 + layouts/AuthorLayout.js | 41 ++ layouts/ListLayout.js | 91 ++++ layouts/PostLayout.js | 152 ++++++ layouts/PostSimple.js | 71 +++ lib/generate-rss.js | 32 ++ lib/mdx.js | 136 ++++++ lib/remark-code-title.js | 32 ++ lib/remark-extract-frontmatter.js | 10 + lib/remark-img-to-jsx.js | 35 ++ lib/remark-toc-headings.js | 15 + lib/tags.js | 30 ++ lib/utils/files.js | 23 + lib/utils/formatDate.js | 14 + lib/utils/htmlEscaper.js | 23 + lib/utils/kebabCase.js | 5 + next.config.js | 87 ++++ package-lock.json | Bin 0 -> 453767 bytes package.json | 73 +++ pages/404.js | 31 ++ pages/_app.js | 31 ++ pages/_document.js | 36 ++ pages/about.js | 21 + pages/api/buttondown.js | 30 ++ pages/api/convertkit.js | 35 ++ pages/api/emailoctopus.js | 33 ++ pages/api/klaviyo.js | 35 ++ pages/api/mailchimp.js | 25 + pages/api/revue.js | 30 ++ pages/blog.js | 31 ++ pages/blog/[...slug].js | 70 +++ pages/blog/page/[page].js | 56 +++ pages/index.js | 101 ++++ pages/projects.js | 35 ++ pages/tags.js | 44 ++ pages/tags/[tag].js | 55 +++ postcss.config.js | 6 + prettier.config.js | 9 + .../static/favicons/android-chrome-96x96.png | Bin 0 -> 3327 bytes public/static/favicons/apple-touch-icon.png | Bin 0 -> 3738 bytes public/static/favicons/browserconfig.xml | 9 + public/static/favicons/favicon-16x16.png | Bin 0 -> 1166 bytes public/static/favicons/favicon-32x32.png | Bin 0 -> 1461 bytes public/static/favicons/favicon.ico | Bin 0 -> 15086 bytes public/static/favicons/mstile-150x150.png | Bin 0 -> 4362 bytes public/static/favicons/safari-pinned-tab.svg | 21 + public/static/favicons/site.webmanifest | 14 + public/static/images/avatar.png | Bin 0 -> 5462 bytes public/static/images/canada/lake.jpg | Bin 0 -> 41767 bytes public/static/images/canada/maple.jpg | Bin 0 -> 57747 bytes public/static/images/canada/mountains.jpg | Bin 0 -> 86231 bytes public/static/images/canada/toronto.jpg | Bin 0 -> 79963 bytes public/static/images/google.png | Bin 0 -> 30353 bytes public/static/images/logo.png | Bin 0 -> 43314 bytes public/static/images/ocean.jpeg | Bin 0 -> 262353 bytes public/static/images/sparrowhawk-avatar.jpg | Bin 0 -> 15300 bytes public/static/images/time-machine.jpg | Bin 0 -> 429232 bytes public/static/images/twitter-card.png | Bin 0 -> 21053 bytes scripts/compose.js | 128 +++++ scripts/generate-sitemap.js | 67 +++ scripts/next-remote-watch.js | 120 +++++ tailwind.config.js | 168 +++++++ 128 files changed, 5805 insertions(+) create mode 100644 .env.example create mode 100644 .eslintignore create mode 100644 .eslintrc.js create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitignore create mode 100644 .husky/.gitignore create mode 100755 .husky/pre-commit create mode 100644 LICENSE create mode 100644 README.md create mode 100644 components/Card.js create mode 100644 components/ClientReload.js create mode 100644 components/Footer.js create mode 100644 components/Image.js create mode 100644 components/LayoutWrapper.js create mode 100644 components/Link.js create mode 100644 components/MDXComponents.js create mode 100644 components/MobileNav.js create mode 100644 components/NewsletterForm.js create mode 100644 components/PageTitle.js create mode 100644 components/Pagination.js create mode 100644 components/Pre.js create mode 100644 components/SEO.js create mode 100644 components/ScrollTopAndComment.js create mode 100644 components/SectionContainer.js create mode 100644 components/TOCInline.js create mode 100644 components/Tag.js create mode 100644 components/ThemeSwitch.js create mode 100644 components/analytics/GoogleAnalytics.js create mode 100644 components/analytics/Plausible.js create mode 100644 components/analytics/Posthog.js create mode 100644 components/analytics/SimpleAnalytics.js create mode 100644 components/analytics/Umami.js create mode 100644 components/analytics/index.js create mode 100644 components/comments/Disqus.js create mode 100644 components/comments/Giscus.js create mode 100644 components/comments/Utterances.js create mode 100644 components/comments/index.js create mode 100644 components/social-icons/facebook.svg create mode 100644 components/social-icons/github.svg create mode 100644 components/social-icons/index.js create mode 100644 components/social-icons/linkedin.svg create mode 100644 components/social-icons/mail.svg create mode 100644 components/social-icons/twitter.svg create mode 100644 components/social-icons/youtube.svg create mode 100644 css/prism.css create mode 100644 css/tailwind.css create mode 100644 data/authors/default.md create mode 100644 data/authors/sparrowhawk.md create mode 100644 data/blog/code-sample.md create mode 100644 data/blog/deriving-ols-estimator.mdx create mode 100644 data/blog/github-markdown-guide.mdx create mode 100644 data/blog/guide-to-using-images-in-nextjs.mdx create mode 100644 data/blog/introducing-tailwind-nextjs-starter-blog.mdx create mode 100644 data/blog/my-fancy-title.md create mode 100644 data/blog/nested-route/introducing-multi-part-posts-with-nested-routing.md create mode 100644 data/blog/new-features-in-v1.mdx create mode 100644 data/blog/pictures-of-canada.mdx create mode 100644 data/blog/the-time-machine.mdx create mode 100644 data/headerNavLinks.js create mode 100644 data/logo.svg create mode 100644 data/projectsData.js create mode 100644 data/references-data.bib create mode 100644 data/siteMetadata.js create mode 100644 jsconfig.json create mode 100644 layouts/AuthorLayout.js create mode 100644 layouts/ListLayout.js create mode 100644 layouts/PostLayout.js create mode 100644 layouts/PostSimple.js create mode 100644 lib/generate-rss.js create mode 100644 lib/mdx.js create mode 100644 lib/remark-code-title.js create mode 100644 lib/remark-extract-frontmatter.js create mode 100644 lib/remark-img-to-jsx.js create mode 100644 lib/remark-toc-headings.js create mode 100644 lib/tags.js create mode 100644 lib/utils/files.js create mode 100644 lib/utils/formatDate.js create mode 100644 lib/utils/htmlEscaper.js create mode 100644 lib/utils/kebabCase.js create mode 100644 next.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 pages/404.js create mode 100644 pages/_app.js create mode 100644 pages/_document.js create mode 100644 pages/about.js create mode 100644 pages/api/buttondown.js create mode 100644 pages/api/convertkit.js create mode 100644 pages/api/emailoctopus.js create mode 100644 pages/api/klaviyo.js create mode 100644 pages/api/mailchimp.js create mode 100644 pages/api/revue.js create mode 100644 pages/blog.js create mode 100644 pages/blog/[...slug].js create mode 100644 pages/blog/page/[page].js create mode 100644 pages/index.js create mode 100644 pages/projects.js create mode 100644 pages/tags.js create mode 100644 pages/tags/[tag].js create mode 100644 postcss.config.js create mode 100644 prettier.config.js create mode 100644 public/static/favicons/android-chrome-96x96.png create mode 100644 public/static/favicons/apple-touch-icon.png create mode 100644 public/static/favicons/browserconfig.xml create mode 100644 public/static/favicons/favicon-16x16.png create mode 100644 public/static/favicons/favicon-32x32.png create mode 100644 public/static/favicons/favicon.ico create mode 100644 public/static/favicons/mstile-150x150.png create mode 100644 public/static/favicons/safari-pinned-tab.svg create mode 100644 public/static/favicons/site.webmanifest create mode 100644 public/static/images/avatar.png create mode 100644 public/static/images/canada/lake.jpg create mode 100644 public/static/images/canada/maple.jpg create mode 100644 public/static/images/canada/mountains.jpg create mode 100644 public/static/images/canada/toronto.jpg create mode 100644 public/static/images/google.png create mode 100644 public/static/images/logo.png create mode 100644 public/static/images/ocean.jpeg create mode 100644 public/static/images/sparrowhawk-avatar.jpg create mode 100644 public/static/images/time-machine.jpg create mode 100644 public/static/images/twitter-card.png create mode 100644 scripts/compose.js create mode 100644 scripts/generate-sitemap.js create mode 100644 scripts/next-remote-watch.js create mode 100644 tailwind.config.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ecf12d0 --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +NEXT_PUBLIC_GISCUS_REPO= +NEXT_PUBLIC_GISCUS_REPOSITORY_ID= +NEXT_PUBLIC_GISCUS_CATEGORY= +NEXT_PUBLIC_GISCUS_CATEGORY_ID= +NEXT_PUBLIC_UTTERANCES_REPO= +NEXT_PUBLIC_DISQUS_SHORTNAME= + + +MAILCHIMP_API_KEY= +MAILCHIMP_API_SERVER= +MAILCHIMP_AUDIENCE_ID= + +BUTTONDOWN_API_URL=https://api.buttondown.email/v1/ +BUTTONDOWN_API_KEY= + +CONVERTKIT_API_URL=https://api.convertkit.com/v3/ +CONVERTKIT_API_KEY= +// curl https://api.convertkit.com/v3/forms?api_key= to get your form ID +CONVERTKIT_FORM_ID= + +KLAVIYO_API_KEY= +KLAVIYO_LIST_ID= + +REVUE_API_URL=https://www.getrevue.co/api/v2/ +REVUE_API_KEY= + +EMAILOCTOPUS_API_URL=https://emailoctopus.com/api/1.6/ +EMAILOCTOPUS_API_KEY= +EMAILOCTOPUS_LIST_ID= \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +node_modules diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..db10a94 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + root: true, + env: { + browser: true, + amd: true, + node: true, + es6: true, + }, + extends: ['eslint:recommended', 'plugin:prettier/recommended', 'next', 'next/core-web-vitals'], + rules: { + 'prettier/prettier': 'error', + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 0, + 'no-unused-vars': 0, + 'react/no-unescaped-entities': 0, + }, +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..71a4289 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,202 @@ +## Source: https://github.com/alexkaratarakis/gitattributes +## Modified * text=auto to * text=auto eol=lf to force LF endings. + +## GITATTRIBUTES FOR WEB PROJECTS +# +# These settings are for any web project. +# +# Details per file setting: +# text These files should be normalized (i.e. convert CRLF to LF). +# binary These files are binary and should be left untouched. +# +# Note that binary is a macro for -text -diff. +###################################################################### + +# Auto detect +## Force LF line endings automatically for files detected as +## text and leave all files detected as binary untouched. +## This will handle all files NOT defined below. +* text=auto eol=lf + +# Source code +*.bash text eol=lf +*.bat text eol=crlf +*.cmd text eol=crlf +*.coffee text +*.css text +*.htm text diff=html +*.html text diff=html +*.inc text +*.ini text +*.js text +*.json text +*.jsx text +*.less text +*.ls text +*.map text -diff +*.od text +*.onlydata text +*.php text diff=php +*.pl text +*.ps1 text eol=crlf +*.py text diff=python +*.rb text diff=ruby +*.sass text +*.scm text +*.scss text diff=css +*.sh text eol=lf +*.sql text +*.styl text +*.tag text +*.ts text +*.tsx text +*.xml text +*.xhtml text diff=html + +# Docker +Dockerfile text + +# Documentation +*.ipynb text +*.markdown text +*.md text +*.mdwn text +*.mdown text +*.mkd text +*.mkdn text +*.mdtxt text +*.mdtext text +*.txt text +AUTHORS text +CHANGELOG text +CHANGES text +CONTRIBUTING text +COPYING text +copyright text +*COPYRIGHT* text +INSTALL text +license text +LICENSE text +NEWS text +readme text +*README* text +TODO text + +# Templates +*.dot text +*.ejs text +*.haml text +*.handlebars text +*.hbs text +*.hbt text +*.jade text +*.latte text +*.mustache text +*.njk text +*.phtml text +*.tmpl text +*.tpl text +*.twig text +*.vue text + +# Configs +*.cnf text +*.conf text +*.config text +.editorconfig text +.env text +.gitattributes text +.gitconfig text +.htaccess text +*.lock text -diff +package-lock.json text -diff +*.toml text +*.yaml text +*.yml text +browserslist text +Makefile text +makefile text + +# Heroku +Procfile text + +# Graphics +*.ai binary +*.bmp binary +*.eps binary +*.gif binary +*.gifv binary +*.ico binary +*.jng binary +*.jp2 binary +*.jpg binary +*.jpeg binary +*.jpx binary +*.jxr binary +*.pdf binary +*.png binary +*.psb binary +*.psd binary +# SVG treated as an asset (binary) by default. +*.svg text +# If you want to treat it as binary, +# use the following line instead. +# *.svg binary +*.svgz binary +*.tif binary +*.tiff binary +*.wbmp binary +*.webp binary + +# Audio +*.kar binary +*.m4a binary +*.mid binary +*.midi binary +*.mp3 binary +*.ogg binary +*.ra binary + +# Video +*.3gpp binary +*.3gp binary +*.as binary +*.asf binary +*.asx binary +*.fla binary +*.flv binary +*.m4v binary +*.mng binary +*.mov binary +*.mp4 binary +*.mpeg binary +*.mpg binary +*.ogv binary +*.swc binary +*.swf binary +*.webm binary + +# Archives +*.7z binary +*.gz binary +*.jar binary +*.rar binary +*.tar binary +*.zip binary + +# Fonts +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary + +# Executables +*.exe binary +*.pyc binary + +# RC files (like .babelrc or .eslintrc) +*.*rc text + +# Ignore files (like .npmignore or .gitignore) +*.*ignore text diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..8ff1b0d --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: timlrx diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..1bcb15f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System Info (if dev / build issue):** + - OS: [e.g. iOS] + - Node version (please ensure you are using 14+) + - Npm version + +**Browser Info (if display / formatting issue):** +- Device [e.g. Desktop, iPhone6] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..225ed9c --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ +public/sitemap.xml +.vercel + +# production +/build +*.xml +# rss feed +/public/feed.xml + +# misc +.DS_Store + +# debug +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local \ No newline at end of file diff --git a/.husky/.gitignore b/.husky/.gitignore new file mode 100644 index 0000000..31354ec --- /dev/null +++ b/.husky/.gitignore @@ -0,0 +1 @@ +_ diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..d37daa0 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,4 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +npx --no-install lint-staged diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..215f501 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Timothy Lin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..790c39b --- /dev/null +++ b/README.md @@ -0,0 +1,250 @@ +![tailwind-nextjs-banner](/public/static/images/twitter-card.png) + +# Tailwind Nextjs Starter Blog + +[![GitHub Repo stars](https://img.shields.io/github/stars/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/stargazers/) +[![GitHub forks](https://img.shields.io/github/forks/timlrx/tailwind-nextjs-starter-blog?style=social)](https://GitHub.com/timlrx/tailwind-nextjs-starter-blog/network/) +[![Twitter URL](https://img.shields.io/twitter/url?style=social&url=https%3A%2F%2Ftwitter.com%2Ftimlrxx)](https://twitter.com/timlrxx) +[![Sponsor](https://img.shields.io/static/v1?label=Sponsor&message=%E2%9D%A4&logo=GitHub&link=https://github.com/sponsors/timlrx)](https://github.com/sponsors/timlrx) + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/timlrx/tailwind-nextjs-starter-blog) + +This is a [Next.js](https://nextjs.org/), [Tailwind CSS](https://tailwindcss.com/) blogging starter template. Probably the most feature-rich Next.js markdown blogging template out there. Comes out of the box configured with the latest technologies to make technical writing a breeze. Easily configurable and customizable. Perfect as a replacement to existing Jekyll and Hugo individual blogs. + +Check out the documentation below to get started. + +Facing issues? Check the [FAQ page](https://github.com/timlrx/tailwind-nextjs-starter-blog/wiki) and do a search on past issues. Feel free to open a new issue if none has been posted previously. + +Feature request? Check the past discussions to see if it has been brought up previously. Otherwise, feel free to start a new discussion thread. All ideas are welcomed! + +## Examples + +- [Demo Blog](https://tailwind-nextjs-starter-blog.vercel.app/) - this repo +- [My personal blog](https://www.timlrx.com) - modified to auto-generate blog posts with dates +- [Aloisdg's cookbook](https://tambouille.vercel.app/) - with pictures and recipes! +- [GautierArcin's demo with next translate](https://tailwind-nextjs-starter-blog-seven.vercel.app/) - includes translation of mdx posts, [source code](https://github.com/GautierArcin/tailwind-nextjs-starter-blog/tree/demo/next-translate) +- [David Levai's digital garden](https://davidlevai.com/) - customized design and added email subscriptions +- [Thinh's Corner](https://thinhcorner.com/) - [customized layout](https://github.com/Th1nhNg0/th1nhng0.vercel.app/blob/5e73a420828d82f01e7147512a2c3273c4ec19f8/layouts/PostLayout.js) with sticky side table of contents +- [Leo's Blog](https://leohuynh.dev) - Tuan Anh Huynh's personal site. Add Snippets Page, Author Profile Card, Image Lightbox, ... +- [thvu.dev](https://thvu.dev) - Added `mdx-embed`, view count, reading minutes and more. +- [fiqrychoerudin.dev](https://www.fiqrychoerudin.dev/) - simple portfolio. +- [irvin.dev](https://www.irvin.dev/) - Irvin Lin's personal site. Added YouTube embedding. +- [the all JavaScript Blog](https://the-all-javascript-blog.vercel.app/) - a JavaScript enlightenment blog uses this +- [KirillSo.com](https://www.kirillso.com/) - Personal blog & website. +- [ghali.dev](https://ghali.dev) - Cyril's Blog +- [DevBoy Blog](https://devboy.vercel.app/) - M.Reza's personal blog +- [slightlysharpe.com](https://slightlysharpe.com) - [Tincre's](https://tincre.com) main company blog +- [blog.b00st.com](https://blog.b00st.com) - [b00st.com's](https://b00st.com) main music promotion blog +- [astrosaurus.me](https://astrosaurus.me/) - Ephraim Atta-Duncan's Personal Blog +- [dhanrajsp.me](https://dhanrajsp.me/) - Dhanraj's personal site and blog. +- [blog.r00ks.io](https://blog.r00ks.io/) - Austin Rooks's personal blog ([source code](https://github.com/Austionian/blog.r00ks)). +- [honghong.me](https://honghong.me) - Tszhong's personal website ([source code](https://github.com/tszhong0411/home)) +- [alfoncode.com](https://alfoncode.com) - Alfonso García's personar website. Customized design ([source code](https://github.com/alfoncode/personal-web)) +- [marceloformentao.dev](https://marceloformentao.dev) - Marcelo Formentão personal website ([source code](https://github.com/marceloavf/marceloformentao.dev)). +- [abiraja.com](https://www.abiraja.com/) - with a [runnable JS code snippet component!](https://www.abiraja.com/blog/querying-solana-blockchain) +- [einargudni.com](https://www.einargudni.com) - with a customized theme, command pallette and more ([source code](https://github.com/einargudnig/einargudni.com)) +- [bpiggin.com](https://www.bpiggin.com) - Ben Piggin's personal blog +- [maqib.cn](https://maqib.cn) - A blog of Chinese front-end developers 狂奔小马的博客 ([源码](https://github.com/maqi1520/nextjs-tailwind-blog)) +- [ambilena.com](https://ambilena.com/) - Electronic Music Blog & imprint for upcoming musicians. +- [kittan.ru](https://www.kittan.ru/) - Kittanb's personal blog about linux ([source code](https://github.com/kittanb/blog)) +- [nchristopher.me](https://nchristopher.me) - Nicholas Christopher's personal website and blog ([source code](https://github.com/nchristopher/blog)) +- [dalelarroder.com](https://dalelarroder.com) - Dale Larroder's personal website and blog ([source code](https://github.com/dlarroder/dalelarroder)) +- [devahoy.com](https://devahoy.com) - Chai's personal blog (Thai language) +- [0xchai.io](https://0xchai.io) - Chai's personal blog +- [techipedia](https://techipedia.vercel.app) - Simple blogging progressive web app with custom installation button and top progress bar +- [reubence.com](https://reubence.com) - Reuben Rapose's Digital Garden +- [axolo.co/blog](https://axolo.co/blog) - Engineering management news & axolo.co updates (with image preview for article in the home page) +- [musing.vercel.app](https://musing.vercel.app/) - Parth Desai's personal blog ([source code](https://github.com/pycoder2000/blog)) +- [onyourmental.com](https://www.onyourmental.com/) - [Curtis Warcup's](https://github.com/Cwarcup) website for the On Your Mental Podcast ([source code](https://github.com/Cwarcup/on-your-mental)) +- [cwarcup.com](https://www.cwarcup.com/) - Curtis Warcup's personal website and blog ([source code](https://github.com/Cwarcup/personal-blog). + +Using the template? Feel free to create a PR and add your blog to this list. + +## Motivation + +I wanted to port my existing blog to Nextjs and Tailwind CSS but there was no easy out of the box template to use so I decided to create one. Design is adapted from [Tailwindlabs blog](https://github.com/tailwindlabs/blog.tailwindcss.com). + +I wanted it to be nearly as feature-rich as popular blogging templates like [beautiful-jekyll](https://github.com/daattali/beautiful-jekyll) and [Hugo Academic](https://github.com/wowchemy/wowchemy-hugo-modules) but with the best of React's ecosystem and current web development's best practices. + +## Features + +- Easy styling customization with [Tailwind 3.0](https://tailwindcss.com/blog/tailwindcss-v3) and primary color attribute +- Near perfect lighthouse score - [Lighthouse report](https://www.webpagetest.org/result/210111_DiC1_08f3670c3430bf4a9b76fc3b927716c5/) +- Lightweight, 45kB first load JS, uses Preact in production build +- Mobile-friendly view +- Light and dark theme +- Self-hosted font with [Fontsource](https://fontsource.org/) +- Supports [plausible](https://plausible.io/), [simple analytics](https://simpleanalytics.com/) and google analytics +- [MDX - write JSX in markdown documents!](https://mdxjs.com/) +- Server-side syntax highlighting with line numbers and line highlighting via [rehype-prism-plus](https://github.com/timlrx/rehype-prism-plus) +- Math display supported via [KaTeX](https://katex.org/) +- Citation and bibliography support via [rehype-citation](https://github.com/timlrx/rehype-citation) +- Automatic image optimization via [next/image](https://nextjs.org/docs/basic-features/image-optimization) +- Flexible data retrieval with [mdx-bundler](https://github.com/kentcdodds/mdx-bundler) +- Support for tags - each unique tag will be its own page +- Support for multiple authors +- Blog templates +- TOC component +- Support for nested routing of blog posts +- Newsletter component with support for mailchimp, buttondown, convertkit, klaviyo, revue, and emailoctopus +- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus +- Projects page +- Preconfigured security headers +- SEO friendly with RSS feed, sitemaps and more! + +## Sample posts + +- [A markdown guide](https://tailwind-nextjs-starter-blog.vercel.app/blog/github-markdown-guide) +- [Learn more about images in Next.js](https://tailwind-nextjs-starter-blog.vercel.app/blog/guide-to-using-images-in-nextjs) +- [A tour of math typesetting](https://tailwind-nextjs-starter-blog.vercel.app/blog/deriving-ols-estimator) +- [Simple MDX image grid](https://tailwind-nextjs-starter-blog.vercel.app/blog/pictures-of-canada) +- [Example of long prose](https://tailwind-nextjs-starter-blog.vercel.app/blog/the-time-machine) +- [Example of Nested Route Post](https://tailwind-nextjs-starter-blog.vercel.app/blog/nested-route/introducing-multi-part-posts-with-nested-routing) + +## Quick Start Guide + +1. Try installing the starter using the new [Pliny project CLI](https://github.com/timlrx/pliny): + +```bash +npm i -g @pliny/cli +pliny new --template=starter-blog my-blog +``` + +It supports the updated version of the blog with Contentlayer, optional choice of TS/JS and different package managers as well as more modularized components which will be the basis of the template going forward. + +Alternatively to stick with the current version, TypeScript and Contentlayer: + +```bash +npx degit 'timlrx/tailwind-nextjs-starter-blog#contentlayer' +``` + +or JS (official support) + +```bash +npx degit https://github.com/timlrx/tailwind-nextjs-starter-blog.git +``` + +2. Personalize `siteMetadata.js` (site related information) +3. Modify the content security policy in `next.config.js` if you want to use + any analytics provider or a commenting solution other than giscus. +4. Personalize `authors/default.md` (main author) +5. Modify `projectsData.js` +6. Modify `headerNavLinks.js` to customize navigation links +7. Add blog posts +8. Deploy on Vercel + +## Installation + +```bash +npm install +``` + +## Development + +First, run the development server: + +```bash +npm start +``` + +or + +```bash +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. + +## Extend / Customize + +`data/siteMetadata.js` - contains most of the site related information which should be modified for a user's need. + +`data/authors/default.md` - default author information (required). Additional authors can be added as files in `data/authors`. + +`data/projectsData.js` - data used to generate styled card on the projects page. + +`data/headerNavLinks.js` - navigation links. + +`data/logo.svg` - replace with your own logo. + +`data/blog` - replace with your own blog posts. + +`public/static` - store assets such as images and favicons. + +`tailwind.config.js` and `css/tailwind.css` - contain the tailwind stylesheet which can be modified to change the overall look and feel of the site. + +`css/prism.css` - controls the styles associated with the code blocks. Feel free to customize it and use your preferred prismjs theme e.g. [prism themes](https://github.com/PrismJS/prism-themes). + +`components/social-icons` - to add other icons, simply copy an svg file from [Simple Icons](https://simpleicons.org/) and map them in `index.js`. Other icons use [heroicons](https://heroicons.com/). + +`components/MDXComponents.js` - pass your own JSX code or React component by specifying it over here. You can then call them directly in the `.mdx` or `.md` file. By default, a custom link and image component is passed. + +`layouts` - main templates used in pages. + +`pages` - pages to route to. Read the [Next.js documentation](https://nextjs.org/docs) for more information. + +`next.config.js` - configuration related to Next.js. You need to adapt the Content Security Policy if you want to load scripts, images etc. from other domains. + +## Post + +### Frontmatter + +Frontmatter follows [Hugo's standards](https://gohugo.io/content-management/front-matter/). + +Currently 7 fields are supported. + +``` +title (required) +date (required) +tags (required, can be empty array) +lastmod (optional) +draft (optional) +summary (optional) +images (optional, if none provided defaults to socialBanner in siteMetadata config) +authors (optional list which should correspond to the file names in `data/authors`. Uses `default` if none is specified) +layout (optional list which should correspond to the file names in `data/layouts`) +canonicalUrl (optional, canonical url for the post for SEO) +``` + +Here's an example of a post's frontmatter: + +``` +--- +title: 'Introducing Tailwind Nexjs Starter Blog' +date: '2021-01-12' +lastmod: '2021-01-18' +tags: ['next-js', 'tailwind', 'guide'] +draft: false +summary: 'Looking for a performant, out of the box template, with all the best in web technology to support your blogging needs? Checkout the Tailwind Nextjs Starter Blog template.' +images: ['/static/images/canada/mountains.jpg', '/static/images/canada/toronto.jpg'] +authors: ['default', 'sparrowhawk'] +layout: PostLayout +canonicalUrl: https://tailwind-nextjs-starter-blog.vercel.app/blog/introducing-tailwind-nextjs-starter-blog +--- +``` + +### Compose + +Run `node ./scripts/compose.js` to bootstrap a new post. + +Follow the interactive prompt to generate a post with pre-filled front matter. + +## Deploy + +**Vercel** +The easiest way to deploy the template is to use the [Vercel Platform](https://vercel.com) from the creators of Next.js. Check out the [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. + +**Netlify / GitHub Pages / Firebase etc.** +As the template uses `next/image` for image optimization, additional configurations have to be made to deploy on other popular static hosting websites like [Netlify](https://www.netlify.com/) or [GitHub Pages](https://pages.github.com/). An alternative image optimization provider such as Imgix, Cloudinary or Akamai has to be used. Alternatively, replace the `next/image` component with a standard `` tag. See [`next/image` documentation](https://nextjs.org/docs/basic-features/image-optimization) for more details. + +The API routes used in the newsletter component cannot be used in a static site export. You will need to use a form API endpoint provider and substitute the route in the newsletter component accordingly. Other hosting platforms such as Netlify also offer alternative solutions - please refer to their docs for more information. + +## Support + +Using the template? Support this effort by giving a star on GitHub, sharing your own blog and giving a shoutout on Twitter or becoming a project [sponsor](https://github.com/sponsors/timlrx). + +## Licence + +[MIT](https://github.com/timlrx/tailwind-nextjs-starter-blog/blob/master/LICENSE) © [Timothy Lin](https://www.timlrx.com) diff --git a/components/Card.js b/components/Card.js new file mode 100644 index 0000000..143681e --- /dev/null +++ b/components/Card.js @@ -0,0 +1,56 @@ +import Image from './Image' +import Link from './Link' + +const Card = ({ title, description, imgSrc, href }) => ( +
+
+ {imgSrc && + (href ? ( + + {title} + + ) : ( + {title} + ))} +
+

+ {href ? ( + + {title} + + ) : ( + title + )} +

+

{description}

+ {href && ( + + Learn more → + + )} +
+
+
+) + +export default Card diff --git a/components/ClientReload.js b/components/ClientReload.js new file mode 100644 index 0000000..babfba5 --- /dev/null +++ b/components/ClientReload.js @@ -0,0 +1,23 @@ +import { useEffect } from 'react' +import Router from 'next/router' + +/** + * Client-side complement to next-remote-watch + * Re-triggers getStaticProps when watched mdx files change + * + */ +export const ClientReload = () => { + // Exclude socket.io from prod bundle + useEffect(() => { + import('socket.io-client').then((module) => { + const socket = module.io() + socket.on('reload', (data) => { + Router.replace(Router.asPath, undefined, { + scroll: false, + }) + }) + }) + }, []) + + return null +} diff --git a/components/Footer.js b/components/Footer.js new file mode 100644 index 0000000..4a15d39 --- /dev/null +++ b/components/Footer.js @@ -0,0 +1,32 @@ +import Link from './Link' +import siteMetadata from '@/data/siteMetadata' +import SocialIcon from '@/components/social-icons' + +export default function Footer() { + return ( +
+
+
+ + + + + + +
+
+
{siteMetadata.author}
+
{` • `}
+
{`© ${new Date().getFullYear()}`}
+
{` • `}
+ {siteMetadata.title} +
+
+ + Tailwind Nextjs Theme + +
+
+
+ ) +} diff --git a/components/Image.js b/components/Image.js new file mode 100644 index 0000000..0959091 --- /dev/null +++ b/components/Image.js @@ -0,0 +1,6 @@ +import NextImage from 'next/image' + +// eslint-disable-next-line jsx-a11y/alt-text +const Image = ({ ...rest }) => + +export default Image diff --git a/components/LayoutWrapper.js b/components/LayoutWrapper.js new file mode 100644 index 0000000..2cdc21f --- /dev/null +++ b/components/LayoutWrapper.js @@ -0,0 +1,54 @@ +import siteMetadata from '@/data/siteMetadata' +import headerNavLinks from '@/data/headerNavLinks' +import Logo from '@/data/logo.svg' +import Link from './Link' +import SectionContainer from './SectionContainer' +import Footer from './Footer' +import MobileNav from './MobileNav' +import ThemeSwitch from './ThemeSwitch' + +const LayoutWrapper = ({ children }) => { + return ( + +
+
+
+ +
+
+ +
+ {typeof siteMetadata.headerTitle === 'string' ? ( +
+ {siteMetadata.headerTitle} +
+ ) : ( + siteMetadata.headerTitle + )} +
+ +
+
+
+ {headerNavLinks.map((link) => ( + + {link.title} + + ))} +
+ + +
+
+
{children}
+
+
+
+ ) +} + +export default LayoutWrapper diff --git a/components/Link.js b/components/Link.js new file mode 100644 index 0000000..185eec9 --- /dev/null +++ b/components/Link.js @@ -0,0 +1,23 @@ +/* eslint-disable jsx-a11y/anchor-has-content */ +import Link from 'next/link' + +const CustomLink = ({ href, ...rest }) => { + const isInternalLink = href && href.startsWith('/') + const isAnchorLink = href && href.startsWith('#') + + if (isInternalLink) { + return ( + + + + ) + } + + if (isAnchorLink) { + return + } + + return +} + +export default CustomLink diff --git a/components/MDXComponents.js b/components/MDXComponents.js new file mode 100644 index 0000000..85b2d0b --- /dev/null +++ b/components/MDXComponents.js @@ -0,0 +1,26 @@ +/* eslint-disable react/display-name */ +import { useMemo } from 'react' +import { getMDXComponent } from 'mdx-bundler/client' +import Image from './Image' +import CustomLink from './Link' +import TOCInline from './TOCInline' +import Pre from './Pre' +import { BlogNewsletterForm } from './NewsletterForm' + +export const MDXComponents = { + Image, + TOCInline, + a: CustomLink, + pre: Pre, + BlogNewsletterForm: BlogNewsletterForm, + wrapper: ({ components, layout, ...rest }) => { + const Layout = require(`../layouts/${layout}`).default + return + }, +} + +export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => { + const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource]) + + return +} diff --git a/components/MobileNav.js b/components/MobileNav.js new file mode 100644 index 0000000..4b91922 --- /dev/null +++ b/components/MobileNav.js @@ -0,0 +1,85 @@ +import { useState } from 'react' +import Link from './Link' +import headerNavLinks from '@/data/headerNavLinks' + +const MobileNav = () => { + const [navShow, setNavShow] = useState(false) + + const onToggleNav = () => { + setNavShow((status) => { + if (status) { + document.body.style.overflow = 'auto' + } else { + // Prevent scrolling + document.body.style.overflow = 'hidden' + } + return !status + }) + } + + return ( +
+ +
+
+ +
+ +
+
+ ) +} + +export default MobileNav diff --git a/components/NewsletterForm.js b/components/NewsletterForm.js new file mode 100644 index 0000000..0273228 --- /dev/null +++ b/components/NewsletterForm.js @@ -0,0 +1,84 @@ +import { useRef, useState } from 'react' + +import siteMetadata from '@/data/siteMetadata' + +const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => { + const inputEl = useRef(null) + const [error, setError] = useState(false) + const [message, setMessage] = useState('') + const [subscribed, setSubscribed] = useState(false) + + const subscribe = async (e) => { + e.preventDefault() + + const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, { + body: JSON.stringify({ + email: inputEl.current.value, + }), + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + }) + + const { error } = await res.json() + if (error) { + setError(true) + setMessage('Your e-mail address is invalid or you are already subscribed!') + return + } + + inputEl.current.value = '' + setError(false) + setSubscribed(true) + setMessage('Successfully! 🎉 You are now subscribed.') + } + + return ( +
+
{title}
+
+
+ + +
+
+ +
+
+ {error && ( +
{message}
+ )} +
+ ) +} + +export default NewsletterForm + +export const BlogNewsletterForm = ({ title }) => ( +
+
+ +
+
+) diff --git a/components/PageTitle.js b/components/PageTitle.js new file mode 100644 index 0000000..82419b5 --- /dev/null +++ b/components/PageTitle.js @@ -0,0 +1,7 @@ +export default function PageTitle({ children }) { + return ( +

+ {children} +

+ ) +} diff --git a/components/Pagination.js b/components/Pagination.js new file mode 100644 index 0000000..2754b6c --- /dev/null +++ b/components/Pagination.js @@ -0,0 +1,36 @@ +import Link from '@/components/Link' + +export default function Pagination({ totalPages, currentPage }) { + const prevPage = parseInt(currentPage) - 1 > 0 + const nextPage = parseInt(currentPage) + 1 <= parseInt(totalPages) + + return ( +
+ +
+ ) +} diff --git a/components/Pre.js b/components/Pre.js new file mode 100644 index 0000000..f2df539 --- /dev/null +++ b/components/Pre.js @@ -0,0 +1,71 @@ +import { useState, useRef } from 'react' + +const Pre = (props) => { + const textInput = useRef(null) + const [hovered, setHovered] = useState(false) + const [copied, setCopied] = useState(false) + + const onEnter = () => { + setHovered(true) + } + const onExit = () => { + setHovered(false) + setCopied(false) + } + const onCopy = () => { + setCopied(true) + navigator.clipboard.writeText(textInput.current.textContent) + setTimeout(() => { + setCopied(false) + }, 2000) + } + + return ( +
+ {hovered && ( + + )} + +
{props.children}
+
+ ) +} + +export default Pre diff --git a/components/SEO.js b/components/SEO.js new file mode 100644 index 0000000..0d1dac0 --- /dev/null +++ b/components/SEO.js @@ -0,0 +1,163 @@ +import Head from 'next/head' +import { useRouter } from 'next/router' +import siteMetadata from '@/data/siteMetadata' + +const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl }) => { + const router = useRouter() + return ( + + {title} + + + + + + + + {ogImage.constructor.name === 'Array' ? ( + ogImage.map(({ url }) => ) + ) : ( + + )} + + + + + + + + ) +} + +export const PageSEO = ({ title, description }) => { + const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner + const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner + return ( + + ) +} + +export const TagSEO = ({ title, description }) => { + const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner + const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner + const router = useRouter() + return ( + <> + + + + + + ) +} + +export const BlogSEO = ({ + authorDetails, + title, + summary, + date, + lastmod, + url, + images = [], + canonicalUrl, +}) => { + const router = useRouter() + const publishedAt = new Date(date).toISOString() + const modifiedAt = new Date(lastmod || date).toISOString() + let imagesArr = + images.length === 0 + ? [siteMetadata.socialBanner] + : typeof images === 'string' + ? [images] + : images + + const featuredImages = imagesArr.map((img) => { + return { + '@type': 'ImageObject', + url: img.includes('http') ? img : siteMetadata.siteUrl + img, + } + }) + + let authorList + if (authorDetails) { + authorList = authorDetails.map((author) => { + return { + '@type': 'Person', + name: author.name, + } + }) + } else { + authorList = { + '@type': 'Person', + name: siteMetadata.author, + } + } + + const structuredData = { + '@context': 'https://schema.org', + '@type': 'Article', + mainEntityOfPage: { + '@type': 'WebPage', + '@id': url, + }, + headline: title, + image: featuredImages, + datePublished: publishedAt, + dateModified: modifiedAt, + author: authorList, + publisher: { + '@type': 'Organization', + name: siteMetadata.author, + logo: { + '@type': 'ImageObject', + url: `${siteMetadata.siteUrl}${siteMetadata.siteLogo}`, + }, + }, + description: summary, + } + + const twImageUrl = featuredImages[0].url + + return ( + <> + + + {date && } + {lastmod && } + + + ) +} + +export default GAScript + +// https://developers.google.com/analytics/devguides/collection/gtagjs/events +export const logEvent = (action, category, label, value) => { + window.gtag?.('event', action, { + event_category: category, + event_label: label, + value: value, + }) +} diff --git a/components/analytics/Plausible.js b/components/analytics/Plausible.js new file mode 100644 index 0000000..5d73d24 --- /dev/null +++ b/components/analytics/Plausible.js @@ -0,0 +1,27 @@ +import Script from 'next/script' + +import siteMetadata from '@/data/siteMetadata' + +const PlausibleScript = () => { + return ( + <> + + + ) +} + +export default PlausibleScript + +// https://plausible.io/docs/custom-event-goals +export const logEvent = (eventName, ...rest) => { + return window.plausible?.(eventName, ...rest) +} diff --git a/components/analytics/Posthog.js b/components/analytics/Posthog.js new file mode 100644 index 0000000..1fc268b --- /dev/null +++ b/components/analytics/Posthog.js @@ -0,0 +1,18 @@ +import Script from 'next/script' + +import siteMetadata from '@/data/siteMetadata' + +const PosthogScript = () => { + return ( + <> + + + ) +} + +export default PosthogScript diff --git a/components/analytics/SimpleAnalytics.js b/components/analytics/SimpleAnalytics.js new file mode 100644 index 0000000..bb36ed8 --- /dev/null +++ b/components/analytics/SimpleAnalytics.js @@ -0,0 +1,25 @@ +import Script from 'next/script' + +const SimpleAnalyticsScript = () => { + return ( + <> + +