Skip to content
Merged
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
60 changes: 60 additions & 0 deletions site/src/components/RedirectPage.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
/**
* Full-page meta-refresh redirect stub.
*
* The standalone spec-sync site has been retired — its marketing and docs now
* live on the CorvidLabs hub. Every route renders one of these so visitors and
* search engines land on the canonical hub URL.
*/
interface Props {
/** Absolute hub URL to redirect to. */
to: string
}

const { to } = Astro.props
---

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>This site has moved to CorvidLabs</title>
<link rel="canonical" href={to} />
<meta http-equiv="refresh" content={`0; url=${to}`} />
<meta name="robots" content="noindex, follow" />
<script is:inline set:html={`location.replace(${JSON.stringify(to)})`}></script>
<style>
:root { color-scheme: light dark; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
background: #0b0d10;
color: #e6e8eb;
text-align: center;
padding: 24px;
}
main { max-width: 30rem; }
p { color: #9aa3ad; line-height: 1.6; }
a.moved {
display: inline-block;
margin-top: 8px;
font-size: 1.125rem;
font-weight: 600;
color: #38bdf8;
text-decoration: none;
}
a.moved:hover { text-decoration: underline; }
</style>
</head>
<body>
<main>
<p>The standalone spec-sync site has been retired.</p>
<a class="moved" href={to}>This site has moved to CorvidLabs →</a>
<p>If you are not redirected automatically, follow the link above.</p>
</main>
</body>
</html>
8 changes: 8 additions & 0 deletions site/src/lib/hub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Canonical CorvidLabs hub URLs that supersede the retired standalone site.
*
* - Marketing (incl. the languages gallery and blog) -> HUB_MARKETING
* - Docs and examples -> HUB_DOCS
*/
export const HUB_MARKETING = 'https://corvidlabs.github.io/corvidlabs-site/spec-sync/'
export const HUB_DOCS = 'https://corvidlabs.github.io/corvidlabs-site/spec-sync/docs/'
20 changes: 3 additions & 17 deletions site/src/pages/404.astro
Original file line number Diff line number Diff line change
@@ -1,19 +1,5 @@
---
import BaseLayout from '../layouts/BaseLayout.astro'
import Button from '../components/Button.astro'
import languages from '../data/languages.json' with { type: 'json' }
import { base } from '../lib/path'
import RedirectPage from '../components/RedirectPage.astro'
import { HUB_MARKETING } from '../lib/hub'
---
<BaseLayout title="404 — spec-sync">
<main id="main" class="container" style="padding: 120px 0 80px; text-align: center;">
<p style="color: var(--accent-bright); font-family: var(--serif); font-style: italic; font-size: 1.25rem; margin-bottom: 8px;">404</p>
<h1 style="font-size: 2.4rem; letter-spacing: -0.02em; margin-bottom: 12px;">This page took flight without us.</h1>
<p style="color: var(--text-muted); font-size: 1.05rem; margin: 0 auto 28px; max-width: 480px;">
The page you're looking for doesn't exist (any more). Try the docs or the language registry.
</p>
<div style="display: flex; gap: 12px; justify-content: center; flex-wrap: wrap;">
<Button href={`${base}docs`} variant="primary">Read the docs</Button>
<Button href={`${base}languages`} variant="secondary">Browse {languages.length} languages</Button>
</div>
</main>
</BaseLayout>
<RedirectPage to={HUB_MARKETING} />
21 changes: 6 additions & 15 deletions site/src/pages/blog/[...slug].astro
Original file line number Diff line number Diff line change
@@ -1,22 +1,13 @@
---
import { getCollection } from 'astro:content'
import ArticleLayout from '../../layouts/ArticleLayout.astro'
import RedirectPage from '../../components/RedirectPage.astro'
import { HUB_MARKETING } from '../../lib/hub'

// Keep enumerating every blog post slug so each retired URL still emits a
// redirect stub pointing at the hub marketing site.
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`
---
<ArticleLayout
title={entry.data.title}
description={entry.data.description}
eyebrow={entry.data.category}
meta={meta}
>
<Content />
</ArticleLayout>
<RedirectPage to={HUB_MARKETING} />
98 changes: 3 additions & 95 deletions site/src/pages/blog/index.astro
Original file line number Diff line number Diff line change
@@ -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 languages from '../../data/languages.json' with { type: 'json' }
import { base } from '../../lib/path'
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 RedirectPage from '../../components/RedirectPage.astro'
import { HUB_MARKETING } from '../../lib/hub'
---
<BaseLayout title="spec-sync blog">
<main id="main">

<section class="page-head">
<div class="container head-row">
<div>
<p class="eyebrow">The spec-sync blog</p>
<h1>Updates, languages, and <em>field notes.</em></h1>
<p class="lede">Release notes, language additions, integration deep-dives, and the occasional design rant.</p>
</div>
<div class="rss-row">
<a href={`${base}rss.xml`} class="rss"><span aria-hidden="true">⌗</span> RSS</a>
</div>
</div>
</section>

{featured && (
<section class="featured">
<div class="container">
<a href={`${base}blog/${featured.slug}`} class="feat-card">
<div class="feat-meta">
<CategoryTag category={featured.data.category} />
<span class="feat-date">{fmt(featured.data.date)} · {featured.data.readTime} min read</span>
</div>
<h2>{featured.data.title}</h2>
<p class="dek">{featured.data.description}</p>
<p class="byline">by {featured.data.author}</p>
</a>
</div>
</section>
)}

<section class="post-list">
<div class="container">
<div class="list-head">
<h2>All posts</h2>
<span class="count">{posts.length} posts</span>
</div>
<ul class="post-grid">
{rest.map(p => <PostCard post={p} />)}
</ul>
</div>
</section>

<section class="read-more">
<div class="container">
<a href={`${base}languages`} class="rm-card">Browse {languages.length} languages <span aria-hidden="true">→</span></a>
<a href={`${base}docs`} class="rm-card">Read the docs <span aria-hidden="true">→</span></a>
<a href="https://github.com/CorvidLabs/spec-sync" class="rm-card">Star on GitHub <span aria-hidden="true">↗</span></a>
</div>
</section>

</main>
</BaseLayout>

<style>
.page-head { padding: 60px 0 36px; background-image: radial-gradient(ellipse 1200px 400px at 50% -100px, rgba(14,165,233,0.10), transparent 70%); border-bottom: 1px solid var(--border); }
.head-row { display: flex; align-items: end; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
.eyebrow { color: var(--accent-bright); font-size: 0.875rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 14px; }
.page-head h1 { font-size: clamp(2.2rem, 4vw, 3rem); letter-spacing: -0.025em; line-height: 1.1; margin-bottom: 10px; }
.page-head h1 em { font-style: italic; font-family: var(--serif); color: var(--accent-bright); font-weight: 400; }
.lede { color: var(--text-muted); font-size: 1.125rem; max-width: 580px; }
.rss { padding: 10px 14px; background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: 8px; color: var(--text-muted); font-size: 0.875rem; display: inline-flex; gap: 6px; align-items: center; }
.rss:hover { color: var(--accent-bright); border-color: var(--accent); }
.featured { padding: 40px 0 20px; }
.feat-card { display: block; padding: 40px; background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: var(--radius); color: inherit; text-decoration: none; }
.feat-card:hover { border-color: var(--accent); }
.feat-meta { display: flex; gap: 12px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
.feat-date { color: var(--text-dim); font-size: 0.875rem; font-family: var(--mono); }
.feat-card h2 { font-size: clamp(1.6rem, 3vw, 2.1rem); line-height: 1.15; margin-bottom: 14px; }
.feat-card .dek { color: var(--text-muted); font-size: 1.0625rem; line-height: 1.55; margin-bottom: 12px; }
.feat-card .byline { color: var(--text-dim); font-size: 0.9375rem; }
.post-list { padding: 40px 0 60px; }
.list-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px; }
.list-head h2 { font-size: 1.5rem; }
.list-head .count { color: var(--text-dim); font-size: 0.9375rem; }
.post-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; }
.read-more { padding: 40px 0 80px; }
.read-more .container { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.rm-card { display: block; padding: 22px; background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius); color: var(--text); font-weight: 500; text-decoration: none; transition: border-color .15s; }
.rm-card:hover { border-color: var(--accent); color: var(--accent-bright); }
@media (max-width: 1024px) { .post-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 760px) { .post-grid, .read-more .container { grid-template-columns: 1fr; } }
</style>
<RedirectPage to={HUB_MARKETING} />
14 changes: 6 additions & 8 deletions site/src/pages/docs/[...slug].astro
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
---
import { getCollection } from 'astro:content'
import DocsLayout from '../../layouts/DocsLayout.astro'
import RedirectPage from '../../components/RedirectPage.astro'
import { HUB_DOCS } from '../../lib/hub'

// Keep enumerating every docs slug (incl. integrations/*) so each retired URL
// still emits a redirect stub pointing at the hub docs.
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()
---
<DocsLayout title={entry.data.title} description={entry.data.description} section={entry.data.section}>
<Content />
</DocsLayout>
<RedirectPage to={HUB_DOCS} />
11 changes: 3 additions & 8 deletions site/src/pages/docs/index.astro
Original file line number Diff line number Diff line change
@@ -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 entry not found')
const { Content } = await entry.render()
import RedirectPage from '../../components/RedirectPage.astro'
import { HUB_DOCS } from '../../lib/hub'
---
<DocsLayout title={entry.data.title} description={entry.data.description} section={entry.data.section}>
<Content />
</DocsLayout>
<RedirectPage to={HUB_DOCS} />
20 changes: 6 additions & 14 deletions site/src/pages/examples/[...slug].astro
Original file line number Diff line number Diff line change
@@ -1,21 +1,13 @@
---
import { getCollection } from 'astro:content'
import ArticleLayout from '../../layouts/ArticleLayout.astro'
import RedirectPage from '../../components/RedirectPage.astro'
import { HUB_DOCS } from '../../lib/hub'

// Keep enumerating every example slug so each retired URL still emits a
// redirect stub pointing at the hub docs.
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(' · ')}`
---
<ArticleLayout
title={entry.data.title}
description={entry.data.description}
eyebrow={entry.data.tag}
meta={meta}
>
<Content />
</ArticleLayout>
<RedirectPage to={HUB_DOCS} />
85 changes: 3 additions & 82 deletions site/src/pages/examples/index.astro
Original file line number Diff line number Diff line change
@@ -1,84 +1,5 @@
---
import { getCollection } from 'astro:content'
import BaseLayout from '../../layouts/BaseLayout.astro'
import { base } from '../../lib/path'
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 RedirectPage from '../../components/RedirectPage.astro'
import { HUB_DOCS } from '../../lib/hub'
---
<BaseLayout title="spec-sync — walkthroughs and examples">
<main id="main">

<section class="page-head">
<div class="container">
<p class="eyebrow">Walkthroughs</p>
<h1>spec-sync on a <em>real</em> project.</h1>
<p class="lede">End-to-end walkthroughs. Every command, every file, every output. Run them yourself or read them like recipes.</p>
</div>
</section>

{featured && (
<section class="featured">
<div class="container">
<a href={`${base}examples/${featured.slug}`} class="featured-card">
<span class="tag">{featured.data.tag}</span>
<h2>{featured.data.title}</h2>
<p>{featured.data.description}</p>
<div class="featured-meta">
<span><strong>{featured.data.steps}</strong> steps</span>
<span><strong>{featured.data.minutes}</strong> min</span>
<span>{featured.data.pillars.join(' · ')}</span>
</div>
</a>
</div>
</section>
)}

<section class="ex-list">
<div class="container">
<ul class="ex-grid">
{rest.map((e, i) => (
<li><a href={`${base}examples/${e.slug}`} class="ex-card">
<div class="number">{String(i + 1).padStart(2, '0')}</div>
<div>
<span class="tag">{e.data.tag}</span>
<h3>{e.data.title}</h3>
<p>{e.data.description}</p>
<div class="meta">{e.data.steps} steps · {e.data.minutes} min</div>
</div>
</a></li>
))}
</ul>
</div>
</section>

</main>
</BaseLayout>

<style>
.page-head { padding: 60px 0 40px; background-image: radial-gradient(ellipse 1200px 400px at 50% -100px, rgba(14,165,233,0.10), transparent 70%); border-bottom: 1px solid var(--border); }
.eyebrow { color: var(--accent-bright); font-size: 0.875rem; font-weight: 600; letter-spacing: 0.12em; text-transform: uppercase; margin-bottom: 14px; }
.page-head h1 { font-size: clamp(2.2rem, 4vw, 3rem); letter-spacing: -0.025em; margin-bottom: 14px; line-height: 1.1; }
.page-head h1 em { font-style: italic; font-family: var(--serif); color: var(--accent-bright); font-weight: 400; }
.lede { color: var(--text-muted); font-size: 1.125rem; max-width: 640px; }
.featured { padding: 40px 0 20px; }
.featured-card { display: block; padding: 40px; background: var(--bg-raised); border: 1px solid var(--border-strong); border-radius: var(--radius); color: inherit; text-decoration: none; }
.featured-card:hover { border-color: var(--accent); }
.featured-card .tag { display: inline-block; padding: 4px 10px; border-radius: 4px; background: var(--accent-muted); color: var(--accent-bright); font-size: 0.8125rem; font-weight: 600; margin-bottom: 14px; letter-spacing: 0.05em; text-transform: uppercase; }
.featured-card h2 { font-size: 1.875rem; margin-bottom: 12px; }
.featured-card p { color: var(--text-muted); font-size: 1.0625rem; margin-bottom: 20px; }
.featured-meta { display: flex; gap: 18px; color: var(--text-dim); font-size: 0.875rem; }
.featured-meta strong { color: var(--text); font-weight: 500; }
.ex-list { padding: 40px 0 80px; }
.ex-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; }
.ex-card { display: grid; grid-template-columns: 100px 1fr; gap: 24px; padding: 28px; background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius); color: inherit; text-decoration: none; }
.ex-card:hover { border-color: var(--accent); transform: translateY(-2px); }
.ex-card .number { font-family: var(--serif); font-size: 3rem; color: var(--accent-bright); font-style: italic; line-height: 1; }
.ex-card .tag { display: inline-block; padding: 4px 10px; border-radius: 4px; background: var(--accent-muted); color: var(--accent-bright); font-size: 0.8125rem; font-weight: 600; margin-bottom: 12px; text-transform: uppercase; letter-spacing: 0.05em; }
.ex-card h3 { font-size: 1.25rem; margin-bottom: 8px; }
.ex-card p { color: var(--text-muted); font-size: 0.9375rem; margin-bottom: 14px; }
.ex-card .meta { color: var(--text-dim); font-size: 0.875rem; font-family: var(--mono); }
@media (max-width: 1024px) { .ex-grid { grid-template-columns: 1fr; } }
</style>
<RedirectPage to={HUB_DOCS} />
Loading
Loading