diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..88249539f
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+.git
+.gitignore
\ No newline at end of file
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 000000000..18c914718
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..57a080ca8
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,107 @@
+# Contributing
+
+We would ❤️ you to contribute to Headless Hashnode Starter kit and help make it better! We want contributing to Hashnode to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including issues, integrations, updates and tweaks.
+
+## Contribution Guidelines
+
+### General Contributions
+
+While we welcome a wide range of contributions, it's important to align with the project's goals. Here are the types of contributions we value the most:
+
+- **New Features**: Adding new capabilities or tools that enhance the functionality of the starter kit.
+- **Bug Fixes**: Resolving existing issues to improve stability and performance.
+- **Integrations**: Building connectors or tools that integrate with other services or platforms.
+- **Improvements**: Enhancements that add value without altering the core look and feel in a subjective manner.
+
+**Note: We generally do not accept pull requests that solely make cosmetic changes, such as altering font weight, padding, text decoration, etc unless they solve an existing issue or add a new generic feature. This is because we believe that cosmetic changes are subjective and users might have different preferences.**
+
+If your contribution falls into the above categories, we encourage you to submit a pull request!
+
+### Theme Contributions
+
+While we appreciate the creativity in creating new themes, we want to keep the starter kit streamlined and focused on its core themes: Personal, Enterprise, and Hashnode. Therefore, we do not accept direct PRs for adding new themes to the main repository. Instead, we encourage you to:
+
+1. **Host your theme in a separate repository**.
+2. **Deploy your theme to a live demo site**.
+3. **Open an issue on our GitHub repository and provide the links in the description.**.
+
+We will showcase these themes under the `Community Themes` section in our README, allowing others to discover and use them.
+
+## How to Start?
+
+If you are worried or don’t know where to start, you can checkout open issues or add new issues and comment your interest and a maintainer can guide you. Alternatively, you can send your questions to anyone from the [Hashnode team on Discord](https://hshno.de/discord).
+
+## Submit a Pull Request 🚀
+
+Branch naming convention is as following
+
+`TYPE-DESCRIPTION`
+
+example:
+
+```
+feat-adds-profile-section
+```
+
+When `TYPE` can be:
+
+- **feat** - a new feature
+- **fix** - a bug fix
+- **refactor** - code change that neither fixes a bug nor adds a feature
+
+**All PRs must include a commit message with the description of the changes made!**
+
+For the initial start, fork the project and use git clone command to download the repository to your computer. A standard procedure for working on an issue would be to:
+
+1. `git pull`, before creating a new branch, pull the changes from upstream. Your main branch needs to be up to date.
+
+```
+$ git pull
+```
+
+2. Create a new branch from `main` like: `feat-adds-profile-section`.
+
+```
+$ git checkout -b [name_of_your_new_branch]
+```
+
+3. Work - commit - repeat (make sure you're on the correct branch!)
+
+4. Push changes to GitHub.
+
+```
+$ git push origin [name_of_your_new_branch]
+```
+
+5. Submit your changes for review
+ If you go to your repository on GitHub, you'll see a `Compare & pull request` button. Click on that button.
+6. Start a Pull Request
+ Now submit the pull request and click on `Create pull request`.
+7. Get a code review approval/reject.
+8. After approval, merge your PR.
+9. GitHub will automatically delete the branch after the merge is done. (they can still be restored).
+
+## Setup From Source
+
+To set up a working **development environment**, just fork the project git repository and install the necessary packages with the `pnpm install` command.
+
+> If you just want to get started with Headless Hashnode for day-to-day use and not as a contributor, you can refer to the [guide](https://hashnode.com/headless) or the [README](README.md) file.
+
+```bash
+git clone git@github.com:[YOUR_FORK_HERE]/starter-kit.git
+
+cd starter-kit
+
+pnpm install
+
+pnpm run dev
+```
+
+ Note- Make sure to run `pnpm run dev` in the correct folder. You can find more instructions about it in the [README](https://github.com/Hashnode/starter-kit/?tab=readme-ov-file#running-locally)
+
+## Resources
+
+To stay updated with latest updates of Hashnode, you can follow:
+
+- [Changelog](https://hashnode.com/changelog)
+- [Hashnode Discord Server](https://hshno.de/discord)
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 000000000..f715addb1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,24 @@
+FROM node:18-alpine
+
+COPY . /app
+
+# Set the working directory
+WORKDIR /app
+
+# Install pnpm
+RUN npm install -g pnpm
+
+# By default, use the enterprise theme
+ARG THEME=enterprise
+
+WORKDIR /app/packages/blog-starter-kit/themes/${THEME}
+RUN cp .env.example .env.local
+RUN pnpm install --frozen-lockfile
+
+RUN pnpm build
+
+# Expose the port Next.js runs on
+EXPOSE 3000
+
+# Run the Next.js start script
+CMD ["pnpm", "start"]
diff --git a/README.md b/README.md
index 593a5cffe..e1c071990 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
-
+
@@ -23,23 +23,36 @@
Blog Starter Kit lets you instantly deploy a Next.js and Tailwind powered frontend for your Hashnode blog. It consumes [Hashnode's Public APIs](https://apidocs.hashnode.com), and gives you a fully customizable blog that can be deployed anywhere, including a subpath of a custom domain. Combined with [Hashnode's headless mode](https://hashnode.com/headless), it unlocks entirely new possibilities. You can now use Hashnode's [world class editor](https://hashnode.com/neptune) and dashboard to author content and collaborate. And use blog starter kit to customize the frontend to your liking.
-# Live Demos:
+# Live Demos
-- [Personal Blog](https://sandeep.dev/blog)
-- [Enterprise Blog](https://demo.hashnode.com/engineering)
+Please note: The themes showcased in these demos have been heavily customized.
+
+- [Personal theme](https://starter-diopgk410-hashnode-prod.vercel.app/blog)
+- [Enterprise theme](https://hashnode.com/blog)
+- [Hashnode theme](https://datazip.io/blog?utm_source=hashnode&utm_medium=github&utm_campaign=promotional)
+
+# Example company blogs built with Headless
+
+- [MindsDB](https://mindsdb.com/blog)
+- [Pangea Cloud](https://pangea.cloud/blog)
+- [Outerbase](https://outerbase.com/blog)
+- [Fern](https://blog.buildwithfern.com/)
+- [Fix](https://fix.tt/blog)
## How to deploy
### Step 1
-The recommended approach is depoying to Vercel. If you don't have an account already, sign up for a free plan.
+The recommended approach is deploying to Vercel. If you don't have an account already, you can sign up for a free plan.
- Fork this repo
- Create a new project on Vercel and connect this repo
-- It's a monorepo. So, choose the either `packages/blog-starter-kit/themes/enterprise` or `packages/blog-starter-kit/themes/personal` as the root directory while importing on Vercel.
- 
+- It's a monorepo, So choose either `packages/blog-starter-kit/themes/enterprise`, `packages/blog-starter-kit/themes/hashnode`, or `packages/blog-starter-kit/themes/personal` as the root directory while importing on Vercel.
+
+ 
+
- Choose `Next.js` as framework preset (just above Root Directory setting).
-- Set the following env vars
+- Set the following environment variables
```
NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT=https://gql.hashnode.com
@@ -56,7 +69,7 @@ Follow the steps below if you would like to install your blog under a custom dom
#### Vercel
-If your main project is deployed on Vercel, add the following rewrite to `next.config.js`:
+If your main project is deployed on Vercel, add the following rewrite to `next.config.js` file:
```
async rewrites() {
@@ -75,6 +88,11 @@ async rewrites() {
Once you deploy your project, the subpath installation should work successfully.
+> Note:
+>
+> - If you are updating your environment variables in Vercel, make sure to manually redeploy to see the changes.
+> - Your main project refers to the project you'll have to have your blog subpath on; for example, if you'd like your blog subpath to be `https://portfolio.com/blog`, then your main project would be `https://portfolio.com`. This means that the rewrites function should be added to the codebase of the main project, not the starter kit codebase.
+
#### Cloudflare
In case you are using Cloudflare in proxy mode (orange cloud on), you can deploy the following worker script and map it to `yourdomain.com/*`:
@@ -96,7 +114,7 @@ async function handleRequest(request) {
if (url.pathname.startsWith(subpath)) {
// Proxy blog requests
- return proxyBlog(request)
+ return proxyBlog(request)
} else {
// Passthrough everything else
return fetch(request)
@@ -113,25 +131,31 @@ async function proxyBlog(request) {
}
```
-Be sure to replace the values of `subpath` and `blogBaseUrl` in the above snippet. This way cloudflare will proxy all the requests starting with `youdomain.com/blog` to your headless blog, and other requests will hit your origin as usual.
+After the above step is done, follow these steps to add the worker route:
+
+- Go to `Websites` then click on your website and select `Worker Routes` from the left pane.
+- Click on `Add route` and add `https://yourdomain/*` , then select the worker you just added above and click `Save`.
+- Go to `https://yourdomain/yoursubpath` and now you should be able to see your blogs.
+
+Make sure to replace the values of `subpath` and `blogBaseUrl` in the above code snippet. This way, Cloudflare will proxy all the requests starting with `yourdomain.com/blog` to your headless blog, and other requests will hit your origin as usual.
-If your main domain is hosted elsewhere, you need to involve engineers from your team to create above rewrites.
+If your main domain is hosted elsewhere, you need to involve engineers from your team to create the above rewrites.
### Step 3
-Now that you have deployed the starter kit on your own domain, you need to tell Hashnode not to generate a UI for your blog. You can do that by visiting your blog dashboard -> advanced tab. Scroll down and locate the section "use Hashnode as a headless CMS". Enable it and enter your blog base URL.
+Now that you have deployed the starter kit on your own domain, you need to tell Hashnode not to generate a UI for your blog. You can do that by visiting your blog dashboard -> domain -> Headless mode. Enable headless mode and enter your blog base URL.
-
+
-After enabling, enter your blog URL like the following and save.
+After enabling, enter your blog URL as shown below and save.
-
+
Congrats 🎉! Hashnode will now treat your blog as a headless blog and send readers directly to the origin.
## Running Locally
-- cd `packages/blog-starter-kit/themes/enterprise` or `packages/blog-starter-kit/themes/personal`
+- cd into either `packages/blog-starter-kit/themes/enterprise`, or `packages/blog-starter-kit/themes/hashnode` or `packages/blog-starter-kit/themes/personal`
- Copy `.env.example` to `.env.local`
- `pnpm install`
- `pnpm dev`
@@ -145,12 +169,30 @@ If you prefer to build your frontend from scratch, you can use our public GraphQ
- [Docs](https://apidocs.hashnode.com)
- [GraphQL Playground](https://gql.hashnode.com)
+## Pricing
+
+**For individual devs:** Hashnode's Headless CMS is free for individual bloggers! Grab our starter kit and start building your blog – no license is needed.
+
+**For teams and enterprises:** Access to headless mode, multiple team members, real-time collaboration, AI, and enterprise reliability. [Request access and get a quote.](https://forms.hashnode.com/headless-hashnode-teams) We will be in touch within the next 24hrs to get you onboarded.
+
## Demo Videos
[](https://youtu.be/5Yuxoqohvrk)
[](https://youtu.be/oH8QG8E0Txk)
+## Community Themes
+
+In addition to our core themes, the community has developed a variety of themes to customize your blog. Check out these themes and explore their unique designs:
+
+| Theme Name | Demo Link | Codebase Link |
+| ------------------------------- | --------------------------------------------------------- | -------------------------------------------------------------------------------------- |
+| **Newsletter Theme** | [View Demo](https://starter-kit1-6yte.vercel.app/) | [GitHub Repository](https://github.com/masterismail/starter-kit1/tree/main) |
+| **Minimalist (Ikigai) Theme** | [View Demo](https://pravinreacts.vercel.app/) | [GitHub Repository](https://github.com/pravintargaryen/starter-kit/tree/main) |
+| **Floaty Portfolio/Blog Theme** | [View Demo](https://floaty-hashnode-headless.vercel.app/) | [GitHub Repository](https://github.com/iammarmirza/floaty-hashnode-headless/tree/main) |
+
+> Note: These themes are maintained by the community and are not part of the official starter kit.
+
## Found an issue?
If you have found an issue or bug, please create an [issue](https://github.com/Hashnode/starter-kit/issues).
@@ -163,6 +205,6 @@ Feel free to create an [issue](https://github.com/Hashnode/starter-kit/issues) w
## Reach out for help
-You can discuss ideas, ask questions, and meet other members from the Hashnode community in our [Discord](https://discord.gg/hashnode). You can also create tickets on [our intercom](https://hashnode.com/#support) to find support.
+You can discuss ideas, ask questions, and meet other members from the Hashnode community in our [Discord](https://discord.gg/qsAQfxX). You can also create tickets on [our intercom](https://hashnode.com/#support) to find support.
If you like, you can also DM us on [X](https://x.com/hashnode)!
diff --git a/license.md b/license.md
index 8a0b4cffa..2248552c8 100644
--- a/license.md
+++ b/license.md
@@ -1,9 +1,9 @@
The MIT License (MIT)
-Copyright (c) 2023 Hashnode.
+Copyright (c) 2024 Hashnode.
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-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.
\ No newline at end of file
+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.
diff --git a/packages/blog-starter-kit/themes/enterprise/.graphqlrc.yml b/packages/blog-starter-kit/themes/enterprise/.graphqlrc.yml
index 4bd27f048..8d2918601 100644
--- a/packages/blog-starter-kit/themes/enterprise/.graphqlrc.yml
+++ b/packages/blog-starter-kit/themes/enterprise/.graphqlrc.yml
@@ -1,2 +1,2 @@
schema: './generated/schema.graphql'
-documents: './{pages,components}/**/*.{graphql,js,ts,jsx,tsx}'
+documents: './{pages,components,lib}/**/*.{graphql,js,ts,jsx,tsx}'
diff --git a/packages/blog-starter-kit/themes/enterprise/README.md b/packages/blog-starter-kit/themes/enterprise/README.md
index 2fbf3d715..2a64a8799 100644
--- a/packages/blog-starter-kit/themes/enterprise/README.md
+++ b/packages/blog-starter-kit/themes/enterprise/README.md
@@ -1,31 +1,11 @@
# A statically generated blog example using Next.js, Markdown, and TypeScript with Hashnode 💫
-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).
+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).
-We've used [Hashnode API's](https://apidocs.hashnode.com) and integrated them with this blog starter kit.
+We've used [Hashnode APIs](https://apidocs.hashnode.com) and integrated them with this blog starter kit.
## Want to have your own?
-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.
+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.
-## Demo
-
-[https://next-blog-starter.vercel.app/](https://next-blog-starter.vercel.app/)
-
-## Deploy your own
-
-Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/blog-starter)
-
-[](https://vercel.com/new/clone?repository-url=https://github.com/vercel/next.js/tree/canary/examples/blog-starter&project-name=blog-starter&repository-name=blog-starter)
-
-# Notes
-
-`blog-starter` uses [Tailwind CSS](https://tailwindcss.com) [(v3.0)](https://tailwindcss.com/blog/tailwindcss-v3).
-
-## To-Do
-
-- [ ] Pagination
-- [ ] Vercel Deploy Button
-- [ ] Submit as Template
+Demo of the `enterprise` theme: [https://demo.hashnode.com/engineering](https://demo.hashnode.com/engineering).
diff --git a/packages/blog-starter-kit/themes/enterprise/components/about-author.tsx b/packages/blog-starter-kit/themes/enterprise/components/about-author.tsx
new file mode 100644
index 000000000..c5fc4dd24
--- /dev/null
+++ b/packages/blog-starter-kit/themes/enterprise/components/about-author.tsx
@@ -0,0 +1,36 @@
+import PostAuthorInfo from './post-author-info';
+import { useAppContext } from './contexts/appContext';
+import { PostFullFragment } from '../generated/graphql';
+
+function AboutAuthor() {
+ const { post: _post } = useAppContext();
+ const post = _post as unknown as PostFullFragment;
+ const { publication, author } = post;
+ let coAuthors = post.coAuthors || [];
+
+ const allAuthors = publication?.isTeam ? [author, ...coAuthors] : [author];
+
+ return (
+
+
+
+
+ Written by
+
+
+ {allAuthors.map((_author) => {
+ return (
+
+ );
+ })}
+
+
+
+
+ );
+}
+
+export default AboutAuthor;
\ No newline at end of file
diff --git a/packages/blog-starter-kit/themes/enterprise/components/analytics.tsx b/packages/blog-starter-kit/themes/enterprise/components/analytics.tsx
index 75db28156..2dcdb1969 100644
--- a/packages/blog-starter-kit/themes/enterprise/components/analytics.tsx
+++ b/packages/blog-starter-kit/themes/enterprise/components/analytics.tsx
@@ -1,24 +1,14 @@
import Cookies from 'js-cookie';
import { useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
-import { useAppContext } from './contexts/appContext';
+import { useAppContext } from './contexts/appContext';
const GA_TRACKING_ID = 'G-72XG3F8LNJ'; // This is Hashnode's GA tracking ID
const isProd = process.env.NEXT_PUBLIC_MODE === 'production';
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_URL || '';
export const Analytics = () => {
- const { publication, post } = useAppContext();
-
- useEffect(() => {
- if (!isProd) return;
-
- _sendPageViewsToHashnodeGoogleAnalytics();
- _sendViewsToHashnodeInternalAnalytics();
- _sendViewsToHashnodeAnalyticsDashboard();
- }, []);
-
- if (!isProd) return null;
+ const { publication, post, series, page } = useAppContext();
const _sendPageViewsToHashnodeGoogleAnalytics = () => {
// @ts-ignore
@@ -62,81 +52,90 @@ export const Analytics = () => {
});
};
- const _sendViewsToHashnodeAnalyticsDashboard = async () => {
- const LOCATION = window.location;
- const NAVIGATOR = window.navigator;
- const currentFullURL =
- LOCATION.protocol +
- '//' +
- LOCATION.hostname +
- LOCATION.pathname +
- LOCATION.search +
- LOCATION.hash;
-
- const query = new URL(currentFullURL).searchParams;
-
- const utm_id = query.get('utm_id');
- const utm_campaign = query.get('utm_campaign');
- const utm_source = query.get('utm_source');
- const utm_medium = query.get('utm_medium');
- const utm_term = query.get('utm_term');
- const utm_content = query.get('utm_content');
-
- let referrer = document.referrer || '';
- if (referrer.indexOf(window.location.hostname) !== -1) {
- referrer = '';
- }
+ function _sendViewsToAdvancedAnalyticsDashboard() {
+ const publicationId = publication.id;
+ const postId = post && post.id;
+ const seriesId = series?.id || post?.series?.id;
+ const staticPageId = page && page.id;
const data = {
- publicationId: publication.id,
- postId: post && post.id,
- timestamp: Date.now(),
- url: currentFullURL,
- referrer: referrer,
- title: document.title,
- charset: document.characterSet || document.charset,
- lang: NAVIGATOR.language,
- userAgent: NAVIGATOR.userAgent,
- historyLength: window.history.length,
- timezoneOffset: new Date().getTimezoneOffset(),
- utm_id,
- utm_campaign,
- utm_source,
- utm_medium,
- utm_term,
- utm_content,
+ publicationId,
+ postId,
+ seriesId,
+ staticPageId,
};
- // send to Umami powered advanced Hashnode analytics
- if (publication.integrations?.umamiWebsiteUUID) {
- await fetch(`${BASE_PATH}/api/collect`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({
- payload: {
- website: publication.integrations.umamiWebsiteUUID,
- url: window.location.pathname,
- referrer: referrer,
- hostname: window.location.hostname,
- language: NAVIGATOR.language,
- screen: `${window.screen.width}x${window.screen.height}`,
- },
- type: 'pageview',
+ if (!publicationId) {
+ console.warn('Publication ID is missing; could not send analytics.');
+ return;
+ }
+
+ const isBrowser = typeof window !== 'undefined';
+ if (!isBrowser) {
+ return;
+ }
+
+ const isLocalhost = window.location.hostname === 'localhost';
+ if (isLocalhost) {
+ console.warn(
+ 'Analytics API call is skipped because you are running on localhost; data:',
+ data,
+ );
+ return;
+ }
+
+ const event = {
+ // timestamp will be added in API
+ payload: {
+ publicationId,
+ postId: postId || null,
+ seriesId: seriesId || null,
+ pageId: staticPageId || null,
+ url: window.location.href,
+ referrer: document.referrer || null,
+ language: navigator.language || null,
+ screen: `${window.screen.width}x${window.screen.height}`,
+ },
+ type: 'pageview',
+ };
+
+ const blob = new Blob(
+ [
+ JSON.stringify({
+ events: [event],
}),
+ ],
+ {
+ type: 'application/json; charset=UTF-8',
+ },
+ );
+
+ let hasSentBeacon = false;
+ try {
+ if (navigator.sendBeacon) {
+ hasSentBeacon = navigator.sendBeacon(`${BASE_PATH}/api/analytics`, blob);
+ }
+ } catch (error) {
+ // do nothing; in case there is an error we fall back to fetch
+ }
+
+ if (!hasSentBeacon) {
+ fetch(`${BASE_PATH}/api/analytics`, {
+ method: 'POST',
+ body: blob,
+ credentials: 'omit',
+ keepalive: true,
});
}
+ }
- // For Hashnode Blog Dashboard Analytics
- fetch(`${BASE_PATH}/ping/view`, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- },
- body: JSON.stringify({ data }),
- });
- };
+ useEffect(() => {
+ if (!isProd) return;
+
+ _sendPageViewsToHashnodeGoogleAnalytics();
+ _sendViewsToHashnodeInternalAnalytics();
+ _sendViewsToAdvancedAnalyticsDashboard();
+ }, []);
return null;
};
diff --git a/packages/blog-starter-kit/themes/enterprise/components/avatar.tsx b/packages/blog-starter-kit/themes/enterprise/components/avatar.tsx
index 39e21f8ab..a52ed1426 100644
--- a/packages/blog-starter-kit/themes/enterprise/components/avatar.tsx
+++ b/packages/blog-starter-kit/themes/enterprise/components/avatar.tsx
@@ -1,7 +1,5 @@
import { resizeImage } from '@starter-kit/utils/image';
-
-const DEFAULT_AVATAR =
- 'https://cdn.hashnode.com/res/hashnode/image/upload/v1659089761812/fsOct5gl6.png';
+import { DEFAULT_AVATAR } from '../utils/const';
type Props = {
username: string;
diff --git a/packages/blog-starter-kit/themes/enterprise/components/co-authors-modal.tsx b/packages/blog-starter-kit/themes/enterprise/components/co-authors-modal.tsx
new file mode 100644
index 000000000..9f0c682ea
--- /dev/null
+++ b/packages/blog-starter-kit/themes/enterprise/components/co-authors-modal.tsx
@@ -0,0 +1,85 @@
+import { Button } from './button';
+import CloseSVG from './icons/svgs/CloseSVG';
+import * as DialogPrimitive from '@radix-ui/react-dialog';
+import CustomScrollArea from './scroll-area';
+import { DEFAULT_AVATAR } from '../utils/const';
+import { ResizableImage } from './resizable-image';
+import { useAppContext } from './contexts/appContext';
+import { PostFullFragment } from '../generated/graphql';
+
+type CoAuthorsModalProps = {
+ closeModal: () => void;
+};
+
+const AuthorCard = ({ author }: { author: PostFullFragment['author']; }) => {
+
+ return (
+
+ );
+};
+
+export default function CoAuthorsModal({ closeModal }: CoAuthorsModalProps) {
+ const { post } = useAppContext();
+ const authors = [post?.author, ...(post?.coAuthors || [])];
+
+ return (
+
+
+
+
+
+ Authors in this article
+
+
+ }
+ className="rounded-xl !border-transparent !px-3 !py-2 hover:bg-neutral-800 dark:text-white"
+ onClick={closeModal}
+ />
+
+
+
+ {authors.map((author) => {
+ if (!author) {
+ return null;
+ }
+ return
;
+ })}
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/packages/blog-starter-kit/themes/enterprise/components/contexts/appContext.tsx b/packages/blog-starter-kit/themes/enterprise/components/contexts/appContext.tsx
index 7c408bbf7..4c8307fcb 100644
--- a/packages/blog-starter-kit/themes/enterprise/components/contexts/appContext.tsx
+++ b/packages/blog-starter-kit/themes/enterprise/components/contexts/appContext.tsx
@@ -1,7 +1,17 @@
import React, { createContext, useContext } from 'react';
-import { PostFullFragment, PublicationFragment } from '../../generated/graphql';
+import {
+ PostFullFragment,
+ PublicationFragment,
+ SeriesPostsByPublicationQuery,
+ StaticPageFragment,
+} from '../../generated/graphql';
-type AppContext = { publication: PublicationFragment; post: PostFullFragment | null };
+type AppContext = {
+ publication: PublicationFragment;
+ post: PostFullFragment | null;
+ page: StaticPageFragment | null;
+ series: NonNullable['series'];
+};
const AppContext = createContext(null);
@@ -9,16 +19,22 @@ const AppProvider = ({
children,
publication,
post,
+ page,
+ series,
}: {
children: React.ReactNode;
publication: PublicationFragment;
post?: PostFullFragment | null;
+ page?: StaticPageFragment | null;
+ series?: NonNullable['series'];
}) => {
return (
{children}
diff --git a/packages/blog-starter-kit/themes/enterprise/components/custom-image.tsx b/packages/blog-starter-kit/themes/enterprise/components/custom-image.tsx
new file mode 100644
index 000000000..31c7f5cc2
--- /dev/null
+++ b/packages/blog-starter-kit/themes/enterprise/components/custom-image.tsx
@@ -0,0 +1,53 @@
+import { ImgHTMLAttributes } from 'react';
+
+import Image, { ImageProps } from 'next/legacy/image';
+
+type Props = {
+ src: any; // can be string or StaticImport of next/image
+ alt: string;
+ originalSrc: string;
+} & ImgHTMLAttributes &
+ ImageProps;
+
+/**
+ * Conditionally renders native img for gifs and next/image for other types
+ * @param props
+ * @returns or
+ */
+function CustomImage(props: Props) {
+ const { originalSrc, ...originalRestOfTheProps } = props;
+ const {
+ alt = '',
+ loader,
+ quality,
+ priority,
+ loading,
+ unoptimized,
+ objectFit,
+ objectPosition,
+ src,
+ width,
+ height,
+ layout,
+ placeholder,
+ blurDataURL,
+ ...restOfTheProps
+ } = originalRestOfTheProps; // Destructured next/image props on purpose, so that unwanted props don't end up in
+
+ if (!originalSrc) {
+ return null;
+ }
+
+ const isGif = originalSrc.substr(-4) === '.gif';
+ const isHashnodeCDNImage = src.indexOf('cdn.hashnode.com') > -1;
+
+ if (isGif || !isHashnodeCDNImage) {
+ // restOfTheProps will contain all props excluding the next/image props
+ return ;
+ }
+
+ // Notes we are passing whole props object here
+ return ;
+}
+
+export default CustomImage;
diff --git a/packages/blog-starter-kit/themes/enterprise/components/date-formatter.tsx b/packages/blog-starter-kit/themes/enterprise/components/date-formatter.tsx
index 00129ab31..b98f7e052 100644
--- a/packages/blog-starter-kit/themes/enterprise/components/date-formatter.tsx
+++ b/packages/blog-starter-kit/themes/enterprise/components/date-formatter.tsx
@@ -8,5 +8,9 @@ export const DateFormatter = ({ dateString }: Props) => {
if (!dateString) return <>>;
const date = parseISO(dateString);
- return {format(date, 'LLL d, yyyy')} ;
+ return (
+ <>
+ {format(date, 'LLL d, yyyy')}
+ >
+ );
};
diff --git a/packages/blog-starter-kit/themes/enterprise/components/footer.tsx b/packages/blog-starter-kit/themes/enterprise/components/footer.tsx
index b373dd40b..bc2e91412 100644
--- a/packages/blog-starter-kit/themes/enterprise/components/footer.tsx
+++ b/packages/blog-starter-kit/themes/enterprise/components/footer.tsx
@@ -20,7 +20,7 @@ export const Footer = () => {
) : (
-
+
{publication.title}
)}
diff --git a/packages/blog-starter-kit/themes/enterprise/components/header.tsx b/packages/blog-starter-kit/themes/enterprise/components/header.tsx
index caf450837..136c48523 100644
--- a/packages/blog-starter-kit/themes/enterprise/components/header.tsx
+++ b/packages/blog-starter-kit/themes/enterprise/components/header.tsx
@@ -1,10 +1,12 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
-import { resizeImage } from '@starter-kit/utils/image';
-import Link from 'next/link';
+import { useState } from 'react';
import { PublicationNavbarItem } from '../generated/graphql';
import { Button } from './button';
import { Container } from './container';
import { useAppContext } from './contexts/appContext';
+import HamburgerSVG from './icons/svgs/HamburgerSVG';
+import { PublicationLogo } from './publication-logo';
+import PublicationSidebar from './sidebar';
function hasUrl(
navbarItem: PublicationNavbarItem,
@@ -14,12 +16,16 @@ function hasUrl(
export const Header = () => {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || '/';
+ const [isSidebarVisible, setIsSidebarVisible] = useState();
const { publication } = useAppContext();
- const PUBLICATION_LOGO = publication.preferences.darkMode?.logo || publication.preferences.logo;
const navbarItems = publication.preferences.navbarItems.filter(hasUrl);
const visibleItems = navbarItems.slice(0, 3);
const hiddenItems = navbarItems.slice(3);
+ const toggleSidebar = () => {
+ setIsSidebarVisible((prevVisibility) => !prevVisibility);
+ };
+
const navList = (
{visibleItems.map((item) => (
@@ -74,42 +80,31 @@ export const Header = () => {
- {/*
- }
- className="!px-3 !py-2 text-white border-transparent rounded-xl hover:bg-neutral-800"
- />
-
*/}
-
-
- {PUBLICATION_LOGO ? (
- <>
-
- Blog
- >
- ) : (
-
- {publication.title}
-
- )}
-
-
+
+
}
+ className="rounded-xl border-transparent !px-3 !py-2 text-white hover:bg-slate-900 dark:hover:bg-neutral-800"
+ onClick={toggleSidebar}
+ />
+
+ {isSidebarVisible && (
+
+ )}
+
+
{navList}
+
);
};
diff --git a/packages/blog-starter-kit/themes/enterprise/components/hero-post.tsx b/packages/blog-starter-kit/themes/enterprise/components/hero-post.tsx
index 5c2bcfddd..6ad94e290 100644
--- a/packages/blog-starter-kit/themes/enterprise/components/hero-post.tsx
+++ b/packages/blog-starter-kit/themes/enterprise/components/hero-post.tsx
@@ -1,5 +1,6 @@
import { resizeImage } from '@starter-kit/utils/image';
import Link from 'next/link';
+import { DEFAULT_COVER } from '../utils/const';
import { CoverImage } from './cover-image';
import { DateFormatter } from './date-formatter';
@@ -11,9 +12,6 @@ type Props = {
slug: string;
};
-const DEFAULT_COVER =
- 'https://cdn.hashnode.com/res/hashnode/image/upload/v1683525272978/MB5H_kgOC.png?auto=format';
-
export const HeroPost = ({ title, coverImage, date, excerpt, slug }: Props) => {
const postURL = `/${slug}`;
diff --git a/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/BookOpenSVG.js b/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/BookOpenSVG.js
new file mode 100644
index 000000000..6fb9be0fd
--- /dev/null
+++ b/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/BookOpenSVG.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default class BookOpenSVG extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/CloseSVG.js b/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/CloseSVG.js
new file mode 100644
index 000000000..3de37bfe5
--- /dev/null
+++ b/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/CloseSVG.js
@@ -0,0 +1,11 @@
+import React from 'react';
+
+export default class CloseSVG extends React.Component {
+ render() {
+ return (
+
+
+
+ );
+ }
+}
diff --git a/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/HamburgerSVG.js b/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/HamburgerSVG.js
index 7256df073..38cc39f81 100644
--- a/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/HamburgerSVG.js
+++ b/packages/blog-starter-kit/themes/enterprise/components/icons/svgs/HamburgerSVG.js
@@ -6,7 +6,7 @@ export default class HamburgerSVG extends React.Component {
{
// @ts-ignore
window.gtag('config', gaTrackingID, {
@@ -99,6 +126,21 @@ export function Integrations() {
dangerouslySetInnerHTML={{ __html: matomoAnalytics }}
>
)}
+ {gTagManagerID && (
+
+ )}
+ {koalaForUsers && (
+
+ )}
+ {msClarityForUsers && (
+
+ )}
{plausibleAnalyticsEnabled && (
+ ) : null}
+ {fathomSiteID && (
+
+ )}
+ {hotjarSiteID && hotjarForUsers && (
+
+ )}
+ {matomoURL && (
+
+ )}
+ {gTagManagerID && (
+
+ )}
+ {koalaForUsers && (
+
+ )}
+ {msClarityForUsers && (
+
+ )}
+ {plausibleAnalyticsEnabled && (
+
+ )}
+ >
+ );
+}
diff --git a/packages/blog-starter-kit/themes/hashnode/components/layout.tsx b/packages/blog-starter-kit/themes/hashnode/components/layout.tsx
new file mode 100644
index 000000000..039f9e30f
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/layout.tsx
@@ -0,0 +1,22 @@
+import { Analytics } from './analytics';
+import { Integrations } from './integrations';
+import { Meta } from './meta';
+import { Scripts } from './scripts';
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export const Layout = ({ children }: Props) => {
+ return (
+ <>
+
+
+
+ {children}
+
+
+
+ >
+ );
+};
diff --git a/packages/blog-starter-kit/themes/hashnode/components/magazine-blog-post-preview.tsx b/packages/blog-starter-kit/themes/hashnode/components/magazine-blog-post-preview.tsx
new file mode 100644
index 000000000..412642b76
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/magazine-blog-post-preview.tsx
@@ -0,0 +1,113 @@
+import Link from 'next/link';
+
+import CustomImage from './custom-image';
+import { BookOpenSVG, ChartMixedSVG } from './icons/svgs';
+import { getDefaultPostCoverImageUrl } from '../utils/commonUtils';
+import { blurImageDimensions } from '../utils/const/images';
+import { getBlurHash, resizeImage } from '../utils/image';
+import { kFormatter } from '../utils/image';
+import { PostThumbnailFragment, PublicationFragment } from '../generated/graphql';
+
+function BlogPostPreview(props: {
+ post: PostThumbnailFragment;
+ publication: Pick;
+}) {
+ const { post, publication } = props;
+ const postURL = `/${post.slug}`;
+ const postCoverImageURL = post.coverImage?.url ?? getDefaultPostCoverImageUrl();
+
+ const preload = async () => {
+ const nextData = document.getElementById('__NEXT_DATA__');
+ if (nextData) {
+ const { buildId } = JSON.parse(nextData.innerHTML);
+ if (buildId) {
+ fetch(`/_next/data/${buildId}/${post.slug}.json?slug=${post.slug}`);
+ }
+ }
+ };
+
+ return (
+
+
undefined}
+ aria-label={`Cover photo of the article titled ${post.title}`}
+ className="mb-4 block w-full overflow-hidden rounded-lg border bg-slate-100 hover:opacity-90 dark:border-slate-800 dark:bg-slate-800"
+ >
+
+
+
+ undefined}>
+ {post.title}
+
+
+
+
+
undefined}
+ >
+ {post.author.name}
+
+
+ {publication.features.readTime.isEnabled && post.readTimeInMinutes ? (
+ <>
+
+ undefined}
+ >
+
+ {post.readTimeInMinutes} min read
+
+
+ >
+ ) : null}
+ {post.readTimeInMinutes && Number(post.views) > 0 && publication.features.viewCount.isEnabled ? (
+
·
+ ) : null}
+ {Number(post.views) > 0 && publication.features.viewCount.isEnabled ? (
+
+ undefined}
+ >
+
+ {kFormatter(post.views)} views
+
+
+ ) : null}
+
+
+
+
+ );
+}
+
+export default BlogPostPreview;
diff --git a/packages/blog-starter-kit/themes/hashnode/components/markdown-styles.module.css b/packages/blog-starter-kit/themes/hashnode/components/markdown-styles.module.css
new file mode 100644
index 000000000..95d4f8b04
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/markdown-styles.module.css
@@ -0,0 +1,18 @@
+.markdown {
+ @apply text-lg leading-relaxed;
+}
+
+.markdown p,
+.markdown ul,
+.markdown ol,
+.markdown blockquote {
+ @apply my-6;
+}
+
+.markdown h2 {
+ @apply text-3xl mt-12 mb-4 leading-snug;
+}
+
+.markdown h3 {
+ @apply text-2xl mt-8 mb-4 leading-snug;
+}
diff --git a/packages/blog-starter-kit/themes/hashnode/components/meta.tsx b/packages/blog-starter-kit/themes/hashnode/components/meta.tsx
new file mode 100644
index 000000000..578c3a741
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/meta.tsx
@@ -0,0 +1,28 @@
+import parse from 'html-react-parser';
+import Head from 'next/head';
+
+import { useAppContext } from './contexts/appContext';
+
+export const Meta = () => {
+ const { publication } = useAppContext();
+ const { metaTags, favicon } = publication;
+ const defaultFavicons = (
+ <>
+
+
+
+
+
+
+ >
+ );
+
+ return (
+
+ {favicon ? : defaultFavicons}
+
+
+ {metaTags && parse(metaTags)}
+
+ );
+};
diff --git a/packages/blog-starter-kit/themes/hashnode/components/modern-layout-posts.tsx b/packages/blog-starter-kit/themes/hashnode/components/modern-layout-posts.tsx
new file mode 100644
index 000000000..de2335d8f
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/modern-layout-posts.tsx
@@ -0,0 +1,90 @@
+import { Waypoint } from 'react-waypoint';
+
+import Button from './hn-button';
+import { ChevronDownSVG } from './icons/svgs';
+import { PageInfo, PublicationFragment, PostThumbnailFragment } from '../generated/graphql';
+import BlogPostPreview from './magazine-blog-post-preview';
+
+const PublicationPosts = (props: {
+ posts: {
+ edges: Array<{
+ cursor: string;
+ node: PostThumbnailFragment;
+ }>;
+ pageInfo: Pick;
+ };
+ publication: Pick;
+ fetchMore: () => void;
+ fetching: boolean;
+ fetchedOnce: boolean;
+}) => {
+ const { posts, publication, fetchMore, fetching, fetchedOnce } = props;
+ const { edges, pageInfo } = posts;
+
+ const slicedPosts = edges.map((edge) => edge.node).slice(3);
+
+ return (
+
+
+ {slicedPosts.map((post) => (
+
+ ))}
+ {fetching && (
+ <>
+
+
+
+ >
+ )}
+ {pageInfo.hasNextPage && !fetchedOnce && !fetching ? (
+
+
+ Load more
+
+
+
+ ) : null}
+
+ {fetchedOnce && pageInfo.hasNextPage ?
: null}
+ {fetchedOnce && !pageInfo.hasNextPage ? (
+
+
You've reached the end! 👋
+
+ ) : null}
+
+ );
+};
+
+export default PublicationPosts;
diff --git a/packages/blog-starter-kit/themes/hashnode/components/other-posts-of-account.tsx b/packages/blog-starter-kit/themes/hashnode/components/other-posts-of-account.tsx
new file mode 100644
index 000000000..1308b7f0e
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/other-posts-of-account.tsx
@@ -0,0 +1,110 @@
+import { twJoin } from 'tailwind-merge';
+import Link from 'next/link';
+import { resizeImage } from '../utils/image';
+import ProfileImage from './profile-image';
+import CustomImage from './custom-image';
+import { PostFullFragment } from '../generated/graphql';
+
+
+type Props = {
+ post: PostFullFragment;
+ morePosts: any
+};
+
+function OtherPostsOfAccount(props: Props) {
+ const { morePosts, post } = props;
+
+ if (!morePosts || morePosts.length === 0) {
+ return
;
+ }
+
+ const morePostsRendered = morePosts.map((postNode: any) => {
+ const post = postNode.node;
+ const postURL = `/${post.slug}`;
+ return (
+
+
+ {post.author && (
+
+ )}
+ {post.coverImage && (
+
+
+
+ )}
+
+
+
+ {post.title.substring(0, 100)}
+ {post.title.length > 100 ? '…' : ''}
+
+
+ {post.brief && (
+
+
+ {post.brief.substring(0, 100)}
+ {post.brief.length > 100 ? '…' : ''}
+
+
+ )}
+
+
+
+ );
+ });
+
+ return (
+
+
+ {true ? 'More articles' : `More Stories by ${post.author.name}`}
+
+
+ {morePostsRendered}
+
+
+ );
+}
+
+
+
+export default OtherPostsOfAccount;
diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-author-info.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-author-info.tsx
new file mode 100644
index 000000000..0b806ea2c
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/post-author-info.tsx
@@ -0,0 +1,71 @@
+import { twJoin } from 'tailwind-merge';
+
+import CustomImage from './custom-image';
+import { getBlurHash, resizeImage } from '../utils/image';
+
+function PostAuthorInfo(props: any) {
+ const {
+ author,
+ } = props;
+
+ return (
+
+
+
+
+
+ {author.bio?.html && (
+
+ )}
+
+
+ {author.bio?.html && (
+
+ )}
+
+ );
+}
+
+export default PostAuthorInfo;
diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-comments-sidebar.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-comments-sidebar.tsx
new file mode 100644
index 000000000..79bfd02b8
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/post-comments-sidebar.tsx
@@ -0,0 +1,31 @@
+import { PostFullFragment } from '../generated/graphql';
+import CommentsSheet from './comments-sheet';
+import ResponseList from './response-list';
+
+const PostCommentsSidebar = ({
+ hideSidebar,
+ isPublicationPost,
+ selectedFilter,
+ post,
+}: {
+ hideSidebar: () => void;
+ isPublicationPost: boolean;
+ selectedFilter: string;
+ post: PostFullFragment;
+}) => (
+
+
+ {!post.preferences.disableComments ? (
+
+ ) : (
+
+
The comments have been disabled by the author for this article
+
+ )}
+
+);
+
+export default PostCommentsSidebar;
diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-comments.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-comments.tsx
new file mode 100644
index 000000000..87b61eb96
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/post-comments.tsx
@@ -0,0 +1,171 @@
+import moment from 'dayjs';
+
+import localizedFormat from 'dayjs/plugin/localizedFormat';
+import relativeTime from 'dayjs/plugin/relativeTime';
+
+import { twJoin } from 'tailwind-merge';
+import { formatDate } from '../utils';
+import Autolinker from '../utils/autolinker';
+import { imageReplacer } from '../utils/image';
+import { useAppContext } from './contexts/appContext';
+import { Button } from './custom-button';
+import { ExternalArrowSVG, HashnodeSVG } from './icons';
+import ProfileImage from './profile-image';
+import ResponseFooter from './response-footer';
+
+moment.extend(relativeTime);
+moment.extend(localizedFormat);
+
+export const PostComments = () => {
+ const { post } = useAppContext();
+ if (!post) return null;
+ const discussionUrl = `https://hashnode.com/discussions/post/${post.id}`;
+ const checkIfCommentByAuthor = (comment: any) => {
+ return comment.author.id.toString() === post.author.id.toString();
+ };
+
+ const loadProfile = (e: any, comment: any) => {
+ e.preventDefault();
+ const isPublication = true;
+ const url = `${isPublication ? 'https://hashnode.com/@' : '/@'}${comment.author.username}`;
+ if (isPublication) {
+ window.location.href = url;
+ }
+ return null;
+ // Router.push(url);
+ };
+
+ const noop = (e: any) => {
+ e.preventDefault();
+ };
+
+ const commentsList = post.comments.edges.map((edge) => {
+ const comment = edge.node as any;
+ return (
+
+
+
string; getTwitterHandle: () => void },
+ ) {
+ switch (match.getType()) {
+ case 'twitter':
+ // eslint-disable-next-line no-case-declarations
+ const username = match.getTwitterHandle();
+ return `
@${username} `;
+ default:
+ return null;
+ }
+ },
+ }),
+ ),
+ }}
+ />
+
+
+ );
+ });
+
+ return (
+
+ );
+};
diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar-tooltip-wrapper.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar-tooltip-wrapper.tsx
new file mode 100644
index 000000000..bddbedf4b
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar-tooltip-wrapper.tsx
@@ -0,0 +1,36 @@
+import * as Tooltip from '@radix-ui/react-tooltip';
+import React from 'react';
+import { twMerge } from 'tailwind-merge';
+
+export default function PostFloatingBarTooltipWrapper({
+ children,
+ label,
+ asChild = true,
+ labelSide = 'top',
+ contentClassName = '',
+ delayDuration,
+}: {
+ children: React.ReactChild;
+ label: string;
+ asChild?: boolean;
+ labelSide?: 'top' | 'right' | 'bottom' | 'left';
+ contentClassName?: string;
+ delayDuration?: number;
+}) {
+ return (
+
+
+ {children}
+
+
+
+ {label}
+
+
+
+ );
+}
diff --git a/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar.tsx b/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar.tsx
new file mode 100644
index 000000000..e5d41d1bc
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/components/post-floating-bar.tsx
@@ -0,0 +1,144 @@
+import * as Tooltip from '@radix-ui/react-tooltip';
+import { useEffect } from 'react';
+
+import { CommentSVGV2 } from './icons/svgs';
+import { kFormatter } from '../utils/image';
+import { Separator } from './separator-root';
+import PostFloatingBarTooltipWrapper from './post-floating-bar-tooltip-wrapper';
+import { PostFullFragment } from '../generated/graphql';
+import TocSheet from './toc-sheet';
+import PostShareWidget from './post-share-widget';
+
+
+function PostFloatingMenu(props: {
+ isPublicationPost: boolean;
+ post: PostFullFragment;
+ shareText: string;
+ showPaymentModal?: () => void;
+ openComments?: () => void;
+ list: any[];
+}) {
+ const {
+ isPublicationPost,
+ post,
+ shareText,
+ showPaymentModal,
+ openComments,
+ list,
+ } = props;
+
+ const handleFloatingBarDisplay = () => {
+ const blogHeader = document.querySelector('.blog-header');
+ const blogContent = document.querySelector('#post-content-parent');
+ const floatingBar = document.querySelector('.post-floating-bar');
+
+ if (!floatingBar?.classList.contains('freeze')) {
+ if (window.scrollY > blogHeader!.clientHeight) {
+ floatingBar?.classList.add('active', 'animation');
+ } else if (floatingBar?.classList.contains('active')) {
+ floatingBar?.classList.remove('active');
+ }
+ }
+
+ const currentViewportHeight = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
+ // Adding 40 as a buffer to adjust the trigger
+ const isPostContentBottomInsideViewport = blogContent!.getBoundingClientRect().bottom + 40 <= currentViewportHeight;
+ // Adding 175 as a buffer to adjust the trigger
+ const isPostContentBottomAlmostOut =
+ window.scrollY - currentViewportHeight - 175 <= blogContent!.clientHeight &&
+ floatingBar?.classList.contains('freeze');
+
+ if (isPostContentBottomInsideViewport) {
+ floatingBar?.classList.remove('active');
+ floatingBar?.classList.add('freeze');
+ } else if (isPostContentBottomAlmostOut) {
+ floatingBar?.classList.remove('freeze', 'animation');
+ floatingBar?.classList.add('active');
+ }
+ };
+
+ useEffect(() => {
+ handleFloatingBarDisplay();
+ window.addEventListener('scroll', handleFloatingBarDisplay);
+ return () => {
+ window.removeEventListener('scroll', handleFloatingBarDisplay);
+ };
+ }, []);
+
+ // Best practice to have the accessible name being with the visible text (comment count)
+ const commentBtnAccessibleLabel =
+ post?.responseCount > 0
+ ? `${kFormatter(post.responseCount + (post.replyCount || 0))} comment${
+ post.responseCount === 1 ? '' : 's'
+ }, open the comments`
+ : 'Open comments';
+
+ return (
+
+
+
+
+ >
+ );
+};
+
+const Page = ({ page }: PageProps) => {
+ const title = page.title;
+ return (
+ <>
+
+ {title}
+
+
+
+
+ >
+ );
+};
+
+export default function PostOrPage(props: Props) {
+ const headerRef = useRef(null);
+ const maybePost = props.type === 'post' ? props.post : null;
+ const maybePage = props.type === 'page' ? props.page : null;
+ const publication = props.publication;
+ const navPositionStyles =
+ 'relative transform-none md:sticky md:top-0 md:left-0 md:backdrop-blur-lg';
+
+ if (props.type === 'post') {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ const description =
+ publication.descriptionSEO || publication.title || `${publication.author.name}'s Blog`;
+
+ return (
+
+
+
+
+ {publication.displayTitle || publication.title || 'Hashnode Blog Starter Kit'}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+type Params = {
+ slug: string;
+};
+
+export const getStaticProps: GetStaticProps = async ({ params }) => {
+ if (!params) {
+ throw new Error('No params');
+ }
+
+ const endpoint = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT;
+ const host = process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST;
+ const slug = params.slug;
+
+ const [postData, morePostsData] = await Promise.all([
+ request(endpoint, SinglePostByPublicationDocument, { host, slug }),
+ request(endpoint, MorePostsByPublicationDocument, { first: 4, host }),
+ ]);
+
+ if (postData.publication?.post) {
+ return {
+ props: {
+ type: 'post',
+ post: postData.publication.post,
+ morePosts: morePostsData.publication?.posts.edges ?? [],
+ publication: postData.publication,
+ },
+ revalidate: 1,
+ };
+ }
+
+ const pageData = await request(endpoint, PageByPublicationDocument, { host, slug });
+
+ if (pageData.publication?.staticPage) {
+ return {
+ props: {
+ type: 'page',
+ page: pageData.publication.staticPage,
+ publication: pageData.publication,
+ },
+ revalidate: 1,
+ };
+ }
+
+ return {
+ notFound: true,
+ revalidate: 1,
+ };
+};
+
+export const getStaticPaths: GetStaticPaths = async () => {
+ const data = await request(
+ process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT,
+ SlugPostsByPublicationDocument,
+ {
+ first: 10,
+ host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
+ },
+ );
+
+ const postSlugs = (data.publication?.posts.edges ?? []).map((edge) => edge.node.slug);
+
+ return {
+ paths: postSlugs.map((slug) => {
+ return {
+ params: {
+ slug: slug,
+ },
+ };
+ }),
+ fallback: 'blocking',
+ };
+};
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/_app.tsx b/packages/blog-starter-kit/themes/hashnode/pages/_app.tsx
new file mode 100644
index 000000000..1bb0a93c4
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/_app.tsx
@@ -0,0 +1,31 @@
+import { withUrqlClient } from 'next-urql';
+import { AppProps } from 'next/app';
+import { useEffect } from 'react';
+import 'tailwindcss/tailwind.css';
+
+import { GlobalFontVariables } from '../components/fonts';
+import { getUrqlClientConfig } from '../lib/api/client';
+import '../styles/index.css';
+
+import { Fragment } from 'react';
+
+function MyApp({ Component, pageProps }: AppProps) {
+ useEffect(() => {
+ (window as any).adjustIframeSize = (id: string, newHeight: string) => {
+ const i = document.getElementById(id);
+ if (!i) return;
+ // eslint-disable-next-line radix
+ i.style.height = `${parseInt(newHeight)}px`;
+ };
+ }, []);
+ return (
+
+
+
+
+ );
+}
+
+// `withUrqlClient` HOC provides the `urqlClient` prop and takes care of restoring cache from urqlState
+// this will provide ssr cache to the provider and enable to use `useQuery` hook on the client side
+export default withUrqlClient(getUrqlClientConfig, { neverSuspend: true })(MyApp);
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/_document.tsx b/packages/blog-starter-kit/themes/hashnode/pages/_document.tsx
new file mode 100644
index 000000000..a01818047
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/_document.tsx
@@ -0,0 +1,14 @@
+import { Head, Html, Main, NextScript } from 'next/document';
+
+export default function Document() {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/api/og/home.tsx b/packages/blog-starter-kit/themes/hashnode/pages/api/og/home.tsx
new file mode 100644
index 000000000..a5f265ef7
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/api/og/home.tsx
@@ -0,0 +1,226 @@
+import { resizeImage } from '@starter-kit/utils/image';
+import { ImageResponse } from '@vercel/og';
+import { type NextRequest } from 'next/server';
+import { DEFAULT_AVATAR } from '../../../utils/const';
+
+export const config = {
+ runtime: 'edge',
+};
+
+const fontRegular = fetch(
+ new URL('../../../assets/PlusJakartaSans-Regular.ttf', import.meta.url),
+).then((res) => res.arrayBuffer());
+
+const fontMedium = fetch(
+ new URL('../../../assets/PlusJakartaSans-Medium.ttf', import.meta.url),
+).then((res) => res.arrayBuffer());
+
+const fontSemiBold = fetch(
+ new URL('../../../assets/PlusJakartaSans-SemiBold.ttf', import.meta.url),
+).then((res) => res.arrayBuffer());
+
+const fontBold = fetch(new URL('../../../assets/PlusJakartaSans-Bold.ttf', import.meta.url)).then(
+ (res) => res.arrayBuffer(),
+);
+
+const fontExtraBold = fetch(
+ new URL('../../../assets/PlusJakartaSans-ExtraBold.ttf', import.meta.url),
+).then((res) => res.arrayBuffer());
+
+const kFormatter = (num: number) => {
+ return num > 999 ? `${(num / 1000).toFixed(1)}K` : num;
+};
+
+export default async function handler(req: NextRequest) {
+ const [fontDataRegular, fontDataMedium, fontDataSemiBold, fontDataBold, fontDataExtraBold] =
+ await Promise.all([fontRegular, fontMedium, fontSemiBold, fontBold, fontExtraBold]);
+
+ const { searchParams } = new URL(req.url);
+
+ const ogData = JSON.parse(atob(searchParams.get('og') as string));
+ const {
+ title: encodedTitle,
+ photo: userPhoto,
+ logo,
+ isTeam,
+ domain,
+ meta: encodedMeta,
+ followers,
+ articles,
+ favicon,
+ } = ogData;
+
+ const title = decodeURIComponent(encodedTitle);
+
+ let meta;
+ if (encodedMeta) {
+ meta = decodeURIComponent(encodedMeta);
+ }
+
+ const bannerBackground = '#f1f5f9';
+ const photo = userPhoto || DEFAULT_AVATAR;
+
+ return new ImageResponse(
+ (
+
+ {/* PERSONAL BLOG The following parent div is for personal blogs */}
+ {/* if the site is set to open in dark mode by default, change text-black to text-white and bg-white to bg-black */}
+ {!isTeam && (
+
+
+
+
+
+
+
+ {/* Either show the Site title below or Site logo depending on whether a blog has a logo or not */}
+
+ {/* Site title */}
+ {!logo && title &&
{title}
}
+
+ {/* Site Logo - load dark logo only if the site is set to open in dark mode */}
+ {logo ? (
+
+ ) : null}
+
+ {/* Show domain name */}
+
{domain}
+
+ {/* If blog's about me is not available hide this p tag */}
+ {meta && (
+
+ {meta}
+
+ )}
+
+ {/* If no of followers is zero hide this p tag */}
+ {followers > 0 && (
+
+ {kFormatter(followers)}
+ follower{followers === 1 ? '' : 's'}
+
+ )}
+ {/* If no of articles are zero, hide this p tag */}
+ {articles > 0 && (
+
+ {kFormatter(articles)}
+ article{articles === 1 ? '' : 's'}
+
+ )}
+
+
+
+
+ )}
+
+ {/* TEAM BLOG The following parent div is for team blogs */}
+ {/* if the site is set to open in dark mode by default, change text-black to text-white and bg-white to bg-black */}
+ {isTeam && (
+
+
+
+ {/* Show the following if the team doesn't have a logo and has a thumbnail/favicon */}
+ {!logo && favicon && (
+
+
+
+ )}
+
+ {/* Either show the Site title below or Site logo depending on whether a blog has a logo or not */}
+
+ {/* Site title */}
+ {!logo && title &&
{title}
}
+
+ {/* Site Logo */}
+ {logo ?
: null}
+
+ {/* Show domain name */}
+
{domain}
+
+ {/* If blog's about me is not available hide this p tag */}
+ {meta && (
+
+ {meta}
+
+ )}
+
+ {/* If no of followers is zero hide this p tag */}
+ {followers > 0 && (
+
+ {kFormatter(followers)}
+ follower{followers === 1 ? '' : 's'}
+
+ )}
+ {/* If no of articles are zero, hide this p tag */}
+ {articles > 0 && (
+
+ {kFormatter(articles)}
+ article{articles === 1 ? '' : 's'}
+
+ )}
+
+
+
+
+ )}
+
+ ),
+ {
+ width: 1200,
+ height: 630,
+ fonts: [
+ {
+ name: 'Typewriter',
+ data: fontDataRegular,
+ style: 'normal',
+ weight: 400,
+ },
+ {
+ name: 'Typewriter',
+ data: fontDataMedium,
+ style: 'normal',
+ weight: 500,
+ },
+ {
+ name: 'Typewriter',
+ data: fontDataSemiBold,
+ style: 'normal',
+ weight: 600,
+ },
+ {
+ name: 'Typewriter',
+ data: fontDataBold,
+ style: 'normal',
+ weight: 700,
+ },
+ {
+ name: 'Typewriter',
+ data: fontDataExtraBold,
+ style: 'normal',
+ weight: 800,
+ },
+ ],
+ },
+ );
+}
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/api/og/post.tsx b/packages/blog-starter-kit/themes/hashnode/pages/api/og/post.tsx
new file mode 100644
index 000000000..78404f092
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/api/og/post.tsx
@@ -0,0 +1,196 @@
+import { resizeImage } from '@starter-kit/utils/image';
+import { ImageResponse } from '@vercel/og';
+import { type NextRequest } from 'next/server';
+import { DEFAULT_AVATAR } from '../../../utils/const';
+
+export const config = {
+ runtime: 'edge',
+};
+
+const fontRegular = fetch(
+ new URL('../../../assets/PlusJakartaSans-Regular.ttf', import.meta.url),
+).then((res) => res.arrayBuffer());
+
+const fontMedium = fetch(
+ new URL('../../../assets/PlusJakartaSans-Medium.ttf', import.meta.url),
+).then((res) => res.arrayBuffer());
+
+const fontSemiBold = fetch(
+ new URL('../../../assets/PlusJakartaSans-SemiBold.ttf', import.meta.url),
+).then((res) => res.arrayBuffer());
+
+const fontBold = fetch(new URL('../../../assets/PlusJakartaSans-Bold.ttf', import.meta.url)).then(
+ (res) => res.arrayBuffer(),
+);
+
+const fontExtraBold = fetch(
+ new URL('../../../assets/PlusJakartaSans-ExtraBold.ttf', import.meta.url),
+).then((res) => res.arrayBuffer());
+
+export default async function handler(req: NextRequest) {
+ const [fontDataRegular, fontDataMedium, fontDataSemiBold, fontDataBold, fontDataExtraBold] =
+ await Promise.all([fontRegular, fontMedium, fontSemiBold, fontBold, fontExtraBold]);
+
+ const { searchParams } = new URL(req.url);
+
+ const ogData = JSON.parse(atob(searchParams.get('og') as string));
+ const {
+ author: authorEncoded,
+ isDefaultModeDark,
+ readTime,
+ comments,
+ reactions,
+ domain,
+ title: encodedTitle,
+ bgcolor,
+ } = ogData;
+ const author = decodeURIComponent(authorEncoded);
+ const photo = ogData.photo ? resizeImage(ogData.photo, {}) : DEFAULT_AVATAR;
+ const title = decodeURIComponent(encodedTitle);
+
+ let bannerBackground = isDefaultModeDark ? '#0f172a' : '#f3f4f6';
+ let titleTailwindClass;
+
+ if (bgcolor) {
+ bannerBackground = bgcolor;
+ }
+
+ if (title.length <= 79) {
+ titleTailwindClass = 'text-7xl';
+ } else if (title.length > 79 && title.length <= 109) {
+ titleTailwindClass = 'text-6xl';
+ } else {
+ titleTailwindClass = 'text-5xl';
+ }
+
+ return new ImageResponse(
+ (
+
+ {/* if blog is set to open in dark mode, then change text-slate-900 to text-white and change bg-white to bg-black */}
+
+
+
+ {/* if author image is not available, use the default author image (DEFAULT_AVATAR) from const */}
+
+
+ {/* Author name, even if it's team */}
+
{author}
+
+ {/* Show custom domain, then hashnode.dev domain */}
+ {/* Show team domain, if team */}
+
{domain}
+
+
+
+ {/* if title char count is >= 110 change the tailwind text-* class to text-5xl */}
+ {/* if title char count is 80 - 109 change the tailwind text-* class to text-6xl */}
+ {/* if title char count is <=79 change the tailwind text-* class to text-7xl */}
+
+ {title}
+
+
+
+
+ {reactions && (
+
+ )}
+ {comments && (
+
+ )}
+
+ {readTime && (
+
+
+
+
+
{readTime} min read
+
+ )}
+
+
+
+ ),
+ {
+ width: 1200,
+ height: 630,
+ fonts: [
+ {
+ name: 'Typewriter',
+ data: fontDataRegular,
+ style: 'normal',
+ weight: 400,
+ },
+ {
+ name: 'Typewriter',
+ data: fontDataMedium,
+ style: 'normal',
+ weight: 500,
+ },
+ {
+ name: 'Typewriter',
+ data: fontDataSemiBold,
+ style: 'normal',
+ weight: 600,
+ },
+ {
+ name: 'Typewriter',
+ data: fontDataBold,
+ style: 'normal',
+ weight: 700,
+ },
+ {
+ name: 'Typewriter',
+ data: fontDataExtraBold,
+ style: 'normal',
+ weight: 800,
+ },
+ ],
+ },
+ );
+}
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/dashboard.tsx b/packages/blog-starter-kit/themes/hashnode/pages/dashboard.tsx
new file mode 100644
index 000000000..e73020d90
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/dashboard.tsx
@@ -0,0 +1,36 @@
+import request from 'graphql-request';
+import { GetServerSideProps } from 'next';
+import {
+ PublicationByHostDocument,
+ PublicationByHostQuery,
+ PublicationByHostQueryVariables,
+} from '../generated/graphql';
+
+const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT;
+const Dashboard = () => null;
+
+export const getServerSideProps: GetServerSideProps = async () => {
+ const data = await request(
+ GQL_ENDPOINT,
+ PublicationByHostDocument,
+ {
+ host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
+ },
+ );
+
+ const publication = data.publication;
+ if (!publication) {
+ return {
+ notFound: true,
+ };
+ }
+
+ return {
+ redirect: {
+ destination: `https://hashnode.com/${publication.id}/dashboard`,
+ permanent: false,
+ },
+ };
+};
+
+export default Dashboard;
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/index.tsx b/packages/blog-starter-kit/themes/hashnode/pages/index.tsx
new file mode 100644
index 000000000..21d6762cf
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/index.tsx
@@ -0,0 +1,278 @@
+import { InferGetStaticPropsType } from 'next';
+import { WithUrqlProps, initUrqlClient } from 'next-urql';
+import Head from 'next/head';
+import Image from 'next/legacy/image';
+import { useState } from 'react';
+import { twJoin } from 'tailwind-merge';
+import { useQuery } from 'urql';
+
+import { addPublicationJsonLd } from '@starter-kit/utils/seo/addPublicationJsonLd';
+import { getAutogeneratedPublicationOG } from '@starter-kit/utils/social/og';
+import { AppProvider } from '../components/contexts/appContext';
+import { Header } from '../components/header';
+import { Layout } from '../components/layout';
+import ModernLayoutPosts from '../components/modern-layout-posts';
+import {
+ HomePageInitialDocument,
+ HomePageInitialQueryVariables,
+ HomePagePostsDocument,
+ HomePagePostsQueryVariables,
+} from '../generated/graphql';
+import { createHeaders, createSSRExchange, getUrqlClientConfig } from '../lib/api/client';
+
+import FeaturedPosts from '../components/features-posts';
+
+import PublicationFooter from '../components/publication-footer';
+import PublicationMeta from '../components/publication-meta';
+import { resizeImage } from '../utils/image';
+
+const REVALIDATION_INTERVAL_POST_VIEWS_ACTIVE = 60 * 60; // 1 hour
+const REVALIDATION_INTERVAL = 60 * 60 * 24 * 30; // 1 month
+
+const NoPostsImage = ({ alt = '' }) => {
+ return (
+
+ );
+};
+
+export default function Index(
+ props: InferGetStaticPropsType & Required,
+) {
+ const { host, publication, initialLimit } = props;
+
+ const ssrCache = createSSRExchange();
+ const urqlClient = initUrqlClient(getUrqlClientConfig(ssrCache), false); // TODO: Check why is urqlClient not automatically being passed in props. Ideally, since we are using WithUrqlClient HOC, it should automatically come
+
+ const [fetching, setFetching] = useState(false);
+
+ const { author, preferences, pinnedPost } = publication;
+ const dynamicLimit = preferences.layout === 'magazine' ? 12 : 6;
+
+ const [{ data }] = useQuery({
+ query: HomePagePostsDocument,
+ variables: { host, first: initialLimit, filter: { excludePinnedPost: !!pinnedPost } },
+ });
+
+ const { posts } = data?.publication!;
+
+ const fetchedOnce = posts.edges.length > initialLimit;
+
+ const postsToBeRendered = {
+ edges: pinnedPost
+ ? [{ node: pinnedPost, cursor: `${pinnedPost.id}_${pinnedPost.publishedAt}` }].concat(
+ posts.edges,
+ )
+ : posts.edges,
+ pageInfo: posts.pageInfo,
+ };
+
+ const fetchMore = async () => {
+ setFetching(true);
+ await urqlClient
+ .query(HomePagePostsDocument, {
+ host,
+ first: dynamicLimit,
+ after: posts.pageInfo.endCursor,
+ filter: { excludePinnedPost: !!pinnedPost },
+ })
+ .toPromise()
+ .finally(() => {
+ setFetching(false);
+ });
+ };
+
+ return (
+
+
+
+
+ {publication.displayTitle || publication.title || 'Hashnode Blog Starter Kit'}
+
+
+
+
+
+
+
+
+
+
+
+ {postsToBeRendered.edges.length > 0 ? (
+
p.node).slice(0, 3)}
+ publication={publication}
+ />
+ ) : null}
+
+ {publication.about?.html ? (
+
+ ) : null}
+
+
+
+ {postsToBeRendered.edges.length === 0 ? (
+ <>
+
+ >
+ ) : null}
+
+
+
+ {postsToBeRendered.edges.length > 3 ? (
+
+ ) : null}
+
+ {publication ? (
+
+ ) : null}
+
+
+ );
+}
+
+export const getStaticProps = async () => {
+ const ssrCache = createSSRExchange();
+ const urqlClient = initUrqlClient(getUrqlClientConfig(ssrCache), false);
+ const host = process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST;
+ const homePageInitialQueryVariables: HomePageInitialQueryVariables = {
+ host,
+ };
+ const publicationInfo = await urqlClient
+ .query(HomePageInitialDocument, homePageInitialQueryVariables, {
+ fetchOptions: {
+ headers: createHeaders({ byPassCache: false }),
+ },
+ requestPolicy: 'network-only',
+ })
+ .toPromise();
+
+ if (publicationInfo.error) {
+ console.error('Error while fetching publication info', {
+ variables: homePageInitialQueryVariables,
+ error: publicationInfo.error,
+ });
+ throw publicationInfo.error;
+ }
+ if (!publicationInfo.data?.publication) {
+ console.error('Publication not found fetching publication info; returning 404', {
+ variables: homePageInitialQueryVariables,
+ });
+ return {
+ notFound: true,
+ revalidate: REVALIDATION_INTERVAL,
+ };
+ }
+
+ const { publication } = publicationInfo.data;
+
+ const subtractValue = publication.pinnedPost ? 1 : 0;
+ const initialLimit =
+ publication.preferences.layout === 'magazine' ? 12 - subtractValue : 6 - subtractValue;
+
+ const homePagePostsVariables: HomePagePostsQueryVariables = {
+ host,
+ first: initialLimit,
+ filter: { excludePinnedPost: !!publication.pinnedPost },
+ };
+ const homePagePostsResponse = await urqlClient
+ .query(HomePagePostsDocument, homePagePostsVariables, {
+ fetchOptions: {
+ headers: createHeaders({ byPassCache: false }),
+ },
+ requestPolicy: 'network-only',
+ })
+ .toPromise();
+ if (homePagePostsResponse.error) {
+ console.error('Error while fetching home page posts', {
+ error: homePagePostsResponse.error,
+ variables: homePagePostsVariables,
+ });
+ throw homePagePostsResponse.error;
+ }
+ if (!homePagePostsResponse.data?.publication) {
+ console.error('Publication not found fetching home page posts; returning 404', {
+ variables: homePagePostsVariables,
+ });
+ return {
+ notFound: true,
+ revalidate: REVALIDATION_INTERVAL,
+ };
+ }
+
+ return {
+ props: {
+ publication,
+ initialLimit,
+ urqlState: ssrCache.extractData(),
+ host,
+ isHome: true,
+ },
+ revalidate: 1,
+ };
+};
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/newsletter.tsx b/packages/blog-starter-kit/themes/hashnode/pages/newsletter.tsx
new file mode 100644
index 000000000..83065ee86
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/newsletter.tsx
@@ -0,0 +1,161 @@
+import CustomImage from '../components/custom-image';
+import PublicationSubscribeStandOut from '../components/publication-subscribe-standout';
+import { resizeImage } from '../utils/image';
+import { AppProvider } from '../components/contexts/appContext';
+
+import BlogPostPreview from '../components/magazine-blog-post-preview';
+import {
+ NewsletterDocument,
+ NewsletterQueryVariables,
+ PostThumbnailFragment,
+ PublicationFragment,
+} from '../generated/graphql';
+import { createHeaders, createSSRExchange, getUrqlClientConfig } from '../lib/api/client';
+import { GetServerSideProps, InferGetServerSidePropsType } from 'next';
+import { log as _log } from 'next-axiom';
+import { initUrqlClient } from 'next-urql';
+import { Header } from '../components/header';
+import PublicationFooter from '../components/publication-footer';
+
+type Props = {
+ publication: PublicationFragment;
+ recent3Posts: PostThumbnailFragment[];
+ currentMenuId: string;
+}
+
+const Newsletter = (props: Props) => {
+ const { recent3Posts, publication, currentMenuId } = props;
+
+ const profile = publication.author;
+
+ const recentPosts = recent3Posts.map((post: any) => (
+
+ ));
+ const originalImageSrc =
+ publication.favicon ||
+ profile?.profilePicture ||
+ 'https://cdn.hashnode.com/res/hashnode/image/upload/v1600792675173/rY-APy9Fc.png';
+
+ const publicationImageUrl = resizeImage(originalImageSrc, { w: 400, h: 400, c: 'face' });
+
+ return (
+
+
+ );
+};
+
+export const getServerSideProps: GetServerSideProps<{
+ publication: PublicationFragment;
+ recent3Posts: PostThumbnailFragment[];
+}> = async (ctx) => {
+ const { req, res, query } = ctx;
+ const host = process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST;
+ const log = _log.with({ host });
+
+ const ssrCache = createSSRExchange();
+ const urqlClient = initUrqlClient(getUrqlClientConfig(ssrCache), false);
+
+ const gqlVariables: NewsletterQueryVariables = {
+ host,
+ slug: 'newsletter',
+ };
+ const publicationInfo = await urqlClient
+ .query(NewsletterDocument, gqlVariables, {
+ fetchOptions: {
+ headers: createHeaders(),
+ },
+ requestPolicy: 'network-only',
+ })
+ .toPromise();
+
+ if (publicationInfo.error) {
+ log.error('Error while fetching publication info', {
+ variables: gqlVariables,
+ error: publicationInfo.error,
+ });
+ throw publicationInfo.error;
+ }
+ if (!publicationInfo.data?.publication) {
+ log.error('Publication not found fetching publication info; returning 404', {
+ variables: gqlVariables,
+ });
+ res.setHeader('cache-control', 's-maxage=3, stale-while-revalidate');
+ return {
+ notFound: true,
+ };
+ }
+
+ const { publication } = publicationInfo.data;
+
+ if (!publication.preferences.enabledPages?.newsletter) {
+ return {
+ notFound: true,
+ };
+ }
+
+ if (process.env.PREVIEW_MODE) {
+ res.setHeader('cache-control', 'no-cache');
+ }
+
+ res.setHeader('Cache-Control', 's-maxage=3, stale-while-revalidate');
+
+ const isDarkTheme =
+ typeof query.isDarkTheme === 'undefined'
+ ? !!publication.preferences.darkMode?.enabled
+ : query.isDarkTheme === 'true';
+ // @ts-ignore
+ req.isDarkTheme = isDarkTheme;
+
+ return {
+ props: {
+ publication,
+ recent3Posts: publication.recentPosts.edges.map((edge) => edge.node),
+ currentMenuId: 'newsletter'
+ },
+ };
+};
+
+export default Newsletter;
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/preview/[id].tsx b/packages/blog-starter-kit/themes/hashnode/pages/preview/[id].tsx
new file mode 100644
index 000000000..de01e4ac8
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/preview/[id].tsx
@@ -0,0 +1,269 @@
+import request from 'graphql-request';
+import ErrorPage from 'next/error';
+import Head from 'next/head';
+import moment from 'dayjs';
+
+import { Container } from '../../components/container';
+import { AppProvider } from '../../components/contexts/appContext';
+import PostPageNavbar from '../../components/post-page-navbar';
+import { Layout } from '../../components/layout';
+
+import {
+ DraftByIdDocument,
+ DraftByIdQuery,
+ DraftByIdQueryVariables,
+ DraftFragment,
+ Post,
+ PublicationByHostDocument,
+ PublicationByHostQuery,
+ PublicationByHostQueryVariables,
+ PublicationFragment,
+} from '../../generated/graphql';
+import PublicationFooter from '../../components/publication-footer';
+import { useRef } from 'react';
+import { twJoin } from 'tailwind-merge';
+import CustomImage from '../../components/custom-image';
+import { getBlurHash, imageReplacer, resizeImage } from '../../utils/image';
+import { blurImageDimensions } from '../../utils/const/images';
+import ProfileImage from '../../components/profile-image';
+import { BookOpenSVG } from '../../components/icons/svgs';
+import getReadTime from '../../utils/getReadTime';
+import Autolinker from "../../utils/autolinker";
+import DraftFloatingMenu from '../../components/draft-floating-menu';
+import { markdownToHtml } from '@starter-kit/utils/renderer/markdownToHtml';
+import TocRenderDesign from '../../components/toc-render-design';
+
+type Props = {
+ draft: DraftFragment; // TODO: to be fixed
+ publication: PublicationFragment;
+};
+
+export default function Post({ publication, draft }: Props) {
+ const headerRef = useRef(null);
+ if (!draft) {
+ return ;
+ }
+ const title = `${draft.title} - Hashnode`;
+ const highlightJsMonokaiTheme =
+ '.hljs{display:block;overflow-x:auto;padding:.5em;background:#23241f}.hljs,.hljs-subst,.hljs-tag{color:#f8f8f2}.hljs-emphasis,.hljs-strong{color:#a8a8a2}.hljs-bullet,.hljs-link,.hljs-literal,.hljs-number,.hljs-quote,.hljs-regexp{color:#ae81ff}.hljs-code,.hljs-section,.hljs-selector-class,.hljs-title{color:#a6e22e}.hljs-strong{font-weight:700}.hljs-emphasis{font-style:italic}.hljs-attr,.hljs-keyword,.hljs-name,.hljs-selector-tag{color:#f92672}.hljs-attribute,.hljs-symbol{color:#66d9ef}.hljs-class .hljs-title,.hljs-params{color:#f8f8f2}.hljs-addition,.hljs-built_in,.hljs-builtin-name,.hljs-selector-attr,.hljs-selector-id,.hljs-selector-pseudo,.hljs-string,.hljs-template-variable,.hljs-type,.hljs-variable{color:#e6db74}.hljs-comment,.hljs-deletion,.hljs-meta{color:#75715e}';
+ const navPositionStyles = 'relative transform-none md:sticky md:top-0 md:left-0 md:backdrop-blur-lg';
+ const readTime = draft && getReadTime(draft.content?.markdown);
+ const content = markdownToHtml(draft.content?.markdown || '');
+ const postContent = Autolinker.link(content, {
+ twitter: true,
+ truncate: 45,
+ className: 'autolinkedURL',
+ replaceFn(_autolinker: any, match: any) {
+ // eslint-disable-next-line default-case
+ switch (match.getType()) {
+ case 'twitter':
+ // eslint-disable-next-line no-unreachable,no-case-declarations
+ const username = match.getTwitterHandle();
+ return (
+ // eslint-disable-next-line no-unreachable
+ `@${username} `
+ );
+ }
+ },
+ });
+
+ const allTags = draft.tags;
+ const toc = draft.features?.tableOfContents?.isEnabled ? draft.features?.tableOfContents?.items.flat() : [];
+ return (
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+ {/* Top cover. StickCovertobottom use-case is not built yet */}
+ {draft.coverImage?.url && (
+
+
+
+ )}
+
+ {/* Article title */}
+
+
+ {draft.title}
+
+
+
+ {/* Article subtitle */}
+ {draft.subtitle && (
+
+
+ {draft.subtitle}
+
+
+ )}
+
+
+
+
+
+
+
+ {draft.features.tableOfContents.isEnabled &&
}
+ {postContent && (
+
+ )}
+ {allTags.length > 0 && (
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
+
+type Params = {
+ params: {
+ id: string;
+ };
+};
+
+export async function getStaticProps({ params }: Params) {
+ const [dataDraft, dataPublication] = await Promise.all([
+ request(
+ process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT,
+ DraftByIdDocument,
+ {
+ id: params.id,
+ },
+ ),
+ request(
+ process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT,
+ PublicationByHostDocument,
+ {
+ host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
+ },
+ ),
+ ]);
+
+ const publication = dataPublication.publication;
+ const draft = dataDraft.draft;
+ return {
+ props: {
+ draft,
+ publication,
+ },
+ revalidate: 1,
+ };
+}
+
+export async function getStaticPaths() {
+ return {
+ paths: [],
+ fallback: 'blocking',
+ };
+}
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/robots.txt.tsx b/packages/blog-starter-kit/themes/hashnode/pages/robots.txt.tsx
new file mode 100644
index 000000000..17e55a2d9
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/robots.txt.tsx
@@ -0,0 +1,35 @@
+import { type GetServerSideProps } from 'next';
+
+const RobotsTxt = () => null;
+
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
+ const { res } = ctx;
+ const host = process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST;
+ if (!host) {
+ throw new Error('Could not determine host');
+ }
+
+ const sitemapUrl = `https://${host}/sitemap.xml`;
+ const robotsTxt = `
+User-agent: *
+Allow: /
+
+# Google adsbot ignores robots.txt unless specifically named!
+User-agent: AdsBot-Google
+Allow: /
+
+User-agent: GPTBot
+Disallow: /
+
+Sitemap: ${sitemapUrl}
+ `.trim();
+
+ res.setHeader('Cache-Control', 's-maxage=86400, stale-while-revalidate');
+ res.setHeader('content-type', 'text/plain');
+ res.write(robotsTxt);
+ res.end();
+
+ return { props: {} };
+};
+
+export default RobotsTxt;
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/rss.xml.tsx b/packages/blog-starter-kit/themes/hashnode/pages/rss.xml.tsx
new file mode 100644
index 000000000..f035e3840
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/rss.xml.tsx
@@ -0,0 +1,44 @@
+import { constructRSSFeedFromPosts } from '@starter-kit/utils/feed';
+import request from 'graphql-request';
+import { GetServerSideProps } from 'next';
+import { RssFeedDocument, RssFeedQuery, RssFeedQueryVariables } from '../generated/graphql';
+
+const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT;
+const RSS = () => null;
+
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
+ const { res, query } = ctx;
+ const after = query.after ? (query.after as string) : null;
+
+ const data = await request(GQL_ENDPOINT, RssFeedDocument, {
+ first: 20,
+ host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
+ after,
+ });
+
+ const publication = data.publication;
+ if (!publication) {
+ return {
+ notFound: true,
+ };
+ }
+ const allPosts = publication.posts.edges.map((edge) => edge.node);
+
+ const xml = constructRSSFeedFromPosts(
+ publication,
+ allPosts,
+ after,
+ publication.posts.pageInfo.hasNextPage && publication.posts.pageInfo.endCursor
+ ? publication.posts.pageInfo.endCursor
+ : null,
+ );
+
+ res.setHeader('Cache-Control', 's-maxage=1, stale-while-revalidate');
+ res.setHeader('content-type', 'text/xml');
+ res.write(xml);
+ res.end();
+
+ return { props: {} };
+};
+
+export default RSS;
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/series/[slug].tsx b/packages/blog-starter-kit/themes/hashnode/pages/series/[slug].tsx
new file mode 100644
index 000000000..614dcc337
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/series/[slug].tsx
@@ -0,0 +1,242 @@
+import { resizeImage } from '@starter-kit/utils/image';
+import { GetServerSideProps } from 'next';
+import { WithUrqlProps, initUrqlClient } from 'next-urql';
+import Head from 'next/head';
+import { useState } from 'react';
+import { twJoin } from 'tailwind-merge';
+import { useQuery } from 'urql';
+import { AppProvider } from '../../components/contexts/appContext';
+import { Header } from '../../components/header';
+import { Layout } from '../../components/layout';
+import PublicationFooter from '../../components/publication-footer';
+import PublicationPosts from '../../components/publication-posts';
+import {
+ PublicationFragment,
+ SeriesPageInitialDocument,
+ SeriesPageInitialQuery,
+} from '../../generated/graphql';
+import { createHeaders, createSSRExchange, getUrqlClientConfig } from '../../lib/api/client';
+
+const INITIAL_LIMIT = 6;
+
+type Props = {
+ publication: PublicationFragment;
+ series: NonNullable['series']>;
+ slug: string;
+ initialLimit: number;
+ currentMenuId: string;
+};
+
+export default function Series({
+ publication,
+ series,
+ slug,
+ currentMenuId,
+}: Required & Props) {
+ const title = `${series.name} - ${publication.title}`;
+ const [after, setAfter] = useState(null);
+ const [{ data, fetching }] = useQuery({
+ query: SeriesPageInitialDocument,
+ variables: {
+ host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
+ slug,
+ first: INITIAL_LIMIT,
+ after,
+ },
+ requestPolicy: 'cache-first',
+ });
+ const postData = data?.publication?.series?.posts ?? {
+ edges: [],
+ pageInfo: { hasNextPage: false },
+ };
+ const posts = postData.edges.map((edge) => edge.node);
+
+ const fetchedOnce = postData.edges.length > INITIAL_LIMIT;
+
+ const fetchMore = () => {
+ if (postData.pageInfo.hasNextPage) {
+ setAfter(postData.edges[postData.edges.length - 1].cursor);
+ }
+ };
+
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+ Series
+
+
+ {series.name}
+
+ {series.description?.html && (
+
+ )}
+
+ {series.coverImage && (
+
+ {/* custom-style */}
+
+
+ )}
+
+
+
+ {posts.length === 0 && publication.isTeam ? (
+
+
+
+ No posts yet
+
+
+ ) : null}
+
+ {posts.length > 0 && (
+
+
+
+ Articles in this series
+
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+type Params = {
+ slug: string;
+};
+
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
+ const { req, query, resolvedUrl, params } = ctx;
+ const slug = params!.slug;
+ const requestHost = query['x-host'] || req.headers.host;
+ const [resolvedPath] = resolvedUrl.split('?');
+ const ssrCache = createSSRExchange();
+ const urqlClient = initUrqlClient(getUrqlClientConfig(ssrCache), false);
+ let rawCurrentMenuId = '';
+ const publicationInfo = await urqlClient
+ .query(
+ SeriesPageInitialDocument,
+ {
+ host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
+ slug,
+ first: INITIAL_LIMIT,
+ after: null,
+ },
+ {
+ fetchOptions: {
+ headers: createHeaders({ byPassCache: false }),
+ },
+ requestPolicy: 'network-only',
+ },
+ )
+ .toPromise();
+
+ const { publication } = publicationInfo.data || {};
+
+ if (!publication) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const { series } = publication || {};
+
+ if (!series) {
+ return {
+ notFound: true,
+ };
+ }
+
+ if (publication && series) {
+ const menu = publication.preferences.navbarItems || [];
+
+ for (let i = 0; i < menu.length; i++) {
+ const menuItem = menu[i];
+
+ if (menuItem.type === 'series' && menuItem.series && menuItem.series.id === series.id) {
+ rawCurrentMenuId = menuItem.id!;
+ break;
+ }
+ // check for links that could be mapped to the series page
+ if (menuItem.type === 'link' && menuItem.url && !rawCurrentMenuId) {
+ const { pathname, host } = new URL(menuItem.url);
+ const isLinkOnSameDomain = requestHost === host;
+ const pathnameMatches = resolvedPath === pathname;
+
+ if (pathnameMatches && isLinkOnSameDomain) {
+ rawCurrentMenuId = menuItem.id.toString();
+ break;
+ }
+ }
+ }
+ }
+
+ return {
+ props: {
+ publication,
+ series,
+ slug,
+ urqlState: ssrCache.extractData(),
+ initialLimit: INITIAL_LIMIT,
+ currentMenuId: rawCurrentMenuId,
+ },
+ };
+};
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/sitemap.xml.tsx b/packages/blog-starter-kit/themes/hashnode/pages/sitemap.xml.tsx
new file mode 100644
index 000000000..9b672e124
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/sitemap.xml.tsx
@@ -0,0 +1,82 @@
+import { getSitemap } from '@starter-kit/utils/seo/sitemap';
+import request from 'graphql-request';
+import { GetServerSideProps } from 'next';
+import {
+ MoreSitemapPostsDocument,
+ MoreSitemapPostsQuery,
+ MoreSitemapPostsQueryVariables,
+ SitemapDocument,
+ SitemapQuery,
+ SitemapQueryVariables,
+} from '../generated/graphql';
+
+const GQL_ENDPOINT = process.env.NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT;
+const MAX_POSTS = 1000;
+const Sitemap = () => null;
+
+export const getServerSideProps: GetServerSideProps = async (ctx) => {
+ const { res } = ctx;
+
+ const initialData = await request(
+ GQL_ENDPOINT,
+ SitemapDocument,
+ {
+ host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
+ postsCount: 20,
+ staticPagesCount: 50,
+ },
+ );
+
+ const publication = initialData.publication;
+ if (!publication) {
+ return {
+ notFound: true,
+ };
+ }
+ const posts = publication.posts.edges.map((edge) => edge.node);
+
+ // Get more posts by pagination if exists
+ const initialPageInfo = publication.posts.pageInfo;
+ const fetchPosts = async (after: string | null | undefined) => {
+ const variables = {
+ host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
+ postsCount: 20,
+ postsAfter: after,
+ };
+
+ const data = await request(
+ GQL_ENDPOINT,
+ MoreSitemapPostsDocument,
+ variables,
+ );
+ const publication = data.publication;
+ if (!publication) {
+ return;
+ }
+ const pageInfo = publication.posts.pageInfo;
+
+ posts.push(...publication.posts.edges.map((edge) => edge.node));
+
+ if (pageInfo.hasNextPage && posts.length < MAX_POSTS) {
+ await fetchPosts(pageInfo.endCursor);
+ }
+ };
+
+ if (initialPageInfo.hasNextPage) {
+ await fetchPosts(initialPageInfo.endCursor);
+ }
+
+ const xml = getSitemap({
+ ...publication,
+ posts,
+ });
+
+ res.setHeader('Cache-Control', 's-maxage=1, stale-while-revalidate');
+ res.setHeader('content-type', 'text/xml');
+ res.write(xml);
+ res.end();
+
+ return { props: {} };
+};
+
+export default Sitemap;
diff --git a/packages/blog-starter-kit/themes/hashnode/pages/tag/[slug].tsx b/packages/blog-starter-kit/themes/hashnode/pages/tag/[slug].tsx
new file mode 100644
index 000000000..0e52cf9f6
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/pages/tag/[slug].tsx
@@ -0,0 +1,189 @@
+import Head from 'next/head';
+import { twJoin } from 'tailwind-merge';
+import { useState } from 'react';
+import { useQuery } from 'urql';
+import { initUrqlClient } from 'next-urql';
+
+import { AppProvider } from '../../components/contexts/appContext';
+import { Header } from '../../components/header';
+import { Layout } from '../../components/layout';
+import {
+ Post,
+ PublicationFragment,
+ TagInitialDocument,
+ TagInitialQuery,
+} from '../../generated/graphql';
+import ExternalLinkSVG from '../../components/icons/svgs/ExternalLinkSVG';
+import { createHeaders, createSSRExchange, getUrqlClientConfig } from '../../lib/api/client';
+import PublicationPosts from '../../components/publication-posts';
+import PublicationFooter from '../../components/publication-footer';
+
+const INITIAL_LIMIT = 6;
+
+type Props = {
+ posts: NonNullable['posts'];
+ publication: PublicationFragment;
+ tag: NonNullable;
+ slug: string;
+ currentMenuId: string;
+};
+
+export default function Post({ publication, posts, tag, slug, currentMenuId }: Props) {
+ const title = `#${tag.name} - ${publication.title}`;
+ const [after, setAfter] = useState(null);
+ const [{ data, fetching }] = useQuery({
+ query: TagInitialDocument,
+ variables: { host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST, slug, first: INITIAL_LIMIT, after },
+ requestPolicy: 'cache-first',
+ });
+ const postData = data?.publication?.posts || posts;
+ const fetchedOnce = postData.edges.length > INITIAL_LIMIT;
+
+ const fetchMore = () => {
+ if (postData.pageInfo.hasNextPage) {
+ setAfter(postData.edges[postData.edges.length - 1].cursor);
+ }
+ };
+ return (
+
+
+
+ {title}
+
+
+
+
+
+
+
+ Tag
+
+
+
+ {tag.name}
+
+
#{tag.slug}
+
+
+ {tag && (
+
+ )}
+
+
+ {posts.edges.length > 0 && (
+
+
+
+ Articles with this tag
+
+
+ )}
+
{' '}
+
+
+
+
+ );
+}
+
+export const getServerSideProps: any = async (ctx: any) => { // TODO: type needs to be fixed
+ const { req, res, query } = ctx;
+ const { resolvedUrl } = ctx;
+ const [resolvedPath] = resolvedUrl.split('?');
+ const { 'x-host': queryHost } = query;
+ const ssrCache = createSSRExchange();
+ const urqlClient = initUrqlClient(getUrqlClientConfig(ssrCache), false);
+ let currentMenu = '';
+
+ const host = (queryHost as string) || req.headers.host!;
+ const slug = query.slug as string;
+
+ const { data } = await urqlClient
+ .query(
+ TagInitialDocument,
+ {
+ host: process.env.NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST,
+ slug: slug,
+ first: INITIAL_LIMIT, after: null
+ },
+ {
+ fetchOptions: {
+ headers: createHeaders({ byPassCache: false }),
+ },
+ requestPolicy: 'network-only',
+ },
+ )
+ .toPromise();
+
+ const { publication, tag } = data || {};
+
+ if (!publication || !tag) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const { posts } = publication || {};
+
+ if (!posts || posts.edges.length === 0) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const menu = publication.preferences.navbarItems || [];
+ for (let i = 0; i < menu.length; i++) {
+ const menuItem = menu[i];
+ if (menuItem.type === 'link') {
+ const { pathname, host: menuItemHost } = new URL(menuItem.url!);
+ const isLinkOnSameDomain = menuItemHost === host;
+ const pathnameMatches = resolvedPath === pathname;
+ if (pathnameMatches && isLinkOnSameDomain) {
+ currentMenu = menuItem.id!;
+ break;
+ }
+ }
+ }
+
+ return {
+ props: {
+ publication,
+ posts,
+ tag,
+ slug: slug,
+ currentMenuId: currentMenu
+ },
+ };
+}
+
diff --git a/packages/blog-starter-kit/themes/hashnode/postcss.config.js b/packages/blog-starter-kit/themes/hashnode/postcss.config.js
new file mode 100644
index 000000000..0ca321c0f
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/postcss.config.js
@@ -0,0 +1,8 @@
+// If you want to use other PostCSS plugins, see the following:
+// https://tailwindcss.com/docs/using-with-preprocessors
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/packages/blog-starter-kit/themes/hashnode/process-env.d.ts b/packages/blog-starter-kit/themes/hashnode/process-env.d.ts
new file mode 100644
index 000000000..378e6ae39
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/process-env.d.ts
@@ -0,0 +1,8 @@
+declare namespace NodeJS {
+ interface ProcessEnv {
+ [key: string]: string | undefined;
+ NEXT_PUBLIC_HASHNODE_GQL_ENDPOINT: string;
+ NEXT_PUBLIC_HASHNODE_PUBLICATION_HOST: string;
+ // add more environment variables and their types here
+ }
+}
diff --git a/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/jj.jpeg b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/jj.jpeg
new file mode 100644
index 000000000..e3d521436
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/jj.jpeg differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/joe.jpeg b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/joe.jpeg
new file mode 100644
index 000000000..d9677ad61
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/joe.jpeg differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/tim.jpeg b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/tim.jpeg
new file mode 100644
index 000000000..cc49257b8
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/authors/tim.jpeg differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/assets/blog/dynamic-routing/cover.jpg b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/dynamic-routing/cover.jpg
new file mode 100644
index 000000000..c660c9267
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/dynamic-routing/cover.jpg differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/assets/blog/hello-world/cover.jpg b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/hello-world/cover.jpg
new file mode 100644
index 000000000..33b7dc4b7
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/hello-world/cover.jpg differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/assets/blog/preview/cover.jpg b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/preview/cover.jpg
new file mode 100644
index 000000000..6a975fb36
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/assets/blog/preview/cover.jpg differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-192x192.png b/packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-192x192.png
new file mode 100644
index 000000000..2f07282a5
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-192x192.png differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-512x512.png b/packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-512x512.png
new file mode 100644
index 000000000..dbb0faea8
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/favicon/android-chrome-512x512.png differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/favicon/apple-touch-icon.png b/packages/blog-starter-kit/themes/hashnode/public/favicon/apple-touch-icon.png
new file mode 100644
index 000000000..8f4033b2a
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/favicon/apple-touch-icon.png differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/favicon/browserconfig.xml b/packages/blog-starter-kit/themes/hashnode/public/favicon/browserconfig.xml
new file mode 100644
index 000000000..9824d87b1
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/public/favicon/browserconfig.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+ #000000
+
+
+
diff --git a/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-16x16.png b/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-16x16.png
new file mode 100644
index 000000000..29deaf671
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-16x16.png differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-32x32.png b/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-32x32.png
new file mode 100644
index 000000000..e3b4277bf
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon-32x32.png differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon.ico b/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon.ico
new file mode 100644
index 000000000..ea2f437d9
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/favicon/favicon.ico differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/favicon/mstile-150x150.png b/packages/blog-starter-kit/themes/hashnode/public/favicon/mstile-150x150.png
new file mode 100644
index 000000000..f2dfd904b
Binary files /dev/null and b/packages/blog-starter-kit/themes/hashnode/public/favicon/mstile-150x150.png differ
diff --git a/packages/blog-starter-kit/themes/hashnode/public/favicon/safari-pinned-tab.svg b/packages/blog-starter-kit/themes/hashnode/public/favicon/safari-pinned-tab.svg
new file mode 100644
index 000000000..72ab6e050
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/public/favicon/safari-pinned-tab.svg
@@ -0,0 +1,33 @@
+
+
+
+
+Created by potrace 1.11, written by Peter Selinger 2001-2013
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/blog-starter-kit/themes/hashnode/public/js/iframe-resizer.js b/packages/blog-starter-kit/themes/hashnode/public/js/iframe-resizer.js
new file mode 100644
index 000000000..9af977b09
--- /dev/null
+++ b/packages/blog-starter-kit/themes/hashnode/public/js/iframe-resizer.js
@@ -0,0 +1,705 @@
+/*! iFrame Resizer (iframeSizer.min.js ) - v4.2.11 - 2020-06-02
+ * Desc: Force cross domain iframes to size to content.
+ * Requires: iframeResizer.contentWindow.min.js to be loaded into the target frame.
+ * Copyright: (c) 2020 David J. Bradshaw - dave@bradshaw.net
+ * License: MIT
+ */
+!(function (e) {
+ if ('undefined' != typeof window) {
+ var n,
+ i = 0,
+ t = !1,
+ o = !1,
+ r = 'message'.length,
+ a = '[iFrameSizer]',
+ s = a.length,
+ d = null,
+ c = window.requestAnimationFrame,
+ u = { max: 1, scroll: 1, bodyScroll: 1, documentElementScroll: 1 },
+ f = {},
+ l = null,
+ m = {
+ autoResize: !0,
+ bodyBackground: null,
+ bodyMargin: null,
+ bodyMarginV1: 8,
+ bodyPadding: null,
+ checkOrigin: !0,
+ inPageLinks: !1,
+ enablePublicMethods: !0,
+ heightCalculationMethod: 'bodyOffset',
+ id: 'iFrameResizer',
+ interval: 32,
+ log: !1,
+ maxHeight: 1 / 0,
+ maxWidth: 1 / 0,
+ minHeight: 0,
+ minWidth: 0,
+ resizeFrom: 'parent',
+ scrolling: !1,
+ sizeHeight: !0,
+ sizeWidth: !1,
+ warningTimeout: 5e3,
+ tolerance: 0,
+ widthCalculationMethod: 'scroll',
+ onClose: function () {
+ return !0;
+ },
+ onClosed: function () {},
+ onInit: function () {},
+ onMessage: function () {
+ I('onMessage function not defined');
+ },
+ onResized: function () {},
+ onScroll: function () {
+ return !0;
+ },
+ },
+ g = {};
+ window.jQuery &&
+ ((n = window.jQuery).fn
+ ? n.fn.iFrameResize ||
+ (n.fn.iFrameResize = function (e) {
+ return this.filter('iframe')
+ .each(function (n, i) {
+ H(i, e);
+ })
+ .end();
+ })
+ : v('', 'Unable to bind to jQuery, it is not fully loaded.')),
+ 'function' == typeof define && define.amd
+ ? define([], B)
+ : 'object' == typeof module && 'object' == typeof module.exports && (module.exports = B()),
+ (window.iFrameResize = window.iFrameResize || B());
+ }
+ function h() {
+ return window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;
+ }
+ function w(e, n, i) {
+ e.addEventListener(n, i, !1);
+ }
+ function p(e, n, i) {
+ e.removeEventListener(n, i, !1);
+ }
+ function b(e) {
+ return f[e] ? f[e].log : t;
+ }
+ function y(e, n) {
+ x('log', e, n, b(e));
+ }
+ function v(e, n) {
+ x('info', e, n, b(e));
+ }
+ function I(e, n) {
+ x('warn', e, n, !0);
+ }
+ function x(e, n, i, t) {
+ !0 === t &&
+ 'object' == typeof window.console &&
+ console[e](
+ (function (e) {
+ return (
+ a +
+ '[' +
+ (function (e) {
+ var n = 'Host page: ' + e;
+ return (
+ window.top !== window.self &&
+ (n =
+ window.parentIFrame && window.parentIFrame.getId
+ ? window.parentIFrame.getId() + ': ' + e
+ : 'Nested host page: ' + e),
+ n
+ );
+ })(e) +
+ ']'
+ );
+ })(n),
+ i,
+ );
+ }
+ function F(e) {
+ function n() {
+ i('Height'),
+ i('Width'),
+ C(
+ function () {
+ W(B), R(L), m('onResized', B);
+ },
+ B,
+ 'init',
+ );
+ }
+ function i(e) {
+ var n = Number(f[L]['max' + e]),
+ i = Number(f[L]['min' + e]),
+ t = e.toLowerCase(),
+ o = Number(B[t]);
+ y(L, 'Checking ' + t + ' is in range ' + i + '-' + n),
+ o < i && ((o = i), y(L, 'Set ' + t + ' to min value')),
+ n < o && ((o = n), y(L, 'Set ' + t + ' to max value')),
+ (B[t] = '' + o);
+ }
+ function t(e) {
+ return A.substr(A.indexOf(':') + r + e);
+ }
+ function o(e, n) {
+ !(function (e, n, i) {
+ g[i] ||
+ (g[i] = setTimeout(function () {
+ (g[i] = null), e();
+ }, 32));
+ })(
+ function () {
+ N(
+ 'Send Page Info',
+ 'pageInfo:' +
+ (function () {
+ var e = document.body.getBoundingClientRect(),
+ n = B.iframe.getBoundingClientRect();
+ return JSON.stringify({
+ iframeHeight: n.height,
+ iframeWidth: n.width,
+ clientHeight: Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
+ clientWidth: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
+ offsetTop: parseInt(n.top - e.top, 10),
+ offsetLeft: parseInt(n.left - e.left, 10),
+ scrollTop: window.pageYOffset,
+ scrollLeft: window.pageXOffset,
+ documentHeight: document.documentElement.clientHeight,
+ documentWidth: document.documentElement.clientWidth,
+ windowHeight: window.innerHeight,
+ windowWidth: window.innerWidth,
+ });
+ })(),
+ e,
+ n,
+ );
+ },
+ 0,
+ n,
+ );
+ }
+ function c(e) {
+ var n = e.getBoundingClientRect();
+ return O(L), { x: Math.floor(Number(n.left) + Number(d.x)), y: Math.floor(Number(n.top) + Number(d.y)) };
+ }
+ function u(e) {
+ var n = e ? c(B.iframe) : { x: 0, y: 0 },
+ i = { x: Number(B.width) + n.x, y: Number(B.height) + n.y };
+ y(L, 'Reposition requested from iFrame (offset x:' + n.x + ' y:' + n.y + ')'),
+ window.top !== window.self
+ ? window.parentIFrame
+ ? window.parentIFrame['scrollTo' + (e ? 'Offset' : '')](i.x, i.y)
+ : I(L, 'Unable to scroll to requested position, window.parentIFrame not found')
+ : ((d = i), l(), y(L, '--'));
+ }
+ function l() {
+ !1 !== m('onScroll', d) ? R(L) : T();
+ }
+ function m(e, n) {
+ return M(L, e, n);
+ }
+ var h,
+ b,
+ x,
+ F,
+ k,
+ H,
+ j,
+ P,
+ A = e.data,
+ B = {},
+ L = null;
+ '[iFrameResizerChild]Ready' === A
+ ? (function () {
+ for (var e in f) N('iFrame requested init', S(e), f[e].iframe, e);
+ })()
+ : a === ('' + A).substr(0, s) && A.substr(s).split(':')[0] in f
+ ? ((H = (k = A.substr(s).split(':'))[1] ? parseInt(k[1], 10) : 0),
+ (j = f[k[0]] && f[k[0]].iframe),
+ (P = getComputedStyle(j)),
+ (B = {
+ iframe: j,
+ id: k[0],
+ height:
+ H +
+ (function (e) {
+ return 'border-box' !== e.boxSizing
+ ? 0
+ : (e.paddingTop ? parseInt(e.paddingTop, 10) : 0) +
+ (e.paddingBottom ? parseInt(e.paddingBottom, 10) : 0);
+ })(P) +
+ (function (e) {
+ return 'border-box' !== e.boxSizing
+ ? 0
+ : (e.borderTopWidth ? parseInt(e.borderTopWidth, 10) : 0) +
+ (e.borderBottomWidth ? parseInt(e.borderBottomWidth, 10) : 0);
+ })(P),
+ width: k[2],
+ type: k[3],
+ }),
+ (L = B.id),
+ f[L] && (f[L].loaded = !0),
+ (F = B.type in { true: 1, false: 1, undefined: 1 }) && y(L, 'Ignoring init message from meta parent page'),
+ !F &&
+ ((x = !0), f[(b = L)] || ((x = !1), I(B.type + ' No settings for ' + b + '. Message was: ' + A)), x) &&
+ (y(L, 'Received: ' + A),
+ (h = !0),
+ null === B.iframe && (I(L, 'IFrame (' + B.id + ') not found'), (h = !1)),
+ h &&
+ (function () {
+ var n,
+ i = e.origin,
+ t = f[L] && f[L].checkOrigin;
+ if (
+ t &&
+ '' + i != 'null' &&
+ !(t.constructor === Array
+ ? (function () {
+ var e = 0,
+ n = !1;
+ for (y(L, 'Checking connection is from allowed list of origins: ' + t); e < t.length; e++)
+ if (t[e] === i) {
+ n = !0;
+ break;
+ }
+ return n;
+ })()
+ : ((n = f[L] && f[L].remoteHost), y(L, 'Checking connection is from: ' + n), i === n))
+ )
+ throw new Error(
+ 'Unexpected message received from: ' +
+ i +
+ ' for ' +
+ B.iframe.id +
+ '. Message was: ' +
+ e.data +
+ '. This error can be disabled by setting the checkOrigin: false option or by providing of array of trusted domains.',
+ );
+ return !0;
+ })() &&
+ (function () {
+ switch ((f[L] && f[L].firstRun && f[L] && (f[L].firstRun = !1), B.type)) {
+ case 'close':
+ z(B.iframe);
+ break;
+ case 'message':
+ !(function (e) {
+ y(L, 'onMessage passed: {iframe: ' + B.iframe.id + ', message: ' + e + '}'),
+ m('onMessage', { iframe: B.iframe, message: JSON.parse(e) }),
+ y(L, '--');
+ })(t(6));
+ break;
+ case 'autoResize':
+ f[L].autoResize = JSON.parse(t(9));
+ break;
+ case 'scrollTo':
+ u(!1);
+ break;
+ case 'scrollToOffset':
+ u(!0);
+ break;
+ case 'pageInfo':
+ o(f[L] && f[L].iframe, L),
+ (function () {
+ function e(e, t) {
+ function r() {
+ f[i] ? o(f[i].iframe, i) : n();
+ }
+ ['scroll', 'resize'].forEach(function (n) {
+ y(i, e + n + ' listener for sendPageInfo'), t(window, n, r);
+ });
+ }
+ function n() {
+ e('Remove ', p);
+ }
+ var i = L;
+ e('Add ', w), f[i] && (f[i].stopPageInfo = n);
+ })();
+ break;
+ case 'pageInfoStop':
+ f[L] && f[L].stopPageInfo && (f[L].stopPageInfo(), delete f[L].stopPageInfo);
+ break;
+ case 'inPageLink':
+ !(function (e) {
+ var n,
+ i = e.split('#')[1] || '',
+ t = decodeURIComponent(i),
+ o = document.getElementById(t) || document.getElementsByName(t)[0];
+ o
+ ? ((n = c(o)),
+ y(L, 'Moving to in page link (#' + i + ') at x: ' + n.x + ' y: ' + n.y),
+ (d = { x: n.x, y: n.y }),
+ l(),
+ y(L, '--'))
+ : window.top !== window.self
+ ? window.parentIFrame
+ ? window.parentIFrame.moveToAnchor(i)
+ : y(L, 'In page link #' + i + ' not found and window.parentIFrame not found')
+ : y(L, 'In page link #' + i + ' not found');
+ })(t(9));
+ break;
+ case 'reset':
+ E(B);
+ break;
+ case 'init':
+ n(), m('onInit', B.iframe);
+ break;
+ default:
+ n();
+ }
+ })()))
+ : v(L, 'Ignored: ' + A);
+ }
+ function M(e, n, i) {
+ var t = null,
+ o = null;
+ if (f[e]) {
+ if ('function' != typeof (t = f[e][n])) throw new TypeError(n + ' on iFrame[' + e + '] is not a function');
+ o = t(i);
+ }
+ return o;
+ }
+ function k(e) {
+ var n = e.id;
+ delete f[n];
+ }
+ function z(e) {
+ var n = e.id;
+ if (!1 !== M(n, 'onClose', n)) {
+ y(n, 'Removing iFrame: ' + n);
+ try {
+ e.parentNode && e.parentNode.removeChild(e);
+ } catch (e) {
+ I(e);
+ }
+ M(n, 'onClosed', n), y(n, '--'), k(e);
+ } else y(n, 'Close iframe cancelled by onClose event');
+ }
+ function O(n) {
+ null === d &&
+ y(
+ n,
+ 'Get page position: ' +
+ (d = {
+ x: window.pageXOffset !== e ? window.pageXOffset : document.documentElement.scrollLeft,
+ y: window.pageYOffset !== e ? window.pageYOffset : document.documentElement.scrollTop,
+ }).x +
+ ',' +
+ d.y,
+ );
+ }
+ function R(e) {
+ null !== d && (window.scrollTo(d.x, d.y), y(e, 'Set page position: ' + d.x + ',' + d.y), T());
+ }
+ function T() {
+ d = null;
+ }
+ function E(e) {
+ y(e.id, 'Size reset requested by ' + ('init' === e.type ? 'host page' : 'iFrame')),
+ O(e.id),
+ C(
+ function () {
+ W(e), N('reset', 'reset', e.iframe, e.id);
+ },
+ e,
+ 'reset',
+ );
+ }
+ function W(e) {
+ function n(n) {
+ !(function (n) {
+ e.id
+ ? ((e.iframe.style[n] = e[n] + 'px'), y(e.id, 'IFrame (' + i + ') ' + n + ' set to ' + e[n] + 'px'))
+ : y('undefined', 'messageData id not set');
+ })(n),
+ (function (n) {
+ o ||
+ '0' !== e[n] ||
+ ((o = !0),
+ y(i, 'Hidden iFrame detected, creating visibility listener'),
+ (function () {
+ function e() {
+ Object.keys(f).forEach(function (e) {
+ !(function (e) {
+ function n(n) {
+ return '0px' === (f[e] && f[e].iframe.style[n]);
+ }
+ f[e] &&
+ null !== f[e].iframe.offsetParent &&
+ (n('height') || n('width')) &&
+ N('Visibility change', 'resize', f[e].iframe, e);
+ })(e);
+ });
+ }
+ function n(n) {
+ y('window', 'Mutation observed: ' + n[0].target + ' ' + n[0].type), j(e, 16);
+ }
+ var i,
+ t = h();
+ t &&
+ ((i = document.querySelector('body')),
+ new t(n).observe(i, {
+ attributes: !0,
+ attributeOldValue: !1,
+ characterData: !0,
+ characterDataOldValue: !1,
+ childList: !0,
+ subtree: !0,
+ }));
+ })());
+ })(n);
+ }
+ var i = e.iframe.id;
+ f[i] && (f[i].sizeHeight && n('height'), f[i].sizeWidth && n('width'));
+ }
+ function C(e, n, i) {
+ i !== n.type && c && !window.jasmine ? (y(n.id, 'Requesting animation frame'), c(e)) : e();
+ }
+ function N(e, n, i, t, o) {
+ var r,
+ s = !1;
+ (t = t || i.id),
+ f[t] &&
+ (i && 'contentWindow' in i && null !== i.contentWindow
+ ? ((r = f[t] && f[t].targetOrigin),
+ y(t, '[' + e + '] Sending msg to iframe[' + t + '] (' + n + ') targetOrigin: ' + r),
+ i.contentWindow.postMessage(a + n, r))
+ : I(t, '[' + e + '] IFrame(' + t + ') not found'),
+ o &&
+ f[t] &&
+ f[t].warningTimeout &&
+ (f[t].msgTimeout = setTimeout(function () {
+ !f[t] ||
+ f[t].loaded ||
+ s ||
+ ((s = !0),
+ I(
+ t,
+ 'IFrame has not responded within ' +
+ f[t].warningTimeout / 1e3 +
+ ' seconds. Check iFrameResizer.contentWindow.js has been loaded in iFrame. This message can be ignored if everything is working, or you can set the warningTimeout option to a higher value or zero to suppress this warning.',
+ ));
+ }, f[t].warningTimeout)));
+ }
+ function S(e) {
+ return (
+ e +
+ ':' +
+ f[e].bodyMarginV1 +
+ ':' +
+ f[e].sizeWidth +
+ ':' +
+ f[e].log +
+ ':' +
+ f[e].interval +
+ ':' +
+ f[e].enablePublicMethods +
+ ':' +
+ f[e].autoResize +
+ ':' +
+ f[e].bodyMargin +
+ ':' +
+ f[e].heightCalculationMethod +
+ ':' +
+ f[e].bodyBackground +
+ ':' +
+ f[e].bodyPadding +
+ ':' +
+ f[e].tolerance +
+ ':' +
+ f[e].inPageLinks +
+ ':' +
+ f[e].resizeFrom +
+ ':' +
+ f[e].widthCalculationMethod
+ );
+ }
+ function H(n, o) {
+ var r,
+ a,
+ s,
+ d,
+ c,
+ l,
+ g =
+ ('' === (a = n.id) &&
+ ((n.id = ((r = (o && o.id) || m.id + i++), null !== document.getElementById(r) && (r += i++), (a = r))),
+ (t = (o || {}).log),
+ y(a, 'Added missing iframe ID: ' + a + ' (' + n.src + ')')),
+ a);
+ function p(e) {
+ 1 / 0 !== f[g][e] && 0 !== f[g][e] && ((n.style[e] = f[g][e] + 'px'), y(g, 'Set ' + e + ' = ' + f[g][e] + 'px'));
+ }
+ function b(e) {
+ if (f[g]['min' + e] > f[g]['max' + e]) throw new Error('Value for min' + e + ' can not be greater than max' + e);
+ }
+ g in f && 'iFrameResizer' in n
+ ? I(g, 'Ignored iFrame, already setup.')
+ : ((l = (l = o) || {}),
+ (f[g] = { firstRun: !0, iframe: n, remoteHost: n.src && n.src.split('/').slice(0, 3).join('/') }),
+ (function (e) {
+ if ('object' != typeof l) throw new TypeError('Options is not an object');
+ })(),
+ Object.keys(l).forEach(function (e) {
+ var n = e.split('Callback');
+ if (2 === n.length) {
+ var i = 'on' + n[0].charAt(0).toUpperCase() + n[0].slice(1);
+ (this[i] = this[e]),
+ delete this[e],
+ I(
+ g,
+ "Deprecated: '" +
+ e +
+ "' has been renamed '" +
+ i +
+ "'. The old method will be removed in the next major version.",
+ );
+ }
+ }, l),
+ (function (e) {
+ for (var n in m)
+ Object.prototype.hasOwnProperty.call(m, n) &&
+ (f[g][n] = Object.prototype.hasOwnProperty.call(e, n) ? e[n] : m[n]);
+ })(l),
+ f[g] &&
+ (f[g].targetOrigin =
+ !0 === f[g].checkOrigin
+ ? (function (e) {
+ return '' === e || null !== e.match(/^(about:blank|javascript:|file:\/\/)/) ? '*' : e;
+ })(f[g].remoteHost)
+ : '*'),
+ (function () {
+ switch (
+ (y(g, 'IFrame scrolling ' + (f[g] && f[g].scrolling ? 'enabled' : 'disabled') + ' for ' + g),
+ (n.style.overflow = !1 === (f[g] && f[g].scrolling) ? 'hidden' : 'auto'),
+ f[g] && f[g].scrolling)
+ ) {
+ case 'omit':
+ break;
+ case !0:
+ n.scrolling = 'yes';
+ break;
+ case !1:
+ n.scrolling = 'no';
+ break;
+ default:
+ n.scrolling = f[g] ? f[g].scrolling : 'no';
+ }
+ })(),
+ b('Height'),
+ b('Width'),
+ p('maxHeight'),
+ p('minHeight'),
+ p('maxWidth'),
+ p('minWidth'),
+ ('number' != typeof (f[g] && f[g].bodyMargin) && '0' !== (f[g] && f[g].bodyMargin)) ||
+ ((f[g].bodyMarginV1 = f[g].bodyMargin), (f[g].bodyMargin = f[g].bodyMargin + 'px')),
+ (s = S(g)),
+ (c = h()) &&
+ ((d = c),
+ n.parentNode &&
+ new d(function (e) {
+ e.forEach(function (e) {
+ Array.prototype.slice.call(e.removedNodes).forEach(function (e) {
+ e === n && z(n);
+ });
+ });
+ }).observe(n.parentNode, { childList: !0 })),
+ w(n, 'load', function () {
+ var i, t;
+ N('iFrame.onload', s, n, e, !0),
+ (i = f[g] && f[g].firstRun),
+ (t = f[g] && f[g].heightCalculationMethod in u),
+ !i && t && E({ iframe: n, height: 0, width: 0, type: 'init' });
+ }),
+ N('init', s, n, e, !0),
+ f[g] &&
+ (f[g].iframe.iFrameResizer = {
+ close: z.bind(null, f[g].iframe),
+ removeListeners: k.bind(null, f[g].iframe),
+ resize: N.bind(null, 'Window resize', 'resize', f[g].iframe),
+ moveToAnchor: function (e) {
+ N('Move to anchor', 'moveToAnchor:' + e, f[g].iframe, g);
+ },
+ sendMessage: function (e) {
+ N('Send Message', 'message:' + (e = JSON.stringify(e)), f[g].iframe, g);
+ },
+ }));
+ }
+ function j(e, n) {
+ null === l &&
+ (l = setTimeout(function () {
+ (l = null), e();
+ }, n));
+ }
+ function P() {
+ 'hidden' !== document.visibilityState &&
+ (y('document', 'Trigger event: Visiblity change'),
+ j(function () {
+ A('Tab Visable', 'resize');
+ }, 16));
+ }
+ function A(e, n) {
+ Object.keys(f).forEach(function (i) {
+ !(function (e) {
+ return f[e] && 'parent' === f[e].resizeFrom && f[e].autoResize && !f[e].firstRun;
+ })(i) || N(e, n, f[i].iframe, i);
+ });
+ }
+ function B() {
+ function n(e, n) {
+ n &&
+ ((function () {
+ if (!n.tagName) throw new TypeError('Object is not a valid DOM element');
+ if ('IFRAME' !== n.tagName.toUpperCase())
+ throw new TypeError('Expected