next.js
npx create-next-app@13
# name, typescript,
npm i bootstrap react-bootstrap nextjs-progressbar
npm run dev
folder
pages --> routing, (ssr,ssg,isr)
components --> ui
models --> interface
routing (pages folder)
pages/index.ts (default page) --> localhost:3000
pages/search.ts -->localhost:3000/search
pages/api --> backend endpoint
pages/_app.ts --> root page(similar to <App /> in react.js)
// pages/_app.ts // <Head> --> for all title , meta, icon // Inter --> local downloaded font from google // '@/styles/globals.css'; --> can only be used in this page only import 'bootstrap/dist/css/bootstrap.min.css'; import '@/styles/globals.css'; import { Inter } from '@next/font/google'; import type { AppProps } from 'next/app'; import Head from 'next/head'; import { Container } from 'react-bootstrap'; import styles from "@/styles/App.module.css"; import NavBar from '@/components/NavBar'; import NextNProgress from "nextjs-progressbar"; const inter = Inter({ subsets: ['latin'] }); export default function App({ Component, pageProps }: AppProps) { return ( <div className={inter.className}> <Head> <title key="title">NextJS News App</title> <meta name="description" key="description" content="NextJS crash course by Coding in Flow" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link rel="icon" href="/favicon.ico" /> </Head> <NextNProgress /> <NavBar /> <Container className={styles.pageContainer}> <Component {...pageProps} /> </Container> </div> ); }
.env.local --> root --> to keep key secret
ssr --> getServerSideProps
NewsArticleEntry --> single card
index -->NewsArticlesGrid --> map ->NewsArticleEntry
// models/NewArtical.ts
export interface NewsArticle {
author: string,
title: string,
description: string,
url: string,
urlToImage?: string,
publishedAt: string,
content: string,
}
export interface NewsResponse {
articles: NewsArticle[], // ->[{author:,title:,...},{},{}] --> arry of NewsArticle object
}
//pages/index.ts --> localhost:3000
//<Head> --> to overwrite the title of _app.ts page
import NewsArticlesGrid from '@/components/NewsArticlesGrid';
import { NewsArticle, NewsResponse } from '@/models/NewsArticles';
import { GetServerSideProps } from 'next';
import Head from 'next/head';
import { Alert } from 'react-bootstrap';
interface BreakingNewsPageProps {
newsArticles: NewsArticle[],
}
//ssr ->getServerSideProps --> better seo,faster load
// return props , so that we can use it --> newsArticles
export const getServerSideProps: GetServerSideProps<BreakingNewsPageProps> = async () => {
const response = await fetch("https://newsapi.org/v2/top-headlines?country=us&apiKey=" + process.env.NEWS_API_KEY);
const newsResponse: NewsResponse = await response.json();
return {
props: { newsArticles: newsResponse.articles }
}
// let error go to 500 page
}
export default function BreakingNewsPage({ newsArticles }: BreakingNewsPageProps) {
return (
<>
<Head>
<title key="title">Breaking News - NextJS News App</title>
</Head>
<main>
<h1>Breaking News</h1>
<Alert>
This page uses <strong>getServerSideProps</strong> to fetch data server-side on every request.
This allows search engines to crawl the page content and <strong>improves SEO</strong>.
</Alert>
<NewsArticlesGrid articles={newsArticles} />
</main>
</>
)
}
// components/NewsArticleEntry.ts
// ssr,isr,ssg --> cant be used in component
//<Image /> --> lazy loading,height and width optimization
import { NewsArticle } from "@/models/NewsArticles";
import Image from "next/image";
import { Card } from "react-bootstrap";
import placeholderImage from "@/assets/images/newsarticle_placeholder.jpg";
import styles from "@/styles/NewsArticleEntry.module.css";
interface NewsArticleEntryProps {
article: NewsArticle,
}
const NewsArticleEntry = ({ article: { title, description, url, urlToImage } }: NewsArticleEntryProps) => {
const validImageUrl = (urlToImage?.startsWith("http://") || urlToImage?.startsWith("https://")) ? urlToImage : undefined;
return (
<a href={url}>
<Card className="h-100">
<Image
src={validImageUrl || placeholderImage}
width={500}
height={200}
alt="News article image"
className={`card-img-top ${styles.image}`}
/>
<Card.Body>
<Card.Title>{title}</Card.Title>
<Card.Text>{description}</Card.Text>
</Card.Body>
</Card>
</a>
);
}
export default NewsArticleEntry;
// next.config.js --> to load image from this -> www.si.com
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ["www.si.com"],
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
{
protocol: "http",
hostname: "**",
},
]
}
}
module.exports = nextConfig
// components/NewsArticlesGrid.ts
// responsive --> <Row xs={1} sm={2} xl={3} ></Row>
import { NewsArticle } from "@/models/NewsArticles";
import { Col, Row } from "react-bootstrap";
import NewsArticleEntry from "./NewsArticleEntry";
interface NewsArticlesGridProps {
articles: NewsArticle[],
}
const NewsArticlesGrid = ({ articles }: NewsArticlesGridProps) => {
return (
<Row xs={1} sm={2} xl={3} className="g-4">
{articles.map(article => (
<Col key={article.url}>
<NewsArticleEntry article={article} />
</Col>
))}
</Row>
);
}
export default NewsArticlesGrid;
csr
// pages/search.ts --> localhost:3000/search
//csr
//swr library can also be used
//<Head> --> to overwrite the title of _app.ts page
import NewsArticlesGrid from "@/components/NewsArticlesGrid";
import { NewsArticle } from "@/models/NewsArticles";
import Head from "next/head";
import { FormEvent, useState } from "react";
import { Alert, Button, Form, Spinner } from "react-bootstrap";
const SearchNewsPage = () => {
const [searchResults, setSearchResults] = useState<NewsArticle[] | null>(null);
const [searchResultsLoading, setSearchResultsLoading] = useState(false);
const [searchResultsLoadingIsError, setSearchResultsLoadingIsError] = useState(false);
async function handleSubmit(e: FormEvent<HTMLFormElement>) {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
const searchQuery = formData.get("searchQuery")?.toString().trim();
if (searchQuery) {
try {
setSearchResults(null);
setSearchResultsLoadingIsError(false);
setSearchResultsLoading(true);
const response = await fetch("/api/search-news?q=" + searchQuery);
const articles: NewsArticle[] = await response.json();
setSearchResults(articles);
} catch (error) {
console.error(error);
setSearchResultsLoadingIsError(true);
} finally {
setSearchResultsLoading(false);
}
}
}
return (
<>
<Head>
<title key="title">Search News - NextJS News App</title>
</Head>
<main>
<h1>Search News</h1>
<Alert>
This is page uses <strong>client-side data fetching</strong> to show fresh data for every search.
Requests are handled by our backend via <strong>API routes</strong>.
</Alert>
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3" controlId="search-input">
<Form.Label>Search query</Form.Label>
<Form.Control
name="searchQuery"
placeholder="E.g. politics, sports, ..."
/>
</Form.Group>
<Button type="submit" className="mb-3" disabled={searchResultsLoading}>
Search
</Button>
</Form>
<div className="d-flex flex-column align-items-center">
{searchResultsLoading && <Spinner animation="border" />}
{searchResultsLoadingIsError && <p>Something went wrong. Please try again.</p>}
{searchResults?.length === 0 && <p>Nothing found. Try a different query!</p>}
{searchResults && <NewsArticlesGrid articles={searchResults} />}
</div>
</main>
</>
);
}
export default SearchNewsPage;
// pages/api/search-news.ts
import { NewsResponse } from '@/models/NewsArticles';
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const searchQuery = req.query.q?.toString();
if (!searchQuery) {
return res.status(400).json({ error: "Please provide a search query" });
}
const response = await fetch(`https://newsapi.org/v2/everything?q=${searchQuery}&apiKey=${process.env.NEWS_API_KEY}`);
const newsResponse: NewsResponse = await response.json();
res.status(200).json(newsResponse.articles);
}
dynamic routing ->[] , (ssg -> getStaticPaths , isr --> revalidate: 5 * 60,) --> hell faster than ssr
//pages/categories/[category].ts --> localhost:3000/category/mamam
// useRouter --> router.query.category?.toString(); --> mamam
import NewsArticlesGrid from "@/components/NewsArticlesGrid";
import { NewsArticle, NewsResponse } from "@/models/NewsArticles";
import { GetStaticPaths, GetStaticProps } from "next";
import Head from "next/head";
import { useRouter } from "next/router";
import { Alert } from "react-bootstrap";
interface CategoryNewsPageProps {
newsArticles: NewsArticle[],
}
export const getStaticPaths: GetStaticPaths = async () => {
const categorySlugs = [ // this could be coming from an API
"business",
"entertainment",
"general",
"health",
"science",
"sports",
"technology",
];
const paths = categorySlugs.map(slug => ({ params: { category: slug } }));
return {
paths,
fallback: false,
}
}
export const getStaticProps: GetStaticProps<CategoryNewsPageProps> = async ({ params }) => {
const category = params?.category?.toString();
const response = await fetch(`https://newsapi.org/v2/top-headlines?country=us&category=${category}&apiKey=${process.env.NEWS_API_KEY}`);
const newsResponse: NewsResponse = await response.json();
return {
props: { newsArticles: newsResponse.articles },
revalidate: 5 * 60,
}
// let error go to 500 page
}
const CategoryNewsPage = ({ newsArticles }: CategoryNewsPageProps) => {
const router = useRouter();
const categoryName = router.query.category?.toString();
const title = "Category: " + categoryName;
return (
<>
<Head>
<title key="title">{`${title} - NextJS News App`}</title>
</Head>
<main>
<h1>{title}</h1>
<Alert>
This is page uses <strong>getStaticProps</strong> for very high page loading speed
and <strong>incremental static regeneration</strong> to show data not older than <strong>5 minutes</strong>.
</Alert>
<NewsArticlesGrid articles={newsArticles} />
</main>
</>
);
}
export default CategoryNewsPage;
customize 404 page
const NotFoundPage = () => {
return (
<div>
<h1>Not Found</h1>
<p>Looks like this page does not exist!</p>
</div>
);
}
export default NotFoundPage;
customize 500 page
const ErrorPage = () => {
return (
<div>
<h1>Error 😵</h1>
<p>Looks like something went wrong. Please refresh the page or contact support!</p>
</div>
);
}
export default ErrorPage;
navbar
//components/NavBar.ts
// <Link> --> navigate between pages -->
// as={Link} --> since i want link feature and nav ui of bootstrap
import Link from "next/link";
import { Container, Nav, Navbar, NavDropdown } from "react-bootstrap";
const NavBar = () => {
return (
<Navbar bg="dark" variant="dark" sticky="top" expand="sm" collapseOnSelect>
<Container>
<Navbar.Toggle aria-controls="main-navbar" />
<Navbar.Collapse id="main-navbar">
<Nav>
<Nav.Link as={Link} href="/">Breaking</Nav.Link>
<Nav.Link as={Link} href="/search">Search</Nav.Link>
<NavDropdown title="Categories" id="categories-dropdown">
<NavDropdown.Item as={Link} href="/categories/business">Business</NavDropdown.Item>
<NavDropdown.Item as={Link} href="/categories/entertainment">Entertainment</NavDropdown.Item>
<NavDropdown.Item as={Link} href="/categories/general">General</NavDropdown.Item>
<NavDropdown.Item as={Link} href="/categories/health">Health</NavDropdown.Item>
<NavDropdown.Item as={Link} href="/categories/science">Science</NavDropdown.Item>
<NavDropdown.Item as={Link} href="/categories/sports">Sports</NavDropdown.Item>
<NavDropdown.Item as={Link} href="/categories/technology">Technology</NavDropdown.Item>
</NavDropdown>
</Nav>
</Navbar.Collapse>
</Container>
</Navbar>
);
}
export default NavBar;