├── .dockerignore ├── .editorconfig ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .vscode └── extensions.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── README.md ├── license.md ├── package.json ├── packages ├── blog-starter-kit │ └── themes │ │ ├── enterprise │ │ ├── .env.example │ │ ├── .eslintrc.js │ │ ├── .graphqlrc.yml │ │ ├── @types │ │ │ └── remark-html.d.ts │ │ ├── README.md │ │ ├── assets │ │ │ ├── PlusJakartaSans-Bold.ttf │ │ │ ├── PlusJakartaSans-ExtraBold.ttf │ │ │ ├── PlusJakartaSans-Medium.ttf │ │ │ ├── PlusJakartaSans-Regular.ttf │ │ │ └── PlusJakartaSans-SemiBold.ttf │ │ ├── codegen.yml │ │ ├── components │ │ │ ├── about-author.tsx │ │ │ ├── analytics.tsx │ │ │ ├── avatar.tsx │ │ │ ├── button.tsx │ │ │ ├── co-authors-modal.tsx │ │ │ ├── container.tsx │ │ │ ├── contexts │ │ │ │ └── appContext.tsx │ │ │ ├── cover-image.tsx │ │ │ ├── custom-image.tsx │ │ │ ├── date-formatter.tsx │ │ │ ├── footer.tsx │ │ │ ├── header.tsx │ │ │ ├── hero-post.tsx │ │ │ ├── icons │ │ │ │ ├── index.js │ │ │ │ └── svgs │ │ │ │ │ ├── ArticleSVG.js │ │ │ │ │ ├── BookOpenSVG.js │ │ │ │ │ ├── ChevronDownSVG.js │ │ │ │ │ ├── CloseSVG.js │ │ │ │ │ ├── ExternalArrowSVG.js │ │ │ │ │ ├── GithubSVG.js │ │ │ │ │ ├── HamburgerSVG.js │ │ │ │ │ ├── HashnodeSVG.js │ │ │ │ │ ├── LinkedinSVG.js │ │ │ │ │ ├── NewsletterPlusSVG.js │ │ │ │ │ ├── PlusCircleSVG.js │ │ │ │ │ ├── RssSVG.js │ │ │ │ │ ├── XSVG.js │ │ │ │ │ └── index.js │ │ │ ├── integrations.tsx │ │ │ ├── layout.tsx │ │ │ ├── markdown-styles.module.css │ │ │ ├── markdown-to-html.tsx │ │ │ ├── meta.tsx │ │ │ ├── more-posts.tsx │ │ │ ├── navbar.tsx │ │ │ ├── post-author-info.tsx │ │ │ ├── post-comments.tsx │ │ │ ├── post-header.tsx │ │ │ ├── post-preview.tsx │ │ │ ├── post-read-time-in-minutes.tsx │ │ │ ├── post-title.tsx │ │ │ ├── post-toc.tsx │ │ │ ├── profile-image.js │ │ │ ├── progressive-image.tsx │ │ │ ├── publication-logo.tsx │ │ │ ├── resizable-image.js │ │ │ ├── scripts.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── searchbar.tsx │ │ │ ├── secondary-post.tsx │ │ │ ├── section-separator.tsx │ │ │ ├── sidebar.tsx │ │ │ ├── social-links.tsx │ │ │ ├── subscribe-form.tsx │ │ │ └── subscribe.tsx │ │ ├── generated │ │ │ ├── graphql.ts │ │ │ └── schema.graphql │ │ ├── lib │ │ │ └── api │ │ │ │ ├── fragments │ │ │ │ ├── PageInfo.graphql │ │ │ │ ├── Post.graphql │ │ │ │ └── Publication.graphql │ │ │ │ ├── mutations │ │ │ │ └── SubscribeToNewsletter.graphql │ │ │ │ └── queries │ │ │ │ ├── DraftById.graphql │ │ │ │ ├── PageByPublication.graphql │ │ │ │ ├── PostsByPublication.graphql │ │ │ │ ├── PublicationByHost.graphql │ │ │ │ ├── RSSFeed.graphql │ │ │ │ ├── SearchPostsOfPublication.graphql │ │ │ │ ├── SeriesPostsByPublication.graphql │ │ │ │ ├── SinglePostByPublication.graphql │ │ │ │ ├── Sitemap.graphql │ │ │ │ ├── SlugPostsByPublication.graphql │ │ │ │ └── TagPostsByPublication.graphql │ │ ├── next.config.js │ │ ├── package.json │ │ ├── pages │ │ │ ├── [slug].tsx │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ ├── api │ │ │ │ └── og │ │ │ │ │ ├── home.tsx │ │ │ │ │ └── post.tsx │ │ │ ├── dashboard.tsx │ │ │ ├── index.tsx │ │ │ ├── preview │ │ │ │ └── [id].tsx │ │ │ ├── robots.txt.tsx │ │ │ ├── rss.xml.tsx │ │ │ ├── series │ │ │ │ └── [slug].tsx │ │ │ ├── sitemap.xml.tsx │ │ │ └── tag │ │ │ │ └── [slug].tsx │ │ ├── postcss.config.js │ │ ├── process-env.d.ts │ │ ├── public │ │ │ ├── assets │ │ │ │ └── blog │ │ │ │ │ ├── authors │ │ │ │ │ ├── jj.jpeg │ │ │ │ │ ├── joe.jpeg │ │ │ │ │ └── tim.jpeg │ │ │ │ │ ├── dynamic-routing │ │ │ │ │ └── cover.jpg │ │ │ │ │ ├── hello-world │ │ │ │ │ └── cover.jpg │ │ │ │ │ └── preview │ │ │ │ │ └── cover.jpg │ │ │ ├── favicon │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── browserconfig.xml │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ ├── mstile-150x150.png │ │ │ │ └── safari-pinned-tab.svg │ │ │ └── js │ │ │ │ └── iframe-resizer.js │ │ ├── styles │ │ │ └── index.css │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── utils │ │ │ └── const │ │ │ │ └── index.ts │ │ └── vercel.json │ │ ├── hashnode │ │ ├── .env.example │ │ ├── .eslintrc.js │ │ ├── .graphqlrc.yml │ │ ├── @types │ │ │ └── remark-html.d.ts │ │ ├── README.md │ │ ├── assets │ │ │ ├── PlusJakartaSans-Bold.ttf │ │ │ ├── PlusJakartaSans-ExtraBold.ttf │ │ │ ├── PlusJakartaSans-Medium.ttf │ │ │ ├── PlusJakartaSans-Regular.ttf │ │ │ └── PlusJakartaSans-SemiBold.ttf │ │ ├── codegen.yml │ │ ├── components │ │ │ ├── about-author.tsx │ │ │ ├── analytics.tsx │ │ │ ├── blog-post-preview.tsx │ │ │ ├── co-authors-modal.tsx │ │ │ ├── comments-sheet.tsx │ │ │ ├── common-header-icon-btn.tsx │ │ │ ├── container.tsx │ │ │ ├── contexts │ │ │ │ └── appContext.tsx │ │ │ ├── custom-button.tsx │ │ │ ├── custom-image.tsx │ │ │ ├── draft-floating-menu.tsx │ │ │ ├── features-posts.tsx │ │ │ ├── fonts │ │ │ │ └── index.tsx │ │ │ ├── header-blog-search.tsx │ │ │ ├── header-left-sidebar.tsx │ │ │ ├── header-tooltip.tsx │ │ │ ├── header.tsx │ │ │ ├── hn-button.tsx │ │ │ ├── icons │ │ │ │ ├── index.js │ │ │ │ └── svgs │ │ │ │ │ ├── AlertSVG.js │ │ │ │ │ ├── ArticleSVG.js │ │ │ │ │ ├── BadgeDollarSVG.js │ │ │ │ │ ├── BarsSVG.js │ │ │ │ │ ├── BookOpenSVG.js │ │ │ │ │ ├── ChartMixedSVG.js │ │ │ │ │ ├── CheckSVG.js │ │ │ │ │ ├── ChevronDownSVG.js │ │ │ │ │ ├── ChevronDownSVGV2.js │ │ │ │ │ ├── ChevronDownSVG_16x16.js │ │ │ │ │ ├── ChevronLeftSVG.js │ │ │ │ │ ├── ChevronRightSVG_16x16.js │ │ │ │ │ ├── ChevronUpSVG_16x16.js │ │ │ │ │ ├── ClipboardSVG.js │ │ │ │ │ ├── CloseSVG.js │ │ │ │ │ ├── CommentSVGV2.js │ │ │ │ │ ├── EarthSVG.js │ │ │ │ │ ├── ExternalArrowSVG.js │ │ │ │ │ ├── ExternalLinkSVG.js │ │ │ │ │ ├── FacebookSVGRound.js │ │ │ │ │ ├── FeaturedStarV2SVG.js │ │ │ │ │ ├── FileLineChartSVG.js │ │ │ │ │ ├── GithubSVG.js │ │ │ │ │ ├── HackernewsSVGV2.js │ │ │ │ │ ├── HamburgerSVG.js │ │ │ │ │ ├── HashnodeLogoIconV2.js │ │ │ │ │ ├── HashnodeSVG.js │ │ │ │ │ ├── HeadphonesSVG.js │ │ │ │ │ ├── InstagramSVG.js │ │ │ │ │ ├── LinkAltSVG.js │ │ │ │ │ ├── LinkSVGV2.js │ │ │ │ │ ├── LinkedInSVGV2.js │ │ │ │ │ ├── LinkedinSVG.js │ │ │ │ │ ├── ListSVG.js │ │ │ │ │ ├── MastodonSVG.js │ │ │ │ │ ├── NewsletterPlusSVG.js │ │ │ │ │ ├── NoCommentsDarkSVG.js │ │ │ │ │ ├── NoCommentsLightSVG.js │ │ │ │ │ ├── PaperPlaneSVG.js │ │ │ │ │ ├── PencilSVG.js │ │ │ │ │ ├── PinSVG.js │ │ │ │ │ ├── PlusCircleSVG.js │ │ │ │ │ ├── RedditSVG.js │ │ │ │ │ ├── RedditSVGV2.js │ │ │ │ │ ├── RefreshSVG.js │ │ │ │ │ ├── RobotSVG.js │ │ │ │ │ ├── RssSVG.js │ │ │ │ │ ├── SearchSvg.js │ │ │ │ │ ├── ShareSVGV2.tsx │ │ │ │ │ ├── TwitterXSVG.js │ │ │ │ │ ├── WhatsappSVG.js │ │ │ │ │ ├── XSVG.js │ │ │ │ │ ├── YoutubeSVG.js │ │ │ │ │ └── index.js │ │ │ ├── integrations.tsx │ │ │ ├── layout.tsx │ │ │ ├── magazine-blog-post-preview.tsx │ │ │ ├── markdown-styles.module.css │ │ │ ├── meta.tsx │ │ │ ├── modern-layout-posts.tsx │ │ │ ├── other-posts-of-account.tsx │ │ │ ├── post-author-info.tsx │ │ │ ├── post-comments-sidebar.tsx │ │ │ ├── post-comments.tsx │ │ │ ├── post-floating-bar-tooltip-wrapper.tsx │ │ │ ├── post-floating-bar.tsx │ │ │ ├── post-header.tsx │ │ │ ├── post-page-navbar.tsx │ │ │ ├── post-share-widget.tsx │ │ │ ├── post-view.tsx │ │ │ ├── profile-image.js │ │ │ ├── progressive-image.tsx │ │ │ ├── pub-loader-component.tsx │ │ │ ├── publication-footer.tsx │ │ │ ├── publication-logo.tsx │ │ │ ├── publication-meta.tsx │ │ │ ├── publication-nav-links-dropdown.tsx │ │ │ ├── publication-nav-links.tsx │ │ │ ├── publication-posts.tsx │ │ │ ├── publication-search.tsx │ │ │ ├── publication-sidebar-nav-links.tsx │ │ │ ├── publication-sidebar.tsx │ │ │ ├── publication-social-link-item.tsx │ │ │ ├── publication-social-links.tsx │ │ │ ├── publication-subscribe-standout.tsx │ │ │ ├── resizable-image.js │ │ │ ├── response-footer.tsx │ │ │ ├── response-list.tsx │ │ │ ├── response-reply-card.tsx │ │ │ ├── scripts.tsx │ │ │ ├── scroll-area.tsx │ │ │ ├── section-separator.tsx │ │ │ ├── separator-root.js │ │ │ ├── static-page-content.tsx │ │ │ ├── toast.js │ │ │ ├── toc-render-design.tsx │ │ │ ├── toc-sheet.tsx │ │ │ └── use-sticky-nav-scroll.tsx │ │ ├── generated │ │ │ ├── graphql.ts │ │ │ └── schema.graphql │ │ ├── lib │ │ │ └── api │ │ │ │ ├── client.ts │ │ │ │ ├── fragments │ │ │ │ ├── Draft.graphql │ │ │ │ ├── PageInfo.graphql │ │ │ │ ├── Post.graphql │ │ │ │ ├── PostThumbnail.graphql │ │ │ │ ├── Publication.graphql │ │ │ │ └── StaticPage.graphql │ │ │ │ ├── mutations │ │ │ │ └── SubscribeToNewsletter.graphql │ │ │ │ └── queries │ │ │ │ ├── DraftById.graphql │ │ │ │ ├── HomePage.graphql │ │ │ │ ├── Newsletter.graphql │ │ │ │ ├── PageByPublication.graphql │ │ │ │ ├── PostsByPublication.graphql │ │ │ │ ├── PublicationByHost.graphql │ │ │ │ ├── RSSFeed.graphql │ │ │ │ ├── SearchPostsOfPublication.graphql │ │ │ │ ├── SeriesPageInitial.graphql │ │ │ │ ├── SeriesPostsByPublication.graphql │ │ │ │ ├── SinglePostByPublication.graphql │ │ │ │ ├── Sitemap.graphql │ │ │ │ ├── SlugPostsByPublication.graphql │ │ │ │ ├── Tag.graphql │ │ │ │ └── TagPostsByPublication.graphql │ │ ├── next.config.js │ │ ├── package.json │ │ ├── pages │ │ │ ├── [slug].tsx │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ ├── api │ │ │ │ └── og │ │ │ │ │ ├── home.tsx │ │ │ │ │ └── post.tsx │ │ │ ├── dashboard.tsx │ │ │ ├── index.tsx │ │ │ ├── newsletter.tsx │ │ │ ├── preview │ │ │ │ └── [id].tsx │ │ │ ├── robots.txt.tsx │ │ │ ├── rss.xml.tsx │ │ │ ├── series │ │ │ │ └── [slug].tsx │ │ │ ├── sitemap.xml.tsx │ │ │ └── tag │ │ │ │ └── [slug].tsx │ │ ├── postcss.config.js │ │ ├── process-env.d.ts │ │ ├── public │ │ │ ├── assets │ │ │ │ └── blog │ │ │ │ │ ├── authors │ │ │ │ │ ├── jj.jpeg │ │ │ │ │ ├── joe.jpeg │ │ │ │ │ └── tim.jpeg │ │ │ │ │ ├── dynamic-routing │ │ │ │ │ └── cover.jpg │ │ │ │ │ ├── hello-world │ │ │ │ │ └── cover.jpg │ │ │ │ │ └── preview │ │ │ │ │ └── cover.jpg │ │ │ ├── favicon │ │ │ │ ├── android-chrome-192x192.png │ │ │ │ ├── android-chrome-512x512.png │ │ │ │ ├── apple-touch-icon.png │ │ │ │ ├── browserconfig.xml │ │ │ │ ├── favicon-16x16.png │ │ │ │ ├── favicon-32x32.png │ │ │ │ ├── favicon.ico │ │ │ │ ├── mstile-150x150.png │ │ │ │ └── safari-pinned-tab.svg │ │ │ └── js │ │ │ │ └── iframe-resizer.js │ │ ├── styles │ │ │ └── index.css │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── types │ │ │ ├── Badge.ts │ │ │ ├── Page.ts │ │ │ ├── Post.ts │ │ │ ├── Publication.ts │ │ │ ├── Response.ts │ │ │ ├── Series.ts │ │ │ ├── User.ts │ │ │ ├── external │ │ │ │ └── mongodb.d.ts │ │ │ ├── extras.ts │ │ │ └── index.ts │ │ ├── utils │ │ │ ├── autolinker.js │ │ │ ├── commonUtils.ts │ │ │ ├── const │ │ │ │ ├── images.ts │ │ │ │ ├── index.ts │ │ │ │ └── styles.ts │ │ │ ├── getReadTime.js │ │ │ ├── gsspHelpers.ts │ │ │ ├── handle-math-jax.js │ │ │ ├── image.js │ │ │ ├── index.js │ │ │ ├── toast.tsx │ │ │ └── urls.ts │ │ └── vercel.json │ │ └── personal │ │ ├── .env.example │ │ ├── .eslintrc.js │ │ ├── .graphqlrc.yml │ │ ├── @types │ │ └── remark-html.d.ts │ │ ├── README.md │ │ ├── assets │ │ ├── PlusJakartaSans-Bold.ttf │ │ ├── PlusJakartaSans-ExtraBold.ttf │ │ ├── PlusJakartaSans-Medium.ttf │ │ ├── PlusJakartaSans-Regular.ttf │ │ └── PlusJakartaSans-SemiBold.ttf │ │ ├── codegen.yml │ │ ├── components │ │ ├── analytics.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── container.tsx │ │ ├── contexts │ │ │ └── appContext.tsx │ │ ├── cover-image.tsx │ │ ├── date-formatter.tsx │ │ ├── footer.tsx │ │ ├── icons │ │ │ ├── index.js │ │ │ └── svgs │ │ │ │ ├── ArticleSVG.js │ │ │ │ ├── ChevronDownSVG.js │ │ │ │ ├── ExternalArrowSVG.js │ │ │ │ ├── GithubSVG.js │ │ │ │ ├── HamburgerSVG.js │ │ │ │ ├── HashnodeSVG.js │ │ │ │ ├── LinkedinSVG.js │ │ │ │ ├── Moon.js │ │ │ │ ├── NewsletterPlusSVG.js │ │ │ │ ├── PlusCircleSVG.js │ │ │ │ ├── RssSVG.js │ │ │ │ ├── Sun.js │ │ │ │ ├── XSVG.js │ │ │ │ └── index.js │ │ ├── integrations.tsx │ │ ├── layout.tsx │ │ ├── markdown-styles.module.css │ │ ├── markdown-to-html.tsx │ │ ├── meta.tsx │ │ ├── minimal-post-preview.tsx │ │ ├── minimal-posts.tsx │ │ ├── personal-theme-header.tsx │ │ ├── scripts.tsx │ │ ├── section-separator.tsx │ │ └── toggle-theme.tsx │ │ ├── generated │ │ ├── graphql.ts │ │ └── schema.graphql │ │ ├── lib │ │ └── api │ │ │ ├── fragments │ │ │ ├── PageInfo.graphql │ │ │ ├── Post.graphql │ │ │ └── Publication.graphql │ │ │ └── queries │ │ │ ├── DraftById.graphql │ │ │ ├── PageByPublication.graphql │ │ │ ├── PostsByPublication.graphql │ │ │ ├── PublicationByHost.graphql │ │ │ ├── RSSFeed.graphql │ │ │ ├── SinglePostByPublication.graphql │ │ │ ├── Sitemap.graphql │ │ │ ├── SlugPostsByPublication.graphql │ │ │ └── TagPostsByPublication.graphql │ │ ├── next.config.js │ │ ├── package.json │ │ ├── pages │ │ ├── [slug].tsx │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── api │ │ │ └── og │ │ │ │ ├── home.tsx │ │ │ │ └── post.tsx │ │ ├── dashboard.tsx │ │ ├── index.tsx │ │ ├── preview │ │ │ └── [id].tsx │ │ ├── robots.txt.tsx │ │ ├── rss.xml.tsx │ │ ├── sitemap.xml.tsx │ │ └── tag │ │ │ └── [slug].tsx │ │ ├── postcss.config.js │ │ ├── process-env.d.ts │ │ ├── public │ │ ├── assets │ │ │ └── blog │ │ │ │ ├── authors │ │ │ │ ├── jj.jpeg │ │ │ │ ├── joe.jpeg │ │ │ │ └── tim.jpeg │ │ │ │ ├── dynamic-routing │ │ │ │ └── cover.jpg │ │ │ │ ├── hello-world │ │ │ │ └── cover.jpg │ │ │ │ └── preview │ │ │ │ └── cover.jpg │ │ ├── favicon │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── browserconfig.xml │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ ├── mstile-150x150.png │ │ │ └── safari-pinned-tab.svg │ │ └── js │ │ │ └── iframe-resizer.js │ │ ├── styles │ │ └── index.css │ │ ├── tailwind.config.js │ │ ├── tsconfig.json │ │ ├── utils │ │ └── const │ │ │ └── index.ts │ │ └── vercel.json ├── eslint-config-custom │ ├── index.js │ └── package.json ├── tsconfig │ ├── base.json │ ├── nextjs.json │ └── package.json └── utils │ ├── feed.ts │ ├── handle-math-jax.js │ ├── image.ts │ ├── package.json │ ├── renderer │ ├── consts │ │ └── images.ts │ ├── headingSlugger.ts │ ├── highlight.js │ ├── hooks │ │ └── useEmbeds.tsx │ ├── image.ts │ ├── markdownToHtml.ts │ ├── marked.js │ ├── sanitizeHTMLOptions.js │ └── services │ │ ├── HNRequest.ts │ │ └── embed.ts │ ├── seo │ ├── addArticleJsonLd.ts │ ├── addPublicationJsonLd.ts │ └── sitemap.ts │ ├── social │ └── og.ts │ └── trigger-custom-widget-embed.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── prettier.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | .gitignore -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_size = 2 7 | indent_style = tabs 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | # Markdown syntax specifies that trailing whitespaces can be meaningful, 12 | # so let’s not trim those. e.g. 2 trailing spaces = linebreak () 13 | # See https://daringfireball.net/projects/markdown/syntax#p 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.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 | **/node_modules 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | **/.next/ 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | # local env files 30 | .env* 31 | !.env.example 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | 40 | # vscode 41 | .vscode/* 42 | !.vscode/extensions.json -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v18 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .cache 3 | .next 4 | */*.yml 5 | generated -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18-alpine 2 | 3 | COPY . /app 4 | 5 | # Set the working directory 6 | WORKDIR /app 7 | 8 | # Install pnpm 9 | RUN npm install -g pnpm 10 | 11 | # By default, use the enterprise theme 12 | ARG THEME=enterprise 13 | 14 | WORKDIR /app/packages/blog-starter-kit/themes/${THEME} 15 | RUN cp .env.example .env.local 16 | RUN pnpm install --frozen-lockfile 17 | 18 | RUN pnpm build 19 | 20 | # Expose the port Next.js runs on 21 | EXPOSE 3000 22 | 23 | # Run the Next.js start script 24 | CMD ["pnpm", "start"] 25 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Hashnode. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hashnode-starter-kit", 3 | "version": "1.0.0", 4 | "description": "An OSS Starter Kit to roll out frontends using Hashnode Public APIs", 5 | "keywords": [], 6 | "license": "MIT", 7 | "author": "", 8 | "scripts": { 9 | "format": "prettier --write \"**/*.{js,jsx,ts,tsx,md,json}\"", 10 | "preinstall": "npx only-allow pnpm" 11 | }, 12 | "dependencies": { 13 | "classnames": "^2.3.1", 14 | "next": "^13.4.19", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^18.0.3", 20 | "@types/react": "^18.0.15", 21 | "@types/react-dom": "^18.0.6", 22 | "prettier": "^3.0.3", 23 | "prettier-plugin-organize-imports": "^3.2.3", 24 | "prettier-plugin-packagejson": "^2.4.6", 25 | "prettier-plugin-tailwindcss": "^0.5.5", 26 | "tailwindcss": "^3.3.3", 27 | "typescript": "^5.2.2" 28 | }, 29 | "engines": { 30 | "node": ">=18.0.0", 31 | "pnpm": ">=8.0.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT=https://gql.hashnode.com 2 | NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST=engineering.hashnode.com 3 | NEXT_PUBLIC_MODE=development -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@starter-kit/eslint-config-custom'], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | schema: './generated/schema.graphql' 2 | documents: './{pages,components,lib}/**/*.{graphql,js,ts,jsx,tsx}' 3 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/@types/remark-html.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'remark-html'; 2 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/README.md: -------------------------------------------------------------------------------- 1 | # A statically generated blog example using Next.js, Markdown, and TypeScript with Hashnode 💫 2 | 3 | This is the existing [blog-starter](https://github.com/vercel/next.js/tree/canary/examples/blog-starter) plus TypeScript, wired with [Hashnode](https://hashnode.com). 4 | 5 | We've used [Hashnode APIs](https://apidocs.hashnode.com) and integrated them with this blog starter kit. 6 | 7 | ## Want to have your own? 8 | 9 | Fork it and change the environment variable `NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST` to your host (engineering.hashnode.dev is the host in the example) and deploy it to Vercel. That's it! You now have your own frontend. You can still use Hashnode for writing your Articles. 10 | 11 | Demo of the `enterprise` theme: [https://demo.hashnode.com/engineering](https://demo.hashnode.com/engineering). 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-Bold.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-ExtraBold.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-Medium.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-Regular.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/assets/PlusJakartaSans-SemiBold.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/codegen.yml: -------------------------------------------------------------------------------- 1 | schema: https://gql.hashnode.com 2 | documents: './**/*.graphql' 3 | generates: 4 | ./generated/schema.graphql: 5 | plugins: 6 | - schema-ast 7 | config: 8 | includeDirectives: true 9 | ./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 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/about-author.tsx: -------------------------------------------------------------------------------- 1 | import PostAuthorInfo from './post-author-info'; 2 | import { useAppContext } from './contexts/appContext'; 3 | import { PostFullFragment } from '../generated/graphql'; 4 | 5 | function AboutAuthor() { 6 | const { post: _post } = useAppContext(); 7 | const post = _post as unknown as PostFullFragment; 8 | const { publication, author } = post; 9 | let coAuthors = post.coAuthors || []; 10 | 11 | const allAuthors = publication?.isTeam ? [author, ...coAuthors] : [author]; 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | Written by 19 | 20 | 21 | {allAuthors.map((_author) => { 22 | return ( 23 | 27 | ); 28 | })} 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default AboutAuthor; -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { resizeImage } from '@starter-kit/utils/image'; 2 | import { DEFAULT_AVATAR } from '../utils/const'; 3 | 4 | type Props = { 5 | username: string; 6 | name: string; 7 | picture: string | null | undefined; 8 | size: number; 9 | }; 10 | 11 | export const Avatar = ({ username, name, picture, size }: Props) => { 12 | return ( 13 | 14 | 24 | 29 | 30 | 31 | 32 | {name} 33 | 34 | 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/container.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | children?: React.ReactNode; 3 | className?: string; 4 | }; 5 | 6 | export const Container = ({ children, className }: Props) => { 7 | return {children}; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/contexts/appContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react'; 2 | import { 3 | PostFullFragment, 4 | PublicationFragment, 5 | SeriesPostsByPublicationQuery, 6 | StaticPageFragment, 7 | } from '../../generated/graphql'; 8 | 9 | type AppContext = { 10 | publication: PublicationFragment; 11 | post: PostFullFragment | null; 12 | page: StaticPageFragment | null; 13 | series: NonNullable['series']; 14 | }; 15 | 16 | const AppContext = createContext(null); 17 | 18 | const AppProvider = ({ 19 | children, 20 | publication, 21 | post, 22 | page, 23 | series, 24 | }: { 25 | children: React.ReactNode; 26 | publication: PublicationFragment; 27 | post?: PostFullFragment | null; 28 | page?: StaticPageFragment | null; 29 | series?: NonNullable['series']; 30 | }) => { 31 | return ( 32 | 40 | {children} 41 | 42 | ); 43 | }; 44 | 45 | const useAppContext = () => { 46 | const context = useContext(AppContext); 47 | 48 | if (!context) { 49 | throw new Error('useAppContext must be used within a '); 50 | } 51 | 52 | return context; 53 | }; 54 | export { AppProvider, useAppContext }; 55 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/cover-image.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | 4 | type Props = { 5 | title: string; 6 | src: string; 7 | slug?: string; 8 | priority?: boolean; 9 | }; 10 | 11 | export const CoverImage = ({ title, src, slug, priority = false }: Props) => { 12 | const postURL = `/${slug}`; 13 | 14 | const image = ( 15 | 16 | 24 | 25 | ); 26 | return ( 27 | 28 | {slug ? ( 29 | 30 | {image} 31 | 32 | ) : ( 33 | image 34 | )} 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/date-formatter.tsx: -------------------------------------------------------------------------------- 1 | import { format, parseISO } from 'date-fns'; 2 | 3 | type Props = { 4 | dateString: string; 5 | }; 6 | 7 | export const DateFormatter = ({ dateString }: Props) => { 8 | if (!dateString) return <>>; 9 | const date = parseISO(dateString); 10 | 11 | return ( 12 | <> 13 | {format(date, 'LLL d, yyyy')} 14 | > 15 | ); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/index.js: -------------------------------------------------------------------------------- 1 | export * from './svgs'; 2 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/ArticleSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ArticleSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/BookOpenSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class BookOpenSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/ChevronDownSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ChevronDownSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/CloseSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class CloseSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/ExternalArrowSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ExternalArrowSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/GithubSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class GithubSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/HamburgerSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class HamburgerSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/HashnodeSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class HashnodeSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 12 | 17 | 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/LinkedinSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class LinkedinSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/PlusCircleSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class PlusCircleSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/RssSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class RssSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/XSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class XSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/icons/svgs/index.js: -------------------------------------------------------------------------------- 1 | import ArticleSVG from './ArticleSVG'; 2 | import ChevronDownSVG from './ChevronDownSVG'; 3 | import ExternalArrowSVG from './ExternalArrowSVG'; 4 | import GithubSVG from './GithubSVG'; 5 | import HashnodeSVG from './HashnodeSVG'; 6 | import LinkedinSVG from './LinkedinSVG'; 7 | import NewsletterPlusSVG from './NewsletterPlusSVG'; 8 | import PlusCircleSVG from './PlusCircleSVG'; 9 | import RssSVG from './RssSVG'; 10 | import XSVG from './XSVG'; 11 | 12 | export { 13 | ArticleSVG, 14 | ChevronDownSVG, 15 | ExternalArrowSVG, 16 | GithubSVG, 17 | HashnodeSVG, 18 | LinkedinSVG, 19 | NewsletterPlusSVG, 20 | PlusCircleSVG, 21 | RssSVG, 22 | XSVG, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from './analytics'; 2 | import { Integrations } from './integrations'; 3 | import { Meta } from './meta'; 4 | import { Scripts } from './scripts'; 5 | 6 | type Props = { 7 | children: React.ReactNode; 8 | }; 9 | 10 | export const Layout = ({ children }: Props) => { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | {children} 17 | 18 | 19 | 20 | > 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/markdown-styles.module.css: -------------------------------------------------------------------------------- 1 | .markdown { 2 | @apply text-lg leading-relaxed; 3 | } 4 | 5 | .markdown p, 6 | .markdown ul, 7 | .markdown ol, 8 | .markdown blockquote { 9 | @apply my-6; 10 | } 11 | 12 | .markdown h2 { 13 | @apply text-3xl mt-12 mb-4 leading-snug; 14 | } 15 | 16 | .markdown h3 { 17 | @apply text-2xl mt-8 mb-4 leading-snug; 18 | } 19 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/markdown-to-html.tsx: -------------------------------------------------------------------------------- 1 | import { useEmbeds } from '@starter-kit/utils/renderer/hooks/useEmbeds'; 2 | import { markdownToHtml } from '@starter-kit/utils/renderer/markdownToHtml'; 3 | import { memo } from 'react'; 4 | 5 | type Props = { 6 | contentMarkdown: string; 7 | }; 8 | 9 | const _MarkdownToHtml = ({ contentMarkdown }: Props) => { 10 | const content = markdownToHtml(contentMarkdown); 11 | useEmbeds({ enabled: true }); 12 | 13 | return ( 14 | 18 | ); 19 | }; 20 | 21 | export const MarkdownToHtml = memo(_MarkdownToHtml); 22 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/meta.tsx: -------------------------------------------------------------------------------- 1 | import parse from 'html-react-parser'; 2 | import Head from 'next/head'; 3 | import { useAppContext } from './contexts/appContext'; 4 | 5 | export const Meta = () => { 6 | const { publication } = useAppContext(); 7 | const { metaTags, favicon } = publication; 8 | const defaultFavicons = ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | 16 | > 17 | ); 18 | 19 | return ( 20 | 21 | {favicon ? : defaultFavicons} 22 | 23 | 24 | {metaTags && parse(metaTags)} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/more-posts.tsx: -------------------------------------------------------------------------------- 1 | import { PostFragment } from '../generated/graphql'; 2 | import { PostPreview } from './post-preview'; 3 | 4 | type Props = { 5 | posts: PostFragment[]; 6 | context: 'home' | 'series' | 'tag'; 7 | }; 8 | 9 | export const MorePosts = ({ posts, context }: Props) => { 10 | return ( 11 | 12 | {context === 'home' && ( 13 | 14 | More Posts 15 | 16 | )} 17 | 18 | {posts.map((post) => ( 19 | 31 | ))} 32 | 33 | 34 | ); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { Search } from './searchbar'; 2 | import { SocialLinks } from './social-links'; 3 | 4 | export const Navbar = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/post-read-time-in-minutes.tsx: -------------------------------------------------------------------------------- 1 | import BookOpenSVG from './icons/svgs/BookOpenSVG'; 2 | 3 | type Props = { readTimeInMinutes: number }; 4 | 5 | export const ReadTimeInMinutes = ({ readTimeInMinutes }: Props) => { 6 | return ( 7 | <> 8 | 9 | 10 | {readTimeInMinutes} min read 11 | 12 | > 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/post-title.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from 'react'; 2 | 3 | type Props = { 4 | children?: ReactNode; 5 | }; 6 | 7 | export const PostTitle = ({ children }: Props) => { 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/publication-logo.tsx: -------------------------------------------------------------------------------- 1 | import { resizeImage } from '@starter-kit/utils/image'; 2 | import Link from 'next/link'; 3 | import { useAppContext } from './contexts/appContext'; 4 | import { PublicationFragment } from '../generated/graphql'; 5 | 6 | const getPublicationLogo = (publication: PublicationFragment, isSidebar?: boolean) => { 7 | if (isSidebar) { 8 | return publication.preferences.logo; // Always display light mode logo in sidebar 9 | } 10 | return publication.preferences.darkMode?.logo || publication.preferences.logo; 11 | } 12 | 13 | export const PublicationLogo = ({ isSidebar }: { isSidebar?: boolean }) => { 14 | const { publication } = useAppContext(); 15 | const PUBLICATION_LOGO = getPublicationLogo(publication, isSidebar); 16 | 17 | return ( 18 | 19 | 24 | {PUBLICATION_LOGO ? ( 25 | <> 26 | 31 | Blog 32 | > 33 | ) : ( 34 | 39 | {publication.title} 40 | 41 | )} 42 | 43 | 44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/resizable-image.js: -------------------------------------------------------------------------------- 1 | import { ProgressiveImage } from './progressive-image'; 2 | 3 | function ResizableImage(props) { 4 | const { src, alt, resize, className, ...restOfTheProps } = props; 5 | 6 | return ( 7 | 8 | ); 9 | } 10 | 11 | export default ResizableImage; 12 | export { ResizableImage }; -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/scripts.tsx: -------------------------------------------------------------------------------- 1 | export const Scripts = () => { 2 | const googleAnalytics = ` 3 | window.dataLayer = window.dataLayer || []; 4 | function gtag(){window.dataLayer.push(arguments);} 5 | gtag('js', new Date());`; 6 | return ( 7 | <> 8 | 9 | 10 | > 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/section-separator.tsx: -------------------------------------------------------------------------------- 1 | export const SectionSeparator = () => { 2 | return ; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/components/subscribe.tsx: -------------------------------------------------------------------------------- 1 | import * as Popover from '@radix-ui/react-popover'; 2 | import { Button } from './button'; 3 | import { NewsletterPlusSVG } from './icons'; 4 | import { SubscribeForm } from './subscribe-form'; 5 | 6 | export const Subscribe = () => { 7 | return ( 8 | 9 | 10 | 11 | } 15 | className="!bg-white dark:!bg-neutral-950" 16 | /> 17 | 18 | 19 | 24 | 25 | Subscribe to our newsletter for updates and changelog. 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/fragments/PageInfo.graphql: -------------------------------------------------------------------------------- 1 | fragment PageInfo on PageInfo { 2 | endCursor 3 | hasNextPage 4 | } 5 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/fragments/Post.graphql: -------------------------------------------------------------------------------- 1 | fragment Post on Post { 2 | id 3 | title 4 | url 5 | author { 6 | name 7 | profilePicture 8 | } 9 | coverImage { 10 | url 11 | } 12 | publishedAt 13 | slug 14 | brief 15 | } 16 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/fragments/Publication.graphql: -------------------------------------------------------------------------------- 1 | fragment Publication on Publication { 2 | id 3 | title 4 | displayTitle 5 | url 6 | metaTags 7 | favicon 8 | isTeam 9 | followersCount 10 | descriptionSEO 11 | author { 12 | name 13 | username 14 | profilePicture 15 | followersCount 16 | } 17 | ogMetaData { 18 | image 19 | } 20 | preferences { 21 | logo 22 | darkMode { 23 | logo 24 | } 25 | navbarItems { 26 | id 27 | type 28 | label 29 | url 30 | } 31 | } 32 | links { 33 | twitter 34 | github 35 | linkedin 36 | hashnode 37 | } 38 | integrations { 39 | umamiWebsiteUUID 40 | gaTrackingID 41 | fbPixelID 42 | hotjarSiteID 43 | matomoURL 44 | matomoSiteID 45 | fathomSiteID 46 | gTagManagerID 47 | fathomCustomDomain 48 | fathomCustomDomainEnabled 49 | plausibleAnalyticsEnabled 50 | koalaPublicKey 51 | msClarityID 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/mutations/SubscribeToNewsletter.graphql: -------------------------------------------------------------------------------- 1 | mutation SubscribeToNewsletter($input: SubscribeToNewsletterInput!) { 2 | subscribeToNewsletter(input: $input) { 3 | status 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/DraftById.graphql: -------------------------------------------------------------------------------- 1 | query DraftById($id: ObjectId!) { 2 | draft(id: $id) { 3 | id 4 | title 5 | content { 6 | markdown 7 | } 8 | author { 9 | id 10 | name 11 | username 12 | profilePicture 13 | } 14 | coverImage { 15 | url 16 | } 17 | readTimeInMinutes 18 | dateUpdated 19 | tags { 20 | id 21 | name 22 | slug 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/PageByPublication.graphql: -------------------------------------------------------------------------------- 1 | query PageByPublication($slug: String!, $host: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: 0) { 5 | totalDocuments 6 | } 7 | staticPage(slug: $slug) { 8 | ...StaticPage 9 | } 10 | } 11 | } 12 | 13 | fragment StaticPage on StaticPage { 14 | id 15 | title 16 | slug 17 | content { 18 | markdown 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/PostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query PostsByPublication($host: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: $first, after: $after) { 5 | totalDocuments 6 | edges { 7 | node { 8 | ...Post 9 | } 10 | } 11 | pageInfo { 12 | ...PageInfo 13 | } 14 | } 15 | } 16 | } 17 | 18 | query MorePostsByPublication($host: String!, $first: Int!, $after: String) { 19 | publication(host: $host) { 20 | posts(first: $first, after: $after) { 21 | edges { 22 | node { 23 | ...Post 24 | comments(first: 0) { 25 | totalDocuments 26 | } 27 | } 28 | } 29 | pageInfo { 30 | ...PageInfo 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/PublicationByHost.graphql: -------------------------------------------------------------------------------- 1 | query PublicationByHost($host: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first:0) { 5 | totalDocuments 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/RSSFeed.graphql: -------------------------------------------------------------------------------- 1 | query RSSFeed($host: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: $first, after: $after) { 5 | edges { 6 | node { 7 | id 8 | title 9 | url 10 | slug 11 | content { 12 | html 13 | } 14 | tags { 15 | id 16 | name 17 | slug 18 | } 19 | author { 20 | name 21 | username 22 | } 23 | } 24 | } 25 | pageInfo { 26 | ...PageInfo 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/SearchPostsOfPublication.graphql: -------------------------------------------------------------------------------- 1 | query SearchPostsOfPublication($first: Int!, $filter: SearchPostsOfPublicationFilter!) { 2 | searchPostsOfPublication(first: $first, filter: $filter) { 3 | edges { 4 | cursor 5 | node { 6 | id 7 | brief 8 | title 9 | cuid 10 | slug 11 | coverImage { 12 | url 13 | } 14 | author { 15 | id 16 | name 17 | } 18 | publication { 19 | title 20 | url 21 | } 22 | } 23 | } 24 | pageInfo { 25 | ...PageInfo 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/SeriesPostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query SeriesPostsByPublication($host: String!, $seriesSlug: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: 0) { 5 | totalDocuments 6 | } 7 | series(slug: $seriesSlug) { 8 | ...Series 9 | } 10 | } 11 | } 12 | 13 | fragment Series on Series { 14 | id 15 | name 16 | slug 17 | description { 18 | html 19 | } 20 | coverImage 21 | posts(first: $first, after: $after) { 22 | edges { 23 | node { 24 | ...Post 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/Sitemap.graphql: -------------------------------------------------------------------------------- 1 | query Sitemap($host: String!, $postsCount: Int!, $postsAfter: String, $staticPagesCount: Int!) { 2 | publication(host: $host) { 3 | id 4 | url 5 | staticPages(first: $staticPagesCount) { 6 | edges { 7 | node { 8 | slug 9 | } 10 | } 11 | } 12 | posts(first: $postsCount, after: $postsAfter) { 13 | edges { 14 | node { 15 | ...RequiredSitemapPostFields 16 | } 17 | } 18 | pageInfo { 19 | ...PageInfo 20 | } 21 | } 22 | } 23 | } 24 | 25 | query MoreSitemapPosts($host: String!, $postsCount: Int!, $postsAfter: String) { 26 | publication(host: $host) { 27 | id 28 | posts(first: $postsCount, after: $postsAfter) { 29 | edges { 30 | node { 31 | ...RequiredSitemapPostFields 32 | } 33 | } 34 | pageInfo { 35 | ...PageInfo 36 | } 37 | } 38 | } 39 | } 40 | 41 | fragment RequiredSitemapPostFields on Post { 42 | id 43 | url 44 | slug 45 | publishedAt 46 | updatedAt 47 | tags { 48 | id 49 | name 50 | slug 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/SlugPostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query SlugPostsByPublication($host: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: $first, after: $after) { 5 | edges { 6 | node { 7 | slug 8 | } 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/lib/api/queries/TagPostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query TagPostsByPublication( 2 | $host: String! 3 | $tagSlug: String! 4 | $first: Int! 5 | $after: String 6 | ) { 7 | publication(host: $host) { 8 | ...Publication 9 | posts(first: $first, filter: { tagSlugs: [$tagSlug] }, after: $after) { 10 | totalDocuments 11 | edges { 12 | node { 13 | ...Post 14 | } 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | import { useEffect } from 'react'; 3 | import '../styles/index.css'; 4 | 5 | export default function MyApp({ Component, pageProps }: AppProps) { 6 | useEffect(() => { 7 | (window as any).adjustIframeSize = (id: string, newHeight: string) => { 8 | const i = document.getElementById(id); 9 | if (!i) return; 10 | // eslint-disable-next-line radix 11 | i.style.height = `${parseInt(newHeight)}px`; 12 | }; 13 | }, []); 14 | return ; 15 | } 16 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import request from 'graphql-request'; 2 | import { GetServerSideProps } from 'next'; 3 | import { 4 | PublicationByHostDocument, 5 | PublicationByHostQuery, 6 | PublicationByHostQueryVariables, 7 | } from '../generated/graphql'; 8 | 9 | const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT; 10 | const Dashboard = () => null; 11 | 12 | export const getServerSideProps: GetServerSideProps = async () => { 13 | const data = await request( 14 | GQL_ENDPOINT, 15 | PublicationByHostDocument, 16 | { 17 | host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST, 18 | }, 19 | ); 20 | 21 | const publication = data.publication; 22 | if (!publication) { 23 | return { 24 | notFound: true, 25 | }; 26 | } 27 | 28 | return { 29 | redirect: { 30 | destination: `https://hashnode.com/${publication.id}/dashboard`, 31 | permanent: false, 32 | }, 33 | }; 34 | }; 35 | 36 | export default Dashboard; 37 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/pages/robots.txt.tsx: -------------------------------------------------------------------------------- 1 | import { type GetServerSideProps } from 'next'; 2 | 3 | const RobotsTxt = () => null; 4 | 5 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 6 | const { res } = ctx; 7 | const host = process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST; 8 | if (!host) { 9 | throw new Error('Could not determine host'); 10 | } 11 | 12 | const sitemapUrl = `https://${host}/sitemap.xml`; 13 | const robotsTxt = ` 14 | User-agent: * 15 | Allow: / 16 | 17 | # Google adsbot ignores robots.txt unless specifically named! 18 | User-agent: AdsBot-Google 19 | Allow: / 20 | 21 | User-agent: GPTBot 22 | Disallow: / 23 | 24 | Sitemap: ${sitemapUrl} 25 | `.trim(); 26 | 27 | res.setHeader('Cache-Control', 's-maxage=86400, stale-while-revalidate'); 28 | res.setHeader('content-type', 'text/plain'); 29 | res.write(robotsTxt); 30 | res.end(); 31 | 32 | return { props: {} }; 33 | }; 34 | 35 | export default RobotsTxt; 36 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/pages/rss.xml.tsx: -------------------------------------------------------------------------------- 1 | import { constructRSSFeedFromPosts } from '@starter-kit/utils/feed'; 2 | import request from 'graphql-request'; 3 | import { GetServerSideProps } from 'next'; 4 | import { RssFeedDocument, RssFeedQuery, RssFeedQueryVariables } from '../generated/graphql'; 5 | 6 | const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT; 7 | const RSS = () => null; 8 | 9 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 10 | const { res, query } = ctx; 11 | const after = query.after ? (query.after as string) : null; 12 | 13 | const data = await request(GQL_ENDPOINT, RssFeedDocument, { 14 | first: 20, 15 | host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST, 16 | after, 17 | }); 18 | 19 | const publication = data.publication; 20 | if (!publication) { 21 | return { 22 | notFound: true, 23 | }; 24 | } 25 | const allPosts = publication.posts.edges.map((edge) => edge.node); 26 | 27 | const xml = constructRSSFeedFromPosts( 28 | publication, 29 | allPosts, 30 | after, 31 | publication.posts.pageInfo.hasNextPage && publication.posts.pageInfo.endCursor 32 | ? publication.posts.pageInfo.endCursor 33 | : null, 34 | ); 35 | 36 | res.setHeader('Cache-Control', 's-maxage=1, stale-while-revalidate'); 37 | res.setHeader('content-type', 'text/xml'); 38 | res.write(xml); 39 | res.end(); 40 | 41 | return { props: {} }; 42 | }; 43 | 44 | export default RSS; 45 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/process-env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | [key: string]: string | undefined; 4 | NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT: string; 5 | NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST: string; 6 | // add more environment variables and their types here 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/assets/blog/authors/jj.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/assets/blog/authors/jj.jpeg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/assets/blog/authors/joe.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/assets/blog/authors/joe.jpeg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/assets/blog/authors/tim.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/assets/blog/authors/tim.jpeg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/assets/blog/dynamic-routing/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/assets/blog/dynamic-routing/cover.jpg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/assets/blog/hello-world/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/assets/blog/hello-world/cover.jpg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/assets/blog/preview/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/assets/blog/preview/cover.jpg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #000000 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/favicon/favicon.ico -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/public/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/enterprise/public/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@starter-kit/tsconfig/nextjs.json", 4 | "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 5 | "exclude": ["node_modules"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/utils/const/index.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_AVATAR = 2 | 'https://cdn.hashnode.com/res/hashnode/image/upload/v1659089761812/fsOct5gl6.png'; 3 | 4 | export const DEFAULT_COVER = 5 | 'https://cdn.hashnode.com/res/hashnode/image/upload/v1683525272978/MB5H_kgOC.png?auto=format'; 6 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/enterprise/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "nextjs" 3 | } -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT=https://gql.hashnode.com 2 | NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST=engineering.hashnode.com 3 | NEXT_PUBLIC_MODE=development -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@starter-kit/eslint-config-custom'], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | schema: './generated/schema.graphql' 2 | documents: './{pages,components,lib}/**/*.{graphql,js,ts,jsx,tsx}' 3 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/@types/remark-html.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'remark-html'; 2 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/README.md: -------------------------------------------------------------------------------- 1 | # A statically generated blog example using Next.js, Markdown, and TypeScript with Hashnode 💫 2 | 3 | This is the existing [blog-starter](https://github.com/vercel/next.js/tree/canary/examples/blog-starter) plus TypeScript, wired with [Hashnode](https://hashnode.com). 4 | 5 | We've used [Hashnode APIs](https://apidocs.hashnode.com) and integrated them with this blog starter kit. 6 | 7 | ## Want to have your own? 8 | 9 | Fork it and change the environment variable `NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST` to your host (engineering.hashnode.dev is the host in the example) and deploy it to Vercel. That's it! You now have your own frontend. You can still use Hashnode for writing your Articles. 10 | 11 | Demo of the `hashnode` theme: [https://saikrishna.dev/blog](https://saikrishna.dev/blog). 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-Bold.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-ExtraBold.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-Medium.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-Regular.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/assets/PlusJakartaSans-SemiBold.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/codegen.yml: -------------------------------------------------------------------------------- 1 | schema: https://gql.hashnode.com 2 | documents: './**/*.graphql' 3 | generates: 4 | ./generated/schema.graphql: 5 | plugins: 6 | - schema-ast 7 | config: 8 | includeDirectives: true 9 | ./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 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/about-author.tsx: -------------------------------------------------------------------------------- 1 | import PostAuthorInfo from './post-author-info'; 2 | import { useAppContext } from './contexts/appContext'; 3 | import { PostFullFragment } from '../generated/graphql'; 4 | 5 | function AboutAuthor() { 6 | const { post: _post } = useAppContext(); 7 | const post = _post as unknown as PostFullFragment; 8 | const { publication, author } = post; 9 | let coAuthors = post.coAuthors || []; 10 | 11 | const allAuthors = publication?.isTeam ? [author, ...coAuthors] : [author]; 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | Written by 19 | 20 | 21 | {allAuthors.map((_author) => { 22 | return ( 23 | 27 | ); 28 | })} 29 | 30 | 31 | 32 | 33 | ); 34 | } 35 | 36 | export default AboutAuthor; 37 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/container.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | children?: React.ReactNode; 3 | className?: string; 4 | }; 5 | 6 | export const Container = ({ children, className }: Props) => { 7 | return {children}; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/contexts/appContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react'; 2 | import { 3 | PostFullFragment, 4 | PublicationFragment, 5 | SeriesPageInitialQuery, 6 | StaticPageFragment, 7 | } from '../../generated/graphql'; 8 | 9 | type AppContext = { 10 | publication: PublicationFragment; 11 | post: PostFullFragment | null; 12 | page: StaticPageFragment | null; 13 | series: NonNullable['series']; 14 | }; 15 | 16 | const AppContext = createContext(null); 17 | 18 | const AppProvider = ({ 19 | children, 20 | publication, 21 | post, 22 | page, 23 | series, 24 | }: { 25 | children: React.ReactNode; 26 | publication: PublicationFragment; 27 | post?: PostFullFragment | null; 28 | page?: StaticPageFragment | null; 29 | series?: NonNullable['series']; 30 | }) => { 31 | return ( 32 | 40 | {children} 41 | 42 | ); 43 | }; 44 | 45 | const useAppContext = () => { 46 | const context = useContext(AppContext); 47 | 48 | if (!context) { 49 | throw new Error('useAppContext must be used within a '); 50 | } 51 | 52 | return context; 53 | }; 54 | export { AppProvider, useAppContext }; 55 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/fonts/index.tsx: -------------------------------------------------------------------------------- 1 | import { Inter, Plus_Jakarta_Sans } from 'next/font/google'; 2 | 3 | const inter = Inter({ 4 | subsets: ['latin'], 5 | variable: '--font-inter', 6 | display: 'swap', 7 | }); 8 | 9 | const plusJakartaSans = Plus_Jakarta_Sans({ 10 | subsets: ['latin'], 11 | variable: '--font-plus-jakarta-sans', 12 | display: 'swap', 13 | }); 14 | 15 | const variableConstant = 'variable'; 16 | const fontInterVar = inter.variable.replace(variableConstant, 'Inter'); 17 | const fontPlusJakartaSansVar = plusJakartaSans.variable.replace(variableConstant, 'Plus_Jakarta_Sans'); 18 | 19 | export const GlobalFontVariables = () => ( 20 | 26 | ); 27 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/header-blog-search.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | import { useState, useRef } from 'react'; 3 | import dynamic from 'next/dynamic'; 4 | 5 | import CommonHeaderIconBtn from './common-header-icon-btn'; 6 | import { PublicationFragment } from '../generated/graphql'; 7 | import SearchSVG from './icons/svgs/SearchSvg'; 8 | 9 | const PublicationSearch = dynamic(() => import('./publication-search'), { ssr: false }); 10 | 11 | interface Props { 12 | publication: Pick 13 | } 14 | 15 | const HeaderBlogSearch = (props: Props) => { 16 | const { publication } = props; 17 | 18 | const [isSearchUIVisible, toggleSearchUIState] = useState(false); 19 | const triggerRef = useRef(null); 20 | 21 | const toggleSearchUI = () => { 22 | toggleSearchUIState(!isSearchUIVisible); 23 | }; 24 | 25 | return ( 26 | <> 27 | {isSearchUIVisible ? ( 28 | 29 | ) : null} 30 | 35 | 36 | 37 | > 38 | ); 39 | }; 40 | 41 | export default HeaderBlogSearch; 42 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/header-left-sidebar.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-nested-ternary */ 2 | import dynamic from 'next/dynamic'; 3 | import { useState, useRef } from 'react'; 4 | 5 | import { PublicationFragment } from '../generated/graphql'; 6 | import { BarsSVG } from './icons/svgs'; 7 | import CommonHeaderIconBtn from './common-header-icon-btn'; 8 | 9 | const PublicationSidebar = dynamic(() => import('./publication-sidebar'), { 10 | ssr: false, 11 | }); 12 | 13 | interface Props { 14 | publication: Pick; 15 | } 16 | 17 | const LeftSidebarButton = (props: Props) => { 18 | const { publication } = props; 19 | 20 | const triggerRef = useRef(null); 21 | const [isSidebarVisible, toggleSidebarVisibility] = useState(false); 22 | 23 | const toggleSidebar = () => { 24 | toggleSidebarVisibility(!isSidebarVisible); 25 | }; 26 | 27 | return ( 28 | <> 29 | {isSidebarVisible ? ( 30 | 31 | ) : null} 32 | 37 | 38 | 39 | > 40 | ); 41 | }; 42 | 43 | export default LeftSidebarButton; 44 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/header-tooltip.tsx: -------------------------------------------------------------------------------- 1 | import * as RadixTooltip from '@radix-ui/react-tooltip'; 2 | import { twJoin } from 'tailwind-merge'; 3 | 4 | import { useAppContext } from './contexts/appContext'; 5 | 6 | interface IHeaderTooltip { 7 | tooltipClassName: string; 8 | tooltipText: string; 9 | children: React.ReactNode; 10 | } 11 | 12 | const HeaderTooltip = (props: IHeaderTooltip) => { 13 | const { tooltipClassName, tooltipText, children } = props; 14 | 15 | return ( 16 | 17 | 18 | {children} 19 | 20 | 31 | {tooltipText} 32 | 33 | 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default HeaderTooltip; 40 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/index.js: -------------------------------------------------------------------------------- 1 | export * from './svgs'; 2 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/AlertSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class AlertSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ArticleSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ArticleSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BarsSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class BarsSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/BookOpenSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class BookOpenSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChartMixedSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ChartMixedSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CheckSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class CheckSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ChevronDownSVG extends React.Component { 4 | render() { 5 | return ( 6 | // 7 | 8 | 9 | 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVGV2.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ChevronDownSVGV2 extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronDownSVG_16x16.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ChevronDownSVG_16x16 extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronLeftSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ChevronLeftSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronRightSVG_16x16.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ChevronRightSVG16x16 extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ChevronUpSVG_16x16.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ChevronUpSVG16X16 extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ClipboardSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ClipboardSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CloseSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class CloseSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/CommentSVGV2.js: -------------------------------------------------------------------------------- 1 | export default function CommentSVGV2(props) { 2 | return ( 3 | 4 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/EarthSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class EarthSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ExternalArrowSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ExternalArrowSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ExternalLinkSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ExternalLinkSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FacebookSVGRound.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class FacebookSVGRound extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/FileLineChartSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class FileLineChartSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HackernewsSVGV2.js: -------------------------------------------------------------------------------- 1 | const HackernewsSVGV2 = (props) => { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | }; 8 | 9 | export default HackernewsSVGV2; 10 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HamburgerSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class HamburgerSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HashnodeLogoIconV2.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class HashnodeLogoIconV2 extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 12 | 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HashnodeSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class HashnodeSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 12 | 17 | 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/HeadphonesSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class HeadphonesSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/InstagramSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class InstagramSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkAltSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class LinkAltSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkSVGV2.js: -------------------------------------------------------------------------------- 1 | const LinkSVGV2 = (props) => { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | }; 8 | 9 | export default LinkSVGV2; 10 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkedInSVGV2.js: -------------------------------------------------------------------------------- 1 | const LinkedInSVGV2 = (props) => { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | }; 8 | 9 | export default LinkedInSVGV2; 10 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/LinkedinSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class LinkedinSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ListSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ListSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/MastodonSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class MastodonSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PaperPlaneSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class PaperPlaneSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PencilSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class PencilSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PinSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class PinSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/PlusCircleSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class PlusCircleSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RedditSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class RedditSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RedditSVGV2.js: -------------------------------------------------------------------------------- 1 | const RedditSVGV2 = (props) => { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | }; 8 | 9 | export default RedditSVGV2; 10 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/RefreshSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class RefreshSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/SearchSvg.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class SearchSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/ShareSVGV2.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const ShareSVGV2 = React.forwardRef>((props, ref) => ( 4 | 5 | 11 | 12 | )); 13 | 14 | ShareSVGV2.displayName = 'ShareSVGV2'; 15 | 16 | export default ShareSVGV2; 17 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/TwitterXSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class TwitterXSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 12 | 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/XSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class XSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/icons/svgs/YoutubeSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class YoutubeSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from './analytics'; 2 | import { Integrations } from './integrations'; 3 | import { Meta } from './meta'; 4 | import { Scripts } from './scripts'; 5 | 6 | type Props = { 7 | children: React.ReactNode; 8 | }; 9 | 10 | export const Layout = ({ children }: Props) => { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | {children} 17 | 18 | 19 | 20 | > 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/markdown-styles.module.css: -------------------------------------------------------------------------------- 1 | .markdown { 2 | @apply text-lg leading-relaxed; 3 | } 4 | 5 | .markdown p, 6 | .markdown ul, 7 | .markdown ol, 8 | .markdown blockquote { 9 | @apply my-6; 10 | } 11 | 12 | .markdown h2 { 13 | @apply text-3xl mt-12 mb-4 leading-snug; 14 | } 15 | 16 | .markdown h3 { 17 | @apply text-2xl mt-8 mb-4 leading-snug; 18 | } 19 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/meta.tsx: -------------------------------------------------------------------------------- 1 | import parse from 'html-react-parser'; 2 | import Head from 'next/head'; 3 | 4 | import { useAppContext } from './contexts/appContext'; 5 | 6 | export const Meta = () => { 7 | const { publication } = useAppContext(); 8 | const { metaTags, favicon } = publication; 9 | const defaultFavicons = ( 10 | <> 11 | 12 | 13 | 14 | 15 | 16 | 17 | > 18 | ); 19 | 20 | return ( 21 | 22 | {favicon ? : defaultFavicons} 23 | 24 | 25 | {metaTags && parse(metaTags)} 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/post-comments-sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { PostFullFragment } from '../generated/graphql'; 2 | import CommentsSheet from './comments-sheet'; 3 | import ResponseList from './response-list'; 4 | 5 | const PostCommentsSidebar = ({ 6 | hideSidebar, 7 | isPublicationPost, 8 | selectedFilter, 9 | post, 10 | }: { 11 | hideSidebar: () => void; 12 | isPublicationPost: boolean; 13 | selectedFilter: string; 14 | post: PostFullFragment; 15 | }) => ( 16 | 17 | 18 | {!post.preferences.disableComments ? ( 19 | 23 | ) : ( 24 | 25 | The comments have been disabled by the author for this article 26 | 27 | )} 28 | 29 | ); 30 | 31 | export default PostCommentsSidebar; 32 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/post-floating-bar-tooltip-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import * as Tooltip from '@radix-ui/react-tooltip'; 2 | import React from 'react'; 3 | import { twMerge } from 'tailwind-merge'; 4 | 5 | export default function PostFloatingBarTooltipWrapper({ 6 | children, 7 | label, 8 | asChild = true, 9 | labelSide = 'top', 10 | contentClassName = '', 11 | delayDuration, 12 | }: { 13 | children: React.ReactChild; 14 | label: string; 15 | asChild?: boolean; 16 | labelSide?: 'top' | 'right' | 'bottom' | 'left'; 17 | contentClassName?: string; 18 | delayDuration?: number; 19 | }) { 20 | return ( 21 | 22 | 23 | {children} 24 | 25 | 26 | 31 | {label} 32 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/publication-meta.tsx: -------------------------------------------------------------------------------- 1 | import { resizeImage } from '../utils/image'; 2 | import { Publication } from '../generated/graphql'; 3 | import { twJoin } from 'tailwind-merge'; 4 | 5 | // TODO: this component name is confusing. 6 | const PublicationMeta = ( 7 | props: Pick & { 8 | author: Pick; 9 | aboutHTML?: string | null; 10 | }, 11 | ) => { 12 | const { isTeam, aboutHTML, author } = props; 13 | const authorImageURL = resizeImage( 14 | author.profilePicture || 'https://cdn.hashnode.com/res/hashnode/image/upload/v1659089761812/fsOct5gl6.png', 15 | { w: 400, h: 400, c: 'face' }, 16 | ); 17 | 18 | return ( 19 | 20 | 21 | 22 | {aboutHTML ? ( 23 | 28 | ) : null} 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default PublicationMeta; 36 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/publication-social-link-item.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twJoin } from 'tailwind-merge'; 3 | 4 | type Props = { 5 | href: string; 6 | labelText: string; 7 | children: React.ReactElement | null; 8 | isSidebar?: boolean; 9 | }; 10 | 11 | function PublicationSocialLinkItem(props: Props) { 12 | const { href, labelText, children, isSidebar } = props; 13 | 14 | return ( 15 | 30 | {children} 31 | 32 | ); 33 | } 34 | 35 | export default PublicationSocialLinkItem; 36 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/resizable-image.js: -------------------------------------------------------------------------------- 1 | import { ProgressiveImage } from './progressive-image'; 2 | 3 | function ResizableImage(props) { 4 | const { src, alt, resize, className, ...restOfTheProps } = props; 5 | 6 | return ( 7 | 8 | ); 9 | } 10 | 11 | export default ResizableImage; 12 | export { ResizableImage }; 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/scripts.tsx: -------------------------------------------------------------------------------- 1 | export const Scripts = () => { 2 | const googleAnalytics = ` 3 | window.dataLayer = window.dataLayer || []; 4 | function gtag(){window.dataLayer.push(arguments);} 5 | gtag('js', new Date());`; 6 | return ( 7 | <> 8 | 9 | 10 | > 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/section-separator.tsx: -------------------------------------------------------------------------------- 1 | export const SectionSeparator = () => { 2 | return ; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/separator-root.js: -------------------------------------------------------------------------------- 1 | import { Root as SeparatorRoot } from '@radix-ui/react-separator'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export const Separator = ({ className, ...props }) => ( 5 | 10 | ); 11 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/static-page-content.tsx: -------------------------------------------------------------------------------- 1 | import { RequiredStaticPageFieldsFragment } from '../generated/graphql'; 2 | 3 | type Props = { 4 | pageContent: RequiredStaticPageFieldsFragment; 5 | }; 6 | 7 | function StaticPageContent(props: Props) { 8 | const { content, title } = props.pageContent; 9 | 10 | return ( 11 | 12 | 15 | 18 | {title} 19 | 20 | 25 | 26 | 27 | ); 28 | } 29 | 30 | export default StaticPageContent; 31 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/components/toast.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { twJoin } from 'tailwind-merge'; 3 | 4 | import { CheckSVG, CloseSVG, AlertSVG } from './icons/svgs'; 5 | 6 | export default class Toast extends React.Component { 7 | constructor(props, context) { 8 | super(props, context); 9 | this.state = {}; 10 | this.classNameMap = { 11 | success: `bg-green-600`, 12 | error: `bg-red-600`, 13 | warning: `bg-yellow-500`, 14 | }; 15 | } 16 | 17 | render() { 18 | const { type, title, message, closeToast } = this.props; 19 | return ( 20 | 27 | 28 | {type === 'success' && } 29 | {type === 'error' && } 30 | {type === 'warning' && } 31 | 32 | 33 | {title} 34 | {message && } 35 | 36 | 37 | ); 38 | } 39 | } 40 | 41 | export { Toast }; 42 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/fragments/Draft.graphql: -------------------------------------------------------------------------------- 1 | fragment Draft on Draft { 2 | id 3 | title 4 | canonicalUrl 5 | subtitle 6 | features { 7 | tableOfContents { 8 | isEnabled 9 | items { 10 | id 11 | level 12 | parentId 13 | slug 14 | title 15 | } 16 | } 17 | } 18 | content { 19 | markdown 20 | } 21 | coverImage { 22 | url 23 | } 24 | author { 25 | id 26 | name 27 | username 28 | profilePicture 29 | } 30 | updatedAt 31 | tags { 32 | id 33 | name 34 | slug 35 | } 36 | } -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/fragments/PageInfo.graphql: -------------------------------------------------------------------------------- 1 | fragment PageInfo on PageInfo { 2 | endCursor 3 | hasNextPage 4 | } 5 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/fragments/Post.graphql: -------------------------------------------------------------------------------- 1 | fragment Post on Post { 2 | id 3 | title 4 | url 5 | author { 6 | name 7 | profilePicture 8 | username 9 | } 10 | coverImage { 11 | url 12 | } 13 | publishedAt 14 | slug 15 | brief 16 | } 17 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/fragments/PostThumbnail.graphql: -------------------------------------------------------------------------------- 1 | fragment PostThumbnail on Post { 2 | __typename 3 | id 4 | title 5 | slug 6 | publishedAt 7 | cuid 8 | url 9 | subtitle 10 | brief 11 | readTimeInMinutes 12 | views 13 | author { 14 | __typename 15 | id 16 | username 17 | name 18 | profilePicture 19 | followersCount 20 | } 21 | coverImage { 22 | __typename 23 | url 24 | isPortrait 25 | isAttributionHidden 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/fragments/StaticPage.graphql: -------------------------------------------------------------------------------- 1 | query StaticPage($host: String!, $slug: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | staticPage(slug: $slug) { 5 | ...RequiredStaticPageFields 6 | } 7 | } 8 | } 9 | 10 | fragment RequiredStaticPageFields on StaticPage { 11 | id 12 | slug 13 | title 14 | content { 15 | html 16 | } 17 | seo { 18 | title 19 | description 20 | } 21 | ogMetaData { 22 | image 23 | } 24 | } -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/mutations/SubscribeToNewsletter.graphql: -------------------------------------------------------------------------------- 1 | mutation SubscribeToNewsletter($input: SubscribeToNewsletterInput!) { 2 | subscribeToNewsletter(input: $input) { 3 | status 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/DraftById.graphql: -------------------------------------------------------------------------------- 1 | query DraftById($id: ObjectId!) { 2 | draft(id: $id) { 3 | ...Draft 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/HomePage.graphql: -------------------------------------------------------------------------------- 1 | query HomePageInitial($host: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | about { 5 | markdown 6 | html 7 | } 8 | posts (first: 10) { 9 | totalDocuments 10 | } 11 | followersCount 12 | author { 13 | id 14 | followersCount 15 | } 16 | pinnedPost { 17 | ...PostThumbnail 18 | } 19 | } 20 | } 21 | 22 | query HomePagePosts($host: String!, $after: String, $first: Int!, $filter: PublicationPostConnectionFilter) { 23 | publication(host: $host) { 24 | id 25 | posts(after: $after, first: $first, filter: $filter) { 26 | totalDocuments 27 | edges { 28 | node { 29 | ...PostThumbnail 30 | } 31 | cursor 32 | } 33 | pageInfo { 34 | hasNextPage 35 | endCursor 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/Newsletter.graphql: -------------------------------------------------------------------------------- 1 | query Newsletter($host: String!, $slug: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | author { 5 | id 6 | followersCount 7 | } 8 | staticPage(slug: $slug) { 9 | id 10 | } 11 | recentPosts: posts(first: 3) { 12 | edges { 13 | node { 14 | ...PostThumbnail 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/PageByPublication.graphql: -------------------------------------------------------------------------------- 1 | query PageByPublication($slug: String!, $host: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: 0) { 5 | totalDocuments 6 | } 7 | staticPage(slug: $slug) { 8 | ...StaticPage 9 | } 10 | } 11 | } 12 | 13 | fragment StaticPage on StaticPage { 14 | id 15 | title 16 | slug 17 | content { 18 | markdown 19 | html 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/PostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query PostsByPublication($host: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: $first, after: $after) { 5 | totalDocuments 6 | edges { 7 | node { 8 | ...Post 9 | } 10 | } 11 | pageInfo { 12 | ...PageInfo 13 | } 14 | } 15 | } 16 | } 17 | 18 | query MorePostsByPublication($host: String!, $first: Int!, $after: String) { 19 | publication(host: $host) { 20 | posts(first: $first, after: $after) { 21 | edges { 22 | ...MorePostsEdge 23 | } 24 | pageInfo { 25 | ...PageInfo 26 | } 27 | } 28 | } 29 | } 30 | 31 | fragment MorePostsEdge on PostEdge { 32 | node { 33 | ...Post 34 | comments(first: 0) { 35 | totalDocuments 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/PublicationByHost.graphql: -------------------------------------------------------------------------------- 1 | query PublicationByHost($host: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first:0) { 5 | totalDocuments 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/RSSFeed.graphql: -------------------------------------------------------------------------------- 1 | query RSSFeed($host: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: $first, after: $after) { 5 | edges { 6 | node { 7 | id 8 | title 9 | url 10 | slug 11 | content { 12 | html 13 | } 14 | tags { 15 | id 16 | name 17 | slug 18 | } 19 | author { 20 | name 21 | username 22 | } 23 | } 24 | } 25 | pageInfo { 26 | ...PageInfo 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/SearchPostsOfPublication.graphql: -------------------------------------------------------------------------------- 1 | query SearchPostsOfPublication( 2 | $first: Int! 3 | $filter: SearchPostsOfPublicationFilter! 4 | $after: String 5 | ) { 6 | searchPostsOfPublication(first: $first, after: $after, filter: $filter) { 7 | edges { 8 | cursor 9 | node { 10 | id 11 | brief 12 | title 13 | cuid 14 | slug 15 | reactionCount 16 | publishedAt 17 | url 18 | coverImage { 19 | url 20 | } 21 | author { 22 | id 23 | name 24 | } 25 | publication { 26 | title 27 | url 28 | } 29 | } 30 | } 31 | pageInfo { 32 | ...PageInfo 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/SeriesPageInitial.graphql: -------------------------------------------------------------------------------- 1 | query SeriesPageInitial($host: String!, $slug: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | 5 | series(slug: $slug) { 6 | id 7 | name 8 | coverImage 9 | slug 10 | description { 11 | html 12 | # We don't need these field. But because of our legacy mapper we need to have it present. 13 | markdown 14 | text 15 | } 16 | cuid 17 | author { 18 | id 19 | name 20 | username 21 | __typename 22 | } 23 | 24 | posts(first: $first, after: $after) { 25 | edges { 26 | node { 27 | ...PostThumbnail 28 | } 29 | cursor 30 | __typename 31 | } 32 | pageInfo { 33 | endCursor 34 | hasNextPage 35 | } 36 | __typename 37 | } 38 | __typename 39 | } 40 | __typename 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/SeriesPostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query SeriesPostsByPublication($host: String!, $seriesSlug: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: 0) { 5 | totalDocuments 6 | } 7 | series(slug: $seriesSlug) { 8 | ...Series 9 | } 10 | } 11 | } 12 | 13 | fragment Series on Series { 14 | id 15 | name 16 | slug 17 | description { 18 | html 19 | } 20 | coverImage 21 | posts(first: $first, after: $after) { 22 | edges { 23 | node { 24 | ...Post 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/Sitemap.graphql: -------------------------------------------------------------------------------- 1 | query Sitemap($host: String!, $postsCount: Int!, $postsAfter: String, $staticPagesCount: Int!) { 2 | publication(host: $host) { 3 | id 4 | url 5 | staticPages(first: $staticPagesCount) { 6 | edges { 7 | node { 8 | slug 9 | } 10 | } 11 | } 12 | posts(first: $postsCount, after: $postsAfter) { 13 | edges { 14 | node { 15 | ...RequiredSitemapPostFields 16 | } 17 | } 18 | pageInfo { 19 | ...PageInfo 20 | } 21 | } 22 | } 23 | } 24 | 25 | query MoreSitemapPosts($host: String!, $postsCount: Int!, $postsAfter: String) { 26 | publication(host: $host) { 27 | id 28 | posts(first: $postsCount, after: $postsAfter) { 29 | edges { 30 | node { 31 | ...RequiredSitemapPostFields 32 | } 33 | } 34 | pageInfo { 35 | ...PageInfo 36 | } 37 | } 38 | } 39 | } 40 | 41 | fragment RequiredSitemapPostFields on Post { 42 | id 43 | url 44 | slug 45 | publishedAt 46 | updatedAt 47 | tags { 48 | id 49 | name 50 | slug 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/SlugPostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query SlugPostsByPublication($host: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: $first, after: $after) { 5 | edges { 6 | node { 7 | slug 8 | } 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/Tag.graphql: -------------------------------------------------------------------------------- 1 | query TagInitial($slug: String!, $host: String!, $first: Int!, $after: String) { 2 | tag(slug: $slug) { 3 | id 4 | name 5 | logo 6 | slug 7 | tagline 8 | } 9 | publication(host: $host) { 10 | ...Publication 11 | posts(first: $first, after: $after, filter: { tagSlugs: [$slug] }) { 12 | edges { 13 | node { 14 | ...PostThumbnail 15 | } 16 | cursor 17 | __typename 18 | } 19 | pageInfo { 20 | endCursor 21 | hasNextPage 22 | } 23 | __typename 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/lib/api/queries/TagPostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query TagPostsByPublication( 2 | $host: String! 3 | $tagSlug: String! 4 | $first: Int! 5 | $after: String 6 | ) { 7 | publication(host: $host) { 8 | ...Publication 9 | posts(first: $first, filter: { tagSlugs: [$tagSlug] }, after: $after) { 10 | totalDocuments 11 | edges { 12 | node { 13 | ...Post 14 | } 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { withUrqlClient } from 'next-urql'; 2 | import { AppProps } from 'next/app'; 3 | import { useEffect } from 'react'; 4 | import 'tailwindcss/tailwind.css'; 5 | 6 | import { GlobalFontVariables } from '../components/fonts'; 7 | import { getUrqlClientConfig } from '../lib/api/client'; 8 | import '../styles/index.css'; 9 | 10 | import { Fragment } from 'react'; 11 | 12 | function MyApp({ Component, pageProps }: AppProps) { 13 | useEffect(() => { 14 | (window as any).adjustIframeSize = (id: string, newHeight: string) => { 15 | const i = document.getElementById(id); 16 | if (!i) return; 17 | // eslint-disable-next-line radix 18 | i.style.height = `${parseInt(newHeight)}px`; 19 | }; 20 | }, []); 21 | return ( 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | 29 | // `withUrqlClient` HOC provides the `urqlClient` prop and takes care of restoring cache from urqlState 30 | // this will provide ssr cache to the provider and enable to use `useQuery` hook on the client side 31 | export default withUrqlClient(getUrqlClientConfig, { neverSuspend: true })(MyApp); 32 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import request from 'graphql-request'; 2 | import { GetServerSideProps } from 'next'; 3 | import { 4 | PublicationByHostDocument, 5 | PublicationByHostQuery, 6 | PublicationByHostQueryVariables, 7 | } from '../generated/graphql'; 8 | 9 | const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT; 10 | const Dashboard = () => null; 11 | 12 | export const getServerSideProps: GetServerSideProps = async () => { 13 | const data = await request( 14 | GQL_ENDPOINT, 15 | PublicationByHostDocument, 16 | { 17 | host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST, 18 | }, 19 | ); 20 | 21 | const publication = data.publication; 22 | if (!publication) { 23 | return { 24 | notFound: true, 25 | }; 26 | } 27 | 28 | return { 29 | redirect: { 30 | destination: `https://hashnode.com/${publication.id}/dashboard`, 31 | permanent: false, 32 | }, 33 | }; 34 | }; 35 | 36 | export default Dashboard; 37 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/pages/robots.txt.tsx: -------------------------------------------------------------------------------- 1 | import { type GetServerSideProps } from 'next'; 2 | 3 | const RobotsTxt = () => null; 4 | 5 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 6 | const { res } = ctx; 7 | const host = process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST; 8 | if (!host) { 9 | throw new Error('Could not determine host'); 10 | } 11 | 12 | const sitemapUrl = `https://${host}/sitemap.xml`; 13 | const robotsTxt = ` 14 | User-agent: * 15 | Allow: / 16 | 17 | # Google adsbot ignores robots.txt unless specifically named! 18 | User-agent: AdsBot-Google 19 | Allow: / 20 | 21 | User-agent: GPTBot 22 | Disallow: / 23 | 24 | Sitemap: ${sitemapUrl} 25 | `.trim(); 26 | 27 | res.setHeader('Cache-Control', 's-maxage=86400, stale-while-revalidate'); 28 | res.setHeader('content-type', 'text/plain'); 29 | res.write(robotsTxt); 30 | res.end(); 31 | 32 | return { props: {} }; 33 | }; 34 | 35 | export default RobotsTxt; 36 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/pages/rss.xml.tsx: -------------------------------------------------------------------------------- 1 | import { constructRSSFeedFromPosts } from '@starter-kit/utils/feed'; 2 | import request from 'graphql-request'; 3 | import { GetServerSideProps } from 'next'; 4 | import { RssFeedDocument, RssFeedQuery, RssFeedQueryVariables } from '../generated/graphql'; 5 | 6 | const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT; 7 | const RSS = () => null; 8 | 9 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 10 | const { res, query } = ctx; 11 | const after = query.after ? (query.after as string) : null; 12 | 13 | const data = await request(GQL_ENDPOINT, RssFeedDocument, { 14 | first: 20, 15 | host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST, 16 | after, 17 | }); 18 | 19 | const publication = data.publication; 20 | if (!publication) { 21 | return { 22 | notFound: true, 23 | }; 24 | } 25 | const allPosts = publication.posts.edges.map((edge) => edge.node); 26 | 27 | const xml = constructRSSFeedFromPosts( 28 | publication, 29 | allPosts, 30 | after, 31 | publication.posts.pageInfo.hasNextPage && publication.posts.pageInfo.endCursor 32 | ? publication.posts.pageInfo.endCursor 33 | : null, 34 | ); 35 | 36 | res.setHeader('Cache-Control', 's-maxage=1, stale-while-revalidate'); 37 | res.setHeader('content-type', 'text/xml'); 38 | res.write(xml); 39 | res.end(); 40 | 41 | return { props: {} }; 42 | }; 43 | 44 | export default RSS; 45 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/process-env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | [key: string]: string | undefined; 4 | NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT: string; 5 | NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST: string; 6 | // add more environment variables and their types here 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/jj.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/jj.jpeg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/joe.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/joe.jpeg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/tim.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/tim.jpeg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/assets/blog/dynamic-routing/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/assets/blog/dynamic-routing/cover.jpg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/assets/blog/hello-world/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/assets/blog/hello-world/cover.jpg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/assets/blog/preview/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/assets/blog/preview/cover.jpg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #000000 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon.ico -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/public/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/hashnode/public/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@starter-kit/tsconfig/nextjs.json", 4 | "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 5 | "exclude": ["node_modules"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/types/Badge.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from 'mongodb'; 2 | 3 | export interface Badge { 4 | _id: ObjectID | string; 5 | name: string; 6 | displayName: string; 7 | infoUrl: string; 8 | image: string; 9 | description: string; 10 | } 11 | export interface UserBadgeMap { 12 | _id: ObjectID; 13 | user: ObjectID; 14 | post: ObjectID; 15 | badge: ObjectID | Badge; 16 | metaData?: object; 17 | isActive: boolean; 18 | assignedOn: Date; 19 | isSuppressed?: boolean; 20 | hiddenFromPublication?: boolean; 21 | actionUrl: string; 22 | } 23 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/types/Page.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from 'mongodb'; 2 | import { Publication } from './index'; 3 | 4 | export interface Page { 5 | _id: string | ObjectID; 6 | publication?: string | Publication | ObjectID; 7 | title?: string; 8 | endpoint?: string; 9 | content?: string; 10 | contentMarkdown?: string; 11 | oldEndpoint?: string; 12 | isActive?: boolean; 13 | isHidden?: boolean; 14 | priority?: number; 15 | ogImage?: string; 16 | description?: string; 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/types/Response.ts: -------------------------------------------------------------------------------- 1 | import { ObjectId } from 'mongodb'; 2 | import { User, Reaction } from './index'; 3 | 4 | export type Reply = { 5 | _id: ObjectId; 6 | content: string; 7 | contentMarkdown: string; 8 | author: User; 9 | dateAdded: string; 10 | isActive: boolean; 11 | stamp: string; 12 | upvotes: string; 13 | reactions: string[] | Reaction[]; 14 | reactionToCountMap: Record; 15 | totalReactions: number; 16 | reactionsByCurrentUser: string[] | Reaction[]; 17 | totalReactionsByCurrentUser: number; 18 | }; 19 | 20 | export type Response = { 21 | _id: ObjectId; 22 | content: string; 23 | contentMarkdown: string; 24 | author: User; 25 | dateAdded: string; 26 | isActive: boolean; 27 | responseBrief: string; 28 | stamp: string; 29 | upvotes: number; 30 | reactions: string[] | Reaction[]; 31 | replies: any; 32 | reactionToCountMap: Record; 33 | totalReactions: number; 34 | reactionsByCurrentUser: string[] | Reaction[]; 35 | totalReactionsByCurrentUser: number; 36 | isCollapsed: boolean; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/types/Series.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from 'mongodb'; 2 | import { PostPreview, User, Publication } from './index'; 3 | 4 | export interface Series { 5 | _id: ObjectID; 6 | cuid?: string; 7 | slug?: string; 8 | name?: string; 9 | brief?: string; 10 | description?: string; 11 | descriptionMarkdown?: string; 12 | coverImage?: string; 13 | author?: User | string; 14 | posts?: string[] | PostPreview[]; 15 | numPosts?: number; 16 | dateAdded?: Date; 17 | isDelisted?: boolean; 18 | isActive?: boolean; 19 | sortOrder?: 'asc' | 'dsc'; 20 | partOfPublication?: boolean; 21 | publication?: Publication | string | ObjectID; 22 | movedToBlog?: boolean; 23 | } 24 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/types/User.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from 'mongodb'; 2 | import { SocialMedia, PostPreview } from './index'; 3 | 4 | export interface User { 5 | _id: ObjectID; 6 | isAmbassador: boolean; 7 | hasGoldRing: boolean; 8 | hasBetaAccess: boolean; 9 | publicationDomain: string; 10 | isDeactivated: boolean; 11 | numReactions: number; 12 | tagline: string; 13 | username: string; 14 | name: string; 15 | email: string; 16 | photo: string; 17 | numFollowers: number; 18 | numFollowing: number; 19 | location: string; 20 | coverImage: string; 21 | dateJoined: Date; 22 | socialMedia: SocialMedia; 23 | totalUpvotesReceived: number; 24 | storiesCreated: PostPreview[] | string[]; 25 | isEvangelist: boolean; 26 | numSeries: number; 27 | numPosts: number; 28 | tagManagerOf: string[]; 29 | role: string; 30 | beingFollowed: boolean; 31 | bio?: string; 32 | betaFeatures: { 33 | referralProgram: boolean; 34 | socialProofLikes: boolean; 35 | }; 36 | pro?: { 37 | hasProAccess?: boolean; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/types/external/mongodb.d.ts: -------------------------------------------------------------------------------- 1 | import { Db, MongoClient } from 'mongodb'; 2 | 3 | declare global { 4 | namespace NodeJS { 5 | type MongoConnection = { 6 | client: MongoClient; 7 | db: Db; 8 | }; 9 | 10 | interface Global { 11 | mongo: { 12 | conn: MongoConnection | null; 13 | promise: Promise | null; 14 | }; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/types/extras.ts: -------------------------------------------------------------------------------- 1 | import { ObjectID } from 'mongodb'; 2 | 3 | export interface SocialMedia { 4 | website?: string; 5 | github?: string; 6 | twitter?: string; 7 | facebook?: string; 8 | stackoverflow?: string; 9 | linkedin?: string; 10 | } 11 | 12 | export interface Reaction { 13 | _id: ObjectID; 14 | name: string; 15 | image: string; 16 | } 17 | 18 | export type Node = { 19 | isApproved: boolean; 20 | isActive: boolean; 21 | }; 22 | 23 | export interface Tag { 24 | _id: ObjectID; 25 | name: string; 26 | slug: string; 27 | isActive: boolean; 28 | isApproved: boolean; 29 | mergedWith?: Tag | ObjectID; 30 | numPosts?: number; 31 | } 32 | 33 | export interface RedirectResponse { 34 | redirect: { permanent: boolean; destination: string }; 35 | } 36 | 37 | export interface Head { 38 | customFavicon: string; 39 | customTheme: string; 40 | customMeta: string; 41 | } 42 | 43 | export type Nullable = { 44 | [P in keyof T]?: T[P] | null; 45 | }; 46 | 47 | export type ReportScrollFunction = { scrollToReportSection: () => void }; 48 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/types/index.ts: -------------------------------------------------------------------------------- 1 | export type Nullish = undefined | null; 2 | /** 3 | * Make some properties of T required. 4 | */ 5 | export type WithRequired = T & { [P in K]-?: NonNullable }; 6 | 7 | export * from './Post'; 8 | export * from './User'; 9 | export * from './Publication'; 10 | export * from './Series'; 11 | export * from './Page'; 12 | export * from './Badge'; 13 | export * from './extras'; 14 | export * from './Response'; 15 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/utils/const/images.ts: -------------------------------------------------------------------------------- 1 | export const blurImageDimensions = { w: 400, h: 210 }; 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const DEFAULT_AVATAR: string = 5 | 'https://cdn.hashnode.com/res/hashnode/image/upload/v1659089761812/fsOct5gl6.png?auto=compress'; -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/utils/const/index.ts: -------------------------------------------------------------------------------- 1 | import { getAppUrl } from "../urls"; 2 | 3 | export const DEFAULT_AVATAR = 4 | 'https://cdn.hashnode.com/res/hashnode/image/upload/v1659089761812/fsOct5gl6.png'; 5 | 6 | export const MAX_MAIN_NAV_LINKS = 7; 7 | 8 | export const DEFAULT_COVER = 9 | 'https://cdn.hashnode.com/res/hashnode/image/upload/v1683525272978/MB5H_kgOC.png?auto=format'; 10 | 11 | 12 | export const DEFAULT_LIGHT_POST_COVER = 13 | 'https://cdn.hashnode.com/res/hashnode/image/upload/v1683525272978/MB5H_kgOC.png?auto=format'; 14 | 15 | export const HASHNODE_NEXT_URL = getAppUrl(); 16 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/utils/const/styles.ts: -------------------------------------------------------------------------------- 1 | // twin classes 2 | export const inputText = 3 | 'w-full p-4 placeholder-slate-500 bg-transparent border rounded-lg outline-none focus:border-blue-600 disabled:bg-slate-50 dark:text-white dark:border-slate-800 dark:focus:bg-slate-900 dark:focus:border-blue-600'; 4 | 5 | export const dropdownMenu = 6 | 'w-full flex flex-row items-center px-4 py-3 text-slate-600 hover:bg-slate-100 focus:outline-none'; 7 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/utils/getReadTime.js: -------------------------------------------------------------------------------- 1 | module.exports = function getReadTime(string) { 2 | const wordsPerMinute = 225; // Average case. 3 | const numberOfWords = 4 | string 5 | .trim() 6 | .replace(/(\r\n|\n|\r)/gm, ' ') 7 | .split(' ') 8 | .filter((word) => word !== '').length || 0; 9 | if (numberOfWords > 0) { 10 | return Math.ceil(numberOfWords / wordsPerMinute); 11 | } 12 | return 0; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/utils/gsspHelpers.ts: -------------------------------------------------------------------------------- 1 | import { GetServerSidePropsContext } from 'next'; 2 | import { getSingleQueryParam } from './urls'; 3 | 4 | export function getHost({ req, query }: Pick) { 5 | const host = getSingleQueryParam(query, 'x-host') || req.headers.host; 6 | if (!host) { 7 | throw new Error('Could not determine host'); 8 | } 9 | return host; 10 | } -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/utils/handle-math-jax.js: -------------------------------------------------------------------------------- 1 | const handleMathJax = (rerun = false) => { 2 | if (typeof window === 'undefined') { 3 | return; 4 | } 5 | 6 | const mathjaxScript = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js'; 7 | if (!window.MathJax) { 8 | window.MathJax = { 9 | tex: { 10 | inlineMath: [['\\(', '\\)']], 11 | }, 12 | }; 13 | } 14 | 15 | let mathjaxScriptTag = document.querySelector(`script[src="${mathjaxScript}"]`); 16 | if (!mathjaxScriptTag) { 17 | let script = document.createElement('script'); 18 | script.type = 'text/javascript'; 19 | script.src = mathjaxScript; 20 | script.onload = function () { 21 | window.MathJax && 'mathml2chtml' in window.MathJax && window.MathJax.mathml2chtml(); 22 | }; 23 | document.head.appendChild(script); 24 | } else if (rerun) { 25 | window.MathJax && 'mathml2chtml' in window.MathJax && window.MathJax.mathml2chtml(); 26 | } 27 | }; 28 | 29 | export default handleMathJax; 30 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/utils/index.js: -------------------------------------------------------------------------------- 1 | import moment from 'dayjs'; 2 | 3 | export const formatDate = (dateString) => { 4 | const difference = moment().diff(moment(dateString), 'minute'); 5 | if (difference <= 1440) { 6 | if (difference <= 1) { 7 | return 'Just now'; 8 | } else if (difference > 1 && difference < 60) { 9 | return `${difference} mins`; 10 | } else if (difference >= 60 && difference <= 1440) { 11 | const diffInHour = moment().diff(moment(dateString), 'hour'); 12 | return `${diffInHour} hr${diffInHour === 1 ? '' : 's'} ago`; 13 | } 14 | } else if (difference > 1440 && difference <= 481801) { 15 | return moment(dateString).format('MMM D'); 16 | } else { 17 | return moment(dateString).format('MMM D, YYYY'); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/utils/toast.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot, Root } from 'react-dom/client'; 2 | import { Toast } from '../components/toast'; 3 | 4 | const TOAST_ELEMENT_ID = 'hn-toast'; 5 | let root: Root | null = null; 6 | let timeout: NodeJS.Timeout | null = null; 7 | 8 | const _closeToast = () => { 9 | root?.unmount(); 10 | root = null; 11 | }; 12 | 13 | // this should be a hook 14 | // TODO: The toast doens't close when Radix modal is used since Radix adds pointer-events: none to the body. 15 | const showToast = (type: 'success' | 'warning' | 'error', title: string, message?: string) => { 16 | timeout && clearTimeout(timeout); 17 | root ||= createRoot(document.getElementById(TOAST_ELEMENT_ID)!); 18 | root.render(); 19 | timeout = setTimeout(_closeToast, 5000); 20 | }; 21 | 22 | export default showToast; 23 | 24 | export { showToast }; 25 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/hashnode/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "nextjs" 3 | } -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT=https://gql.hashnode.com 2 | NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST=engineering.hashnode.com 3 | NEXT_PUBLIC_MODE=development -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ['@starter-kit/eslint-config-custom'], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | schema: './generated/schema.graphql' 2 | documents: './{pages,components,lib}/**/*.{graphql,js,ts,jsx,tsx}' 3 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/@types/remark-html.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'remark-html'; 2 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/README.md: -------------------------------------------------------------------------------- 1 | # A statically generated blog example using Next.js, Markdown, and TypeScript with Hashnode 💫 2 | 3 | This is the existing [blog-starter](https://github.com/vercel/next.js/tree/canary/examples/blog-starter) plus TypeScript, wired with [Hashnode](https://hashnode.com). 4 | 5 | We've used [Hashnode APIs](https://apidocs.hashnode.com) and integrated them with this blog starter kit. 6 | 7 | ## Want to have your own? 8 | 9 | Fork it and change the environment variable `NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST` to your host (engineering.hashnode.dev is the host in the example) and deploy it to Vercel. That's it! You now have your own frontend. You can still use Hashnode for writing your Articles. 10 | 11 | Demo of the `personal` theme: [https://sandeep.dev/blog](https://sandeep.dev/blog). 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-Bold.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-ExtraBold.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-Medium.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-Regular.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/assets/PlusJakartaSans-SemiBold.ttf -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/codegen.yml: -------------------------------------------------------------------------------- 1 | schema: https://gql.hashnode.com 2 | documents: './**/*.graphql' 3 | generates: 4 | ./generated/schema.graphql: 5 | plugins: 6 | - schema-ast 7 | config: 8 | includeDirectives: true 9 | ./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 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/avatar.tsx: -------------------------------------------------------------------------------- 1 | import { resizeImage } from '@starter-kit/utils/image'; 2 | import { DEFAULT_AVATAR } from '../utils/const'; 3 | 4 | type Props = { 5 | username: string; 6 | name: string; 7 | picture: string; 8 | size: number; 9 | }; 10 | 11 | export const Avatar = ({ username, name, picture, size }: Props) => { 12 | return ( 13 | 14 | 15 | 20 | 21 | 22 | 23 | {name} 24 | 25 | 26 | 27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/container.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | children?: React.ReactNode; 3 | className?: string; 4 | }; 5 | 6 | export const Container = ({ children, className }: Props) => { 7 | return {children}; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/contexts/appContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext } from 'react'; 2 | import { 3 | PageByPublicationQuery, 4 | PostFullFragment, 5 | PublicationFragment, 6 | } from '../../generated/graphql'; 7 | 8 | type AppContext = { 9 | publication: PublicationFragment; 10 | post: PostFullFragment | null; 11 | page: NonNullable['staticPage']; 12 | }; 13 | 14 | const AppContext = createContext(null); 15 | 16 | const AppProvider = ({ 17 | children, 18 | publication, 19 | post, 20 | page, 21 | }: { 22 | children: React.ReactNode; 23 | publication: PublicationFragment; 24 | post?: PostFullFragment | null; 25 | page?: NonNullable['staticPage']; 26 | }) => { 27 | return ( 28 | 35 | {children} 36 | 37 | ); 38 | }; 39 | 40 | const useAppContext = () => { 41 | const context = useContext(AppContext); 42 | 43 | if (!context) { 44 | throw new Error('useAppContext must be used within a '); 45 | } 46 | 47 | return context; 48 | }; 49 | export { AppProvider, useAppContext }; 50 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/cover-image.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image'; 2 | import Link from 'next/link'; 3 | 4 | type Props = { 5 | title: string; 6 | src: string; 7 | slug?: string; 8 | priority?: boolean; 9 | }; 10 | 11 | export const CoverImage = ({ title, src, slug, priority = false }: Props) => { 12 | const postURL = `/${slug}`; 13 | 14 | const image = ( 15 | 16 | 24 | 25 | ); 26 | return ( 27 | 28 | {slug ? ( 29 | 30 | {image} 31 | 32 | ) : ( 33 | image 34 | )} 35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/date-formatter.tsx: -------------------------------------------------------------------------------- 1 | import { format, parseISO } from 'date-fns'; 2 | 3 | type Props = { 4 | dateString: string; 5 | }; 6 | 7 | export const DateFormatter = ({ dateString }: Props) => { 8 | if (!dateString) return <>>; 9 | const date = parseISO(dateString); 10 | 11 | return {format(date, 'LLL d, yyyy')}; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { useAppContext } from './contexts/appContext'; 2 | 3 | export const Footer = () => { 4 | const { publication } = useAppContext(); 5 | 6 | return ( 7 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/index.js: -------------------------------------------------------------------------------- 1 | export * from './svgs'; 2 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/ArticleSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ArticleSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/ChevronDownSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ChevronDownSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/ExternalArrowSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class ExternalArrowSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/GithubSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class GithubSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/HamburgerSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class HamburgerSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/HashnodeSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class HashnodeSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 12 | 17 | 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/LinkedinSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class LinkedinSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/Moon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Moon extends React.Component { 4 | render() { 5 | return ( 6 | 16 | 17 | 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/PlusCircleSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class PlusCircleSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/RssSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class RssSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 14 | 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/Sun.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Sun extends React.Component { 4 | render() { 5 | return ( 6 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/XSVG.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class XSVG extends React.Component { 4 | render() { 5 | return ( 6 | 7 | 13 | 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/icons/svgs/index.js: -------------------------------------------------------------------------------- 1 | import ArticleSVG from './ArticleSVG'; 2 | import ChevronDownSVG from './ChevronDownSVG'; 3 | import ExternalArrowSVG from './ExternalArrowSVG'; 4 | import GithubSVG from './GithubSVG'; 5 | import HashnodeSVG from './HashnodeSVG'; 6 | import LinkedinSVG from './LinkedinSVG'; 7 | import Moon from './Moon'; 8 | import NewsletterPlusSVG from './NewsletterPlusSVG'; 9 | import PlusCircleSVG from './PlusCircleSVG'; 10 | import RssSVG from './RssSVG'; 11 | import Sun from './Sun'; 12 | import XSVG from './XSVG'; 13 | 14 | export { 15 | ArticleSVG, 16 | ChevronDownSVG, 17 | ExternalArrowSVG, 18 | GithubSVG, 19 | HashnodeSVG, 20 | LinkedinSVG, 21 | Moon, 22 | NewsletterPlusSVG, 23 | PlusCircleSVG, 24 | RssSVG, 25 | Sun, 26 | XSVG, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Analytics } from './analytics'; 2 | import { Integrations } from './integrations'; 3 | import { Meta } from './meta'; 4 | import { Scripts } from './scripts'; 5 | 6 | type Props = { 7 | children: React.ReactNode; 8 | }; 9 | 10 | export const Layout = ({ children }: Props) => { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | {children} 17 | 18 | 19 | 20 | > 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/markdown-styles.module.css: -------------------------------------------------------------------------------- 1 | .markdown { 2 | @apply text-lg leading-relaxed; 3 | } 4 | 5 | .markdown p, 6 | .markdown ul, 7 | .markdown ol, 8 | .markdown blockquote { 9 | @apply my-6; 10 | } 11 | 12 | .markdown h2 { 13 | @apply text-3xl mt-12 mb-4 leading-snug; 14 | } 15 | 16 | .markdown h3 { 17 | @apply text-2xl mt-8 mb-4 leading-snug; 18 | } 19 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/markdown-to-html.tsx: -------------------------------------------------------------------------------- 1 | import { useEmbeds } from '@starter-kit/utils/renderer/hooks/useEmbeds'; 2 | import { markdownToHtml } from '@starter-kit/utils/renderer/markdownToHtml'; 3 | import { memo } from 'react'; 4 | 5 | type Props = { 6 | contentMarkdown: string; 7 | }; 8 | 9 | const _MarkdownToHtml = ({ contentMarkdown }: Props) => { 10 | const content = markdownToHtml(contentMarkdown); 11 | useEmbeds({ enabled: true }); 12 | 13 | return ( 14 | 18 | ); 19 | }; 20 | 21 | export const MarkdownToHtml = memo(_MarkdownToHtml); 22 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/meta.tsx: -------------------------------------------------------------------------------- 1 | import parse from 'html-react-parser'; 2 | import Head from 'next/head'; 3 | import { useAppContext } from './contexts/appContext'; 4 | 5 | export const Meta = () => { 6 | const { publication } = useAppContext(); 7 | const { metaTags, favicon } = publication; 8 | const defaultFavicons = ( 9 | <> 10 | 11 | 12 | 13 | 14 | 15 | 16 | > 17 | ); 18 | 19 | return ( 20 | 21 | {favicon ? : defaultFavicons} 22 | 23 | 24 | {metaTags && parse(metaTags)} 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/minimal-post-preview.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { User } from '../generated/graphql'; 3 | import { DateFormatter } from './date-formatter'; 4 | 5 | type Author = Pick; 6 | 7 | type Props = { 8 | title: string; 9 | date: string; 10 | author: Author; 11 | slug: string; 12 | commentCount: number; 13 | }; 14 | 15 | export const MinimalPostPreview = ({ title, date, slug, commentCount }: Props) => { 16 | const postURL = `/${slug}`; 17 | 18 | return ( 19 | 20 | 21 | {title} 22 | 23 | 24 | 25 | 26 | 27 | {commentCount > 2 && ( 28 | <> 29 | · 30 | 31 | {commentCount} comments 32 | 33 | > 34 | )} 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/minimal-posts.tsx: -------------------------------------------------------------------------------- 1 | import { PostFragment } from '../generated/graphql'; 2 | import { MinimalPostPreview } from './minimal-post-preview'; 3 | 4 | type Props = { 5 | posts: PostFragment[]; 6 | context: 'home' | 'series' | 'tag'; 7 | }; 8 | 9 | export const MinimalPosts = ({ posts }: Props) => { 10 | return ( 11 | 12 | {posts.map((post) => ( 13 | 23 | ))} 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/scripts.tsx: -------------------------------------------------------------------------------- 1 | export const Scripts = () => { 2 | const googleAnalytics = ` 3 | window.dataLayer = window.dataLayer || []; 4 | function gtag(){window.dataLayer.push(arguments);} 5 | gtag('js', new Date());`; 6 | return ( 7 | <> 8 | 9 | 10 | > 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/section-separator.tsx: -------------------------------------------------------------------------------- 1 | export const SectionSeparator = () => { 2 | return ; 3 | }; 4 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/components/toggle-theme.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps } from 'react'; 2 | import { useTheme } from 'next-themes'; 3 | import { Moon, Sun } from './icons'; 4 | 5 | type Props = ComponentProps<'button'>; 6 | 7 | export function ToggleTheme(props: Props) { 8 | const { setTheme, theme } = useTheme(); 9 | 10 | return ( 11 | { 15 | setTheme(theme === 'dark' ? 'light' : 'dark'); 16 | }} 17 | {...props} 18 | > 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/fragments/PageInfo.graphql: -------------------------------------------------------------------------------- 1 | fragment PageInfo on PageInfo { 2 | endCursor 3 | hasNextPage 4 | } 5 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/fragments/Post.graphql: -------------------------------------------------------------------------------- 1 | fragment Post on Post { 2 | id 3 | title 4 | url 5 | author { 6 | name 7 | profilePicture 8 | } 9 | coverImage { 10 | url 11 | } 12 | publishedAt 13 | slug 14 | brief 15 | comments(first: 0) { 16 | totalDocuments 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/fragments/Publication.graphql: -------------------------------------------------------------------------------- 1 | fragment Publication on Publication { 2 | id 3 | title 4 | displayTitle 5 | url 6 | metaTags 7 | favicon 8 | isTeam 9 | followersCount 10 | descriptionSEO 11 | author { 12 | name 13 | username 14 | profilePicture 15 | followersCount 16 | } 17 | ogMetaData { 18 | image 19 | } 20 | preferences { 21 | logo 22 | darkMode { 23 | logo 24 | } 25 | navbarItems { 26 | id 27 | type 28 | label 29 | url 30 | } 31 | } 32 | links { 33 | twitter 34 | github 35 | linkedin 36 | hashnode 37 | } 38 | integrations { 39 | umamiWebsiteUUID 40 | gaTrackingID 41 | fbPixelID 42 | hotjarSiteID 43 | matomoURL 44 | matomoSiteID 45 | fathomSiteID 46 | gTagManagerID 47 | fathomCustomDomain 48 | fathomCustomDomainEnabled 49 | plausibleAnalyticsEnabled 50 | koalaPublicKey 51 | msClarityID 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/queries/DraftById.graphql: -------------------------------------------------------------------------------- 1 | query DraftById($id: ObjectId!) { 2 | draft(id: $id) { 3 | id 4 | title 5 | content { 6 | markdown 7 | } 8 | author { 9 | id 10 | name 11 | username 12 | } 13 | dateUpdated 14 | tags { 15 | id 16 | name 17 | slug 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/queries/PageByPublication.graphql: -------------------------------------------------------------------------------- 1 | query PageByPublication($slug: String!, $host: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | staticPage(slug: $slug) { 5 | ...StaticPage 6 | } 7 | } 8 | } 9 | 10 | fragment StaticPage on StaticPage { 11 | id 12 | title 13 | slug 14 | content { 15 | markdown 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/queries/PostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query PostsByPublication($host: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: $first, after: $after) { 5 | totalDocuments 6 | edges { 7 | node { 8 | ...Post 9 | comments(first: 0) { 10 | totalDocuments 11 | } 12 | } 13 | } 14 | pageInfo { 15 | ...PageInfo 16 | } 17 | } 18 | } 19 | } 20 | 21 | query MorePostsByPublication($host: String!, $first: Int!, $after: String) { 22 | publication(host: $host) { 23 | posts(first: $first, after: $after) { 24 | edges { 25 | node { 26 | ...Post 27 | comments(first: 0) { 28 | totalDocuments 29 | } 30 | } 31 | } 32 | pageInfo { 33 | ...PageInfo 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/queries/PublicationByHost.graphql: -------------------------------------------------------------------------------- 1 | query PublicationByHost($host: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first:0) { 5 | totalDocuments 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/queries/RSSFeed.graphql: -------------------------------------------------------------------------------- 1 | query RSSFeed($host: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: $first, after: $after) { 5 | edges { 6 | node { 7 | id 8 | title 9 | url 10 | slug 11 | content { 12 | html 13 | } 14 | tags { 15 | id 16 | name 17 | slug 18 | } 19 | author { 20 | name 21 | username 22 | } 23 | } 24 | } 25 | pageInfo { 26 | ...PageInfo 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/queries/SinglePostByPublication.graphql: -------------------------------------------------------------------------------- 1 | query SinglePostByPublication($slug: String!, $host: String!) { 2 | publication(host: $host) { 3 | ...Publication 4 | post(slug: $slug) { 5 | ...PostFull 6 | } 7 | } 8 | } 9 | 10 | fragment PostFull on Post { 11 | id 12 | slug 13 | url 14 | brief 15 | title 16 | subtitle 17 | hasLatexInPost 18 | publishedAt 19 | updatedAt 20 | readTimeInMinutes 21 | reactionCount 22 | responseCount 23 | publication { 24 | id 25 | } 26 | seo { 27 | title 28 | description 29 | } 30 | coverImage { 31 | url 32 | } 33 | author { 34 | name 35 | username 36 | profilePicture 37 | } 38 | title 39 | content { 40 | markdown 41 | html 42 | } 43 | ogMetaData { 44 | image 45 | } 46 | tags { 47 | id 48 | name 49 | slug 50 | } 51 | features { 52 | tableOfContents { 53 | isEnabled 54 | items { 55 | id 56 | level 57 | parentId 58 | slug 59 | title 60 | } 61 | } 62 | } 63 | preferences { 64 | disableComments 65 | } 66 | comments(first: 25) { 67 | totalDocuments 68 | edges { 69 | node { 70 | id 71 | totalReactions 72 | content { 73 | markdown 74 | } 75 | author { 76 | name 77 | username 78 | profilePicture 79 | } 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/queries/Sitemap.graphql: -------------------------------------------------------------------------------- 1 | query Sitemap($host: String!, $postsCount: Int!, $postsAfter: String, $staticPagesCount: Int!) { 2 | publication(host: $host) { 3 | id 4 | url 5 | staticPages(first: $staticPagesCount) { 6 | edges { 7 | node { 8 | slug 9 | } 10 | } 11 | } 12 | posts(first: $postsCount, after: $postsAfter) { 13 | edges { 14 | node { 15 | ...RequiredSitemapPostFields 16 | } 17 | } 18 | pageInfo { 19 | ...PageInfo 20 | } 21 | } 22 | } 23 | } 24 | 25 | query MoreSitemapPosts($host: String!, $postsCount: Int!, $postsAfter: String) { 26 | publication(host: $host) { 27 | id 28 | posts(first: $postsCount, after: $postsAfter) { 29 | edges { 30 | node { 31 | ...RequiredSitemapPostFields 32 | } 33 | } 34 | pageInfo { 35 | ...PageInfo 36 | } 37 | } 38 | } 39 | } 40 | 41 | fragment RequiredSitemapPostFields on Post { 42 | id 43 | url 44 | slug 45 | publishedAt 46 | updatedAt 47 | tags { 48 | id 49 | name 50 | slug 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/queries/SlugPostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query SlugPostsByPublication($host: String!, $first: Int!, $after: String) { 2 | publication(host: $host) { 3 | ...Publication 4 | posts(first: $first, after: $after) { 5 | edges { 6 | node { 7 | slug 8 | } 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/lib/api/queries/TagPostsByPublication.graphql: -------------------------------------------------------------------------------- 1 | query TagPostsByPublication( 2 | $host: String! 3 | $tagSlug: String! 4 | $first: Int! 5 | $after: String 6 | ) { 7 | publication(host: $host) { 8 | ...Publication 9 | posts(first: $first, filter: { tagSlugs: [$tagSlug] }, after: $after) { 10 | totalDocuments 11 | edges { 12 | node { 13 | ...Post 14 | } 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ThemeProvider } from 'next-themes'; 2 | import { AppProps } from 'next/app'; 3 | import { useEffect } from 'react'; 4 | import '../styles/index.css'; 5 | 6 | export default function MyApp({ Component, pageProps }: AppProps) { 7 | useEffect(() => { 8 | (window as any).adjustIframeSize = (id: string, newHeight: string) => { 9 | const i = document.getElementById(id); 10 | if (!i) return; 11 | // eslint-disable-next-line radix 12 | i.style.height = `${parseInt(newHeight)}px`; 13 | }; 14 | }, []); 15 | return ( 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Head, Html, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import request from 'graphql-request'; 2 | import { GetServerSideProps } from 'next'; 3 | import { 4 | PublicationByHostDocument, 5 | PublicationByHostQuery, 6 | PublicationByHostQueryVariables, 7 | } from '../generated/graphql'; 8 | 9 | const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT; 10 | const Dashboard = () => null; 11 | 12 | export const getServerSideProps: GetServerSideProps = async () => { 13 | const data = await request( 14 | GQL_ENDPOINT, 15 | PublicationByHostDocument, 16 | { 17 | host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST, 18 | }, 19 | ); 20 | 21 | const publication = data.publication; 22 | if (!publication) { 23 | return { 24 | notFound: true, 25 | }; 26 | } 27 | 28 | return { 29 | redirect: { 30 | destination: `https://hashnode.com/${publication.id}/dashboard`, 31 | permanent: false, 32 | }, 33 | }; 34 | }; 35 | 36 | export default Dashboard; 37 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/pages/robots.txt.tsx: -------------------------------------------------------------------------------- 1 | import { type GetServerSideProps } from 'next'; 2 | 3 | const RobotsTxt = () => null; 4 | 5 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 6 | const { res } = ctx; 7 | const host = process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST; 8 | if (!host) { 9 | throw new Error('Could not determine host'); 10 | } 11 | 12 | const sitemapUrl = `https://${host}/sitemap.xml`; 13 | const robotsTxt = ` 14 | User-agent: * 15 | Allow: / 16 | 17 | # Google adsbot ignores robots.txt unless specifically named! 18 | User-agent: AdsBot-Google 19 | Allow: / 20 | 21 | User-agent: GPTBot 22 | Disallow: / 23 | 24 | Sitemap: ${sitemapUrl} 25 | `.trim(); 26 | 27 | res.setHeader('Cache-Control', 's-maxage=86400, stale-while-revalidate'); 28 | res.setHeader('content-type', 'text/plain'); 29 | res.write(robotsTxt); 30 | res.end(); 31 | 32 | return { props: {} }; 33 | }; 34 | 35 | export default RobotsTxt; 36 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/pages/rss.xml.tsx: -------------------------------------------------------------------------------- 1 | import { constructRSSFeedFromPosts } from '@starter-kit/utils/feed'; 2 | import request from 'graphql-request'; 3 | import { GetServerSideProps } from 'next'; 4 | import { RssFeedDocument, RssFeedQuery, RssFeedQueryVariables } from '../generated/graphql'; 5 | 6 | const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT; 7 | const RSS = () => null; 8 | 9 | export const getServerSideProps: GetServerSideProps = async (ctx) => { 10 | const { res, query } = ctx; 11 | const after = query.after ? (query.after as string) : null; 12 | 13 | const data = await request(GQL_ENDPOINT, RssFeedDocument, { 14 | first: 20, 15 | host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST, 16 | after, 17 | }); 18 | 19 | const publication = data.publication; 20 | if (!publication) { 21 | return { 22 | notFound: true, 23 | }; 24 | } 25 | const allPosts = publication.posts.edges.map((edge) => edge.node); 26 | 27 | const xml = constructRSSFeedFromPosts( 28 | publication, 29 | allPosts, 30 | after, 31 | publication.posts.pageInfo.hasNextPage && publication.posts.pageInfo.endCursor 32 | ? publication.posts.pageInfo.endCursor 33 | : null, 34 | ); 35 | 36 | res.setHeader('Cache-Control', 's-maxage=1, stale-while-revalidate'); 37 | res.setHeader('content-type', 'text/xml'); 38 | res.write(xml); 39 | res.end(); 40 | 41 | return { props: {} }; 42 | }; 43 | 44 | export default RSS; 45 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/postcss.config.js: -------------------------------------------------------------------------------- 1 | // If you want to use other PostCSS plugins, see the following: 2 | // https://tailwindcss.com/docs/using-with-preprocessors 3 | module.exports = { 4 | plugins: { 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/process-env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface ProcessEnv { 3 | [key: string]: string | undefined; 4 | NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT: string; 5 | NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST: string; 6 | // add more environment variables and their types here 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/assets/blog/authors/jj.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/assets/blog/authors/jj.jpeg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/assets/blog/authors/joe.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/assets/blog/authors/joe.jpeg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/assets/blog/authors/tim.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/assets/blog/authors/tim.jpeg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/assets/blog/dynamic-routing/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/assets/blog/dynamic-routing/cover.jpg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/assets/blog/hello-world/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/assets/blog/hello-world/cover.jpg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/assets/blog/preview/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/assets/blog/preview/cover.jpg -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #000000 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/favicon/favicon.ico -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/public/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hashnode/starter-kit/b90275211c2cf594391b14e17771abb4837c5e84/packages/blog-starter-kit/themes/personal/public/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "@starter-kit/tsconfig/nextjs.json", 4 | "include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"], 5 | "exclude": ["node_modules"] 6 | } 7 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/utils/const/index.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_AVATAR = 2 | 'https://cdn.hashnode.com/res/hashnode/image/upload/v1659089761812/fsOct5gl6.png'; 3 | -------------------------------------------------------------------------------- /packages/blog-starter-kit/themes/personal/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "nextjs" 3 | } -------------------------------------------------------------------------------- /packages/eslint-config-custom/index.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@types/eslint').Linter.BaseConfig} */ 2 | module.exports = { 3 | extends: ['next/core-web-vitals', 'prettier'], 4 | }; 5 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@starter-kit/eslint-config-custom", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "dependencies": { 7 | "eslint-config-next": "^13.5.4", 8 | "eslint-config-prettier": "^9.0.0" 9 | }, 10 | "peerDependencies": { 11 | "eslint": "^8.0.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "incremental": true, 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "target": "es2022", 9 | "verbatimModuleSyntax": false, 10 | "allowJs": true, 11 | "resolveJsonModule": true, 12 | "moduleDetection": "force", 13 | "isolatedModules": true, 14 | 15 | "strict": true, 16 | "noUncheckedIndexedAccess": false, 17 | "forceConsistentCasingInFileNames": true, 18 | 19 | "moduleResolution": "Bundler", 20 | "module": "ESNext", 21 | "noEmit": true, 22 | 23 | "lib": ["es2022", "dom", "dom.iterable"] 24 | }, 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./base.json", 5 | "compilerOptions": { 6 | "plugins": [{ "name": "next" }], 7 | "jsx": "preserve" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@starter-kit/tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT" 6 | } 7 | -------------------------------------------------------------------------------- /packages/utils/handle-math-jax.js: -------------------------------------------------------------------------------- 1 | const handleMathJax = (rerun = false) => { 2 | if (typeof window === 'undefined') { 3 | return; 4 | } 5 | 6 | const mathjaxScript = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js'; 7 | if (!window.MathJax) { 8 | window.MathJax = { 9 | tex: { 10 | inlineMath: [['\\(', '\\)']], 11 | }, 12 | }; 13 | } 14 | 15 | let mathjaxScriptTag = document.querySelector(`script[src="${mathjaxScript}"]`); 16 | if (!mathjaxScriptTag) { 17 | let script = document.createElement('script'); 18 | script.type = 'text/javascript'; 19 | script.src = mathjaxScript; 20 | script.onload = function () { 21 | window.MathJax && 'mathml2chtml' in window.MathJax && window.MathJax.mathml2chtml(); 22 | }; 23 | document.head.appendChild(script); 24 | } else if (rerun) { 25 | window.MathJax && 'mathml2chtml' in window.MathJax && window.MathJax.mathml2chtml(); 26 | } 27 | }; 28 | 29 | export default handleMathJax; 30 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@starter-kit/utils", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "license": "MIT", 7 | "author": "", 8 | "main": "index.js", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "dependencies": { 13 | "js-base64": "^3.7.5", 14 | "rss": "^1.2.2", 15 | "sanitize-html": "^2.11.0", 16 | "slug": "^8.2.3", 17 | "validator": "^13.11.0" 18 | }, 19 | "devDependencies": { 20 | "@types/rss": "^0.0.30", 21 | "@types/sanitize-html": "^2.9.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/utils/renderer/consts/images.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/prefer-default-export 2 | export const DEFAULT_AVATAR: string = 3 | 'https://cdn.hashnode.com/res/hashnode/image/upload/v1659089761812/fsOct5gl6.png?auto=compress'; 4 | 5 | export const DEFAULT_TEAM_PUB_AVATAR = 6 | 'https://cdn.hashnode.com/res/hashnode/image/upload/v1655710917204/_cHQMSjt3.png'; 7 | 8 | export const blurImageDimensions = { w: 400, h: 210 }; 9 | -------------------------------------------------------------------------------- /packages/utils/renderer/headingSlugger.ts: -------------------------------------------------------------------------------- 1 | import sanitizeHtml from 'sanitize-html'; 2 | import slug from 'slug'; 3 | 4 | export class HeadingSlugger { 5 | headings: { [key: string]: number }; 6 | 7 | constructor() { 8 | this.headings = {}; 9 | } 10 | 11 | static sanitizeSlug(str: string): string { 12 | return slug(sanitizeHtml(str, { allowedTags: [] }), { lower: true }); 13 | } 14 | 15 | private doesHeadingExist(slug: string): boolean { 16 | // eslint-disable-next-line no-prototype-builtins 17 | return this.headings.hasOwnProperty(slug); 18 | } 19 | 20 | private findSafeSlug(originalSlug: string) { 21 | const headingExists = this.doesHeadingExist(originalSlug); 22 | 23 | if (!headingExists) { 24 | this.headings[originalSlug] = 0; 25 | return originalSlug; 26 | } 27 | let modifiedSlug; 28 | let duplicateCount = this.headings[originalSlug]; 29 | 30 | do { 31 | duplicateCount += 1; 32 | modifiedSlug = `${originalSlug}-${duplicateCount}`; 33 | } while (this.doesHeadingExist(modifiedSlug)); 34 | 35 | this.headings[modifiedSlug] = 0; 36 | this.headings[originalSlug] += 1; 37 | return modifiedSlug; 38 | } 39 | 40 | public getSlug(str: string) { 41 | const sanitizedSlug = HeadingSlugger.sanitizeSlug(str); 42 | return this.findSafeSlug(sanitizedSlug); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/utils/renderer/hooks/useEmbeds.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { triggerEmbed } from '../services/embed'; 3 | 4 | export const useEmbeds = ({ enabled }: { enabled: boolean }) => { 5 | useEffect(() => { 6 | // if enabled we need to trigger all embeds on page load 7 | if (enabled) { 8 | (async () => { 9 | document.querySelectorAll('a.embed-card').forEach((node) => { 10 | triggerEmbed(node); 11 | }); 12 | })(); 13 | } 14 | }, [enabled]); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/utils/renderer/sanitizeHTMLOptions.js: -------------------------------------------------------------------------------- 1 | const sanitizeHtml = require('sanitize-html'); 2 | 3 | const allowedTags = sanitizeHtml.defaults.allowedTags.concat([ 4 | 'h1', 5 | 'h2', 6 | 'h3', 7 | 'span', 8 | 'img', 9 | 'div', 10 | 'iframe', 11 | 'abbr', 12 | 'kbd', 13 | 'cite', 14 | 'dl', 15 | 'dt', 16 | 'dd', 17 | 's', 18 | 'sub', 19 | 'sup', 20 | 'details', 21 | 'summary', 22 | ]); 23 | const allowedAttributes = { 24 | '*': ['id'], 25 | iframe: ['src', 'class', 'sandbox', 'style', 'width', 'height'], 26 | div: ['class', 'data-*'], 27 | a: ['href', 'class', 'target'], 28 | img: ['src', 'alt', 'class'], 29 | code: ['class'], 30 | span: ['class'], 31 | abbr: ['title'], 32 | }; 33 | // const disallowedTagsMode = 'discard'; 34 | 35 | const sanitizeHtmlOptions = { allowedTags, allowedAttributes }; 36 | 37 | module.exports = sanitizeHtmlOptions; 38 | -------------------------------------------------------------------------------- /packages/utils/seo/addArticleJsonLd.ts: -------------------------------------------------------------------------------- 1 | export const addArticleJsonLd = (publication: any, post: any) => { 2 | const tags = (post.tags ?? []).map((tag: any) => tag.name); 3 | const schema = { 4 | '@context': 'https://schema.org/', 5 | '@type': 'Blog', 6 | '@id': publication.url, 7 | mainEntityOfPage: publication.url, 8 | name: publication.title, 9 | description: publication.about?.markdown, 10 | publisher: { 11 | '@type': publication.isTeam ? 'Organization' : 'Person', 12 | '@id': publication.url, 13 | name: publication.title, 14 | image: { 15 | '@type': 'ImageObject', 16 | url: publication.preferences?.logo || publication.preferences?.darkMode?.logo, 17 | }, 18 | }, 19 | blogPost: [ 20 | { 21 | '@type': 'BlogPosting', 22 | '@id': post.url, 23 | mainEntityOfPage: post.url, 24 | headline: post.title, 25 | name: post.title, 26 | description: post.seo?.description || post.brief, 27 | datePublished: post.publishedAt, 28 | dateModified: post.updatedAt, 29 | author: { 30 | '@type': 'Person', 31 | '@id': `https://hashnode.com/@${post.author?.username}`, 32 | name: post.author?.name, 33 | url: `https://hashnode.com/@${post.author?.username}`, 34 | }, 35 | image: { 36 | '@type': 'ImageObject', 37 | url: post.coverImage?.url, 38 | }, 39 | url: post.url, 40 | keywords: tags, 41 | }, 42 | ], 43 | }; 44 | return schema; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/utils/seo/addPublicationJsonLd.ts: -------------------------------------------------------------------------------- 1 | export const addPublicationJsonLd = (publication: any) => { 2 | const schema = { 3 | '@context': 'https://schema.org/', 4 | '@type': 'Blog', 5 | '@id': publication.url, 6 | mainEntityOfPage: publication.url, 7 | name: publication.title, 8 | description: publication.descriptionSEO, 9 | publisher: { 10 | '@type': publication.isTeam ? 'Organization' : 'Person', 11 | '@id': publication.url, 12 | name: publication.title, 13 | image: { 14 | '@type': 'ImageObject', 15 | url: publication.preferences?.logo, 16 | }, 17 | }, 18 | }; 19 | return schema; 20 | }; 21 | -------------------------------------------------------------------------------- /packages/utils/trigger-custom-widget-embed.js: -------------------------------------------------------------------------------- 1 | export const triggerCustomWidgetEmbed = async (pubId) => { 2 | const frames = document.querySelectorAll('.hn-embed-widget'); 3 | if (frames.length === 0) { 4 | return; 5 | } 6 | frames.forEach(async (frame) => { 7 | try { 8 | const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || ''; 9 | const iframe = document.createElement('iframe'); 10 | const host = window.location.hostname; 11 | iframe.id = `frame-${frame.id}`; 12 | iframe.sandbox = 13 | 'allow-same-origin allow-forms allow-presentation allow-scripts allow-popups'; 14 | iframe.src = 15 | host.indexOf('.hashnode.net') !== -1 || host.indexOf('.app.localhost') !== -1 16 | ? `${baseUrl}/api/pub/${pubId}/embed/${frame.id}` 17 | : `https://embeds.hashnode.co?p=${pubId}&w=${frame.id}`; 18 | iframe.width = '100%'; 19 | frame.innerHTML = ''; 20 | frame.appendChild(iframe); 21 | setTimeout(() => { 22 | // TODO: 23 | // eslint-disable-next-line no-undef 24 | iFrameResize({ log: false, autoResize: true }, `#${iframe.id}`); 25 | }, 1000); 26 | frame.setAttribute('class', 'hn-embed-widget-expanded'); 27 | } catch (e) { 28 | console.log(e); 29 | } 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "./packages/**" -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | const config = { 3 | bracketSpacing: true, 4 | printWidth: 100, 5 | trailingComma: 'all', 6 | tabWidth: 2, 7 | useTabs: true, 8 | semi: true, 9 | singleQuote: true, 10 | plugins: [ 11 | // comment for better diff 12 | 'prettier-plugin-organize-imports', 13 | 'prettier-plugin-tailwindcss', 14 | 'prettier-plugin-packagejson', 15 | ], 16 | }; 17 | 18 | module.exports = config; 19 | --------------------------------------------------------------------------------
9 | 10 | {readTimeInMinutes} min read 11 |
The comments have been disabled by the author for this article
24 | 25 | 26 | 27 | {commentCount > 2 && ( 28 | <> 29 | · 30 | 31 | {commentCount} comments 32 | 33 | > 34 | )} 35 |