├── .env.example ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── pull_request_template.md ├── .gitignore ├── .nvmrc ├── .prettierignore ├── LICENSE.md ├── README.md ├── codegen.yml ├── components.json ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── demo.png └── opengraph-image.png ├── src ├── app │ ├── about │ │ └── page.tsx │ ├── blog │ │ ├── [slug] │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── preview │ │ │ └── [id] │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── all-blog-posts-list.tsx │ ├── analytics.tsx │ ├── badge-list-item.tsx │ ├── badge-list.tsx │ ├── bio.tsx │ ├── blog-post-list-item.tsx │ ├── blog-post-list.tsx │ ├── blog-tags-filter.tsx │ ├── card-list-skeleton.tsx │ ├── create-next-app.tsx │ ├── filter.tsx │ ├── footer.tsx │ ├── header-nav.tsx │ ├── header.tsx │ ├── mdx.tsx │ ├── mode-toggle.tsx │ ├── paragraph-skeleton.tsx │ ├── providers.tsx │ ├── scripts.tsx │ ├── search.tsx │ ├── sort.tsx │ ├── theme-provider.tsx │ └── ui │ │ ├── accordion.tsx │ │ ├── alert-dialog.tsx │ │ ├── alert.tsx │ │ ├── aspect-ratio.tsx │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── calendar.tsx │ │ ├── card.tsx │ │ ├── carousel.tsx │ │ ├── checkbox.tsx │ │ ├── collapsible.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── menubar.tsx │ │ ├── navigation-menu.tsx │ │ ├── pagination.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── radio-group.tsx │ │ ├── resizable.tsx │ │ ├── scroll-area.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── slider.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ ├── textarea.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ ├── tooltip.tsx │ │ └── use-toast.ts ├── hashnode │ ├── api-url.ts │ ├── fragments │ │ ├── Draft.graphql │ │ ├── PageInfo.graphql │ │ ├── Post.graphql │ │ └── Publication.graphql │ ├── generated │ │ ├── graphql.ts │ │ └── schema.graphql │ └── queries │ │ ├── GetAuthorUsername.graphql │ │ ├── GetDraftById.graphql │ │ ├── GetPostBySlug.graphql │ │ ├── GetPosts.graphql │ │ ├── GetPublication.graphql │ │ └── GetUser.graphql ├── lib │ ├── create-post-json-ld.ts │ ├── create-publication-json-ld.ts │ └── utils.ts ├── server │ ├── get-all-blog-posts.ts │ ├── get-all-blog-tags.ts │ ├── get-author-username.ts │ ├── get-blog-post-draft.ts │ ├── get-blog-post.ts │ ├── get-blog-posts.ts │ ├── get-publication.ts │ └── get-user.ts └── types │ └── sort-types.ts ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | HASHNODE_HOST= -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @alexkates -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to hashnode-next 2 | 3 | Thank you for considering contributing to hashnode-next. Your contributions are highly valued and appreciated. 4 | 5 | ## The basics 6 | 7 | - Before submitting a new issue or PR, check if it already exists in [issues](https://github.com/alexkates/hashnode-next/issues) or [PRs](https://github.com/alexkates/hashnode-next/pulls). 8 | - If there isn't an issue please _create one_ before any development begins 9 | - If you're working on an issue, please _comment_ on it so that others know you're working on it 10 | 11 | ## Developing 12 | 13 | The development branch is `main`. This is the branch that all pull 14 | requests should be made against. 15 | 16 | To develop locally: 17 | 18 | 1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your 19 | own GitHub account and then 20 | [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. 21 | 2. Create a new branch: 22 | 23 | ```sh 24 | git switch -c MY_BRANCH_NAME 25 | ``` 26 | 27 | ## Installing 28 | 29 | hashnode-next uses [NVM](https://github.com/nvm-sh/nvm/blob/master/README.md) and [PNPM](https://pnpm.io/) for package management. 30 | 31 | To set the correct version of PNPM, run `nvm use` from the root. There is a `.nvmrc` file that controls the correct node version. 32 | 33 | ## Installing dependencies 34 | 35 | ```bash 36 | pnpm install 37 | ``` 38 | 39 | ## Hashnode GQL codegen 40 | 41 | If you need to add or update the Hashnode GQL schema, run the following command to generate types and gql documents. 42 | 43 | ```bash 44 | pnpm codegen 45 | ``` 46 | 47 | ## Building 48 | 49 | ```bash 50 | pnpm build 51 | ``` 52 | 53 | ## Linting 54 | 55 | ```sh 56 | pnpm format 57 | pnpm lint 58 | ``` 59 | 60 | If you get errors, be sure to fix them before committing. 61 | 62 | ## Making a Pull Request 63 | 64 | - Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating your PR. 65 | - If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). 66 | - Be sure to fill the PR Template accordingly. 67 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## What does this PR do? 2 | 3 | Fixes # (issue) 4 | 5 | _If there is not an issue for this, please create one first. This is used to tracking purposes and also helps use understand why this PR exists_ 6 | 7 | ## Type of change 8 | 9 | 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] Chore (refactoring code, technical debt, workflow improvements) 13 | - [ ] Enhancement (small improvements) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 16 | 17 | ## How should this be tested? 18 | 19 | - Do a thing, then do another thing 20 | - Confirm that this thing happened 21 | 22 | ### Things to remember 23 | 24 | - [ ] Filled out the "How to test" section in this PR 25 | - [ ] Read [Contributing Guide](./CONTRIBUTING.md) 26 | - [ ] Ran `pnpm build` 27 | - [ ] Ran `pnpm format` 28 | - [ ] Ran `pnpm lint` 29 | - [ ] Checked for warnings, there are none 30 | - [ ] Removed all `console.logs` 31 | - [ ] Merged the latest changes from main onto my branch with `git pull origin main` 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | node_modules 3 | src/hashnode/generated -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 hashnode-next 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |
6 | View Demo 7 | · 8 | Report Bug 9 | · 10 | Request Feature 11 |

12 |
13 | 14 | ## Getting Started 15 | 16 | ```console 17 | npx create-next-app -e https://github.com/alexkates/hashnode-next 18 | ``` 19 | 20 | ## Environment Variables 21 | 22 | [.env.example](.env.example) contains all the environment variables you need to run a copy of hashnode-next for your headless blog. You can simply copy this file and rename it to `.env.local` to get started. 23 | 24 | ## Contributing 25 | 26 | Please read through our [contributing guide](.github/CONTRIBUTING.md) before starting any work. 27 | 28 | 1. Fork the Project 29 | 2. Create your Feature Branch (`git checkout -b feature/my-amazing-feature`) 30 | 3. Commit your Changes (`git commit -am 'Add some my-amazing-feature'`) 31 | 4. Push to the Branch (`git push origin feature/my-amazing-feature`) 32 | 5. Open a Pull Request 33 | 34 | ## Authors 35 | 36 | 37 | 38 | 39 | 40 | ## Stats 41 | 42 | ![Alt](https://repobeats.axiom.co/api/embed/31e702ad0bc3e2c1d5fa3bf57d06845b2a052336.svg "Repobeats analytics image") 43 | 44 | ## License 45 | 46 | Distributed under the MIT License. See the [license](LICENSE.md) for more information. 47 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | schema: https://gql.hashnode.com 2 | documents: "./src/**/hashnode/**/*.graphql" 3 | generates: 4 | ./src/hashnode/generated/schema.graphql: 5 | plugins: 6 | - schema-ast 7 | config: 8 | includeDirectives: true 9 | ./src/hashnode/generated/graphql.ts: 10 | plugins: 11 | - typescript 12 | - typescript-operations 13 | - typed-document-node 14 | config: 15 | scalars: 16 | Date: string 17 | DateTime: string 18 | ObjectId: string 19 | JSONObject: Record 20 | Decimal: string 21 | CurrencyCode: string 22 | ImageContentType: string 23 | ImageUrl: string 24 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const HASHNODE_ANALYTICS_BASE_URL = "https://hn-ping2.hashnode.com"; 2 | 3 | /** @type {import('next').NextConfig} */ 4 | const nextConfig = { 5 | async rewrites() { 6 | return [ 7 | { 8 | source: "/ping/data-event", 9 | destination: `${HASHNODE_ANALYTICS_BASE_URL}/api/data-event`, 10 | }, 11 | { 12 | source: "/ping/view", 13 | destination: `${HASHNODE_ANALYTICS_BASE_URL}/api/view`, 14 | }, 15 | ]; 16 | }, 17 | images: { 18 | remotePatterns: [ 19 | { 20 | protocol: "https", 21 | hostname: "cdn.hashnode.com", 22 | }, 23 | ], 24 | }, 25 | }; 26 | 27 | module.exports = nextConfig; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hashnode-next", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "format": "prettier --write .", 11 | "codegen": "graphql-codegen --config codegen.yml" 12 | }, 13 | "dependencies": { 14 | "@graphql-typed-document-node/core": "^3.2.0", 15 | "@hookform/resolvers": "^3.3.4", 16 | "@radix-ui/react-accordion": "^1.1.2", 17 | "@radix-ui/react-alert-dialog": "^1.0.5", 18 | "@radix-ui/react-aspect-ratio": "^1.0.3", 19 | "@radix-ui/react-avatar": "^1.0.4", 20 | "@radix-ui/react-checkbox": "^1.0.4", 21 | "@radix-ui/react-collapsible": "^1.0.3", 22 | "@radix-ui/react-context-menu": "^2.1.5", 23 | "@radix-ui/react-dialog": "^1.0.5", 24 | "@radix-ui/react-dropdown-menu": "^2.0.6", 25 | "@radix-ui/react-hover-card": "^1.0.7", 26 | "@radix-ui/react-icons": "^1.3.0", 27 | "@radix-ui/react-label": "^2.0.2", 28 | "@radix-ui/react-menubar": "^1.0.4", 29 | "@radix-ui/react-navigation-menu": "^1.1.4", 30 | "@radix-ui/react-popover": "^1.0.7", 31 | "@radix-ui/react-progress": "^1.0.3", 32 | "@radix-ui/react-radio-group": "^1.1.3", 33 | "@radix-ui/react-scroll-area": "^1.0.5", 34 | "@radix-ui/react-select": "^2.0.0", 35 | "@radix-ui/react-separator": "^1.0.3", 36 | "@radix-ui/react-slider": "^1.1.2", 37 | "@radix-ui/react-slot": "^1.0.2", 38 | "@radix-ui/react-switch": "^1.0.3", 39 | "@radix-ui/react-tabs": "^1.0.4", 40 | "@radix-ui/react-toast": "^1.1.5", 41 | "@radix-ui/react-toggle": "^1.0.3", 42 | "@radix-ui/react-toggle-group": "^1.0.4", 43 | "@radix-ui/react-tooltip": "^1.0.7", 44 | "@vercel/analytics": "^1.1.4", 45 | "class-variance-authority": "^0.7.0", 46 | "clsx": "^2.1.0", 47 | "cmdk": "^0.2.1", 48 | "date-fns": "^3.3.1", 49 | "embla-carousel-react": "8.0.0-rc21", 50 | "framer-motion": "^11.0.5", 51 | "graphql-request": "^6.1.0", 52 | "js-cookie": "^3.0.5", 53 | "lucide-react": "^0.316.0", 54 | "next": "14.1.0", 55 | "next-themes": "^0.2.1", 56 | "react": "^18.2.0", 57 | "react-day-picker": "^8.10.0", 58 | "react-dom": "^18.2.0", 59 | "react-hook-form": "^7.50.1", 60 | "react-markdown": "^9.0.1", 61 | "react-resizable-panels": "^1.0.10", 62 | "react-tweet": "^3.2.0", 63 | "react-wrap-balancer": "^1.1.0", 64 | "rehype-autolink-headings": "^7.1.0", 65 | "rehype-highlight": "^7.0.0", 66 | "rehype-raw": "^7.0.0", 67 | "rehype-slug": "^6.0.0", 68 | "remark-gfm": "^4.0.0", 69 | "sonner": "^1.4.0", 70 | "tailwind-merge": "^2.2.1", 71 | "use-debounce": "^10.0.0", 72 | "uuid": "^9.0.1", 73 | "vaul": "^0.8.9", 74 | "zod": "^3.22.4" 75 | }, 76 | "devDependencies": { 77 | "@graphql-codegen/cli": "^5.0.2", 78 | "@graphql-codegen/typed-document-node": "^5.0.4", 79 | "@graphql-codegen/typescript": "^4.0.4", 80 | "@graphql-codegen/typescript-operations": "^4.1.2", 81 | "@tailwindcss/typography": "^0.5.10", 82 | "@types/js-cookie": "^3.0.6", 83 | "@types/node": "^20.11.19", 84 | "@types/react": "^18.2.55", 85 | "@types/react-dom": "^18.2.19", 86 | "@types/uuid": "^9.0.8", 87 | "autoprefixer": "^10.4.17", 88 | "eslint": "^8.56.0", 89 | "eslint-config-next": "14.1.0", 90 | "postcss": "^8.4.35", 91 | "prettier-plugin-organize-imports": "^3.2.4", 92 | "prettier-plugin-tailwindcss": "^0.5.11", 93 | "tailwindcss": "^3.4.1", 94 | "tailwindcss-animate": "^1.0.7", 95 | "tailwindcss-animation-delay": "^1.2.0", 96 | "typescript": "^5.3.3" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | // prettier.config.js, .prettierrc.js, prettier.config.mjs, or .prettierrc.mjs 2 | 3 | /** @type {import("prettier").Config} */ 4 | const config = { 5 | printWidth: 150, 6 | plugins: ["prettier-plugin-tailwindcss", "prettier-plugin-organize-imports"], 7 | }; 8 | 9 | module.exports = config; 10 | -------------------------------------------------------------------------------- /public/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexkates/hashnode-next/5b0bc6249ccb5c44bebb4c990c26a4056648a34f/public/demo.png -------------------------------------------------------------------------------- /public/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexkates/hashnode-next/5b0bc6249ccb5c44bebb4c990c26a4056648a34f/public/opengraph-image.png -------------------------------------------------------------------------------- /src/app/about/page.tsx: -------------------------------------------------------------------------------- 1 | import BadgeList from "@/components/badge-list"; 2 | import Bio from "@/components/bio"; 3 | import CardListSkeleton from "@/components/card-list-skeleton"; 4 | import ParagraphSkeleton from "@/components/paragraph-skeleton"; 5 | import { cn, fadeIn } from "@/lib/utils"; 6 | import { Suspense } from "react"; 7 | 8 | export default async function Home() { 9 | return ( 10 |
11 |
12 | }> 13 | 14 | 15 |
16 |
17 | }> 18 | Here are some Hashnode badges that I've earned 19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/app/blog/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | import Analytics from "@/components/analytics"; 2 | import { Mdx } from "@/components/mdx"; 3 | import createPostJsonLd from "@/lib/create-post-json-ld"; 4 | import { cn, fadeIn } from "@/lib/utils"; 5 | import getBlogPost from "@/server/get-blog-post"; 6 | import getPublication from "@/server/get-publication"; 7 | import { Metadata } from "next/types"; 8 | 9 | type Props = { 10 | params: { 11 | slug: string; 12 | }; 13 | }; 14 | 15 | export async function generateMetadata({ params }: Props) { 16 | const post = await getBlogPost(params); 17 | 18 | const title = post?.seo?.title || post?.title; 19 | const canonicalUrl = post?.canonicalUrl; 20 | const description = post?.seo?.description || post?.subtitle || post?.title; 21 | const images = post?.coverImage?.url; 22 | 23 | const metadata: Metadata = { 24 | title, 25 | description, 26 | alternates: { 27 | canonical: canonicalUrl, 28 | }, 29 | openGraph: { 30 | title, 31 | description, 32 | type: "article", 33 | siteName: "Alex Kates | Blog", 34 | images, 35 | }, 36 | twitter: { 37 | card: "summary_large_image", 38 | title, 39 | description, 40 | images, 41 | creator: "@thealexkates", 42 | }, 43 | }; 44 | 45 | return metadata; 46 | } 47 | 48 | export default async function Page({ params }: Props) { 49 | const post = await getBlogPost(params); 50 | const publication = await getPublication(); 51 | 52 | if (!post) { 53 | return null; 54 | } 55 | 56 | const jsonLd = createPostJsonLd(publication, post); 57 | 58 | const { 59 | publishedAt, 60 | readTimeInMinutes, 61 | title, 62 | views, 63 | id, 64 | content: { markdown }, 65 | } = post; 66 | 67 | return ( 68 | <> 69 |
70 |

{title}

71 |

72 | {new Date(publishedAt).toLocaleDateString()} • {views} views • {readTimeInMinutes} min read 73 |

74 |
75 |
76 | 77 |
78 | 79 |