upstream #1
15
.env.example
15
.env.example
@ -3,4 +3,17 @@ NEXT_PUBLIC_GISCUS_REPOSITORY_ID=
|
|||||||
NEXT_PUBLIC_GISCUS_CATEGORY=
|
NEXT_PUBLIC_GISCUS_CATEGORY=
|
||||||
NEXT_PUBLIC_GISCUS_CATEGORY_ID=
|
NEXT_PUBLIC_GISCUS_CATEGORY_ID=
|
||||||
NEXT_PUBLIC_UTTERANCES_REPO=
|
NEXT_PUBLIC_UTTERANCES_REPO=
|
||||||
NEXT_PUBLIC_DISQUS_SHORTNAME=
|
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=<your_public_api_key> to get your form ID
|
||||||
|
CONVERTKIT_FORM_ID=
|
@ -53,6 +53,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
|||||||
- Blog templates
|
- Blog templates
|
||||||
- TOC component
|
- TOC component
|
||||||
- Support for nested routing of blog posts
|
- Support for nested routing of blog posts
|
||||||
|
- Newsletter component with support for mailchimp, buttondown and convertkit
|
||||||
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
|
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
|
||||||
- Projects page
|
- Projects page
|
||||||
- SEO friendly with RSS feed, sitemaps and more!
|
- SEO friendly with RSS feed, sitemaps and more!
|
||||||
@ -168,6 +169,8 @@ The easiest way to deploy the template is to use the [Vercel Platform](https://v
|
|||||||
**Netlify / Github Pages / Firebase etc.**
|
**Netlify / Github Pages / Firebase etc.**
|
||||||
As the template uses `next/image` for image optimization, additional configurations has 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 `<img>` tag. See [`next/image` documentation](https://nextjs.org/docs/basic-features/image-optimization) for more details.
|
As the template uses `next/image` for image optimization, additional configurations has 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 `<img>` 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
|
## Support
|
||||||
|
|
||||||
Using the template? Support this effort by giving a star on Github, sharing your own blog and giving a shoutout on Twitter or be a project [sponsor](https://github.com/sponsors/timlrx).
|
Using the template? Support this effort by giving a star on Github, sharing your own blog and giving a shoutout on Twitter or be a project [sponsor](https://github.com/sponsors/timlrx).
|
||||||
|
72
components/FormSubscribe.js
Normal file
72
components/FormSubscribe.js
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import siteMetadata from '@/data/siteMetadata'
|
||||||
|
|
||||||
|
const FormSubscribe = () => {
|
||||||
|
const inputEl = useRef(null)
|
||||||
|
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) {
|
||||||
|
setMessage('Your e-mail adress is invalid or you are already subscribed!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inputEl.current.value = ''
|
||||||
|
setSubscribed(true)
|
||||||
|
setMessage('Successfully! 🎉 You are now subscribed.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="pb-1 text-lg font-semibold text-gray-800 dark:text-gray-100">
|
||||||
|
Subscribe to the newsletter
|
||||||
|
</div>
|
||||||
|
<form className="flex flex-col sm:flex-row" onSubmit={subscribe}>
|
||||||
|
<div>
|
||||||
|
<label className="sr-only" htmlFor="email-input">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
autoComplete="email"
|
||||||
|
className="px-4 py-2 placeholder-gray-500 bg-white border rounded-md appearance-none w-72 border-neutrals-cool-grey-300 text-neutrals-cool-grey-900 dark:bg-black focus:outline-none focus:ring-primary-400 dark:focus:border-primary-600"
|
||||||
|
id="email-input"
|
||||||
|
name="email"
|
||||||
|
placeholder={subscribed ? "You're subscribed ! 🎉" : 'Enter your email'}
|
||||||
|
ref={inputEl}
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
disabled={subscribed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full mt-2 rounded-md shadow-sm sm:mt-0 sm:ml-3">
|
||||||
|
<button
|
||||||
|
className={`w-full bg-primary-500 dark:bg-primary-500 px-4 py-2 border border-transparent rounded-md font-medium text-white ${
|
||||||
|
subscribed ? 'cursor-default' : 'hover:bg-primary-700 dark:hover:bg-primary-400'
|
||||||
|
} focus:outline-none focus:ring-2 focus:ring-offset-2 focus:primary-700`}
|
||||||
|
type="submit"
|
||||||
|
disabled={subscribed}
|
||||||
|
>
|
||||||
|
{subscribed ? 'Thank you!' : 'Sign up'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FormSubscribe }
|
@ -9,3 +9,9 @@
|
|||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* https://stackoverflow.com/questions/61083813/how-to-avoid-internal-autofill-selected-style-to-be-applied */
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:focus {
|
||||||
|
transition: background-color 600000s 0s, color 600000s 0s;
|
||||||
|
}
|
||||||
|
@ -58,6 +58,7 @@ I wanted it to be nearly as feature-rich as popular blogging templates like [bea
|
|||||||
- Blog templates
|
- Blog templates
|
||||||
- TOC component
|
- TOC component
|
||||||
- Support for nested routing of blog posts
|
- Support for nested routing of blog posts
|
||||||
|
- Newsletter component with support for mailchimp, buttondown and convertkit
|
||||||
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
|
- Supports [giscus](https://github.com/laymonage/giscus), [utterances](https://github.com/utterance/utterances) or disqus
|
||||||
- Projects page
|
- Projects page
|
||||||
- SEO friendly with RSS feed, sitemaps and more!
|
- SEO friendly with RSS feed, sitemaps and more!
|
||||||
@ -173,6 +174,8 @@ The easiest way to deploy the template is to use the [Vercel Platform](https://v
|
|||||||
**Netlify / Github Pages / Firebase etc.**
|
**Netlify / Github Pages / Firebase etc.**
|
||||||
As the template uses `next/image` for image optimization, additional configurations has 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 `<img>` tag. See [`next/image` documentation](https://nextjs.org/docs/basic-features/image-optimization) for more details.
|
As the template uses `next/image` for image optimization, additional configurations has 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 `<img>` 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
|
## Support
|
||||||
|
|
||||||
Using the template? Support this effort by giving a star on Github, sharing your own blog and giving a shoutout on Twitter or be a project [sponsor](https://github.com/sponsors/timlrx).
|
Using the template? Support this effort by giving a star on Github, sharing your own blog and giving a shoutout on Twitter or be a project [sponsor](https://github.com/sponsors/timlrx).
|
||||||
|
@ -22,6 +22,11 @@ const siteMetadata = {
|
|||||||
simpleAnalytics: false, // true or false
|
simpleAnalytics: false, // true or false
|
||||||
googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX
|
googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX
|
||||||
},
|
},
|
||||||
|
newsletter: {
|
||||||
|
// supports mailchimp, buttondown, convertkit
|
||||||
|
// Please add your .env file and modify it according to your selection
|
||||||
|
provider: 'buttondown',
|
||||||
|
},
|
||||||
comment: {
|
comment: {
|
||||||
// Select a provider and use the environment variables associated to it
|
// Select a provider and use the environment variables associated to it
|
||||||
// https://vercel.com/docs/environment-variables
|
// https://vercel.com/docs/environment-variables
|
||||||
@ -59,7 +64,7 @@ const siteMetadata = {
|
|||||||
// theme when dark mode
|
// theme when dark mode
|
||||||
darkTheme: '',
|
darkTheme: '',
|
||||||
},
|
},
|
||||||
disqus: {
|
disqusConfig: {
|
||||||
// https://help.disqus.com/en/articles/1717111-what-s-a-shortname
|
// https://help.disqus.com/en/articles/1717111-what-s-a-shortname
|
||||||
shortname: process.env.NEXT_PUBLIC_DISQUS_SHORTNAME,
|
shortname: process.env.NEXT_PUBLIC_DISQUS_SHORTNAME,
|
||||||
},
|
},
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@mailchimp/mailchimp_marketing": "^3.0.58",
|
||||||
"@tailwindcss/forms": "^0.3.2",
|
"@tailwindcss/forms": "^0.3.2",
|
||||||
"@tailwindcss/typography": "^0.4.0",
|
"@tailwindcss/typography": "^0.4.0",
|
||||||
"autoprefixer": "^10.2.5",
|
"autoprefixer": "^10.2.5",
|
||||||
|
31
pages/api/buttondown.js
Normal file
31
pages/api/buttondown.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// eslint-disable-next-line import/no-anonymous-default-export
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { email } = req.body
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({ error: 'Email is required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const API_KEY = process.env.BUTTONDOWN_API_KEY
|
||||||
|
const buttondownRoute = `${process.env.BUTTONDOWN_API_URL}subscribers`
|
||||||
|
console.log('route : ', buttondownRoute)
|
||||||
|
const response = await fetch(buttondownRoute, {
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
}),
|
||||||
|
headers: {
|
||||||
|
Authorization: `Token ${API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
return res.status(500).json({ error: `There was an error subscribing to the list.` })
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(201).json({ error: '' })
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({ error: error.message || error.toString() })
|
||||||
|
}
|
||||||
|
}
|
36
pages/api/convertkit.js
Normal file
36
pages/api/convertkit.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/* eslint-disable import/no-anonymous-default-export */
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { email } = req.body
|
||||||
|
console.log('email : ', email)
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({ error: 'Email is required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const FORM_ID = process.env.CONVERTKIT_FORM_ID
|
||||||
|
const API_KEY = process.env.CONVERTKIT_API_KEY
|
||||||
|
const API_URL = process.env.CONVERTKIT_API_URL
|
||||||
|
|
||||||
|
// Send request to ConvertKit
|
||||||
|
const data = { email, api_key: API_KEY }
|
||||||
|
|
||||||
|
const response = await fetch(`${API_URL}forms/${FORM_ID}/subscribe`, {
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.status >= 400) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `There was an error subscribing to the list.`,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(201).json({ error: '' })
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({ error: error.message || error.toString() })
|
||||||
|
}
|
||||||
|
}
|
25
pages/api/mailchimp.js
Normal file
25
pages/api/mailchimp.js
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import mailchimp from '@mailchimp/mailchimp_marketing'
|
||||||
|
|
||||||
|
mailchimp.setConfig({
|
||||||
|
apiKey: process.env.MAILCHIMP_API_KEY,
|
||||||
|
server: process.env.MAILCHIMP_API_SERVER, // E.g. us1
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-anonymous-default-export
|
||||||
|
export default async (req, res) => {
|
||||||
|
const { email } = req.body
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
return res.status(400).json({ error: 'Email is required' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const test = await mailchimp.lists.addListMember(process.env.MAILCHIMP_AUDIENCE_ID, {
|
||||||
|
email_address: email,
|
||||||
|
status: 'subscribed',
|
||||||
|
})
|
||||||
|
return res.status(201).json({ error: '' })
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(500).json({ error: error.message || error.toString() })
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,8 @@ import siteMetadata from '@/data/siteMetadata'
|
|||||||
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
import { getAllFilesFrontMatter } from '@/lib/mdx'
|
||||||
import formatDate from '@/lib/utils/formatDate'
|
import formatDate from '@/lib/utils/formatDate'
|
||||||
|
|
||||||
|
import { FormSubscribe } from '@/components/FormSubscribe'
|
||||||
|
|
||||||
const MAX_DISPLAY = 5
|
const MAX_DISPLAY = 5
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
@ -89,6 +91,11 @@ export default function Home({ posts }) {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{siteMetadata.newsletter.provider !== '' && (
|
||||||
|
<div className="flex items-center justify-center pt-4">
|
||||||
|
<FormSubscribe />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user