ai-startup-skills
AI 제품을 만드는 스타트업을 위한 7단계 에이전트 스킬: 검증 → 비즈플랜 → 정책 → 로드쇼 → 고객 확보 → 가격 책정 → 등록
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Personal portfolio website built with Next.js 16 App Router and deployed to sumitgautam.tech.README 전문 보기 ↗Personal portfolio website built with Next.js 16 App Router and deployed to sumitgautam.tech.
git clone https://github.com/SumitGA/SumitGautam.tech.git
cd SumitGautam.tech
npm install
cp .env.local.example .env.local
Edit .env.local and fill in:
| Variable | Where to get it |
|---|---|
NEXT_PUBLIC_SUPABASE_URL | Supabase Dashboard → Settings → API |
NEXT_PUBLIC_SUPABASE_ANON_KEY | Supabase Dashboard → Settings → API |
SUPABASE_SERVICE_ROLE_KEY | Supabase Dashboard → Settings → API (server-only) |
RESEND_API_KEY | resend.com/api-keys |
GEMINI_API_KEY | aistudio.google.com/apikey — free, no card |
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME | Cloudinary Dashboard — not a secret, it appears in every image URL |
REVALIDATE_SECRET | Any random string (openssl rand -hex 32); must match the admin project |
Every integration degrades on its own. Without Supabase the site runs on the static fallback data in src/portfolio.js. Without a Resend key the contact form returns a 503, without a Gemini key the chat widget does the same, and without a Cloudinary cloud name posts simply render without images. Nothing takes the rest of the site down with it.
npm run dev
# http://localhost:3000
Edit src/portfolio.js — all portfolio sections are plain JS objects.
supabase/ in the SQL editor, in the order listed under Supabase migrationsThe admin panel lives in admin/ and is a separate Next.js 15 project deployed at admin.sumitgautam.tech.
cd admin
cp .env.local.example .env.local # fill in Supabase, Cloudinary and revalidate vars
npm install
npm run dev # http://localhost:3001
Admin sections: Greeting · Skills · Experience · Education · Certifications · Projects · Blog · Contact · Resume/CV · Analytics · Settings
Login uses Supabase Auth — create a user in the Supabase dashboard under Authentication → Users and sign in with that email and password. There is no separate admin password to configure.
Saving anything in the admin pushes a cache purge to the portfolio, so edits appear on the live site immediately rather than waiting for a revalidation window. That needs REVALIDATE_SECRET set to the same value in both projects; without it saves still work and the site falls back to an hourly refresh.
The /resume route renders an A4-style CV card. Hit Download PDF to print it through the browser.
Content is editable in the admin panel under "Resume / CV" and stored in separate Supabase tables (resume_header, resume_summary, resume_skills, resume_jobs, resume_education_entries, resume_certifications, resume_references).
The contact form on /contact posts to POST /api/contact, which sends an email via Resend to the portfolio owner. Reply-To is set to the sender's address.
By default emails come from Resend's [email protected] test address. To send from your own domain (e.g. [email protected]), verify the domain in the Resend dashboard and set RESEND_FROM_EMAIL in your env vars.
A floating chat widget on the home page answers visitor questions about your background.
The system prompt is assembled from your live Supabase data — resume summary, skills, every job with its bullets, education, certifications and projects — so answers stay accurate and update automatically whenever you edit content in the admin panel. It's instructed to answer only from that data and to point visitors at the contact form for anything it doesn't know.
Rate limiting is enforced per IP by a Postgres function (supabase/chat_rate_limit.sql), defaulting to 15 messages per hour. Tune it with CHAT_RATE_LIMIT and CHAT_RATE_WINDOW_SECONDS — no code change needed.
Model note: the default is gemini-3.1-flash-lite. Google's full Flash models are capped at 20 requests/day on the free tier, which a public site exhausts almost immediately — Flash-Lite has a much higher allowance. If you change GEMINI_MODEL, test the streaming path specifically: some models serve generateContent fine but stall on generateContentStream.
Posts live at /blog, are written in markdown through the admin panel, and are stored in Supabase.
Markdown, not HTML. Content is stored as raw markdown in a text column, so select slug, content from posts gives you a directory of .md files if you ever move off this stack. Storing rendered HTML is how blogs become unmigratable.
Syntax highlighting is server-side. Shiki runs at build time and emits both light and dark themes as CSS variables, so code blocks follow the theme toggle with no client-side highlighter and no flash of unstyled code. Adding callouts or diagrams later is a plugin in lib/markdown.js — existing posts start rendering better with no migration.
Images go to Cloudinary, uploaded straight from the browser using a signature minted server-side, so the API secret never reaches the client. Cover images store a public_id rather than a URL, so changing cloud accounts or transformations is an environment-variable edit rather than a rewrite of every post. Deleting a post also deletes its images, unless another post is using them.
Renaming a published post keeps the old URL working. Every slug a post has ever had is recorded by a database trigger, and old addresses redirect permanently to the current one.
Drafts and scheduling come from Row Level Security rather than application code: the public policy only exposes posts that are published and whose date has arrived, so dating a post in the future publishes it on its own.
Tags get real pages at /blog/tag/<tag>, there is an RSS feed at /blog/rss.xml, and posts and tags are added to the sitemap automatically — publishing needs no deploy.
Self-hosted in your own Supabase, with no third-party service and no cookies. The admin panel has a dashboard covering visitors, sessions, pageviews, bounce rate, new versus returning, a traffic chart, top pages, referrers, countries and devices, conversion rates, and weekly retention cohorts.
Nothing identifying is stored. The IP address is used to rate limit and never written; the user agent is reduced to a device class and browser name and discarded. Visitor and session identifiers are random UUIDs minted in the browser — not derived from the visitor, not cookies, not shared across sites. Referrers are truncated to the host.
Ingest goes through POST /api/analytics using the service role. The events table has no INSERT policy on purpose: the anon key is public, so if it could write, anyone could forge events. The endpoint returns 204 on every failure path, including a missing table — analytics breaking should cost you data, not a working site.
Conversions tracked: contact form submissions, resume downloads, chat opens and messages, outbound link clicks and case study views.
Because the identifier lives in localStorage, a strict reading of ePrivacy would call for consent in the EU. See the Analytics section of CLAUDE.md for the reasoning and the no-storage alternative.
. (default)NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, RESEND_API_KEY, GEMINI_API_KEY, NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, REVALIDATE_SECRETmain-branch (not main)www redirecting to it — every canonical URL and sitemap entry uses the apex, so the reverse would give search engines conflicting signalsNEXT_PUBLIC_ variables must be added as Config, not Secret. Vercel blocks the combination, correctly: the prefix inlines the value into browser JavaScript, so it cannot also be secret. They are public by design — the Supabase anon key is protected by Row Level Security, not by being hidden. They are also inlined at build time, so redeploy after adding one.
admin/main-branchNEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, REVALIDATE_SECRET, NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRETadmin.yourdomain.comCLOUDINARY_API_SECRET must be a Secret and must never carry the NEXT_PUBLIC_ prefix. Login is Supabase Auth — create a user under Authentication → Users; there is no admin password variable.
Run SQL files in Supabase SQL Editor (Dashboard → SQL Editor), in order. All are idempotent and safe to re-run:
supabase/schema.sql — portfolio tablessupabase/resume_schema.sql — resume tablessupabase/project_case_studies.sql — case study columns on projectssupabase/chat_rate_limit.sql — chat rate limitingsupabase/analytics_schema.sql — analytics events and aggregation functionssupabase/rate_limit.sql — shared rate limiter used by the contact formsupabase/blog_schema.sql — posts, slug history and image ownershipsupabase/blog_series.sql — series ordering and project links on postsIf RLS policies fail mid-run, supabase/resume_patch.sql is an idempotent patch that is safe to re-run.
Based on ashutosh1919/masterPortfolio and saadpasta/developerFolio. Illustrations: undraw.co