add Astro Hashnode integration and dependencies

This commit is contained in:
Adam Matthiesen 2024-03-11 05:56:23 -07:00
parent 61f90b9bb1
commit 26dab56bd3
40 changed files with 6594 additions and 86 deletions

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2024 Adam Matthiesen Copyright (c) 2024 MatthiesenXYZ
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,9 +1,30 @@
# PACKAGE-NAME # Astro Hashnode
DESCRIPTION An Integration to bring your Hashnode Headless Blog content into Astro!
To see how to get started, check out the [package README](./package/README.md) To see how to get started, check out the [package README](./package/README.md)
## Contributing
This package is structured as a monorepo:
- `playground` contains code for testing the package
- `package` contains the actual package
Install dependencies using pnpm:
```bash
pnpm i --frozen-lockfile
```
Start the playground:
```bash
pnpm playground:dev
```
You can now edit files in `package`. Please note that making changes to those files may require restarting the playground dev server.
## Licensing ## Licensing
[MIT Licensed](./LICENSE). Made with ❤️ by [Adam M.](https://github.com/adammatthiesen). [MIT Licensed](./LICENSE). Made with ❤️ by [Adam M.](https://github.com/adammatthiesen).

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2024 Adam Matthiesen Copyright (c) 2024 MatthiesenXYZ
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -1,21 +1,21 @@
# PACKAGE-NAME # Astro Hashnode
DESCRIPTION An Integration to bring your Hashnode Headless Blog content into Astro!
## Installation ## Installation
Install the integration **automatically** using the Astro CLI: Install the integration **automatically** using the Astro CLI:
```bash ```bash
pnpm astro add PACKAGE-NAME pnpm astro add @matthiesenxyz/astro-hashnode
``` ```
```bash ```bash
npm astro add PACKAGE-NAME npm astro add @matthiesenxyz/astro-hashnode
``` ```
```bash ```bash
yarn astro add PACKAGE-NAME yarn astro add @matthiesenxyz/astro-hashnode
``` ```
Or install it **manually**: Or install it **manually**:
@ -23,53 +23,43 @@ Or install it **manually**:
1. Install the required dependencies 1. Install the required dependencies
```bash ```bash
pnpm add PACKAGE-NAME pnpm add @matthiesenxyz/astro-hashnode
``` ```
```bash ```bash
npm install PACKAGE-NAME npm install @matthiesenxyz/astro-hashnode
``` ```
```bash ```bash
yarn add PACKAGE-NAME yarn add @matthiesenxyz/astro-hashnode
``` ```
2. Add the integration to your astro config 2. Add the integration to your astro config
```diff ```diff
+import PACKAGE-NAME from "PACKAGE-NAME"; +import astroHashnode from "@matthiesenxyz/astro-hashnode";
export default defineConfig({ export default defineConfig({
integrations: [ integrations: [
+ PACKAGE-NAME(), + astroHashnode({
+ hashnodeURL: 'astroplayground.hashnode.dev'
}),
], ],
}); });
``` ```
## Basic Usage ## Full Configuration Options
```ts
astroHashnode({
## Contributing hashnodeURL: 'astroplayground.hashnode.dev', // Your hashnode URL
landingPage: true, // Lets you disable the default landing page!
This package is structured as a monorepo: layoutComponent: './src/layouts/YourLayout.astro' // Lets you change the default Layout.astro being used by the Integration Pages.
verbose: false // Change to Verbose console output
- `playground` contains code for testing the package })
- `package` contains the actual package
Install dependencies using pnpm:
```bash
pnpm i --frozen-lockfile
``` ```
Start the playground: Node: This Integration uses the new Tailwind v4 There is no config options in this version of tailwindCSS, and applyBaseStyles is enabled! So if you are building your own LayoutComponent feel free to use TailwindCSS!
```bash
pnpm playground:dev
```
You can now edit files in `package`. Please note that making changes to those files may require restarting the playground dev server.
## Licensing ## Licensing
@ -77,4 +67,9 @@ You can now edit files in `package`. Please note that making changes to those fi
## Acknowledgements ## Acknowledgements
TODO: - [`astro-integration-kit`](https://github.com/florian-lefebvre/astro-integration-kit) by Florian
- [`Hashnode - HeadlessCMS`](https://hashnode.com/headless) by the Hashnode
- [`TailwindCSS v4`](https://tailwindcss.com/blog/tailwindcss-v4-alpha) by the TailwindCSS team
- [`Astro-Font`](https://github.com/rishi-raj-jain/astro-font) by Rishi
- [`Astro-SEO`](https://github.com/jonasmerlin/astro-seo) by Jonas
- [`Astro-Remote`](https://github.com/natemoo-re/astro-remote) by Nate

View File

@ -1,7 +1,7 @@
{ {
"name": "PACKAGE-NAME", "name": "@matthiesenxyz/astro-hashnode",
"version": "0.0.0", "version": "0.0.0",
"description": "DESCRIPTION", "description": "An Integration to bring your Hashnode Headless Blog content into Astro!",
"author": { "author": {
"email": "adam@matthiesen.xyz", "email": "adam@matthiesen.xyz",
"name": "Adam Matthiesen", "name": "Adam Matthiesen",
@ -10,12 +10,18 @@
"license": "MIT", "license": "MIT",
"keywords": [ "keywords": [
"astro-integration", "astro-integration",
"astro-component",
"withastro", "withastro",
"astro", "astro",
"commercejs" "hashnode",
"blog",
"graphql",
"hashnode-headless"
], ],
"homepage": "https://github.com/adammatthiesen/astro-commercejs", "homepage": "https://github.com/matthiesenxyz/astro-hashnode",
"repository": {
"type": "git",
"url": "git+https://github.com/MatthiesenXYZ/astro-hashnode.git"
},
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
}, },
@ -29,7 +35,19 @@
"peerDependencies": { "peerDependencies": {
"astro": ">=4.4.1" "astro": ">=4.4.1"
}, },
"devDependencies": {
"vite": "^5.1.5"
},
"dependencies": { "dependencies": {
"astro-integration-kit": "^0.5.1" "@tailwindcss/vite": "4.0.0-alpha.7",
"astro-font": "0.0.77",
"astro-integration-kit": "^0.5.1",
"astro-remote": "^0.3.2",
"astro-seo": "^0.8.3",
"graphql": "^16.8.1",
"graphql-request": "^6.1.0",
"picocolors": "1.0.0",
"tailwindcss": "4.0.0-alpha.7",
"ultrahtml": "^1.5.3"
} }
} }

BIN
package/src/assets/blog.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

View File

@ -0,0 +1,136 @@
import { createResolver, defineIntegration } from "astro-integration-kit";
import { addDtsPlugin, corePlugins } from "astro-integration-kit/plugins";
import tailwindcss from "@tailwindcss/vite";
import { optionsSchema } from "./schemas/user-config";
import c from "picocolors";
import { AstroError } from "astro/errors";
import { readFileSync } from "node:fs";
/**
* Astro-Hashnode Integration
*/
export default defineIntegration({
name: "@matthiesenxyz/astro-hashnode",
optionsSchema,
plugins: [ ...corePlugins, addDtsPlugin ],
setup({ options }) {
return {
"astro:config:setup": ({
watchIntegration,
addVitePlugin,
addVirtualImports,
addDts,
injectScript,
injectRoute,
updateConfig,
config,
logger,
}) => {
logger.info("Initializing...")
// Create Resolvers
const { resolve } = createResolver(import.meta.url);
const { resolve: rootResolve} = createResolver(config.root.pathname)
// Watch Integration for changes in DEV
watchIntegration(resolve())
const HashLogger = logger.fork(c.bold(c.blue("Astro-Hashnode")));
const hashLogNoVerbose = (message:string) => {
HashLogger.info(c.magenta(message))
}
const hashLog = (message:string) => {
if (options.verbose) {
HashLogger.info(c.magenta(message))
}
}
const hashError = (message:string) => {
HashLogger.error(c.magenta(message))
throw new AstroError(message)
}
hashLogNoVerbose("Setting up Astro-Hashnode Integration")
// Check for Hashnode URL
if (!options.hashnodeURL) {
hashError("You must provide a hashnodeURL in your astro.config.mjs file. Use the hashnodeURL from your hashnode blog. It should look something like this: 'yourblog.hashnode.dev'")
}
hashLog(`Using Hashnode URL: ${options.hashnodeURL}`)
hashLog("Setting up Virtual Imports and Layout Component")
// Setup Layout Component
// biome-ignore lint/suspicious/noImplicitAnyLet: This is a false positive
let layoutComponentPath
if (options.layoutComponent) {
layoutComponentPath = rootResolve(options.layoutComponent)
hashLog('Using user defined layout component')
} else {
layoutComponentPath = resolve('./layouts/Layout.astro')
}
addVirtualImports({
'virtual:astro-hashnode/config': `export default ${JSON.stringify(options) }`,
'virtual:astro-hashnode/components': `export { default as Layout } from "${layoutComponentPath}";`,
})
addDts({
name: 'astro-hashnode',
content: readFileSync(resolve("./definitions/astro-hashnode.d.ts"), "utf-8"),
})
hashLog("Setting up 'Tailwind CSS v4' Integration")
// Add & Setup Tailwind CSS
const twplugin = tailwindcss();
for (const twp of twplugin) {
addVitePlugin(twp);
}
injectScript(
"page-ssr",
`import "${resolve("./styles/tailwind.css")}";`
);
updateConfig({
vite: {
css: { transformer: "lightningcss" },
}
})
hashLog("Setting up Page Routes")
// Add Page Routes
if (options.landingPage) {
injectRoute({
pattern: config.base,
entrypoint: resolve("./pages/index.astro"),
});
}
injectRoute({
pattern: `${config.base}blog`,
entrypoint: resolve("./pages/blog/index.astro"),
})
injectRoute({
pattern: `${config.base}blog/[slug]`,
entrypoint: resolve("./pages/blog/[slug].astro"),
})
injectRoute({
pattern: `${config.base}blog/about`,
entrypoint: resolve("./pages/blog/about.astro"),
})
injectRoute({
pattern: `${config.base}blog/tags/[tag]`,
entrypoint: resolve("./pages/blog/tags/[tag].astro"),
})
},
"astro:config:done": ({ logger }) => {
const HashLogger = logger.fork(c.bold(c.blue("Astro-Hashnode")));
const hashLog = (message:string) => {
HashLogger.info(c.green(message))
}
hashLog("Astro-Hashnode Integration Setup Complete")
}
}
}
})
export { type AstroHashnodeLayoutProps } from "./proptypes/layouttypes"

View File

@ -0,0 +1,35 @@
---
import { Image } from 'astro:assets';
import type { Post } from '../hn-gql';
import {getFormattedDate} from '../utils/utility';
interface Props {
post: Post;
}
const { post } = Astro.props;
---
<div class="relative flex flex-row flex-wrap items-center justify-center">
<div class="mb-5 flex w-full flex-row items-center justify-center md:mb-0 md:w-auto md:justify-start">
<div class="flex mr-1">
<Image
src={post.author.profilePicture}
alt={post.author.name}
width={50}
height={50}
class="rounded-3xl mr-3"
loading="eager"
/>
<div class="mt-3 flex">
<span>{post.author.name}</span>
<span class="mx-3 block font-bold text-slate-500">.</span>
</div>
</div>
</div>
<div class="mb-5 flex w-full flex-row items-center justify-center md:mb-0 md:w-auto md:justify-start">
<span>{getFormattedDate(post.publishedAt)}</span>
<span class="mx-3 block font-bold text-slate-500">.</span>
<span>{post.readTimeInMinutes} min read</span>
</div>
</div>

View File

@ -0,0 +1,36 @@
---
import { getPublication } from "../hn-gql"
import Social from './Social.astro';
const pub = await getPublication();
const { links, preferences } = pub;
const { disableFooterBranding } = preferences;
const currentYear = new Date().getFullYear();
---
<footer class=" mt-3 flex flex-col items-center justify-center bg-blue-200 p-1">
<div class="mt-1">
<center>
<p class="text-sm text-gray-400 p-2">
© {currentYear} {pub.title}. All rights reserved.
</p>
{!disableFooterBranding && (<p class="text-sm text-gray-400 p-2">
Made with ❤️ and passion using Headless Hashnode and Astro.
</p>)}
</center>
</div>
<div class="mt-3 flex">
{links.dailydev && <Social url={links.dailydev} displayName="DailyDev" />}
{links.github && <Social url={links.github} displayName="Github" />}
{links.hashnode && <Social url={links.hashnode} displayName="Hashnode" />}
{links.twitter && <Social url={links.twitter} displayName="Twitter" />}
{links.instagram && <Social url={links.instagram} displayName="Instagram" />}
{links.website && <Social url={links.website} displayName="Website" />}
{links.youtube && <Social url={links.youtube} displayName="Youtube" />}
{links.linkedin && <Social url={links.linkedin} displayName="Linkedin" />}
{links.mastodon && <Social url={links.mastodon} displayName="Mastodon" />}
</div>
</footer>

View File

@ -0,0 +1,42 @@
---
import { Image } from "astro:assets";
import { getAboutPage, getPublication } from "../hn-gql"
const publication = await getPublication();
const aboutPageData = await getAboutPage();
const baseURL = import.meta.env.BASE_URL;
---
<header class="flex bg-blue-200 w-full p-3">
<h1 class="text-2xl">
<a href={baseURL}>
{ publication.preferences.logo ?
<Image
src={publication.preferences.logo}
height={32}
width={150}
class="inline"
alt={publication.title}
loading="eager"
/> :
<>
{ publication.favicon && <Image
src={publication.favicon}
height={32}
width={32}
class="inline"
alt={publication.title}
loading="eager"
/> }
{publication.title}
</> }
</a>
</h1>
<div class="ml-5 pt-0.5 text-lg">
<a class="mr-3" href={baseURL}>Home</a>
<a class="mr-3" href={`${baseURL}blog`}>Blog</a>
{aboutPageData && <a href={`${baseURL}blog/about/`}>About</a>}
</div>
</header>

View File

@ -0,0 +1,41 @@
---
import { Image } from 'astro:assets';
import { getPost, type Post } from '../hn-gql';
import Author from './Author.astro';
import Tag from './Tag.astro';
type TagType = {
name: string;
slug: string;
};
interface Props {
post: Post
}
const baseURL = import.meta.env.BASE_URL;
const { post } = Astro.props;
const p = await getPost(post.slug);
---
<a href={`${baseURL}blog/${post.slug}`} aria-label="Post">
<div class="p-6 bg-white rounded shadow-sm my-4">
<h2 class="text-4xl pb-5 font-semibold">{post.title}</h2>
<div class="flex lg:flex-row md:flex-col max-sm:flex-col ">
<Image
class="w-full rounded-lg shadow-xl"
src={p.coverImage.url}
alt={post.title}
inferSize={true}
/>
<div class="flex flex-col m-4">
<p class="mb-2 text-lg">{post.brief}</p>
<div class="mt-5 mb-5">
<Author post={post} />
</div>
<div class="flex justify-center items-center">
{post.tags.map((tag: TagType) => <Tag tag={tag} />)}
</div>
</div>
</div>
</div>
</a>

View File

@ -0,0 +1,14 @@
---
import PostCard from './PostCard.astro';
import type { AllPostsGenerated } from '../hn-gql'
interface Props{
allPosts: AllPostsGenerated[]
}
const { allPosts } = Astro.props;
---
<div>
{allPosts.map((post: AllPostsGenerated) => <PostCard post={post.node} />)}
</div>

View File

@ -0,0 +1,14 @@
---
interface Props {
url: string;
displayName: string;
}
const { url, displayName } = Astro.props;
---
<a
class="p-1 rounded-lg text-md mr-2 bg-purple-800 text-white"
href={url}
target="_blank">
{displayName}
</a>

View File

@ -0,0 +1,15 @@
---
interface Props {
tag: {
name?: string;
slug?: string;
}
};
const baseURL = import.meta.env.BASE_URL;
const { tag } = Astro.props;
---
<div class="m-1 bg-purple-500 text-white rounded-md p-1 text-sm">
<a href={`${baseURL}blog/tags/${tag.slug}`}>{tag.name}</a>
</div>

View File

@ -0,0 +1,23 @@
---
import { Image } from "astro:assets";
interface Props {
src: string;
alt: string;
}
const {
alt = "No Alt Text",
src,
...rest
} = Astro.props;
---
<Image
class="rounded-lg"
src={src}
alt={alt}
inferSize={true}
loading="lazy"
{...rest}
/>

View File

@ -0,0 +1,8 @@
declare module 'virtual:astro-hashnode/config' {
const userConfig: import("./src/schemas/user-config").Options;
export default config as userConfig;
}
declare module 'virtual:astro-hashnode/components' {
export const Layout: typeof import("./src/layouts/Layout.astro").default
}

View File

@ -0,0 +1,142 @@
import { gql, GraphQLClient } from "graphql-request";
import type { AllPostsData, PostOrPageData, PublicationData } from "./schema";
import Config from "virtual:astro-hashnode/config";
export const getClient = () => {
return new GraphQLClient("https://gql.hashnode.com")
}
export const getAllPosts = async () => {
const client = getClient();
const allPosts = await client.request<AllPostsData>(
gql`
query allPosts {
publication(host: "${Config.hashnodeURL}") {
title
posts(first: 20) {
pageInfo{
hasNextPage
endCursor
}
edges {
node {
author{
name
profilePicture
}
title
subtitle
brief
slug
coverImage {
url
}
tags {
name
slug
}
publishedAt
readTimeInMinutes
}
}
}
}
}
`
);
return allPosts;
};
export const getPost = async (slug: string) => {
const client = getClient();
const data = await client.request<PostOrPageData>(
gql`
query postDetails($slug: String!) {
publication(host: "${Config.hashnodeURL}") {
post(slug: $slug) {
author{
name
profilePicture
}
publishedAt
title
subtitle
readTimeInMinutes
content{
html
}
tags {
name
slug
}
coverImage {
url
}
}
}
}
`,
{ slug: slug }
);
return data.publication.post;
};
export const getAboutPage = async () => {
const client = getClient();
const page = await client.request<PostOrPageData>(
gql`
query pageData {
publication(host: "${Config.hashnodeURL}") {
staticPage(slug: "about") {
title
content {
markdown
}
}
title
}
}
`
);
return page.publication.staticPage;
};
export const getPublication = async () => {
const client = getClient();
const data = await client.request<PublicationData>(
gql`
query pubData {
publication(host: "${Config.hashnodeURL}") {
title
displayTitle
descriptionSEO
favicon
preferences {
logo
disableFooterBranding
}
links{
twitter
instagram
github
website
hashnode
youtube
dailydev
linkedin
mastodon
}
}
}
`
);
return data.publication;
}

View File

@ -0,0 +1,2 @@
export * from "./client";
export * from "./schema";

View File

@ -0,0 +1,89 @@
import { z } from "astro/zod";
export const PostSchema = z.object({
author: z.object({
name: z.string(),
profilePicture: z.string(),
}),
publishedAt: z.string(),
title: z.string(),
subtitle: z.string(),
brief: z.string(),
slug: z.string(),
readTimeInMinutes: z.number(),
content: z.object({
html: z.string(),
}),
tags: z.array(z.object({
name: z.string(),
slug: z.string(),
})),
coverImage: z.object({
url: z.string(),
}),
})
export const PageSchema = z.object({
title: z.string(),
content: z.object({
markdown: z.string(),
}),
})
export const PostOrPageDataSchema = z.object({
publication: z.object({
title: z.string(),
post: PostSchema,
staticPage: PageSchema,
}),
})
export const AllPostsDataSchema = z.object({
publication: z.object({
title: z.string(),
posts: z.object({
pageInfo: z.object({
hasNextPage: z.boolean(),
endCursor: z.string(),
}),
edges: z.array(z.object({
node: PostSchema,
})),
}),
}),
})
export const PublicationDataSchema = z.object({
publication: z.object({
title: z.string(),
displayTitle: z.string(),
descriptionSEO: z.string(),
favicon: z.string(),
preferences: z.object({
logo: z.string(),
disableFooterBranding: z.boolean(),
}),
links: z.object({
twitter: z.string(),
instagram: z.string(),
github: z.string(),
website: z.string(),
hashnode: z.string(),
youtube: z.string(),
dailydev: z.string(),
linkedin: z.string(),
mastodon: z.string(),
}),
}),
})
export const AllPostsGeneratedSchema = z.object({
node: PostSchema,
})
export type PostOrPageData = z.infer<typeof PostOrPageDataSchema>
export type AllPostsData = z.infer<typeof AllPostsDataSchema>
export type PublicationData = z.infer<typeof PublicationDataSchema>
export type AllPostsGenerated = z.infer<typeof AllPostsGeneratedSchema>
export type Post = z.infer<typeof PostSchema>

View File

@ -1,3 +1,3 @@
import integration from "./integration.js"; import astroHashnode from "./astro-hashnode.js";
export default integration; export default astroHashnode;

View File

@ -1,24 +0,0 @@
import {
createResolver,
defineIntegration,
} from "astro-integration-kit";
import { corePlugins } from "astro-integration-kit/plugins";
import { z } from "astro/zod";
export default defineIntegration({
name: "PACKAGE-NAME",
optionsSchema: z.object({
/** A comment */
foo: z.string().optional().default("bar"),
}),
plugins: [...corePlugins],
setup({ options }) {
const { resolve } = createResolver(import.meta.url);
return {
"astro:config:setup": ({ watchIntegration }) => {
watchIntegration(resolve())
}
}
}
})

View File

@ -0,0 +1,67 @@
---
import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import '../styles/global.css';
import { getPublication } from '../hn-gql'
import { SEO } from "astro-seo";
import { AstroFont } from "astro-font";
import type { AstroHashnodeLayoutProps } from '../proptypes/layouttypes'
const pubData = await getPublication();
const pubHeader = pubData.title;
const { pageTitle, hideFooter, hideHeader, ogImage } = Astro.props as AstroHashnodeLayoutProps;
---
<html lang="en">
<head>
<AstroFont
config={[
{
src: [],
name: "Poppins",
googleFontsURL: 'https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,400;0,600;1,400;1,700&display=swap',
preload: true,
display: "swap",
selector: "body",
cssVariable: "astro-font",
fallback: "sans-serif",
},
]}
/>
<SEO
title={pageTitle ? pageTitle + " | " + pubHeader : pubHeader}
description={pubData.descriptionSEO}
charset='utf-8'
openGraph={{
basic: {
title: pageTitle ? pageTitle + " | " + pubHeader : pubHeader,
type: 'text',
image: ogImage || pubData.favicon,
},
optional: {
description: pubData.descriptionSEO,
siteName: pubHeader,
}
}}
extend={{
link: [
{ rel: 'icon', type: 'image/svg+xml', href: pubData.favicon },
],
meta: [
{ name: 'viewport', content: "width=device-width, initial-scale=1" },
{ name: 'generator', content: Astro.generator },
]
}}
/>
</head>
<body>
<div class="flex flex-col">
{!hideHeader && <Header />}
<div class="flex flex-wrap flex-col mt-0 mr-auto mb-0 ml-auto lg:w-[60%]">
<slot />
</div>
{!hideFooter && <Footer />}
</div>
</body>
</html>

View File

@ -0,0 +1,50 @@
---
import { Layout } from "virtual:astro-hashnode/components";
import { getAllPosts, getPost } from '../../hn-gql';
import Tag from '../../components/Tag.astro';
import Author from '../../components/Author.astro';
import { Markup } from 'astro-remote';
import { Image } from 'astro:assets';
import RemoteImage from '../../components/astro-remote/RemoteImage.astro';
export async function getStaticPaths() {
const data = await getAllPosts();
const allPosts = data.publication.posts.edges;
return allPosts.map((post) => {
return {
params: { slug: post.node.slug },
}
})
}
const { slug } = Astro.params;
const post = await getPost(slug);
---
<Layout pageTitle={post.title} ogImage={post.coverImage.url}>
<article class="bg-white p-3 mt-3 flex flex-col">
<Image
class="rounded-lg"
src={post.coverImage.url}
alt={post.title}
inferSize={true}
loading="eager"
/>
<h1 class="text-4xl font-bold pt-5">{post.title}</h1>
<h2 class="text-xl pt-3 pb-3" aria-label="CoverPhoto Subtitle">{post.subtitle}</h2>
<Author post={post} />
<div class="flex flex-wrap justify-center items-center mt-5 mb-5">
{post.tags && post.tags.map((tag) => <Tag tag={tag} />)}
</div>
<div class="post-details">
<Markup
content={post.content.html}
components={{
img: RemoteImage
}}
/>
</div>
</article>
</Layout>

View File

@ -0,0 +1,20 @@
---
import { Layout } from "virtual:astro-hashnode/components";
import { getAboutPage } from '../../hn-gql';
import { Markdown } from 'astro-remote';
import RemoteImage from '../../components/astro-remote/RemoteImage.astro';
const data = await getAboutPage();
---
<Layout pageTitle="About">
<div class="flex flex-col justify-center p-2">
<h2 class="text-3xl mb-3">{data.title} Page</h2>
<div class="about-content">
<Markdown content={data.content.markdown}
components={{
img: RemoteImage
}}/>
</div>
</div>
</Layout>

View File

@ -0,0 +1,17 @@
---
import { Layout } from "virtual:astro-hashnode/components";
import Posts from '../../components/Posts.astro';
import { getAllPosts, getPublication } from '../../hn-gql';
const data = await getAllPosts();
const pub = await getPublication();
const allPosts = data.publication.posts.edges;
---
<Layout pageTitle="Blog">
<div class="flex flex-col justify-center items-center p-2">
<h2 class="text-2xl pt-2 font-semibold">{`Welcome to ${pub.displayTitle || pub.title}`}</h2>
<Posts allPosts={allPosts}/>
</div>
</Layout>

View File

@ -0,0 +1,47 @@
---
import { Layout } from "virtual:astro-hashnode/components";
import Posts from '../../../components/Posts.astro';
import {getAllPosts} from '../../../hn-gql';
import Taged from '../../../components/Tag.astro';
export async function getStaticPaths() {
const data = await getAllPosts();
const allPosts = data.publication.posts.edges;
const allTags = [...new Set(allPosts.map((post) => post.node.tags).flat())];
const jsonObject = allTags.map((object) => JSON.stringify(object));
const uniqueSet = new Set(jsonObject);
const uniqueTags = Array.from(uniqueSet).map((u) => JSON.parse(u));
return uniqueTags.map((uTag) => {
const filteredPosts: { node: { author: { name: string; profilePicture: string; }; publishedAt: string; title: string; subtitle: string; brief: string; slug: string; readTimeInMinutes: number; content: { html: string; }; tags: { name: string; slug: string; }[]; coverImage: { url: string; }; }; }[] = [];
allPosts.forEach((post) => {
const tags = post.node.tags;
tags.forEach((tag) => {
if(tag.slug === uTag.slug) {
filteredPosts.push(post)
}
})
})
return {
params: { tag: uTag.slug },
props: { posts: filteredPosts, matchedTag: uTag },
};
});
}
const { tag } = Astro.params;
const { posts, matchedTag } = Astro.props;
const baseURL = import.meta.env.BASE_URL;
---
<Layout pageTitle={tag}>
<div class="flex pt-3">
<p class="text-lg pt-1 px-1 mr-1">{posts.length} post(s) matched the tag</p>
<Taged tag={matchedTag} />
<span class="mx-3 mt-1 block font-bold text-slate-500 text-xl"> | </span>
<a class="mt-1.5" href={`${baseURL}blog`}>See all posts</a>
</div>
<Posts allPosts={posts} />
</Layout>

View File

@ -0,0 +1,27 @@
---
import { Image } from "astro:assets"
import { Layout } from "virtual:astro-hashnode/components";
import background from "../assets/blog.jpg";
---
<Layout pageTitle="Home" hideFooter hideHeader>
<div class="flex relative flex-col justify-center items-center h-screen">
<Image
class="w-full h-screen bg-center bg-cover blur-sm"
alt="Background"
src={background}
height={1080}
width={1920}
loading="eager"
/>
<div class="absolute p-2 flex flex-col justify-center items-center z-10 bg-purple-50 lg:w-2/5 h-1/4 rounded-md">
<div class="flex pb-5 mb-5 text-5xl text-purple-800">
<p>Hashnode Blog</p>
</div>
<div>
<a href="/blog" class="bg-purple-700 text-white hover:bg-purple-900 p-3 rounded-sm text-lg text mr-2">
TAKE ME TO THE BLOG
</a>
</div>
</div>
</Layout>

View File

@ -0,0 +1,6 @@
export type AstroHashnodeLayoutProps = {
pageTitle: string;
hideHeader?: boolean;
hideFooter?: boolean;
ogImage?: string;
}

View File

@ -0,0 +1,18 @@
import { z } from "astro/zod";
import { createResolver } from 'astro-integration-kit';
const { resolve } = createResolver(import.meta.url)
export function LayoutConfigSchema() {
return z
.string()
.optional()
}
export const optionsSchema = z.object({
hashnodeURL: z.string(),
landingPage: z.boolean().default(true),
layoutComponent: LayoutConfigSchema(),
verbose: z.boolean().default(false),
});
export type Options = z.infer<typeof optionsSchema>;

View File

@ -0,0 +1,117 @@
html {
background-color: #f1f5f9;
font-family: var(--astro-font);
}
body {
line-height: 1.5;
margin: 0 auto;
}
@media only screen and (max-width: 560px) {
body {
display: flex;
}
}
* {
box-sizing: border-box;
}
hr{
padding: 10px;
}
blockquote {
background: #f9f9f9;
border-left: 10px solid #ccc;
margin: 1.5em 10px;
padding: 0.5em 10px;
quotes: "\201C""\201D""\2018""\2019";
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: 0.1em;
margin-right: 0.25em;
vertical-align: -0.4em;
}
blockquote p {
display: inline;
}
.post-details p{
color: rgb(17, 22, 20);
margin: 2px;
}
.post-details h2{
font-size: 25px;
margin-top: 5px;
margin-bottom: 5px;
font-weight: 600;
}
.post-details h3{
font-size: 20px;
margin-top: 5px;
margin-bottom: 5px;
font-weight: 600;
}
.post-details img{
margin: 0 auto;
padding: 5px;
}
.post-details UL {
margin: 5px;
padding: 15px;
}
.post-details li{
list-style: square;
}
.post-details p{
margin-top: 5px;
margin-bottom: 5px;
}
.post-details a{
text-decoration: underline;
}
.post-details pre{
border: 1px solid #ebebeb;
border-radius: 5px;
padding: 2px;
margin: 2px;
background-color: rgb(246, 244, 244);
}
.about-content h1{
font-size: 25px;
margin-top: 5px;
margin-bottom: 5px;
}
.about-content p{
margin-top: 5px;
margin-bottom: 5px;
}
.about-content UL{
margin: 5px;
padding: 5px;
}
.about-content UL LI {
padding: 5px;
margin: 5px;
}
.about-content UL LI a{
margin-right: 2px;
text-decoration: underline;
}

View File

@ -0,0 +1 @@
@import "tailwindcss";

View File

@ -0,0 +1,7 @@
export const getFormattedDate = (dateString: string) => {
return new Intl.DateTimeFormat('en-US', {
year: "numeric",
month: "long",
day: "numeric",
}).format(new Date(dateString));
}

View File

@ -1,5 +1,5 @@
{ {
"extends": "astro/tsconfigs/strictest", "extends": "astro/tsconfigs/strict",
"compilerOptions": { "compilerOptions": {
"jsx": "preserve" "jsx": "preserve"
} }

8
package/virtual.d.ts vendored Normal file
View File

@ -0,0 +1,8 @@
declare module 'virtual:astro-hashnode/config' {
const userConfig: import("./src/schemas/user-config").Options;
export default config as userConfig;
}
declare module 'virtual:astro-hashnode/components' {
export const Layout: typeof import("./src/layouts/Layout.astro").default
}

View File

@ -1,7 +1,12 @@
import { defineConfig } from "astro/config"; import { defineConfig } from "astro/config";
import packagename from "@adammatthiesen/astro-commercejs"; import astroHashnode from "@matthiesenxyz/astro-hashnode";
// https://astro.build/config // https://astro.build/config
export default defineConfig({ export default defineConfig({
integrations: [packagename()], integrations: [
astroHashnode({
hashnodeURL: "astroplayground.hashnode.dev",
verbose: true,
})
],
}); });

View File

@ -12,7 +12,7 @@
}, },
"dependencies": { "dependencies": {
"astro": "^4.3.5", "astro": "^4.3.5",
"PACKAGE-NAME": "workspace:*" "@matthiesenxyz/astro-hashnode": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"@astrojs/check": "^0.5.1", "@astrojs/check": "^0.5.1",

View File

@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

View File

@ -1 +1,3 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" /> /// <reference types="astro/client" />
/// <reference types="../.astro/astro-hashnode.d.ts" />

View File

@ -1,5 +0,0 @@
---
---
<h1>Hello World!</h1>

5523
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff