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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.nitro
.cache
dist
_site

# Node dependencies
node_modules
Expand Down
130 changes: 130 additions & 0 deletions docs/migration-architecture-brief.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Forklore Migration Architecture Brief

## Premise

Forklore is not just a directory. It is a browsable archive of maintainer stories, projects, posts, and small pieces of personality. The migration should reduce dependency churn without flattening the experience into static HTML-only pages.

## Goals

- Keep authoring ergonomic for maintainers and contributors.
- Preserve interactive UI components for search, filtering, exploration, and story-led browsing.
- Move validation, normalization, and derived fields into one data pipeline.
- Keep generated routes static and GitHub Pages friendly.
- Reduce recurring maintenance from framework upgrades, generated feed commits, and schema drift.

## Recommended Direction

Use a dead-simple static site generator for the public site, with Markdown/frontmatter as the maintainer authoring format.

Current spike candidate: Eleventy.

Rationale:

- Maintainers can be authored as readable Markdown instead of JSON.
- Structured fields stay in frontmatter for templates, validation, and automation.
- Long answers, notes, and profile content stay in Markdown where humans can edit them.
- One template can generate all maintainer detail pages.
- The landing page can still be interactive through small progressive-enhancement scripts.
- The dependency surface is much smaller than Nuxt plus Nuxt Content plus Nitro.

Astro remains the fallback if the Eleventy spike proves too limiting for component ergonomics. Zola is less attractive for this repo because per-maintainer pages from structured content require an extra generation layer, which becomes another thing to maintain.

## Content Model

Maintainer profiles should move from machine-first JSON to human-first Markdown/frontmatter:

```text
content/maintainers/<username>.md
```

Frontmatter owns structured fields:

- `username`
- `full_name`
- `photo`
- `designation`
- `socials`
- `projects`
- `created_on`

Markdown body owns long-form answers and story content.

GitHub issue automation can still generate or update these files later. The important shift is that humans reviewing a PR can read and edit the file without fighting JSON escaping.

## Shared Data Layer

All prototype branches should consume the same generated data artifacts:

- `maintainers`: normalized profiles with canonical social labels.
- `projects`: flattened project index with maintainer references.
- `answers`: extracted form answers keyed by stable question IDs.
- `planet`: post metadata and optional full post content.
- `search`: lightweight precomputed search records.
- `stats`: counts and facets used by UI controls.

## Migration Constraints

- Preserve public routes where possible:
- `/`
- `/maintainers/:username`
- `/planet`
- `/planet/:username`
- `/planet/:username/:slug`
- `/rss.xml`
- `/planet/rss.xml`
- Do not commit recurring Planet refresh output unless explicitly curated.
- Keep images local, but add size validation and optimization checks.
- Treat the maintainer schema as a single source of truth and validate Markdown frontmatter against it.

## Planet Strategy

Planet remains part of the architecture, but it should not block the maintainer-page migration.

- Maintainer RSS URLs live in maintainer frontmatter.
- Planet posts are machine-fetched content, not human-authored content.
- A later phase should fetch feeds during deploy or through a scheduled action.
- Generated Planet cache should either be deploy-only or committed through explicit review PRs, not recurring unreviewed churn.
- Existing Planet routes should be preserved when the Planet phase starts.

## Spike Scope

The first implementation spike should prove:

- Markdown/frontmatter can represent current maintainer data.
- A landing page can list maintainers and projects.
- A detail page can render profile, image, socials, project buttons, and long-form body content.
- The current Nuxt app can remain in place while the static spike is evaluated.

The spike must not change the production deployment path. Until the migration is explicitly accepted:

- `yarn generate` continues to run the Nuxt site.
- GitHub Pages continues to publish `dist` from Nuxt.
- Eleventy is isolated under `site/` and is not installed by the root deployment workflow.
- Eleventy is only invoked from the spike package with `cd site && yarn build` or `cd site && yarn serve`.
- `_site/` remains ignored and is not a deployment artifact.

## Visual Parity Requirement

The migration baseline should look like the current Nuxt site before design exploration starts.

The Eleventy spike should preserve:

- Dark default palette.
- Centered `max-w-screen-md` style shell.
- Two-pixel dashed borders and dividers.
- Current header links.
- Current home intro copy and Planet CTA.
- Maintainer cards with profile header and project sections.
- Maintainer detail pages with project column and maintainer/story column.

Intentional redesign work belongs on later `design/*` branches, not the migration branch.

## Prototype Branches

The three design branches should branch from this migration base:

- `design/interactive-wall`
- `design/lore-map`
- `design/story-scroll`

Each branch should first define the design system, interaction model, and data requirements before implementation.
82 changes: 82 additions & 0 deletions eleventy.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
const SOCIAL_LABELS = {
github: "GitHub",
gitlab: "GitLab",
codeberg: "Codeberg",
bitbucket: "BitBucket",
linkedin: "LinkedIn",
x: "X",
"x/twitter": "X",
twitter: "X",
mastodon: "Mastodon",
bluesky: "Bluesky",
blog: "Web",
web: "Web",
website: "Web",
rss: "RSS",
medium: "Medium",
substack: "Substack",
reddit: "Reddit",
youtube: "Youtube",
};

export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy({ "public/images": "images" });
eleventyConfig.addPassthroughCopy({ "public/logo": "logo" });
eleventyConfig.addPassthroughCopy({ "public/favicon.svg": "favicon.svg" });
eleventyConfig.addPassthroughCopy({
"public/maintainer_photo_light.svg": "maintainer_photo_light.svg",
});
eleventyConfig.addPassthroughCopy({
"public/maintainer_photo_dark.svg": "maintainer_photo_dark.svg",
});
eleventyConfig.addPassthroughCopy({ "site/assets": "assets" });

eleventyConfig.addCollection("maintainers", (collectionApi) =>
collectionApi
.getFilteredByTag("maintainer")
.sort(
(a, b) =>
new Date(b.data.created_on).getTime() -
new Date(a.data.created_on).getTime(),
),
);

eleventyConfig.addFilter("socialLabel", (label) => {
if (!label) return "Web";
return SOCIAL_LABELS[String(label).trim().toLowerCase()] || label;
});

eleventyConfig.addFilter("dateISO", (date) => {
const value = new Date(date);
return Number.isNaN(value.getTime()) ? "" : value.toISOString();
});

eleventyConfig.addFilter("year", (date) => {
const value = new Date(date);
return Number.isNaN(value.getTime()) ? "" : value.getFullYear();
});

eleventyConfig.addFilter("firstEmoji", (content) => {
const match = String(content || "").match(
/convey what it is like to be a FOSS maintainer[\s\S]*?<p>(.*?)<\/p>/i,
);
if (!match) return "";
const text = match[1].replace(/<[^>]*>/g, "").trim();
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
for (const item of segmenter.segment(text)) {
if (/\p{Extended_Pictographic}/u.test(item.segment)) return item.segment;
}
return "";
});

return {
dir: {
input: "site",
includes: "_includes",
data: "_data",
output: "_site",
},
markdownTemplateEngine: "njk",
htmlTemplateEngine: "njk",
};
}
6 changes: 6 additions & 0 deletions site/_data/site.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
title: "Forklore",
description:
"Confessions, quirks, and occasional rants from India's open source keepers.",
url: "https://forklore.in",
};
59 changes: 59 additions & 0 deletions site/_includes/base.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<!doctype html>
<html lang="en" class="dark-mode">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% if full_name %}{{ full_name }} | Forklore{% else %}{{ title or site.title }}{% endif %}</title>
<meta
name="description"
content="{% if full_name %}Get to know {{ full_name }} and their work.{% else %}{{ description or site.description }}{% endif %}"
>
<link rel="icon" href="/favicon.svg">
<link rel="stylesheet" href="/assets/main.css">
<script>
const storedTheme = localStorage.getItem("forklore-theme");
if (storedTheme === "light") {
document.documentElement.classList.remove("dark-mode");
document.documentElement.classList.add("light-mode");
}
</script>
</head>
<body>
<div class="page-shell">
<header class="site-header">
<a class="brand" href="/" aria-label="Forklore home">
<img src="/logo/logo_light.svg" alt="Forklore" class="brand-mark" data-theme-logo>
</a>
<nav class="site-nav" aria-label="Primary">
<a href="https://github.com/fossunited/forklore/blob/develop/GET_FEATURED.md">Get featured</a>
<a href="https://fossunited.org/grants">FOSS United Grants</a>
<a href="https://forum.fossunited.org/c/maintainers/">Discussion Forum</a>
</nav>
<button class="button solid theme-toggle" type="button" data-theme-toggle aria-label="Switch to Light Mode">
</button>
</header>
<main>
{{ content | safe }}
</main>
</div>
<footer class="site-footer">
<div class="footer-inner">
<div class="footer-copy">
<p>
Built and Maintained by
<a href="https://fossunited.org">FOSS United</a>
</p>
<div class="footer-links">
<a href="https://github.com/fossunited/forklore">Contribute</a>
<a href="https://forum.fossunited.org/c/maintainers/">Forum</a>
<a href="/rss.xml">RSS</a>
</div>
<a href="mailto:foundation@fossunited.org">foundation@fossunited.org</a>
</div>
<img src="/logo/unitedbyfoss_dark.svg" alt="United By FOSS" class="footer-logo">
</div>
</footer>
<script src="/assets/main.js"></script>
</body>
</html>
46 changes: 46 additions & 0 deletions site/_includes/maintainer-card.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<article
class="maintainer-card"
data-maintainer-card
data-name="{{ maintainer.data.full_name }}"
data-username="{{ maintainer.data.username }}"
data-projects="{% for project in maintainer.data.projects %}{{ project.name }} {% endfor %}"
data-created="{{ maintainer.data.created_on | dateISO }}"
>
<a class="card-hit" href="{{ maintainer.url }}" aria-label="Read {{ maintainer.data.full_name }}'s profile"></a>
<header class="maintainer-card-header">
<img
src="{{ maintainer.data.photo or '/maintainer_photo_light.svg' }}"
alt="Photo of {{ maintainer.data.full_name }}"
class="avatar"
loading="lazy"
>
<div>
<h2>{{ maintainer.data.full_name }}</h2>
<p>{{ maintainer.data.designation }}</p>
</div>
</header>
<div
class="project-strip{% if maintainer.data.projects.length > 1 %} project-strip-grid{% endif %}{% if maintainer.data.projects.length % 2 == 1 %} project-strip-odd{% endif %}"
aria-label="Projects"
>
{% for project in maintainer.data.projects %}
<div class="project-summary">
{% if project.logo %}
<img src="{{ project.logo }}" alt="Logo of {{ project.name }}" loading="lazy">
{% endif %}
<div>
<h3>{{ project.name }}</h3>
{% if project.project_link %}
<a class="button subtle card-source" href="{{ project.project_link }}">
View Source <span aria-hidden="true">↗</span>
</a>
{% endif %}
<p>{{ project.short_description }}</p>
</div>
</div>
{% endfor %}
{% if maintainer.data.projects.length > 1 and maintainer.data.projects.length % 2 == 1 %}
<div class="project-summary project-summary-empty" aria-hidden="true"></div>
{% endif %}
</div>
</article>
50 changes: 50 additions & 0 deletions site/_includes/maintainer.njk
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
layout: base.njk
---

<article class="profile-layout">
<section class="profile-main" aria-label="Projects">
{% for project in projects %}
{% include "project-card.njk" %}
{% endfor %}
</section>

<aside class="profile-aside" aria-label="Maintainer details">
<div class="profile-panel">
<div class="profile-card">
<img
src="{{ photo or '/maintainer_photo_light.svg' }}"
alt="Photo of {{ full_name }}"
class="avatar large"
>
<div>
<p class="eyebrow">@{{ username }}</p>
<h1>{{ full_name }}</h1>
<p>{{ designation }}</p>
</div>
</div>

<nav class="social-list" aria-label="Social links">
{% for social in socials %}
<a href="{{ social.link }}" aria-label="{{ social.label | socialLabel }} (opens in new tab)">
{{ social.label | socialLabel }}
</a>
{% endfor %}
</nav>

{% set hasRss = false %}
{% for social in socials %}
{% if (social.label | socialLabel) == "RSS" %}
{% set hasRss = true %}
{% endif %}
{% endfor %}
{% if hasRss %}
<a class="button outline planet-link" href="/planet/{{ username }}/">View posts on Planet</a>
{% endif %}
</div>

<div class="story-content">
{{ content | safe }}
</div>
</aside>
</article>
Loading
Loading