diff --git a/apps/logicsrc-web/next.config.ts b/apps/logicsrc-web/next.config.ts index f307edf..b04c5ce 100644 --- a/apps/logicsrc-web/next.config.ts +++ b/apps/logicsrc-web/next.config.ts @@ -6,7 +6,19 @@ import type { NextConfig } from "next"; // webhooks) are filesystem routes and match before these afterFiles rewrites. const commandboardApiUrl = process.env.COMMANDBOARD_API_URL; +const securityHeaders = [ + // HSTS — site is HTTPS-only behind Railway. No `preload` (irreversible). + { key: "Strict-Transport-Security", value: "max-age=31536000; includeSubDomains" }, + { key: "X-Content-Type-Options", value: "nosniff" }, + { key: "X-Frame-Options", value: "SAMEORIGIN" }, + { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" }, + { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" }, +]; + const nextConfig: NextConfig = { + async headers() { + return [{ source: "/:path*", headers: securityHeaders }]; + }, async rewrites() { if (!commandboardApiUrl) return []; const base = commandboardApiUrl.replace(/\/$/, ""); diff --git a/apps/logicsrc-web/src/app/.well-known/security.txt/route.ts b/apps/logicsrc-web/src/app/.well-known/security.txt/route.ts new file mode 100644 index 0000000..8a1de08 --- /dev/null +++ b/apps/logicsrc-web/src/app/.well-known/security.txt/route.ts @@ -0,0 +1,17 @@ +const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, ""); + +// GET /.well-known/security.txt (RFC 9116). +export function GET(): Response { + const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(); + const body = `Contact: mailto:security@profullstack.com +Expires: ${expires} +Preferred-Languages: en +Canonical: ${SITE_URL}/.well-known/security.txt +`; + return new Response(body, { + headers: { + "content-type": "text/plain; charset=utf-8", + "cache-control": "public, max-age=86400", + }, + }); +} diff --git a/apps/logicsrc-web/src/app/[[...slug]]/page.tsx b/apps/logicsrc-web/src/app/[[...slug]]/page.tsx index 0647ce8..1a2168c 100644 --- a/apps/logicsrc-web/src/app/[[...slug]]/page.tsx +++ b/apps/logicsrc-web/src/app/[[...slug]]/page.tsx @@ -1,4 +1,5 @@ import { notFound } from "next/navigation"; +import type { Metadata } from "next"; import type { ReactNode } from "react"; import { renderPageMarkup } from "@/lib/page-markup"; import { HomeInteractivity } from "@/components/home-interactivity"; @@ -7,18 +8,57 @@ import { HomeInteractivity } from "@/components/home-interactivity"; // scrolled to the matching section. We preserve those URLs (they are canonical // in sitemap.xml) by rendering the same page for each known route and 404ing // anything else. -const KNOWN_ROUTES = new Set([ - "docs", - "blog", - "openspec", - "credential-sharing", - "hire-us", - "about", - "terms", - "privacy", - "agent-swarm", - "agentbyte" -]); +const ROUTE_META: Record = { + docs: { + title: "Docs · LogicSRC", + description: "LogicSRC specification guides, schemas, and CLI conventions for human–AI agent coordination.", + }, + openspec: { + title: "LogicSRC vs OpenSpec.dev · LogicSRC", + description: "How LogicSRC's coordination standard compares with OpenSpec.dev, including MCP and agent support.", + }, + "credential-sharing": { + title: "Credential Sharing · LogicSRC", + description: "Source/target credential diffs, approval, sync, rollback, and audit across .env, Doppler, Railway, and GitHub Secrets.", + }, + "hire-us": { + title: "Hire Us · LogicSRC", + description: "Implementation help for LogicSRC, AgentSwarm, and Credential Sharing at $250/week for accepted work, paid via CoinPay.", + }, + about: { + title: "About · LogicSRC", + description: "LogicSRC is the Profullstack, Inc. open-specification project for human and AI agent coordination.", + }, + terms: { title: "Terms · LogicSRC", description: "LogicSRC terms of use." }, + privacy: { title: "Privacy · LogicSRC", description: "LogicSRC privacy notes." }, + "agent-swarm": { + title: "AgentSwarm · LogicSRC", + description: "Provider-neutral agent orchestration with model routing, cost controls, and GitHub integration.", + }, + agentbyte: { + title: "AgentByte · LogicSRC", + description: "Agent screening sessions, AI-assisted humans, policy events, and APIs.", + }, +}; + +const KNOWN_ROUTES = new Set(Object.keys(ROUTE_META)); + +export async function generateMetadata({ + params, +}: { + params: Promise<{ slug?: string[] }>; +}): Promise { + const { slug } = await params; + const key = slug?.[0]; + if (key && ROUTE_META[key]) { + return { + title: ROUTE_META[key].title, + description: ROUTE_META[key].description, + alternates: { canonical: `/${key}` }, + }; + } + return {}; +} export default async function Page({ params diff --git a/apps/logicsrc-web/src/app/blog/[slug]/page.tsx b/apps/logicsrc-web/src/app/blog/[slug]/page.tsx index 7b5d7be..ba66313 100644 --- a/apps/logicsrc-web/src/app/blog/[slug]/page.tsx +++ b/apps/logicsrc-web/src/app/blog/[slug]/page.tsx @@ -14,13 +14,16 @@ type PostRow = { html: string; featured_image: { url?: string } | null; published_at: string; + updated_at: string; }; +const SITE_URL = (process.env.PUBLIC_URL ?? "https://logicsrc.com").replace(/\/$/, ""); + async function loadPost(slug: string): Promise { const supabase = publicClient(); const { data } = await supabase .from("blog_posts") - .select("slug, title, excerpt, html, featured_image, published_at") + .select("slug, title, excerpt, html, featured_image, published_at, updated_at") .eq("slug", slug) .eq("status", "published") .maybeSingle(); @@ -65,8 +68,25 @@ export default async function BlogPostPage({ const post = await loadPost(slug); if (!post) notFound(); + const jsonLd = { + "@context": "https://schema.org", + "@type": "BlogPosting", + headline: post.title, + description: post.excerpt ?? undefined, + image: post.featured_image?.url ? [post.featured_image.url] : undefined, + datePublished: post.published_at, + dateModified: post.updated_at, + url: `${SITE_URL}/blog/${post.slug}`, + mainEntityOfPage: `${SITE_URL}/blog/${post.slug}`, + publisher: { "@id": `${SITE_URL}/#organization` }, + }; + return ( +