In this article, we will see how to create a blog app using React, Typescript and GraphQl :
Let’s start by building our base:
$ npx create-react-app myblog
$ cd myblog
Then we need to install our dependencies :
$ yarn add react react-dom bootstrap jquery popper.js react-bootstrap react-router-bootstrap react-router-dom
Installing Typescript and Types :
$ yarn add typescript @types/node @types/react @types/react-bootstrap @types/react-dom @types/react-router-bootstrap
Installing Graphql
$ yarn add graphql @apollo/client
Create a tsconfig.json and add this :
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"./src/**/*.ts"
]
}
Now, our typescript setup is ready. We need to change all our files with .js extention to .tsx
Setting Up GraphCMS
We need to create a account in GraphCMS. Add create content model for our Blog.
After we have created our model, we need to create some content :
We need to get our Graphql API Endpoint. This API endpoint will help us query our data from GRAPHCMS. It will look something like this url ‘https://api-ap-northeast-1.graphcms.com/v2/ckl88sdscw296sxk12301xs1gyvefbb/master’
Setting up our Graphql Client in React :
Open index.tsx and add this :
import { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import App from './App';
import './index.css';
import reportWebVitals from './reportWebVitals';
// Apollo client
const client = new ApolloClient({
uri: YOUR_GRAPHCMS_API_ENDPOINT_GOES_HERE,
cache: new InMemoryCache()
});
ReactDOM.render(
<ApolloProvider client={client}>
<React.StrictMode>
<App />
</React.StrictMode>
</ApolloProvider>,
document.getElementById('root')
);
reportWebVitals();
Create our Header and Footer components inside components folder :
components/Header/index.tsx
import 'bootstrap/dist/css/bootstrap.min.css';
import * as React from 'react';
import { Button, Image, Nav, Navbar, NavDropdown } from 'react-bootstrap';
import { LinkContainer } from 'react-router-bootstrap';
import './Header.css';
interface IProps {
brand?: string;
}
const Header: React.FC<IProps> = (props: IProps) => (
<Navbar bg="light" expand="md" fixed="top" className="justify-content-between">
<LinkContainer to="/">
<Navbar.Brand className="font-weight-bold text-muted">
<h6>{props.brand}</h6>
</Navbar.Brand>
</LinkContainer>
<Navbar.Toggle aria-controls="top-nav"/>
<Navbar.Collapse id="top-nav" className="justify-content-end">
<Nav className="ml-auto">
<LinkContainer to="/blog">
<Nav.Link>Blog</Nav.Link>
</LinkContainer>
</Nav>
</Navbar.Collapse>
</Navbar>
);
Header.defaultProps = {
brand: 'MyApp',
}
export default Header;
and components/Footer/index.tsx
import 'bootstrap/dist/css/bootstrap.min.css';
import * as React from 'react';
import './Footer.css';
interface IProps {
brand?: string;
}
const Footer: React.FC<IProps> = (props: IProps) => (
<p className="footerText"> © Copyright { props.brand }. All Rights Reserved. 2021</p>
);
Footer.defaultProps = {
brand: 'MyApp',
}
export default Footer;
Create a Layout.tsx
import * as React from 'react';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Footer from './components/Footer';
import Header from './components/Header';
import Blog from './pages/Blog';
import Post from './pages/Blog/Post';
import Home from './pages/Home';
const Layout: React.FC = () => {
return(
<Router>
<div>
<Header />
<main>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/blog" component={Blog} />
<Route path="/post/:slug" component={Post} />
</Switch>
</main>
<Footer />
</div>
</Router>
)
};
export default Layout;
Now let’s import our Layout in our App
In our App.tsx
import * as React from 'react';
import './App.css';
import { Container } from 'react-bootstrap';
import Layout from "./Layout";
class App extends React.Component {
public render(){
return(
<Container fluid>
<Layout />
</Container>
)
}
}
export default App;
Now let’s create our pages for Blog. Create a Blogs component at pages/Blog/index.tsx to query all our Blog posts.
import { gql, useQuery } from '@apollo/client';
import * as React from 'react';
import { Button, Card, Col, Container, Row } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import './Blog.css';
interface BlogPosts{
id: string,
title: string,
excerpt: string,
slug: string,
coverImage: {
fileName: string,
url: string
}
}
interface BlogData {
posts: BlogPosts[];
}
const GET_BLOG_POSTS = gql`
query GetBlogPosts {
posts {
id
title
excerpt
slug
coverImage {
fileName
url
}
}
}
`;
const Blog = () => {
const { loading, data } = useQuery<BlogData>(GET_BLOG_POSTS);
return (
<Container>
<Row>
<Col>
<h1 className="blogTitle"> Blog</h1>
<p className="blogSubTitle"> Articles on Freelancing, Tech, Engineering,Products and Business </p>
</Col>
</Row>
<Row>
<Col lg={12}>
<h4> Recent Articles : </h4>
<Row className="blogCardsContainer">
{data && data.posts.map(post => (
<Card className="blogCard">
<Card.Img variant="top" src={post.coverImage.url} alt={post.coverImage.fileName} />
<Card.Body>
<Card.Title>{post.title}</Card.Title>
<Card.Text>
{post.excerpt}
</Card.Text>
<Link to={`/post/${post.slug}`}>
<Button variant="success" size="sm"> Read More</Button>
</Link>
</Card.Body>
<Card.Footer>
<small className="text-muted">Last updated 3 mins ago</small>
</Card.Footer>
</Card>
))}
</Row>
</Col>
</Row>
</Container>
)
}
export default Blog;
To fetch single posts, we can create a post component at /pages/Blog/Post.tsx
import { gql, useQuery } from '@apollo/client';
import * as React from 'react';
import { Col, Container, Image, Row } from 'react-bootstrap';
import { useParams } from 'react-router-dom';
import './Blog.css';
interface PostData{
id: string,
date: string,
createdAt: string,
title: string,
excerpt: string,
tags: string,
slug: string,
coverImage: {
fileName: string,
url: string
},
author: {
name: string
},
content: {
html: any
}
}
const GET_BLOG_POST = gql`
query getBlogPost($slug: String) {
post (where: {slug: $slug}) {
author {
name
}
createdAt
date
id
excerpt
slug
title
tags
coverImage{
url
fileName
}
content{
html
}
}
}
`;
interface RouteParams {
slug: any
}
const BlogPost: React.FC<RouteParams> = () => {
// useQuery<Data>(Query, variables)
const params = useParams<RouteParams>();
const { loading, data } = useQuery<PostData>(GET_BLOG_POST, { variables : { slug: params.slug } });
return (
<Container>
<Row>
<Col>
<h1 className="blogTitle"> Blog</h1>
</Col>
</Row>
<Row>
<Col lg={10} md=>
{loading ? (
<p>Loading ...</p>
):(
<article className="blogArticle">
<h1 className="blogPostTitle"> {data.post.title}</h1>
<p className="blogPostSubtitle"> <p dangerouslySetInnerHTML= /></p>
<span className="blogPostCoverImageContainer">
<Image src={data.post.coverImage.url} alt={data.post.coverImage.fileName} className="blogPostCoverImage" />
</span>
<p dangerouslySetInnerHTML= />
</article>
)}
</Col>
</Row>
</Container>
)
}
export default BlogPost;
Great ! we now have a blog. Let’s test it out :
$ yarn run start
This will start our server at http://localhost:3000
If you want to run a production build :
$ yarn run build
This will create a build directory with all the static files that are ready to serve. You can upload in any static hosting and it will be live. (Eg. using Netlify, Github Pages or Vercel)
Happy Blogging !
Leave a Reply