Vibecode Readymag
track this build4 phases, 12 steps, beginner friendly0%The visible editorial website builder loop is buildable, but a credible replacement needs more than the first screen. Readymag earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Readymag. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Readymag · indie build A page builder for editorial sites whose hard problems are a layout model that survives a phone screen, publishing deterministic static output with no runtime dependency, and motion that a reader can switch off. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Editor | Next.js 15 with TypeScript, running on localhost only | the editor is a tool you run, not a service you operate | | Storage | SQLite holding page source, plus an assets folder | the source of the site, backed up as one pair | | Output | A folder of static files per publish, written atomically | the published site has to outlive the thing that made it | | Layout | Sections with per-breakpoint overrides, never free pixel positioning | absolute coordinates are a beautiful desktop page and an unusable phone one | | Fonts | Self-hosted files with the licence recorded beside them | a font served from somebody else's domain is a dependency and sometimes a licence problem | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the editor and the publish step. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Somewhere to serve a folder of static files** · free tiers are common, otherwise pennies - Why: The output has no runtime, so hosting it is genuinely just a folder, and proving that on a real host is the test that the rule zero claim is true. - Get it: Any static host, an object store with website serving, or a small machine with a web server. - Verify: The published folder is served and every page loads with the builder switched off - [ ] **A domain and a certificate** (optional) · a few pounds a year, certificates free - Why: An editorial site lives at a name people type, and certificate renewal is the operational chore that comes with owning the publishing rather than renting it. - Get it: Any registrar, with automatic certificates from your host or a certificate client on your own machine. - Verify: The site loads over TLS at the name and renewal is automated - [ ] **Font files you are licensed to self-host** · free for open families, otherwise per licence - Why: Web font licences differ sharply on self-hosting and the publish step refuses a font whose licence note is empty, which is deliberate. - Get it: Open licence families, or a licence that explicitly permits self-hosting. - Verify: Each font file has a licence note recorded and the publish succeeds - [ ] **Your breakpoint set** · free - Why: Every layout row, every preview and every publish check is defined against this list, and adding one later means revisiting every section on every page. - Get it: Pick two or three widths and write them into the configuration before building a page. - Verify: The list is in the configuration and the editor opens at the narrowest ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Arbitrary visual editing of any element. - Managed hosting, forms and analytics. - Multi-author editing and client handoff. - A template gallery. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries If one of those is essential to you, that is the reason to keep paying for Readymag, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Readymag The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an editorial page builder that publishes plain static files you can host anywhere to replace Readymag. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero What the builder publishes is a folder of ordinary HTML, CSS, images and fonts that renders with the builder switched off, the database deleted and the laptop in a cupboard. The reader's browser never calls back to your editor for anything, not for content, not for fonts, not for a configuration file. If deleting the SQLite database takes the published site down, you have accidentally built a hosting company and taken on its obligations. The practical test, written as a test: publish a site, move the output folder to a machine with no network route to the builder, serve it with a plain static server, and confirm every page, font and image renders and every internal link resolves. Run that before you add a second feature, not after somebody's portfolio is live. ### Stack (fixed, do not substitute) - Editor: Next.js 15 with TypeScript, running on localhost only · the editor is a tool you run, not a service you operate - Storage: SQLite holding page source, plus an assets folder · the source of the site, backed up as one pair - Output: A folder of static files per publish, written atomically · the published site has to outlive the thing that made it - Layout: Sections with per-breakpoint overrides, never free pixel positioning · absolute coordinates are a beautiful desktop page and an unusable phone one - Fonts: Self-hosted files with the licence recorded beside them · a font served from somebody else's domain is a dependency and sometimes a licence problem ### Data model (create this before Phase 1) - sites(id, name, base_path, default_locale, created_at) - pages(id, site_id, slug, title, description, ordinal, published_version_id) · slug is the URL and is validated once, at creation - sections(id, page_id, ordinal, kind, content_json) · kind is a small closed set: cover, text, gallery, quote, embed - layouts(section_id, breakpoint, layout_json) · one row per breakpoint, so the narrow view is data rather than an accident - assets(id, site_id, path, kind, bytes, width, height, sha256, licence_note) · images and fonts, with the licence written down at upload - publishes(id, site_id, version, output_path, started_at, finished_at, file_count, bytes, status) · every publish kept, so rolling back is a directory swap - motion(section_id, effect, duration_ms, reduced_alternative) · every effect names what it does when motion is reduced, or it cannot be saved ### Phase 1 · Sections, not coordinates Build: Define a small closed set of section kinds and a layout per breakpoint for each. Edit the content and the layout separately. Make the editor open at the narrowest breakpoint by default so the phone view is the first thing anyone sees, not the last. Trap: Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. Done when: A section has a stored layout at every declared breakpoint and publishing is refused until the narrowest one has been opened and saved. Reordering sections changes the page and nothing else, tested by comparing output. A section kind that does not exist is rejected rather than rendered as an empty box. Content and layout are edited independently, so changing a heading never moves anything. Do not build yet: No publishing, fonts, images or motion. ### Phase 2 · Publishing a folder that needs nothing Build: Render every page to HTML with inlined or copied CSS, write assets with content-hashed names, and produce the output in a temporary directory that is swapped into place only when complete. Keep each publish as its own version directory. Trap: Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. Done when: The published folder renders every page correctly on a machine with no route to the builder, served by a plain static file server, exercised as a test. A publish interrupted halfway leaves the previous version live and untouched, tested by killing the process mid-publish. Publishing the same unchanged source twice produces byte identical output. Every internal link in the output resolves to a file that exists, checked by a link crawl over the folder. Do not build yet: No fonts, images, motion or rollback yet. ### Phase 3 · Type and pictures that do not shift the page Build: Upload font files, record the licence note beside each, and serve them from the output folder. Generate several widths per image, emit a source set, and write intrinsic width and height into the markup so nothing jumps while loading. Trap: Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. Done when: Every image in the output has intrinsic dimensions in the markup and at least two widths available, checked by parsing the published HTML. A page's total transferred weight at the narrowest breakpoint is measured and recorded in the README. Every font file has a licence note recorded and the publish refuses a font with an empty note. No asset in the output is requested from a domain other than the site's own, asserted by the link crawl. Do not build yet: No motion or rollback yet. ### Phase 4 · Motion a reader can switch off Build: Offer a handful of scroll and entrance effects. Every effect stores what it does when motion is reduced, and the published CSS honours the reduced-motion media feature. Anything that moves on its own has a control to stop it. Trap: Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. Done when: Every saved effect has a reduced alternative and an effect cannot be saved without one, asserted in the suite. The published CSS honours the reduced-motion preference, verified by toggling the system setting and by inspecting the emitted stylesheet. Anything that moves on its own offers a visible control to stop it. The editor previews the reduced variant beside the full one so the author has seen both. Do not build yet: No rollback, backups or domain handoff yet. ### Phase 5 · Rollback, backup and handing the site over Build: Keep the last several publishes as version directories and make rollback a swap. Export the site source as a folder of content files plus assets, so it can be rebuilt without this app. Document exactly what to put in front of the output folder, and back up source and assets together. Trap: Keeping only the current output means the fastest way to recover from a publish that broke the homepage is to find the mistake under pressure, with the broken version live the entire time. Keep the previous versions, make rollback a swap of a symbolic link, and practise it once before you need it. Done when: Rolling back to the previous publish restores the earlier site within seconds and the rollback is exercised in the suite. Source export produces content files and assets that a person could rebuild the site from without this application, demonstrated by opening them. Backup covers the database and the assets folder as one unit and a restore is tested. The README names a static host and the exact directory to point it at, without the builder being involved. Do not build yet: Nothing after this. ### Out of scope (and why) - General purpose visual editing of arbitrary elements, which is the feature that makes a builder feel unlimited and makes its output impossible to keep responsive. - Managed hosting, forms and analytics, each of which makes you responsible for somebody else's uptime and somebody else's visitors. - Multi-author editing and client handoff, which needs a permission model and a review flow before it is worth anything. - A template gallery, since templates are a design project rather than an engineering one and a bad one is worse than none. ### README must contain - That the published output is plain static files and renders with the builder switched off - The exact directory to point a static host at, and what to do about a custom domain and a certificate - That every publish is kept as a version and how to roll back to the previous one - The measured page weight at the narrowest breakpoint, so a heavy page is a known quantity - That every font carries a recorded licence note and a publish is refused without one ===== AGENTS.md ===== # Agent instructions · Readymag indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript, running on localhost only, SQLite holding page source, plus an assets folder, A folder of static files per publish, written atomically, Sections with per-breakpoint overrides, never free pixel positioning, Self-hosted files with the licence recorded beside them. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. ===== BUILD_PLAN.md ===== # Build plan · Readymag A page builder for editorial sites whose hard problems are a layout model that survives a phone screen, publishing deterministic static output with no runtime dependency, and motion that a reader can switch off. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Sections, not coordinates A page is an ordered list of sections whose layout is defined per breakpoint. ### Steps 1. Close the set of section kinds An open set becomes a component library and then becomes your whole weekend. Files: `src/model/sections.ts` 2. Store a layout per breakpoint Per-breakpoint rows make the narrow view a thing you can require. Files: `src/model/layouts.ts` 3. Open the editor at the narrowest view People design what they look at first. Files: `src/editor/Canvas.tsx` ### Done when - [ ] Publishing is refused until the narrow layout is saved - [ ] Reordering sections changes only order - [ ] Unknown section kinds are rejected - [ ] Editing content never moves the layout ### Watch out - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. ## Phase 2 · Publishing a folder that needs nothing A publish writes static files that render with the builder switched off. ### Steps 1. Build into a temporary directory, then swap A half-published site is worse than an old one. Files: `src/publish/build.ts`, `test/interrupt.test.ts` 2. Crawl the output for broken links Cheap, mechanical, and it catches every slug typo before a reader does. Files: `src/publish/verify.ts` 3. Make the output deterministic Byte identical output turns a diff into a review. Files: `src/publish/render.ts` ### Done when - [ ] The output renders with no route to the builder - [ ] An interrupted publish leaves the old version live - [ ] Unchanged source publishes identical bytes - [ ] Every internal link resolves in the output ### Watch out - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. ## Phase 3 · Type and pictures that do not shift the page Fonts are self-hosted with a recorded licence and images ship at sensible sizes with known dimensions. ### Steps 1. Generate widths at upload Doing it at publish time makes every publish slow instead of one upload. Files: `src/assets/images.ts` 2. Always write intrinsic dimensions The one line that stops the page jumping under the reader's thumb. Files: `src/publish/render.ts` 3. Refuse a font with no licence note Fonts are the asset most often used without permission, and it is the operator who is liable. Files: `src/assets/fonts.ts` ### Done when - [ ] Published images carry dimensions and multiple widths - [ ] Narrow-breakpoint page weight is measured and recorded - [ ] Fonts without a licence note block the publish - [ ] No output asset is fetched from another domain ### Watch out - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. ## Phase 4 · Motion a reader can switch off Every effect has a defined reduced-motion behaviour and nothing moves for longer than the reader allows. ### Steps 1. Require a reduced alternative per effect A required field is the only version of this that survives a deadline. Files: `src/model/motion.ts`, `test/motion.test.ts` 2. Emit the media query in the published CSS The reader's own setting does the work; you just have to honour it. Files: `src/publish/css.ts` 3. Preview both variants in the editor Authors design for what they can see. Files: `src/editor/MotionPanel.tsx` ### Done when - [ ] An effect cannot be saved without a reduced alternative - [ ] The published CSS honours reduced motion - [ ] Self-starting motion has a stop control - [ ] The editor previews both variants ### Watch out - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. ## Not in this build - Arbitrary visual editing of any element. - Managed hosting, forms and analytics. - Multi-author editing and client handoff. - A template gallery. ## After v1, if you want it - A second locale per page, which the section model already almost supports - A simple content import from Markdown files, once the section kinds have settled - A print stylesheet, which editorial sites benefit from more than most and costs one file ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections, layouts. Half of what you back up. DATABASE_PATH=./data/site.db # Required. Images and fonts with their licence notes. The other half. ASSETS_DIR=./data/assets # Required. One directory per publish version, with a pointer to the live one. OUTPUT_ROOT=./out # Required. How many publishes to retain. Rollback is a swap between these. PUBLISH_KEEP_VERSIONS=5 # Required. The narrowest is the one the editor opens at and the one publish requires. BREAKPOINTS=360,768,1280 # Required. Generated at upload, emitted as a source set. IMAGE_WIDTHS=480,960,1920 # Required. Localhost. The editor is never part of the published site. EDITOR_BIND=127.0.0.1:3000
You are building a lean indie version of Readymag. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Readymag · indie build A page builder for editorial sites whose hard problems are a layout model that survives a phone screen, publishing deterministic static output with no runtime dependency, and motion that a reader can switch off. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Editor | Next.js 15 with TypeScript, running on localhost only | the editor is a tool you run, not a service you operate | | Storage | SQLite holding page source, plus an assets folder | the source of the site, backed up as one pair | | Output | A folder of static files per publish, written atomically | the published site has to outlive the thing that made it | | Layout | Sections with per-breakpoint overrides, never free pixel positioning | absolute coordinates are a beautiful desktop page and an unusable phone one | | Fonts | Self-hosted files with the licence recorded beside them | a font served from somebody else's domain is a dependency and sometimes a licence problem | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the editor and the publish step. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Somewhere to serve a folder of static files** · free tiers are common, otherwise pennies - Why: The output has no runtime, so hosting it is genuinely just a folder, and proving that on a real host is the test that the rule zero claim is true. - Get it: Any static host, an object store with website serving, or a small machine with a web server. - Verify: The published folder is served and every page loads with the builder switched off - [ ] **A domain and a certificate** (optional) · a few pounds a year, certificates free - Why: An editorial site lives at a name people type, and certificate renewal is the operational chore that comes with owning the publishing rather than renting it. - Get it: Any registrar, with automatic certificates from your host or a certificate client on your own machine. - Verify: The site loads over TLS at the name and renewal is automated - [ ] **Font files you are licensed to self-host** · free for open families, otherwise per licence - Why: Web font licences differ sharply on self-hosting and the publish step refuses a font whose licence note is empty, which is deliberate. - Get it: Open licence families, or a licence that explicitly permits self-hosting. - Verify: Each font file has a licence note recorded and the publish succeeds - [ ] **Your breakpoint set** · free - Why: Every layout row, every preview and every publish check is defined against this list, and adding one later means revisiting every section on every page. - Get it: Pick two or three widths and write them into the configuration before building a page. - Verify: The list is in the configuration and the editor opens at the narrowest ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Arbitrary visual editing of any element. - Managed hosting, forms and analytics. - Multi-author editing and client handoff. - A template gallery. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries If one of those is essential to you, that is the reason to keep paying for Readymag, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Readymag The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an editorial page builder that publishes plain static files you can host anywhere to replace Readymag. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero What the builder publishes is a folder of ordinary HTML, CSS, images and fonts that renders with the builder switched off, the database deleted and the laptop in a cupboard. The reader's browser never calls back to your editor for anything, not for content, not for fonts, not for a configuration file. If deleting the SQLite database takes the published site down, you have accidentally built a hosting company and taken on its obligations. The practical test, written as a test: publish a site, move the output folder to a machine with no network route to the builder, serve it with a plain static server, and confirm every page, font and image renders and every internal link resolves. Run that before you add a second feature, not after somebody's portfolio is live. ### Stack (fixed, do not substitute) - Editor: Next.js 15 with TypeScript, running on localhost only · the editor is a tool you run, not a service you operate - Storage: SQLite holding page source, plus an assets folder · the source of the site, backed up as one pair - Output: A folder of static files per publish, written atomically · the published site has to outlive the thing that made it - Layout: Sections with per-breakpoint overrides, never free pixel positioning · absolute coordinates are a beautiful desktop page and an unusable phone one - Fonts: Self-hosted files with the licence recorded beside them · a font served from somebody else's domain is a dependency and sometimes a licence problem ### Data model (create this before Phase 1) - sites(id, name, base_path, default_locale, created_at) - pages(id, site_id, slug, title, description, ordinal, published_version_id) · slug is the URL and is validated once, at creation - sections(id, page_id, ordinal, kind, content_json) · kind is a small closed set: cover, text, gallery, quote, embed - layouts(section_id, breakpoint, layout_json) · one row per breakpoint, so the narrow view is data rather than an accident - assets(id, site_id, path, kind, bytes, width, height, sha256, licence_note) · images and fonts, with the licence written down at upload - publishes(id, site_id, version, output_path, started_at, finished_at, file_count, bytes, status) · every publish kept, so rolling back is a directory swap - motion(section_id, effect, duration_ms, reduced_alternative) · every effect names what it does when motion is reduced, or it cannot be saved ### Phase 1 · Sections, not coordinates Build: Define a small closed set of section kinds and a layout per breakpoint for each. Edit the content and the layout separately. Make the editor open at the narrowest breakpoint by default so the phone view is the first thing anyone sees, not the last. Trap: Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. Done when: A section has a stored layout at every declared breakpoint and publishing is refused until the narrowest one has been opened and saved. Reordering sections changes the page and nothing else, tested by comparing output. A section kind that does not exist is rejected rather than rendered as an empty box. Content and layout are edited independently, so changing a heading never moves anything. Do not build yet: No publishing, fonts, images or motion. ### Phase 2 · Publishing a folder that needs nothing Build: Render every page to HTML with inlined or copied CSS, write assets with content-hashed names, and produce the output in a temporary directory that is swapped into place only when complete. Keep each publish as its own version directory. Trap: Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. Done when: The published folder renders every page correctly on a machine with no route to the builder, served by a plain static file server, exercised as a test. A publish interrupted halfway leaves the previous version live and untouched, tested by killing the process mid-publish. Publishing the same unchanged source twice produces byte identical output. Every internal link in the output resolves to a file that exists, checked by a link crawl over the folder. Do not build yet: No fonts, images, motion or rollback yet. ### Phase 3 · Type and pictures that do not shift the page Build: Upload font files, record the licence note beside each, and serve them from the output folder. Generate several widths per image, emit a source set, and write intrinsic width and height into the markup so nothing jumps while loading. Trap: Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. Done when: Every image in the output has intrinsic dimensions in the markup and at least two widths available, checked by parsing the published HTML. A page's total transferred weight at the narrowest breakpoint is measured and recorded in the README. Every font file has a licence note recorded and the publish refuses a font with an empty note. No asset in the output is requested from a domain other than the site's own, asserted by the link crawl. Do not build yet: No motion or rollback yet. ### Phase 4 · Motion a reader can switch off Build: Offer a handful of scroll and entrance effects. Every effect stores what it does when motion is reduced, and the published CSS honours the reduced-motion media feature. Anything that moves on its own has a control to stop it. Trap: Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. Done when: Every saved effect has a reduced alternative and an effect cannot be saved without one, asserted in the suite. The published CSS honours the reduced-motion preference, verified by toggling the system setting and by inspecting the emitted stylesheet. Anything that moves on its own offers a visible control to stop it. The editor previews the reduced variant beside the full one so the author has seen both. Do not build yet: No rollback, backups or domain handoff yet. ### Phase 5 · Rollback, backup and handing the site over Build: Keep the last several publishes as version directories and make rollback a swap. Export the site source as a folder of content files plus assets, so it can be rebuilt without this app. Document exactly what to put in front of the output folder, and back up source and assets together. Trap: Keeping only the current output means the fastest way to recover from a publish that broke the homepage is to find the mistake under pressure, with the broken version live the entire time. Keep the previous versions, make rollback a swap of a symbolic link, and practise it once before you need it. Done when: Rolling back to the previous publish restores the earlier site within seconds and the rollback is exercised in the suite. Source export produces content files and assets that a person could rebuild the site from without this application, demonstrated by opening them. Backup covers the database and the assets folder as one unit and a restore is tested. The README names a static host and the exact directory to point it at, without the builder being involved. Do not build yet: Nothing after this. ### Out of scope (and why) - General purpose visual editing of arbitrary elements, which is the feature that makes a builder feel unlimited and makes its output impossible to keep responsive. - Managed hosting, forms and analytics, each of which makes you responsible for somebody else's uptime and somebody else's visitors. - Multi-author editing and client handoff, which needs a permission model and a review flow before it is worth anything. - A template gallery, since templates are a design project rather than an engineering one and a bad one is worse than none. ### README must contain - That the published output is plain static files and renders with the builder switched off - The exact directory to point a static host at, and what to do about a custom domain and a certificate - That every publish is kept as a version and how to roll back to the previous one - The measured page weight at the narrowest breakpoint, so a heavy page is a known quantity - That every font carries a recorded licence note and a publish is refused without one ===== AGENTS.md ===== # Agent instructions · Readymag indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript, running on localhost only, SQLite holding page source, plus an assets folder, A folder of static files per publish, written atomically, Sections with per-breakpoint overrides, never free pixel positioning, Self-hosted files with the licence recorded beside them. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. ===== BUILD_PLAN.md ===== # Build plan · Readymag A page builder for editorial sites whose hard problems are a layout model that survives a phone screen, publishing deterministic static output with no runtime dependency, and motion that a reader can switch off. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Sections, not coordinates A page is an ordered list of sections whose layout is defined per breakpoint. ### Steps 1. Close the set of section kinds An open set becomes a component library and then becomes your whole weekend. Files: `src/model/sections.ts` 2. Store a layout per breakpoint Per-breakpoint rows make the narrow view a thing you can require. Files: `src/model/layouts.ts` 3. Open the editor at the narrowest view People design what they look at first. Files: `src/editor/Canvas.tsx` ### Done when - [ ] Publishing is refused until the narrow layout is saved - [ ] Reordering sections changes only order - [ ] Unknown section kinds are rejected - [ ] Editing content never moves the layout ### Watch out - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. ## Phase 2 · Publishing a folder that needs nothing A publish writes static files that render with the builder switched off. ### Steps 1. Build into a temporary directory, then swap A half-published site is worse than an old one. Files: `src/publish/build.ts`, `test/interrupt.test.ts` 2. Crawl the output for broken links Cheap, mechanical, and it catches every slug typo before a reader does. Files: `src/publish/verify.ts` 3. Make the output deterministic Byte identical output turns a diff into a review. Files: `src/publish/render.ts` ### Done when - [ ] The output renders with no route to the builder - [ ] An interrupted publish leaves the old version live - [ ] Unchanged source publishes identical bytes - [ ] Every internal link resolves in the output ### Watch out - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. ## Phase 3 · Type and pictures that do not shift the page Fonts are self-hosted with a recorded licence and images ship at sensible sizes with known dimensions. ### Steps 1. Generate widths at upload Doing it at publish time makes every publish slow instead of one upload. Files: `src/assets/images.ts` 2. Always write intrinsic dimensions The one line that stops the page jumping under the reader's thumb. Files: `src/publish/render.ts` 3. Refuse a font with no licence note Fonts are the asset most often used without permission, and it is the operator who is liable. Files: `src/assets/fonts.ts` ### Done when - [ ] Published images carry dimensions and multiple widths - [ ] Narrow-breakpoint page weight is measured and recorded - [ ] Fonts without a licence note block the publish - [ ] No output asset is fetched from another domain ### Watch out - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. ## Phase 4 · Motion a reader can switch off Every effect has a defined reduced-motion behaviour and nothing moves for longer than the reader allows. ### Steps 1. Require a reduced alternative per effect A required field is the only version of this that survives a deadline. Files: `src/model/motion.ts`, `test/motion.test.ts` 2. Emit the media query in the published CSS The reader's own setting does the work; you just have to honour it. Files: `src/publish/css.ts` 3. Preview both variants in the editor Authors design for what they can see. Files: `src/editor/MotionPanel.tsx` ### Done when - [ ] An effect cannot be saved without a reduced alternative - [ ] The published CSS honours reduced motion - [ ] Self-starting motion has a stop control - [ ] The editor previews both variants ### Watch out - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. ## Not in this build - Arbitrary visual editing of any element. - Managed hosting, forms and analytics. - Multi-author editing and client handoff. - A template gallery. ## After v1, if you want it - A second locale per page, which the section model already almost supports - A simple content import from Markdown files, once the section kinds have settled - A print stylesheet, which editorial sites benefit from more than most and costs one file ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections, layouts. Half of what you back up. DATABASE_PATH=./data/site.db # Required. Images and fonts with their licence notes. The other half. ASSETS_DIR=./data/assets # Required. One directory per publish version, with a pointer to the live one. OUTPUT_ROOT=./out # Required. How many publishes to retain. Rollback is a swap between these. PUBLISH_KEEP_VERSIONS=5 # Required. The narrowest is the one the editor opens at and the one publish requires. BREAKPOINTS=360,768,1280 # Required. Generated at upload, emitted as a source set. IMAGE_WIDTHS=480,960,1920 # Required. Localhost. The editor is never part of the published site. EDITOR_BIND=127.0.0.1:3000
You are building a production product version of Readymag. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Readymag · product brief ## Problem The visible editorial website builder loop is buildable, but a credible replacement needs more than the first screen. Readymag earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A builder whose output is a folder anybody can host, whose pages are readable on a phone because the narrow layout was required rather than hoped for, and whose motion stops when the reader has asked for it to stop. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - deploy target or static hosting - user-owned domain for publishing ## Explicit non-goals for v1 - Arbitrary visual editing of any element. - Managed hosting, forms and analytics. - Multi-author editing and client handoff. - A template gallery. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries ## Success criteria - The output is confirmed to render with no route to the builder - An interrupted publish is confirmed to leave the previous version live - Every published image is confirmed to carry intrinsic dimensions - An effect is confirmed unsaveable without a reduced alternative - Rollback to the previous publish is confirmed to work ===== BRIEF.md ===== # Build brief · Readymag The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an editorial page builder that publishes plain static files you can host anywhere to replace Readymag. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero What the builder publishes is a folder of ordinary HTML, CSS, images and fonts that renders with the builder switched off, the database deleted and the laptop in a cupboard. The reader's browser never calls back to your editor for anything, not for content, not for fonts, not for a configuration file. If deleting the SQLite database takes the published site down, you have accidentally built a hosting company and taken on its obligations. The practical test, written as a test: publish a site, move the output folder to a machine with no network route to the builder, serve it with a plain static server, and confirm every page, font and image renders and every internal link resolves. Run that before you add a second feature, not after somebody's portfolio is live. ### Stack (fixed, do not substitute) - Editor: Next.js 15 with TypeScript, running on localhost only · the editor is a tool you run, not a service you operate - Storage: SQLite holding page source, plus an assets folder · the source of the site, backed up as one pair - Output: A folder of static files per publish, written atomically · the published site has to outlive the thing that made it - Layout: Sections with per-breakpoint overrides, never free pixel positioning · absolute coordinates are a beautiful desktop page and an unusable phone one - Fonts: Self-hosted files with the licence recorded beside them · a font served from somebody else's domain is a dependency and sometimes a licence problem ### Data model (create this before Phase 1) - sites(id, name, base_path, default_locale, created_at) - pages(id, site_id, slug, title, description, ordinal, published_version_id) · slug is the URL and is validated once, at creation - sections(id, page_id, ordinal, kind, content_json) · kind is a small closed set: cover, text, gallery, quote, embed - layouts(section_id, breakpoint, layout_json) · one row per breakpoint, so the narrow view is data rather than an accident - assets(id, site_id, path, kind, bytes, width, height, sha256, licence_note) · images and fonts, with the licence written down at upload - publishes(id, site_id, version, output_path, started_at, finished_at, file_count, bytes, status) · every publish kept, so rolling back is a directory swap - motion(section_id, effect, duration_ms, reduced_alternative) · every effect names what it does when motion is reduced, or it cannot be saved ### Phase 1 · Sections, not coordinates Build: Define a small closed set of section kinds and a layout per breakpoint for each. Edit the content and the layout separately. Make the editor open at the narrowest breakpoint by default so the phone view is the first thing anyone sees, not the last. Trap: Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. Done when: A section has a stored layout at every declared breakpoint and publishing is refused until the narrowest one has been opened and saved. Reordering sections changes the page and nothing else, tested by comparing output. A section kind that does not exist is rejected rather than rendered as an empty box. Content and layout are edited independently, so changing a heading never moves anything. Do not build yet: No publishing, fonts, images or motion. ### Phase 2 · Publishing a folder that needs nothing Build: Render every page to HTML with inlined or copied CSS, write assets with content-hashed names, and produce the output in a temporary directory that is swapped into place only when complete. Keep each publish as its own version directory. Trap: Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. Done when: The published folder renders every page correctly on a machine with no route to the builder, served by a plain static file server, exercised as a test. A publish interrupted halfway leaves the previous version live and untouched, tested by killing the process mid-publish. Publishing the same unchanged source twice produces byte identical output. Every internal link in the output resolves to a file that exists, checked by a link crawl over the folder. Do not build yet: No fonts, images, motion or rollback yet. ### Phase 3 · Type and pictures that do not shift the page Build: Upload font files, record the licence note beside each, and serve them from the output folder. Generate several widths per image, emit a source set, and write intrinsic width and height into the markup so nothing jumps while loading. Trap: Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. Done when: Every image in the output has intrinsic dimensions in the markup and at least two widths available, checked by parsing the published HTML. A page's total transferred weight at the narrowest breakpoint is measured and recorded in the README. Every font file has a licence note recorded and the publish refuses a font with an empty note. No asset in the output is requested from a domain other than the site's own, asserted by the link crawl. Do not build yet: No motion or rollback yet. ### Phase 4 · Motion a reader can switch off Build: Offer a handful of scroll and entrance effects. Every effect stores what it does when motion is reduced, and the published CSS honours the reduced-motion media feature. Anything that moves on its own has a control to stop it. Trap: Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. Done when: Every saved effect has a reduced alternative and an effect cannot be saved without one, asserted in the suite. The published CSS honours the reduced-motion preference, verified by toggling the system setting and by inspecting the emitted stylesheet. Anything that moves on its own offers a visible control to stop it. The editor previews the reduced variant beside the full one so the author has seen both. Do not build yet: No rollback, backups or domain handoff yet. ### Phase 5 · Rollback, backup and handing the site over Build: Keep the last several publishes as version directories and make rollback a swap. Export the site source as a folder of content files plus assets, so it can be rebuilt without this app. Document exactly what to put in front of the output folder, and back up source and assets together. Trap: Keeping only the current output means the fastest way to recover from a publish that broke the homepage is to find the mistake under pressure, with the broken version live the entire time. Keep the previous versions, make rollback a swap of a symbolic link, and practise it once before you need it. Done when: Rolling back to the previous publish restores the earlier site within seconds and the rollback is exercised in the suite. Source export produces content files and assets that a person could rebuild the site from without this application, demonstrated by opening them. Backup covers the database and the assets folder as one unit and a restore is tested. The README names a static host and the exact directory to point it at, without the builder being involved. Do not build yet: Nothing after this. ### Out of scope (and why) - General purpose visual editing of arbitrary elements, which is the feature that makes a builder feel unlimited and makes its output impossible to keep responsive. - Managed hosting, forms and analytics, each of which makes you responsible for somebody else's uptime and somebody else's visitors. - Multi-author editing and client handoff, which needs a permission model and a review flow before it is worth anything. - A template gallery, since templates are a design project rather than an engineering one and a bad one is worse than none. ### README must contain - That the published output is plain static files and renders with the builder switched off - The exact directory to point a static host at, and what to do about a custom domain and a certificate - That every publish is kept as a version and how to roll back to the previous one - The measured page weight at the narrowest breakpoint, so a heavy page is a known quantity - That every font carries a recorded licence note and a publish is refused without one ===== ARCHITECTURE.md ===== # Architecture · Readymag ## Stack | Part | Choice | Why | | --- | --- | --- | | Editor | Next.js 15 with TypeScript, running on localhost only | the editor is a tool you run, not a service you operate | | Storage | SQLite holding page source, plus an assets folder | the source of the site, backed up as one pair | | Output | A folder of static files per publish, written atomically | the published site has to outlive the thing that made it | | Layout | Sections with per-breakpoint overrides, never free pixel positioning | absolute coordinates are a beautiful desktop page and an unusable phone one | | Fonts | Self-hosted files with the licence recorded beside them | a font served from somebody else's domain is a dependency and sometimes a licence problem | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Model | Sections and per-breakpoint layouts. | Any editor, provided layouts stay per breakpoint. | | Publish | Deterministic static output with an atomic swap. | Any renderer, provided the output needs no runtime. | | Assets | Image widths, font licences, content hashing. | Any pipeline, provided dimensions are always emitted. | | Motion | Effects paired with reduced alternatives. | Any effect set, provided the pairing stays required. | | Versions | Publish history and rollback. | Any scheme, provided rollback stays a swap rather than a rebuild. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Pages, sections, layouts. Half of what you back up. - `ASSETS_DIR` · required · Images and fonts with their licence notes. The other half. - `OUTPUT_ROOT` · required · One directory per publish version, with a pointer to the live one. - `PUBLISH_KEEP_VERSIONS` · required · How many publishes to retain. Rollback is a swap between these. - `BREAKPOINTS` · required · The narrowest is the one the editor opens at and the one publish requires. - `IMAGE_WIDTHS` · required · Generated at upload, emitted as a source set. - `EDITOR_BIND` · required · Localhost. The editor is never part of the published site. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Readymag product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript, running on localhost only, SQLite holding page source, plus an assets folder, A folder of static files per publish, written atomically, Sections with per-breakpoint overrides, never free pixel positioning, Self-hosted files with the licence recorded beside them. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. - Keeping only the current output means the fastest way to recover from a publish that broke the homepage is to find the mistake under pressure, with the broken version live the entire time. Keep the previous versions, make rollback a swap of a symbolic link, and practise it once before you need it. ===== MILESTONES.md ===== # Delivery milestones · Readymag Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Sections, not coordinates A page is an ordered list of sections whose layout is defined per breakpoint. ### Steps 1. Close the set of section kinds An open set becomes a component library and then becomes your whole weekend. Files: `src/model/sections.ts` 2. Store a layout per breakpoint Per-breakpoint rows make the narrow view a thing you can require. Files: `src/model/layouts.ts` 3. Open the editor at the narrowest view People design what they look at first. Files: `src/editor/Canvas.tsx` ### Done when - [ ] Publishing is refused until the narrow layout is saved - [ ] Reordering sections changes only order - [ ] Unknown section kinds are rejected - [ ] Editing content never moves the layout ### Watch out - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. ## M2 · Publishing a folder that needs nothing A publish writes static files that render with the builder switched off. ### Steps 1. Build into a temporary directory, then swap A half-published site is worse than an old one. Files: `src/publish/build.ts`, `test/interrupt.test.ts` 2. Crawl the output for broken links Cheap, mechanical, and it catches every slug typo before a reader does. Files: `src/publish/verify.ts` 3. Make the output deterministic Byte identical output turns a diff into a review. Files: `src/publish/render.ts` ### Done when - [ ] The output renders with no route to the builder - [ ] An interrupted publish leaves the old version live - [ ] Unchanged source publishes identical bytes - [ ] Every internal link resolves in the output ### Watch out - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. ## M3 · Type and pictures that do not shift the page Fonts are self-hosted with a recorded licence and images ship at sensible sizes with known dimensions. ### Steps 1. Generate widths at upload Doing it at publish time makes every publish slow instead of one upload. Files: `src/assets/images.ts` 2. Always write intrinsic dimensions The one line that stops the page jumping under the reader's thumb. Files: `src/publish/render.ts` 3. Refuse a font with no licence note Fonts are the asset most often used without permission, and it is the operator who is liable. Files: `src/assets/fonts.ts` ### Done when - [ ] Published images carry dimensions and multiple widths - [ ] Narrow-breakpoint page weight is measured and recorded - [ ] Fonts without a licence note block the publish - [ ] No output asset is fetched from another domain ### Watch out - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. ## M4 · Motion a reader can switch off Every effect has a defined reduced-motion behaviour and nothing moves for longer than the reader allows. ### Steps 1. Require a reduced alternative per effect A required field is the only version of this that survives a deadline. Files: `src/model/motion.ts`, `test/motion.test.ts` 2. Emit the media query in the published CSS The reader's own setting does the work; you just have to honour it. Files: `src/publish/css.ts` 3. Preview both variants in the editor Authors design for what they can see. Files: `src/editor/MotionPanel.tsx` ### Done when - [ ] An effect cannot be saved without a reduced alternative - [ ] The published CSS honours reduced motion - [ ] Self-starting motion has a stop control - [ ] The editor previews both variants ### Watch out - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. ## M5 · Rollback, backup and handing the site over (production only) A bad publish is one command to undo and the whole site can leave with its owner. ### Steps 1. Keep publishes as versioned directories Rollback becomes a swap instead of an investigation. Files: `src/publish/versions.ts` 2. Export source, not just output Output is a rendering; source is what somebody can edit in five years. Files: `src/export/source.ts` 3. Back up the database and assets together Either one alone restores to a site with holes in it. Files: `scripts/backup.sh`, `docs/operations.md` ### Done when - [ ] Rollback restores the previous publish quickly - [ ] Source export is usable without this app - [ ] Backup and restore cover source and assets together - [ ] The README names the host and the directory ### Watch out - Keeping only the current output means the fastest way to recover from a publish that broke the homepage is to find the mistake under pressure, with the broken version live the entire time. Keep the previous versions, make rollback a swap of a symbolic link, and practise it once before you need it. ===== OPERATIONS.md ===== # Operations · Readymag ## Backup The database and the assets folder as one unit, on the same schedule. The published output is derived and does not need backing up, which is a pleasant consequence of the publish being deterministic. ## Restore Restore the pair, run a publish, and compare the output against the live version. Identical bytes mean the restore was complete; a difference means an asset did not come back. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a static site the checks are external: the site answers, the certificate has time left, and the published file count matches the last publish record. A silently shrinking file count is an interrupted publish that somehow swapped. ## Incident checklist A broken homepage is a rollback, not a debugging session, and the previous version directory is still there. A missing font or image after a publish is almost always an asset that failed to copy, which the link crawl should have caught before the swap. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The output is confirmed to render with no route to the builder - [ ] An interrupted publish is confirmed to leave the previous version live - [ ] Every published image is confirmed to carry intrinsic dimensions - [ ] An effect is confirmed unsaveable without a reduced alternative - [ ] Rollback to the previous publish is confirmed to work ## Launch constraint Do not market omitted Readymag capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections, layouts. Half of what you back up. DATABASE_PATH=./data/site.db # Required. Images and fonts with their licence notes. The other half. ASSETS_DIR=./data/assets # Required. One directory per publish version, with a pointer to the live one. OUTPUT_ROOT=./out # Required. How many publishes to retain. Rollback is a swap between these. PUBLISH_KEEP_VERSIONS=5 # Required. The narrowest is the one the editor opens at and the one publish requires. BREAKPOINTS=360,768,1280 # Required. Generated at upload, emitted as a source set. IMAGE_WIDTHS=480,960,1920 # Required. Localhost. The editor is never part of the published site. EDITOR_BIND=127.0.0.1:3000
# Readymag · indie build A page builder for editorial sites whose hard problems are a layout model that survives a phone screen, publishing deterministic static output with no runtime dependency, and motion that a reader can switch off. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Editor | Next.js 15 with TypeScript, running on localhost only | the editor is a tool you run, not a service you operate | | Storage | SQLite holding page source, plus an assets folder | the source of the site, backed up as one pair | | Output | A folder of static files per publish, written atomically | the published site has to outlive the thing that made it | | Layout | Sections with per-breakpoint overrides, never free pixel positioning | absolute coordinates are a beautiful desktop page and an unusable phone one | | Fonts | Self-hosted files with the licence recorded beside them | a font served from somebody else's domain is a dependency and sometimes a licence problem | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the editor and the publish step. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **Somewhere to serve a folder of static files** · free tiers are common, otherwise pennies - Why: The output has no runtime, so hosting it is genuinely just a folder, and proving that on a real host is the test that the rule zero claim is true. - Get it: Any static host, an object store with website serving, or a small machine with a web server. - Verify: The published folder is served and every page loads with the builder switched off - [ ] **A domain and a certificate** (optional) · a few pounds a year, certificates free - Why: An editorial site lives at a name people type, and certificate renewal is the operational chore that comes with owning the publishing rather than renting it. - Get it: Any registrar, with automatic certificates from your host or a certificate client on your own machine. - Verify: The site loads over TLS at the name and renewal is automated - [ ] **Font files you are licensed to self-host** · free for open families, otherwise per licence - Why: Web font licences differ sharply on self-hosting and the publish step refuses a font whose licence note is empty, which is deliberate. - Get it: Open licence families, or a licence that explicitly permits self-hosting. - Verify: Each font file has a licence note recorded and the publish succeeds - [ ] **Your breakpoint set** · free - Why: Every layout row, every preview and every publish check is defined against this list, and adding one later means revisiting every section on every page. - Get it: Pick two or three widths and write them into the configuration before building a page. - Verify: The list is in the configuration and the editor opens at the narrowest ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Arbitrary visual editing of any element. - Managed hosting, forms and analytics. - Multi-author editing and client handoff. - A template gallery. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries If one of those is essential to you, that is the reason to keep paying for Readymag, and the README should say so rather than pretend.
# Build brief · Readymag The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an editorial page builder that publishes plain static files you can host anywhere to replace Readymag. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero What the builder publishes is a folder of ordinary HTML, CSS, images and fonts that renders with the builder switched off, the database deleted and the laptop in a cupboard. The reader's browser never calls back to your editor for anything, not for content, not for fonts, not for a configuration file. If deleting the SQLite database takes the published site down, you have accidentally built a hosting company and taken on its obligations. The practical test, written as a test: publish a site, move the output folder to a machine with no network route to the builder, serve it with a plain static server, and confirm every page, font and image renders and every internal link resolves. Run that before you add a second feature, not after somebody's portfolio is live. ### Stack (fixed, do not substitute) - Editor: Next.js 15 with TypeScript, running on localhost only · the editor is a tool you run, not a service you operate - Storage: SQLite holding page source, plus an assets folder · the source of the site, backed up as one pair - Output: A folder of static files per publish, written atomically · the published site has to outlive the thing that made it - Layout: Sections with per-breakpoint overrides, never free pixel positioning · absolute coordinates are a beautiful desktop page and an unusable phone one - Fonts: Self-hosted files with the licence recorded beside them · a font served from somebody else's domain is a dependency and sometimes a licence problem ### Data model (create this before Phase 1) - sites(id, name, base_path, default_locale, created_at) - pages(id, site_id, slug, title, description, ordinal, published_version_id) · slug is the URL and is validated once, at creation - sections(id, page_id, ordinal, kind, content_json) · kind is a small closed set: cover, text, gallery, quote, embed - layouts(section_id, breakpoint, layout_json) · one row per breakpoint, so the narrow view is data rather than an accident - assets(id, site_id, path, kind, bytes, width, height, sha256, licence_note) · images and fonts, with the licence written down at upload - publishes(id, site_id, version, output_path, started_at, finished_at, file_count, bytes, status) · every publish kept, so rolling back is a directory swap - motion(section_id, effect, duration_ms, reduced_alternative) · every effect names what it does when motion is reduced, or it cannot be saved ### Phase 1 · Sections, not coordinates Build: Define a small closed set of section kinds and a layout per breakpoint for each. Edit the content and the layout separately. Make the editor open at the narrowest breakpoint by default so the phone view is the first thing anyone sees, not the last. Trap: Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. Done when: A section has a stored layout at every declared breakpoint and publishing is refused until the narrowest one has been opened and saved. Reordering sections changes the page and nothing else, tested by comparing output. A section kind that does not exist is rejected rather than rendered as an empty box. Content and layout are edited independently, so changing a heading never moves anything. Do not build yet: No publishing, fonts, images or motion. ### Phase 2 · Publishing a folder that needs nothing Build: Render every page to HTML with inlined or copied CSS, write assets with content-hashed names, and produce the output in a temporary directory that is swapped into place only when complete. Keep each publish as its own version directory. Trap: Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. Done when: The published folder renders every page correctly on a machine with no route to the builder, served by a plain static file server, exercised as a test. A publish interrupted halfway leaves the previous version live and untouched, tested by killing the process mid-publish. Publishing the same unchanged source twice produces byte identical output. Every internal link in the output resolves to a file that exists, checked by a link crawl over the folder. Do not build yet: No fonts, images, motion or rollback yet. ### Phase 3 · Type and pictures that do not shift the page Build: Upload font files, record the licence note beside each, and serve them from the output folder. Generate several widths per image, emit a source set, and write intrinsic width and height into the markup so nothing jumps while loading. Trap: Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. Done when: Every image in the output has intrinsic dimensions in the markup and at least two widths available, checked by parsing the published HTML. A page's total transferred weight at the narrowest breakpoint is measured and recorded in the README. Every font file has a licence note recorded and the publish refuses a font with an empty note. No asset in the output is requested from a domain other than the site's own, asserted by the link crawl. Do not build yet: No motion or rollback yet. ### Phase 4 · Motion a reader can switch off Build: Offer a handful of scroll and entrance effects. Every effect stores what it does when motion is reduced, and the published CSS honours the reduced-motion media feature. Anything that moves on its own has a control to stop it. Trap: Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. Done when: Every saved effect has a reduced alternative and an effect cannot be saved without one, asserted in the suite. The published CSS honours the reduced-motion preference, verified by toggling the system setting and by inspecting the emitted stylesheet. Anything that moves on its own offers a visible control to stop it. The editor previews the reduced variant beside the full one so the author has seen both. Do not build yet: No rollback, backups or domain handoff yet. ### Phase 5 · Rollback, backup and handing the site over Build: Keep the last several publishes as version directories and make rollback a swap. Export the site source as a folder of content files plus assets, so it can be rebuilt without this app. Document exactly what to put in front of the output folder, and back up source and assets together. Trap: Keeping only the current output means the fastest way to recover from a publish that broke the homepage is to find the mistake under pressure, with the broken version live the entire time. Keep the previous versions, make rollback a swap of a symbolic link, and practise it once before you need it. Done when: Rolling back to the previous publish restores the earlier site within seconds and the rollback is exercised in the suite. Source export produces content files and assets that a person could rebuild the site from without this application, demonstrated by opening them. Backup covers the database and the assets folder as one unit and a restore is tested. The README names a static host and the exact directory to point it at, without the builder being involved. Do not build yet: Nothing after this. ### Out of scope (and why) - General purpose visual editing of arbitrary elements, which is the feature that makes a builder feel unlimited and makes its output impossible to keep responsive. - Managed hosting, forms and analytics, each of which makes you responsible for somebody else's uptime and somebody else's visitors. - Multi-author editing and client handoff, which needs a permission model and a review flow before it is worth anything. - A template gallery, since templates are a design project rather than an engineering one and a bad one is worse than none. ### README must contain - That the published output is plain static files and renders with the builder switched off - The exact directory to point a static host at, and what to do about a custom domain and a certificate - That every publish is kept as a version and how to roll back to the previous one - The measured page weight at the narrowest breakpoint, so a heavy page is a known quantity - That every font carries a recorded licence note and a publish is refused without one
# Agent instructions · Readymag indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript, running on localhost only, SQLite holding page source, plus an assets folder, A folder of static files per publish, written atomically, Sections with per-breakpoint overrides, never free pixel positioning, Self-hosted files with the licence recorded beside them. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it.
# Build plan · Readymag A page builder for editorial sites whose hard problems are a layout model that survives a phone screen, publishing deterministic static output with no runtime dependency, and motion that a reader can switch off. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Sections, not coordinates A page is an ordered list of sections whose layout is defined per breakpoint. ### Steps 1. Close the set of section kinds An open set becomes a component library and then becomes your whole weekend. Files: `src/model/sections.ts` 2. Store a layout per breakpoint Per-breakpoint rows make the narrow view a thing you can require. Files: `src/model/layouts.ts` 3. Open the editor at the narrowest view People design what they look at first. Files: `src/editor/Canvas.tsx` ### Done when - [ ] Publishing is refused until the narrow layout is saved - [ ] Reordering sections changes only order - [ ] Unknown section kinds are rejected - [ ] Editing content never moves the layout ### Watch out - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. ## Phase 2 · Publishing a folder that needs nothing A publish writes static files that render with the builder switched off. ### Steps 1. Build into a temporary directory, then swap A half-published site is worse than an old one. Files: `src/publish/build.ts`, `test/interrupt.test.ts` 2. Crawl the output for broken links Cheap, mechanical, and it catches every slug typo before a reader does. Files: `src/publish/verify.ts` 3. Make the output deterministic Byte identical output turns a diff into a review. Files: `src/publish/render.ts` ### Done when - [ ] The output renders with no route to the builder - [ ] An interrupted publish leaves the old version live - [ ] Unchanged source publishes identical bytes - [ ] Every internal link resolves in the output ### Watch out - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. ## Phase 3 · Type and pictures that do not shift the page Fonts are self-hosted with a recorded licence and images ship at sensible sizes with known dimensions. ### Steps 1. Generate widths at upload Doing it at publish time makes every publish slow instead of one upload. Files: `src/assets/images.ts` 2. Always write intrinsic dimensions The one line that stops the page jumping under the reader's thumb. Files: `src/publish/render.ts` 3. Refuse a font with no licence note Fonts are the asset most often used without permission, and it is the operator who is liable. Files: `src/assets/fonts.ts` ### Done when - [ ] Published images carry dimensions and multiple widths - [ ] Narrow-breakpoint page weight is measured and recorded - [ ] Fonts without a licence note block the publish - [ ] No output asset is fetched from another domain ### Watch out - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. ## Phase 4 · Motion a reader can switch off Every effect has a defined reduced-motion behaviour and nothing moves for longer than the reader allows. ### Steps 1. Require a reduced alternative per effect A required field is the only version of this that survives a deadline. Files: `src/model/motion.ts`, `test/motion.test.ts` 2. Emit the media query in the published CSS The reader's own setting does the work; you just have to honour it. Files: `src/publish/css.ts` 3. Preview both variants in the editor Authors design for what they can see. Files: `src/editor/MotionPanel.tsx` ### Done when - [ ] An effect cannot be saved without a reduced alternative - [ ] The published CSS honours reduced motion - [ ] Self-starting motion has a stop control - [ ] The editor previews both variants ### Watch out - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. ## Not in this build - Arbitrary visual editing of any element. - Managed hosting, forms and analytics. - Multi-author editing and client handoff. - A template gallery. ## After v1, if you want it - A second locale per page, which the section model already almost supports - A simple content import from Markdown files, once the section kinds have settled - A print stylesheet, which editorial sites benefit from more than most and costs one file
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections, layouts. Half of what you back up. DATABASE_PATH=./data/site.db # Required. Images and fonts with their licence notes. The other half. ASSETS_DIR=./data/assets # Required. One directory per publish version, with a pointer to the live one. OUTPUT_ROOT=./out # Required. How many publishes to retain. Rollback is a swap between these. PUBLISH_KEEP_VERSIONS=5 # Required. The narrowest is the one the editor opens at and the one publish requires. BREAKPOINTS=360,768,1280 # Required. Generated at upload, emitted as a source set. IMAGE_WIDTHS=480,960,1920 # Required. Localhost. The editor is never part of the published site. EDITOR_BIND=127.0.0.1:3000
# Readymag · product brief ## Problem The visible editorial website builder loop is buildable, but a credible replacement needs more than the first screen. Readymag earns its keep through editor, CMS, hosting, polish, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A builder whose output is a folder anybody can host, whose pages are readable on a phone because the narrow layout was required rather than hoped for, and whose motion stops when the reader has asked for it to stop. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node.js 22 - deploy target or static hosting - user-owned domain for publishing ## Explicit non-goals for v1 - Arbitrary visual editing of any element. - Managed hosting, forms and analytics. - Multi-author editing and client handoff. - A template gallery. - CMS collaboration and client handoff - managed hosting, forms, and analytics - general-purpose visual editing - large template and component libraries ## Success criteria - The output is confirmed to render with no route to the builder - An interrupted publish is confirmed to leave the previous version live - Every published image is confirmed to carry intrinsic dimensions - An effect is confirmed unsaveable without a reduced alternative - Rollback to the previous publish is confirmed to work
# Build brief · Readymag The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me an editorial page builder that publishes plain static files you can host anywhere to replace Readymag. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero What the builder publishes is a folder of ordinary HTML, CSS, images and fonts that renders with the builder switched off, the database deleted and the laptop in a cupboard. The reader's browser never calls back to your editor for anything, not for content, not for fonts, not for a configuration file. If deleting the SQLite database takes the published site down, you have accidentally built a hosting company and taken on its obligations. The practical test, written as a test: publish a site, move the output folder to a machine with no network route to the builder, serve it with a plain static server, and confirm every page, font and image renders and every internal link resolves. Run that before you add a second feature, not after somebody's portfolio is live. ### Stack (fixed, do not substitute) - Editor: Next.js 15 with TypeScript, running on localhost only · the editor is a tool you run, not a service you operate - Storage: SQLite holding page source, plus an assets folder · the source of the site, backed up as one pair - Output: A folder of static files per publish, written atomically · the published site has to outlive the thing that made it - Layout: Sections with per-breakpoint overrides, never free pixel positioning · absolute coordinates are a beautiful desktop page and an unusable phone one - Fonts: Self-hosted files with the licence recorded beside them · a font served from somebody else's domain is a dependency and sometimes a licence problem ### Data model (create this before Phase 1) - sites(id, name, base_path, default_locale, created_at) - pages(id, site_id, slug, title, description, ordinal, published_version_id) · slug is the URL and is validated once, at creation - sections(id, page_id, ordinal, kind, content_json) · kind is a small closed set: cover, text, gallery, quote, embed - layouts(section_id, breakpoint, layout_json) · one row per breakpoint, so the narrow view is data rather than an accident - assets(id, site_id, path, kind, bytes, width, height, sha256, licence_note) · images and fonts, with the licence written down at upload - publishes(id, site_id, version, output_path, started_at, finished_at, file_count, bytes, status) · every publish kept, so rolling back is a directory swap - motion(section_id, effect, duration_ms, reduced_alternative) · every effect names what it does when motion is reduced, or it cannot be saved ### Phase 1 · Sections, not coordinates Build: Define a small closed set of section kinds and a layout per breakpoint for each. Edit the content and the layout separately. Make the editor open at the narrowest breakpoint by default so the phone view is the first thing anyone sees, not the last. Trap: Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. Done when: A section has a stored layout at every declared breakpoint and publishing is refused until the narrowest one has been opened and saved. Reordering sections changes the page and nothing else, tested by comparing output. A section kind that does not exist is rejected rather than rendered as an empty box. Content and layout are edited independently, so changing a heading never moves anything. Do not build yet: No publishing, fonts, images or motion. ### Phase 2 · Publishing a folder that needs nothing Build: Render every page to HTML with inlined or copied CSS, write assets with content-hashed names, and produce the output in a temporary directory that is swapped into place only when complete. Keep each publish as its own version directory. Trap: Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. Done when: The published folder renders every page correctly on a machine with no route to the builder, served by a plain static file server, exercised as a test. A publish interrupted halfway leaves the previous version live and untouched, tested by killing the process mid-publish. Publishing the same unchanged source twice produces byte identical output. Every internal link in the output resolves to a file that exists, checked by a link crawl over the folder. Do not build yet: No fonts, images, motion or rollback yet. ### Phase 3 · Type and pictures that do not shift the page Build: Upload font files, record the licence note beside each, and serve them from the output folder. Generate several widths per image, emit a source set, and write intrinsic width and height into the markup so nothing jumps while loading. Trap: Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. Done when: Every image in the output has intrinsic dimensions in the markup and at least two widths available, checked by parsing the published HTML. A page's total transferred weight at the narrowest breakpoint is measured and recorded in the README. Every font file has a licence note recorded and the publish refuses a font with an empty note. No asset in the output is requested from a domain other than the site's own, asserted by the link crawl. Do not build yet: No motion or rollback yet. ### Phase 4 · Motion a reader can switch off Build: Offer a handful of scroll and entrance effects. Every effect stores what it does when motion is reduced, and the published CSS honours the reduced-motion media feature. Anything that moves on its own has a control to stop it. Trap: Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. Done when: Every saved effect has a reduced alternative and an effect cannot be saved without one, asserted in the suite. The published CSS honours the reduced-motion preference, verified by toggling the system setting and by inspecting the emitted stylesheet. Anything that moves on its own offers a visible control to stop it. The editor previews the reduced variant beside the full one so the author has seen both. Do not build yet: No rollback, backups or domain handoff yet. ### Phase 5 · Rollback, backup and handing the site over Build: Keep the last several publishes as version directories and make rollback a swap. Export the site source as a folder of content files plus assets, so it can be rebuilt without this app. Document exactly what to put in front of the output folder, and back up source and assets together. Trap: Keeping only the current output means the fastest way to recover from a publish that broke the homepage is to find the mistake under pressure, with the broken version live the entire time. Keep the previous versions, make rollback a swap of a symbolic link, and practise it once before you need it. Done when: Rolling back to the previous publish restores the earlier site within seconds and the rollback is exercised in the suite. Source export produces content files and assets that a person could rebuild the site from without this application, demonstrated by opening them. Backup covers the database and the assets folder as one unit and a restore is tested. The README names a static host and the exact directory to point it at, without the builder being involved. Do not build yet: Nothing after this. ### Out of scope (and why) - General purpose visual editing of arbitrary elements, which is the feature that makes a builder feel unlimited and makes its output impossible to keep responsive. - Managed hosting, forms and analytics, each of which makes you responsible for somebody else's uptime and somebody else's visitors. - Multi-author editing and client handoff, which needs a permission model and a review flow before it is worth anything. - A template gallery, since templates are a design project rather than an engineering one and a bad one is worse than none. ### README must contain - That the published output is plain static files and renders with the builder switched off - The exact directory to point a static host at, and what to do about a custom domain and a certificate - That every publish is kept as a version and how to roll back to the previous one - The measured page weight at the narrowest breakpoint, so a heavy page is a known quantity - That every font carries a recorded licence note and a publish is refused without one
# Architecture · Readymag ## Stack | Part | Choice | Why | | --- | --- | --- | | Editor | Next.js 15 with TypeScript, running on localhost only | the editor is a tool you run, not a service you operate | | Storage | SQLite holding page source, plus an assets folder | the source of the site, backed up as one pair | | Output | A folder of static files per publish, written atomically | the published site has to outlive the thing that made it | | Layout | Sections with per-breakpoint overrides, never free pixel positioning | absolute coordinates are a beautiful desktop page and an unusable phone one | | Fonts | Self-hosted files with the licence recorded beside them | a font served from somebody else's domain is a dependency and sometimes a licence problem | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Model | Sections and per-breakpoint layouts. | Any editor, provided layouts stay per breakpoint. | | Publish | Deterministic static output with an atomic swap. | Any renderer, provided the output needs no runtime. | | Assets | Image widths, font licences, content hashing. | Any pipeline, provided dimensions are always emitted. | | Motion | Effects paired with reduced alternatives. | Any effect set, provided the pairing stays required. | | Versions | Publish history and rollback. | Any scheme, provided rollback stays a swap rather than a rebuild. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Pages, sections, layouts. Half of what you back up. - `ASSETS_DIR` · required · Images and fonts with their licence notes. The other half. - `OUTPUT_ROOT` · required · One directory per publish version, with a pointer to the live one. - `PUBLISH_KEEP_VERSIONS` · required · How many publishes to retain. Rollback is a swap between these. - `BREAKPOINTS` · required · The narrowest is the one the editor opens at and the one publish requires. - `IMAGE_WIDTHS` · required · Generated at upload, emitted as a source set. - `EDITOR_BIND` · required · Localhost. The editor is never part of the published site. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Readymag product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript, running on localhost only, SQLite holding page source, plus an assets folder, A folder of static files per publish, written atomically, Sections with per-breakpoint overrides, never free pixel positioning, Self-hosted files with the licence recorded beside them. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. - Keeping only the current output means the fastest way to recover from a publish that broke the homepage is to find the mistake under pressure, with the broken version live the entire time. Keep the previous versions, make rollback a swap of a symbolic link, and practise it once before you need it.
# Delivery milestones · Readymag Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Sections, not coordinates A page is an ordered list of sections whose layout is defined per breakpoint. ### Steps 1. Close the set of section kinds An open set becomes a component library and then becomes your whole weekend. Files: `src/model/sections.ts` 2. Store a layout per breakpoint Per-breakpoint rows make the narrow view a thing you can require. Files: `src/model/layouts.ts` 3. Open the editor at the narrowest view People design what they look at first. Files: `src/editor/Canvas.tsx` ### Done when - [ ] Publishing is refused until the narrow layout is saved - [ ] Reordering sections changes only order - [ ] Unknown section kinds are rejected - [ ] Editing content never moves the layout ### Watch out - Storing an absolute position for every element is what makes a freeform editor feel good for an afternoon and produces a site that is unreadable on a phone, which is where most of the traffic will come from. Store a layout per breakpoint, require the narrow one to be set before a page can publish, and refuse to publish a page whose narrow layout has never been looked at. ## M2 · Publishing a folder that needs nothing A publish writes static files that render with the builder switched off. ### Steps 1. Build into a temporary directory, then swap A half-published site is worse than an old one. Files: `src/publish/build.ts`, `test/interrupt.test.ts` 2. Crawl the output for broken links Cheap, mechanical, and it catches every slug typo before a reader does. Files: `src/publish/verify.ts` 3. Make the output deterministic Byte identical output turns a diff into a review. Files: `src/publish/render.ts` ### Done when - [ ] The output renders with no route to the builder - [ ] An interrupted publish leaves the old version live - [ ] Unchanged source publishes identical bytes - [ ] Every internal link resolves in the output ### Watch out - Writing the output over the live folder page by page means a publish that fails halfway leaves a site that is half the new version and half the old one, with broken links between the two, and it happens at exactly the moment somebody is sharing the link. Build into a temporary directory, verify the file count and the internal links, then swap. ## M3 · Type and pictures that do not shift the page Fonts are self-hosted with a recorded licence and images ship at sensible sizes with known dimensions. ### Steps 1. Generate widths at upload Doing it at publish time makes every publish slow instead of one upload. Files: `src/assets/images.ts` 2. Always write intrinsic dimensions The one line that stops the page jumping under the reader's thumb. Files: `src/publish/render.ts` 3. Refuse a font with no licence note Fonts are the asset most often used without permission, and it is the operator who is liable. Files: `src/assets/fonts.ts` ### Done when - [ ] Published images carry dimensions and multiple widths - [ ] Narrow-breakpoint page weight is measured and recorded - [ ] Fonts without a licence note block the publish - [ ] No output asset is fetched from another domain ### Watch out - Shipping the original camera image and letting the browser scale it sends several megabytes to a phone for a picture displayed at four hundred pixels, and omitting the intrinsic dimensions makes the text jump down the page while each one loads. Generate widths at upload, emit the source set, and always write the dimensions. ## M4 · Motion a reader can switch off Every effect has a defined reduced-motion behaviour and nothing moves for longer than the reader allows. ### Steps 1. Require a reduced alternative per effect A required field is the only version of this that survives a deadline. Files: `src/model/motion.ts`, `test/motion.test.ts` 2. Emit the media query in the published CSS The reader's own setting does the work; you just have to honour it. Files: `src/publish/css.ts` 3. Preview both variants in the editor Authors design for what they can see. Files: `src/editor/MotionPanel.tsx` ### Done when - [ ] An effect cannot be saved without a reduced alternative - [ ] The published CSS honours reduced motion - [ ] Self-starting motion has a stop control - [ ] The editor previews both variants ### Watch out - Parallax and entrance animations with no reduced-motion path make a portfolio actively unpleasant for readers with vestibular sensitivity, and the fix is a media query rather than a redesign. The CSS Media Queries Level 5 draft defines prefers-reduced-motion for exactly this case, WCAG 2.2 makes disabling interaction-triggered animation a AAA criterion, and its Level A criterion on pausing moving content applies to anything that starts by itself and runs past a few seconds. Store a reduced alternative per effect and let the query pick it. ## M5 · Rollback, backup and handing the site over (production only) A bad publish is one command to undo and the whole site can leave with its owner. ### Steps 1. Keep publishes as versioned directories Rollback becomes a swap instead of an investigation. Files: `src/publish/versions.ts` 2. Export source, not just output Output is a rendering; source is what somebody can edit in five years. Files: `src/export/source.ts` 3. Back up the database and assets together Either one alone restores to a site with holes in it. Files: `scripts/backup.sh`, `docs/operations.md` ### Done when - [ ] Rollback restores the previous publish quickly - [ ] Source export is usable without this app - [ ] Backup and restore cover source and assets together - [ ] The README names the host and the directory ### Watch out - Keeping only the current output means the fastest way to recover from a publish that broke the homepage is to find the mistake under pressure, with the broken version live the entire time. Keep the previous versions, make rollback a swap of a symbolic link, and practise it once before you need it.
# Operations · Readymag ## Backup The database and the assets folder as one unit, on the same schedule. The published output is derived and does not need backing up, which is a pleasant consequence of the publish being deterministic. ## Restore Restore the pair, run a publish, and compare the output against the live version. Identical bytes mean the restore was complete; a difference means an asset did not come back. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a static site the checks are external: the site answers, the certificate has time left, and the published file count matches the last publish record. A silently shrinking file count is an interrupted publish that somehow swapped. ## Incident checklist A broken homepage is a rollback, not a debugging session, and the previous version directory is still there. A missing font or image after a publish is almost always an asset that failed to copy, which the link crawl should have caught before the swap. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] The output is confirmed to render with no route to the builder - [ ] An interrupted publish is confirmed to leave the previous version live - [ ] Every published image is confirmed to carry intrinsic dimensions - [ ] An effect is confirmed unsaveable without a reduced alternative - [ ] Rollback to the previous publish is confirmed to work ## Launch constraint Do not market omitted Readymag capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Pages, sections, layouts. Half of what you back up. DATABASE_PATH=./data/site.db # Required. Images and fonts with their licence notes. The other half. ASSETS_DIR=./data/assets # Required. One directory per publish version, with a pointer to the live one. OUTPUT_ROOT=./out # Required. How many publishes to retain. Rollback is a swap between these. PUBLISH_KEEP_VERSIONS=5 # Required. The narrowest is the one the editor opens at and the one publish requires. BREAKPOINTS=360,768,1280 # Required. Generated at upload, emitted as a source set. IMAGE_WIDTHS=480,960,1920 # Required. Localhost. The editor is never part of the published site. EDITOR_BIND=127.0.0.1:3000
$ choose a build depth, inspect the files, then open the complete pack in your agent
Readymag: A single site is easy to code. People pay for safe ongoing edits, templates, publishing, domains, collaboration, and no-maintenance hosting.
xCMS collaboration and client handoff
xmanaged hosting, forms, and analytics
xgeneral-purpose visual editing
xlarge template and component libraries
Don't feel like building it? These folks already made it free.
all 3 free alternatives to Readymag →· no votes, no pay-to-list · just what's real
Vibecode Readymag
Kinda. The core of Readymag is buildable in a weekend with the prompt on this page, but there are real gaps: CMS collaboration and client handoff, managed hosting, forms, and analytics. Read the honest list above before committing.
How much does Readymag cost?
Readymag costs about $20/month (Personal, checked 2026-07-31), which is $240 per year.
What do I lose by replacing Readymag?
Honestly: CMS collaboration and client handoff; managed hosting, forms, and analytics; general-purpose visual editing; large template and component libraries. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Readymag?
Yes: Webstudio (Visual web publishing with precise CSS and CMS data; the design freedom survives, the managed hosting does not.) Plasmic (A genuinely freeform hosted canvas with responsive publishing; presentation-specific niceties are lighter.) Silex (Freeform stories, portfolios and magazines exported as ordinary HTML; less theatrical, more portable.) All 3 curated free alternatives are at vibecodeit.com/readymag/alternatives. The prompt is for when you want it exactly your way.