feat: add README rendering and enhanced package metadata display

**Database Schema:**
- Add npmReadme, npmKeywords, npmAuthor, npmMaintainers fields to Tool model

**NPM Client:**
- Add fetchLatestPackageWithMetadata() function to fetch README and top-level metadata
- Export new PackageVersionWithReadme type

**Sync Workers:**
- Update keyword and changes sync to fetch and store README content
- Store author, maintainers, and keywords from package.json

**UI Components:**
- Create Markdown component using react-markdown with GitHub Flavored Markdown
- Add rehype-sanitize for security and remark-gfm for tables/strikethrough support

**Tool Detail Page:**
- Convert from createElement to JSX for better maintainability
- Display README in a dedicated card with proper markdown rendering
- Show NPM keywords, author, and maintainers in sidebar
- Add ThemeToggle to header
- Improve layout with better spacing and organization

This brings the tool detail pages much closer to NPM's package pages,
providing users with comprehensive information about each tool including
the full README documentation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ajax Davis 2025-11-29 22:41:50 +10:00
parent e38e627a07
commit f9ba1c094e
9 changed files with 1583 additions and 560 deletions

View file

@ -1,5 +1,5 @@
import { prisma } from '@tpmjs/db';
import { fetchChanges, fetchLatestPackageVersion } from '@tpmjs/npm-client';
import { fetchChanges, fetchLatestPackageWithMetadata } from '@tpmjs/npm-client';
import { validateTpmjsField } from '@tpmjs/types/tpmjs';
import { type NextRequest, NextResponse } from 'next/server';
import { env } from '~/env';
@ -51,8 +51,8 @@ export async function POST(request: NextRequest) {
// Process each change
for (const change of changesResult.results) {
try {
// Fetch full package metadata
const pkg = await fetchLatestPackageVersion(change.id);
// Fetch full package metadata with README
const pkg = await fetchLatestPackageWithMetadata(change.id);
// Skip if package not found
if (!pkg) {
@ -100,6 +100,10 @@ export async function POST(request: NextRequest) {
npmRepository: pkg.repository ?? undefined,
npmHomepage: pkg.homepage ?? undefined,
npmLicense: pkg.license ?? undefined,
npmKeywords: pkg.topLevelKeywords || pkg.keywords || [],
npmReadme: pkg.readme ?? undefined,
npmAuthor: pkg.author ?? undefined,
npmMaintainers: pkg.maintainers ?? undefined,
category: tpmjsData.category,
description: tpmjsData.description,
example: tpmjsData.example,

View file

@ -1,5 +1,5 @@
import { prisma } from '@tpmjs/db';
import { fetchLatestPackageVersion, searchByKeyword } from '@tpmjs/npm-client';
import { fetchLatestPackageWithMetadata, searchByKeyword } from '@tpmjs/npm-client';
import { validateTpmjsField } from '@tpmjs/types/tpmjs';
import { type NextRequest, NextResponse } from 'next/server';
import { env } from '~/env';
@ -41,8 +41,8 @@ export async function POST(request: NextRequest) {
// Process each package
for (const result of searchResults) {
try {
// Fetch full package metadata
const pkg = await fetchLatestPackageVersion(result.package.name);
// Fetch full package metadata with README
const pkg = await fetchLatestPackageWithMetadata(result.package.name);
// Skip if package not found
if (!pkg) {
@ -90,6 +90,10 @@ export async function POST(request: NextRequest) {
npmRepository: pkg.repository ?? undefined,
npmHomepage: pkg.homepage ?? undefined,
npmLicense: pkg.license ?? undefined,
npmKeywords: pkg.topLevelKeywords || pkg.keywords || [],
npmReadme: pkg.readme ?? undefined,
npmAuthor: pkg.author ?? undefined,
npmMaintainers: pkg.maintainers ?? undefined,
category: tpmjsData.category,
description: tpmjsData.description,
example: tpmjsData.example,