Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
289 changes: 157 additions & 132 deletions .marko-run/routes.d.ts

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/explanation/immutable-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Consider this Marko template. Reassigning the array triggers an update; mutating
</ul>
```

Immutable updates work naturally with Marko's assignment-based reactivity. Replacing a value (object, array, map-like structure) makes change propagation explicit and reliable.
Immutable updates work naturally with Marko's [assignment-based reactivity](../reference/reactivity.md). Replacing a value (object, array, map-like structure) makes change propagation explicit and reliable.

## Functional UI

Expand Down
4 changes: 0 additions & 4 deletions docs/explanation/nested-reactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,3 @@ import { produce } from "immer"
});
}>
```

## Case 3: Complex Hoisted State

<!-- TODO: discuss `<mut>` tag -->
8 changes: 8 additions & 0 deletions docs/explanation/serializable-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@ Most standard data types can be serialized, including:

... and many more.

## Reaching the Client

State does not travel through a separate data request or a client-side re-render. When a template renders on the server, inline `<script>` tags are emitted alongside the markup, carrying the serialized state and markers that tie each value back to its place in the DOM. When the page loads, the runtime reads this data and resumes exactly where the server finished, without re-executing the template.

The wire format of this data is an implementation detail that changes between versions, so application code should never read or write it directly.

Since values are only serialized when client-side logic can reference them, state that is rendered into HTML and never touched again in the browser adds nothing to the serialized client-state payload. [Fine-Grained Bundling](./fine-grained-bundling.md) explains how the compiler makes that determination.

## Unserializable Data

Some values cannot be serialized. When these values are encountered the Marko runtime will provide a helpful message to locate the relevant code.
Expand Down
55 changes: 53 additions & 2 deletions docs/guide/duplicate-form-submissions.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,54 @@
# Preventing Multiple Form Submissions
# Preventing Duplicate Form Submissions

This guide will discuss disabling buttons & forms after the first submission.
A native form sends an HTTP request and follows the server's response. The browser may send the same request more than once because of repeated submissions, retries, or multiple open tabs, so correctness cannot depend on disabling a button. The server should make repeated submissions safe.

## Submission Keys

Include a unique submission key in the rendered form. Because the page renders on the server, the form works without browser JavaScript.

```marko
/* src/routes/rsvp/+page.marko */
<const/submissionId=crypto.randomUUID()>

<form method="POST" action="/rsvp">
<input type="hidden" name="submissionId" value=submissionId>
Comment on lines +3 to +14

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not claim that per-render keys protect multiple tabs.

Each rendered tab receives a new submissionId, so two tabs can create two RSVP records. Limit this claim to retries and repeated submissions of the same rendered form, or document a stable operation-level key when cross-tab deduplication is required.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guide/duplicate-form-submissions.md` around lines 3 - 14, Update the
duplicate-submission guidance around submissionId to state that per-render keys
only deduplicate retries or repeated submissions of the same rendered form, not
submissions from multiple tabs. If cross-tab deduplication is required, direct
readers to use a stable operation-level key.


<label for="guests">Number of guests</label>
<input id="guests" name="guests" type="number" min="1" required>

<button type="submit">RSVP</button>
</form>
```

The handler validates the submitted fields and passes the key to the persistence layer. Repeating the request returns the record created by the first request instead of creating another one.

```ts
/* src/routes/rsvp/+handler.ts */
export async function POST(context) {
const data = await context.request.formData();
const submissionId = data.get("submissionId");
const guestsValue = data.get("guests");
const guests = typeof guestsValue === "string" ? Number(guestsValue) : NaN;

if (
typeof submissionId !== "string" ||
!submissionId ||
!Number.isInteger(guests) ||
guests < 1
) {
return new Response("Invalid submission.", { status: 400 });
}

const rsvp = await saveRsvpOnce({ submissionId, guests });
return context.redirect(`/rsvp/${rsvp.id}`, 303);
}
```

> [!WARNING]
> `saveRsvpOnce` must enforce uniqueness for the submission key in the same database transaction that creates the record. Checking for an existing key and inserting in separate operations leaves a race condition.
Comment on lines +42 to +48

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Require database-enforced uniqueness explicitly.

A check-then-insert operation can still race even when both statements share a transaction. Specify a unique index or constraint on submissionId, plus an atomic insert/upsert that handles the duplicate-key result by returning the existing record.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/guide/duplicate-form-submissions.md` around lines 42 - 48, Update the
saveRsvpOnce guidance to require a database-level unique index or constraint on
submissionId, and use an atomic insert or upsert rather than a separate
existence check and insert. Handle the duplicate-key outcome by returning the
existing RSVP record, preserving the redirect flow shown in the example.


## Redirecting

After processing the `POST`, the handler returns a `303` redirect to a `GET` page. This [Post/Redirect/Get pattern](https://en.wikipedia.org/wiki/Post/Redirect/Get) means refreshing the resulting page repeats only the `GET`, not the form submission. The submission key still protects against duplicate requests that reach the server before the redirect.

For the complete native form flow, see the [Forms guide](./forms.md).
149 changes: 149 additions & 0 deletions docs/guide/forms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# Forms

Marko renders HTML, and HTML forms work without any JavaScript. Starting from a native `<form>` keeps submissions functional before scripts load, and Marko's reactivity can then layer on richer behavior where it helps.

## Server Submission

In a [Marko Run](../marko-run/getting-started.md) app, a form posts to a route, and a [`+handler`](../marko-run/file-based-routing.md#handler) beside the page receives the submission. The handler reads the submitted fields with the standard [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) API and redirects when finished.

```marko
/* src/routes/feedback/+page.marko */
<h1>Send Feedback</h1>
<form method="POST">
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>

<button type="submit">Send</button>
</form>
```

```ts
/* src/routes/feedback/+handler.ts */
export async function POST(context) {
const data = await context.request.formData();
const message = data.get("message");

if (typeof message !== "string" || !message.trim()) {
return new Response("A message is required.", { status: 400 });
}

await saveFeedback(message);
return context.redirect("/feedback/thanks", 303);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
```

`GET` requests continue to the page as usual, while the `POST` export handles submissions from the form above.

> [!NOTE]
> Redirecting after a successful `POST` (the [Post/Redirect/Get pattern](https://en.wikipedia.org/wiki/Post/Redirect/Get)) prevents a page refresh from resubmitting the form. See [Preventing Duplicate Form Submissions](./duplicate-form-submissions.md) for making repeated requests safe on the server.

## Validating

Native [validation attributes](https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Form_validation) such as `required`, `min`, and `pattern` block invalid submissions in the browser without any additional code.

```marko
<form method="POST">
<label for="age">Age</label>
<input id="age" name="age" type="number" min="18" required>

<button type="submit">Continue</button>
</form>
```

> [!WARNING]
> Browser validation is a convenience, not a boundary. Requests can be constructed without the form, so handlers must validate submitted values again on the server.

## Reacting to Input

Binding form controls to [tag variables](../reference/language.md#tag-variables) enables live behavior such as previews, character counts, or dependent fields. The [change handler shorthand](../reference/language.md#shorthand-change-handlers-two-way-binding) (`:=`) keeps a variable in sync with a control.

```marko
<let/message="">

<form method="POST">
<label for="message">Message</label>
<textarea id="message" name="message" maxlength="280" value:=message/>
<p>${280 - message.length} characters left</p>

<button type="submit">Send</button>
</form>
```

Because the form still posts natively, this enhancement degrades gracefully: without JavaScript the character counter stays static, but the form submits all the same.

> [!WARNING]
> Deriving `disabled` on the submit button from bound state (for example, disabling it until a field is filled in) renders the button disabled in the server HTML, locking out visitors whose JavaScript has not loaded. Prefer [validation attributes](#validating), which the browser enforces on its own.

## Binding Controls

Every stateful form control has a [`Change` handler](../reference/native-tag.md#change-handlers) in Marko, so the `:=` shorthand from the previous section is not limited to text. Radio groups and checkboxes bind through the [`checkedValue` attribute](../reference/native-tag.md#input-typeradio-and-input-typecheckbox), which holds a string for radios and an array for checkbox groups, and `<select>` binds through its [enhanced `value` attribute](../reference/native-tag.md#select).

```marko
<let/format="html">
<let/topics=[]>
<let/frequency="weekly">

<form method="POST" action="/newsletter">
<fieldset>
<legend>Format</legend>
<label><input type="radio" name="format" value="html" checkedValue:=format> HTML</label>
<label><input type="radio" name="format" value="text" checkedValue:=format> Plain text</label>
</fieldset>

<fieldset>
<legend>Topics</legend>
<label><input type="checkbox" name="topics" value="releases" checkedValue:=topics> Releases</label>
<label><input type="checkbox" name="topics" value="community" checkedValue:=topics> Community</label>
</fieldset>

<label for="frequency">Frequency</label>
<select id="frequency" name="frequency" value:=frequency>
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>

<p>${topics.length} topic${topics.length === 1 ? "" : "s"}, delivered ${frequency}.</p>

<button type="submit">Subscribe</button>
</form>
```

Successful, named controls still submit their values natively; the bound variables exist so the rest of the template can react, as the summary line does here.

> [!CAUTION]
> `value` bindings report strings. Binding a numeric input directly (`value:=quantity`) turns the variable into a string on the first edit. In contrast, `checked` bindings produce booleans, while `checkedValue` may produce an array for checkbox groups. Add a [refining function](../reference/language.md#refining-function) to convert each `value` change before it is assigned:
>
> ```marko
> <let/quantity=1>
>
> <input type="number" name="quantity" min="1" value:parseFloat:=quantity>
> ```

## Reusable Fields

Repeated label-and-input markup can move into a [custom tag](../reference/custom-tag.md#relative-custom-tags), discovered from a `tags/` directory. The [`<id>` tag](../reference/core-tag.md#id) generates a unique id per instance to associate the label with its control, and binding the native input to `input.value` forwards both the value and its change handler to the parent, so the tag is bound with `:=` exactly like a native control.

```marko
/* tags/labeled-input.marko */
<id/fieldId>

<div class="field">
<label for=fieldId>${input.label}</label>
<input id=fieldId name=input.name value:=input.value>
</div>
```

```marko
/* profile-form.marko */
<let/displayName="">

<form method="POST">
<labeled-input label="Display name" name="displayName" value:=displayName/>
<p hidden=!displayName>Previewing as ${displayName}</p>

<button type="submit">Save</button>
</form>
```

Components that hold their own state can offer the same interface by making that state controllable; [Controllable Components](../explanation/controllable-components.md) covers the pattern in depth.
4 changes: 2 additions & 2 deletions docs/guide/styling.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This section explains some different ways to style HTML within Marko. From simpl

## Inline Styles

Marko [enhances the HTML `<style>` tag](../reference/core-tag.md#style) to be processed and optimized by the [bundler used in the project](TODO). A template may specify any number of `<style>` tags.
Marko [enhances the HTML `<style>` tag](../reference/core-tag.md#style) to be processed and optimized by the [bundler used in the project](../introduction/integrations.md). A template may specify any number of `<style>` tags.

By default, all styles defined in the template are **globally scoped**. As such, many Marko projects use patterns like [BEM](https://getbem.com/introduction/) to avoid name conflicts.

Expand Down Expand Up @@ -45,7 +45,7 @@ The `<style>` may include a file extension to enable css preprocessors such as [

### Inline CSS Modules

If the `<style>` tag has a [Tag Variable](../reference/language.md#tag-variables), it leverages [CSS Modules](https://github.com/css-modules/css-modules) to expose its classes as an object.
If the `<style>` tag has a [tag variable](../reference/language.md#tag-variables) (the `/styles` in `<style/styles>` below), it leverages [CSS Modules](https://github.com/css-modules/css-modules) to expose its classes as an object.

```marko
<style/styles>
Expand Down
39 changes: 28 additions & 11 deletions docs/marko-run/file-based-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@ The router only recognizes certain filenames, all prefixed with `+`. The followi

These files establish a route at the current directory path, which will be served for `GET` requests with the HTML content of the page. Only one page may exist for any served path.

Within a page, the request, path parameters, URL, and route metadata are available through [`$global`](../reference/language.md#global). For example, a page under a [dynamic directory](#path-structure) reads its parameter from `$global.params`:

```marko
/* src/routes/blog/$slug/+page.marko */
<h1>Reading post: ${$global.params.slug}</h1>
```

### `+layout.marko`

These files provide a **layout component**, which will wrap all nested layouts and pages. Information is obtained from [`$global`](../reference/language.md#global) and `input`
Layouts are like any other Marko component, with no extra constraints. Each layout receives the request, path params, URL, and route metadata as input, as well as a `content` which refers to the nested page that is being rendered.
These files provide a **layout component**, which will wrap all nested layouts and pages. Layouts are like any other Marko component, with no extra constraints. Each layout receives a `content` input, which refers to the nested page that is being rendered. As with [pages](#pagemarko), the request, path parameters, URL, and route metadata are available through [`$global`](../reference/language.md#global).

```marko
/* +layout.marko */
Expand Down Expand Up @@ -78,6 +84,9 @@ Typically, these will be `.js` or `.ts` files, depending on your project. Like p
}
```

> [!TIP]
> The [Forms guide](../guide/forms.md) shows a handler receiving a form submission and redirecting on success.

### `+middleware.*`

These files are like layouts, but for handlers. Middleware files are called before handlers and let you perform arbitrary work before and after.
Expand Down Expand Up @@ -225,12 +234,12 @@ Without flat routes, you would have a file structure like:

```text
routes/
+layout.marko
projects/
+layout.marko
$projectId/
$members/
+page.marko
+layout.marko
+layout.marko
members/
+page.marko
```

With flat routes, move the path defined by the directories into the files and separate with a period
Expand All @@ -242,17 +251,19 @@ routes/
projects.$projectId.members+page.marko
```

Additionally, you can continue to organize files under directories to decrease duplication and use flat route syntax in the folder name
Additionally, you can continue to organize files under directories to decrease duplication and use flat route syntax in the folder name. Every file inside the directory is nested under its flat route prefix, so a members page and a settings page can share one folder:

```text
routes/
projects.$projectId/
+layout.marko
members+page.marko
+layout.marko
projects.$projectId/
members+page.marko
settings+page.marko
```

Finally, flat routes and routes defined with directories are all treated equally and merged together. For example, this page will have layout
This serves both `/projects/$projectId/members` and `/projects/$projectId/settings`.

Finally, flat routes and routes defined with directories are all treated equally and merged together. For example, this page will have the layout

```text
routes/
Expand Down Expand Up @@ -305,3 +316,9 @@ routes/
```

While both of these create a route which matches the paths, they have slightly different semantics. Using a pathless segment is the same as creating a pathless directory, which allows you to isolate middleware and layouts. Using an empty segment is the same as defining a file at the current location.

## Next Steps

- [TypeScript](./typescript.md)
- [Forms](../guide/forms.md)
- [HTML Streaming](../explanation/streaming.md)
7 changes: 6 additions & 1 deletion docs/marko-run/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Starting with a template:
Manual project setup:

1. Install the required package: `npm install @marko/run`
2. Create the entry file: `src/routes/+page.marko`
2. Create the entry file: `src/routes/+page.marko` (see [File-based Routing](./file-based-routing.md))
3. Start the development server: `npm exec marko-run`

The application will be available at `http://localhost:3000` 🚀
Expand Down Expand Up @@ -92,3 +92,8 @@ Creates a production build and start the preview server
```sh
npm exec marko-run preview
```

## Next Steps

- [File-based Routing](./file-based-routing.md)
- [TypeScript](./typescript.md)
5 changes: 5 additions & 0 deletions docs/marko-run/typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@ These types are replaced with more specific versions per routable file:

- In middleware and layouts which are used in many routes, this type will be a union of all possible routes that the file will see.
- When an adapter is used, it can provide types for the platform

## Next Steps

- [TypeScript Reference](../reference/typescript.md)
- [Supported Environments](../reference/supported-environments.md)
4 changes: 3 additions & 1 deletion docs/tutorial/components-and-reactivity.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ Because this is such a common pattern, Marko provides a [shorthand](../reference
<div>It's ${degF}°F</div>
```

The `:=` binds `value` to `degF` and wires up the `valueChange` handler for us, and the function between the colons (`parseFloat`) is a [refining function](../reference/language.md#refining-function) that transforms each new value before it is assigned.

## Adding Computed Values

Now we can use [the `<const>` tag](../reference/core-tag.md#const) to convert to celsius!
Expand Down Expand Up @@ -99,7 +101,7 @@ Now that we have a reactive variable, let's see what else we can do! Maybe some

## Adding Styles and Visualization

Or what about a temperature gauge, with some fancy CSS?
Or what about a temperature gauge, with some fancy CSS? By default styles in a `.marko` file are globally scoped and loaded once, and the [Styling guide](../guide/styling.md) covers more options.

```marko
<let/degF=80>
Expand Down
3 changes: 3 additions & 0 deletions docs/tutorial/fundamentals.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,9 @@ Marko provides many helpful [Core Tags](../reference/core-tag.md). For example,
</div>
```

> [!NOTE]
> Buttons like these respond to clicks with [event handlers](../reference/native-tag.md#event-handlers), which we use in [Components and Reactivity](./components-and-reactivity.md).

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Now we can show different states based on the product data:

```marko
Expand Down
Loading
Loading