marko6 について
marko6 は `.marko` ファイルを作成・編集するエージェント向けの Marko 6 言語リファレンスです。構文、リアクティビティ、型、レンダリングを扱い、Marko 5 は対象外です。
主な機能
- 01Marko 6 の構文リファレンス
- 02リアクティブな状態と更新規則
- 03TypeScript とテンプレートの型
- 04サーバー・クライアント間のシリアライズ指針
- 05タグ検索とアセットの規約
活用シーン
- 01`.marko` コンポーネントの作成・編集
- 02リアクティビティやレンダリングの問題調査
- 03Marko 固有の TypeScript 型の確認
- 04サーバー・クライアント間のシリアライズエラー回避
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ svallory/ marko-ui --skill marko6インストール後、Claude Code で/marko6を入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/core-tags.mdcustom-tags-styling.mdlanguage.mdnative-tags.mdpatterns.mdtemplate-api.mdtypescript.md詳細
Marko 6 言語
Marko 6 のドキュメント全体(リファレンス、解説、ガイド、チュートリアル)から要点を抽出しています(Marko 5/クラス API は意図的に除外)。このファイルには、エージェントが間違えやすいルールとクイックリファレンスをまとめています。詳しくは references/ を参照してください。ここに記載された内容が実際の動作と異なる場合は、最新のドキュメントを優先してください。Marko は頻繁に更新されています。インデックスは https://markojs.com/llms.txt です。各ページの URL の末尾に .md を付けると、Markdown として取得できます(例:https://markojs.com/docs/reference/core-tag.md)。
Marko 6 のみ — Marko 5 は決して書かない
見つけたらすぐに却下してください。class { onCreate/onMount... } ブロック、$ const x = ... スクリプトレット、<if(cond)>/<for(x in y)> の括弧付き引数、文字列ハンドラー on-click("handleClick")、state/this.state、<include>/<invoke>、components/ による検出、Marko.Component 型。Marko 6 での対応する書き方は、<let>/<const>/<script>/<lifecycle>、<if=cond>、<for|x| of=y>、onClick() {}、tags/ による検出、export interface Input です。
新規作成または編集する .marko ファイルのチェックリスト
- Marko 6 の構文のみを使用する(上記一覧を参照)。
- まず状態を検討します。
<let>/<const>/制御可能なネイティブ要素(value:=)で実現できる場合は、<script>/<lifecycle>を使わないでください。<script>は、実際にブラウザーでの副作用が必要な場合にのみ使用します。 - ハンドラーには関数(
onClick() {}/onClick=fn)を使い、文字列は使いません。引数は(event, element)とし、currentTargetは使いません。 -
export interface Inputを宣言します。mtc(@marko/type-check)で型チェックしてください。tscは.markoを無視するため、成功と報告されます。 - クライアント側の JavaScript は最小限にします。スクリプトによる動作よりも HTML/CSS(
<details>、<dialog>、:has、popover)を優先してください。実際に配信されるのはリアクティブな部分のみです。 - コンポーネントは小さく保ち、技術別ではなく関心事ごとに分割します(分割してもバンドルサイズには影響しません)。
- SSR に対応させます。
<let>にシリアライズできないものを置かないでください。$signal/要素参照は<script>/<lifecycle>/ハンドラー内でのみ使用します。 - イミュータブルに更新します(
x = x.concat())。並べ替え可能な<for>にはby=を指定し、<show>と<if>を意図的に使い分けます。
| ファイル | 参照するタイミング |
|---|---|
references/language.md | 構文:文、属性と省略記法、コンテンツ、属性タグ、タグ変数/パラメーター、動的タグ、省略モード、空白 |
references/core-tags.md | <if> <show> <for> <let> <const> <return> <script> <style> <define> <lifecycle> <id> <await> <try> とリアクティビティ/スケジューリングのモデル |
references/native-tags.md | class=/style=/content=、イベント委譲、制御可能な value:=/checked:=/open:=、スプレッドの所有権 |
references/typescript.md | Input、Marko.* 型、ネイティブ要素/グローバル/CSS の拡張、タグ内の TS 構文 |
references/template-api.md | render/mount、$global(serializedGlobals、cspNonce、renderId、runtimeId) |
references/custom-tags-styling.md | タグの検出順序、tags/ ディレクトリ、marko.json、スタイル、遅延読み込みの with { load } |
references/patterns.md | 制御可能なコンポーネント、ネストされた状態、シリアライズ可能/イミュータブルな状態、ストリーミング、ツール |
このリポジトリのコンポーネントのアーキテクチャ(Zag の配線、属性タグ API)については、skills/port-to-marko/references/marko-gotchas.md を参照してください。
メンタルモデル(10行)
- ファイル = コンポーネント。
input= 属性。tags/ディレクトリは<tag-name>を自動検出します。PascalCase の変数はタグです。<${...}>内の文字列は常にネイティブ要素です。 - モジュール文は
import、export、static、server、clientのみです。それ以外はすべてテンプレート本体(リアクティブ)です。 - リアクティブなソース:タグ変数(
/x)、タグパラメーター(|x|)、input。グラフはコンパイル時に構築され、実行時には追跡されません。式は純粋である必要があります。 <let>= 状態(代入にリアクティブ、更新は不変、SSR 時にシリアライズ)。<const>= 派生値(遅延評価、フラッシュ時に評価)。static const= モジュール定数。- 更新はマイクロタスクの後にまとめて処理されます。
count++の後、<let>は新しい値になりますが、フラッシュされるまではすべての<const>と DOM が古い状態です。 <script>= ブラウザー専用のエフェクトで、参照しているタグ変数/パラメーターが変更されると再実行されます。$signal.onabortでクリーンアップできます。<lifecycle>= 永続的なthisを持つonMount/onUpdate/onDestroyです。- ネイティブタグ変数
<div/el/>はゲッター関数で、ブラウザー専用です。レンダリング後にのみ使用できます。 - あらゆるものを制御可能です:
value:=x≡value=x valueChange(v){x=v}。メンバー式では ≡value=input.x valueChange=input.xChange。<let/x:=input.x>にすると、コンポーネントを制御可能にできます。 - サーバー/クライアントの境界:シリアライズ可能なプレーンデータのみが境界を越えます。状態にクラスインスタンス、DOM ノード、外部モジュールのクロージャーは含められません。
$globalはserializedGlobalsに含めない限りサーバー側にとどまります。 - デフォルトでは JavaScript はゼロです:リアクティブな部分のみがブラウザーに送信されます。コンポーネントの粒度によってバンドルサイズは変わりません。
クイックリファレンス — つまずきやすいルール
構文
- 属性値内の括られていない
>→value=(a > b)。>=と=>は問題ありません。 <div#id.cls.cls2/>は省略記法です。<Comp.x/>はclass="x"であり、プロパティアクセスではありません →<${Comp.x}/>。- 簡潔モードでは、ルート直下のテキストはタグとして解析されます →
-- text。複数行の属性では、行頭にカンマを置きます。 - 属性の文字列は JavaScript リテラルです。
aria-*の列挙型属性には文字列が必要です:aria-pressed=on && "true"。 null|undefined|falseの属性は省略されます。0、""、NaNは出力されます。テキストではnull|undefined|false|""|NaN|0nは省略され、0はレンダリングされます。- 2つのインラインタグの間に改行を入れても、スペースは入りません。スペースは同じ行に置いてください。
<if>/<else if=…>/<else>は直接の兄弟要素である必要があります。ドキュメントでは<else if=cond>と記載されています。<else-if=cond>も 6.3.34 ではコンパイルされます(確認済み)。<${input.content}/>はコンテンツを転送します。ネイティブタグでもcontent=input.contentまたは単に<div ...input/>を使用できます。リテラルの本体(コメントも含む)はcontent=を上書きします。- コンポーネントの
content属性は、デフォルトの本体全体である必要があります。Marko は、タグの属性タグではない子要素からinput.contentを自動的に設定します。呼び出し側で<@content>を書くことはありません。本体を直接記述します(パラメーターも同様です:<Tag|params|>body</Tag>であり、<@content|params|>ではありません)。コンポーネントに同じ階層の本体形式のスロットが2つ以上必要な場合は、mainなどの名前を付けてください。contentは「本体の残り」を意味するため、同じ形式のスロットをもう1つ作ると問題が起きます。 <@item>を繰り返すと、input.itemは最初の項目とイテラブルの両方になります:<for|it| of=input.item>または[...input.item || []]。型はMarko.AttrTag<{...}>です。- タグパラメーター:
<child|{ n }|>。引数の形式<${input.content}(1,2)/>→|a, b|。属性と引数はどちらか一方のみです。属性タグはパラメーターを読み取れません。 - 閉じタグ名は省略できます(
</>)。動的タグは</>で閉じます。
リアクティビティとタイミング
<let/x=input.x>は、後から変更されるinput.xに追従しません。<let/x:=input.x>(制御可能)を使うか、input.xを直接読み取ってください。- ミューテーションはリアクティブではありません。
arr.push()❌ →arr = arr.concat()、obj.k = v❌ →obj = { ...obj, k: v }とします。 - ハンドラーでは
<let>から値を再計算してください。<const>の値と DOM は、フラッシュされるまで古いままです。 <if>は状態を破棄して再作成しますが、<show>はマウントしたままにします。並べ替え時に状態を保持するには<for ... by="id">を使います。$signalは<script>/<lifecycle>/ハンドラー内でのみ使用できます(それ以外ではサーバー上で例外が発生します)。awaitの前に取得してください。<lifecycle>:onMountからオブジェクトを返してください(関数を直接返した場合、クリーンアップは破棄されます)。thisの属性プロパティを上書きしないでください。<return>:テンプレート/コンテンツごとに1つだけ、トップレベルに記述します。value=を変えてください。
ネイティブタグとイベント
- ハンドラーの引数は
(event, element)です。event.currentTargetは使えません(イベントが委譲されるため)。第2引数または ref を使ってください。Marko のハンドラーはネイティブリスナーより先に実行されます。バブリングしないイベントはターゲット上でのみ発生します。 <input value:=n>は文字列を返します →value:parseFloat:=n/valueChange(v){ n = +v }を使います。<select value:=x>:すべての<option>にvalue=が必要です。selected=は使いません。style={}のキーはハイフン区切りで、単位は自動判定されません。条件付きの宣言は配列レベルで指定します。- Spread は属性を所有し、後から値がなくなると属性を削除します。固定属性は Spread の後に記述してください。
<details open:=o>、<dialog open:=o>を使います。showModal()はopenChangeを発火しません。
型
export interface Input {}を使います(interfaceのみだとパースエラーになります)。Marko.Body<[params], { value }>、Marko.AttrTag<T>、ネイティブ要素を拡張するMarko.HTML.Button、Marko.Globalによる拡張、カスタム要素用のMarko.NativeTagsが使えます。tscではなくmtc(@marko/type-check)で型チェックしてください。
検出とアセット
- 検索されるのは
tags/ディレクトリのみです(上位階層を検索)。同階層のファイルにはimportが必要です。パッケージはmarko.jsonのexportsで公開します。隣接するstyle.css/foo.style.cssは自動的に適用されます。 <style>はグローバルで、1回のみ使用します。<style/styles>は CSS Modules です。<style>内の${}はカスタムプロパティになり、宣言値にのみ使用できます。また、スタイルを適用するコンテンツより上に記述します。- 遅延読み込み:
import X from "<x>" with { load: "visible#id | idle?timeout=2000" }のように静的文字列を指定します。共通化するにはファサードタグを使います。
テンプレート API
T.render(input)は一度だけ消費できます。for await、.pipe(res)、.toReadable()、await、.toString()(同期のみ)を使います。T.mount(input, node, pos)はupdate/destroy/valueを返します。- クライアントにグローバルを公開するには
$global.serializedGlobalsを使います(シークレットは絶対に含めないでください)。cspNonce、renderId/runtimeIdは識別子として安全です。
エージェントの既知のエラー一覧(このリポジトリで検証済み)
packages/shadcn/ui/ 内の実際のコンポーネントと notes/bug-marko-dynamic-tag-hydration-crash.md に照らして検証済みです。
-
<if>/<else>はテンプレート直下で兄弟として記述する必要があります —<else>をラッパー内にネストすることはできません。また、</if>と<else>の間に、他の<else-if>/<else>タグ以外の要素を置くことはできません(空白のみのテキスト式も不可)。<if=a>A</if> <else-if=b>B</else-if> <else>C</else> <div> <if=a>A</if> </div> <else>C</else> -
トップレベルの
interface/type/constのみの宣言は、TypeScript ではなくタグとして解析されます。 すべてのトップレベルの TS 宣言の前にexport(実行時のconst/functionにはstatic)が必要です。interface Input { name: string } export interface Input { name: string } const PAGE_SIZE = 20; static const PAGE_SIZE = 20; -
!/は曖昧です — Marko では!(エスケープされない HTML のマーカー$!{...})と/(タグ変数のマーカー<tag/name/>)が、隣接する1文字の記号として多重定義されています。手書きの!/の並びは、誤った記号として解析されるおそれがあります。このスキルについてはコンパイラーを使った独立検証を行っていません(公開前に最小再現例が見つからなかったため、検証が完了するまでは確度が低いものとして扱ってください)。いずれの場合も安全なルールは、エスケープされない出力には常に明示的な$!{expr}形式を使い、エスケープされない出力の直後にタグ変数を置く場合は、空白を挿入するか属性の順序を入れ替えることです。<div>$!{input.articleHtml}</div> -
自己再帰するタグのインポート — 自分自身をインポートするタグ(たとえば、自身の子をレンダリングするツリーノードコンポーネント)は、相対ファイルのカスタムタグパスまたはコンパイル済みファイルの明示的な PascalCase インポートを使って自身をインポートする必要があります。すでにグローバルなカスタムタグであるかのように、自身のタグ名だけを参照してはいけません。相対パスによる検出では
tags/ディレクトリ内のタグだけが見つかるため(https://markojs.com/docs/reference/custom-tag を参照)、同じディレクトリ内の再帰コンポーネントには明示的な自己インポートが必要です。import TreeNode from "./tree-node.marko"; <li> ${input.label} <if=input.children?.length> <ul> <for|child| of=input.children> <TreeNode ...child/> </for> </ul> </if> </li> -
SSR→ハイドレーション境界のシリアライズ可能性に関する契約(クロージャーで包むルール)。 サーバーでレンダリングされた
<let>/リアクティブな状態からクライアントへ渡る値はすべて、プレーンでシリアライズ可能なデータでなければなりません。メソッドを持つライブオブジェクト(Zag サービス、connect()の結果、クロージャーを保持するクラスインスタンスなど)は渡してはいけません。これは Marko の再開モデルにおける要件であり、回避策が必要なバグではありません。再開時にデータが壊れた場合、まず「シリアライズできない値がまだどこかで境界を越えている」と考えるべきで、「Marko が壊れている」と考えるべきではありません。修正方法は、プリミティブだけをシリアライズし、状態を持つものはテンプレートに記述したクロージャー(<zag/api=() => mod .../>のモジュールゲッター、または<zag>/<zag-machine>のprops=)内で再導出することです。このクロージャーは各側で新たに実行されます。inputを介して生の値を渡してはいけません。<let/api=switchMachine.connect(service, normalizeProps)> <zag/api=() => switchMachine from=input/> -
走査順序による
<const>の問題 — marko 6.3.46 で上流修正済み、回避策は不要です。 コンポーネントが、祖先自身のポータル/動的タグマウント内にネストされた動的タグコンテンツ(<${input.content}/>)を介してマウントされると、そのコンポーネント自身の<const>由来の値を走査時に読み取る処理(<if>/<for>の分岐セレクター、属性スプレッドなど、初回マウントの走査中に評価される箇所)は、新しいスコープで<const>の遅延/シグナル延期値がまだ一度も計算されていないうちに実行され、TypeError: ... is not a functionをスローすることがありました。これは@marko/runtime-tagsの実際の不具合で、marko-js/marko#4062 によって修正され、@marko/[email protected]でリリースされました。通常のパターンである、プレーンな<zag/api=...>と各呼び出し箇所でのapi()は正しい方法です。connectFreshのような静的関数のミラーを再導入しないでください。<zag/api=() => machine from=input/> <span ...api().getRootProps()> <if=api().open>...</if> </span>履歴に関する注記:修正が入る前は、回避策として
calendar.marko/avatar.marko/radio-group.markoのそれぞれにモジュールレベルのstatic function connectFresh(...)があり、<const>を完全に迂回して、生のサービスに対して直接machine.connect()を呼び出していました。ワークスペースのmarkoの固定バージョンが 6.3.46 に移行した後、3つともプレーンなapi()に戻されました(2026-08-26)。試行してもクラッシュしたミラーの形式を含むインシデントの詳細については、notes/bug-marko-dynamic-tag-hydration-crash.mdを参照してください。 -
デッドスコープクロージャの危険性:SSR 時にレンダリングされなかったコンテンツは、キャプチャした変数を失います。クロージャではなく、タグパラメーターでエントリーデータを渡してください。 クロージャによるキャプチャ自体は機能します。
<for|person|>内の<@item>本文で${person}を読み取ると、サーバー上でも通常のハイドレーション処理でも正しくレンダリングされます(常に開いている制御構文で確認済みです)。この危険性は再開可能性に固有のものです。各コンテンツ本文は、その OWNER SCOPE(ループのバインディングを保持する、反復ごとの<for>スコープ)への参照としてシリアライズされます。SSR 中に一度もレンダリングされなかった本文(たとえば、閉じた<if>の背後にあるもの)には、シリアライズされたオーナースコープがありません。そのため、クライアントのみでのマウント時に初めて実行されると、クロージャ経由の読み取り(${person}プレースホルダーもvalue=person属性式も)は、いずれも未定義のスコープにアクセスしてTypeError: Cannot read properties of undefined (reading 'person')(最小化後は(reading '2')/'N')をスローします。タグパラメーターなら、構造上この問題を回避できます。データはシリアライズされたスコープ参照ではなく、実行時のレンダー呼び出し内を通って渡されます(<${entry.content}(entry.value)/>と<@item|name| value=person>で${name}を読み取る構成)。したがって、メニューのようなコンポーネントでは、アイテムコンテンツにエントリーデータを常に引数で渡してください。Marko Playground で、違いを明確に示すペアとして検証済みです(v6.3.45、SSR+ハイドレーション)。解説と Playground へのリンク:notes/bug-marko-dynamic-tag-hydration-crash.md、最小限の再現コード:scratch/content-closures-repro/。 -
JSON.stringify(input.item)が設計上、最初の属性タグしか表示しない — 繰り返し属性タグのオブジェクトは「最初の項目+イテラブル」です。stringify は自身の列挙可能なプロパティのみをシリアライズします。[...input.item].length で数え、Array.isArray(input.item)に基づいて分岐しないでください(配列になることはありません。Array.isArray(x) ? x : [x]とすると、最初のタグ以外がすべて黙って破棄されます。直接スプレッドしてください:[...(input.item ?? [])])。
4. 簡潔モードと HTML モードの注意点
ソース:https://markojs.com/docs/reference/concise-syntax
-
簡潔モードでは、ルートレベルのテキストをタグとして誤解析します。 テキストには
--の区切りが必要です(または、HTML モードのタグのコンテンツ内に置いてください)。Hello World Welcome to Marko -- Hello World -- Welcome to Marko -
簡潔モードで複数行にわたる属性リストでは、慣例として継続行の先頭にカンマを置きます(末尾ではありません)。
div id="hello" class="world", style={ border: "1px solid red" } -
ファイルは簡潔モードで始まり、最初の HTML 構文(角括弧で囲まれた)タグで HTML モードに切り替わります — ファイルレベルでの混在は可能です(一度切り替えたら、タグごとに切り替えを繰り返さないでください)が、タグの各出現はどちらか一方の形式で完全に記述する必要があります。
-
簡潔モードの属性値に括弧で囲まず
>を含める場合は、括弧が必要です — 単独の>はタグの終わりと解釈されます。曖昧さのない>=(前後に空白がある場合)と=>は例外です。<if=count > 0>...</if> <if=(count > 0)>...</if>
検証済みサンプル
意図的に壊した(「誤り」)断片ではない、上記の複雑な .marko サンプルはすべて、marko 自身の依存関係ツリーを解決できるよう apps/docs/ 内のスクラッチファイルに配置し、bun で実行した node_modules/.bun/[email protected]/node_modules/@marko/compiler/dist/index.js の compileFile でコンパイルしました(通常の node では、bun の .bun/ ストアレイアウト内にある marko/translator のネストした自己参照を解決できません。リポジトリ内のツールが依存するコンパイラーの解決方法と同様に、bun <script>.mjs が必要です)。実際のサンプル 17 件はすべて正常にコンパイルされました。
OK: 01-class-id-shorthand.marko -> code length 293
OK: 02-dynamic-native-tag.marko -> code length 771
OK: 03-content-forward.marko -> code length 595
OK: 04-attr-tag-collect.marko -> code length 995
OK: 05-tag-params.marko -> code length 575
OK: 05b-tag-params-parent.marko -> code length 865
OK: 06-tag-var-ref.marko -> code length 444
OK: 07-let-script-signal.marko -> code length 492
OK: 08-lifecycle-cleanup.marko -> code length 382
OK: 09-static-function.marko -> code length 651
OK: 10-if-elseif-else.marko -> code length 1459
OK: 11-export-interface-static-const.marko -> code length 635
OK: 12-self-recursive.marko -> code length 2150
OK: 13-if-paren-compare.marko -> code length 1014
OK: 14-root-text-fence.marko -> code length 270
OK: 15-leading-comma-attrs.marko -> code length 310
OK: 16-connect-closure.marko -> code length 1276
OK: 17-connect-fresh-static.marko -> code length 990
意図的に誤りを含めた 4 つの断片(トップレベルの interface、<div> を間に挟んで分割された <if>/<else>、括弧で囲んでいない <if=input.count > 0>、-- で区切っていないルートテキスト)もコンパイルし、それぞれが対応するルールで述べた理由で失敗し、無関係な構文ミスではないことを確認しました。コンパイラー独自の診断も、ルールの説明を個別に裏付けています。
w1-bare-interface.marko -> "Invalid attribute name." (bare `interface` parsed as a tag)
w2-if-else-wrapped.marko -> "The <else> tag must have a preceding <if=cond> or <else if=cond>."
w3-unparenthesized-if.marko -> "Ambiguous \">\" in attribute... wrap it in parentheses, eg \"=(input.count > 0)\"."
w4-root-text-no-fence.marko -> "Unable to find entry point for custom tag <Hello>... prefix it with -- ... or wrap it in an element"