Initial Paprika Next.js implementation

This commit is contained in:
2026-07-06 12:12:36 -04:00
commit 7d585484a6
38 changed files with 14824 additions and 0 deletions

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
DATABASE_URL="postgresql://paprika_app:REPLACE_WITH_SECURE_PASSWORD@127.0.0.1:5432/paprika"
NEXT_PUBLIC_SITE_URL="https://paprikalaw.com"

9
.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.next/
node_modules/
dist/
coverage/
.env
.env*.local
*.log
tsconfig.tsbuildinfo
git.txt

31
app/[page]/page.tsx Normal file
View File

@@ -0,0 +1,31 @@
import { notFound } from "next/navigation";
import { LegacyPage } from "@/components/LegacyPage";
import { legacyPages, type LegacyPageKey } from "@/lib/legacy-pages";
const routablePages = new Set(Object.keys(legacyPages));
const replacedPages = new Set(["home", "start", "ask", "thanks"]);
export function generateStaticParams() {
return Object.keys(legacyPages)
.filter((page) => !replacedPages.has(page))
.map((page) => ({ page }));
}
export async function generateMetadata({ params }: { params: Promise<{ page: string }> }) {
const { page: pageParam } = await params;
if (!routablePages.has(pageParam)) return {};
const page = legacyPages[pageParam as LegacyPageKey];
return {
title: page.title.replace("Paprika - ", "")
};
}
export default async function StaticPage({ params }: { params: Promise<{ page: string }> }) {
const { page } = await params;
if (!routablePages.has(page) || replacedPages.has(page)) {
notFound();
}
return <LegacyPage pageKey={page as LegacyPageKey} />;
}

View File

@@ -0,0 +1,33 @@
import { NextResponse } from "next/server";
import { ZodError } from "zod";
import { createSubmission } from "@/lib/submissions";
import { submissionSchema } from "@/lib/submission-schema";
export async function POST(request: Request) {
try {
const payload = await request.json();
const input = submissionSchema.parse(payload);
const submission = await createSubmission(input);
return NextResponse.json({
submission_id: submission.id,
created_at: submission.createdAt.toISOString()
});
} catch (error) {
if (error instanceof ZodError) {
return NextResponse.json(
{
error: "Validation failed",
issues: error.flatten().fieldErrors
},
{ status: 400 }
);
}
console.error("Submission failed", error);
return NextResponse.json(
{ error: "Submission could not be saved. Please try again." },
{ status: 500 }
);
}
}

29
app/ask/page.tsx Normal file
View File

@@ -0,0 +1,29 @@
import Link from "next/link";
import { SubmissionForm } from "@/components/forms/SubmissionForm";
export const metadata = {
title: "Ask a Question"
};
export default function AskPage() {
return (
<>
<section className="hero subpage press-hero">
<h1>Ask a question.</h1>
<p className="subhead">
Not ready to submit your full profile yet? Send us a quick question. We will help you
understand whether Paprika may be a fit.
</p>
<div className="actions">
<Link className="button primary" href="/start">
Start Here
</Link>
<Link className="button secondary" href="/ask">
Ask a Question
</Link>
</div>
</section>
<SubmissionForm type="ask" />
</>
);
}

2263
app/globals.css Normal file

File diff suppressed because it is too large Load Diff

44
app/layout.tsx Normal file
View File

@@ -0,0 +1,44 @@
import type { Metadata } from "next";
import Link from "next/link";
import { SiteHeader } from "@/components/SiteHeader";
import "./globals.css";
export const metadata: Metadata = {
metadataBase: new URL(process.env.NEXT_PUBLIC_SITE_URL || "https://paprikalaw.com"),
title: {
default: "Paprika",
template: "Paprika - %s"
},
description:
"Attorney-curated evidence building for O-1 talent through press, judging opportunities, scholarly articles, and documented profile strategy.",
openGraph: {
title: "Paprika",
description:
"Attorney-curated evidence building for O-1 talent through press, judging opportunities, scholarly articles, and documented profile strategy.",
url: "/",
siteName: "Paprika",
type: "website"
}
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SiteHeader />
<main>{children}</main>
<footer>
<div className="footer-links">
<Link href="/why">About</Link>
<Link href="/faq">FAQ</Link>
<Link href="/terms">Terms</Link>
<Link href="/privacy">Privacy</Link>
<Link href="/disclaimer">Disclaimer</Link>
</div>
<div>Paprika is not a law firm and does not provide legal advice or immigration representation.</div>
<div>Use of our services does not create an attorney-client relationship.</div>
</footer>
</body>
</html>
);
}

5
app/page.tsx Normal file
View File

@@ -0,0 +1,5 @@
import { LegacyPage } from "@/components/LegacyPage";
export default function HomePage() {
return <LegacyPage pageKey="home" />;
}

13
app/robots.ts Normal file
View File

@@ -0,0 +1,13 @@
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://paprikalaw.com";
return {
rules: {
userAgent: "*",
allow: "/"
},
sitemap: `${siteUrl}/sitemap.xml`
};
}

28
app/sitemap.ts Normal file
View File

@@ -0,0 +1,28 @@
import type { MetadataRoute } from "next";
const routeOrder = [
"home",
"press",
"judging",
"scholarly",
"services",
"why",
"resources",
"faq",
"start",
"ask",
"terms",
"privacy",
"disclaimer"
] as const;
export default function sitemap(): MetadataRoute.Sitemap {
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://paprikalaw.com";
return routeOrder.map((key) => ({
url: `${siteUrl}${key === "home" ? "" : `/${key}`}`,
lastModified: new Date("2026-07-06"),
changeFrequency: key === "home" ? "weekly" : "monthly",
priority: key === "home" ? 1 : key === "start" ? 0.9 : 0.7
}));
}

30
app/start/page.tsx Normal file
View File

@@ -0,0 +1,30 @@
import Link from "next/link";
import { SubmissionForm } from "@/components/forms/SubmissionForm";
export const metadata = {
title: "Start Here"
};
export default function StartPage() {
return (
<>
<section className="hero subpage press-hero">
<h1>Start here.</h1>
<p className="subhead">
Tell us a little about your background, goals, and timeline, whether you are actively
preparing or just starting to explore. No need to have everything figured out. A quick
summary is enough to start.
</p>
<div className="actions">
<Link className="button primary" href="/start">
Start Here
</Link>
<Link className="button secondary" href="/ask">
Ask a Question
</Link>
</div>
</section>
<SubmissionForm type="start" />
</>
);
}

29
app/thanks/page.tsx Normal file
View File

@@ -0,0 +1,29 @@
import Link from "next/link";
export const metadata = {
title: "Thank You"
};
export default function ThanksPage({
searchParams
}: {
searchParams: { submission_id?: string };
}) {
return (
<section className="notice">
<h2>Thank you. We received your background.</h2>
<p>
The Paprika team will review your profile and follow up with next steps. If your timeline is
urgent, mention that in your reply.
</p>
{searchParams.submission_id ? (
<p className="form-success">Submission ID: {searchParams.submission_id}</p>
) : null}
<div className="actions">
<Link className="button secondary" href="/">
Back to Homepage
</Link>
</div>
</section>
);
}

View File

@@ -0,0 +1,8 @@
import type { LegacyPageKey } from "@/lib/legacy-pages";
import { legacyPages } from "@/lib/legacy-pages";
export function LegacyPage({ pageKey }: { pageKey: LegacyPageKey }) {
const page = legacyPages[pageKey];
return <div dangerouslySetInnerHTML={{ __html: page.html }} />;
}

56
components/SiteHeader.tsx Normal file
View File

@@ -0,0 +1,56 @@
"use client";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { navItems, resourceItems } from "@/lib/navigation";
export function SiteHeader() {
const pathname = usePathname();
const current = pathname === "/" ? "home" : pathname.split("/")[1];
const resourcesActive = current === "faq" || current === "resources";
return (
<header className="site-header">
<Link className="brand" href="/">
<div className="brand-name">Paprika</div>
<div className="brand-tag">Attorney Curated Evidence Building for O-1 Talent</div>
</Link>
<nav className="nav" id="nav">
{navItems.map((item) => (
<Link
key={item.key}
href={item.href}
data-page={item.key}
className={current === item.key ? "active" : undefined}
>
{item.label}
</Link>
))}
<div className="nav-dropdown">
<Link
href="/resources"
className={`nav-trigger${resourcesActive ? " active" : ""}`}
data-page="resources"
>
Resources <span className="nav-chevron"></span>
</Link>
<div className="resources-menu" aria-label="Resources menu">
{resourceItems.map((item) => (
<Link key={item.key} href={item.href} data-page={item.key}>
<strong>{item.label}</strong>
<span>{item.description}</span>
</Link>
))}
</div>
</div>
<Link
href="/start"
className={`start${current === "start" ? " active" : ""}`}
data-page="start"
>
Start Here <span className="button-arrow"></span>
</Link>
</nav>
</header>
);
}

View File

@@ -0,0 +1,150 @@
"use client";
import { useRouter } from "next/navigation";
import { useState, type FormEvent } from "react";
type FormType = "start" | "ask";
type FieldErrors = Record<string, string[] | undefined>;
export function SubmissionForm({ type }: { type: FormType }) {
const router = useRouter();
const [errors, setErrors] = useState<FieldErrors>({});
const [status, setStatus] = useState<"idle" | "submitting" | "error">("idle");
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
setErrors({});
setStatus("submitting");
const form = new FormData(event.currentTarget);
const payload = Object.fromEntries(form.entries());
const response = await fetch("/api/submissions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...payload,
type,
consentAccepted: form.get("consentAccepted") === "on",
sourcePath: type === "start" ? "/start" : "/ask"
})
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
setErrors(result.issues || { form: [result.error || "Submission failed"] });
setStatus("error");
return;
}
router.push(`/thanks?submission_id=${encodeURIComponent(result.submission_id)}`);
}
return (
<form className="form-wrap" onSubmit={handleSubmit}>
<label className="field">
Name <span className="req">*</span>
<input className="field-control" name="name" autoComplete="name" required />
<FieldError errors={errors} name="name" />
</label>
<label className="field">
Email <span className="req">*</span>
<input className="field-control" name="email" type="email" autoComplete="email" required />
<FieldError errors={errors} name="email" />
</label>
<label className="field">
LinkedIn {type === "ask" ? <span>(optional)</span> : null}
<input className="field-control" name="linkedin" type="url" placeholder="https://linkedin.com/in/..." />
<FieldError errors={errors} name="linkedin" />
</label>
{type === "start" ? <StartFields errors={errors} /> : <AskFields errors={errors} />}
<label className="field large checkbox-field">
<input name="consentAccepted" type="checkbox" required />
<span>
I understand that submitting this form does not create an attorney-client relationship or
constitute legal advice. Paprika is not a law firm and does not provide legal services.
</span>
<FieldError errors={errors} name="consentAccepted" />
</label>
{errors.form?.length ? <div className="form-error">{errors.form[0]}</div> : null}
<button className="button primary" type="submit" disabled={status === "submitting"}>
{status === "submitting" ? "Submitting..." : type === "start" ? "Submit Profile" : "Send Question"}
</button>
<div className="form-note">
File upload is intentionally disabled until storage, size limits, access control, and privacy
handling are approved.
</div>
</form>
);
}
function StartFields({ errors }: { errors: FieldErrors }) {
return (
<>
<label className="field">
Company Website
<input className="field-control" name="companyWebsite" type="url" placeholder="https://..." />
<FieldError errors={errors} name="companyWebsite" />
</label>
<label className="field">
Current Role / Company
<input className="field-control" name="currentRoleCompany" />
</label>
<label className="field">
Field or Industry
<input className="field-control" name="fieldOrIndustry" />
</label>
<label className="field large start-question-field">
<strong>
What brings you to Paprika? <span className="req">*</span>
</strong>
<span>
Tell us where you are in your O-1 journey. Whether you have a specific goal, are building
evidence early, or are just exploring, a quick summary is enough.
</span>
<textarea className="field-control" name="reason" required />
<FieldError errors={errors} name="reason" />
</label>
<label className="field large start-question-field">
<strong>What recognition have you already built, if any?</strong>
<span>
Press, interviews, judging roles, awards, speaking, scholarly articles, patents, funding,
major projects, or other recognition.
</span>
<textarea className="field-control" name="existingRecognition" />
</label>
<label className="field large start-question-field">
<strong>Is there a timeline on your mind?</strong>
<span>
Whether it is urgent, months away, or just a future possibility, an approximate timeline
helps us recommend the right pace.
</span>
<textarea className="field-control" name="timeline" />
</label>
</>
);
}
function AskFields({ errors }: { errors: FieldErrors }) {
return (
<label className="field large question-field">
<strong>
Your question <span className="req">*</span>
</strong>
<span>Ask us anything about our services, your situation, or where to start.</span>
<textarea className="field-control" name="question" required />
<FieldError errors={errors} name="question" />
</label>
);
}
function FieldError({ errors, name }: { errors: FieldErrors; name: string }) {
const message = errors[name]?.[0];
return message ? <span className="form-error">{message}</span> : null;
}

27
doc/README.md Normal file
View File

@@ -0,0 +1,27 @@
# Paprika Frontend Mockup
This repository contains the current Paprika website mockup.
## Open the mockup
Open `index.html` in a browser.
The mockup is a single self-contained HTML file with all page routes handled through query parameters, including:
- `?page=home`
- `?page=press`
- `?page=judging`
- `?page=scholarly`
- `?page=services`
- `?page=why`
- `?page=resources`
- `?page=faq`
- `?page=terms`
- `?page=privacy`
- `?page=disclaimer`
- `?page=start`
- `?page=ask`
## Upload rule
After each approved refinement is completed, commit the updated mockup and upload it to the cloud repository immediately.

3423
doc/index.html Normal file

File diff suppressed because it is too large Load Diff

10
eslint.config.mjs Normal file
View File

@@ -0,0 +1,10 @@
import nextVitals from "eslint-config-next/core-web-vitals";
const config = [
...nextVitals,
{
ignores: [".next/**", "node_modules/**", "lib/legacy-pages.ts"]
}
];
export default config;

15
lib/db.ts Normal file
View File

@@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma?: PrismaClient;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["error", "warn"] : ["error"]
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}

83
lib/legacy-pages.ts Normal file

File diff suppressed because one or more lines are too long

22
lib/navigation.ts Normal file
View File

@@ -0,0 +1,22 @@
export const navItems = [
{ href: "/press", label: "Press", key: "press" },
{ href: "/judging", label: "Judging", key: "judging" },
{ href: "/scholarly", label: "Scholarly Articles", key: "scholarly" },
{ href: "/services", label: "Services", key: "services" },
{ href: "/why", label: "Why Paprika", key: "why" }
] as const;
export const resourceItems = [
{
href: "/faq",
key: "faq",
label: "Frequently Asked Questions",
description: "Common questions before you start"
},
{
href: "/resources",
key: "resources",
label: "O-1A Criteria Overview",
description: "All 8 O-1A criteria, explained"
}
] as const;

46
lib/submission-schema.ts Normal file
View File

@@ -0,0 +1,46 @@
import { z } from "zod";
const emptyToUndefined = (value: unknown) => {
if (typeof value !== "string") return value;
const trimmed = value.trim();
return trimmed.length ? trimmed : undefined;
};
const optionalString = z.preprocess(emptyToUndefined, z.string().max(4000).optional());
const optionalUrl = z.preprocess(
emptyToUndefined,
z.string().url("Enter a valid URL, including https://").max(500).optional()
);
const baseSubmissionSchema = z.object({
type: z.enum(["start", "ask"]),
name: z.string().trim().min(1, "Name is required").max(160),
email: z.string().trim().email("Enter a valid email").max(255),
linkedin: optionalUrl,
consentAccepted: z.literal(true, {
errorMap: () => ({ message: "You must acknowledge the disclaimer before submitting" })
}),
sourcePath: z.string().max(200).optional()
});
export const startSubmissionSchema = baseSubmissionSchema.extend({
type: z.literal("start"),
companyWebsite: optionalUrl,
currentRoleCompany: optionalString,
fieldOrIndustry: optionalString,
reason: z.string().trim().min(1, "Tell us what brings you to Paprika").max(4000),
existingRecognition: optionalString,
timeline: optionalString
});
export const askSubmissionSchema = baseSubmissionSchema.extend({
type: z.literal("ask"),
question: z.string().trim().min(1, "Question is required").max(4000)
});
export const submissionSchema = z.discriminatedUnion("type", [
startSubmissionSchema,
askSubmissionSchema
]);
export type SubmissionInput = z.infer<typeof submissionSchema>;

26
lib/submissions.ts Normal file
View File

@@ -0,0 +1,26 @@
import { prisma } from "@/lib/db";
import type { SubmissionInput } from "@/lib/submission-schema";
export async function createSubmission(input: SubmissionInput) {
return prisma.submission.create({
data: {
type: input.type,
name: input.name,
email: input.email,
linkedin: input.linkedin,
companyWebsite: input.type === "start" ? input.companyWebsite : undefined,
currentRoleCompany: input.type === "start" ? input.currentRoleCompany : undefined,
fieldOrIndustry: input.type === "start" ? input.fieldOrIndustry : undefined,
reason: input.type === "start" ? input.reason : undefined,
existingRecognition: input.type === "start" ? input.existingRecognition : undefined,
timeline: input.type === "start" ? input.timeline : undefined,
question: input.type === "ask" ? input.question : undefined,
consentAccepted: input.consentAccepted,
sourcePath: input.sourcePath
},
select: {
id: true,
createdAt: true
}
});
}

6
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

4
next.config.mjs Normal file
View File

@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;

5783
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"name": "meetpaprika",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"generate:legacy": "node scripts/extract-legacy-pages.mjs",
"prisma:generate": "prisma generate",
"prisma:migrate": "prisma migrate dev"
},
"dependencies": {
"@prisma/client": "^5.22.0",
"next": "^16.2.10",
"prisma": "^5.22.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.14.10",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
"eslint": "^9.39.4",
"eslint-config-next": "^16.2.10",
"typescript": "^5.5.3"
}
}

View File

@@ -0,0 +1,39 @@
-- CreateTable
CREATE TABLE "Submission" (
"id" TEXT NOT NULL,
"type" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"linkedin" TEXT,
"companyWebsite" TEXT,
"currentRoleCompany" TEXT,
"fieldOrIndustry" TEXT,
"reason" TEXT,
"existingRecognition" TEXT,
"timeline" TEXT,
"question" TEXT,
"consentAccepted" BOOLEAN NOT NULL DEFAULT false,
"sourcePath" TEXT,
"status" TEXT NOT NULL DEFAULT 'new',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Submission_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SubmissionFile" (
"id" TEXT NOT NULL,
"submissionId" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"mimeType" TEXT NOT NULL,
"byteSize" INTEGER NOT NULL,
"storageKey" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "SubmissionFile_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "SubmissionFile" ADD CONSTRAINT "SubmissionFile_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "Submission"("id") ON DELETE CASCADE ON UPDATE CASCADE;

40
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,40 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Submission {
id String @id @default(cuid())
type String
name String
email String
linkedin String?
companyWebsite String?
currentRoleCompany String?
fieldOrIndustry String?
reason String?
existingRecognition String?
timeline String?
question String?
consentAccepted Boolean @default(false)
sourcePath String?
status String @default("new")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
files SubmissionFile[]
}
model SubmissionFile {
id String @id @default(cuid())
submissionId String
fileName String
mimeType String
byteSize Int
storageKey String
createdAt DateTime @default(now())
submission Submission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
}

View File

@@ -0,0 +1,101 @@
import fs from "node:fs";
import vm from "node:vm";
const sourcePath = "doc/index.html";
const html = fs.readFileSync(sourcePath, "utf8");
const styleMatch = html.match(/<style>([\s\S]*?)<\/style>/);
const scriptMatch = html.match(/<script>([\s\S]*?)<\/script>/);
if (!styleMatch || !scriptMatch) {
throw new Error("Could not find inline style or script in doc/index.html");
}
const css = styleMatch[1]
.replaceAll("href=\"?page=", "href=\"/")
.replace(/href="\/home"/g, "href=\"/\"");
const legacyScript = scriptMatch[1];
const renderScript = legacyScript.replace(
/const params = new URLSearchParams\(window\.location\.search\);[\s\S]*$/,
"globalThis.__pageData = pageData;"
);
const sandbox = {
console,
URLSearchParams,
window: { location: { search: "" } },
document: {
title: "",
getElementById() {
return { innerHTML: "" };
},
querySelectorAll() {
return [];
},
querySelector() {
return null;
}
}
};
vm.createContext(sandbox);
vm.runInContext(renderScript, sandbox, { filename: sourcePath });
const pageData = sandbox.__pageData;
const routeMap = {
home: "/",
press: "/press",
judging: "/judging",
scholarly: "/scholarly",
services: "/services",
why: "/why",
resources: "/resources",
faq: "/faq",
thanks: "/thanks",
terms: "/terms",
privacy: "/privacy",
disclaimer: "/disclaimer"
};
const pages = Object.fromEntries(
Object.entries(routeMap).map(([key, path]) => {
const page = pageData[key];
if (!page?.body) {
throw new Error(`Missing page body for ${key}`);
}
return [
key,
{
key,
path,
title: key === "home" ? "Paprika" : `Paprika - ${key}`,
html: normalizeLinks(page.body)
}
];
})
);
function normalizeLinks(value) {
return value
.replaceAll("href=\"?page=home\"", "href=\"/\"")
.replace(/href="\?page=([a-z-]+)"/g, (_match, page) => {
if (page === "home") return "href=\"/\"";
return `href="/${page}"`;
});
}
fs.mkdirSync("app", { recursive: true });
fs.mkdirSync("lib", { recursive: true });
fs.writeFileSync(
"app/globals.css",
`${css}\n\n.form-error { color: var(--deep-red); font-size: 14px; font-weight: 750; line-height: 1.4; }\n.form-success { color: var(--green); font-size: 14px; font-weight: 750; line-height: 1.4; }\n.field-control { width: 100%; border: 0; outline: 0; background: transparent; color: var(--ink); font: inherit; font-weight: 650; }\n.field-control::placeholder { color: var(--muted); opacity: .78; }\ntextarea.field-control { min-height: 126px; resize: vertical; line-height: 1.45; }\n.checkbox-field { align-items: flex-start; gap: 12px; text-align: left; color: var(--text); font-size: 14px; line-height: 1.45; }\n.checkbox-field input { margin-top: 3px; accent-color: var(--red); }\n.button[disabled] { cursor: not-allowed; opacity: .62; }\n`
);
fs.writeFileSync(
"lib/legacy-pages.ts",
`export type LegacyPageKey = ${Object.keys(pages).map((key) => JSON.stringify(key)).join(" | ")};\n\nexport type LegacyPage = {\n key: LegacyPageKey;\n path: string;\n title: string;\n html: string;\n};\n\nexport const legacyPages = ${JSON.stringify(pages, null, 2)} as const satisfies Record<LegacyPageKey, LegacyPage>;\n`
);
console.log(`Generated ${Object.keys(pages).length} pages from ${sourcePath}`);

41
tsconfig.json Normal file
View File

@@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "es2017",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": [
"./*"
]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}

View File

@@ -0,0 +1,771 @@
# 01-项目功能内容
## 1. 项目基线
- 项目名称MeetPaprika / Paprika
- 当前对标文件:`/home/mes123456/MeetPaprika/doc/index.html`
- 原始对标形态:单文件静态网站 mockupHTML、CSS、页面数据、页面渲染脚本全部内联在 `doc/index.html`
- 品牌主张Attorney Curated Evidence Building for O-1 Talent
- 业务定位:面向 O-1 人才的证据建设服务,帮助 founders、engineers、researchers、executives、creators 等提前建设媒体报道、评审机会、学术文章和完整证据包
- 页面运行方式:读取 URL 查询参数 `?page=...`,将对应页面 HTML 字符串写入 `<main id="app"></main>`
## 2. 技术路线基准
### 当前静态实现
- 入口文件:`doc/index.html`
- 样式:内联 `<style>`
- 脚本:内联 `<script>`
- 状态:作为旧版静态 mockup 和视觉对标基线保留;正式工程已在根目录新增
- 路由:查询参数路由,不依赖 History API
### 已落地的正式开发技术架构
已明确采用:
- 前端框架Next.js + React
- 数据库PostgreSQL
- 数据访问层Prisma ORM
- 表单处理Next.js Route Handler `POST /api/submissions` 承接 Start Here 和 Ask a Question
- 部署端口Next.js 服务或反向代理后的应用服务监听 `8082`
- HTTPS由 Nginx/Caddy/云服务反向代理终止 TLS转发到本机 `8082`
架构目标:
- 将当前 `doc/index.html` 的视觉和内容完整迁移到 Next.js 页面/组件。
-`start``ask` 从视觉 mockup 改为真实表单。
- 将提交数据保存到 PostgreSQL。
- 预留 CV/resume 文件上传能力,但必须先明确存储方案、文件大小、访问权限和隐私规则。
- 保留所有 legal disclaimer防止用户误以为提交表单会创建 attorney-client relationship。
当前代码状态:
- 根目录已存在 Next.js App Router 工程。
- `app/[page]/page.tsx` 渲染从 `doc/index.html` 抽取的静态页面内容。
- `app/start/page.tsx``app/ask/page.tsx` 已替换为真实 React 表单。
- `app/api/submissions/route.ts` 已实现 JSON 提交、Zod 校验和 Prisma 写入。
- `prisma/schema.prisma``prisma/migrations/20260706120000_init/migration.sql` 已建立 `Submission``SubmissionFile` 表。
- `.env.example` 已提供 `DATABASE_URL``NEXT_PUBLIC_SITE_URL` 模板,不包含真实密码。
- 成功入库尚需生产或本地 PostgreSQL `DATABASE_URL`
建议工程结构:
```text
app/
page.tsx
press/page.tsx
judging/page.tsx
scholarly/page.tsx
services/page.tsx
why/page.tsx
resources/page.tsx
faq/page.tsx
start/page.tsx
ask/page.tsx
thanks/page.tsx
terms/page.tsx
privacy/page.tsx
disclaimer/page.tsx
api/
submissions/route.ts
components/
SiteHeader.tsx
SiteFooter.tsx
Hero.tsx
ServiceCard.tsx
ServicesCta.tsx
LegalPage.tsx
forms/
StartForm.tsx
AskForm.tsx
lib/
db.ts
validators.ts
submissions.ts
prisma/
schema.prisma
```
### 目标发布环境
- 目标域名:`https://paprikalaw.com/`
- 云服务器:`170.106.192.152`
- 登录用户:`ubuntu`
- 目标端口:`8082`
- 可用端口段:`8080-8099`
- PostgreSQL用户确认已在服务器安装完成待验证服务状态、版本、应用数据库和应用用户
- SSH 凭据:用户已在交接信息中提供;长期文档不保存明文密码,后续应改为 SSH key、部署密钥或受控密钥库
- 目标发布形态HTTPS 对外访问,服务内部监听 `8082` 或由反向代理转发至 `8082`
### 邮箱系统需求
- 普通用户网页登录:`https://mail.paprikalaw.com/webmail/`
- 管理员网页登录:`https://mail.paprikalaw.com/admin/`
- 管理员邮箱:`admin@paprikalaw.com`
- 普通公司邮箱:
- `info@paprikalaw.com`
- `contact@paprikalaw.com`
- `support@paprikalaw.com`
- `sales@paprikalaw.com`
- `marketing@paprikalaw.com`
- `hr@paprikalaw.com`
- `finance@paprikalaw.com`
- `billing@paprikalaw.com`
- `legal@paprikalaw.com`
- `service@paprikalaw.com`
- 凭据规则:用户已提供统一初始密码;长期文档不保存明文密码。创建后应要求首次登录修改,或为每个邮箱生成独立强密码。
邮件系统与网站的关系:
- 表单通知邮箱建议使用 `support@paprikalaw.com``contact@paprikalaw.com`
- 法务和隐私页面中的联系邮箱目前是 `support@meetpaprika.com`,上线前需统一为 `paprikalaw.com` 域名邮箱或保留现状并记录理由。
- 若 Next.js 表单提交后发送邮件通知,需要配置 SMTP 凭据、发件地址、收件地址和退信策略。
## 3. 当前页面路由清单
| 路由 | 页面 | 导航入口 | 当前用途 | 状态 |
| --- | --- | --- | --- | --- |
| `?page=home` | Home | 品牌 logo 默认入口 | 品牌主张、三大核心服务、提前建设证据、工作流程、价格起点 | 已实现 mockup |
| `?page=press` | Press | 顶部导航 Press | 媒体报道服务、证据标准、应避免的 press 类型、代表性 outlet 网络 | 已实现 mockup |
| `?page=judging` | Judging | 顶部导航 Judging | 评审机会服务、哪些 judging counts、judging network、证据包内容 | 已实现 mockup |
| `?page=scholarly` | Scholarly Articles | 顶部导航 Scholarly Articles | 学术文章服务、发表策略、证据包价值 | 已实现 mockup |
| `?page=services` | Services | 顶部导航 Services | 6 类服务卡片和服务转化 CTA | 已实现 mockup |
| `?page=why` | Why Paprika | 顶部导航 Why Paprika、footer About | 差异化说明、证据质量、时间节奏、完整证据记录 | 已实现 mockup |
| `?page=resources` | Resources / O-1A Criteria | Resources 下拉 | 8 项 O-1A criteria 的 plain-English overview | 已实现 mockup |
| `?page=faq` | FAQ | Resources 下拉、footer FAQ | Paprika、O-1、timeline、pricing/refund FAQ | 已实现 mockup |
| `?page=start` | Start Here | 顶部 CTA、各页面 CTA | 背景资料提交视觉 mockup | 已实现视觉,未实现提交 |
| `?page=ask` | Ask a Question | 各页面 secondary CTA | 简短问题提交视觉 mockup | 已实现视觉,未实现提交 |
| `?page=thanks` | Thank You | 提交按钮跳转 | 感谢页 | 已实现 mockup |
| `?page=terms` | Terms | footer Terms | 服务条款草稿 | 已实现草稿,需法务确认 |
| `?page=privacy` | Privacy | footer Privacy | 隐私政策草稿 | 已实现草稿,需法务确认 |
| `?page=disclaimer` | Disclaimer | footer Disclaimer | 法律免责声明草稿 | 已实现草稿,需法务确认 |
| `?page=blog` | Blog | 无公开导航 | Field notes 文章卡片占位 | 代码存在,未公开 |
| `?page=about` | About | 无公开导航footer About 实际指向 `why` | 公司说明占位 | 代码存在,未公开 |
未识别路由会回退到 `home`,页面标题为 `Paprika - ${selected}`
## 4. 全站公共模块
### Header
- 品牌区:
- `Paprika`
- `Attorney Curated Evidence Building for O-1 Talent`
- 顶部导航:
- Press
- Judging
- Scholarly Articles
- Services
- Why Paprika
- Resources 下拉
- Start Here CTA
- Resources 下拉:
- Frequently Asked Questions -> `?page=faq`
- O-1A Criteria Overview -> `?page=resources`
- 当前页高亮:
- 根据 `data-page` 和当前 `selected` 页面添加 `active`
-`selected``faq``resources`Resources 触发器高亮
### Footer
- 链接:
- About -> `?page=why`
- FAQ -> `?page=faq`
- Terms -> `?page=terms`
- Privacy -> `?page=privacy`
- Disclaimer -> `?page=disclaimer`
- 固定声明:
- Paprika is not a law firm and does not provide legal advice or immigration representation.
- Use of our services does not create an attorney-client relationship.
## 5. 首页内容
### Hero
- H1O-1 evidence should not be built in a panic.
- Subhead帮助高成就人才提前建设 credible public recognition
- Primary CTASend Us Your Profile -> `?page=start`
- Secondary CTAAsk a Question -> `?page=ask`
- Microcopy提交 LinkedIn 或 CV 后 review background并推荐 evidence path
- 时间主张The only thing you cannot buy later is time.
### Core services
三张服务卡片:
1. PressEditorial feature in a recognized publication in your field.
2. JudgingOpportunities to judge work that matters.
3. Scholarly ArticlesScholarly articles in credible, peer-reviewed publications.
### Start Early
- 强调证据不能临近 filing 时集中出现
- 目标是构建 natural publication and judging timeline
- 输出给用户或 immigration attorney 使用的 stronger materials
### Our standard
提出推荐前要判断的 8 个问题:
1. 文章是否关于个人而不只是公司或产品
2. 是否连接特定专业领域和 O-1 criteria
3. outlet 是否 credible、independent、documentable
4. timing 是否自然
5. judging 是否真实 evaluative
6. authorship 是否体现 expertise
7. 是否能清晰整理进 evidence package
8. 是否补强其他 existing criteria
### How it works
四步流程:
1. Send us your profile
2. Assess background and recommend evidence path
3. Build credible public record
4. Receive documented evidence package
价格带:
- Packages start at `$1,699+`
- final pricing depends on outlet level, timeline, field, evidence goals
## 6. Press 页面内容
### 核心主张
- Attorney-Curated Press for O-1 Talent
- Press that works as evidence, not just visibility
- 关注 outlet、format、field relevance、documentation
### Cadence note
- 避免 filing 前突然集中出现的 uncurated evidence
- 构建 natural cadence of press over time
### What Paprika looks for
Press 推荐前检查:
1. 文章是否关于本人
2. headline 是否出现姓名并连接领域
3. 是否 genuine editorial coverage
4. outlet 是否有真实 readership、editorial standards、clean track record
5. 是否存在 contributor、sponsored、editorial-staff-not-involved 等 disclosure
6. timing 是否自然
7. 是否 complement other O-1 criteria
结论句The outlet name is not the evidence. The editorial independence is.
### What to avoid
当前展示三类不推荐 press
- Contributor content
- Editorial staff not involved
- Sponsored or promotional placement
原因:
- outlet 没有 editorially choose to cover this person
- third party produced or paid for content
- 不体现 independent recognition
### Representative outlets
页面列出代表性 outlet 网络,包括 Forbes、Bloomberg、Vogue、Business Insider、USA Today、TechCrunch、VentureBeat、Los Angeles Times、Newsweek、Fast Company Mexico、Time 等。正式上线前需要确认是否拥有真实合作网络、是否允许展示、是否需要增加免责声明。
## 7. Judging 页面内容
### 核心主张
- Judging roles should show that your field trusts your expertise.
- 服务目标source and vet selective evaluation opportunities
### What counts as judging
当前列出 6 类:
1. Startup pitch competitions
2. Hackathon & innovation judging
3. Industry award committees
4. Academic & conference peer review
5. Grant review panels
6. Research competition judging
### Judging network examples
当前展示示例 chipsY Combinator Demo Day Adjacent、TechCrunch Disrupt、SXSW Pitch、Web Summit、Slush、Collision、Startup Grind Global、MassChallenge、Plug and Play、500 Global、Berkeley SkyDeck、Techstars、Founder Institute、academic journal review、conference TPCs、design competition juries、arts award panels、research grant committees 等。
正式上线前需要确认名称使用风险、真实性、合作关系或改为“示例类型”。
### Evidence package
Judging package 当前包含:
- Signed organizer recommendation letter
- Judge selection documentation
- Organizer correspondence
- Agenda & event page
- Panel agenda & bio listing
- Ready-to-use packet
## 8. Scholarly Articles 页面内容
核心内容:
- Scholarly articles in credible, peer-reviewed publications
- 服务覆盖 peer-reviewed、Scopus-indexed、Google-verified journals
- 两个 panel
- Attorney-curated strategy
- Complete evidence packet
正式上线前需确认:
- 是否能承诺具体 indexing 或 journal 类型
- 是否需要补充 publication ethics、authorship、conflict disclosure
- 是否避免暗示 pay-to-publish 或不当 authorship arrangement
## 9. Services 页面内容
六张服务卡:
| 编号 | 服务 | 当前标题 | CTA |
| --- | --- | --- | --- |
| 01 | Press | Editorial feature in a recognized publication in your field. | Explore Press |
| 02 | Judging | Opportunities to judge work that matters. | Explore Judging |
| 03 | Scholarly Articles | Scholarly articles in credible, peer-reviewed publications. | Explore Articles |
| 04 | Expert consultation | Know where you stand before you commit. | Start Here |
| 05 | Agency representation | Useful when you have multiple engagements or no single employer. | Start Here |
| 06 | Attorney Referral | The right attorney, when you need one. | Start Here |
注意:服务 04、05、06 曾经传入不存在的 `contact` 路由R007 已改为 `start`
## 10. Why Paprika 页面内容
五个差异化理由:
1. Attorney-curated. Built to matter.
2. Built before you need it.
3. A coherent profile, tailored to you.
4. A complete evidence record.
5. Everything in one place.
结尾 peace of mind
- You do not have to second-guess what you are building.
- You just build.
## 11. Resources / O-1A Criteria 页面内容
页面解释 O-1A 8 项 criteria
1. Awards
2. Memberships
3. Published Material About You
4. Judging the Work of Others
5. Original Contributions of Major Significance
6. Scholarly Articles
7. Critical or Essential Role
8. High Salary or Remuneration
每项都包含:
- subtitle
- plain-English body
- stronger examples
- watch out for
- bottom line
页面有 disclaimer此 overview 是 general information不是 legal advicePaprika 不是 law firm。
## 12. FAQ 页面内容
FAQ 分组:
- About Paprika
- O-1 Basics
- Timeline
- Pricing, scope, and refunds
关键问答覆盖:
- Paprika 是否 law firm
- Paprika 做什么
- 适合谁
- process 如何工作
- 何时开始
- 是否 guarantee visa approval
- 是否需要 immigration attorney
- evidence-building 是否 legitimate
- 信息安全
- O-1A 基础
- 更多证据是否总是更好
- top-tier outlet 是否必须
- hackathon judging 是否有帮助
- timeline 和 rush concerns
- pricing、refund、scope
## 13. Start Here 表单现状
当前不是实际 `<form>`,只是视觉字段。
字段视觉:
- Name required
- Email required
- LinkedIn
- Company Website
- Current Role / Company
- Field or Industry
- Attach Resume or CV optional
- What brings you to Paprika? required
- What recognition have you already built, if any?
- Is there a timeline on your mind?
提交按钮:
- `<a class="button primary" href="?page=thanks">Submit Profile</a>`
边界:
- 无输入控件
- 无校验
- 无上传
- 无提交接口
- 无邮件通知
- 无 CRM/数据库记录
## 14. Ask a Question 表单现状
当前不是实际 `<form>`,只是视觉字段。
字段视觉:
- Name required
- Email required
- LinkedIn optional
- Attach Resume or CV optional
- Your question required
提交按钮:
- `<a class="button primary" href="?page=thanks">Send Question</a>`
边界同 Start Here。
## 15. Legal 页面内容
已有页面:
- Terms of Service
- Privacy Policy
- Disclaimer
当前固定日期:
- `Last updated: July 6, 2026`
风险:
- 日期已修正为当前工作日期;正式发布前仍需确认是否应改为实际发布日期。
- 法务文本涉及 immigration、attorney referral、not a law firm、privacy、refund、payment、third-party services 等高风险内容,必须由负责人或法律顾问确认。
## 16. 当前已知缺陷和风险
| 编号 | 问题 | 影响 | 建议 |
| --- | --- | --- | --- |
| RISK-001 | Services 中 04/05/06 CTA 指向不存在的 `contact` 页面 | 点击后回退首页,转化断点 | 改为 `start` 或新增 `contact` |
| RISK-002 | Start/Ask 不是表单 | 无法收集线索 | 改成真实 `<form>` 并接后端或第三方表单服务 |
| RISK-003 | 法务页面日期晚于当前日期 | 发布可信度和合规风险 | 发布前确认日期 |
| RISK-004 | outlet/network 名称可能暗示合作关系 | 法律和商业陈述风险 | 确认授权或改为“representative examples/types” |
| RISK-005 | 无 SEO/OG/sitemap/robots | 正式站收录和分享效果弱 | 上线前补齐 |
| RISK-006 | 无截图回归和移动端验收 | 可能存在响应式问题 | 引入 Playwright 或人工截图台账 |
| RISK-007 | 无部署配置 | 不能可重复发布 | 建立服务、反向代理、HTTPS 和部署脚本 |
| RISK-008 | PostgreSQL schema 尚未设计 | 表单无法稳定入库和追踪 | 建立 `submissions``submission_files` 等数据表 |
| RISK-009 | 文件上传存储未定 | CV/resume 涉及隐私和访问控制 | 先确定本地磁盘、对象存储或禁用上传 |
| RISK-010 | 邮箱统一初始密码 | 多账号共用弱初始密码有安全风险 | 首次登录强制修改或生成独立强密码 |
| RISK-011 | 网站联系邮箱域名不一致 | `support@meetpaprika.com``paprikalaw.com` 品牌不统一 | 上线前统一 contact/support/legal 邮箱 |
## 17. 视觉设计对标
`doc/index.html` 的视觉风格不是通用模板,而是一套完整的 Paprika 品牌 mockup。后续重构或拆分必须保持以下基线除非在 `06-决策记录.md` 新增变更决策。
### 17.1 CSS 变量
| 变量 | 值 | 用途 |
| --- | --- | --- |
| `--red` | `#d9432f` | 主品牌红、按钮、kicker、编号 |
| `--deep-red` | `#a9372d` | 深红强调、active、结论句 |
| `--ink` | `#241b14` | 主文字色 |
| `--text` | `#5c5249` | 正文灰褐色 |
| `--muted` | `#8d847a` | 次级文字 |
| `--line` | `#e9e1d8` | 边线 |
| `--paper` | `#fffdfa` | 页面底色 |
| `--panel` | `rgba(255,255,255,.86)` | 卡片背景 |
| `--green` | `#4f7d61` | 少量绿色标签 |
| `--header-text` | `#41505a` | 导航文字 |
### 17.2 字体和排版
- 全站主字体:`Avenir`, `"Avenir Next"`, system sans-serif。
- 强调语句和 italic accent 使用 `Georgia`, `"Times New Roman"`, serif。
- 页面不使用图片资源,视觉主要由排版、红色强调、浅纸色背景、网格纹理、卡片阴影组成。
- `h1` 首页字号范围为 `clamp(54px, 6.3vw, 74px)`
- 子页 `h1` 字号范围为 `clamp(44px, 5.2vw, 66px)`
- 主要 section title 使用 `clamp(34px, 4vw, 46px)` 或更大的 `clamp(48px, 6vw, 74px)`
### 17.3 背景和布局
- `body` 背景由两层浅网格线、中心 radial glow 和 `--paper` 组成。
- Header 高度 `92px`sticky top半透明纸色背景`backdrop-filter: blur(14px)`
- 主内容容器常用:
- `.hero``width: min(1040px, calc(100% - 56px))`
- `.cards/.section/.form-wrap``width: min(1160px, calc(100% - 72px))`
- `.legal-doc``width: min(920px, calc(100% - 72px))`
- 卡片圆角主要是 `4px``8px`,按钮和 chip 使用 `999px`
### 17.4 响应式规则
唯一显式断点:`@media (max-width: 1080px)`
断点内行为:
- Header 允许换行,`.nav` 占满宽度并 flex-wrap。
- `.cards``.two-col``.grid``.split-feature``.avoid-grid``.why-v2-grid``.criteria-grid``.faq-section``.form-wrap``.questions``.steps` 等多列布局统一变为单列。
- `.field.large``.form-note` 从跨两列改为单列。
- `.pricing-band` 改为单列并居中。
- `.press-check-item``46px 1fr` 调整为 `38px 1fr`
注意:当前只有一个 1080px 断点,未针对窄屏 header 高度、FAQ hero nowrap、长 outlet chip、法务长段落做更细处理移动端必须截图验收。
## 18. 组件和 CSS 类对标
| 模块 | 关键类 | 当前用途 |
| --- | --- | --- |
| Header | `.site-header`, `.brand`, `.brand-name`, `.brand-tag`, `.nav`, `.nav-dropdown`, `.resources-menu`, `.start` | 顶部品牌、导航、下拉和 CTA |
| Hero | `.hero`, `.subpage`, `.hero-kicker`, `.subhead`, `.actions`, `.button`, `.microcopy`, `.time-line` | 首页和子页首屏 |
| 服务卡 | `.cards`, `.card`, `.meta`, `.num`, `.label`, `.explore` | 首页和 Services 入口 |
| 通用内容 | `.section`, `.section-head`, `.section-title`, `.section-copy`, `.two-col`, `.grid`, `.panel`, `.mini`, `.article` | 双栏、四栏、文章卡片 |
| CTA | `.services-cta`, `.services-cta-inner`, `.button-arrow` | 服务转化横幅 |
| Why | `.why-v2`, `.why-v2-grid`, `.why-card`, `.why-v2-peace`, `.why-card-line` | Why Paprika 页面 |
| Resources | `.criteria-page`, `.criteria-intro`, `.criteria-grid`, `.criterion-card`, `.criterion-block`, `.criteria-disclaimer` | O-1A criteria 页面 |
| FAQ | `.faq-page`, `.faq-section`, `.faq-section-head`, `.faq-list`, `.faq-item`, `.faq-num` | FAQ 页面 |
| Press | `.press-cadence`, `.press-standard`, `.press-check-card`, `.outlet-network`, `.outlet-grid`, `.avoid-section`, `.avoid-card`, `.avoid-explain` | Press 页面 |
| Judging | `.judging-counts`, `.judging-counts-grid`, `.judging-network`, `.judging-network-chip`, `.judging-package`, `.check-dot` | Judging 页面 |
| Process | `.process-section`, `.steps`, `.step`, `.step-num`, `.pricing-band` | 首页四步流程和价格 |
| Form mockup | `.form-wrap`, `.field`, `.upload`, `.upload-inline`, `.large`, `.question-field`, `.start-question-field`, `.form-note`, `.req` | Start/Ask 视觉表单 |
| Legal | `.legal-doc`, `.legal-doc-head`, `.legal-date`, `.legal-sections`, `.legal-section` | Terms/Privacy/Disclaimer |
| Footer | `footer`, `.footer-links` | 底部链接和固定声明 |
已存在但当前页面未使用或未公开使用的样式/函数相关模块:
- `.resource-hub`, `.resource-tabs`, `.resource-group`, `.resource-card`:对应 `resourcesHub()`,当前 `pageData.resources` 使用的是 `o1CriteriaPage()`,未使用 hub。
- `.why-proof`, `.why-reason`, `.why-closing`:疑似旧版 Why 结构,当前 `whyPaprikaProof()` 使用 `.why-v2`
- `pressQuality()`:旧版 press 对比区块,当前 `pressPage()` 未调用。
- `standardGrid()`:标准 mini grid当前未调用。
## 19. JavaScript 函数对标
| 函数/常量 | 当前状态 | 作用 |
| --- | --- | --- |
| `serviceCards` | 使用中 | 首页三张服务卡 HTML 片段 |
| `pageData` | 使用中 | 所有路由页面数据表 |
| `hero()` | 使用中 | 通用子页 hero含 Start/Ask CTA |
| `heroWithKicker()` | 使用中 | 带 kicker 的 hero可关闭 actions |
| `servicesCta()` | 使用中 | 红色服务 CTA 横幅 |
| `card()` | 使用中 | 服务卡片 |
| `panel()` | 使用中 | 双栏 panel |
| `productPage()` | 当前未调用 | 通用产品页工厂 |
| `judgingPage()` | 使用中 | Judging 页面组合 |
| `scholarlyPage()` | 使用中 | Scholarly 页面组合 |
| `productPageWithKicker()` | 当前未调用 | 带 kicker 的产品页工厂 |
| `pressPage()` | 使用中 | Press 页面组合 |
| `pressHero()` | 使用中 | Press 专属 hero |
| `judgingCounts()` | 使用中 | Judging counts 区块 |
| `judgingCountCard()` | 使用中 | Judging count card |
| `judgingNetwork()` | 使用中 | Judging network chips |
| `judgingNetworkGroup()` | 当前未调用 | 分组 network chips |
| `judgingPackage()` | 使用中 | Judging evidence package |
| `judgingPackageCard()` | 使用中 | Package card |
| `pressLookFor()` | 使用中 | Press standards checklist |
| `outletNetwork()` | 使用中 | Representative outlet chips |
| `pressCheck()` | 使用中 | Press checklist item |
| `pressCadenceNote()` | 使用中 | Press cadence note |
| `pressAvoidSection()` | 使用中 | Press avoid section |
| `avoidCard()` | 使用中 | Avoid card |
| `miniGrid()` | 使用中 | mini card grid |
| `articleGrid()` | 使用中 | blog article grid |
| `o1CriteriaPage()` | 使用中 | O-1A 8 criteria 页面 |
| `criterionCard()` | 使用中 | Criteria card |
| `faqPage()` | 使用中 | FAQ 页面 |
| `faqGroup()` | 使用中 | FAQ 分组 |
| `faqItem()` | 使用中 | FAQ 单项 |
| `resourcesHub()` | 当前未调用 | Resource hub 草稿 |
| `resourceGroup()` | 当前未调用 | Resource group 草稿 |
| `legalPage()` | 当前未调用 | 旧版 legal page 工厂 |
| `legalTermsPage()` | 使用中 | Terms 页面 |
| `legalPrivacyPage()` | 使用中 | Privacy 页面 |
| `legalDisclaimerPage()` | 使用中 | Disclaimer 页面 |
| `disclaimerSection()` | 使用中 | Disclaimer section |
| `disclaimerPart()` | 使用中 | Disclaimer p/ul 渲染 |
| `whyEarlySection()` | 使用中 | 首页 Start Early |
| `servicesIntro()` | 使用中 | 首页 Core services intro |
| `standardPreview()` | 使用中 | 首页标准问题 |
| `question()` | 使用中 | 标准问题 item |
| `howItWorks()` | 使用中 | 首页四步流程和价格 |
| `step()` | 使用中 | 流程 step |
| `standardGrid()` | 当前未调用 | 标准 grid 草稿 |
| `whyPaprikaProof()` | 使用中 | Why 页面 v2 内容 |
| `whyV2Card()` | 使用中 | Why card |
| `pressQuality()` | 当前未调用 | 旧版 Press quality 对比 |
最终渲染逻辑:
```js
const params = new URLSearchParams(window.location.search);
const page = params.get("page") || "home";
const selected = pageData[page] ? page : "home";
document.title = `Paprika - ${selected}`;
document.getElementById("app").innerHTML = pageData[selected].body;
```
active 逻辑:
- `[data-page]``selected` 相同时添加 `.active`
- `selected``faq``resources` 时,额外给 `.nav-trigger` 添加 `.active`
## 20. CTA 和链接全量映射
| 位置 | 文案 | 目标 |
| --- | --- | --- |
| Header brand | Paprika | `?page=home` |
| Header nav | Press | `?page=press` |
| Header nav | Judging | `?page=judging` |
| Header nav | Scholarly Articles | `?page=scholarly` |
| Header nav | Services | `?page=services` |
| Header nav | Why Paprika | `?page=why` |
| Header Resources | Resources | `?page=resources` |
| Resources menu | Frequently Asked Questions | `?page=faq` |
| Resources menu | O-1A Criteria Overview | `?page=resources` |
| Header CTA | Start Here | `?page=start` |
| Footer | About | `?page=why` |
| Footer | FAQ | `?page=faq` |
| Footer | Terms | `?page=terms` |
| Footer | Privacy | `?page=privacy` |
| Footer | Disclaimer | `?page=disclaimer` |
| Home hero | Send Us Your Profile | `?page=start` |
| Home hero | Ask a Question | `?page=ask` |
| 通用 hero | Start Here | `?page=start` |
| 通用 hero | Ask a Question | `?page=ask` |
| Services CTA | Send Us Your Profile | `?page=start` |
| Home pricing band | See Services | `?page=services` |
| Start mockup | Submit Profile | `?page=thanks` |
| Ask mockup | Send Question | `?page=thanks` |
| Thanks | Back to Homepage | `?page=home` |
| Service card 01 | Explore Press | `?page=press` |
| Service card 02 | Explore Judging | `?page=judging` |
| Service card 03 | Explore Articles | `?page=scholarly` |
| Service card 04 | Start Here | 原为 `?page=contact`,已修复为 `?page=start` |
| Service card 05 | Start Here | 原为 `?page=contact`,已修复为 `?page=start` |
| Service card 06 | Start Here | 原为 `?page=contact`,已修复为 `?page=start` |
## 21. FAQ 完整编号对标
当前 FAQ 共 4 组、20 个问题。
| 编号范围 | 分组 kicker | 分组标题 | 问题数量 |
| --- | --- | --- | --- |
| 01-09 | About Paprika | How Paprika works. | 9 |
| 10-14 | O1 Visa | O-1 Basics | 5 |
| 15-18 | Timeline | How long things take. | 4 |
| 19-20 | Pricing Structure | Pricing, scope, and refunds | 2 |
需要注意的原文问题:
- `O1 Visa` kicker 少了 hyphen是否改为 `O-1 Visa` 需产品确认。
- FAQ 15 中原文 `Coverage spaced out over time demonstrate...` 主谓可能不一致。
- FAQ 20 写了 “If we are unable to secure the agreed-upon media placements...” 可能只覆盖 media placements不覆盖 judging/scholarly应由法务或产品确认。
## 22. Legal 页面逐项结构对标
### Terms of Service
当前 Terms 共 18 个 section但编号为 1-16、18、19缺少 17。
| 编号 | 标题 |
| --- | --- |
| 1 | Agreement to These Terms |
| 2 | What Paprika Provides |
| 3 | Not a Law Firm — No Legal Advice |
| 4 | No Attorney-Client Relationship |
| 5 | No Guarantee of Immigration Outcomes |
| 6 | Attorney Referrals |
| 7 | Fees, Payment, and Refunds |
| 8 | Client Responsibilities and Accuracy of Information |
| 9 | Evidence Integrity |
| 10 | Third-Party Decisions |
| 11 | Your Materials |
| 12 | Work Product |
| 13 | Prohibited Uses |
| 14 | Disclaimer of Warranties |
| 15 | Limitation of Liability |
| 16 | Changes to These Terms |
| 18 | Governing Law |
| 19 | Contact |
### Privacy Policy
当前 Privacy 共 12 个 section
1. Introduction
2. Information We Collect
3. How We Use Your Information
4. How We Share Your Information
5. Data Storage and Security
6. Data Retention
7. Your Rights
8. Cookies
9. Children's Privacy
10. International Users
11. Changes to This Policy
12. Contact
### Disclaimer
当前 Disclaimer 共 9 个 section
1. Not a Law Firm
2. What "Attorney-Curated" Means
3. No Attorney-Client Relationship
4. Independent Counsel
5. Attorney Referral Disclosure
6. No Outcome Guarantee
7. User Responsibility
8. General Information Only
9. Questions
需要注意的原文问题:
- Disclaimer 中 `Paprika's evidence-building services is shaped...` 主谓不一致。
- Disclaimer 中 `Use of Paprika's does not create...` 语句不完整。
- 三个 legal 页面日期已由 `Last updated: July 10, 2026` 修正为 `Last updated: July 6, 2026`
- 当前日期为 `2026-07-06`,发布前仍需法务确认文本内容和最终发布日期。
## 23. 源码级对标结论
- `doc/index.html` 共 3423 行。
- 前 2261 行主要是 CSS 和 HTML shell。
- 2262 行后进入 body、header、main、footer、script。
- 2300 行后定义 `serviceCards``pageData`
- 2428 行后定义组件/页面函数。
- 3410 行后执行路由选择、渲染和 active 状态设置。
- 当前没有外部依赖、图片、字体文件、构建脚本、后端接口或测试。
## 24. 后续推进入口
- 任务领取:`04-任务矩阵.md`
- 每轮记录:`03-推进台账.md`
- 验收证据:`05-验收证据.md`
- 决策依据:`06-决策记录.md`
- 开发步骤:`02-项目程序开发详细步骤.md`

View File

@@ -0,0 +1,670 @@
# 02-项目程序开发详细步骤
## 1. 开发原则
1. 先完整对标 `doc/index.html`,再做工程化拆分。
2. 每一轮开发只领取 `04-任务矩阵.md` 中明确编号的任务,避免重复做。
3. 每一轮必须同步更新:
- `03-推进台账.md`
- `04-任务矩阵.md`
- `05-验收证据.md`
- 必要时更新 `06-决策记录.md`
4. 涉及移民、法律、隐私、退款、attorney referral、表单收集的内容必须留有验收证据和确认记录。
5. 不把明文服务器密码写入长期文档;部署凭据用 SSH key、环境变量或受控密钥库。
## 2. 当前基线复现
目标:确认 `doc/index.html` 当前 mockup 可回溯、可打开、可作为工程化基准。
步骤:
1. 进入项目目录:
```bash
cd /home/mes123456/MeetPaprika
```
2. 确认文件存在:
```bash
test -f doc/index.html && wc -l doc/index.html
```
3. 检查当前页面路由和脚本入口:
```bash
rg -n "pageData|URLSearchParams|data-page|page=start|page=ask|page=terms|page=privacy|page=disclaimer" doc/index.html
```
4. 浏览器打开以下页面并记录截图:
- `doc/index.html?page=home`
- `doc/index.html?page=press`
- `doc/index.html?page=judging`
- `doc/index.html?page=scholarly`
- `doc/index.html?page=services`
- `doc/index.html?page=why`
- `doc/index.html?page=resources`
- `doc/index.html?page=faq`
- `doc/index.html?page=start`
- `doc/index.html?page=ask`
- `doc/index.html?page=terms`
- `doc/index.html?page=privacy`
- `doc/index.html?page=disclaimer`
验收标准:
- 所有公开路由能渲染。
- 未识别 `?page=unknown` 回退 `home`。
- 顶部导航 active 状态正确。
- Resources 下拉可见且链接正确。
- 证据记录写入 `05-验收证据.md`。
## 3. 阶段 A静态 mockup 修复
目标:不引入构建系统,先修复当前单文件中影响体验和转化的明确问题。
任务:
1. 修复 Services 页面 04/05/06 卡片 CTA。
- 已由 `contact` 改为 `start`。
2. 明确 `blog` 和 `about` 是否保留。
- 若保留,加入导航或 footer。
- 若不保留,记录为隐藏路由。
3. 检查 legal 页面日期。
- 已由 `July 10, 2026` 改为 `July 6, 2026`。
- 当前工作日期为 `2026-07-06`。
4. 检查首页、Press、Judging、Services、FAQ、Legal 移动端布局。
5. 补充基础 SEO
- description
- canonical
- Open Graph
- favicon 占位或正式资源
6. 将 mockup 表单明确标注为占位,或进入阶段 B 改为真实表单。
验收标准:
- 无明显死链。
- 所有 CTA 指向存在页面。
- 法务日期有明确确认。
- 桌面和移动端核心页面无明显重叠。
- 变更写入台账和证据。
## 4. 阶段 B真实表单与线索收集
目标Start Here 和 Ask a Question 从视觉 mockup 变成真实可提交入口。
### 推荐字段
Start Here
- name required
- email required
- linkedin
- company_website
- current_role_company
- field_or_industry
- resume_cv optional upload
- reason required
- existing_recognition
- timeline
- consent/disclaimer acknowledgement
Ask a Question
- name required
- email required
- linkedin optional
- resume_cv optional upload
- question required
- consent/disclaimer acknowledgement
### 实现路径选项
| 方案 | 适用 | 优点 | 风险 |
| --- | --- | --- | --- |
| 第三方表单服务 | 快速上线 | 开发少,有后台记录 | 文件上传和隐私策略受限 |
| 自建轻量 API | 需要控制数据 | 可控、可扩展 | 需要安全、日志、备份 |
| CRM 直连 | 销售流程明确 | 直接进入业务系统 | 依赖第三方 API 和权限 |
建议优先级:
1. 若目标是快速 HTTPS 上线:先接第三方表单或 webhook。
2. 若会收 CV/敏感信息:优先自建 API加上传限制和访问控制。
3. 若已有 CRM按 CRM 字段建映射并保留 submission id。
### 表单验收标准
- 必填字段缺失时不能提交。
- email 格式错误时提示。
- 文件上传明确限制类型和大小。
- 成功后有 submission id 或后台记录 id。
- 失败时显示错误状态,不静默跳转。
- disclaimer 可见:提交不创建 attorney-client relationship。
- 验收证据包含页面截图、提交记录、job_id/report_id/submission id。
## 5. 阶段 CNext.js + React + PostgreSQL 工程化
目标:把单文件 mockup 迁移为 Next.js + React 应用,并接入 PostgreSQL形成可维护、可部署、可验收的正式工程。
### 5.1 技术架构
- 前端和服务端框架Next.js + React
- 语言TypeScript
- 样式:先使用全局 CSS / CSS Modules 迁移 `doc/index.html` 视觉;不强行引入 UI 框架
- 数据库PostgreSQL
- ORMPrisma
- 表单校验Zod 或等价 schema 校验
- 部署Node.js 进程监听 `8082`Nginx/Caddy 提供 HTTPS 反向代理
- 验收Playwright 页面截图 + API/数据库写入验证
### 5.2 初始化工程
当前已完成初始化。保留建议命令供复建参考:
```bash
npx create-next-app@latest paprika-site --ts --eslint --app --src-dir=false
cd paprika-site
npm install prisma @prisma/client zod
npx prisma init
```
本项目已直接在当前根目录工程化,并保留 `doc/index.html` 为对标基线。
### 5.3 页面迁移
从 `doc/index.html` 迁移为真实路由:
| 旧路由 | Next.js 路由 |
| --- | --- |
| `?page=home` | `/` |
| `?page=press` | `/press` |
| `?page=judging` | `/judging` |
| `?page=scholarly` | `/scholarly` |
| `?page=services` | `/services` |
| `?page=why` | `/why` |
| `?page=resources` | `/resources` |
| `?page=faq` | `/faq` |
| `?page=start` | `/start` |
| `?page=ask` | `/ask` |
| `?page=thanks` | `/thanks` |
| `?page=terms` | `/terms` |
| `?page=privacy` | `/privacy` |
| `?page=disclaimer` | `/disclaimer` |
隐藏路由 `blog`、`about` 先不要公开,除非产品确认。
### 5.4 PostgreSQL 数据模型初稿
建议最小表:
```prisma
model Submission {
id String @id @default(cuid())
type String
name String
email String
linkedin String?
companyWebsite String?
currentRoleCompany String?
fieldOrIndustry String?
reason String?
existingRecognition String?
timeline String?
question String?
consentAccepted Boolean @default(false)
sourcePath String?
status String @default("new")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
files SubmissionFile[]
}
model SubmissionFile {
id String @id @default(cuid())
submissionId String
fileName String
mimeType String
byteSize Int
storageKey String
createdAt DateTime @default(now())
submission Submission @relation(fields: [submissionId], references: [id])
}
```
字段说明:
- `type``start` 或 `ask`
- `sourcePath`:提交来源,如 `/start`、`/ask`
- `status``new`、`reviewed`、`spam`、`archived`
- 文件上传未落地前,可以先不创建 `SubmissionFile` 或禁用上传。
### 5.5 表单 API
建议 API
```text
POST /api/submissions
```
请求类型:
- `type=start`
- `type=ask`
验收:
- 必填字段缺失返回 400。
- 邮箱格式错误返回 400。
- 成功写入 PostgreSQL 并返回 `submission_id`。
- 前端成功后跳转 `/thanks?submission_id=...` 或展示成功状态。
- 失败时显示明确错误。
### 5.6 环境变量
`.env` 至少包含:
```bash
DATABASE_URL="postgresql://USER:PASSWORD@HOST:5432/paprika"
NEXT_PUBLIC_SITE_URL="https://paprikalaw.com"
```
规则:
- `.env` 不提交。
- 服务器凭据和数据库密码不写入 `working` 文档。
- 生产数据库用户只授予应用所需权限。
### 5.7 服务器 PostgreSQL 验证
用户已确认 PostgreSQL 在服务器 `170.106.192.152` 安装完成。正式接入前需要在服务器上验证:
```bash
ssh ubuntu@170.106.192.152
systemctl status postgresql --no-pager
psql --version
sudo -u postgres psql -c '\l'
```
建议创建应用数据库和受限应用用户:
```sql
CREATE DATABASE paprika;
CREATE USER paprika_app WITH ENCRYPTED PASSWORD 'REPLACE_WITH_SECURE_PASSWORD';
GRANT CONNECT ON DATABASE paprika TO paprika_app;
```
进入数据库后授予 schema 权限:
```sql
\c paprika
GRANT USAGE, CREATE ON SCHEMA public TO paprika_app;
```
生产 `DATABASE_URL` 示例:
```bash
DATABASE_URL="postgresql://paprika_app:REPLACE_WITH_SECURE_PASSWORD@127.0.0.1:5432/paprika"
```
注意:
- 不使用 SSH 登录密码作为数据库密码。
- 不把数据库密码写入仓库、`working` 文档或聊天后的可复制文档。
- 如果 PostgreSQL 仅供本机 Next.js 使用,优先监听 `127.0.0.1`,不要对公网开放 5432。
### 5.8 构建和本地验证
```bash
npm install
npm run generate:legacy
npm run prisma:generate
npm run lint
npx tsc --noEmit
npm run build
npm run start -- -p 8082
```
本地生产服务冒烟:
```bash
for p in / /press /judging /scholarly /services /why /resources /faq /start /ask /thanks /terms /privacy /disclaimer /robots.txt /sitemap.xml; do
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8082$p)
printf '%s %s\n' "$code" "$p"
done
```
生产数据库迁移:
```bash
npx prisma migrate deploy
```
验收标准:
- `npm run dev` 可本地启动。
- `npm run build` 通过。
- `npm run start -- -p 8082` 可在 8082 启动。
- `npx prisma migrate deploy` 可在生产环境执行。
- 核心页面内容、视觉和 CTA 与 `doc/index.html` 对标。
- Start/Ask 提交能写入 PostgreSQL。当前本地未配置 `DATABASE_URL` 时只能验证 400 校验路径和 500 失败路径,不能关闭入库验收。
## 6. 阶段 D部署到云服务器
目标:让 `https://paprikalaw.com/` 访问目标站点,内部服务端口使用 `8082`。
### 服务器信息
- Host`170.106.192.152`
- User`ubuntu`
- 端口范围:`8080-8099` 已开放
- 本项目端口:`8082`
- 密码:不写入文档;使用用户交接的临时凭据登录后立即配置 SSH key
### 建议部署结构
```text
/opt/paprika/
current/
releases/
shared/
.env
logs/
```
### 基础命令模板
```bash
ssh ubuntu@170.106.192.152
sudo mkdir -p /opt/paprika/releases /opt/paprika/shared/logs
sudo chown -R ubuntu:ubuntu /opt/paprika
```
### Next.js 服务示例
构建并启动:
```bash
npm ci
npm run build
npm run start -- -p 8082
```
生产建议用 systemd
```ini
[Unit]
Description=Paprika static site
After=network.target
[Service]
Type=simple
User=ubuntu
WorkingDirectory=/opt/paprika/current
Environment=NODE_ENV=production
EnvironmentFile=/opt/paprika/shared/.env
ExecStart=/usr/bin/npm run start -- -p 8082
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
```
### Nginx 反向代理示例
```nginx
server {
listen 80;
server_name paprikalaw.com www.paprikalaw.com;
location / {
proxy_pass http://127.0.0.1:8082;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
HTTPS
```bash
sudo certbot --nginx -d paprikalaw.com -d www.paprikalaw.com
```
验收标准:
- `curl -I http://127.0.0.1:8082` 在服务器上返回 200。
- `curl -I https://paprikalaw.com/` 返回 200。
- `https://paprikalaw.com/`、`/start`、`/faq` 可访问。
- `POST /api/submissions` 能写入 PostgreSQL 并返回 `submission_id`。
- systemd 服务 `active (running)`。
- Nginx 配置通过 `sudo nginx -t`。
- HTTPS 证书有效。
## 7. 阶段 E回归验收
目标:上线前后能证明页面、表单、合规、部署都可用。
### 页面回归
必测页面:
- home
- press
- judging
- scholarly
- services
- why
- resources
- faq
- start
- ask
- thanks
- terms
- privacy
- disclaimer
### 设备尺寸
- Desktop1440x900
- Tablet768x1024
- Mobile390x844
### 验收内容
- 页面可访问。
- 首屏品牌和 CTA 可见。
- Header 不遮挡内容。
- 文本无明显重叠。
- CTA 链接可达。
- 表单可提交或明确是 mockup。
- Footer legal 链接可达。
- Legal disclaimer 可见。
### 证据格式
每次验收写入 `05-验收证据.md`
```text
EV-YYYYMMDD-序号
时间:
任务:
命令/页面:
结果:
证据文件:
job_id/report_id/submission_id
结论:
```
## 8. 阶段 F后续产品增强
可选任务:
- 增加真实 Contact 页面。
- 增加 Blog/Resources 内容系统。
- 增加案例或证据包示例,但必须避免误导和隐私泄露。
- 增加 analytics并同步 Privacy Policy。
- 增加 sitemap、robots、structured data。
- 增加 Playwright screenshot regression。
- 增加后台 leads 管理或 CRM 同步。
## 9. 阶段 G邮箱系统配置与验证
目标:确认 `mail.paprikalaw.com` 邮件系统可用,管理员和普通公司邮箱能登录、收发和承接网站通知。
### 9.1 邮箱入口
- 普通用户网页登录:`https://mail.paprikalaw.com/webmail/`
- 管理员网页登录:`https://mail.paprikalaw.com/admin/`
- 管理员邮箱:`admin@paprikalaw.com`
### 9.2 需要创建或确认的普通邮箱
```text
info@paprikalaw.com
contact@paprikalaw.com
support@paprikalaw.com
sales@paprikalaw.com
marketing@paprikalaw.com
hr@paprikalaw.com
finance@paprikalaw.com
billing@paprikalaw.com
legal@paprikalaw.com
service@paprikalaw.com
```
凭据规则:
- 用户已提供统一初始密码,但不写入长期文档。
- 建议创建后立即为每个邮箱设置独立强密码。
- 如果邮件系统支持启用首次登录改密、2FA、登录审计和反垃圾策略。
### 9.3 DNS 和投递验证
需要确认:
- `mail.paprikalaw.com` A/AAAA 记录正确。
- `paprikalaw.com` MX 记录指向邮件服务器。
- SPF 记录包含授权发件源。
- DKIM 已生成并加入 DNS。
- DMARC 已配置,至少先从 `p=none` 观察。
- 反向 DNS/PTR 与邮件服务器发信域名匹配。
建议验证:
```bash
dig MX paprikalaw.com
dig TXT paprikalaw.com
dig TXT default._domainkey.paprikalaw.com
dig TXT _dmarc.paprikalaw.com
```
### 9.4 网站表单通知接入
Next.js 表单写入 PostgreSQL 后,可选邮件通知:
- 发件邮箱:建议 `support@paprikalaw.com` 或专用 `no-reply@paprikalaw.com`
- 收件邮箱:建议 `support@paprikalaw.com`、`contact@paprikalaw.com`
- 表单类型:
- `/start` 提交通知
- `/ask` 提交通知
- 邮件内容不得包含完整敏感 CV 文件,只发送 submission id 和后台查看提示。
环境变量示例:
```bash
SMTP_HOST="mail.paprikalaw.com"
SMTP_PORT="587"
SMTP_USER="support@paprikalaw.com"
SMTP_PASSWORD="REPLACE_WITH_SECURE_PASSWORD"
LEADS_NOTIFY_TO="support@paprikalaw.com,contact@paprikalaw.com"
```
验收标准:
- 管理员能登录 admin。
- 10 个普通邮箱能登录 webmail。
- 至少完成一封内部互发、一封外部收信、一封外部发信测试。
- SPF/DKIM/DMARC 检查通过或有明确待处理项。
- 表单邮件通知如启用,必须记录 message id 或测试截图。
## 10. 每轮工作固定流程
1. 查看 `04-任务矩阵.md`,领取一个或一组任务。
2. 查看相关决策:`06-决策记录.md`。
3. 修改代码或文档。
4. 执行对应验证。
5. 将证据写入 `05-验收证据.md`。
6. 更新任务状态。
7. 在 `03-推进台账.md` 记录:
- 本轮做了什么
- 改了哪些文件
- 验证了什么
- 下一步是什么
## 11. 完全对标核验清单
每次声称“已完全对标 `doc/index.html`”时,至少执行以下核验。
### 10.1 源文件结构核验
```bash
wc -l doc/index.html
rg -n 'function [A-Za-z0-9_]+\(|const [A-Za-z0-9_]+ =|href="\?page=|Last updated|Packages start|@media|--[a-z-]+:' doc/index.html
```
验收:
- 记录 `doc/index.html` 行数。
- 记录 CSS token、断点、路由、CTA、函数、法务日期、价格文案。
### 10.2 页面和函数映射核验
```bash
rg -n 'pageData|home:|press:|judging:|scholarly:|services:|why:|resources:|faq:|blog:|start:|ask:|thanks:|about:|terms:|privacy:|disclaimer:' doc/index.html
rg -n 'function (hero|heroWithKicker|servicesCta|card|pressPage|judgingPage|scholarlyPage|o1CriteriaPage|faqPage|legalTermsPage|legalPrivacyPage|legalDisclaimerPage)' doc/index.html
```
验收:
- `01-项目功能内容.md` 中的路由清单与 `pageData` 保持一致。
- 使用中函数、未调用函数都有记录。
- 隐藏路由 `blog`、`about` 不被误删。
### 10.3 链接和 CTA 核验
```bash
rg -n 'href="\?page=|card\("' doc/index.html
```
验收:
- Header、footer、hero、services CTA、form mockup、thanks、service card 链接全部登记。
- 不存在的 `contact` 路由历史缺陷必须保留在风险和任务矩阵中;修复后记录验收证据。
### 10.4 Legal 和 FAQ 核验
```bash
sed -n '2870,3035p' doc/index.html
sed -n '3070,3228p' doc/index.html
```
验收:
- FAQ 4 组 20 个问题记录完整。
- Terms、Privacy、Disclaimer section 数量和标题记录完整。
- Terms 缺少 17、legal 日期晚于当前日期、原文语法风险均进入任务矩阵。
### 10.5 视觉样式核验
```bash
rg -n '^\\s*\\.[A-Za-z0-9_-]+|@media|grid-template-columns|width: min|border-radius|background:' doc/index.html
```
验收:
- CSS token、布局宽度、卡片、表单、Press、Judging、FAQ、Legal、响应式断点在文档中有对应说明。
- 后续工程化拆分不能只迁移内容,必须迁移视觉规则。

253
working/03-推进台账.md Normal file
View File

@@ -0,0 +1,253 @@
# 03-推进台账
本文件按轮次记录工作。每轮结束必须写清楚:做了什么、改了哪些文件、验证了什么、下一步是什么。
## R001 - 建立 working 文档体系
- 日期2026-07-06
- 任务编号T001、T002
- 做了什么:
- 阅读 `doc/index.html` 的页面结构、导航、路由、表单 mockup、服务模块、FAQ、O-1A criteria、法务页面和渲染脚本。
- 建立 `working` 目录下 6 份编号文档和 README 索引。
- 初步记录当前项目是自包含静态 HTML mockup。
- 改了哪些文件:
- `working/README.md`
- `working/01-项目功能内容.md`
- `working/02-项目程序开发详细步骤.md`
- `working/03-推进台账.md`
- `working/04-任务矩阵.md`
- `working/05-验收证据.md`
- `working/06-决策记录.md`
- 验证了什么:
- 确认 `doc/index.html` 是自包含静态 HTML。
- 确认页面通过 `?page=...` 查询参数切换。
- 确认 Start Here 和 Ask a Question 当前是视觉 mockup不是真实表单。
- 证据链接或编号E001、E002、E003
- 遗留问题:
- 未做浏览器截图验收。
- 未做移动端回归。
- 未实现表单和部署。
- 下一步:
-`04-任务矩阵.md``VERIFY``TODO` 任务继续推进。
## R002 - 完全对标 doc/index.html 补强 working 文档
- 日期2026-07-06
- 任务编号T028
- 做了什么:
- 复读 `doc/index.html` 中 header、footer、`pageData`、页面组合函数、Press/Judging/Resources/FAQ/Legal/Form 模块。
- 补充 `01-项目功能内容.md`把页面路由、内容模块、表单字段、服务卡片、O-1A criteria、法律页面、已知缺陷和发布环境写完整。
- 补充 `02-项目程序开发详细步骤.md`形成阶段化路线基线复现、静态修复、真实表单、工程化拆分、8082 端口部署、HTTPS、回归验收。
- 补强任务矩阵新增服务器部署、凭据处理、contact 死链、outlet/network 陈述风险、README 索引维护等任务。
- 补强验收证据,记录本轮命令、源码发现和后续证据清单。
- 补强决策记录,明确不在长期文档保存明文密码、服务默认走 8082、Services 的 `contact` 路由是缺陷。
- 改了哪些文件:
- `working/README.md`
- `working/01-项目功能内容.md`
- `working/02-项目程序开发详细步骤.md`
- `working/03-推进台账.md`
- `working/04-任务矩阵.md`
- `working/05-验收证据.md`
- `working/06-决策记录.md`
- 验证了什么:
- `rg --files` 确认项目当前主要文件为 `doc/index.html``doc/README.md``working/*.md`
- `rg -n` 确认公开路由、隐藏路由、表单 mockup、legal 页面、动态路由逻辑均在 `doc/index.html`
- `sed` 分段读取源文件,确认 Services 04/05/06 卡片指向不存在的 `contact` 页面。
- `git status --short` 失败,确认当前目录不是 Git 仓库或未在 Git 工作树内。
- 证据链接或编号E004、E005、E006、E007
- 遗留问题:
- 尚未改 `doc/index.html`
- 尚未启动本地服务器或做截图。
- 尚未登录远端服务器。
- 用户提供了密码,但长期文档未写入明文密码。
- 下一步:
- 优先处理 T029修复 Services 页面不存在的 `contact` 路由。
- 继续 T003-T011对核心页面做浏览器截图验收。
- 之后按产品选择推进 T012/T013 表单落地或 T022 工程化拆分。
## R003 - 源码级对标附录完善
- 日期2026-07-06
- 任务编号T034
- 做了什么:
- 重新扫描 `doc/index.html` 的 CSS token、响应式断点、路由链接、函数列表、FAQ、Legal sections 和渲染逻辑。
-`01-项目功能内容.md` 增加视觉设计、组件 CSS 类、JavaScript 函数、CTA 全量映射、FAQ 完整编号、Legal 逐项结构和源码级结论。
-`02-项目程序开发详细步骤.md` 增加“完全对标核验清单”,规定以后如何证明文档与源 HTML 对齐。
- 更新任务矩阵、验收证据和决策记录。
- 改了哪些文件:
- `working/01-项目功能内容.md`
- `working/02-项目程序开发详细步骤.md`
- `working/03-推进台账.md`
- `working/04-任务矩阵.md`
- `working/05-验收证据.md`
- `working/06-决策记录.md`
- 验证了什么:
- `doc/index.html` 共 3423 行。
- CSS 只有一个显式响应式断点:`@media (max-width: 1080px)`
- `pageData` 包含公开路由、隐藏路由和 legal 页面。
- FAQ 共 4 组 20 个问题。
- Terms 编号缺少 17Legal 日期均为 `July 10, 2026`
- 多个函数和 CSS 模块存在但当前未调用。
- 证据链接或编号E016
- 遗留问题:
- 仍未修改 `doc/index.html` 源码。
- 仍未做浏览器截图和真实部署验收。
- 下一步:
- 按 T029 修复 `contact` 缺陷,或按 T003-T011 做页面截图验收。
## R004 - 明确正式开发技术架构
- 日期2026-07-06
- 任务编号T035
- 做了什么:
- 根据用户确认,将正式开发路线从候选方案收敛为 Next.js + React + PostgreSQL。
- 将默认数据访问层定为 Prisma ORM。
- 补充 Next.js 路由迁移表、PostgreSQL 数据模型初稿、表单 API、环境变量、8082 启动方式和部署验收标准。
- 更新任务矩阵和决策记录。
- 改了哪些文件:
- `working/01-项目功能内容.md`
- `working/02-项目程序开发详细步骤.md`
- `working/03-推进台账.md`
- `working/04-任务矩阵.md`
- `working/05-验收证据.md`
- `working/06-决策记录.md`
- `working/README.md`
- 验证了什么:
- 文档已不再把 Vite/静态拆分作为推荐路线。
- 部署命令已调整为 Next.js `npm run start -- -p 8082`
- 数据库任务已进入矩阵。
- 证据链接或编号E017
- 遗留问题:
- 尚未实际创建 Next.js 工程。
- 尚未创建 PostgreSQL 数据库或 Prisma migration。
- 文件上传存储方案仍需单独确认。
- 下一步:
- 执行 T036 初始化 Next.js 工程。
- 执行 T037 设计并迁移 PostgreSQL schema。
## R005 - 记录服务器 PostgreSQL 安装状态
- 日期2026-07-06
- 任务编号T040
- 做了什么:
- 根据用户反馈,记录 PostgreSQL 已在服务器 `170.106.192.152` 安装完成。
- 补充服务器 PostgreSQL 验证命令、应用数据库和受限用户创建建议。
- 保持凭据安全规则:不把明文 SSH 密码写入长期文档。
- 改了哪些文件:
- `working/01-项目功能内容.md`
- `working/02-项目程序开发详细步骤.md`
- `working/03-推进台账.md`
- `working/04-任务矩阵.md`
- `working/05-验收证据.md`
- `working/06-决策记录.md`
- 验证了什么:
- 本轮未登录服务器验证,仅记录用户确认。
- 证据链接或编号E018
- 遗留问题:
- 需要远程执行 `systemctl status postgresql``psql --version`、数据库列表检查。
- 需要创建或确认 `paprika` 数据库和 `paprika_app` 应用用户。
- 下一步:
- 执行 T040 远程验证 PostgreSQL。
- 执行 T037 接入 Prisma schema 和 migration。
## R006 - 记录邮箱系统账号需求
- 日期2026-07-06
- 任务编号T041
- 做了什么:
- 记录普通用户 webmail 入口、管理员入口和 `admin@paprikalaw.com` 管理员账号。
- 记录 10 个普通公司邮箱账号清单。
- 补充邮件 DNS、收发验证、表单通知接入和凭据安全要求。
- 明确用户提供的统一初始密码不写入长期文档。
- 改了哪些文件:
- `working/01-项目功能内容.md`
- `working/02-项目程序开发详细步骤.md`
- `working/03-推进台账.md`
- `working/04-任务矩阵.md`
- `working/05-验收证据.md`
- `working/06-决策记录.md`
- `working/README.md`
- 验证了什么:
- 本轮只记录需求,未登录邮件后台,未验证 DNS/MX/SPF/DKIM/DMARC。
- 证据链接或编号E019
- 遗留问题:
- 需要登录 `https://mail.paprikalaw.com/admin/` 确认账号是否已创建。
- 需要测试 webmail 登录、内外部收发和 DNS 记录。
- 需要决定网站表单通知使用哪个邮箱。
- 下一步:
- 执行 T041 邮箱账号创建/确认。
- 执行 T042 邮件 DNS 和收发测试。
- 执行 T043 Next.js 表单邮件通知配置。
## R007 - 落地 Next.js 工程、Prisma 和真实表单
- 日期2026-07-06
- 任务编号T012、T013、T017、T020、T022、T029、T036、T037、T038、T039、T044
- 做了什么:
- 修复 `doc/index.html` 中 Services 04/05/06 CTA 指向不存在 `contact` 的缺陷,统一改为 `start`
- 将 legal 页面日期从未来日期 `July 10, 2026` 改为当前工作日期 `July 6, 2026`
- 在项目根目录初始化 Next.js + React + TypeScript App Router 工程。
- 编写 `scripts/extract-legacy-pages.mjs`,从 `doc/index.html` 抽取 CSS 和静态页面 HTML生成 `app/globals.css``lib/legacy-pages.ts`
- 建立 `app/[page]/page.tsx` 静态路由,迁移公开页面到 `/press``/judging``/services` 等路径路由。
- 建立 `/start``/ask` 真实 React 表单,提交到 `POST /api/submissions`
- 建立 Prisma schema、初始化 migration、`.env.example`、数据库访问层和 Zod 校验。
- 增加基础 SEO metadata、`robots.txt``sitemap.xml`
- 升级到 Next `16.2.10`、React `19.2.7`,并配置 ESLint 9 flat config。
- 改了哪些文件:
- `doc/index.html`
- `.env.example`
- `.gitignore`
- `package.json`
- `package-lock.json`
- `tsconfig.json`
- `next-env.d.ts`
- `next.config.mjs`
- `eslint.config.mjs`
- `scripts/extract-legacy-pages.mjs`
- `app/*`
- `components/*`
- `lib/*`
- `prisma/schema.prisma`
- `prisma/migrations/20260706120000_init/migration.sql`
- `working/README.md`
- `working/01-项目功能内容.md`
- `working/02-项目程序开发详细步骤.md`
- `working/03-推进台账.md`
- `working/04-任务矩阵.md`
- `working/05-验收证据.md`
- `working/06-决策记录.md`
- 验证了什么:
- `npm run generate:legacy` 成功生成 12 个旧页面数据。
- `npm run prisma:generate` 成功生成 Prisma Client。
- `npm run lint` 通过。
- `npx tsc --noEmit` 通过。
- `npm run build` 通过,生成 19 个 app route。
- `npm run start -- -p 8082` 本地生产服务可启动。
- `/``/press``/judging``/scholarly``/services``/why``/resources``/faq``/start``/ask``/thanks``/terms``/privacy``/disclaimer``/robots.txt``/sitemap.xml` 本地生产服务均返回 200。
- `/api/submissions` 对缺失必填字段和错误 email 返回 400 校验错误。
- 有效提交在未配置 `DATABASE_URL` 时返回 500确认入库成功仍需真实 PostgreSQL。
- 证据链接或编号E022、E023、E024、E025、E026
- 遗留问题:
- 本地未配置 PostgreSQL `DATABASE_URL`,尚不能完成真实入库验收。
- 远程 PostgreSQL、应用数据库、应用用户、migration deploy 尚未验证。
- 邮箱后台、DNS、SMTP 通知未验证。
- 法务内容审核、outlet/network 名称确认、文件上传策略仍需外部确认。
- 尚未做浏览器截图/PDF 归档。
- 下一步:
- 执行 T040/T044远程验证 PostgreSQL配置生产 `DATABASE_URL`,执行 migration并提交 Start/Ask 获取 `submission_id`
- 执行 T039/T024部署 Next.js 到服务器 `8082` 并配置 systemd。
- 执行 T041-T043验证邮箱和 SMTP 通知。
- 执行 T018/T026补桌面和移动端截图证据。
## 后续轮次模板
### RXXX - 标题
- 日期:
- 任务编号:
- 做了什么:
- 改了哪些文件:
- 验证了什么:
- 证据链接或编号:
- 遗留问题:
- 下一步:

View File

@@ -0,0 +1,81 @@
# 04-任务矩阵
状态定义:
- `DONE`:已完成并有证据。
- `VERIFY`:源码看起来具备,但缺浏览器、截图、端到端或人工确认。
- `TODO`:尚未实现。
- `BLOCKED`被外部输入、账号、法律审核、DNS 或产品决策阻塞。
- `DEFER`:暂缓,不影响当前阶段交付。
## 任务总表
| 编号 | 任务 | 状态 | 验收标准 | 依赖 | 备注 |
| --- | --- | --- | --- | --- | --- |
| T001 | 建立 working 文档体系 | DONE | 6 份编号文档和 README 索引存在 | 无 | R001 |
| T002 | 盘点当前页面路由 | DONE | 路由清单覆盖公开路由、隐藏路由和回退逻辑 | T001 | 见 01 |
| T003 | 顶部导航可用 | VERIFY | 点击 Press/Judging/Scholarly/Services/Why/Start 能进入对应页面active 状态正确 | T002 | 需截图 |
| T004 | Resources 下拉可用 | VERIFY | hover/focus 显示 FAQ 和 O-1A Criteria Overview链接正确 | T002 | 需桌面验证 |
| T005 | 首页内容完整 | VERIFY | hero、三大服务、Start Early、标准问题、四步流程、价格 band 渲染正常 | T002 | 需截图 |
| T006 | Services 页面完整 | VERIFY | 6 张服务卡片和说明渲染正常 | T002 | 存在 T029 缺陷 |
| T007 | Press 页面完整 | VERIFY | press hero、cadence、standards、avoid、outlet network、CTA 渲染正常 | T002 | 需截图 |
| T008 | Judging 页面完整 | VERIFY | judging counts、network、evidence package、CTA 渲染正常 | T002 | 需截图 |
| T009 | Scholarly 页面完整 | VERIFY | hero、两栏说明、CTA 渲染正常 | T002 | 需截图 |
| T010 | Resources/O-1A criteria 页面完整 | VERIFY | 8 项 criteria 全部展示,免责声明展示 | T002 | 需内容抽查 |
| T011 | FAQ 页面完整 | VERIFY | FAQ 分组和问题渲染正常 | T002 | 需内容抽查 |
| T012 | Start Here 表单真实可用 | VERIFY | 真实 `<form>` 或等价组件;必填校验;提交后产生可追踪记录 | T037/T044 | React 表单和 API 已实现,待真实 PostgreSQL 入库证据 |
| T013 | Ask a Question 表单真实可用 | VERIFY | 真实 `<form>` 或等价组件;必填校验;提交后产生可追踪记录 | T037/T044 | React 表单和 API 已实现,待真实 PostgreSQL 入库证据 |
| T014 | 文件上传策略 | DEFER | 明确支持或不支持上传;若支持,限制类型、大小、存储、访问权限 | T012/T013 | 当前代码禁用上传,待隐私/存储方案确认 |
| T015 | Thanks 页面流程验收 | VERIFY | 成功提交后进入 thanks直接访问 thanks 不造成误导 | T012/T013 | 当前仅链接跳转 |
| T016 | 法务页面内容审核 | BLOCKED | Terms、Privacy、Disclaimer 经负责人或律师确认 | 法务输入 | 当前为草稿 |
| T017 | 法务日期修正 | DONE | 页面日期不晚于实际发布日,或明确未来生效逻辑 | T016 | 已改为 `July 6, 2026`,内容审核仍见 T016 |
| T018 | 响应式布局验证 | TODO | 1440、768、390 宽度核心页面无重叠、溢出、不可读 | T003-T011 | 需截图 |
| T019 | 无障碍基础检查 | TODO | heading 层级合理;交互元素可聚焦;颜色对比和表单标签达标 | T012/T013 | 当前表单不是 input |
| T020 | SEO 基础 | VERIFY | 每页 title/description、OG、favicon、robots/sitemap 有明确策略 | 内容确认 | 已有 metadata、robots、sitemapfavicon/逐页文案待确认 |
| T021 | 分析和隐私策略 | DEFER | 若添加 analyticsPrivacy 同步说明并记录供应商 | T016 | 当前未添加 analytics |
| T022 | 工程化拆分 | DONE | 从单文件拆为可维护结构,或明确保持单文件的理由 | T002-T011 | 已新增 Next.js App Router 工程,保留 doc 基线 |
| T023 | 自动化测试 | TODO | 至少覆盖核心路由冒烟、表单提交、未知路由回退 | T022 | 当前无测试框架 |
| T024 | 部署到云服务器 8082 | TODO | 服务器 `127.0.0.1:8082` 返回 200systemd 或等价进程可自恢复 | T022 或静态服务 | 目标主机 170.106.192.152 |
| T025 | HTTPS 域名发布 | BLOCKED | `https://paprikalaw.com/` 返回 200证书有效 | DNS/服务器权限/T024 | 需确认 DNS 指向 |
| T026 | 截图/PDF 验收资产 | TODO | 核心页面截图或 PDF 存档,路径写入 `05-验收证据.md` | T018 | 用于回归比较 |
| T027 | `blog``about` 去留确认 | TODO | 明确保留、隐藏或删除;导航和 footer 一致 | 产品输入 | 当前代码存在但未公开 |
| T028 | 完全对标补强 working 文档 | DONE | 01-06 和 README 覆盖页面、开发步骤、台账、任务矩阵、证据、决策 | T001/T002 | R002 |
| T029 | 修复 Services 中不存在的 `contact` 路由 | DONE | Services 04/05/06 CTA 指向存在页面,点击不回退首页 | T006 | 已改 `start`Next `/services` 产物确认 href `/start` |
| T030 | 服务器凭据安全处理 | TODO | 不在仓库保存明文密码;配置 SSH key 或受控密钥交接 | T024 | 用户已提供临时凭据 |
| T031 | Outlet/network 陈述风险确认 | BLOCKED | 负责人确认可展示 outlet/network 名称或改成示例类型 | 产品/法务输入 | Press/Judging 涉及名称 |
| T032 | README 索引维护 | DONE | README 能指向 01-06并说明后续入口 | T028 | 本轮更新 |
| T033 | Git 仓库状态确认 | TODO | 明确项目是否应纳入 Git若是初始化或进入正确仓库后记录 commit hash | 用户/仓库输入 | 当前 `git status` 失败 |
| T034 | 源码级对标附录完善 | DONE | 文档覆盖 CSS token、组件类、函数清单、CTA 映射、FAQ 编号、Legal 结构、核验命令 | T028 | R003 |
| T035 | 明确正式技术架构 | DONE | 文档明确采用 Next.js + React + PostgreSQL并记录 ORM、路由、API、部署方式 | 用户确认 | R004 |
| T036 | 初始化 Next.js + React 工程 | DONE | `npm run dev``npm run build` 可用,基础页面路由建立 | T035 | TypeScript + App RouterNext 16 + React 19 |
| T037 | PostgreSQL + Prisma 接入 | VERIFY | `schema.prisma``.env.example`、migration 存在,能连接数据库 | T035/T040 | 本地代码完成;真实连接待生产 `DATABASE_URL` |
| T038 | 表单入库实现 | VERIFY | `/start``/ask` 提交写入 PostgreSQL并返回 `submission_id` | T036/T037/T044 | API 写入代码完成;缺真实 DB 成功提交证据 |
| T039 | Next.js 部署到 8082 | VERIFY | `npm run start -- -p 8082` 生产可运行systemd 管理 | T036/T037 | 本地生产 8082 通过;远程 systemd 未做 |
| T040 | 远程验证 PostgreSQL 安装 | TODO | 服务器上 `systemctl status postgresql` 正常,`psql --version` 可用,应用数据库/用户确认 | 用户已确认安装 | 不记录明文密码 |
| T041 | 邮箱账号创建/确认 | TODO | admin 入口可登录10 个普通邮箱可通过 webmail 登录 | 邮件后台权限 | 不记录统一初始密码 |
| T042 | 邮件 DNS 与收发验证 | TODO | MX/SPF/DKIM/DMARC 检查完成,内外部收发测试通过 | T041/DNS 权限 | 记录 dig 输出和测试截图 |
| T043 | 表单邮件通知配置 | TODO | `/start``/ask` 入库后向指定邮箱发送通知,记录 message id | T038/T041 | SMTP 密码放环境变量 |
| T044 | 真实 PostgreSQL 入库验收 | TODO | 配置 `DATABASE_URL`Start/Ask 成功返回 `submission_id`,数据库可查记录 | T037/T040 | 本地未配置数据库,远程待执行 migration |
## 优先级建议
| 优先级 | 任务 | 原因 |
| --- | --- | --- |
| P0 | T029 | 明确死链,改动小,影响转化 |
| P0 | T017/T016 | 法务日期和文本影响正式发布可信度 |
| P0 | T012/T013/T014 | 当前无法真实收集线索 |
| P0 | T035/T036/T037/T038 | 正式架构已明确,需落地工程和数据库 |
| P0 | T040 | PostgreSQL 已安装,需验证后才能接 Prisma |
| P0 | T041/T042 | 公司邮箱是对外联系和表单通知基础设施 |
| P1 | T003-T011/T018/T026 | 页面上线前必须有截图证据 |
| P1 | T024/T025/T030 | 服务器发布和凭据安全 |
| P2 | T020/T021/T023 | SEO、analytics、自动化测试 |
| P2 | T022 | 工程化拆分取决于上线节奏 |
## 防重复规则
1. 开始任务前先查本表编号。
2. 已是 `DONE` 的任务不得重复做,除非新建变更任务并说明原因。
3. `VERIFY` 任务完成验证后改为 `DONE`,并在 `05-验收证据.md` 填证据。
4. 涉及法务、隐私、表单数据、部署凭据的任务必须有明确验收证据,不能只凭页面看起来正常关闭。
5. 每次新增任务必须使用下一个编号,并补齐状态、验收标准和依赖。

374
working/05-验收证据.md Normal file
View File

@@ -0,0 +1,374 @@
# 05-验收证据
本文件记录可复查证据命令、页面、job_id、report_id、PDF、截图、部署链接、提交记录等。
## E001 - 文档基线盘点
- 日期2026-07-06
- 关联任务T001、T002
- 类型:命令输出摘要
- 命令:
```bash
pwd
rg --files -g '!*node_modules*' -g '!*.png' -g '!*.jpg' -g '!*.jpeg' -g '!*.gif' -g '!*.pdf' | sed -n '1,160p'
sed -n '1,260p' doc/index.html
find working -maxdepth 2 -type f -print | sort
```
- 结果摘要:
- 当前工作目录为 `/home/mes123456/MeetPaprika`
- 参考文件存在:`doc/index.html`
- `working` 目录存在。
- `doc/index.html` 包含 `<main id="app"></main>`、内联 CSS、内联 JS。
- 页面通过 `?page=...` 查询参数选择内容。
## E002 - 当前路由证据
- 日期2026-07-06
- 关联任务T002
- 类型:源码证据
- 命令:
```bash
rg -n "const pages|function|data-page|render|page:|title:|href=|main.innerHTML|case|pages\\[|\\?page=|<body|<script|</html>|Services|Resources|FAQ|Privacy|Terms|Get Started|Application|Contact" doc/index.html
```
- 结果摘要:
- 导航显式链接:`press``judging``scholarly``services``why``resources``faq``start`
- Footer 链接:`why``faq``terms``privacy``disclaimer`
- `pageData` 中定义:`home``press``judging``scholarly``services``why``resources``faq``blog``start``ask``thanks``about``terms``privacy``disclaimer`
- 路由选择逻辑:未识别页面回退为 `home`
## E003 - 表单 mockup 证据
- 日期2026-07-06
- 关联任务T012、T013、T014、T015
- 类型:源码证据
- 文件:`doc/index.html`
- 结果摘要:
- `start` 页面字段使用 `<div class="field">`,提交入口是 `<a class="button primary" href="?page=thanks">Submit Profile</a>`
- `ask` 页面字段使用 `<div class="field">`,提交入口是 `<a class="button primary" href="?page=thanks">Send Question</a>`
- 当前没有 `<form>``input``textarea``file``method``action` 或提交接口。
- 因此当前只能证明视觉流程存在,不能证明真实提交能力。
## E004 - R002 源码复核证据
- 日期2026-07-06
- 关联任务T028
- 类型:命令输出摘要
- 命令:
```bash
sed -n '2262,2468p' doc/index.html
sed -n '2469,2765p' doc/index.html
sed -n '2766,3428p' doc/index.html
```
- 结果摘要:
- 复核 header、footer、`serviceCards``pageData``hero``heroWithKicker``pressPage``judgingPage``o1CriteriaPage``faqPage`、legal 页面和最终渲染逻辑。
- `document.getElementById("app").innerHTML = pageData[selected].body;`
- `if (["faq", "resources"].includes(selected))` 会高亮 Resources trigger。
- legal 页面当时日期写为 `Last updated: July 10, 2026`;后续已在 E024 修复。
## E005 - Services contact 路由缺陷证据
- 日期2026-07-06
- 关联任务T029
- 类型:源码证据
- 文件:`doc/index.html`
- 结果摘要:
- Services 页面卡片 04、05、06 调用:
- `card("04", ..., "Start Here", "contact")`
- `card("05", ..., "Start Here", "contact")`
- `card("06", ..., "Start Here", "contact")`
- `pageData` 没有 `contact`
- 当前路由逻辑会把未知 `contact` 回退到 `home`
- 结论:这是明确死链/误跳转缺陷;后续已在 E024 修复。
## E006 - Git 状态证据
- 日期2026-07-06
- 关联任务T033
- 类型:命令输出摘要
- 命令:
```bash
git status --short
```
- 结果摘要:
- 输出:`fatal: not a git repository (or any of the parent directories): .git`
- 结论:当前路径不是 Git 仓库,暂时无法记录 commit hash。
## E007 - Working 文档更新证据
- 日期2026-07-06
- 关联任务T028、T032
- 类型:文件证据
- 文件:
- `working/README.md`
- `working/01-项目功能内容.md`
- `working/02-项目程序开发详细步骤.md`
- `working/03-推进台账.md`
- `working/04-任务矩阵.md`
- `working/05-验收证据.md`
- `working/06-决策记录.md`
- 结果摘要:
- 已形成 README 索引。
- 已形成从项目功能、开发步骤、推进台账、任务矩阵、验收证据到决策记录的闭环。
## E016 - 源码级对标扫描证据
- 日期2026-07-06
- 关联任务T034
- 类型:命令输出摘要
- 命令:
```bash
wc -l doc/index.html working/*.md
rg -n 'function [A-Za-z0-9_]+\(|const [A-Za-z0-9_]+ =|^[[:space:]]+[A-Za-z0-9_]+: \{|href="\?page=|Last updated|Packages start|@media|--[a-z-]+:' doc/index.html
rg -n '^\s*\.[A-Za-z0-9_-]+|^\s*#[A-Za-z0-9_-]+|^\s*[a-z]+\s*\{|@media|grid-template-columns|width: min|border-radius|background:' doc/index.html
sed -n '2870,3035p' doc/index.html
sed -n '3070,3228p' doc/index.html
```
- 结果摘要:
- `doc/index.html` 共 3423 行。
- CSS token 包含 `--red``--deep-red``--ink``--text``--muted``--line``--paper``--panel``--green``--header-text`
- 唯一显式断点为 `@media (max-width: 1080px)`
- `pageData` 包含 `home``press``judging``scholarly``services``why``resources``faq``blog``start``ask``thanks``about``terms``privacy``disclaimer`
- FAQ 共 4 组 20 个问题。
- Terms 共 18 个 section但编号缺少 17。
- Privacy 共 12 个 section。
- Disclaimer 共 9 个 section。
- Legal 页面当时日期均为 `Last updated: July 10, 2026`;后续已在 E024 修复。
- 发现未调用函数/模块:`productPage``productPageWithKicker``judgingNetworkGroup``resourcesHub``resourceGroup``legalPage``standardGrid``pressQuality`
## E017 - 技术架构确认记录
- 日期2026-07-06
- 关联任务T035
- 类型:用户确认 + 文档证据
- 用户确认:
```text
Next+ React ,数据库使用 PostGreSQL
```
- 结果摘要:
- 正式开发技术架构已收敛为 Next.js + React + PostgreSQL。
- 文档默认 ORM 为 Prisma。
- 表单提交目标改为 Next.js API/Server Actions 写入 PostgreSQL。
- 生产启动方式改为 `npm run start -- -p 8082`
- 新增任务 T036-T039覆盖工程初始化、数据库接入、表单入库、Next.js 部署。
## E018 - 服务器 PostgreSQL 安装用户确认
- 日期2026-07-06
- 关联任务T040
- 类型:用户确认
- 用户确认摘要:
- PostgreSQL 已在云服务器安装完成。
- 服务器:`170.106.192.152`
- SSH 用户:`ubuntu`
- 可用端口:`8080-8099`
- 应用目标端口:`8082`
- 凭据处理:
- 用户在对话中提供了 SSH 密码。
- 长期文档不保存明文密码。
- 待验证命令:
```bash
ssh ubuntu@170.106.192.152
systemctl status postgresql --no-pager
psql --version
sudo -u postgres psql -c '\l'
```
- 结论:
- 当前状态为“用户确认已安装,待远程验证”。
## E019 - 邮箱系统账号需求用户确认
- 日期2026-07-06
- 关联任务T041、T042、T043
- 类型:用户确认
- 用户确认摘要:
- 普通用户入口:`https://mail.paprikalaw.com/webmail/`
- 管理员入口:`https://mail.paprikalaw.com/admin/`
- 管理员账号:`admin@paprikalaw.com`
- 普通邮箱账号:
- `info@paprikalaw.com`
- `contact@paprikalaw.com`
- `support@paprikalaw.com`
- `sales@paprikalaw.com`
- `marketing@paprikalaw.com`
- `hr@paprikalaw.com`
- `finance@paprikalaw.com`
- `billing@paprikalaw.com`
- `legal@paprikalaw.com`
- `service@paprikalaw.com`
- 凭据处理:
- 用户在对话中提供了统一初始密码。
- 长期文档不保存明文密码。
- 待验证:
- 管理员登录。
- 普通邮箱 webmail 登录。
- MX/SPF/DKIM/DMARC。
- 内部互发、外部收信、外部发信。
- Next.js 表单通知 SMTP 配置。
## E022 - Next.js 工程初始化和构建证据
- 日期2026-07-06
- 关联任务T036、T022
- 类型:命令输出摘要
- 命令:
```bash
npm install
npm run generate:legacy
npm run lint
npx tsc --noEmit
npm run build
```
- 结果摘要:
- 依赖安装完成。
- `generate:legacy``doc/index.html` 生成 12 个页面数据。
- `lint` 通过。
- TypeScript `--noEmit` 通过。
- `next build` 通过,生成 `/``/[page]``/api/submissions``/ask``/start``/thanks``/robots.txt``/sitemap.xml`
## E023 - Prisma schema 和 migration 证据
- 日期2026-07-06
- 关联任务T037
- 类型:文件和命令证据
- 文件:
- `prisma/schema.prisma`
- `prisma/migrations/20260706120000_init/migration.sql`
- `.env.example`
- 命令:
```bash
npm run prisma:generate
npx prisma migrate diff --from-empty --to-schema-datamodel prisma/schema.prisma --script
```
- 结果摘要:
- Prisma Client 生成成功。
- schema 包含 `Submission``SubmissionFile`
- `.env.example` 只保留占位 `DATABASE_URL`,未写入真实密码。
- 待补:
- 生产 PostgreSQL 执行 `npx prisma migrate deploy`
- 成功入库后记录 `submission_id`
## E024 - Services CTA 和 legal 日期修复证据
- 日期2026-07-06
- 关联任务T017、T029
- 类型:源码和 HTTP 产物证据
- 命令:
```bash
rg -n "contact|Last updated" doc/index.html
curl -s http://localhost:8082/services | rg -n 'href="/start"|Expert consultation|Attorney Referral'
curl -s http://localhost:8082/terms | rg -n "Last updated: July 6, 2026"
```
- 结果摘要:
- `doc/index.html` 中 Services 04/05/06 已不再指向不存在的 `contact`
- `/services` 产物中 Expert consultation、Agency representation、Attorney Referral 的 CTA 均为 `href="/start"`
- legal 页面显示 `Last updated: July 6, 2026`
## E025 - 本地生产 8082 路由冒烟证据
- 日期2026-07-06
- 关联任务T003-T011、T015、T020、T039
- 类型HTTP 状态码证据
- 命令:
```bash
npm run start -- -p 8082
for p in / /press /judging /scholarly /services /why /resources /faq /start /ask /thanks /terms /privacy /disclaimer /robots.txt /sitemap.xml; do
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:8082$p)
printf '%s %s\n' "$code" "$p"
done
```
- 结果摘要:
- 所有列出的路径均返回 `200`
- `robots.txt` 输出 `Sitemap: https://paprikalaw.com/sitemap.xml`
- `sitemap.xml` 包含核心公开路径。
- 备注:
- 这是本地生产服务验收,不等同于远程服务器和 HTTPS 验收。
## E026 - 表单 API 校验证据
- 日期2026-07-06
- 关联任务T012、T013、T038、T044
- 类型API 响应证据
- 命令:
```bash
curl -s -X POST http://localhost:8082/api/submissions \
-H 'Content-Type: application/json' \
-d '{"type":"ask","name":"","email":"bad","question":"","consentAccepted":false}'
```
- 结果摘要:
- 返回 `Validation failed`
- 字段错误包含 `name``email``consentAccepted``question`
- 待补:
- 配置 `DATABASE_URL` 后提交有效 `/start``/ask` 表单,记录 `submission_id` 和数据库查询结果。
- 未配置 `DATABASE_URL` 时,有效提交会返回 500不能作为入库成功证据。
## E027 - npm audit 结果
- 日期2026-07-06
- 关联任务T023、T036
- 类型:安全扫描摘要
- 命令:
```bash
npm audit --audit-level=moderate --json
```
- 结果摘要:
- 剩余 2 个 `moderate` vulnerability。
- 来源为 `next` 依赖链中的 `postcss <8.5.10` 公告。
- 当前项目已使用 registry 当前稳定版本 Next `16.2.10`
- npm 给出的 `fixAvailable` 会降级到 `next@9.3.3`,属于不合理破坏性修复,未执行。
- 后续:
- 等待 Next/PostCSS 依赖链发布可用修复版本后升级。
## 待补证据清单
| 编号 | 关联任务 | 需要补充的证据 |
| --- | --- | --- |
| E008 | T003-T011 | 桌面浏览器截图,覆盖 home、press、judging、scholarly、services、why、resources、faq |
| E009 | T018 | 移动端截图,至少 390px 宽度覆盖 home、services、faq、start |
| E010 | T012/T013 | 表单提交成功记录、submission id、后台记录截图或 webhook job_id |
| E011 | T016/T017 | 法务审核记录、确认人、确认日期 |
| E012 | T023 | 自动化测试命令和结果 |
| E013 | T024/T025 | 部署 URL、systemd 状态、Nginx 测试、证书、job_id、commit hash |
| E014 | T026 | PDF 或截图归档路径 |
| E015 | T030 | SSH key 或密钥管理方式确认,不能包含明文密码 |
| E020 | T041/T042 | 邮箱后台截图、DNS 查询输出、收发测试截图 |
| E021 | T043 | 表单通知邮件 message id 或测试截图 |
| E028 | T044 | PostgreSQL `DATABASE_URL` 配置后的真实表单提交、`submission_id` 和数据库查询结果 |
## 证据模板
### EXXX - 标题
- 日期:
- 关联任务:
- 类型:
- 命令或页面:
- 结果:
- 文件或链接:
- job_id/report_id/submission_id
- 备注:

195
working/06-决策记录.md Normal file
View File

@@ -0,0 +1,195 @@
# 06-决策记录
本文件记录已经做出的工程、产品和流程决策。后续若要改变决策,需要新增记录说明原因,不直接覆盖旧记录。
## D001 - 当前基线按静态单文件 mockup 记录
- 日期2026-07-06
- 决策:当前以 `doc/index.html` 作为项目基线,而不是假设已有完整前端工程。
- 原因:
- 当前目录未发现 `package.json`、构建配置或后端代码。
- `doc/index.html` 已包含页面、样式和路由脚本。
- 用户本轮要求是在 `working` 中编写推进文档。
- 影响:
- 当前验收以静态页面可打开、路由可渲染、视觉可检查为主。
- 表单、部署、测试、工程化都作为后续任务单独推进。
## D002 - 表单不按真实功能验收
- 日期2026-07-06
- 决策:`start``ask` 当前只能记为表单视觉 mockup不能记为真实线索收集功能。
- 原因:
- 字段是 `<div>`,不是输入控件。
- 提交按钮是跳转链接,不会提交数据。
- 没有后端、第三方表单服务、文件上传或校验逻辑。
- 影响:
- `T012``T013``T014` 保持 `TODO`
- 上线前必须实现真实提交,或在页面明确标注不可提交。
## D003 - 任务推进以任务矩阵为唯一入口
- 日期2026-07-06
- 决策:后续开发先查 `04-任务矩阵.md`,每个任务必须有编号、状态、验收标准和依赖。
- 原因:
- 用户明确要求防止重复做。
- 当前项目范围包含内容、前端、表单、法务、部署和证据留存,容易混线。
- 影响:
- 新工作不得只写在聊天记录里。
- 完成任务必须同步推进台账和验收证据。
## D004 - 法务内容按草稿处理
- 日期2026-07-06
- 决策Terms、Privacy、Disclaimer 当前按草稿记录,正式上线前需要法务或负责人确认。
- 原因:
- 页面涉及 non-law-firm statement、attorney-client relationship、refund、privacy、file upload、attorney referral、immigration outcome guarantee 等高风险内容。
- 页面曾使用 `Last updated: July 10, 2026`,晚于当前日期 `2026-07-06`R007 已修正日期,但内容仍需审核。
- 影响:
- `T016` 标为 `BLOCKED`,等待法务/负责人输入。
- `T017` 后续已完成日期修正;法务内容审核仍不关闭。
## D005 - 本轮不改 doc/index.html
- 日期2026-07-06
- 决策R002 只补强 `working` 文档,不修改 `doc/index.html`
- 原因:
- 用户要求是在 `working` 中编写文档体系。
- 当前还没有要求开始代码修复或工程化迁移。
- 影响:
- `T029` 虽已识别,但仍为 `TODO`
- 后续修复死链时应单独开一轮并记录证据。
## D006 - 长期文档不保存明文服务器密码
- 日期2026-07-06
- 决策:用户提供的 SSH 密码只视为临时交接信息,不写入 `working` 长期文档。
- 原因:
- `working` 文档会作为后续推进资料,可能被复制、上传或纳入版本控制。
- 明文密码进入项目文件会扩大泄露面。
- 影响:
- 服务器信息保留 host、user、port、domain。
- `T030` 要求改用 SSH key、部署密钥或受控密钥库。
## D007 - 目标部署端口采用 8082
- 日期2026-07-06
- 决策:部署方案默认内部服务监听 `8082`HTTPS 由域名和反向代理对外提供。
- 原因:
- 用户明确给出端口号 `8082`
- 用户说明 `8080-8099` 已开放。
- HTTPS 通常由 Nginx/Caddy/负载均衡终止,再转发到本地应用端口。
- 影响:
- `T024` 验收服务器本地 `127.0.0.1:8082`
- `T025` 验收 `https://paprikalaw.com/`
## D008 - Services 的 contact 路由判定为缺陷
- 日期2026-07-06
- 决策Services 页面 04/05/06 CTA 指向 `contact` 是缺陷,不按设计接受。
- 原因:
- `pageData` 没有 `contact` 页面。
- 当前路由逻辑会把未知页面回退到 `home`
- CTA 文案是 Start Here合理目标应为 `start`,除非新增真实 Contact 页面。
- 影响:
- 新增 `T029`
- 修复前 `T006` 只能保持 `VERIFY`,不能标为 `DONE`
## D009 - Outlet/network 名称正式发布前需确认
- 日期2026-07-06
- 决策Press 和 Judging 页面中的 outlet/network 名称上线前必须由负责人确认,或改成示例类型表达。
- 原因:
- 页面列出大量媒体和 judging/network 名称,可能被理解为合作关系、可保证 placement 或背书。
- 这类陈述涉及商业真实性和法律风险。
- 影响:
- 新增 `T031`,状态 `BLOCKED`
- 未确认前不建议直接正式上线。
## D010 - 完全对标必须包含视觉和源码结构
- 日期2026-07-06
- 决策:后续提到“完全对标 `doc/index.html`”时,不能只记录页面文案,还必须覆盖 CSS token、组件类、函数清单、CTA 映射、FAQ 编号、Legal section、未调用模块和源码缺陷。
- 原因:
- `doc/index.html` 是 3423 行单文件 mockup页面内容、视觉规范和路由逻辑耦合在一起。
- 如果只记录页面功能,工程化拆分时容易丢失视觉细节或保留隐藏缺陷。
- 影响:
- `01-项目功能内容.md` 增加源码级对标附录。
- `02-项目程序开发详细步骤.md` 增加完全对标核验清单。
- 新增 `T034``E016`
## D011 - 正式开发架构采用 Next.js + React + PostgreSQL
- 日期2026-07-06
- 决策:正式开发技术架构采用 Next.js + React数据库使用 PostgreSQL。
- 默认补充决策:
- 使用 TypeScript。
- 使用 Next.js App Router。
- 使用 Prisma 作为 PostgreSQL ORM。
- 使用 Next.js Route Handlers 或 Server Actions 处理表单提交。
- 生产服务监听 `8082`HTTPS 由反向代理处理。
- 原因:
- 用户明确指定 `Next+ React``PostGreSQL`
- 项目需要真实表单、数据持久化、后续可能的文件上传和后台线索管理,单纯静态站不够。
- Next.js 可同时承接页面、API、SEO、构建和生产服务。
- 影响:
- `02-项目程序开发详细步骤.md` 中工程化路线改为 Next.js + PostgreSQL。
- 新增 `T035-T039`
- 后续不再优先推进 Vite/纯静态拆分,除非另有新决策。
## D012 - PostgreSQL 已安装但仍需远程验证
- 日期2026-07-06
- 决策:将服务器 PostgreSQL 状态记录为“用户确认已安装,待远程验证”,不直接视为已完成数据库接入。
- 原因:
- 用户已明确说明 PostgreSQL 在服务器安装完成。
- 但尚未执行服务状态、版本、数据库列表、应用数据库、应用用户和连接字符串验证。
- 影响:
- 新增 `T040` 远程验证 PostgreSQL 安装。
- `T037` PostgreSQL + Prisma 接入仍保持 `TODO`
- 继续执行不保存明文密码的凭据规则。
## D013 - 邮箱账号统一记录但不保存初始密码
- 日期2026-07-06
- 决策:记录 `paprikalaw.com` 邮箱入口、管理员账号和 10 个普通邮箱账号,但不在长期文档保存用户提供的统一初始密码。
- 原因:
- 邮箱账号属于项目基础设施,网站联系、表单通知和法务联系需要引用。
- 多账号共用初始密码存在安全风险,进入文档会扩大泄露面。
- 影响:
- 新增 `T041-T043`
- 创建或确认邮箱后,应尽快改为独立强密码并记录验证证据。
- Next.js SMTP 凭据只能放服务器环境变量或受控密钥库。
## D014 - 保留 doc/index.html 作为对标基线,同时以 Next.js 为正式工程
- 日期2026-07-06
- 决策:不删除 `doc/index.html`,正式代码在根目录 Next.js 工程中维护。
- 原因:
- `doc/index.html` 是既有视觉、文案和路由逻辑的完整基线。
- 新工程需要持续对标旧 mockup避免内容迁移漂移。
- `scripts/extract-legacy-pages.mjs` 可从旧基线重新生成静态页面 CSS 和 HTML 数据。
- 影响:
- 静态内容页面暂由 `lib/legacy-pages.ts``LegacyPage` 渲染。
- `/start``/ask``/thanks` 已由 React 页面接管,不再使用旧 mockup 表单。
## D015 - 文件上传暂不启用
- 日期2026-07-06
- 决策Start/Ask 表单当前不启用 CV/resume 上传。
- 原因:
- 文件上传涉及存储位置、大小限制、访问权限、隐私保留期限和删除策略。
- 未确认前收集 CV 会扩大敏感数据风险。
- 影响:
- `SubmissionFile` 表作为未来能力预留。
- 前端表单明确说明 file upload 暂停,直到存储和隐私方案获批。
## D016 - 表单入库任务不在缺少 DATABASE_URL 时关闭
- 日期2026-07-06
- 决策:虽然 `/api/submissions` 已实现 Prisma 写入代码,但在没有真实 PostgreSQL `DATABASE_URL` 和成功 `submission_id` 证据前T038/T044 不标为 `DONE`
- 原因:
- API 代码通过构建不等于数据已经成功持久化。
- 生产数据库连接、migration、权限和网络策略都可能导致运行时失败。
- 影响:
- T012/T013/T038 保持 `VERIFY`
- 新增 T044专门记录真实 PostgreSQL 入库验收。

54
working/README.md Normal file
View File

@@ -0,0 +1,54 @@
# MeetPaprika Working Index
本目录用于承接 Paprika 网站后续开发推进。所有后续工作优先从 `04-任务矩阵.md` 领取任务,并在完成后同步更新 `03-推进台账.md``05-验收证据.md` 和必要的 `06-决策记录.md`
## 文档索引
1. [01-项目功能内容.md](01-项目功能内容.md)
对标 `/home/mes123456/MeetPaprika/doc/index.html`,说明当前 Paprika 静态网站 mockup 的页面、功能、内容模块、交互方式、视觉规范、CSS 类、JS 函数、CTA 映射、表单边界、服务器目标和已知风险。
2. [02-项目程序开发详细步骤.md](02-项目程序开发详细步骤.md)
说明从当前单文件 mockup 到可维护、可上线版本的开发步骤包括静态修复、真实表单、工程化拆分、8082 部署、HTTPS、回归验收和完全对标核验命令。
3. [03-推进台账.md](03-推进台账.md)
记录每轮做了什么、改了哪些文件、验证了什么、下一步是什么。
4. [04-任务矩阵.md](04-任务矩阵.md)
统一维护任务编号、状态、验收标准和依赖,防止重复做同一件事。
5. [05-验收证据.md](05-验收证据.md)
记录命令、页面、截图、PDF、job_id、report_id、submission_id、部署链接等证据。
6. [06-决策记录.md](06-决策记录.md)
记录关键设计、工程、部署和安全决策,后续变更时先查这里,避免反复争论同一个问题。
## 当前基线
- 参考文件:`/home/mes123456/MeetPaprika/doc/index.html`
- 当前形态:已新增 Next.js + React 工程;`doc/index.html` 保留为静态 mockup 对标基线。
- 静态基线:`doc/index.html` 仍通过查询参数 `?page=...` 渲染不同页面。
- Next 路由:`/``/press``/judging``/scholarly``/services``/why``/resources``/faq``/start``/ask``/thanks``/terms``/privacy``/disclaimer`
- 当前表单:`/start``/ask` 已改为真实 React 表单,提交到 `POST /api/submissions`;成功入库依赖生产 `DATABASE_URL`
- 数据访问层Prisma ORM已存在 `prisma/schema.prisma` 和初始化 migration。
- 目标域名:`https://paprikalaw.com/`
- 目标服务器:`ubuntu@170.106.192.152`
- 目标端口:`8082`
- 邮箱普通入口:`https://mail.paprikalaw.com/webmail/`
- 邮箱管理员入口:`https://mail.paprikalaw.com/admin/`
- 凭据规则:长期文档不保存明文密码,后续部署改用 SSH key 或受控密钥交接。
## 下一步建议
1. 配置生产 `.env``DATABASE_URL`,在服务器 PostgreSQL 上执行 Prisma migration。
2. 远程验证 `170.106.192.152` 的 PostgreSQL、应用数据库和应用用户。
3. 将 Next.js 应用部署到服务器 `8082`,再配置 HTTPS 反向代理到 `https://paprikalaw.com/`
4. 登录邮件后台确认邮箱、DNS 和收发,再接入表单通知。
5. 完成法务文本、outlet/network 名称、文件上传策略和截图验收。
## 工作规则
1. 开始前查 `04-任务矩阵.md`
2. 完成后写 `03-推进台账.md`
3. 有命令、截图、部署、表单提交、PDF、job_id、report_id 时写 `05-验收证据.md`
4. 有路线、合规、安全、部署、架构取舍时写 `06-决策记录.md`
5. 不把 SSH 密码、API key、表单 webhook secret、CRM token 写入本目录。