Skip to content

Blog into docs #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions .firebaserc
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,11 @@
"targets": {
"docs-and-blog": {
"hosting": {
"docs": [
"gumnut-docs"
],
"blog": [
"gumnut-blog"
]
}
}
},
"etags": {}
}
}
19 changes: 19 additions & 0 deletions docs/.vitepress/case-studies.data.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createContentLoader } from "vitepress";

export default createContentLoader("case-studies/**/*.md", {
excerpt: true,
transform(raw) {
return raw
.map(({ url, frontmatter, excerpt }) => ({
title: frontmatter.title,
url,
excerpt,
date: frontmatter.date,
tags: frontmatter.tags || [],
author: frontmatter.author,
description: frontmatter.description,
image: frontmatter.image,
}))
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
},
});
132 changes: 109 additions & 23 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,62 @@
import { defineConfig } from "vitepress";
import { getFeed } from "./getFeed";
import { createContentLoader } from "vitepress";
import implicitFigures from "markdown-it-implicit-figures";

// Helper function to parse dates consistently
function parseDate(dateStr: any): Date {
if (!dateStr) {
return new Date(0); // Return a very old date if no date is provided
}
if (typeof dateStr !== "string") {
return new Date(dateStr); // Try to parse whatever we got
}
// If the date string is already in ISO format, use it directly
if (dateStr.includes("T")) {
return new Date(dateStr);
}
// Otherwise, assume it's in YYYY-MM-DD format and add time
return new Date(`${dateStr}T00:00:00.000Z`);
}

// https://vitepress.dev/reference/site-config
export default defineConfig({
title: "Docs",
description: "Documentation for Gumnut - the modern textbox",
title: "Gumnut 🦩",
description: "Modern SaaS for teams",
ignoreDeadLinks: true,
cleanUrls: true,
themeConfig: {
// https://vitepress.dev/reference/default-theme-config
siteTitle: false,
externalLinkIcon: false,
logo: {
light: "/logo/light.svg",
dark: "/logo/dark.svg",
alt: "Gumnut Logo",
},
// nav: [{ text: "Support", link: "mailto:[email protected]" }],
nav: [
{ text: "Pricing", link: "/pricing" },
{
text: "Resources",
items: [
{ text: "Blog", link: "/blog" },
{ text: "Questions Hub", link: "/questions" },
],
},
{ text: "Case Studies", link: "/case-studies" },
{ text: "Docs", link: "/docs" },
{ text: "Login", link: "https://dashboard.gumnut.dev" },
{ text: "Book a demo", link: "https://calendly.com/owen-gumnut/30min" },
],
socialLinks: [
{ icon: "discord", link: "https://discord.gg/yu3u87AUNR" },
{ icon: "github", link: "https://github.com/gumnutdev" },
{
icon: "linkedin",
link: "https://www.linkedin.com/company/gumnut-dev/",
},
{
icon: "discord",
link: "https://discord.gg/yu3u87AUNR",
},
],
sidebar: [
{
Expand All @@ -28,33 +65,33 @@ export default defineConfig({
{
text: "Get Started",
items: [
{ text: "Introduction", link: "/introduction" },
{ text: "Quick Start", link: "/quickstart" },
{ text: "Walkthrough", link: "/walkthrough" },
{ text: "Introduction", link: "/docs/" },
{ text: "Quick Start", link: "/docs/quickstart" },
{ text: "Walkthrough", link: "/docs/walkthrough" },
],
},
{
text: "Guides",
items: [
{ text: "Authentication", link: "/guides/authentication" },
{ text: "Hackathon", link: "/guides/hackathon" },
{ text: "Agent", link: "/guides/agent" },
{ text: "Authentication", link: "/docs/guides/authentication" },
{ text: "Hackathon", link: "/docs/guides/hackathon" },
{ text: "Agent", link: "/docs/guides/agent" },
],
},
{
text: "Frameworks",
items: [
{ text: "React", link: "/components/react" },
{ text: "Vue", link: "/components/vue" },
{ text: "React", link: "/docs/components/react" },
{ text: "Vue", link: "/docs/components/vue" },
],
},
{
text: "Components",
items: [
{ text: "Gumnut Data", link: "/components/gumnut-data" },
{ text: "Gumnut Focus", link: "/components/gumnut-focus" },
{ text: "Gumnut Status", link: "/components/gumnut-status" },
{ text: "Gumnut Text", link: "/components/gumnut-text" },
{ text: "Gumnut Data", link: "/docs/components/gumnut-data" },
{ text: "Gumnut Focus", link: "/docs/components/gumnut-focus" },
{ text: "Gumnut Status", link: "/docs/components/gumnut-status" },
{ text: "Gumnut Text", link: "/docs/components/gumnut-text" },
],
},
],
Expand All @@ -65,20 +102,25 @@ export default defineConfig({
{
text: "API Documentation",
items: [
{ text: "Gumnut API", link: "/api-reference/gumnut-api" },
{ text: "Gumnut Doc", link: "/api-reference/gumnut-doc" },
{ text: "Gumnut Node", link: "/api-reference/gumnut-node" },
{ text: "Gumnut API", link: "/docs/api-reference/gumnut-api" },
{ text: "Gumnut Doc", link: "/docs/api-reference/gumnut-doc" },
{ text: "Gumnut Node", link: "/docs/api-reference/gumnut-node" },
],
},
],
},
],
footer: {
copyright: "Copyright © 2025 Gumnut Dev Pty Ltd",
},
markdown: {
config: (md) => {
md.use(implicitFigures, {
figcaption: true,
copyAttrs: "^class$",
});
},
},
head: [
["link", { rel: "icon", href: "/logo/favicon.ico" }],
["link", { rel: "icon", href: "/images/favicon.ico" }],
[
"script",
{
Expand All @@ -88,4 +130,48 @@ export default defineConfig({
},
],
],
vite: {
css: {
devSourcemap: true,
},
},
async transformPageData(pageData) {
if (pageData.relativePath.startsWith("articles/")) {
if (pageData.frontmatter.image) {
pageData.frontmatter.class = "has-header-image";
}
}

if (pageData.relativePath.startsWith("case-studies/")) {
if (pageData.frontmatter.image) {
pageData.frontmatter.class = "has-header-image";
}
}
},
async buildEnd() {
// Create content loader for blog posts for RSS feed
const postsLoader = createContentLoader("articles/*.md", {
includeSrc: true,
render: true,
excerpt: true,
transform(rawData) {
return rawData
.sort((a, b) => {
const dateA = parseDate(a.frontmatter.date);
const dateB = parseDate(b.frontmatter.date);
return dateB.getTime() - dateA.getTime();
})
.map((page) => {
return {
url: page.url,
frontmatter: page.frontmatter,
excerpt: page.excerpt,
};
});
},
});

const posts = await postsLoader.load();
await getFeed(posts);
},
});
63 changes: 63 additions & 0 deletions docs/.vitepress/getFeed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Feed } from "feed";
import { fileURLToPath } from "url";
import { dirname, resolve } from "path";
import fs from "fs";

// Define the post type to match VitePress ContentData
interface Post {
url: string;
frontmatter: Record<string, any>;
}

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

export async function getFeed(posts: Post[]) {
const feed = new Feed({
title: "The Collaborative Textbox",
description: "Building Gumnut - the modern textbox",
id: "https://blog.gumnut.dev/",
link: "https://blog.gumnut.dev/",
language: "en",
image: "https://blog.gumnut.dev/logo/dark.svg",
favicon: "https://blog.gumnut.dev/logo/dark.svg",
copyright: `Copyright © ${new Date().getFullYear()} Gumnut`,
updated: new Date(),
generator: "VitePress",
feedLinks: {
rss2: "https://blog.gumnut.dev/rss.xml",
},
});

// Add posts to feed
for (const post of posts) {
feed.addItem({
title: post.frontmatter.title || "Untitled",
id: post.url,
link: `https://blog.gumnut.dev${post.url}`,
description: post.frontmatter.description || "",
content: post.frontmatter.description || "",
author: [
{
name: post.frontmatter.author || "Gumnut Team",
},
],
date: new Date(post.frontmatter.date || Date.now()),
});
}

// Ensure the dist directory exists
const distDir = resolve(__dirname, "dist");
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}

// Write feed to file
fs.writeFileSync(resolve(distDir, "rss.xml"), feed.rss2());

// Write posts data to a JSON file for the Vue component
fs.writeFileSync(
resolve(distDir, "posts.json"),
JSON.stringify(posts, null, 2)
);
}
19 changes: 19 additions & 0 deletions docs/.vitepress/posts.data.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createContentLoader } from "vitepress";

export default createContentLoader("articles/**/*.md", {
excerpt: true,
transform(raw) {
return raw
.map(({ url, frontmatter, excerpt }) => ({
title: frontmatter.title,
url,
excerpt,
date: frontmatter.date,
tags: frontmatter.tags || [],
author: frontmatter.author,
description: frontmatter.description,
image: frontmatter.image,
}))
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
},
});
34 changes: 34 additions & 0 deletions docs/.vitepress/theme/MyLayout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup>
import DefaultTheme from 'vitepress/theme'
import SiteFooter from './components/SiteFooter.vue'
import BlogHeader from './components/BlogHeader.vue'
import { useData } from 'vitepress/client'
import { computed } from 'vue'

const { Layout } = DefaultTheme
const { page } = useData()

// Check if this is an individual blog post (articles directory) and not a listing page
const isBlogPost = computed(() =>
page.value?.relativePath?.startsWith('articles/') &&
page.value?.relativePath?.includes('.md') &&
!page.value?.relativePath?.endsWith('index.md')
)

// Check if this is a docs page (docs directory)
const isDocsPage = computed(() =>
page.value?.relativePath?.startsWith('docs/') ||
(page.value?.url && page.value.url.startsWith('/docs/'))
)
</script>

<template>
<Layout>
<template #doc-before>
<BlogHeader v-if="isBlogPost" />
</template>
<template #layout-bottom>
<SiteFooter v-if="!isDocsPage" />
</template>
</Layout>
</template>
39 changes: 39 additions & 0 deletions docs/.vitepress/theme/components/BlogHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<script setup lang="ts">
import { useData } from 'vitepress/client'
import { defineComponent } from 'vue'

interface PageData {
frontmatter: {
image?: string
title?: string
}
}

const { page } = useData()

defineComponent({
name: 'BlogHeader'
})
</script>

<template>
<div v-if="page?.frontmatter?.image" class="blog-header-image">
<img :src="page.frontmatter.image" :alt="page.frontmatter.title" />
</div>
</template>

<style scoped>
.blog-header-image {
width: 100%;
max-height: 400px;
overflow: hidden;
margin-bottom: 2rem;
border-radius: 8px;
}

.blog-header-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
Loading