diff --git a/site/scripts/generate-doc-redirects.test.ts b/site/scripts/generate-doc-redirects.test.ts index a1dc65a..2b92f5f 100644 --- a/site/scripts/generate-doc-redirects.test.ts +++ b/site/scripts/generate-doc-redirects.test.ts @@ -1,35 +1,42 @@ import { describe, test, expect } from 'bun:test' import { redirectHtml, computeRedirects } from './generate-doc-redirects' +import { HUB_DOCS } from '../src/data/hub' describe('redirectHtml', () => { test('emits meta-refresh + canonical', () => { - const html = redirectHtml('/fledge/docs/lanes') - expect(html).toContain('') + const html = redirectHtml(HUB_DOCS) + expect(html).toContain(``) expect(html).toContain('canonical') expect(html).toContain('location.replace') }) + + test('includes a visible "moved to CorvidLabs" link for no-JS users', () => { + const html = redirectHtml(HUB_DOCS) + expect(html).toContain('This site has moved to CorvidLabs') + expect(html).toContain(`href="${HUB_DOCS}"`) + }) }) describe('computeRedirects', () => { - test('maps top-level mdBook pages to new docs paths', () => { + test('maps every legacy mdBook page to the hub docs index', () => { const mapped = computeRedirects(['lanes.md', 'pillars.md', 'getting-started/installation.md']) - expect(mapped['lanes.html']).toBe('/fledge/docs/lanes') - expect(mapped['pillars.html']).toBe('/fledge/docs/pillars') - expect(mapped['getting-started/installation.html']).toBe('/fledge/docs/getting-started/installation') + expect(mapped['lanes.html']).toBe(HUB_DOCS) + expect(mapped['pillars.html']).toBe(HUB_DOCS) + expect(mapped['getting-started/installation.html']).toBe(HUB_DOCS) }) test('skips top-level .md whose stem collides with an Astro page dir', () => { // plugins.md would emit public/plugins.html, which on GitHub Pages - // shadows the Astro-built plugins/index.html (the registry). Skip it. + // shadows the Astro-built plugins/index.html. Skip it. const mapped = computeRedirects(['plugins.md', 'lanes.md'], new Set(['plugins'])) expect(mapped['plugins.html']).toBeUndefined() - expect(mapped['lanes.html']).toBe('/fledge/docs/lanes') + expect(mapped['lanes.html']).toBe(HUB_DOCS) }) test('skip only applies to top-level files, not nested ones', () => { // getting-started/plugins.md emits getting-started/plugins.html and can't // collide with the top-level plugins/ dir, so it stays. const mapped = computeRedirects(['getting-started/plugins.md'], new Set(['plugins'])) - expect(mapped['getting-started/plugins.html']).toBe('/fledge/docs/getting-started/plugins') + expect(mapped['getting-started/plugins.html']).toBe(HUB_DOCS) }) }) diff --git a/site/scripts/generate-doc-redirects.ts b/site/scripts/generate-doc-redirects.ts index 5d897fa..19aafbd 100644 --- a/site/scripts/generate-doc-redirects.ts +++ b/site/scripts/generate-doc-redirects.ts @@ -1,21 +1,26 @@ import { readdirSync, statSync, mkdirSync, writeFileSync } from 'node:fs' import { join, dirname } from 'node:path' import { fileURLToPath } from 'node:url' +import { HUB_DOCS } from '../src/data/hub' const __dirname = dirname(fileURLToPath(import.meta.url)) const PUBLIC_DIR = join(__dirname, '..', 'public') -const BASE = '/fledge/' + +// The standalone site is retired: every legacy mdBook `.html` path now +// redirects straight to the CorvidLabs hub docs index (no internal hop). +const TARGET = HUB_DOCS export function redirectHtml(target: string): string { return ` - + -Redirecting… +Moved to CorvidLabs + -

This page has moved. Continue →

+

This page has moved. This site has moved to CorvidLabs →

` } @@ -33,8 +38,7 @@ export function computeRedirects( const stem = f.replace(/\.md$/, '') if (isTopLevel && skipStems.has(stem)) continue const html = f.replace(/\.md$/, '.html') - const newPath = `${BASE}docs/${stem}`.replace(/\/+/g, '/') - out[html] = newPath + out[html] = TARGET } return out } @@ -66,7 +70,10 @@ function main() { // Source-of-truth = the migrated docs/ tree under site/src/content/docs const docsSrc = join(__dirname, '..', 'src', 'content', 'docs') const mdFiles = walk(docsSrc) - const skip = topLevelPageDirs() + // index.md would emit public/index.html and shadow the root redirect page + // (src/pages/index.astro → the hub marketing URL). Skip it so the root keeps + // pointing at marketing rather than the docs index. + const skip = topLevelPageDirs().add('index') const mapped = computeRedirects(mdFiles, skip) for (const [oldPath, newPath] of Object.entries(mapped)) { const dest = join(PUBLIC_DIR, oldPath) diff --git a/site/src/components/Redirect.astro b/site/src/components/Redirect.astro new file mode 100644 index 0000000..5bf20bb --- /dev/null +++ b/site/src/components/Redirect.astro @@ -0,0 +1,74 @@ +--- +/** + * Full-page redirect to the CorvidLabs hub. + * + * The standalone fledge site has been retired — both its marketing pages and + * its docs now live on the CorvidLabs hub. Every route on this site renders + * this component so that, however a visitor (or crawler) arrives, they land on + * the canonical hub URL. + * + * Emits, per the retirement plan: + * - for immediate browser redirect + * - so search engines fold ranking into the hub URL + * - a visible "This site has moved to CorvidLabs →" link for no-JS users + */ +interface Props { + /** Absolute hub URL to redirect to. */ + to: string +} + +const { to } = Astro.props as Props +--- + + + + + + + Moved to CorvidLabs + + + + + + + +
+

This page has moved.

+

+ This site has moved to CorvidLabs → +

+
+ + diff --git a/site/src/data/hub.ts b/site/src/data/hub.ts new file mode 100644 index 0000000..da160aa --- /dev/null +++ b/site/src/data/hub.ts @@ -0,0 +1,9 @@ +/** + * Canonical CorvidLabs hub URLs. + * + * The standalone fledge site has been retired; its marketing and docs are fully + * migrated to the CorvidLabs hub. These constants are the redirect targets used + * by every route on this (now redirect-only) site. + */ +export const HUB_MARKETING = 'https://corvidlabs.github.io/corvidlabs-site/fledge/' +export const HUB_DOCS = 'https://corvidlabs.github.io/corvidlabs-site/fledge/docs/' diff --git a/site/src/pages/404.astro b/site/src/pages/404.astro index 8527465..e034a7f 100644 --- a/site/src/pages/404.astro +++ b/site/src/pages/404.astro @@ -1,18 +1,5 @@ --- -import BaseLayout from '../layouts/BaseLayout.astro' -import Button from '../components/Button.astro' -const base = import.meta.env.BASE_URL +import Redirect from '../components/Redirect.astro' +import { HUB_MARKETING } from '../data/hub' --- - -
-

404

-

This page took flight without us.

-

- The page you're looking for doesn't exist (any more). Try the docs or the plugin registry. -

-
- - -
-
-
+ diff --git a/site/src/pages/blog/[...slug].astro b/site/src/pages/blog/[...slug].astro index acc2cf2..6fae24d 100644 --- a/site/src/pages/blog/[...slug].astro +++ b/site/src/pages/blog/[...slug].astro @@ -1,22 +1,11 @@ --- import { getCollection } from 'astro:content' -import ArticleLayout from '../../layouts/ArticleLayout.astro' +import Redirect from '../../components/Redirect.astro' +import { HUB_MARKETING } from '../../data/hub' export async function getStaticPaths() { const entries = await getCollection('blog', p => !p.data.draft) - return entries.map(entry => ({ params: { slug: entry.slug }, props: { entry } })) + return entries.map(entry => ({ params: { slug: entry.slug } })) } - -const { entry } = Astro.props -const { Content } = await entry.render() -const date = entry.data.date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) -const meta = `${date} · ${entry.data.author} · ${entry.data.readTime} min read` --- - - - + diff --git a/site/src/pages/blog/index.astro b/site/src/pages/blog/index.astro index b63cbd9..12aee2d 100644 --- a/site/src/pages/blog/index.astro +++ b/site/src/pages/blog/index.astro @@ -1,97 +1,5 @@ --- -import { getCollection } from 'astro:content' -import BaseLayout from '../../layouts/BaseLayout.astro' -import PostCard from '../../components/PostCard.astro' -import CategoryTag from '../../components/CategoryTag.astro' -import plugins from '../../data/plugins.json' with { type: 'json' } -const base = import.meta.env.BASE_URL -const posts = (await getCollection('blog', p => !p.data.draft)) - .sort((a, b) => +b.data.date - +a.data.date) -const featured = posts.find(p => p.data.featured) -const rest = posts.filter(p => p !== featured) -const fmt = (d: Date) => d.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) +import Redirect from '../../components/Redirect.astro' +import { HUB_MARKETING } from '../../data/hub' --- - -
- -
-
-
-

The fledge blog

-

Updates, plugins, and field notes.

-

Release notes, plugin spotlights, workflow deep-dives, and the occasional design rant.

-
-
- RSS -
-
-
- -{featured && ( - -)} - -
-
-
-

All posts

- {posts.length} posts -
-
    - {rest.map(p => )} -
-
-
- -
- -
- -
-
- - + diff --git a/site/src/pages/docs/[...slug].astro b/site/src/pages/docs/[...slug].astro index 1a5c4c5..9ebbdb9 100644 --- a/site/src/pages/docs/[...slug].astro +++ b/site/src/pages/docs/[...slug].astro @@ -1,17 +1,15 @@ --- import { getCollection } from 'astro:content' -import DocsLayout from '../../layouts/DocsLayout.astro' +import Redirect from '../../components/Redirect.astro' +import { HUB_DOCS } from '../../data/hub' +// Each retired doc URL still builds and redirects to the hub docs index. +// Deep per-page mapping is intentionally collapsed to the docs index. export async function getStaticPaths() { const entries = await getCollection('docs') return entries .filter(e => e.slug !== 'index') - .map(entry => ({ params: { slug: entry.slug }, props: { entry } })) + .map(entry => ({ params: { slug: entry.slug } })) } - -const { entry } = Astro.props -const { Content } = await entry.render() --- - - - + diff --git a/site/src/pages/docs/index.astro b/site/src/pages/docs/index.astro index 8a3d479..c978153 100644 --- a/site/src/pages/docs/index.astro +++ b/site/src/pages/docs/index.astro @@ -1,10 +1,5 @@ --- -import { getEntry } from 'astro:content' -import DocsLayout from '../../layouts/DocsLayout.astro' -const entry = await getEntry('docs', 'index') -if (!entry) throw new Error('docs/index not found') -const { Content } = await entry.render() +import Redirect from '../../components/Redirect.astro' +import { HUB_DOCS } from '../../data/hub' --- - - - + diff --git a/site/src/pages/examples/[...slug].astro b/site/src/pages/examples/[...slug].astro index 291bf3b..66cba45 100644 --- a/site/src/pages/examples/[...slug].astro +++ b/site/src/pages/examples/[...slug].astro @@ -1,21 +1,11 @@ --- import { getCollection } from 'astro:content' -import ArticleLayout from '../../layouts/ArticleLayout.astro' +import Redirect from '../../components/Redirect.astro' +import { HUB_MARKETING } from '../../data/hub' export async function getStaticPaths() { const entries = await getCollection('examples', e => !e.data.draft) - return entries.map(entry => ({ params: { slug: entry.slug }, props: { entry } })) + return entries.map(entry => ({ params: { slug: entry.slug } })) } - -const { entry } = Astro.props -const { Content } = await entry.render() -const meta = `${entry.data.steps} steps · ${entry.data.minutes} min · ${entry.data.pillars.join(' · ')}` --- - - - + diff --git a/site/src/pages/examples/index.astro b/site/src/pages/examples/index.astro index 2677a8a..12aee2d 100644 --- a/site/src/pages/examples/index.astro +++ b/site/src/pages/examples/index.astro @@ -1,84 +1,5 @@ --- -import { getCollection } from 'astro:content' -import BaseLayout from '../../layouts/BaseLayout.astro' -const base = import.meta.env.BASE_URL -const all = (await getCollection('examples', e => !e.data.draft)).sort( - (a, b) => (a.data.order ?? 999) - (b.data.order ?? 999), -) -const featured = all.find(e => e.data.featured) -const rest = all.filter(e => !e.data.featured) +import Redirect from '../../components/Redirect.astro' +import { HUB_MARKETING } from '../../data/hub' --- - -
- -
-
-

Walkthroughs

-

fledge on a real project.

-

End-to-end walkthroughs. Every command, every file, every output. Run them yourself or read them like recipes.

-
-
- -{featured && ( - -)} - -
- -
- -
-
- - + diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 4612633..e034a7f 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -1,226 +1,5 @@ --- -import BaseLayout from '../layouts/BaseLayout.astro' -import Badge from '../components/Badge.astro' -import Button from '../components/Button.astro' -import Terminal from '../components/Terminal.astro' -import Pillar from '../components/Pillar.astro' -import { getCollection } from 'astro:content' -import plugins from '../data/plugins.json' with { type: 'json' } -import type { RegistryEntry } from '../../scripts/build-plugin-registry' -import { VERSION_MINOR } from '../data/version' - -const base = import.meta.env.BASE_URL -const examples = await getCollection('examples', ({ data }) => !data.draft) -const featuredExamples = examples.sort((a, b) => (a.data.order ?? 99) - (b.data.order ?? 99)).slice(0, 3) - -const spotlight = (plugins as RegistryEntry[]) - .filter(p => p.trust_tier === 'official') - .slice(0, 4) +import Redirect from '../components/Redirect.astro' +import { HUB_MARKETING } from '../data/hub' --- - -
- -
-
-
-
- {plugins.length} plugins shipping in {VERSION_MINOR} -

Get your projects
ready to fly.

-

One CLI for the dev loop. Any language. JSON by default. Scaffold, run, ship — without the bash spaghetti.

-
- - -
-

Install in a single command: cargo install fledge

-
-
- -
# nothing → shipped, in three commands
-
$ fledge templates init my-cli -t rust-cli
-
✓ Scaffolded my-cli/
-
$ fledge lanes init
-
✓ fledge.toml: build, test, lint, ci
-
$ fledge lanes run ci
-
✓ build (1.8s)
-
✓ test (24 passed, 0.6s)
-
✓ lint
-
★ ci passed in 12s
-
$
-
-
-
-
-
- -
-
-
    -
  • {plugins.length}
    plugins shipping
    official + community
  • -
  • 6
    pillars
    scaffold · run · spec · AI · ship · extend
  • -
  • languages
    Rust, TS, Python, Go, anything
  • -
  • 1
    binary
    install, done
  • -
-
-
- - -
-
-
-

Six pillars

-

Everything you need from nothing
to shipped, in one binary.

-

Each pillar is a focused subcommand. Plugins extend any of them. Pick what you need.

-
-
    - Built-in templates for Rust, TS, Python, Go. Tera placeholders. Community registry. - Task runner with composable lanes. Parallel/sequential. File watcher built in. - Specs as constraints. Validate code matches spec. Agent-friendly source of truth. - Spec-aware ask and review. Works with Claude, Ollama, OpenAI, any provider. - Branch → commit (AI optional) → push → release → changelog. - Plugin protocol in any language. {plugins.length} plugins to install. Or write your own. -
-
-
- -
-
-
-

{plugins.length} plugins and counting.

- Browse the registry -
- -
-
- -
-
-
-

Walkthroughs

-

See it on a real project.

-

End-to-end examples — every command, every file, every output.

-
- -
-
- -
-
-

Stop wrangling shell scripts.
Take flight.

-

Install fledge and run your first lane in under a minute.

-
- - cargo install fledge -
-
- - -
-
-
- -
-
- - + diff --git a/site/src/pages/plugins/[slug].astro b/site/src/pages/plugins/[slug].astro index c15f3a9..99b3574 100644 --- a/site/src/pages/plugins/[slug].astro +++ b/site/src/pages/plugins/[slug].astro @@ -1,162 +1,18 @@ --- -import { readdirSync, readFileSync, existsSync } from 'node:fs' +import { readdirSync, existsSync } from 'node:fs' import { join } from 'node:path' -import BaseLayout from '../../layouts/BaseLayout.astro' -import Button from '../../components/Button.astro' -import type { FullEntry } from '../../../scripts/build-plugin-registry' - -// We use process.cwd() (not fileURLToPath(import.meta.url) per the plan) because -// Astro 5 / Vite transforms .astro files into Vite chunks — import.meta.url then -// resolves to a /@fs/... chunk path that doesn't reach the source tree. cwd is -// stable because both `astro dev` and `astro build` are invoked from `site/`. -const PER_PLUGIN_DIR = join(process.cwd(), 'src', 'data', 'plugins') +import Redirect from '../../components/Redirect.astro' +import { HUB_MARKETING } from '../../data/hub' +// Enumerate the same plugin slugs the live site shipped so each retired plugin +// URL still builds and redirects to the hub. Per-plugin pages now live on the +// CorvidLabs hub marketing site. export async function getStaticPaths() { const dir = join(process.cwd(), 'src', 'data', 'plugins') - if (!existsSync(dir)) return [] // (defensive: dir always exists after prebuild populates it; empty array fallback prevents a build hang when running ad-hoc from another directory) + if (!existsSync(dir)) return [] return readdirSync(dir) .filter(f => f.endsWith('.json')) - .map(f => { - const slug = f.replace(/\.json$/, '') - const data = JSON.parse(readFileSync(join(dir, f), 'utf-8')) as FullEntry - return { params: { slug }, props: { plugin: data } } - }) + .map(f => ({ params: { slug: f.replace(/\.json$/, '') } })) } - -const { plugin } = Astro.props as { plugin: FullEntry } -const tierLabel = plugin.trust_tier.charAt(0).toUpperCase() + plugin.trust_tier.slice(1) -const base = import.meta.env.BASE_URL - -// load related plugins from disk so we have title + description for cards -const related = plugin.related_slugs - .map(s => { - const p = join(PER_PLUGIN_DIR, `${s}.json`) - return existsSync(p) ? (JSON.parse(readFileSync(p, 'utf-8')) as FullEntry) : null - }) - .filter((x): x is FullEntry => !!x) --- - -
- -
-
- Back to plugins -

{plugin.name}

-
- v{plugin.version} - {tierLabel} - {plugin.language} - ★ {plugin.stars} - Updated {new Date(plugin.updated_at).toLocaleDateString()} - GitHub -
-
-
- -
-
-
-
Install with fledge
-
{plugin.install}
-
-
-
- -
-
-
- {plugin.readme_html - ? - :

No README yet.

} -
-
-

Metadata

-
-
License
{plugin.license ?? '—'}
-
Default branch
{plugin.default_branch}
-
Open issues
{plugin.open_issues}
-
Topics
{plugin.topics.length ? plugin.topics.join(', ') : '—'}
-
-
-
-
- -{related.length > 0 && ( - -)} - -
-
-

Built something similar?

- -
-
- -
-
- - + diff --git a/site/src/pages/plugins/index.astro b/site/src/pages/plugins/index.astro index 0034190..12aee2d 100644 --- a/site/src/pages/plugins/index.astro +++ b/site/src/pages/plugins/index.astro @@ -1,143 +1,5 @@ --- -import BaseLayout from '../../layouts/BaseLayout.astro' -import PluginCard from '../../components/PluginCard.astro' -import plugins from '../../data/plugins.json' with { type: 'json' } -import type { RegistryEntry } from '../../../scripts/build-plugin-registry' -const list = plugins as RegistryEntry[] -const officialCount = list.filter(p => p.trust_tier === 'official').length -const communityCount = list.filter(p => p.trust_tier === 'community').length -const langCounts = list.reduce>((acc, p) => { - acc[p.language] = (acc[p.language] ?? 0) + 1 - return acc -}, {}) +import Redirect from '../../components/Redirect.astro' +import { HUB_MARKETING } from '../../data/hub' --- - -
- -
-
-

Plugin Registry

-

Extend fledge with one command.

-

Browse {list.length} plugins maintained by CorvidLabs and the community. Install any of them in one line.

-
- {list.length} plugins - {officialCount} official - {communityCount} community - Auto-refreshed weekly -
-
-
- -
-
- - - -

Showing {list.length} of {list.length} plugins

- -
    - {list.map(p => )} -
- -
-
- -
-
- - - - + diff --git a/site/src/pages/rss.xml.ts b/site/src/pages/rss.xml.ts index 810433d..07a6a37 100644 --- a/site/src/pages/rss.xml.ts +++ b/site/src/pages/rss.xml.ts @@ -1,61 +1,26 @@ /** - * Minimal RSS 2.0 feed for the fledge blog. + * Retired RSS feed. * - * Avoids pulling in @astrojs/rss as a new dependency — the feed shape is small - * and we already escape the few characters that matter (XML doesn't allow - * unescaped &, <, > in element text or CDATA-less descriptions). + * The standalone fledge blog has moved to the CorvidLabs hub. This endpoint + * still builds at /fledge/rss.xml but now serves an HTML redirect to the hub + * so any subscribed reader / crawler is sent to the canonical location. */ -import type { APIContext } from 'astro' -import { getCollection } from 'astro:content' +import { HUB_MARKETING } from '../data/hub' -function xmlEscape(s: string): string { - return s - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} - -export async function GET(context: APIContext) { - const site = context.site?.toString().replace(/\/$/, '') ?? 'https://corvidlabs.github.io' - const base = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '') - const posts = (await getCollection('blog', (p) => !p.data.draft)).sort( - (a, b) => +b.data.date - +a.data.date, - ) - - const items = posts - .map((p) => { - const url = `${site}${base}/blog/${p.slug}` - return ` - ${xmlEscape(p.data.title)} - ${url} - ${url} - ${p.data.date.toUTCString()} - ${xmlEscape(p.data.description)} - noreply@corvidlabs.xyz (${xmlEscape(p.data.author)}) - ` - }) - .join('\n') - - const lastBuild = (posts[0]?.data.date ?? new Date()).toUTCString() - const channelLink = `${site}${base}/blog` - - const body = ` - - - fledge blog - ${channelLink} - Release notes, plugin spotlights, and workflow deep-dives for fledge. - en-us - ${lastBuild} - -${items} - - +export function GET() { + const to = HUB_MARKETING + const body = ` + + +Moved to CorvidLabs + + + + +

This feed has moved. This site has moved to CorvidLabs →

+ ` - return new Response(body, { - headers: { 'Content-Type': 'application/rss+xml; charset=utf-8' }, + headers: { 'Content-Type': 'text/html; charset=utf-8' }, }) }