Tag: Next.js

Helpful Tips for Starting a Next.js Chrome Extension

I recently rewrote one of my projects — Minimal Theme for Twitter — as a Next.js Chrome extension because I wanted to use React for the pop-up. Using React would allow me to clearly separate my extension’s pop-up component and its application logic from its content scripts, which are the CSS and JavaScript files needed to execute the functionality of the extension.

As you may know, there are several ways to get started with React, from simply adding script tags to using a recommended toolchain like Create React App, Gatsby, or Next.js. There are some immediate benefits you get from Next.js as a React framework, like the static HTML feature you get with next export. While features like preloading JavaScript and built-in routing are great, my main goal with rewriting my Chrome extension was better code organization, and that’s really where Next.js shines. It gives you the most out-of-the-box for the least amount of unnecessary files and configuration. I tried fiddling around with Create React App and it has a surprising amount of boilerplate code that I didn’t need.

I thought it might be straightforward to convert over to a Next.js Chrome extension since it’s possible to export a Next.js application to static HTML. However, there are some gotchas involved, and this article is where I tell you about them so you can avoid some mistakes I made.

First, here’s the GitHub repo if you want to skip straight to the code.

New to developing Chrome extensions? Sarah Drasner has a primer to get you started.

Folder structure

next-export is a post-processing step that compiles your Next.js code, so we don’t need to include the actual Next.js or React code in the extension. This allows us to keep our extension at its lowest possible file size, which is what we want for when the extension is eventually published to the Chrome Web Store.

So, here’s how the code for my Next.js Chrome extension is organized. There are two directories — one for the extension’s code, and one containing the Next.js app.

📂 extension   📄 manifest.json 📂 next-app   📂 pages   📂 public   📂 styles   📄 package.json README.md

The build script

To use next export in a normal web project, you would modify the default Next.js build script in package.json to this:

"scripts": {   "build": "next build && next export" }

Then, running npm run build (or yarn build) generates an out directory.

In this case involving a Chrome extension, however, we need to export the output to our extension directory instead of out. Plus, we have to rename any files that begin with an underscore (_), as Chrome will fire off a warning that “Filenames starting with “_” are reserved for use by the system.”

Screenshot of the Next.js Chrome Extension Chrome Extension Store with a failed to load extension error pop-up.
What we need is a way to customize those filenames so Chrome is less cranky.

This leads us to have a new build script like this:

"scripts": {   "build": "next build && next export && mv out/_next out/next && sed -i '' -e 's//_next/./next/g' out/**.html && mv out/index.html ../extension && rsync -va --delete-after out/next/ ../extension/next/" }

sed on works differently on MacOS than it does on Linux. MacOS requires the '' -e flag to work correctly. If you’re on Linux you can omit that additional flag.

Assets

If you are using any assets in the public folder of your Next.js project, we need to bring that into our Chrome extension folder as well. For organization, adding a next-assets folder inside public ensures your assets aren’t output directly into the extension directory.

The full build script with assets is this, and it’s a big one:

"scripts": {   "build": "next build && next export && mv out/_next out/next && sed -i '' -e 's//_next/./next/g' out/**.html && mv out/index.html ../extension && rsync -va --delete-after out/next/ ../extension/next/ && rm -rf out && rsync -va --delete-after public/next-assets ../extension/" }

Chrome Extension Manifest

The most common pattern for activating a Chrome extension is to trigger a pop-up when the extension is clicked. We can do that in Manifest V3 by using the action keyword. And in that, we can specify default_popup so that it points to an HTML file.

Here we are pointing to an index.html from Next.js:

{   "name": "Next Chrome",   "description": "Next.js Chrome Extension starter",   "version": "0.0.1",   "manifest_version": 3,   "action": {     "default_title": "Next.js app",     "default_popup": "index.html"   } }

The action API replaced browserAction and pageAction` in Manifest V3.

Next.js features that are unsupported by Chrome extensions

Some Next.js features require a Node.js web server, so server-related features, like next/image, are unsupported by a Chrome extension.

Start developing

Last step is to test the updated Next.js Chrome extension. Run npm build (or yarn build) from the next-app directory, while making sure that the manifest.json file is in the extension directory.

Then, head over to chrome://extensions in a new Chrome browser window, enable Developer Mode*,* and click on the Load Unpacked button. Select your extension directory, and you should be able to start developing!

Screenshot of Chrome open to Google's homepage and a Next.js Chrome extension pop-up along the right side.

Wrapping up

That’s it! Like I said, none of this was immediately obvious to me as I was getting started with my Chrome extension rewrite. But hopefully now you see how relatively straightforward it is to get the benefits of Next.js development for developing a Chrome extension. And I hope it saves you the time it took me to figure it out!


Helpful Tips for Starting a Next.js Chrome Extension originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , , ,

How to Create a Contact Form With Next.js and Netlify

We’re going to create a contact form with Next.js and Netlify that displays a confirmation screen and features enhanced spam detection.

Next.js is a powerful React framework for developing performant React applications that scale. By integrating a Next.js site with Netlify’s technology, we can quickly get a working contact form up and running without having to write any server-side code.

Not only is it a relatively fast process to set up forms to be processed by Netlify, but it’s also free to get started (with up to 100 free submissions/per site hosted on Netlify). Form submissions automatically go through Netlify’s built-in spam filter which uses Akismet and there are also options that can be configured to increase the level of spam detection.

Creating the contact form

Within the Next.js application we should create a ContactForm component to render the contact form inside of the contact page. If you’d like for this form to render at /contact, then the ContactForm component below with labels and input fields should be used within the pages/contact.js file.

const ContactForm = (   <form     name="contact-form"     method="POST"     action="contact/?success=true"   >     <label htmlFor="name">Name *</label>     <input       id="name"       name="name"       required       type="text"     />     <label htmlFor="company">Company *</label>     <input id="company" name="company" required type="text" />     <label htmlFor="email">E-mail Address *</label>     <input id="email" type="email" name="email" required />     <label htmlFor="message">Message *</label>     <textarea id="message" name="message" required></textarea>     <button type="submit">Submit</button>   </form> );

The above markup is required to render a form with a field for Name, Company, Email address and message with a submit button. When submitting the form, based on the value of the form’s action, it should redirect to contact/?success=true from /contact. Right now there is not yet a difference between the page’s appearance with and without the success query parameter, but we will update that later.

Our Contact.js file looks like this so far:

import React from "react"; const ContactPage = () => {  const ContactForm = (/* code in above code sample*/)    return (    <div>      <h1>Contact Us</h1>      {ContactForm}    </div>  ); };   export default ContactPage;

Now that we have the basic form set up, the real magic will happen after we add additional information for Netlify to auto-recognize the form during future site deployments. To accomplish this we should update the form to have the attribute data-netlify="true" and a hidden input field that contains the name of our contact form. In Netlify, once we navigate to our site in the dashboard and then click on the “forms” tab  we will be able to view our form responses based on the name that we’ve put in our hidden field. It’s important that if you have multiple forms within a site that they have unique names so that they are recorded properly in Netlify.

<form   method="POST"   name="contact-form"   action="contact/?success=true"   data-netlify="true" > <input type="hidden" name="form-name" value="contact-form" />

After successfully deploying the site to Netlify with the data-netlify attribute and the form-name field  then we can go to the deployed version of the site and fill out the form. Upon submitting the form and navigating to https://app.netlify.com/sites/site-name/forms (where site-name is the name of your site) then our most recent form submission should appear if we have successfully set up the form. 

Redirect to confirmation screen 

In order to improve the user experience, we should add some logic to redirect to a confirmation screen on form submission when the URL changes to /contact/?success=true. There is also the option to redirect to an entirely different page as the action when the form is submitted but using query params we can achieve something similar with the Next Router. We can accomplish this by creating a new variable to determine if the confirmation screen or the form should be visible based on the query parameter. The next/router which is imported with import { useRouter } from "next/router"; can be used to retrieve the current query params. 

const router = useRouter();   const confirmationScreenVisible = router.query?.success && router.query.success === "true";

In our case, the confirmation screen and form can never be visible at the same time; therefore, the following statement can be used to determine if the form is visible or not.

const formVisible = !confirmationScreenVisible; 

To give users the option to resubmit the form, we can add a button to the confirmation screen to reset the form by clearing the query params. Using router.replace (instead of router.push) not only updates the page but replaces the current page in the history to the version without query params. 

<button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>

We can then conditionally render the form based on whether or not the form is visible with:

{formVisible ? ContactForm : ConfirmationMessage}

Putting it all together, we can use the following code to conditionally render the form based on the query params (which are updated when the form is submitted):

import React, { useState } from "react"; import { useRouter } from "next/router";   const ContactPage = () => {  const [submitterName, setSubmitterName] = useState("");  const router = useRouter();  const confirmationScreenVisible =    router.query?.success && router.query.success === "true";  const formVisible = !confirmationScreenVisible;    const ConfirmationMessage = (    <React.Fragment>      <p>        Thank you for submitting this form. Someone should get back to you within 24-48 hours.      </p>        <button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>    </React.Fragment>  );    const ContactForm = (/* code in first code example */);    return (    <div>      <h1>Contact Us</h1> {formVisible ? ContactForm : ConfirmationMessage}    </div>  ); };   export default ContactPage;

Adding a hidden bot field

Now that the core functionality of our form is working, we can add additional spam detection to our form in addition to the base spam detection because Akismet is included with all Netlify Forms by default. We can enable this by adding data-netlify-honeypot="bot-field" to our form.

<form   className="container"   method="POST"   name="contact-form"   action="contact/?success=true"   data-netlify="true"   data-netlify-honeypot="bot-field" >

We also need to create a new hidden paragraph that contains a label named bot-field that contains the input. This field is “visible” to bots, but not humans. When this honeypot form field is filled, Netlify detects a bot and then the submission is flagged as spam.

<p hidden>   <label>     Don’t fill this out: <input name="bot-field" />   </label> </p>

Further customizations

  • We could explore another spam prevention option that Netlify supports by adding reCAPTCHA 2 to a Netlify form.
  • We could update the form to allow uploaded files with input <input type="file">.
  • We could set up notifications for form submissions. That happens over at https://app.netlify.com/sites/[your-site-name]/settings/forms where we can include a custom subject field (which can be hidden) for email notifications.

Full code

The code for the full site code is available over at GitHub.

 Bonus

The following code includes everything we covered as well as the logic for setting a custom subject line with what was submitted in the name field.

import React, { useState } from "react"; import { useRouter } from "next/router";   const ContactPage = () => {  const [submitterName, setSubmitterName] = useState("");  const router = useRouter();  const confirmationScreenVisible =    router.query?.success && router.query.success === "true";  const formVisible = !confirmationScreenVisible;    const ConfirmationMessage = (    <React.Fragment>      <p>        Thank you for submitting this form. Someone should get back to you        within 24-48 hours.      </p>        <button onClick={() => router.replace("/contact", undefined, { shallow: true })}> Submit Another Response </button>    </React.Fragment>  );    const ContactForm = (    <form      className="container"      method="POST"      name="contact-form"      action="contact/?success=true"      data-netlify="true"      data-netlify-honeypot="bot-field"    >      <input        type="hidden"        name="subject"        value={`You've got mail from $ {submitterName}`}      />      <input type="hidden" name="form-name" value="contact-form" />      <p hidden>        <label>          Don’t fill this out: <input name="bot-field" />        </label>      </p>        <label htmlFor="name">Name *</label>      <input        id="name"        name="name"        required        onChange={(e) => setSubmitterName(e.target.value)}        type="text"      />      <label htmlFor="company">Company *</label>      <input id="company" name="company" required type="text" />      <label htmlFor="email">E-mail Address *</label>      <input id="email" type="email" name="email" required />      <label htmlFor="message">Message *</label>      <textarea id="message" name="message" required/>      <button type="submit">Submit</button>    </form>  );    return (    <div>      <h1>Contact Us</h1> {formVisible ? ContactForm : ConfirmationMessage}    </div>  ); };   export default ContactPage;

The post How to Create a Contact Form With Next.js and Netlify appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Building a Tennis Trivia App With Next.js and Netlify

Today we will be learning how to build a tennis trivia app using Next.js and Netlify. This technology stack has become my go-to on many projects. It allows for rapid development and easy deployment.

Without further ado let’s jump in!

What we’re using

  • Next.js
  • Netlify
  • TypeScript
  • Tailwind CSS

Why Next.js and Netlify

You may think that this is a simple app that might not require a React framework. The truth is that Next.js gives me a ton of features out of the box that allow me to just start coding the main part of my app. Things like webpack configuration, getServerSideProps, and Netlify’s automatic creation of serverless functions are a few examples.

Netlify also makes deploying a Next.js git repo super easy. More on the deployment a bit later on.

What we’re building

Basically, we are going to build a trivia game that randomly shows you the name of a tennis player and you have to guess what country they are from. It consists of five rounds and keeps a running score of how many you get correct.

The data we need for this application is a list of players along with their country. Initially, I was thinking of querying some live API, but on second thought, decided to just use a local JSON file. I took a snapshot from RapidAPI and have included it in the starter repo.

The final product looks something like this:

You can find the final deployed version on Netlify.

Starter repo tour

If you want to follow along you can clone this repository and then go to the start branch:

git clone git@github.com:brenelz/tennis-trivia.git cd tennis-trivia git checkout start

In this starter repo, I went ahead and wrote some boilerplate to get things going. I created a Next.js app using the command npx create-next-app tennis-trivia. I then proceeded to manually change a couple JavaScript files to .ts and .tsx. Surprisingly, Next.js automatically picked up that I wanted to use TypeScript. It was too easy! I also went ahead and configured Tailwind CSS using this article as a guide.

Enough talk, let’s code!

Initial setup

The first step is setting up environment variables. For local development, we do this with a .env.local file. You can copy the .env.sample from the starter repo.

cp .env.sample .env.local

Notice it currently has one value, which is the path of our application. We will use this on the front end of our app, so we must prefix it with NEXT_PUBLIC_.

Finally, let’s use the following commands to install the dependencies and start the dev server: 

npm install npm run dev

Now we access our application at http://localhost:3000. We should see a fairly empty page with just a headline:

Creating the UI markup

In pages/index.tsx, let’s add the following markup to the existing Home() function:

export default function Home() {   return (     <div className="bg-blue-500">     <div className="max-w-2xl mx-auto text-center py-16 px-4 sm:py-20 sm:px-6 lg:px-8">       <h2 className="text-3xl font-extrabold text-white sm:text-4xl">         <span className="block">Tennis Trivia - Next.js Netlify</span>       </h2>       <div>         <p className="mt-4 text-lg leading-6 text-blue-200">           What country is the following tennis player from?         </p>         <h2 className="text-lg font-extrabold text-white my-5">           Roger Federer         </h2>          <form>           <input             list="countries"             type="text"             className="p-2 outline-none"             placeholder="Choose Country"           />           <datalist id="countries">             <option>Switzerland</option>            </datalist>            <p>              <button                className="mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"                type="submit"              >                Guess             </button>           </p>         </form>          <p className="mt-4 text-lg leading-6 text-white">           <strong>Current score:</strong> 0         </p>       </div>     </div>     </div>   );

This forms the scaffold for our UI. As you can see, we are using lots of utility classes from Tailwind CSS to make things look a little prettier. We also have a simple autocomplete input and a submit button. This is where you will select the country you think the player is from and then hit the button. Lastly, at the bottom, there is a score that changes based on correct or incorrect answers.

Setting up our data

If you take a look at the data folder, there should be a tennisPlayers.json with all the data we will need for this application. Create a lib folder at the root and, inside of it, create a players.ts file. Remember, the .ts extension is required since is a TypeScript file. Let’s define a type that matches our JSON data..

export type Player = {   id: number,   first_name: string,   last_name: string,   full_name: string,   country: string,   ranking: number,   movement: string,   ranking_points: number, };

This is how we create a type in TypeScript. We have the name of the property on the left, and the type it is on the right. They can be basic types, or even other types themselves.

From here, let’s create specific variables that represent our data:

export const playerData: Player[] = require("../data/tennisPlayers.json"); export const top100Players = playerData.slice(0, 100);  const allCountries = playerData.map((player) => player.country).sort(); export const uniqueCountries = [...Array.from(new Set(allCountries))];

A couple things to note is that we are saying our playerData is an array of Player types. This is denoted by the colon followed by the type. In fact, if we hover over the playerData we can see its type:

In that last line we are getting a unique list of countries to use in our country dropdown. We pass our countries into a JavaScript Set, which gets rid of the duplicate values. We then create an array from it, and spread it into a new array. It may seem unnecessary but this was done to make TypeScript happy.

Believe it or not, that is really all the data we need for our application!

Let’s make our UI dynamic!

All our values are hardcoded currently, but let’s change that. The dynamic pieces are the tennis player’s name, the list of countries, and the score.

Back in pages/index.tsx, let’s modify our getServerSideProps function to create a list of five random players as well as pull in our uniqueCountries variable.

import { Player, uniqueCountries, top100Players } from "../lib/players"; ... export async function getServerSideProps() {   const randomizedPlayers = top100Players.sort((a, b) => 0.5 - Math.random());   const players = randomizedPlayers.slice(0, 5);    return {     props: {       players,       countries: uniqueCountries,     },   }; }

Whatever is in the props object we return will be passed to our React component. Let’s use them on our page:

type HomeProps = {   players: Player[];   countries: string[]; };  export default function Home({ players, countries }: HomeProps) {   const player = players[0];   ... }  

As you can see, we define another type for our page component. Then we add the HomeProps type to the Home() function. We have again specified that players is an array of the Player type.

Now we can use these props further down in our UI. Replace “Roger Federer” with {player.full_name} (he’s my favorite tennis player by the way). You should be getting nice autocompletion on the player variable as it lists all the property names we have access to because of the types that we defined.

Further down from this, let’s now update the list of countries to this:

<datalist id="countries">   {countries.map((country, i) => (     <option key={i}>{country}</option>   ))} </datalist>

Now that we have two of the three dynamic pieces in place, we need to tackle the score. Specifically, we need to create a piece of state for the current score.

export default function Home({ players, countries }: HomeProps) {   const [score, setScore] = useState(0);   ... }

Once this is done, replace the 0 with {score} in our UI.

You can now check our progress by going to http://localhost:3000. You can see that every time the page refreshes, we get a new name; and when typing in the input field, it lists all of the available unique countries.

Adding some interactivity

We’ve come a decent way but we need to add some interactivity.

Hooking up the guess button

For this we need to have some way of knowing what country was picked. We do this by adding some more state and attaching it to our input field.

export default function Home({ players, countries }: HomeProps) {   const [score, setScore] = useState(0);   const [pickedCountry, setPickedCountry] = useState("");   ...   return (     ...     <input       list="countries"       type="text"       value={pickedCountry}       onChange={(e) => setPickedCountry(e.target.value)}       className="p-2 outline-none"       placeholder="Choose Country"     />    ...   ); }

Next, let’s add a guessCountry function and attach it to the form submission:

const guessCountry = () => {   if (player.country.toLowerCase() === pickedCountry.toLowerCase()) {     setScore(score + 1);   } else {     alert(‘incorrect’);   } }; ... <form   onSubmit={(e) => {     e.preventDefault();     guessCountry();   }} >

All we do is basically compare the current player’s country to the guessed country. Now, when we go back to the app and guess the country right, the score increases as expected.

Adding a status indicator

To make this a bit nicer, we can render some UI depending whether the guess is correct or not.

So, let’s create another piece of state for status, and update the guess country method:

const [status, setStatus] = useState(null); ... const guessCountry = () => {   if (player.country.toLowerCase() === pickedCountry.toLowerCase()) {     setStatus({ status: "correct", country: player.country });     setScore(score + 1);   } else {     setStatus({ status: "incorrect", country: player.country });   } };

Then render this UI below the player name:

{status && (   <div className="mt-4 text-lg leading-6 text-white">     <p>             You are {status.status}. It is {status.country}     </p>     <p>       <button         autoFocus         className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"       >         Next Player       </button>     </p>   </div> )}

Lastly, we want to make sure our input field doesn’t show when we are in a correct or incorrect status. We achieve this by wrapping the form with the following:

{!status && (   <form>   ...   </form> )}

Now, if we go back to the app and guess the player’s country, we get a nice message with the result of the guess.

Progressing through players

Now probably comes the most challenging part: How do we go from one player to the next?

First thing we need to do is store the currentStep in state so that we can update it with a number from 0 to 4. Then, when it hits 5, we want to show a completed state since the trivia game is over.

Once again, let’s add the following state variables:

const [currentStep, setCurrentStep] = useState(0); const [playersData, setPlayersData] = useState(players);

…then replace our previous player variable with:

const player = playersData[currentStep];

Next, we create a nextStep function and hook it up to the UI:

const nextStep = () => {   setPickedCountry("");   setCurrentStep(currentStep + 1);   setStatus(null); }; ... <button   autoFocus   onClick={nextStep}   className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-blue-600 bg-white hover:bg-blue-50 sm:w-auto"  >     Next Player </button>

Now, when we make a guess and hit the next step button, we’re taken to a new tennis player. Guess again and we see the next, and so on. 

What happens when we hit next on the last player? Right now, we get an error. Let’s fix that by adding a conditional that represents that the game has been completed. This happens when the player variable is undefined.

{player ? (   <div>     <p className="mt-4 text-lg leading-6 text-blue-200">       What country is the following tennis player from?     </p>     ...     <p className="mt-4 text-lg leading-6 text-white">       <strong>Current score:</strong> {score}     </p>   </div> ) : (   <div>     <button       autoFocus       className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50 sm:w-auto"       >       Play Again     </button>   </div> )}

Now we see a nice completed state at the end of the game.

Play again button

We are almost done! For our “Play Again” button we want to reset the state all of the game. We also want to get a new list of players from the server without needing a refresh. We do it like this:

const playAgain = async () => {   setPickedCountry("");   setPlayersData([]);   const response = await fetch(     process.env.NEXT_PUBLIC_API_URL + "/api/newGame"   );   const data = await response.json();   setPlayersData(data.players);   setCurrentStep(0);   setScore(0); };  <button   autoFocus   onClick={playAgain}   className="outline-none mt-8 w-full inline-flex items-center justify-center px-5 py-3 border border-transparent text-base font-medium rounded-md text-indigo-600 bg-white hover:bg-indigo-50 sm:w-auto" >   Play Again </button>

Notice we are using the environment variable we set up before via the process.env object. We are also updating our playersData by overriding our server state with our client state that we just retrieved.

We haven’t filled out our newGame route yet, but this is easy with Next.js and Netlify serverless functions . We only need to edit the file in pages/api/newGame.ts.

import { NextApiRequest, NextApiResponse } from "next" import { top100Players } from "../../lib/players";  export default (req: NextApiRequest, res: NextApiResponse) => {   const randomizedPlayers = top100Players.sort((a, b) => 0.5 - Math.random());   const top5Players = randomizedPlayers.slice(0, 5);   res.status(200).json({players: top5Players}); }

This looks much the same as our getServerSideProps because we can reuse our nice helper variables.

If we go back to the app, notice the “Play Again” button works as expected.

Improving focus states

One last thing we can do to improve our user experience is set the focus on the country input field every time the step changes. That’s just a nice touch and convenient for the user. We do this using a ref and a useEffect:

const inputRef = useRef(null); ... useEffect(() => {   inputRef?.current?.focus(); }, [currentStep]);  <input   list="countries"   type="text"   value={pickedCountry}   onChange={(e) => setPickedCountry(e.target.value)}   ref={inputRef}   className="p-2 outline-none"   placeholder="Choose Country" />

Now we can navigate much easier just using the Enter key and typing a country.

Deploying to Netlify

You may be wondering how we deploy this thing. Well, using Netlify makes it so simple as it detects a Next.js application out of the box and automatically configures it.

All I did was set up a GitHub repo and connect my GitHub account to my Netlify account. From there, I simply pick a repo to deploy and use all the defaults.

The one thing to note is that you have to add the NEXT_PUBLIC_API_URL environment variable and redeploy for it to take effect.

You can find my final deployed version here.

Also note that you can just hit the “Deploy to Netlify” button on the GitHub repo.

Conclusion

Woohoo, you made it! That was a journey and I hope you learned something about React, Next.js, and Netlify along the way.

I have plans to expand this tennis trivia app to use Supabase in the near future so stay tuned!

If you have any questions/comments feel free to reach out to me on Twitter.


The post Building a Tennis Trivia App With Next.js and Netlify appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Architecting With Next.js

(This is a sponsored post.)

Free event hosted by Netlify coming up next week (Wednesday, August 25th): Architecting with Next.js. It’s just a little half-day thing. No brainer.

Join us for a special event where we’ll highlight business teams using Next.js in production, including architecture deep dives, best practices and challenges. Next.js is the fastest-growing framework for Jamstack developers. With a compelling developer experience and highly performant results, it’s an emerging choice for delivering customer-facing sites and apps.

Next.js is such a nice framework, it’s no surprise to me it’s blowing up. It’s in React, a framework familiar to tons of people, thus enabling component-based front-ends, with common niceties built right in, like CSS modules. It produces HTML output, so it’s fast and good for SEO. It has smart defaults, so you’re rarely doing stuff like schlubbing your way through webpack config (unless you need that control, then you can). It does basic routing without you having to code it. Good stuff.

Direct Link to ArticlePermalink


The post Architecting With Next.js appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

,
[Top]

Responsible Markdown in Next.js

Markdown truly is a great format. It’s close enough to plain text so that anyone can quickly learn it, and it’s structured enough that it can be parsed and eventually converted to you name it.

That being said: parsing, processing, enhancing, and converting Markdown needs code. Shipping all that code in the client comes at a cost. It’s not huge per se, but it’s still a few dozens of kilobytes of code that are used only to deal with Markdown and nothing else.

In this article, I want to explain how to keep Markdown out of the client in a Next.js application, using the Unified/Remark ecosystem (genuinely not sure which name to use, this is all super confusing).

General idea

The idea is to only use Markdown in the getStaticProps functions from Next.js so this is done during a build (or in a Next serverless function if using Vercel’s incremental builds), but never in the client. I guess getServerSideProps would also be fine, but I think getStaticProps is more likely to be the common use case.

This would return an AST (Abstract Syntax Tree, which is to say a big nested object describing our content) resulting from parsing and processing the Markdown content, and the client would only be responsible for rendering that AST into React components.

I guess we could even render the Markdown as HTML directly in getStaticProps and return that to render with dangerouslySetInnerHtml but we’re not that kind of people. Security matters. And also, flexibility of rendering Markdown the way we want with our components instead of it rendering as plain HTML. Seriously folks, do not do that. 😅

export const getStaticProps = async () => {   // Get the Markdown content from somewhere, like a CMS or whatnot. It doesn’t   // matter for the sake of this article, really. It could also be read from a   // file.   const markdown = await getMarkdownContentFromSomewhere()   const ast = parseMarkdown(markdown)    return { props: { ast } } }  const Page = props => {   // This would usually have your layout and whatnot as well, but omitted here   // for sake of simplicity of course.   return <MarkdownRenderer ast={props.ast} /> }  export default Page

Parsing Markdown

We are going to use the Unified/Remark ecosystem. We need to install unified and remark-parse and that’s about it. Parsing the Markdown itself is relatively straightforward:

import unified from 'unified' import markdown from 'remark-parse'  const parseMarkdown = content => unified().use(markdown).parse(content)  export default parseMarkdown

Now, what took me a long while to understand is why my extra plugins, like remark-prism or remark-slug, did not work like this. This is because the .parse(..) method from Unified does not process the AST with plugins. As the name suggests, it only parses the string of Markdown content into a tree.

If we want Unified to apply our plugins, we need Unified to go through what they call the “run” phase. Normally, this is done by using the .process(..) method instead of the .parse(..) method. Unfortunately, .process(..) not only parses Markdown and applies plugins, but also stringifies the AST into another format (like HTML via remark-html, or JSX with remark-react). And this is not what we want, as we want to preserve the AST, but after it’s been processed by plugins.

| ........................ process ........................... | | .......... parse ... | ... run ... | ... stringify ..........|            +--------+                     +----------+ Input ->- | Parser | ->- Syntax Tree ->- | Compiler | ->- Output           +--------+          |          +----------+                               X                               |                        +--------------+                        | Transformers |                        +--------------+

So what we need to do is run both the parsing and running phases, but not the stringifying phase. Unified does not provide a method to do these 2 out of 3 phases, but it provides individual methods for every phase, so we can do it manually:

import unified from 'unified' import markdown from 'remark-parse' import prism from 'remark-prism'  const parseMarkdown = content => {   const engine = unified().use(markdown).use(prism)   const ast = engine.parse(content)    // Unified‘s *process* contains 3 distinct phases: parsing, running and   // stringifying. We do not want to go through the stringifying phase, since we   // want to preserve an AST, so we cannot call `.process(..)`. Calling   // `.parse(..)` is not enough though as plugins (so Prism) are executed during   // the running phase. So we need to manually call the run phase (synchronously   // for simplicity).   // See: https://github.com/unifiedjs/unified#description   return engine.runSync(ast) }

Tada! We parsed our Markdown into a syntax tree. And then we ran our plugins on that tree (done here synchronously for sake of simplicity, but you could use .run(..) to do it asynchronously). But we did not convert our tree into some other syntax like HTML or JSX. We can do that ourselves, in the render.

Rendering Markdown

Now that we have our cool tree at the ready, we can render it the way we intend to. Let’s have a MarkdownRenderer component that receives the tree as an ast prop, and renders it all with React components.

const getComponent = node => {   switch (node.type) {     case 'root':       return React.Fragment      case 'paragraph':       return 'p'      case 'emphasis':       return 'em'      case 'heading':       return ({ children, depth = 2 }) => {         const Heading = `h$  {depth}`         return <Heading>{children}</Heading>       }      /* Handle all types here … */      default:       console.log('Unhandled node type', node)       return React.Fragment   } }  const Node = node => {   const Component = getComponent(node)   const { children } = node    return children ? (     <Component {...node}>       {children.map((child, index) => (         <Node key={index} {...child} />       ))}     </Component>   ) : (     <Component {...node} />   ) }  const MarkdownRenderer = props => <Node {...props.ast} />  export default React.memo(MarkdownRenderer)

Most of the logic of our renderer lives in the Node component. It finds out what to render based on the type key of the AST node (this is our getComponent method handling every type of node), and then renders it. If the node has children, it recursively goes into the children; otherwise it just renders the component as a final leaf.

Cleaning up the tree

Depending on which Remark plugins we use, we might encounter the following problem when trying to render our page:

Error: Error serializing .content[0].content.children[3].data.hChildren[0].data.hChildren[0].data.hChildren[0].data.hChildren[0].data.hName returned from getStaticProps in “/”. Reason: undefined cannot be serialized as JSON. Please use null or omit this value.

This happens because our AST contains keys whose values are undefined, which is not something that can be safely serialized as JSON. Next gives us the solution: either we omit the value entirely, or if we need it somewhat, replace it with null.

We’re not going to fix every path by hand though, so we need to walk that AST recursively and clean it up. I found out that this happened when using remark-prism, a plugin to enable syntax highlighting for code blocks. The plugin indeed adds a [data] object to nodes.

What we can do is walk our AST before returning it to clean up these nodes:

const cleanNode = node => {   if (node.value === undefined) delete node.value   if (node.tagName === undefined) delete node.tagName   if (node.data) {     delete node.data.hName     delete node.data.hChildren     delete node.data.hProperties   }    if (node.children) node.children.forEach(cleanNode)    return node }  const parseMarkdown = content => {   const engine = unified().use(markdown).use(prism)   const ast = engine.parse(content)   const processedAst = engine.runSync(parsed)    cleanNode(processedAst)    return processedAst }

One last thing we can do to ship less data to the client is remove the position object which exists on every single node and holds the original position in the Markdown string. It’s not a big object (it has only two keys), but when the tree gets big, it adds up quickly.

const cleanNode = node => {   delete node.position 

Wrapping up

That’s it folks! We managed to restrict Markdown handling to the build-/server-side code so we don’t ship a Markdown runtime to the browser, which is unnecessarily costly. We pass a tree of data to the client, which we can walk and convert into whatever React components we want.

I hope this helps. 🙂


The post Responsible Markdown in Next.js appeared first on CSS-Tricks. You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, ,
[Top]

Next.js on Netlify

(This is a sponsored post.)

If you want to put Next.js on Netlify, here’s a 5 minute tutorial¹. One of the many strengths of Next.js is that it can do server-side rendering (SSR) with a Node server behind it. But Netlify does static hosting not Node hosting, right? Well Netlify has functions, and those functions can handle the SSR. But you don’t even really need to know that, you can just use the plugin.

Need a little bit more hand-holding than that? You got it, Cassidy is doing a free Webinar about all the next Thursday (March 4th, 2021) at 9am Pacific. That way you can watch live and ask questions and stuff. Netlify has a bunch of webinars they have now smartly archived on a new resources site.

  1. I’ve also mentioned this before if it sounds familiar, the fact that it supports the best of the entire rendering spectrum is very good.

Direct Link to ArticlePermalink


The post Next.js on Netlify appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

,
[Top]

Netlify & Next.js

Cassidy Williams has been doing a Blogvent (blogging every day for a month) over on the Netlify Blog. A lot of the blog posts are about Next.js. There is a lot to like about Next.js. I just pulled one of Cassidy’s starters for fun. It’s very nice that it has React Fast-Refresh built-in. I like how on any given “Page” you can import and use a <Head> to control stuff that would be in a <head>. This was my first tiny little play with Next so, excuse my basicness.

But the most compelling thing about Next.js, to me, is how easily it supports the entire rendering spectrum. It encourages you to do static-file rendering by default (smart), then if you need to do server-side rendering (SSR), you just update any given Page component to have this:

export async function getServerSideProps() {   // Fetch data from external API   const res = await fetch(`https://.../data`)   const data = await res.json()    // Pass data to the page via props   return { props: { data } } }

The assumption is that you’re doing SSR because you need to hit a server for data in order to render the page, but would prefer to do that server-side so that the page can render quickly and without JavaScript if needed (great for SEO). That assumes a Node server is sitting there ready to do that work. On Netlify, that means a function (Node Lambda), but you barely even have to think about it, because you just put this in your netlify.toml file:

[[plugins]]   package = "@netlify/plugin-nextjs"

Now you’ve got static where you need it, server-rendered where you need it, but you aren’t giving up on client-side rendering either, which is nice and fast after the site is all booted up. I think it shoots some JSON around or something, framework magic.

I set up a quick SSR route off my homepage to have a play, and I can clearly see that both my homepage (static) and /cool route (SSR) both return static HTML on load.

I even had to prettify this source, as you HTML minification out of the box

I admit I like working in React, and Next.js is a darn nice framework to do it with because of the balance of simplicity and power. It’s great it runs on Netlify so easily.


The post Netlify & Next.js appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

,
[Top]

How to Create a Commenting Engine with Next.js and Sanity

One of the arguments against the Jamstack approach for building websites is that developing features gets complex and often requires a number of other services. Take commenting, for example. To set up commenting for a Jamstack site, you often need a third-party solution such as Disqus, Facebook, or even just a separate database service. That third-party solution usually means your comments live disconnected from their content.

When we use third-party systems, we have to live with the trade-offs of using someone else’s code. We get a plug-and-play solution, but at what cost? Ads displayed to our users? Unnecessary JavaScript that we can’t optimize? The fact that the comments content is owned by someone else? These are definitely things worth considering.

Monolithic services, like WordPress, have solved this by having everything housed under the same application. What if we could house our comments in the same database and CMS as our content, query it in the same way we query our content, and display it with the same framework on the front end?

It would make this particular Jamstack application feel much more cohesive, both for our developers and our editors.

Let’s make our own commenting engine

In this article, we’ll use Next.js and Sanity.io to create a commenting engine that meets those needs. One unified platform for content, editors, commenters, and developers.

Why Next.js?

Next.js is a meta-framework for React, built by the team at Vercel. It has built-in functionality for serverless functions, static site generation, and server-side rendering.

For our work, we’ll mostly be using its built-in “API routes” for serverless functions and its static site generation capabilities. The API routes will simplify the project considerably, but if you’re deploying to something like Netlify, these can be converted to serverless functions or we can use Netlify’s next-on-netlify package.

It’s this intersection of static, server-rendered, and serverless functions that makes Next.js a great solution for a project like this.

Why Sanity?

Sanity.io is a flexible platform for structured content. At its core, it is a data store that encourages developers to think about content as structured data. It often comes paired with an open-source CMS solution called the Sanity Studio.

We’ll be using Sanity to keep the author’s content together with any user-generated content, like comments. In the end, Sanity is a content platform with a strong API and a configurable CMS that allows for the customization we need to tie these things together.

Setting up Sanity and Next.js

We’re not going to start from scratch on this project. We’ll begin by using the simple blog starter created by Vercel to get working with a Next.js and Sanity integration. Since the Vercel starter repository has the front end and Sanity Studio separate, I’ve created a simplified repository that includes both.

We’ll clone this repository, and use it to create our commenting base. Want to see the final code? This “Starter” will get you set up with the repository, Vercel project, and Sanity project all connected.

The starter repo comes in two parts: the front end powered by Next.js, and Sanity Studio. Before we go any further, we need to get these running locally.

To get started, we need to set up our content and our CMS for Next to consume the data. First, we need to install the dependencies required for running the Studio and connecting to the Sanity API.

# Install the Sanity CLI globally npm install -g @sanity/cli # Move into the Studio directory and install the Studio's dependencies cd studio npm install

Once these finish installing, from within the /studio directory, we can set up a new project with the CLI.

# If you're not logged into Sanity via the CLI already sanity login # Run init to set up a new project (or connect an existing project) sanity init

The init command asks us a few questions to set everything up. Because the Studio code already has some configuration values, the CLI will ask us if we want to reconfigure it. We do.

From there, it will ask us which project to connect to, or if we want to configure a new project.

We’ll configure a new project with a descriptive project name. It will ask us to name the “dataset” we’re creating. This defaults to “production” which is perfectly fine, but can be overridden with whatever name makes sense for your project.

The CLI will modify the file ~/studio/sanity.json with the project’s ID and dataset name. These values will be important later, so keep this file handy.

For now, we’re ready to run the Studio locally.

# From within /studio npm run start

After the Studio compiles, it can be opened in the browser at http://localhost:3333.

At this point, it makes sense to go into the admin and create some test content. To make the front end work properly, we’ll need at least one blog post and one author, but additional content is always nice to get a feel for things. Note that the content will be synced in real-time to the data store even when you’re working from the Studio on localhost. It will become instantly available to query. Don’t forget to push publish so that the content is publicly available.

Once we have some content, it’s time to get our Next.js project running.

Getting set up with Next.js

Most things needed for Next.js are already set up in the repository. The main thing we need to do is connect our Sanity project to Next.js. To do this, there’s an example set of environment variables set in /blog-frontent/.env.local.example. Remove .example from that file and then we’ll modify the environment variables with the proper values.

We need an API token from our Sanity project. To create this value, let’s head over to the Sanity dashboard. In the dashboard, locate the current project and navigate to the Settings → API area. From here, we can create new tokens to use in our project. In many projects, creating a read-only token is all we need. In our project, we’ll be posting data back to Sanity, so we’ll need to create a Read+Write token.

Showing a modal open in the Sanity dashboard with a Add New Token heading, a text field to set the token label with a value of Comment Engine, and three radio buttons that set if the token as read, write or deploy studio access where the write option is selected.
Adding a new read and write token in the Sanity dashboard

When clicking “Add New Token,” we receive a pop-up with the token value. Once it’s closed, we can’t retrieve the token again, so be sure to grab it!

This string goes in our .env.local file as the value for SANITY_API_TOKEN. Since we’re already logged into manage.sanity.io , we can also grab the project ID from the top of the project page and paste it as the value of NEXT_PUBLIC_SANITY_PROJECT_ID. The SANITY_PREVIEW_SECRET is important for when we want to run Next.js in “preview mode”, but for the purposes of this demo, we don’t need to fill that out.

We’re almost ready to run our Next front-end. While we still have our Sanity Dashboard open, we need to make one more change to our Settings → API view. We need to allow our Next.js localhost server to make requests.

In the CORS Origins, we’ll add a new origin and populate it with the current localhost port: http://localhost:3000. We don’t need to be able to send authenticated requests, so we can leave this off When this goes live, we’ll need to add an additional Origin with the production URL to allow the live site to make requests as well.

Our blog is now ready to run locally!

# From inside /blog-frontend npm run dev

After running the command above, we now have a blog up and running on our computer with data pulling from the Sanity API. We can visit http://localhost:3000 to view the site.

Creating the schema for comments

To add comments to our database with a view in our Studio, we need to set up our schema for the data.

To add our schema, we’ll add a new file in our /studio/schemas directory named comment.js. This JavaScript file will export an object that will contain the definition of the overall data structure. This will tell the Studio how to display the data, as well as structuring the data that we will return to our frontend.

In the case of a comment, we’ll want what might be considered the “defaults” of the commenting world. We’ll have a field for a user’s name, their email, and a text area for a comment string. Along with those basics, we’ll also need a way of attaching the comment to a specific post. In Sanity’s API, the field type is a “reference” to another type of data.

If we wanted our site to get spammed, we could end there, but it would probably be a good idea to add an approval process. We can do that by adding a boolean field to our comment that will control whether or not to display a comment on our site.

export default {   name: 'comment',   type: 'document',   title: 'Comment',   fields: [     {       name: 'name',       type: 'string',     },     {       title: 'Approved',       name: 'approved',       type: 'boolean',       description: "Comments won't show on the site without approval"     },        {       name: 'email',       type: 'string',     },     {       name: 'comment',       type: 'text',     },     {       name: 'post',       type: 'reference',       to: [         {type: 'post'}       ]     }   ], }

After we add this document, we also need to add it to our /studio/schemas/schema.js file to register it as a new document type.

import createSchema from 'part:@sanity/base/schema-creator' import schemaTypes from 'all:part:@sanity/base/schema-type' import blockContent from './blockContent' import category from './category' import post from './post' import author from './author' import comment from './comment' // <- Import our new Schema export default createSchema({   name: 'default',   types: schemaTypes.concat([     post,     author,     category,     comment, // <- Use our new Schema     blockContent   ]) }) 

Once these changes are made, when we look into our Studio again, we’ll see a comment section in our main content list. We can even go in and add our first comment for testing (since we haven’t built any UI for it in the front end yet).

An astute developer will notice that, after adding the comment, the preview our comments list view is not very helpful. Now that we have data, we can provide a custom preview for that list view.

Adding a CMS preview for comments in the list view

After the fields array, we can specify a preview object. The preview object will tell Sanity’s list views what data to display and in what configuration. We’ll add a property and a method to this object. The select property is an object that we can use to gather data from our schema. In this case, we’ll take the comment’s name, comment, and post.title values. We pass these new variables into our prepare() method and use that to return a title and subtitle for use in list views.

export default {   // ... Fields information   preview: {       select: {         name: 'name',         comment: 'comment',         post: 'post.title'       },       prepare({name, comment, post}) {         return {           title: `$ {name} on $ {post}`,           subtitle: comment         }       }     }   }  }

The title will display large and the subtitle will be smaller and more faded. In this preview, we’ll make the title a string that contains the comment author’s name and the comment’s post, with a subtitle of the comment body itself. You can configure the previews to match your needs.

The data now exists, and our CMS preview is ready, but it’s not yet pulling into our site. We need to modify our data fetch to pull our comments onto each post.

Displaying each post’s comments

In this repository, we have a file dedicated to functions we can use to interact with Sanity’s API. The /blog-frontend/lib/api.js file has specific exported functions for the use cases of various routes in our site. We need to update the getPostAndMorePosts function in this file, which pulls the data for each post. It returns the proper data for posts associated with the current page’s slug, as well as a selection of new posts to display alongside it.

In this function, there are two queries: one to grab the data for the current post and one for the additional posts. The request we need to modify is the first request.

Changing the returned data with a GROQ projection

The query is made in the open-source graph-based querying language GROQ, used by Sanity for pulling data out of the data store. The query comes in three parts:

  • The filter – what set of data to find and send back *[_type == "post" && slug.current == $ slug]
  • An optional pipeline component — a modification to the data returned by the component to its left | order(_updatedAt desc)
  • An optional projection — the specific data elements to return for the query. In our case, everything between the brackets ({}).

In this example, we have a variable list of fields that most of our queries need, as well as the body data for the blog post. Directly following the body, we want to pull all the comments associated with this post.

In order to do this, we create a named property on the object returned called 'comments' and then run a new query to return the comments that contain the reference to the current post context.

The entire filter looks like this:

*[_type == "comment" && post._ref == ^._id && approved == true]

The filter matches all documents that meet the interior criteria of the square brackets ([]). In this case, we’ll find all documents of _type == "comment". We’ll then test if the current post’s _ref matches the comment’s _id. Finally, we check to see if the comment is approved == true.

Once we have that data, we select the data we want to return using an optional projection. Without the projection, we’d get all the data for each comment. Not important in this example, but a good habit to be in.

curClient.fetch(     `*[_type == "post" && slug.current == $ slug] | order(_updatedAt desc) {         $ {postFields}         body,         'comments': *[_type == "comment" && post._ref == ^._id && approved == true]{             _id,              name,              email,              comment,              _createdAt         }     }`,  { slug }  )  .then((res) => res?.[0]),

Sanity returns an array of data in the response. This can be helpful in many cases but, for us, we just need the first item in the array, so we’ll limit the response to just the zero position in the index.

Adding a Comment component to our post

Our individual posts are rendered using code found in the /blog-frontend/pages/posts/[slug].js file. The components in this file are already receiving the updated data in our API file. The main Post() function returns our layout. This is where we’ll add our new component.

Comments typically appear after the post’s content, so let’s add this immediately following the closing </article> tag.

// ... The rest of the component </article> // The comments list component with comments being passed in <Comments comments={post?.comments} />

We now need to create our component file. The component files in this project live in the /blog-frontend/components directory. We’ll follow the standard pattern for the components. The main functionality of this component is to take the array passed to it and create an unordered list with proper markup.

Since we already have a <Date /> component, we can use that to format our date properly.

# /blog-frontend/components/comments.js  import Date from './date'  export default function Comments({ comments = [] }) {   return (     <>      <h2 className="mt-10 mb-4 text-4xl lg:text-6xl leading-tight">Comments:</h2>       <ul>         {comments?.map(({ _id, _createdAt, name, email, comment }) => (           <li key={_id} className="mb-5">             <hr className="mb-5" />             <h4 className="mb-2 leading-tight"><a href={`mailto:$ {email}`}>{name}</a> (<Date dateString={_createdAt}/>)</h4>             <p>{comment}</p>             <hr className="mt-5 mb-5" />          </li>         ))       </ul>     </>   ) }

Back in our /blog-frontend/pages/posts/[slug].js file, we need to import this component at the top, and then we have a comment section displayed for posts that have comments.

import Comments from '../../components/comments'

We now have our manually-entered comment listed. That’s great, but not very interactive. Let’s add a form to the page to allow users to submit a comment to our dataset.

Adding a comment form to a blog post

For our comment form, why reinvent the wheel? We’re already in the React ecosystem with Next.js, so we might as well take advantage of it. We’ll use the react-hook-form package, but any form or form component will do.

First, we need to install our package.

npm install react-hook-form

While that installs, we can go ahead and set up our Form component. In the Post component, we can add a <Form /> component right after our new <Comments /> component.

// ... Rest of the component <Comments comments={post.comments} /> <Form _id={post._id} />

Note that we’re passing the current post _id value into our new component. This is how we’ll tie our comment to our post.

As we did with our comment component, we need to create a file for this component at /blog-frontend/components/form.js.

export default function Form ({_id}) {    // Sets up basic data state   const [formData, setFormData] = useState()             // Sets up our form states    const [isSubmitting, setIsSubmitting] = useState(false)   const [hasSubmitted, setHasSubmitted] = useState(false)            // Prepares the functions from react-hook-form   const { register, handleSubmit, watch, errors } = useForm()    // Function for handling the form submission   const onSubmit = async data => {     // ... Submit handler   }    if (isSubmitting) {     // Returns a "Submitting comment" state if being processed     return <h3>Submitting comment…</h3>   }   if (hasSubmitted) {     // Returns the data that the user submitted for them to preview after submission     return (       <>         <h3>Thanks for your comment!</h3>         <ul>           <li>             Name: {formData.name} <br />             Email: {formData.email} <br />             Comment: {formData.comment}           </li>         </ul>       </>     )   }    return (     // Sets up the Form markup   ) }

This code is primarily boilerplate for handling the various states of the form. The form itself will be the markup that we return.

// Sets up the Form markup <form onSubmit={handleSubmit(onSubmit)} className="w-full max-w-lg" disabled>   <input ref={register} type="hidden" name="_id" value={_id} /> 									   <label className="block mb-5">     <span className="text-gray-700">Name</span>     <input name="name" ref={register({required: true})} className="form-input mt-1 block w-full" placeholder="John Appleseed"/>     </label> 																																																									   <label className="block mb-5">     <span className="text-gray-700">Email</span>     <input name="email" type="email" ref={register({required: true})} className="form-input mt-1 block w-full" placeholder="your@email.com"/>   </label>    <label className="block mb-5">     <span className="text-gray-700">Comment</span>     <textarea ref={register({required: true})} name="comment" className="form-textarea mt-1 block w-full" rows="8" placeholder="Enter some long form content."></textarea>   </label> 																																					   {/* errors will return when field validation fails  */}   {errors.exampleRequired && <span>This field is required</span>} 	   <input type="submit" className="shadow bg-purple-500 hover:bg-purple-400 focus:shadow-outline focus:outline-none text-white font-bold py-2 px-4 rounded" /> </form>

In this markup, we’ve got a couple of special cases. First, our <form> element has an onSubmit attribute that accepts the handleSubmit() hook. That hook provided by our package takes the name of the function to handle the submission of our form.

The very first input in our comment form is a hidden field that contains the _id of our post. Any required form field will use the ref attribute to register with react-hook-form’s validation. When our form is submitted we need to do something with the data submitted. That’s what our onSubmit() function is for.

// Function for handling the form submission const onSubmit = async data => {   setIsSubmitting(true)            setFormData(data)            try {     await fetch('/api/createComment', {       method: 'POST',      body: JSON.stringify(data),      type: 'application/json'     })       setIsSubmitting(false)     setHasSubmitted(true)   } catch (err) {     setFormData(err)   } }

This function has two primary goals:

  1. Set state for the form through the process of submitting with the state we created earlier
  2. Submit the data to a serverless function via a fetch() request. Next.js comes with fetch() built in, so we don’t need to install an extra package.

We can take the data submitted from the form — the data argument for our form handler — and submit that to a serverless function that we need to create.

We could post this directly to the Sanity API, but that requires an API key with write access and you should protect that with environment variables outside of your front-end. A serverless function lets you run this logic without exposing the secret token to your visitors.

Submitting the comment to Sanity with a Next.js API route

In order to protect our credentials, we’ll write our form handler as a serverless function. In Next.js, we can use “API routes” to create serverless function. These live alongside our page routes in the /blog-frontent/pages directory in the api directory. We can create a new file here called createComment.js.

To write to the Sanity API, we first need to set up a client that has write permissions. Earlier in this demo, we set up a read+write token and put it in /blog-frontent/.env.local. This environment variable is already in use in a client object from /blog-frontend/lib/sanity.js. There’s a read+write client set up with the name previewClient that uses the token to fetch unpublished changes for preview mode.

At the top of our createClient file, we can import that object for use in our serverless function. A Next.js API route needs to export its handler as a default function with request and response arguments. Inside our function, we’ll destructure our form data from the request object’s body and use that to create a new document.

Sanity’s JavaScript client has a create() method which accepts a data object. The data object should have a _type that matches the type of document we wish to create along with any data we wish to store. In our example, we’ll pass it the name, email, and comment.

We need to do a little extra work to turn our post’s _id into a reference to the post in Sanity. We’ll define the post property as a reference and give the_id as the _ref property on this object. After we submit it to the API, we can return either a success status or an error status depending on our response from Sanity.

// This Next.js template already is configured to write with this Sanity Client import {previewClient} from '../../lib/sanity'  export default async function createComment(req, res) {   // Destructure the pieces of our request   const { _id, name, email, comment} = JSON.parse(req.body)   try {     // Use our Client to create a new document in Sanity with an object       await previewClient.create({       _type: 'comment',       post: {         _type: 'reference',         _ref: _id,       },      name,      email,      comment     })   } catch (err) {     console.error(err)     return res.status(500).json({message: `Couldn't submit comment`, err})   }        return res.status(200).json({ message: 'Comment submitted' }) }

Once this serverless function is in place, we can navigate to our blog post and submit a comment via the form. Since we have an approval process in place, after we submit a comment, we can view it in the Sanity Studio and choose to approve it, deny it, or leave it as pending.

Take the commenting engine further

This gets us the basic functionality of a commenting system and it lives directly with our content. There is a lot of potential when you control both sides of this flow. Here are a few ideas for taking this commenting engine further.


The post How to Create a Commenting Engine with Next.js and Sanity appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,
[Top]

Building a Blog with Next.js

In this article, we will use Next.js to build a static blog framework with the design and structure inspired by Jekyll. I’ve always been a big fan of how Jekyll makes it easier for beginners to setup a blog and at the same time also provides a great degree of control over every aspect of the blog for the advanced users.

With the introduction of Next.js in recent years, combined with the popularity of React, there is a new avenue to explore for static blogs. Next.js makes it super easy to build static websites based on the file system itself with little to no configuration required.

The directory structure of a typical bare-bones Jekyll blog looks like this:

. ├─── _posts/          ...blog posts in markdown ├─── _layouts/        ...layouts for different pages ├─── _includes/       ...re-usable components ├─── index.md         ...homepage └─── config.yml       ...blog config

The idea is to design our framework around this directory structure as much as possible so that it becomes easier to  migrate a blog from Jekyll by simply reusing the posts and configs defined in the blog.

For those unfamiliar with Jekyll, it is a static site generator that can transform your plain text into static websites and blogs. Refer the quick start guide to get up and running with Jekyll.

This article also assumes that you have a basic knowledge of React. If not, React’s getting started page is a good place to start.

Installation

Next.js is powered by React and written in Node.js. So we need to install npm first, before adding next, react and react-dom to the project.

mkdir nextjs-blog && cd $ _ npm init -y npm install next react react-dom --save

To run Next.js scripts on the command line, we have to add the next command to the scripts section of our package.json.

"scripts": {   "dev": "next" }

We can now run npm run dev on the command line for the first time. Let’s see what happens.

$  npm run dev > nextjs-blog@1.0.0 dev /~user/nextjs-blog > next  ready - started server on http://localhost:3000 Error: > Couldn't find a `pages` directory. Please create one under the project root

The compiler is complaining about a missing pages directory in the root of the project. We’ll learn about the concept of pages in the next section.

Concept of pages

Next.js is built around the concept of pages. Each page is a React component that can be of type .js or .jsx which is mapped to a route based on the filename. For example:

File                            Route ----                            ----- /pages/about.js                 /about /pages/projects/work1.js        /projects/work1 /pages/index.js                 /

Let’s create the pages directory in the root of the project and populate our first page, index.js, with a basic React component.

// pages/index.js export default function Blog() {   return <div>Welcome to the Next.js blog</div> }

Run npm run dev once again to start the server and navigate to http://localhost:3000 in the browser to view your blog for the first time.

Screenshot of the homepage in the browser. The content says welcome to the next.js blog.

Out of the box, we get:

  • Hot reloading so we don’t have to refresh the browser for every code change.
  • Static generation of all pages inside the /pages/** directory.
  • Static file serving for assets living in the/public/** directory.
  • 404 error page.

Navigate to a random path on localhost to see the 404 page in action. If you need a custom 404 page, the Next.js docs have great information.

Screenshot of the 404 page. It says 404 This page could not be found.

Dynamic pages

Pages with static routes are useful to build the homepage, about page, etc. However, to dynamically build all our posts, we will use the dynamic route capability of Next.js. For example:

File                        Route ----                        ----- /pages/posts/[slug].js      /posts/1                             /posts/abc                             /posts/hello-world

Any route, like /posts/1, /posts/abc, etc., will be matched by /posts/[slug].js and the slug parameter will be sent as a query parameter to the page. This is especially useful for our blog posts because we don’t want to create one file per post; instead we could dynamically pass the slug to render the corresponding post.

Anatomy of a blog

Now, since we understand the basic building blocks of Next.js, let’s define the anatomy of our blog.

. ├─ api │  └─ index.js             # fetch posts, load configs, parse .md files etc ├─ _includes │  ├─ footer.js            # footer component │  └─ header.js            # header component ├─ _layouts │  ├─ default.js           # default layout for static pages like index, about │  └─ post.js              # post layout inherts from the default layout ├─ pages │  ├─ index.js             # homepage |  └─ posts                # posts will be available on the route /posts/ |     └─ [slug].js       # dynamic page to build posts └─ _posts    ├─ welcome-to-nextjs.md    └─ style-guide-101.md

Blog API

A basic blog framework needs two API functions: 

  • A function to fetch the metadata of all the posts in _posts directory
  • A function to fetch a single post for a given slug with the complete HTML and metadata

Optionally, we would also like all the site’s configuration defined in config.yml to be available across all the components. So we need a function that will parse the YAML config into a native object.

Since, we would be dealing with a lot of non-JavaScript files, like Markdown (.md), YAML (.yml), etc, we’ll use the raw-loader library to load such files as strings to make it easier to process them. 

npm install raw-loader --save-dev

Next we need to tell Next.js to use raw-loader when we import .md and .yml file formats by creating a next.config.js file in the root of the project (more info on that).

module.exports = {   target: 'serverless',   webpack: function (config) {     config.module.rules.push({test:  /.md$ /, use: 'raw-loader'})     config.module.rules.push({test: /.yml$ /, use: 'raw-loader'})     return config   } }

Next.js 9.4 introduced aliases for relative imports which helps clean up the import statement spaghetti caused by relative paths. To use aliases, create a jsconfig.json file in the project’s root directory specifying the base path and all the module aliases needed for the project.

{   "compilerOptions": {     "baseUrl": "./",     "paths": {       "@includes/*": ["_includes/*"],       "@layouts/*": ["_layouts/*"],       "@posts/*": ["_posts/*"],       "@api": ["api/index"],     }   } }

For example, this allows us to import our layouts by just using:

import DefaultLayout from '@layouts/default'

Fetch all the posts

This function will read all the Markdown files in the _posts directory, parse the front matter defined at the beginning of the post using gray-matter and return the array of metadata for all the posts.

// api/index.js import matter from 'gray-matter' 
 export async function getAllPosts() {   const context = require.context('../_posts', false, /.md$ /)   const posts = []   for(const key of context.keys()){     const post = key.slice(2);     const content = await import(`../_posts/$ {post}`);     const meta = matter(content.default)     posts.push({       slug: post.replace('.md',''),       title: meta.data.title     })   }   return posts; }

A typical Markdown post looks like this:

--- title:  "Welcome to Next.js blog!" --- **Hello world**, this is my first Next.js blog post and it is written in Markdown. I hope you like it!

The section outlined by --- is called the front matter which holds the metadata of the post like, title, permalink, tags, etc. Here’s the output:

[   { slug: 'style-guide-101', title: 'Style Guide 101' },   { slug: 'welcome-to-nextjs', title: 'Welcome to Next.js blog!' } ]

Make sure you install the gray-matter library from npm first using the command npm install gray-matter --save-dev.

Fetch a single post

For a given slug, this function will locate the file in the _posts directory, parse the Markdown with the marked library and return the output HTML with metadata.

// api/index.js import matter from 'gray-matter' import marked from 'marked' 
 export async function getPostBySlug(slug) {   const fileContent = await import(`../_posts/$ {slug}.md`)   const meta = matter(fileContent.default)   const content = marked(meta.content)       return {     title: meta.data.title,      content: content   } }

Sample output:

{   title: 'Style Guide 101',   content: '<p>Incididunt cupidatat eiusmod ...</p>' }

Make sure you install the marked library from npm first using the command npm install marked --save-dev.

Config

In order to re-use the Jekyll config for our Next.js blog, we’ll parse the YAML file using the js-yaml library and export this config so that it can be used across components.

// config.yml title: "Next.js blog" description: "This blog is powered by Next.js" 
 // api/index.js import yaml from 'js-yaml' export async function getConfig() {   const config = await import(`../config.yml`)   return yaml.safeLoad(config.default) }

Make sure you install js-yaml from npm first using the command npm install js-yaml --save-dev.

Includes

Our _includes directory contains two basic React components, <Header> and <Footer>, which will be used in the different layout components defined in the _layouts directory.

// _includes/header.js export default function Header() {   return <header><p>Blog | Powered by Next.js</p></header> } 
 // _includes/footer.js export default function Footer() {   return <footer><p>©2020 | Footer</p></footer> }

Layouts

We have two layout components in the _layouts directory. One is the <DefaultLayout> which is the base layout on top of which every other layout component will be built.

// _layouts/default.js import Head from 'next/head' import Header from '@includes/header' import Footer from '@includes/footer' 
 export default function DefaultLayout(props) {   return (     <main>       <Head>         <title>{props.title}</title>         <meta name='description' content={props.description}/>       </Head>       <Header/>       {props.children}       <Footer/>     </main>   ) }

The second layout is the <PostLayout> component that will override the title defined in the <DefaultLayout> with the post title and render the HTML of the post. It also includes a link back to the homepage.

// _layouts/post.js import DefaultLayout from '@layouts/default' import Head from 'next/head' import Link from 'next/link' 
 export default function PostLayout(props) {   return (     <DefaultLayout>       <Head>         <title>{props.title}</title>       </Head>       <article>         <h1>{props.title}</h1>         <div dangerouslySetInnerHTML={{__html:props.content}}/>         <div><Link href='/'><a>Home</a></Link></div>        </article>     </DefaultLayout>   ) }

next/head is a built-in component to append elements to the <head> of the page. next/link is a built-in component that handles client-side transitions between the routes defined in the pages directory.

Homepage

As part of the index page, aka homepage, we will list all the posts inside the _posts directory. The list will contain the post title and the permalink to the individual post page. The index page will use the <DefaultLayout> and we’ll import the config in the homepage to pass the title and description to the layout.

// pages/index.js import DefaultLayout from '@layouts/default' import Link from 'next/link' import { getConfig, getAllPosts } from '@api' 
 export default function Blog(props) {   return (     <DefaultLayout title={props.title} description={props.description}>       <p>List of posts:</p>       <ul>         {props.posts.map(function(post, idx) {           return (             <li key={idx}>               <Link href={'/posts/'+post.slug}>                 <a>{post.title}</a>               </Link>             </li>           )         })}       </ul>     </DefaultLayout>   ) }  
 export async function getStaticProps() {   const config = await getConfig()   const allPosts = await getAllPosts()   return {     props: {       posts: allPosts,       title: config.title,       description: config.description     }   } }

getStaticProps is called at the build time to pre-render pages by passing props to the default component of the page. We use this function to fetch the list of all posts at build time and render the posts archive on the homepage.

Screenshot of the homepage showing the page title, a list with two post titles, and the footer.

Post page

This page will render the title and contents of the post for the slug supplied as part of the context. The post page will use the <PostLayout> component.

// pages/posts/[slug].js import PostLayout from '@layouts/post' import { getPostBySlug, getAllPosts } from "@api" 
 export default function Post(props) {   return <PostLayout title={props.title} content={props.content}/> } 
 export async function getStaticProps(context) {   return {     props: await getPostBySlug(context.params.slug)   } } 
 export async function getStaticPaths() {   let paths = await getAllPosts()   paths = paths.map(post => ({     params: { slug:post.slug }   }));   return {     paths: paths,     fallback: false   } }

If a page has dynamic routes, Next.js needs to know all the possible paths at build time. getStaticPaths supplies the list of paths that has to be rendered to HTML at build time. The fallback property ensures that if you visit a route that does not exist in the list of paths, it will return a 404 page.

Screenshot of the blog page showing a welcome header and a hello world blue above the footer.

Production ready

Add the following commands for build and start in package.json, under the scripts section and then run npm run build followed by npm run start to build the static blog and start the production server.

// package.json "scripts": {   "dev": "next",   "build": "next build",   "start": "next start" }

The entire source code in this article is available on this GitHub repository. Feel free to clone it locally and play around with it. The repository also includes some basic placeholders to apply CSS to your blog.

Improvements

The blog, although functional, is perhaps too basic for most average cases. It would be nice to extend the framework or submit a patch to include some more features like:

  • Pagination
  • Syntax highlighting
  • Categories and Tags for posts
  • Styling

Overall, Next.js seems really very promising to build static websites, like a blog. Combined with its ability to export static HTML, we can built a truly standalone app without the need of a server!

The post Building a Blog with Next.js appeared first on CSS-Tricks.

CSS-Tricks

, ,
[Top]