尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Build a client's homepage live: pick, reorder and customise section layouts with an instant preview, optionally pre-filled from their current site with AI.
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Next.js 15 (App Router) + Tailwind CSS v4 recreation of the "Your homepage, as we'd build it" card.
npm run dev # http://localhost:3000
npm run build
Two features need keys — reading a client's existing site, and designing the page with AI. Copy .env.example to .env.local (git-ignored) and fill them in:
OPENAI_API_KEY=sk-proj-...
DEEPSEEK_API_KEY=sk-...
Without them every other feature works; each one reports that its key is missing and the previews stay on placeholder copy. Both calls are server-side route handlers, so neither key reaches the browser.
Satoshi carries the interface and body copy, Recoleta the headings. Both are design tokens — --font-sans and --font-display in globals.css — so either can be retargeted in one place, and font-sans/font-display work as Tailwind utilities.
Satoshi is free under the ITF Free Font Licence and is committed in src/app/fonts/satoshi/ (400/500/700/900), loaded through next/font/local so it's hashed, preloaded and prefixed correctly for the Pages sub-path.
Recoleta is a licensed Latinotype font, so its files can't be committed. Its @font-face rules are already in place: buy a webfont licence and drop the woff2s into public/fonts/recoleta/ (see the README there for the filenames) and headings pick it up with no code change. Until then they render in the serif fallback stack.
.github/workflows/pages.yml builds a static export on every push to main and publishes it to https://ivelnaj.github.io/homepage-section-picker/. Enable it once under Settings → Pages → Source → GitHub Actions.
Pages has no server, so that build drops src/app/api and hides the Read my site button — the picker, the previews and the live page all work, but the analyser doesn't. next.config.mjs only switches to output: 'export' when NEXT_PUBLIC_STATIC_BUILD=true, which nothing but the workflow sets, so local development is unaffected.
Never move the model call into the browser to work around this: the API key would ship inside the JavaScript bundle. A full deployment needs a host that runs Node (Vercel's free tier fits, with the key as an encrypted environment variable).
Live page preview on the left. The whole homepage is assembled from the current order and layout choices, so every pick on the right updates it immediately. The section being edited is ringed and scrolled into view; hovering any section names it. Each section gets a ratio roughly matching its real height (a header is 16:5, a full content block 16:10) so the stack reads like a page. It sticks to the top of the viewport on wide screens and drops above the card on narrow ones.
The finished page opens in its own window. Build my homepage — and New window in the preview's chrome — open the assembled page on /preview, sized to the screen and running the full width of it, so it reads as the site rather than as a preview of one. It follows along: every change republishes, and the window re-renders. See Two windows.
Reads the client's existing site. The card asks for their current address up front — no yes/no question first, and blank means building from scratch. Enter it and Read my site pulls their brand colour, logo, navigation, hero copy and hero image into the live preview — their own content, in the layouts we propose. The contrast is the pitch, so copy is used verbatim and never rewritten. Anything we can't read falls back to the wireframe, section by section.
Every section carries editable content. Opening a row gives Layout on the left and Content on the right, both at once: layout arranges the section, content fills it. Side by side rather than a tab each, because the option cards preview the section’s real content — behind tabs, the thing you were checking was a tab away from the thing you were changing. Each section declares its own fields — headings, body copy, button labels, list items, images — pre-filled with placeholder copy and locally generated placeholder artwork, so a page looks finished before anyone types anything. Images take a URL or a local file, and every section has a reset that restores its placeholders. A field that only one design draws is only offered when that design is selected, so the editor never promises something the preview won’t show.
Lists the suggested sections; required ones show a badge instead of buttons.
Drag to reorder (native HTML5 drag events, no library). A blue line shows where it will land. Two flags govern a row: required hides its Swap/Remove buttons in favour of a badge, and pinned makes it an anchor that never moves and that nothing can be dropped onto — so the header and hero stay first and the footer stays last. The contact form is required but not pinned: it can't be removed, but it can still be dragged anywhere.
| Path | Purpose |
|---|---|
| src/lib/sections.ts | The approved sections, their option groups, and the suggested default order |
| src/components/SectionPicker.tsx | Two-column shell, state, swap/add/remove logic |
| src/components/PagePreview.tsx | The assembled page on the left, in browser chrome |
| src/components/PageBody.tsx | The page behaving like a website — shared by the full-screen view and the second window |
| src/app/preview/page.tsx | The page on its own, for a second window or screen |
| src/lib/share.ts | Hands the page between windows through localStorage |
| src/components/SectionRow.tsx | One row: icon, label, badge or buttons |
| src/components/SectionMenu.tsx | Overlay list used by both swap and add |
| src/components/OptionPicker.tsx | Inline radiogroups — preview cards and chip rows — for a section's option axes |
Colors live entirely in the @theme block — --color-row, --color-go, etc. — so restyling means editing that one block.
The editing view keeps the shape it always had — preview left, picker right — and the finished page lives in a second window at /preview. Build my homepage opens it; so does New window in the preview's chrome. Both reuse one named window, so clicking again brings the existing one forward rather than opening a second.
That window runs the page at the full width of the screen, where the in-app view caps it at a page-width column (PageBody's width prop). Everything inside a section is sized in cqw, so the wider column scales the whole page rather than reflowing it.
Pop-up blockers are common enough that Build my homepage checks: if the window is refused, it falls back to the full-screen in-app view, so the button always does something.
The picker's state lives in React, which another window can't reach, so the editing window publishes the page to localStorage on every change and the second window reads it and listens for storage events. That event fires in every other document on the origin, which is the direction needed here: the window doing the editing doesn't need telling what it just did. Publishing happens on every change rather than only while a window is open, so one opened later starts on the current state instead of an empty page.
Both the window and the in-app fallback render through the same PageBody, so they can't drift apart. It also carries the page-view class the behaving-like-a-website rules in globals.css hang off — hover states, the nav underline, keeping dropdowns shut until they're pointed at — so neither caller can render the page without them.
The one failure worth knowing about: uploaded images are stored as data URIs, and a few large ones will exhaust the storage quota. share.ts reports that rather than throwing, and the picker says so under the preview — otherwise a second window would sit frozen on old content with nothing to explain why.
The previews are written as documents, not as drawings. Each section returns a real landmark — <header>, <section>, <footer> — with a heading outline under it: the hero carries the page's one <h1>, section headings are <h2>, and card and column titles are <h3>. Card grids, tick lists, nav, footer columns and pricing features are <ul>/<li>; blog cards are <article> with a <time>; testimonials are <figure>/<blockquote>/<cite>; the FAQ is <details>/<summary>, so it actually opens rather than miming it; forms are <form> with <label>-attached inputs; buttons are <button> and links are <a>.
Two rules keep that from causing trouble:
A wireframe bar is not content. Where a field has no words yet the preview draws a grey bar. Those are aria-hidden, because a heading with no text or a button with no name is a drawing of one — there is nothing to announce.
A picture of a page is not a page. The same components render at 64px in a row thumbnail and across the picker cards, where a dozen <h1>s and a tab stop for every button would wreck the editing screen. PreviewFrame takes a decorative prop that sets inert and aria-hidden on the whole subtree, and the thumbnail, menu, card and sidebar callers all pass it. Only PageBody — the assembled page — renders without it.
The effect, measured in the browser: the editor page exposes no stray headings at all (59 headings and buttons sit inside inert subtrees) and only its own 50 controls are reachable by keyboard, while the page itself comes out with one h1, ten h2s, thirteen h3s, header/nav/section/footer landmarks, 13 lists, 3 articles, 3 blockquotes and 6 labelled inputs.
Preflight strips the browser's default heading and list styling, so the tags are structural only — changing a heading level never changes how a preview looks. Links point at a placeholder #; PageBody swallows the click so the page doesn't jump.
public/design-sets/ holds the artwork exported from Figma, keeping the names it exported them with — section-cogs/hero/Type=Hero V1.svg and so on. Thirteen sections are wired to their sets, and each lists the drawn designs on a design axis rather than the combinable axes the rest still use, because a design set is a fixed set and listing it verbatim keeps the picker and the design file describing the same thing:
prefers-reduced-motion. Background slides in the content tab takes as many pictures as you want to give it, and only appears when one of those two is selected.Style= rather than Type=, so that prefix differs on purpose.prefers-reduced-motion. V3 is three rows standing still.Style= rather than Type= like the content section. Three frames: the tall footer with link columns, a strip built round the logo, and a bare line of type.section-cogs/contact form/Type=Contact Forms V1.svg. All six are the same five-field form; what varies is what holds it and which side it sits on.Alongside the section artwork, design-sets/font-style/Font Style.svg is the type scale, and it's transcribed into @layer base in globals.css: h1 and h2 take the display serif, everything from h3 down is the sans, and the sizes step at 768, 1024 and 1280 for the four widths the sheet names. The sheet says nothing about line height, so the two values there are ours rather than its; it also draws a "blog" column and an uppercase serif footnote for a context this app doesn't have yet, and neither is carried.
Each design appears twice over, and the two are not the same picture:
SectionPreview's screenshot prop is what separates the two, so a caller picks a side rather than the artwork leaking onto the page.
Each set is variations on a handful of frames, so every rebuilt preview describes its designs in a SPECS table and renders from that — the heroes over three frames (copy beside media, copy above media, copy over a full-width image), the cards over one grid, the content sections over four, the testimonials over three (a grid of quote cards, one quote paged by arrows, a portrait beside each quote), the logo strips over one row of six. A newly exported design is usually one row in that table, one entry in previewImages.ts, one option in the section's groups, and its artboard ratio in aspect.ts so the page stands it at the height it was drawn at. Those ratios are written out as whole class names rather than built from the numbers: Tailwind only generates an arbitrary value it can read literally in the source, so an aspect-[1440/${n}] would name a class that never exists.
Two details the artwork settles, and the wireframes follow: the card and column links are drawn in the same warm ink as the labels (#563F3D), not green — the set keeps its greens for buttons, ticks and numerals; and a square-cornered image needs rounded-none!, because ImageBlock brings its own rounded-xs and Tailwind emits the radius utilities alphabetically, so plain rounded-none lands earlier in the stylesheet and loses whatever the class order says.
Paths into public/ go through asset.ts rather than being written inline. Every image here renders through a plain <img> (see parts.tsx), so nothing else would apply the Pages basePath, and the Figma names need percent-encoding — Next serves Type%3DHero%20V1.svg but 404s on Type=Hero%20V1.svg, which encodeURI alone would leave as-is.
A section's Choice says how it is arranged; its SectionContent says what it contains. Both are flat string maps, which keeps them easy to store, prefill and reset.
Three conventions run through every section:
· separates a pair inside one line — 200+ · Projects delivered for a figure and its label, Insights · 12 March for a category and date. One rule rather than a convention per section.Repeated blocks share a single body field rather than exposing one per card. Nine textareas for a nine-card grid would bury the editor, and at preview scale the repetition reads correctly as a layout.
Everything degrades field by field: clear a field and that element goes back to its wireframe bar while the rest of the section keeps its content.
A parser does the extraction and the model does the judging. extractPage.ts finds what a regex can find reliably — <title>, meta tags, icon links, <img> sources, every hex colour and its frequency, and the visible text with tags stripped. The model then answers only the questions that need judgement: which candidate colour is the brand colour, which heading is the hero, which image is the hero image, what the nav labels are.
The model call is confined to the route handler, so swapping providers touches one file — everything either side of it is provider-agnostic.
Brand colour reaches every section through CSS variables, not props. Each preview draws its greens from var(--brand, #3f6b30) and friends, and brandVariables() declares those on the shell once the site has been read — so all fourteen sections, the row thumbnails and the picker cards recolour together. The tints (--brand-band, --brand-figure, --brand-dim, --brand-soft) are mixed from the single colour the analyser is confident about, via color-mix(). With no site analysed the variables are absent and every fallback is the template's own palette, so nothing changes.
Two rules keep the output trustworthy:
<img> tag.Failures are not error states: an unreachable site, a missing key, or a field the model wasn't sure about all resolve to null, and the affected preview simply draws its wireframe. Only the large left-hand preview receives real content — at 64 px a row thumbnail would render real text as illegible mush, so those stay abstract.
Keyboard equivalent: focus a row's grip handle (Tab) and press ↑/↓. Note that HTML5 drag events don't fire on touchscreens; the grip keys are the fallback there.
Sections that declare options show a live thumbnail of the chosen layout in place of their icon, plus its name as a caption — so the current choice is visible without opening anything. Change expands the alternatives inline under that row (the list above and below stays put); the selected one is ringed and marked. Picking keeps the panel open so choices can be compared; Done collapses it.
Options are grouped into independent axes (OptionGroup). A group renders either as preview cards (display: 'cards') or a compact chip row (display: 'chips'), and every preview card renders its option combined with the section's other current choices. A group can declare appliesTo(choice) so axes the current layout doesn't use are hidden from both the picker and the caption — e.g. "Image on" disappears when the content section isn't a split layout. Sections configured so far:
| Section | Axes |
|---|---|
| Site header | Structure (4) × Nav position (3) × Nav band (3) × Button (3) |
| Hero | Design: the 23-strong V1…V28 set — see Design sets · Buttons (4) · Images (4) |
| Logo strip | Design: the 3-strong V1…V3 set — see Design sets |
| Content card | Design: the 22-strong V1…V22 set × Rows (3) — see Design sets |
| Content section | Design: the 20-strong V1…V23 set — see Design sets |
| Testimonials | Design: the 6-strong V1…V6 set — see Design sets |
| Team members | Design: the 3-strong V1…V3 set — see Design sets |
| FAQ accordion | Design: the 5-strong V1…V5 set — see Design sets |
| Stats | Design: the 4-strong V1…V4 set — see Design sets |
| Call to action | Design: the 10-strong V1…V10 set — see Design sets |
| Pricing | Design: the 6-strong V1…V6 set — see Design sets |
| Blogs | Design: the 6-strong V1…V6 set — see Design sets |
| Contact form | Design: the 6-strong V1…V6 set — see Design sets |
| Site footer | Design: the 8-strong V1…V8 set — see Design sets |
Swap opens a picker of every other optional section, each shown with its own preview thumbnail and caption. If the section picked isn't on the page, the row becomes it; if it already is, the two rows trade places (tagged as such in the menu) rather than duplicating.
Remove drops the row; the footer count and the "…" menu update accordingly. Removed sections reappear under "Add a section", still previewing the layout they were last given.
The "…" menu adds a section back (inserted above the pinned rows that close the page) or resets to the audit's suggestion.
Clicking outside the card or pressing Esc closes any open menu.
| src/components/previews/ |
Miniature page wireframes — shared parts.tsx, one file per section, dispatched by SectionPreview.tsx |
| src/components/icons.tsx | Inline SVG icon set (no icon dependency) |
| src/app/api/analyze/route.ts | POST { url } → SiteProfile; fetches the page, then asks the model for the judgement calls |
| src/app/api/generate/route.ts | POST { ids, layouts, outlines, business } → one AI-designed HTML page. DeepSeek picks a palette and font pair, then draws each section from the outline src/lib/outline.tsx measured off its wireframe, so the structure is the chosen designs and the look is the model's. Shown in a sandboxed iframe by AiDesignPreview.tsx, and full-size in its own tab at /ai-preview. Header and button text colours are contrast-checked against what they sit on, and the carousels are driven by one hash-pinned script the route writes itself |
| src/lib/extractPage.ts | Deterministic half: fetch, strip, and pull out titles, icons, images and candidate colours |
| src/lib/siteProfile.ts | The SiteProfile shape plus the guards that keep model output honest |
| src/lib/content.ts | Per-section content fields, their placeholder copy, and the list helpers |
| src/lib/placeholder.ts | Placeholder artwork, generated as SVG data URIs — nothing is fetched |
| src/lib/previewImages.ts | Maps a chosen design to its artwork under public/design-sets/ |
| src/lib/asset.ts | URLs for our own files in public/ — basePath and percent-encoding |
| src/components/ContentEditor.tsx | The Content tab: one input per field, image URL or upload, reset |
| src/app/globals.css | Theme colors as Tailwind v4 @theme tokens |
section-cogs/accordions/Type=Accordion V1.svg. They vary by boxed or ruled rows, one column or two, and whether the heading sits above the list or in the half beside it.