Skip to content
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
26 changes: 26 additions & 0 deletions GET_FEATURED.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,29 @@ Looking to get yourself up there along with others? It's easy!

- Open a "Get Featured" issue via https://github.com/fossunited/forklore/issues/new/choose
- Add your information and responses to the questions in the issue template

## Submit a blog post

You can submit a blog post by creating a pull request that adds a Markdown file to the `content/blog/` directory.

### Steps:

1. Create markdown file:

```sh
touch content/blog/<your-post-slug>.md
```

2. Add front-matter metadata to top of the file:

```markdown
---
title: TITLE
author: username # (if in maintainers list) or simple name to represent
date: 2025-10-17 # ISO format: YYYY-MM-DD
description: A short summary of your post that appears in previews.
tags: ["blog", "example", "topic"] # keep it short
---
```

That's it, you can do `yarn run dev` to see the live changes in localhost:3000 or do a PR and let us take care!
28 changes: 28 additions & 0 deletions assets/css/main.css
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import "tailwindcss";
@plugin "@tailwindcss/typography";

@custom-variant dark (&:where(.dark-mode, .dark-mode *));

Expand Down Expand Up @@ -35,6 +36,8 @@
--color-secondary-light: #18222a;
--color-tertiary-dark: #3c4b4e;
--color-tertiary-light: #eef0f1;
--color-quaternary-dark: #495B5F;
--color-quaternary-light: #DDE1E3;
}

html {
Expand Down Expand Up @@ -108,6 +111,31 @@ html {
.sans-text a {
text-decoration: underline;
}

.tag-card {
@apply text-sm px-2 py-1;
background-color: var(--color-quaternary-light);
color: var(--color-secondary-light);
}

.dark-mode .tag-card {
background-color: var(--color-quaternary-dark);
color: var(--color-secondary-dark);
}

.prose h1 {
font-size: 1.6rem;
}

.prose h1 > a,
.prose h2 > a,
.prose h3 > a,
.prose h4 > a,
.prose h5 > a,
.prose h6 > a {
text-decoration: none !important;
}

}

@layer base {
Expand Down
44 changes: 44 additions & 0 deletions components/AuthorLink.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script setup lang="ts">
import { ref, watch } from 'vue'

const props = defineProps<{ author: string }>()

const maintainer = ref(null)

const fetchMaintainer = async (username: string) => {
if (!username) {
maintainer.value = null
return
}

const { data } = await useAsyncData(
() => `maintainer-${username}`,
() =>
queryCollection('maintainers')
.where('username', '==', username)
.first()
)
maintainer.value = data.value
}

watch(
() => props.author,
(newAuthor) => {
fetchMaintainer(newAuthor)
},
{ immediate: true }
)
</script>

<template>
<span>
<template v-if="maintainer">
<NuxtLink :to="`/maintainers/${maintainer.username}`" class="underline text-primary">
{{ maintainer.full_name || maintainer.username }}
</NuxtLink>
</template>
<template v-else>
<strong>{{ author }}</strong>
</template>
</span>
</template>
6 changes: 5 additions & 1 deletion components/HeaderLinks.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ const navbarItems = [
href: "https://fossunited.org/grants",
},
{
label: "Discussion Forum",
label: "Blog",
href: "/blog",
},
{
label: "Forum",
href: "https://forum.fossunited.org/c/maintainers/",
},
];
Expand Down
2 changes: 1 addition & 1 deletion components/ProjectCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defineProps(["project"]);
</a>
</div>
</div>
<p class="text-sm">
<p class="text-sm sans-text">
{{ project.short_description }}
</p>
</div>
Expand Down
19 changes: 18 additions & 1 deletion content.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineCollection, defineContentConfig, z } from "@nuxt/content";
import { defineCollection, defineContentConfig, property } from "@nuxt/content";
import { z } from 'zod'

const maintainerSchema = z.object({
username: z.string(),
Expand Down Expand Up @@ -37,5 +38,21 @@ export default defineContentConfig({
source: "maintainers/**.json",
schema: maintainerSchema,
}),
blog: defineCollection({
type: 'page',
source: 'blog/*.md',
schema: z.object({
title: z.string(),
author: z.string(),
description: z.optional(z.string()),
date: z.date(),
draft: z.optional(z.boolean()),
tags: z.optional(z.array(z.string())),
hero: z.object({
image: property(z.string()).editor({ input: 'media' }),
caption: z.optional(z.string())
})
})
})
},
});
80 changes: 80 additions & 0 deletions content/sample-blog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
title: TITLE
author: username (if in maintainers list or simply name)
date: 2025-10-17 (ISO format YYYY-MM-DD)
description: Content appears as small description.
tags: ["blog", "hello"]
---

NOTE: Please start writing heading from level-2 (##)

## Heading One

Welcome to the **prose formatting demo**. This blog post covers most of the Markdown formatting styles you'll use.

## Heading Two

Lorem ipsum dolor sit amet, *consectetur* adipiscing elit. `Inline code` looks like this.

Here’s a simple [link to Nuxt](https://nuxt.com).

### Heading Three (`h3`)

> This is a blockquote.
> It supports multiple lines and renders with indentation.

#### Heading Four

- Unordered list item 1
- Unordered list item 2
- Nested item
- Another nested item
- Final unordered item

1. Ordered list item one
2. Ordered list item two
3. Ordered list item three

---

## 📸 Images

![forklore logo](../public/logo/logo.svg)

---

## 🧠 Code Blocks

### JavaScript Example

```js
export default defineNuxtConfig({
modules: ['@nuxt/content'],
})
```


### Html

```html
<div class="prose dark:prose-invert">
<p>Hello world</p>
</div>
```

## Table

| Key | Type | Description |
| --- | --------- | ----------- |
| 1 | Wonderful | Table |
| 2 | Wonderful | Data |
| 3 | Wonderful | Website |


| Feature | Supported | Notes |
|----------------|-----------|----------------------------|
| Headings | ✅ | `#`, `##`, `###`, etc. |
| Lists | ✅ | Ordered + unordered |
| Code blocks | ✅ | Syntax highlighting works |
| Blockquotes | ✅ | Fully styled |
| Tables | ✅ | Supported by Markdown |
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"**/*.svg",
"package.json",
"content/sample.json",
"content/sample-blog.md",
"**/*.ts"
]
}
31 changes: 24 additions & 7 deletions nuxt.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,26 @@ export default defineNuxtConfig({
modules: ["@nuxt/content", "@nuxtjs/color-mode"],
css: ["/assets/css/main.css"],
vite: {
plugins: [tailwindcss()],
plugins: [tailwindcss(), require("@tailwindcss/typography")],
},
colorMode: {
storage: 'cookie',
storage: "cookie",
},
content: {
build: {
markdown: {
toc: {
depth: 2,
},
highlight: {
theme: 'one-dark-pro',
},
},
},
},
app: {
head: {
viewport: 'width=device-width, initial-scale=1',
viewport: "width=device-width, initial-scale=1",

htmlAttrs: {
lang: "en",
Expand All @@ -31,10 +43,15 @@ export default defineNuxtConfig({
},
},
compatibilityDate: "2025-05-15",
routeRules: {
"/maintainers": {
redirect: "/",
},
},
nitro: {
prerender: {
prerender: {
crawlLinks: true,
routes: ['/rss']
}
}
routes: ["/rss"],
},
},
});
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@nuxt/content": "^3.7.1",
"@nuxtjs/color-mode": "3.5.2",
"@tailwindcss/vite": "^4.1.15",
"@tailwindcss/typography": "^0.5.19",
"better-sqlite3": "^12.4.1",
"feed": "^5.1.0",
"minisearch": "^7.2.0",
Expand Down
75 changes: 75 additions & 0 deletions pages/blog/[slug].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<script lang="ts" setup>
import AuthorLink from '@/components/AuthorLink.vue'
const route = useRoute();

const { data: page } = await useAsyncData(route.path, () => {
return queryCollection("blog").path(route.path).first();
});

const formatDate = (dateString) => {
const date = new Date(dateString);
return date.toLocaleDateString("en-GB", {
day: "2-digit",
month: "short",
year: "numeric",
});
};
</script>

<template>
<div v-if="page" class="max-w-5xl mx-auto px-4 py-12 flex flex-col gap-6">
<div
class="bg-tertiary-light dark:bg-tertiary-dark border-custom p-8 flex flex-col gap-4"
>
<h1 class="text-3xl font-bold">{{ page.title }}</h1>

<div class="text-base text-secondary-light dark:text-secondary-dark">
<p>
by <AuthorLink :author="page.author" /> • {{ formatDate(page.date) }}
</p>
</div>

<div v-if="page.tags?.length" class="flex flex-wrap gap-2 mt-2">
<span v-for="tag in page.tags" :key="tag" class="tag-card">
{{ tag }}
</span>
</div>
</div>

<div
v-if="page.body?.toc?.links?.length"
class="toc-wrapper border-l-4 pl-4"
>
<h2 class="text-lg font-bold mb-2">Table of Contents</h2>
<ul class="space-y-1 text-sm list-disc list-inside">
<li v-for="link in page.body.toc.links" :key="link.id">
<a :href="`#${link.id}`" class="hover:underline">
{{ link.text }}
</a>

<ul v-if="link.children" class="ml-4 list-disc">
<li v-for="child in link.children" :key="child.id">
<a :href="`#${child.id}`" class="hover:underline">
{{ child.text }}
</a>
</li>
</ul>
</li>
</ul>
</div>

<div class="prose dark:prose-invert sans-text max-w-none px-2">
<ContentRenderer :value="page" :prose="true" class="max-w-none" />
</div>
</div>

<div v-else class="empty-page text-center py-24">
<h1 class="text-2xl font-bold">Page Not Found</h1>
<p class="text-gray-500 mt-2">
Oops! The content you're looking for doesn't exist.
</p>
<NuxtLink to="/" class="mt-4 inline-block underline">
Go back home
</NuxtLink>
</div>
</template>
Loading