mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 08:56:46 -04:00
chore: add initial changes to support new content
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Supabase vs Firebase
|
||||
description: Supabase vs Firebase
|
||||
author: ant_wilson
|
||||
image: all-pull-together/all-pull-together-thumb.png
|
||||
thumb: all-pull-together/all-pull-together-thumb.png
|
||||
tags:
|
||||
- community
|
||||
- case-study
|
||||
- q-and-a
|
||||
date: '2022-05-26'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
## What is Firebase?
|
||||
|
||||
Now owned by Google, Firebase is a collection of tools aimed at mobile and web developers. At its core is the Firestore database.
|
||||
|
||||
Firestore allows you to store “documents”. These are collections of key:value pairs where the value can be another sub-document. Document based storage is perfect for unstructured data, since two documents in a collection do not necessarily need to have the same structure.
|
||||
|
||||
Firebase also offers other things that web developers find useful, like an auth service for user management, and wrappers for other Google services such as Cloud Functions, and File Storage.
|
||||
|
||||
## What is Supabase?
|
||||
|
||||
Supabase is an open source firebase alternative, but instead of being built around a document-based datastore, Supabase offers a relational database management system - called PostgreSQL. This comes with a few advantages:
|
||||
|
||||
- It’s open source, so there is zero lock in.
|
||||
- You can query it with SQL, a proven and powerful query language.
|
||||
- It has a long track record of being used at scale.
|
||||
- It’s the database of choice for transactional workloads (think apps and websites, or other things that require near-instant responses to queries)
|
||||
- It comes with decades of [useful postgres extensions and plug-ins](https://supabase.com/docs/guides/database/extensions)
|
||||
|
||||
At Supabase we’ve always been huge fans of Firebase - so we started adding a few things on top of PostgreSQL in an attempt to reach feature parity, including:
|
||||
|
||||
- Auto-generated API - [query your data straight from the client](https://supabase.com/docs/guides/api#rest-api-2)
|
||||
- Realtime - [changes in your data will be streamed directly to your application](https://supabase.com/docs/reference/dart/subscribe)
|
||||
- Auth - [a simple to integrate auth system and SQL based rules engine](https://supabase.com/auth)
|
||||
- Functions - [javascript and typescript functions that deploy out globally](https://supabase.com/edge-functions)
|
||||
- Storage - [hosting images, videos, and pdfs easily](https://supabase.com/storage)
|
||||
|
||||
## How are they similar?
|
||||
|
||||
Both Firebase and Supabase are based on the idea of bringing a superior developer experience to databases. With both platforms you can spin up a new project from directly inside the browser, without the need to download any extra tools or software to your machine. Both platforms come with a useful dashboard UI for debugging your data in realtime, which is especially useful for fast iterations when in development.
|
||||
|
||||
Both Firebase and Supabase have invested heavily in client side libraries so you can communicate with your database, directly from the client. Firebase has their [Firebase Javascript SDK](https://github.com/firebase/firebase-js-sdk) and Supabase has [supabase-js an isomorphic client](https://github.com/supabase/supabase-js/) that can be used both on the client also on the server in a node-js environment.
|
||||
|
||||
## How are they different?
|
||||
|
||||
Firebase and Supabase differ in several ways. The main one being that Firebase is a document store, whereas Supabase is based on PostgreSQL - a relational, SQL-based database management system. But there are also some other important differences.
|
||||
|
||||
### Open Source
|
||||
|
||||
Supabase is fully open source, meaning that along with the hosted cloud platform, you can also take the Supabase stack, and host it inside your own cloud or run it locally on your machine. The primary benefit here is no vendor lock in, which can be a frustrating problem for some Firebase users.
|
||||
|
||||
### Pricing
|
||||
|
||||
[Firebase charges for reads, writes and deletes](https://firebase.google.com/pricing), which can lead to some unpredictability, especially in the early stages of a project when your application is in heavy development. Supabase on the other hand [charges based on the amount of data stored](https://supabase.com/pricing), with breathing room for unlimited API requests and an unlimited number of Auth users.
|
||||
|
||||
### Scaling
|
||||
|
||||
One scaling limitation you might face when using Firestore - is the 1 document per second limitation on writes. It can be hard to architect around this issue for users who want to be able to sustain high write loads from a single client. Supabase is built on Postgres which is a well battle tested at scale, to help scale the number of connections, every deployment comes with an instance with PgBouncer, a [Postgres connection pooler perfect for use in Serverless computing](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool) environments.
|
||||
|
||||
## How do I migrate from Firebase to Supabase?
|
||||
|
||||
Since Firebase is documents based, migrating into a relational database requires you to map your data structure across into a SQL schema. Luckily we’ve built a [handy conversion tool to do it for you](https://github.com/supabase-community/firebase-to-supabase/tree/main/firestore).
|
||||
|
||||
We also have a tool for [migrating Firebase Auth to Supabase Auth](https://github.com/supabase-community/firebase-to-supabase/tree/main/auth). And one for [migrating Firebase Storage files to Supabase Storage](https://github.com/supabase-community/firebase-to-supabase/tree/main/storage).
|
||||
|
||||
These are by far the most complete Firebase to Postgres migration tools available anywhere on the web.
|
||||
|
||||
If you require Enterprise level support with your project or migration, please get in touch using our [Enterprise contact form](https://supabase.com/contact/enterprise).
|
||||
|
||||
[Try Supabase for free today](https://app.supabase.io).
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Supabase vs Heroku Postgres
|
||||
description: Supabase vs Heroku Postgres
|
||||
author: ant_wilson
|
||||
image: all-pull-together/all-pull-together-thumb.png
|
||||
thumb: all-pull-together/all-pull-together-thumb.png
|
||||
tags:
|
||||
- community
|
||||
- case-study
|
||||
- q-and-a
|
||||
date: '2022-05-26'
|
||||
toc_depth: 3
|
||||
---
|
||||
|
||||
## What is Heroku Postgres?
|
||||
|
||||
Heroku is a cloud application platform that offers managed PostgreSQL as a service. They offer 5 levels of Postgres support from the Hobby Tier up to the Shield Tier, each with different levels of features and pricing.
|
||||
|
||||
## What is Supabase?
|
||||
|
||||
Supabase also offers managed Postgres, the main difference is that with each deployment you also get:
|
||||
|
||||
- Auto-generated API - [never write an API again](https://supabase.com/docs/guides/api#rest-api-2)
|
||||
- Realtime - [subscribe to data changes via websockets](https://supabase.com/docs/reference/dart/subscribe)
|
||||
- Auth - [users can log in and out of your application](https://supabase.com/auth)
|
||||
- Functions - [deploy custom logic to the edge](https://supabase.com/edge-functions)
|
||||
- Storage - [serve large files and folders](https://supabase.com/storage)
|
||||
- PgBouncer - [connection pooling useful for serverless computing](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool)
|
||||
|
||||
## How are they similar?
|
||||
|
||||
Heroku Postgres and Supabase both offer:
|
||||
|
||||
- [A web UI for managing your instance](https://supabase.com/docs/guides/database#table-view)
|
||||
- SLAs and Enterprise-grade support packages
|
||||
- Direct SSL connections to Postgres
|
||||
- Postgres Extensions (see [Supabase Extensions](https://supabase.com/docs/guides/database/extensions), [Heroku Extensions](https://devcenter.heroku.com/articles/heroku-postgres-extensions-postgis-full-text-search))
|
||||
- [Backups and PITR](https://supabase.com/blog/2020/08/02/continuous-postgresql-backup-walg) (not on Heroku’s Hobby tier)
|
||||
- [Postgres logs](https://supabase.com/docs/guides/platform/logs) (not on Heroku’s Hobby tier)
|
||||
- Encryption-at-rest (not on Heroku’s Hobby tier)
|
||||
|
||||
## What are the differences?
|
||||
|
||||
### Core Features
|
||||
|
||||
Both solutions run PostgreSQL, but in a time when Developer Experience matters, there is a lot you can do to improve the speed at which developers can build products faster and with less human resource. These are some of the key differences between Heroku Postgres and Supabase in terms of features:
|
||||
|
||||
- Supabase is more than just the raw database, it also comes with:
|
||||
- [Connection pooling](https://supabase.com/docs/guides/database/connecting-to-postgres#connection-pool) so that you won’t run out of connections in a serverless environment.
|
||||
- [Auto-generated APIs](https://supabase.com/docs/guides/api#rest-api-2) based on your schema, so you can communicate with your database directly from the client.
|
||||
- [Realtime API](https://supabase.com/docs/reference/dart/subscribe) is useful for when you want to subscribe to changes to your database over websockets.
|
||||
- [Auth API](https://supabase.com/auth) can be used to leverage Postgres’s Row Level Security model, and control access to sensitive data on a per user, or per group level.
|
||||
- [Functions](https://supabase.com/edge-functions) can be deployed out to the edge directly from the Supabase CLI, which means you can run sensitive business logic or transformations in a serverless fashion.
|
||||
- [File Storage](https://supabase.com/storage) is useful for when your app needs to store large files and folders that aren’t suitable for storing within Postgres itself.
|
||||
- A spreadsheet-like web interface for building your schemas and inspecting data.
|
||||
- You can also deploy edge functions to Heroku using their [Dynos](https://www.heroku.com/dynos) runtime in conjunction with something like [Fastly](https://www.fastly.com/).
|
||||
|
||||
### Pricing
|
||||
|
||||
Simple and predictable pricing are key. The two services price quite differently, the key differences being:
|
||||
|
||||
- [Supabase pricing is based around usage](https://supabase.com/pricing), so you only pay for what you use.
|
||||
- Heroku prices based on a tier model with [37 plans to choose from](https://elements.heroku.com/addons/heroku-postgresql#pricing).
|
||||
|
||||
Supabase’s free tier also includes a dedicated Postgres instance, and the best bit is you can upgrade to pro later without any interruptions.
|
||||
|
||||
### Global Deployments
|
||||
|
||||
You may have strict data regulations that you must comply with, so choosing your region can be very important. Here’s how the deployment options stack up:
|
||||
|
||||
- Supabase can be deployed to any one of [12 data centers across the globe](https://github.com/supabase/supabase/discussions/4815#discussioncomment-1915129) (free tier included).
|
||||
- Since Supabase is fully open source - you can also [self host wherever you like](https://supabase.com/docs/guides/hosting/overview).
|
||||
- You can deploy Heroku Postgres to two data centers (US and Europe) however [6 more data centers](https://devcenter.heroku.com/articles/regions) are available on the Enterprise plan.
|
||||
|
||||
## How to migrate from Heroku Postgres to Supabase
|
||||
|
||||
Migrating is surprisingly simple. You just need to use the standard Postgres `pg_dump` and `pg_restore` tools to dump and restore from a backup. We created a handy guide for [migrating from heroku to supabase](https://github.com/supabase-community/heroku-to-supabase).
|
||||
@@ -0,0 +1,200 @@
|
||||
import { Badge, Divider, IconChevronLeft, IconFile } from '@supabase/ui'
|
||||
import hydrate from 'next-mdx-remote/hydrate'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import CTABanner from '~/components/CTABanner'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import { generateReadingTime } from '~/lib/helpers'
|
||||
import authors from 'lib/authors.json'
|
||||
|
||||
import blogStyles from './[slug].module.css'
|
||||
import { Gfm } from 'remark-gfm'
|
||||
|
||||
interface Props {
|
||||
components: React.ReactNode
|
||||
props: any
|
||||
gfm: Gfm
|
||||
slug: string
|
||||
}
|
||||
|
||||
const LayoutComparison = ({ components, props, gfm, slug }: Props) => {
|
||||
// @ts-ignore
|
||||
const content = hydrate(props.blog.content, { components })
|
||||
|
||||
const authorArray = props.blog.author.split(',')
|
||||
|
||||
const author = []
|
||||
for (let i = 0; i < authorArray.length; i++) {
|
||||
author.push(
|
||||
// @ts-ignore
|
||||
authors.find((authors: string) => {
|
||||
// @ts-ignore
|
||||
return authors.author_id === authorArray[i]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const { basePath } = useRouter()
|
||||
|
||||
const NextCard = (props: any) => {
|
||||
const { post, label, className } = props
|
||||
return (
|
||||
<Link href={`/blog/${post.url}`} as={`/blog/${post.url}`}>
|
||||
<div className={className}>
|
||||
<div className="border-scale-500 hover:bg-scale-100 dark:hover:bg-scale-300 cursor-pointer rounded border p-6 transition">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-scale-900 text-sm">{label}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-scale-1200 text-lg">{post.title}</h4>
|
||||
<p className="small">{post.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
title={props.blog.title}
|
||||
openGraph={{
|
||||
title: props.blog.title,
|
||||
description: props.blog.description,
|
||||
url: `https://supabase.com/blog/${props.blog.slug}`,
|
||||
type: 'article',
|
||||
article: {
|
||||
//
|
||||
// to do: add expiration and modified dates
|
||||
// https://github.com/garmeeh/next-seo#article
|
||||
publishedTime: props.blog.date,
|
||||
//
|
||||
// to do: author urls should be internal in future
|
||||
// currently we have external links to github profiles
|
||||
authors: [props.blog.author_url],
|
||||
tags: props.blog.tags.map((cat: string) => {
|
||||
return cat
|
||||
}),
|
||||
},
|
||||
images: [
|
||||
{
|
||||
url: `https://supabase.com${basePath}/images/blog/${
|
||||
props.blog.image ? props.blog.image : props.blog.thumb
|
||||
}`,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
<DefaultLayout>
|
||||
<article className="mx-auto max-w-5xl px-8 py-16 sm:px-16 xl:px-20">
|
||||
{/* Title and description */}
|
||||
<div className="mb-16 max-w-5xl space-y-8">
|
||||
<div className="space-y-4">
|
||||
<p className="text-brand-900 text-center">Alternative</p>
|
||||
<h1 className="h1 text-center">{props.blog.title}</h1>
|
||||
<div className="text-scale-900 flex justify-center space-x-3 text-sm">
|
||||
<p>{props.blog.date}</p>
|
||||
<p>•</p>
|
||||
<p>{generateReadingTime(props.blog.content.renderedOutput)}</p>
|
||||
</div>
|
||||
<div className="flex justify-center gap-3">
|
||||
{author.map((author: any) => {
|
||||
return (
|
||||
<div className="mt-6 mb-8 mr-4 w-max lg:mb-0">
|
||||
<Link href={author.author_url}>
|
||||
<a className="cursor-pointer">
|
||||
<div className="flex items-center gap-3">
|
||||
{author.author_image_url && (
|
||||
<div className="w-10">
|
||||
<Image
|
||||
src={author.author_image_url}
|
||||
className="dark:border-dark rounded-full border"
|
||||
width="100%"
|
||||
height="100%"
|
||||
layout="responsive"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-scale-1200 mb-0 text-sm">{author.author}</span>
|
||||
<span className="text-scale-900 mb-0 text-xs">{author.position}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="">
|
||||
{/* Content */}
|
||||
<div className="prose prose-docs max-w-none">{content}</div>
|
||||
<div className="py-16">
|
||||
<div className="text-scale-900 dark:text-scale-1000 text-sm">Share this article</div>
|
||||
<div className="mt-4 flex items-center space-x-4">
|
||||
<Link
|
||||
passHref
|
||||
href={`https://twitter.com/share?text=${props.blog.title}&url=https://supabase.com/blog/${props.blog.slug}`}
|
||||
>
|
||||
<a target="_blank" className="text-scale-900 hover:text-scale-1200">
|
||||
<svg
|
||||
height="26"
|
||||
width="26"
|
||||
viewBox="-89 -46.8 644 446.8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
d="m154.729 400c185.669 0 287.205-153.876 287.205-287.312 0-4.37-.089-8.72-.286-13.052a205.304 205.304 0 0 0 50.352-52.29c-18.087 8.044-37.55 13.458-57.968 15.899 20.841-12.501 36.84-32.278 44.389-55.852a202.42 202.42 0 0 1 -64.098 24.511c-18.42-19.628-44.644-31.904-73.682-31.904-55.744 0-100.948 45.222-100.948 100.965 0 7.925.887 15.631 2.619 23.025-83.895-4.223-158.287-44.405-208.074-105.504a100.739 100.739 0 0 0 -13.668 50.754c0 35.034 17.82 65.961 44.92 84.055a100.172 100.172 0 0 1 -45.716-12.63c-.015.424-.015.837-.015 1.29 0 48.903 34.794 89.734 80.982 98.986a101.036 101.036 0 0 1 -26.617 3.553c-6.493 0-12.821-.639-18.971-1.82 12.851 40.122 50.115 69.319 94.296 70.135-34.549 27.089-78.07 43.224-125.371 43.224a204.9 204.9 0 0 1 -24.078-1.399c44.674 28.645 97.72 45.359 154.734 45.359"
|
||||
fillRule="nonzero"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
passHref
|
||||
href={`https://www.linkedin.com/shareArticle?url=https://supabase.com/blog/${props.blog.slug}&title=${props.blog.title}`}
|
||||
>
|
||||
<a target="_blank" className="text-scale-900 hover:text-scale-1200">
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 5 1036 990"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M0 120c0-33.334 11.667-60.834 35-82.5C58.333 15.833 88.667 5 126 5c36.667 0 66.333 10.666 89 32 23.333 22 35 50.666 35 86 0 32-11.333 58.666-34 80-23.333 22-54 33-92 33h-1c-36.667 0-66.333-11-89-33S0 153.333 0 120zm13 875V327h222v668H13zm345 0h222V622c0-23.334 2.667-41.334 8-54 9.333-22.667 23.5-41.834 42.5-57.5 19-15.667 42.833-23.5 71.5-23.5 74.667 0 112 50.333 112 151v357h222V612c0-98.667-23.333-173.5-70-224.5S857.667 311 781 311c-86 0-153 37-201 111v2h-1l1-2v-95H358c1.333 21.333 2 87.666 2 199 0 111.333-.667 267.666-2 469z" />
|
||||
</svg>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-8 py-8 lg:grid-cols-1">
|
||||
<div>
|
||||
{props.prevPost && <NextCard post={props.prevPost} label="Previous comparison" />}
|
||||
</div>
|
||||
<div>
|
||||
{props.nextPost && (
|
||||
<NextCard post={props.nextPost} label="Next comparison" className="text-right" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
<CTABanner />
|
||||
</DefaultLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LayoutComparison
|
||||
@@ -7,12 +7,14 @@ import { generateReadingTime } from './helpers'
|
||||
// based on YYYY-MM-DD format
|
||||
const FILENAME_SUBSTRING = 11
|
||||
|
||||
type Directories = '_blog' | '_case-studies'
|
||||
type Directories = '_blog' | '_case-studies' | '_alternatives'
|
||||
|
||||
export const getSortedPosts = (directory: Directories, limit?: number, tags?: any) => {
|
||||
//Finding directory named "blog" from the current working directory of Node.
|
||||
const postDirectory = path.join(process.cwd(), directory)
|
||||
|
||||
console.log(postDirectory)
|
||||
|
||||
//Reads all the files in the post directory
|
||||
const fileNames = fs.readdirSync(postDirectory)
|
||||
|
||||
@@ -109,8 +111,12 @@ export const getPostdata = async (slug: string, directory: string) => {
|
||||
//Finding directory named "blog" from the current working directory of Node.
|
||||
const postDirectory = path.join(process.cwd(), directory)
|
||||
|
||||
console.log('postDirectory', postDirectory)
|
||||
|
||||
const fullPath = path.join(postDirectory, `${slug}.mdx`)
|
||||
|
||||
console.log('fullPath', fullPath)
|
||||
|
||||
const postContent = fs.readFileSync(fullPath, 'utf8')
|
||||
|
||||
return postContent
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/* .toc a {
|
||||
text-decoration: none !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.toc > ul > li {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
@apply text-sm !important;
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
|
||||
.toc ul > li:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: none;
|
||||
border-radius: 50%;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
top: 0.6875em;
|
||||
left: 0.25em;
|
||||
}
|
||||
|
||||
.header:hover svg,
|
||||
.header svg:hover {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.article h1,
|
||||
.article h2,
|
||||
.article h3,
|
||||
.article h4,
|
||||
.article h5 {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.article {
|
||||
@apply mb-16;
|
||||
}
|
||||
|
||||
.article > div > pre {
|
||||
padding: 0;
|
||||
background: none;
|
||||
} */
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Badge, Card, Divider, IconChevronLeft, IconFile, Space } from '@supabase/ui'
|
||||
import matter from 'gray-matter'
|
||||
import authors from 'lib/authors.json'
|
||||
import hydrate from 'next-mdx-remote/hydrate'
|
||||
import renderToString from 'next-mdx-remote/render-to-string'
|
||||
import { NextSeo } from 'next-seo'
|
||||
import Link from 'next/link'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import CTABanner from '~/components/CTABanner'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import Quote from '~/components/Quote'
|
||||
import Avatar from '~/components/Avatar'
|
||||
import ImageGrid from '~/components/ImageGrid'
|
||||
import { generateReadingTime } from '~/lib/helpers'
|
||||
import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts'
|
||||
import blogStyles from './[slug].module.css'
|
||||
import LayoutComparison from '~/layouts/comparison'
|
||||
|
||||
// import all components used in blog articles here
|
||||
// for instance, if you use a button, you must add `Button` in the components object below.
|
||||
const components = {
|
||||
CodeBlock,
|
||||
Quote,
|
||||
Avatar,
|
||||
code: (props: any) => {
|
||||
return <CodeBlock {...props} />
|
||||
},
|
||||
ImageGrid,
|
||||
}
|
||||
|
||||
// plugins for next-mdx-remote
|
||||
const gfm = require('remark-gfm')
|
||||
const slug = require('rehype-slug')
|
||||
|
||||
// table of contents extractor
|
||||
const toc = require('markdown-toc')
|
||||
|
||||
export async function getStaticPaths() {
|
||||
console.log('slug', slug)
|
||||
console.log('gfm', gfm)
|
||||
const paths = getAllPostSlugs('_comparison_landing_pages')
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }: any) {
|
||||
const filePath = `${params.slug}`
|
||||
const postContent = await getPostdata(filePath, '_comparison_landing_pages')
|
||||
const { data, content } = matter(postContent)
|
||||
|
||||
const mdxSource: any = await renderToString(content, {
|
||||
components,
|
||||
scope: data,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [gfm],
|
||||
rehypePlugins: [slug],
|
||||
},
|
||||
})
|
||||
|
||||
const relatedPosts = getSortedPosts('_comparison_landing_pages', 5, mdxSource.scope.tags)
|
||||
|
||||
const allPosts = getSortedPosts('_comparison_landing_pages')
|
||||
|
||||
const currentIndex = allPosts
|
||||
.map(function (e) {
|
||||
return e.slug
|
||||
})
|
||||
.indexOf(filePath)
|
||||
|
||||
const nextPost = allPosts[currentIndex + 1]
|
||||
const prevPost = allPosts[currentIndex - 1]
|
||||
|
||||
return {
|
||||
props: {
|
||||
prevPost: currentIndex === 0 ? null : prevPost ? prevPost : null,
|
||||
nextPost: currentIndex === allPosts.length ? null : nextPost ? nextPost : null,
|
||||
relatedPosts,
|
||||
blog: {
|
||||
slug: `${params.year}/${params.month}/${params.day}/${params.slug}`,
|
||||
content: mdxSource,
|
||||
...data,
|
||||
toc: toc(content, { maxdepth: data.toc_depth ? data.toc_depth : 2 }),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function BlogPostPage(props: any) {
|
||||
// @ts-ignore
|
||||
const content = hydrate(props.blog.content, { components })
|
||||
const authorArray = props.blog.author.split(',')
|
||||
|
||||
const author = []
|
||||
for (let i = 0; i < authorArray.length; i++) {
|
||||
author.push(
|
||||
// @ts-ignore
|
||||
authors.find((authors: string) => {
|
||||
// @ts-ignore
|
||||
return authors.author_id === authorArray[i]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const { basePath } = useRouter()
|
||||
|
||||
const NextCard = (props: any) => {
|
||||
const { post, label, className } = props
|
||||
return (
|
||||
<Link href={`/blog/${post.url}`} as={`/blog/${post.url}`}>
|
||||
<div className={className}>
|
||||
<div className="border-scale-500 hover:bg-scale-100 dark:hover:bg-scale-300 cursor-pointer rounded border p-6 transition">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-scale-900 text-sm">{label}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-scale-1200 text-lg">{post.title}</h4>
|
||||
<p className="small">{post.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const toc = props.blog.toc && (
|
||||
<div className="space-y-8 py-8 lg:py-0">
|
||||
<div>
|
||||
<div className="space-x-2">
|
||||
{props.blog.tags.map((tag: string) => {
|
||||
return (
|
||||
<a href={`/blog/tags/${tag}`} key={`category-badge-${tag}`}>
|
||||
<Badge>{tag}</Badge>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-scale-1200">On this page</p>
|
||||
<div>
|
||||
<div className={[blogStyles['toc'], 'prose prose-toc'].join(' ')}>
|
||||
<ReactMarkdown plugins={[gfm]}>{props.blog.toc.content}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return <LayoutComparison components={components} props={props} gfm={gfm} slug={slug} />
|
||||
}
|
||||
|
||||
// function BlogPostPage() {
|
||||
// return <h1>blog post</h1>
|
||||
// }
|
||||
|
||||
export default BlogPostPage
|
||||
@@ -0,0 +1,46 @@
|
||||
/* .toc a {
|
||||
text-decoration: none !important;
|
||||
font-weight: 400 !important;
|
||||
}
|
||||
|
||||
.toc > ul > li {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
@apply text-sm !important;
|
||||
padding-left: 1rem !important;
|
||||
}
|
||||
|
||||
.toc ul > li:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: none;
|
||||
border-radius: 50%;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
top: 0.6875em;
|
||||
left: 0.25em;
|
||||
}
|
||||
|
||||
.header:hover svg,
|
||||
.header svg:hover {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.article h1,
|
||||
.article h2,
|
||||
.article h3,
|
||||
.article h4,
|
||||
.article h5 {
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
|
||||
.article {
|
||||
@apply mb-16;
|
||||
}
|
||||
|
||||
.article > div > pre {
|
||||
padding: 0;
|
||||
background: none;
|
||||
} */
|
||||
@@ -0,0 +1,107 @@
|
||||
import matter from 'gray-matter'
|
||||
import authors from 'lib/authors.json'
|
||||
import hydrate from 'next-mdx-remote/hydrate'
|
||||
import renderToString from 'next-mdx-remote/render-to-string'
|
||||
import React from 'react'
|
||||
import Avatar from '~/components/Avatar'
|
||||
import CodeBlock from '~/components/CodeBlock/CodeBlock'
|
||||
import ImageGrid from '~/components/ImageGrid'
|
||||
import Quote from '~/components/Quote'
|
||||
import LayoutComparison from '~/layouts/comparison'
|
||||
import { getAllPostSlugs, getPostdata, getSortedPosts } from '~/lib/posts'
|
||||
|
||||
// import all components used in blog articles here
|
||||
// for instance, if you use a button, you must add `Button` in the components object below.
|
||||
const components = {
|
||||
CodeBlock,
|
||||
Quote,
|
||||
Avatar,
|
||||
code: (props: any) => {
|
||||
return <CodeBlock {...props} />
|
||||
},
|
||||
ImageGrid,
|
||||
}
|
||||
|
||||
// plugins for next-mdx-remote
|
||||
const gfm = require('remark-gfm')
|
||||
const slug = require('rehype-slug')
|
||||
|
||||
// table of contents extractor
|
||||
const toc = require('markdown-toc')
|
||||
|
||||
export async function getStaticPaths() {
|
||||
console.log('slug', slug)
|
||||
console.log('gfm', gfm)
|
||||
const paths = getAllPostSlugs('_alternatives')
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticProps({ params }: any) {
|
||||
const filePath = `${params.slug}`
|
||||
const postContent = await getPostdata(filePath, '_alternatives')
|
||||
const { data, content } = matter(postContent)
|
||||
|
||||
const mdxSource: any = await renderToString(content, {
|
||||
components,
|
||||
scope: data,
|
||||
mdxOptions: {
|
||||
remarkPlugins: [gfm],
|
||||
rehypePlugins: [slug],
|
||||
},
|
||||
})
|
||||
|
||||
const relatedPosts = getSortedPosts('_alternatives', 5, mdxSource.scope.tags)
|
||||
|
||||
const allPosts = getSortedPosts('_alternatives')
|
||||
|
||||
const currentIndex = allPosts
|
||||
.map(function (e) {
|
||||
return e.slug
|
||||
})
|
||||
.indexOf(filePath)
|
||||
|
||||
const nextPost = allPosts[currentIndex + 1]
|
||||
const prevPost = allPosts[currentIndex - 1]
|
||||
|
||||
return {
|
||||
props: {
|
||||
prevPost: currentIndex === 0 ? null : prevPost ? prevPost : null,
|
||||
nextPost: currentIndex === allPosts.length ? null : nextPost ? nextPost : null,
|
||||
relatedPosts,
|
||||
blog: {
|
||||
slug: `${params.year}/${params.month}/${params.day}/${params.slug}`,
|
||||
content: mdxSource,
|
||||
...data,
|
||||
toc: toc(content, { maxdepth: data.toc_depth ? data.toc_depth : 2 }),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function BlogPostPage(props: any) {
|
||||
// @ts-ignore
|
||||
const content = hydrate(props.blog.content, { components })
|
||||
const authorArray = props.blog.author.split(',')
|
||||
|
||||
const author = []
|
||||
for (let i = 0; i < authorArray.length; i++) {
|
||||
author.push(
|
||||
// @ts-ignore
|
||||
authors.find((authors: string) => {
|
||||
// @ts-ignore
|
||||
return authors.author_id === authorArray[i]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
return <LayoutComparison components={components} props={props} gfm={gfm} slug={slug} />
|
||||
}
|
||||
|
||||
// function BlogPostPage() {
|
||||
// return <h1>blog post</h1>
|
||||
// }
|
||||
|
||||
export default BlogPostPage
|
||||
@@ -18,6 +18,7 @@ module.exports = ui({
|
||||
'../../packages/common/**/*.{ts,tsx}',
|
||||
'./src/**/*.{ts,tsx}',
|
||||
'./components/**/*.tsx',
|
||||
'./layouts/**/*.tsx',
|
||||
'./pages/**/*.tsx',
|
||||
'./_blog/*.mdx',
|
||||
// purge styles from supabase ui theme
|
||||
|
||||
Reference in New Issue
Block a user