diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 6abb44dc58..98e435a6b5 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1489,8 +1489,6 @@ _pages:
   eyeCatchingImageRemove: "アイキャッチ画像を削除"
   chooseBlock: "ブロックを追加"
   selectType: "種類を選択"
-  enterVariableName: "変数名を決めてください"
-  variableNameIsAlreadyUsed: "その変数名は既に使われています"
   contentBlocks: "コンテンツ"
   inputBlocks: "入力"
   specialBlocks: "特殊"
@@ -1501,261 +1499,12 @@ _pages:
     image: "画像"
     button: "ボタン"
 
-    if: "もし"
-    _if:
-      variable: "変数"
-
-    post: "投稿フォーム"
-    _post:
-      text: "内容"
-      attachCanvasImage: "キャンバスの画像を添付する"
-      canvasId: "キャンバスID"
-
-    textInput: "テキスト入力"
-    _textInput:
-      name: "変数名"
-      text: "タイトル"
-      default: "デフォルト値"
-
-    textareaInput: "複数行テキスト入力"
-    _textareaInput:
-      name: "変数名"
-      text: "タイトル"
-      default: "デフォルト値"
-
-    numberInput: "数値入力"
-    _numberInput:
-      name: "変数名"
-      text: "タイトル"
-      default: "デフォルト値"
-
-    canvas: "キャンバス"
-    _canvas:
-      id: "キャンバスID"
-      width: "幅"
-      height: "高さ"
-
     note: "ノート埋め込み"
     _note:
       id: "ノートID"
       idDescription: "ノートURLをペーストして設定することもできます。"
       detailed: "詳細な表示"
 
-    switch: "スイッチ"
-    _switch:
-      name: "変数名"
-      text: "タイトル"
-      default: "デフォルト値"
-
-    counter: "カウンター"
-    _counter:
-      name: "変数名"
-      text: "タイトル"
-      inc: "増加値"
-
-    _button:
-      text: "タイトル"
-      colored: "色付き"
-      action: "ボタンを押したときの動作"
-      _action:
-        dialog: "ダイアログを表示する"
-        _dialog:
-          content: "内容"
-        resetRandom: "乱数をリセット"
-        pushEvent: "イベントを送信させる"
-        _pushEvent:
-          event: "イベント名"
-          message: "押したときに表示するメッセージ"
-          variable: "送信する変数"
-          no-variable: "なし"
-        callAiScript: "AiScript呼び出し"
-        _callAiScript:
-          functionName: "関数名"
-
-    radioButton: "選択肢"
-    _radioButton:
-      name: "変数名"
-      title: "タイトル"
-      values: "改行で区切った選択肢"
-      default: "デフォルト値"
-
-  script:
-    categories:
-      flow: "制御"
-      logical: "論理演算"
-      operation: "計算"
-      comparison: "比較"
-      random: "ランダム"
-      value: "値"
-      fn: "関数"
-      text: "テキスト操作"
-      convert: "変換"
-      list: "リスト"
-    blocks:
-      text: "テキスト"
-      multiLineText: "テキスト(複数行)"
-      textList: "テキストのリスト"
-      _textList:
-        info: "ひとつひとつを改行で区切ってください"
-      strLen: "テキストの長さ"
-      _strLen:
-        arg1: "テキスト"
-      strPick: "文字取り出し"
-      _strPick:
-        arg1: "テキスト"
-        arg2: "文字の位置"
-      strReplace: "テキスト置き換え"
-      _strReplace:
-        arg1: "テキスト"
-        arg2: "置き換え前"
-        arg3: "置き換え後"
-      strReverse: "テキストを反転"
-      _strReverse:
-        arg1: "テキスト"
-      join: "テキストを連結"
-      _join:
-        arg1: "リスト"
-        arg2: "区切り"
-      add: "足す"
-      _add:
-        arg1: "A"
-        arg2: "B"
-      subtract: "引く"
-      _subtract:
-        arg1: "A"
-        arg2: "B"
-      multiply: "掛ける"
-      _multiply:
-        arg1: "A"
-        arg2: "B"
-      divide: "割る"
-      _divide:
-        arg1: "A"
-        arg2: "B"
-      mod: "割った余り"
-      _mod:
-        arg1: "A"
-        arg2: "B"
-      round: "小数を丸める"
-      _round:
-        arg1: "数値"
-      eq: "AとBが同じ"
-      _eq:
-        arg1: "A"
-        arg2: "B"
-      notEq: "AとBが異なる"
-      _notEq:
-        arg1: "A"
-        arg2: "B"
-      and: "AかつB"
-      _and:
-        arg1: "A"
-        arg2: "B"
-      or: "AまたはB"
-      _or:
-        arg1: "A"
-        arg2: "B"
-      lt: "< AがBより小さい"
-      _lt:
-        arg1: "A"
-        arg2: "B"
-      gt: "> AがBより大きい"
-      _gt:
-        arg1: "A"
-        arg2: "B"
-      ltEq: "<= AがBと同じか小さい"
-      _ltEq:
-        arg1: "A"
-        arg2: "B"
-      gtEq: ">= AがBと同じか大きい"
-      _gtEq:
-        arg1: "A"
-        arg2: "B"
-      if: "分岐"
-      _if:
-        arg1: "もし"
-        arg2: "なら"
-        arg3: "そうでなければ"
-      not: "否定"
-      _not:
-        arg1: "否定"
-      random: "ランダム"
-      _random:
-        arg1: "確率"
-      rannum: "乱数"
-      _rannum:
-        arg1: "最小"
-        arg2: "最大"
-      randomPick: "リストからランダムに選択"
-      _randomPick:
-        arg1: "リスト"
-      dailyRandom: "ランダム (ユーザーごとに日替わり)"
-      _dailyRandom:
-        arg1: "確率"
-      dailyRannum: "乱数 (ユーザーごとに日替わり)"
-      _dailyRannum:
-        arg1: "最小"
-        arg2: "最大"
-      dailyRandomPick: "リストからランダムに選択 (ユーザーごとに日替わり)"
-      _dailyRandomPick:
-        arg1: "リスト"
-      seedRandom: "ランダム (シード)"
-      _seedRandom:
-        arg1: "シード"
-        arg2: "確率"
-      seedRannum: "乱数 (シード)"
-      _seedRannum:
-        arg1: "シード"
-        arg2: "最小"
-        arg3: "最大"
-      seedRandomPick: "リストからランダムに選択 (シード)"
-      _seedRandomPick:
-        arg1: "シード"
-        arg2: "リスト"
-      DRPWPM: "確率付きリストからランダムに選択 (ユーザーごとに日替わり)"
-      _DRPWPM:
-        arg1: "テキストのリスト"
-      pick: "リストから選択"
-      _pick:
-        arg1: "リスト"
-        arg2: "位置"
-      listLen: "リストの長さを取得"
-      _listLen:
-        arg1: "リスト"
-      number: "数値"
-      stringToNumber: "テキストを数値に"
-      _stringToNumber:
-        arg1: "テキスト"
-      numberToString: "数値をテキストに"
-      _numberToString:
-        arg1: "数値"
-      splitStrByLine: "テキストを行で分割"
-      _splitStrByLine:
-        arg1: "テキスト"
-      ref: "変数"
-      aiScriptVar: "AiScript変数"
-      fn: "関数"
-      _fn:
-        slots: "スロット"
-        slots-info: "スロットひとつひとつを改行で区切ってください"
-        arg1: "出力"
-      for: "繰り返し"
-      _for:
-        arg1: "回数"
-        arg2: "処理"
-    typeError: "スロット{slot}は\"{expect}\"を受け付けますが、\"{actual}\"が入れられています!"
-    thereIsEmptySlot: "スロット{slot}が空です!"
-    types:
-      string: "テキスト"
-      number: "数値"
-      boolean: "フラグ"
-      array: "リスト"
-      stringArray: "テキストのリスト"
-    emptySlot: "空のスロット"
-    enviromentVariables: "環境変数"
-    pageVariables: "ページ要素"
-    argVariables: "入力スロット"
-
 _relayStatus:
   requesting: "承認待ち"
   accepted: "承認済み"
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.button.vue b/packages/client/src/pages/page-editor/els/page-editor.el.button.vue
deleted file mode 100644
index 08f993c426..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.button.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ti ti-bolt"></i> {{ $ts._pages.blocks.button }}</template>
-
-	<section class="xfhsjczc">
-		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._button.text }}</template></MkInput>
-		<MkSwitch v-model="value.primary"><span>{{ $ts._pages.blocks._button.colored }}</span></MkSwitch>
-		<MkSelect v-model="value.action">
-			<template #label>{{ $ts._pages.blocks._button.action }}</template>
-			<option value="dialog">{{ $ts._pages.blocks._button._action.dialog }}</option>
-			<option value="resetRandom">{{ $ts._pages.blocks._button._action.resetRandom }}</option>
-			<option value="pushEvent">{{ $ts._pages.blocks._button._action.pushEvent }}</option>
-			<option value="callAiScript">{{ $ts._pages.blocks._button._action.callAiScript }}</option>
-		</MkSelect>
-		<template v-if="value.action === 'dialog'">
-			<MkInput v-model="value.content"><template #label>{{ $ts._pages.blocks._button._action._dialog.content }}</template></MkInput>
-		</template>
-		<template v-else-if="value.action === 'pushEvent'">
-			<MkInput v-model="value.event"><template #label>{{ $ts._pages.blocks._button._action._pushEvent.event }}</template></MkInput>
-			<MkInput v-model="value.message"><template #label>{{ $ts._pages.blocks._button._action._pushEvent.message }}</template></MkInput>
-			<MkSelect v-model="value.var">
-				<template #label>{{ $ts._pages.blocks._button._action._pushEvent.variable }}</template>
-				<option :value="null">{{ $t('_pages.blocks._button._action._pushEvent.no-variable') }}</option>
-				<option v-for="v in hpml.getVarsByType()" :value="v.name">{{ v.name }}</option>
-				<optgroup :label="$ts._pages.script.pageVariables">
-					<option v-for="v in hpml.getPageVarsByType()" :value="v">{{ v }}</option>
-				</optgroup>
-				<optgroup :label="$ts._pages.script.enviromentVariables">
-					<option v-for="v in hpml.getEnvVarsByType()" :value="v">{{ v }}</option>
-				</optgroup>
-			</MkSelect>
-		</template>
-		<template v-else-if="value.action === 'callAiScript'">
-			<MkInput v-model="value.fn"><template #label>{{ $ts._pages.blocks._button._action._callAiScript.functionName }}</template></MkInput>
-		</template>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkSelect from '@/components/form/select.vue';
-import MkInput from '@/components/form/input.vue';
-import MkSwitch from '@/components/form/switch.vue';
-
-withDefaults(defineProps<{
-	value: any,
-	hpml: any
-}>(), {
-	value: {
-		text: '',
-		action: 'dialog',
-		content: null,
-		event: null,
-		message: null,
-		primary: false,
-		var: null,
-		fn: null,
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.xfhsjczc {
-	padding: 0 16px 0 16px;
-}
-</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.canvas.vue b/packages/client/src/pages/page-editor/els/page-editor.el.canvas.vue
deleted file mode 100644
index 7ebf8c1478..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.canvas.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="fas fa-paint-brush"></i> {{ $ts._pages.blocks.canvas }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<MkInput v-model="value.name">
-			<template #prefix><i class="fas fa-magic"></i></template>
-			<template #label>{{ $ts._pages.blocks._canvas.id }}</template>
-		</MkInput>
-		<MkInput v-model="value.width" type="number">
-			<template #label>{{ $ts._pages.blocks._canvas.width }}</template>
-			<template #suffix>px</template>
-		</MkInput>
-		<MkInput v-model="value.height" type="number">
-			<template #label>{{ $ts._pages.blocks._canvas.height }}</template>
-			<template #suffix>px</template>
-		</MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkInput from '@/components/form/input.vue';
-
-withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		name: '',
-		width: 300,
-		height: 200,
-	},
-});
-</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.counter.vue b/packages/client/src/pages/page-editor/els/page-editor.el.counter.vue
deleted file mode 100644
index 994d0b5b00..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.counter.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ti ti-bolt"></i> {{ $ts._pages.blocks.counter }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<MkInput v-model="value.name">
-			<template #prefix><i class="fas fa-magic"></i></template>
-			<template #label>{{ $ts._pages.blocks._counter.name }}</template>
-		</MkInput>
-		<MkInput v-model="value.text">
-			<template #label>{{ $ts._pages.blocks._counter.text }}</template>
-		</MkInput>
-		<MkInput v-model="value.inc" type="number">
-			<template #label>{{ $ts._pages.blocks._counter.inc }}</template>
-		</MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkInput from '@/components/form/input.vue';
-
-withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		name: '',
-	},
-});
-</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.if.vue b/packages/client/src/pages/page-editor/els/page-editor.el.if.vue
deleted file mode 100644
index f56c8aec8a..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.if.vue
+++ /dev/null
@@ -1,67 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="fas fa-question"></i> {{ $ts._pages.blocks.if }}</template>
-	<template #func>
-		<button class="_button" @click="add()">
-			<i class="ti ti-plus"></i>
-		</button>
-	</template>
-
-	<section class="romcojzs">
-		<MkSelect v-model="value.var">
-			<template #label>{{ $ts._pages.blocks._if.variable }}</template>
-			<option v-for="v in hpml.getVarsByType('boolean')" :value="v.name">{{ v.name }}</option>
-			<optgroup :label="$ts._pages.script.pageVariables">
-				<option v-for="v in hpml.getPageVarsByType('boolean')" :value="v">{{ v }}</option>
-			</optgroup>
-			<optgroup :label="$ts._pages.script.enviromentVariables">
-				<option v-for="v in hpml.getEnvVarsByType('boolean')" :value="v">{{ v }}</option>
-			</optgroup>
-		</MkSelect>
-
-		<XBlocks v-model="value.children" class="children" :hpml="hpml"/>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { defineAsyncComponent, inject } from 'vue';
-import { v4 as uuid } from 'uuid';
-import XContainer from '../page-editor.container.vue';
-import MkSelect from '@/components/form/select.vue';
-import * as os from '@/os';
-import { i18n } from '@/i18n';
-
-const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
-
-const props = withDefaults(defineProps<{
-	value: any,
-	hpml: any
-}>(), {
-	value: {
-		children: [],
-		var: null,
-	},
-});
-
-const getPageBlockList = inject<(any) => any>('getPageBlockList');
-
-async function add() {
-	const { canceled, result: type } = await os.select({
-		title: i18n.ts._pages.chooseBlock,
-		groupedItems: getPageBlockList(),
-	});
-	if (canceled) return;
-
-	const id = uuid();
-	props.value.children.push({ id, type });
-}
-</script>
-
-<style lang="scss" scoped>
-.romcojzs {
-	padding: 0 16px 16px 16px;
-}
-</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
index 4b3c6cbf10..a84cb1e80e 100644
--- a/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.image.vue
@@ -21,29 +21,32 @@ import XContainer from '../page-editor.container.vue';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import * as os from '@/os';
 
-const props = withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		fileId: null,
-	},
-});
+const props = defineProps<{
+	modelValue: any
+}>();
+
+const emit = defineEmits<{
+	(ev: 'update:modelValue', value: any): void;
+}>();
 
 let file: any = $ref(null);
 
 async function choose() {
 	os.selectDriveFile(false).then((fileResponse: any) => {
 		file = fileResponse;
-		props.value.fileId = fileResponse.id;
+		emit('update:modelValue', {
+			...props.modelValue,
+			fileId: fileResponse.id,
+		});
 	});
 }
 
 onMounted(async () => {
-	if (props.value.fileId == null) {
+	if (props.modelValue.fileId == null) {
 		await choose();
 	} else {
 		os.api('drive/files/show', {
-			fileId: props.value.fileId,
+			fileId: props.modelValue.fileId,
 		}).then(fileResponse => {
 			file = fileResponse;
 		});
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.note.vue b/packages/client/src/pages/page-editor/els/page-editor.el.note.vue
index c3b3e3b37e..dcc1a32636 100644
--- a/packages/client/src/pages/page-editor/els/page-editor.el.note.vue
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.note.vue
@@ -8,10 +8,10 @@
 			<template #label>{{ $ts._pages.blocks._note.id }}</template>
 			<template #caption>{{ $ts._pages.blocks._note.idDescription }}</template>
 		</MkInput>
-		<MkSwitch v-model="value.detailed"><span>{{ $ts._pages.blocks._note.detailed }}</span></MkSwitch>
+		<MkSwitch v-model="props.modelValue.detailed"><span>{{ $ts._pages.blocks._note.detailed }}</span></MkSwitch>
 
-		<XNote v-if="note && !value.detailed" :key="note.id + ':normal'" v-model:note="note" style="margin-bottom: 16px;"/>
-		<XNoteDetailed v-if="note && value.detailed" :key="note.id + ':detail'" v-model:note="note" style="margin-bottom: 16px;"/>
+		<XNote v-if="note && !props.modelValue.detailed" :key="note.id + ':normal'" v-model:note="note" style="margin-bottom: 16px;"/>
+		<XNoteDetailed v-if="note && props.modelValue.detailed" :key="note.id + ':detail'" v-model:note="note" style="margin-bottom: 16px;"/>
 	</section>
 </XContainer>
 </template>
@@ -26,26 +26,31 @@ import XNote from '@/components/MkNote.vue';
 import XNoteDetailed from '@/components/MkNoteDetailed.vue';
 import * as os from '@/os';
 
-const props = withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		note: null,
-		detailed: false,
-	},
-});
+const props = defineProps<{
+	modelValue: any
+}>();
 
-let id: any = $ref(props.value.note);
+const emit = defineEmits<{
+	(ev: 'update:modelValue', value: any): void;
+}>();
+
+let id: any = $ref(props.modelValue.note);
 let note: any = $ref(null);
 
 watch(id, async () => {
 	if (id && (id.startsWith('http://') || id.startsWith('https://'))) {
-		props.value.note = (id.endsWith('/') ? id.slice(0, -1) : id).split('/').pop();
+		emit('update:modelValue', {
+			...props.modelValue,
+			note: (id.endsWith('/') ? id.slice(0, -1) : id).split('/').pop(),
+		});
 	} else {
-		props.value.note = id;
+		emit('update:modelValue', {
+			...props.modelValue,
+			note: id,
+		});
 	}
 
-	note = await os.api('notes/show', { noteId: props.value.note });
+	note = await os.api('notes/show', { noteId: props.modelValue.note });
 }, {
 	immediate: true,
 });
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.number-input.vue b/packages/client/src/pages/page-editor/els/page-editor.el.number-input.vue
deleted file mode 100644
index dc89d0287b..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.number-input.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ti ti-bolt"></i> {{ $ts._pages.blocks.numberInput }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<MkInput v-model="value.name">
-			<template #prefix><i class="fas fa-magic"></i></template>
-			<template #label>{{ $ts._pages.blocks._numberInput.name }}</template>
-		</MkInput>
-		<MkInput v-model="value.text">
-			<template #label>{{ $ts._pages.blocks._numberInput.text }}</template>
-		</MkInput>
-		<MkInput v-model="value.default" type="number">
-			<template #label>{{ $ts._pages.blocks._numberInput.default }}</template>
-		</MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkInput from '@/components/form/input.vue';
-
-withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		name: '',
-	},
-});
-</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.post.vue b/packages/client/src/pages/page-editor/els/page-editor.el.post.vue
deleted file mode 100644
index 9592d36829..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.post.vue
+++ /dev/null
@@ -1,31 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ti ti-send"></i> {{ $ts._pages.blocks.post }}</template>
-
-	<section style="padding: 16px;">
-		<MkTextarea v-model="value.text"><template #label>{{ $ts._pages.blocks._post.text }}</template></MkTextarea>
-		<MkSwitch v-model="value.attachCanvasImage"><span>{{ $ts._pages.blocks._post.attachCanvasImage }}</span></MkSwitch>
-		<MkInput v-if="value.attachCanvasImage" v-model="value.canvasId"><template #label>{{ $ts._pages.blocks._post.canvasId }}</template></MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkTextarea from '@/components/form/textarea.vue';
-import MkInput from '@/components/form/input.vue';
-import MkSwitch from '@/components/form/switch.vue';
-
-withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		text: '',
-		attachCanvasImage: false,
-		canvasId: '',
-	},
-});
-</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue b/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue
deleted file mode 100644
index 1aa2f5ca4b..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.radio-button.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ti ti-bolt"></i> {{ $ts._pages.blocks.radioButton }}</template>
-
-	<section style="padding: 0 16px 16px 16px;">
-		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._radioButton.name }}</template></MkInput>
-		<MkInput v-model="value.title"><template #label>{{ $ts._pages.blocks._radioButton.title }}</template></MkInput>
-		<MkTextarea v-model="values"><template #label>{{ $ts._pages.blocks._radioButton.values }}</template></MkTextarea>
-		<MkInput v-model="value.default"><template #label>{{ $ts._pages.blocks._radioButton.default }}</template></MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { watch } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkTextarea from '@/components/form/textarea.vue';
-import MkInput from '@/components/form/input.vue';
-
-const props = withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		name: '',
-		title: '',
-		values: [],
-	},
-});
-
-let values: string = $ref(props.value.values.join('\n'));
-
-watch(values, () => {
-	props.value.values = values.split('\n');
-}, {
-	deep: true,
-});
-</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.section.vue b/packages/client/src/pages/page-editor/els/page-editor.el.section.vue
index f18188ce61..fd1d3fa69c 100644
--- a/packages/client/src/pages/page-editor/els/page-editor.el.section.vue
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.section.vue
@@ -1,40 +1,51 @@
 <template>
 <!-- eslint-disable vue/no-mutating-props -->
 <XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="fas fa-sticky-note"></i> {{ value.title }}</template>
+	<template #header><i class="fas fa-sticky-note"></i> {{ props.modelValue.title }}</template>
 	<template #func>
 		<button class="_button" @click="rename()">
 			<i class="ti ti-pencil"></i>
 		</button>
-		<button class="_button" @click="add()">
-			<i class="ti ti-plus"></i>
-		</button>
 	</template>
 
 	<section class="ilrvjyvi">
-		<XBlocks v-model="value.children" class="children" :hpml="hpml"/>
+		<XBlocks v-model="children" class="children"/>
+		<MkButton rounded class="add" @click="add()"><i class="ti ti-plus"></i></MkButton>
 	</section>
 </XContainer>
 </template>
 
 <script lang="ts" setup>
 /* eslint-disable vue/no-mutating-props */
-import { defineAsyncComponent, inject, onMounted } from 'vue';
+import { defineAsyncComponent, inject, onMounted, watch } from 'vue';
 import { v4 as uuid } from 'uuid';
 import XContainer from '../page-editor.container.vue';
 import * as os from '@/os';
 import { i18n } from '@/i18n';
+import { deepClone } from '@/scripts/clone';
+import MkButton from '@/components/MkButton.vue';
 
 const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
 
 const props = withDefaults(defineProps<{
-	value: any,
-	hpml: any
+	modelValue: any,
 }>(), {
-	value: {
-		title: null,
-		children: [],
-	},
+	modelValue: {},
+});
+
+const emit = defineEmits<{
+	(ev: 'update:modelValue', value: any): void;
+}>();
+
+const children = $ref(deepClone(props.modelValue.children ?? []));
+
+watch($$(children), () => {
+	emit('update:modelValue', {
+		...props.modelValue,
+		children,
+	});
+}, {
+	deep: true,
 });
 
 const getPageBlockList = inject<(any) => any>('getPageBlockList');
@@ -42,25 +53,28 @@ const getPageBlockList = inject<(any) => any>('getPageBlockList');
 async function rename() {
 	const { canceled, result: title } = await os.inputText({
 		title: 'Enter title',
-		default: props.value.title,
+		default: props.modelValue.title,
 	});
 	if (canceled) return;
-	props.value.title = title;
+	emit('update:modelValue', {
+		...props.modelValue,
+		title,
+	});
 }
 
 async function add() {
 	const { canceled, result: type } = await os.select({
 		title: i18n.ts._pages.chooseBlock,
-		groupedItems: getPageBlockList(),
+		items: getPageBlockList(),
 	});
 	if (canceled) return;
 
 	const id = uuid();
-	props.value.children.push({ id, type });
+	children.push({ id, type });
 }
 
 onMounted(() => {
-	if (props.value.title == null) {
+	if (props.modelValue.title == null) {
 		rename();
 	}
 });
@@ -69,7 +83,15 @@ onMounted(() => {
 <style lang="scss" scoped>
 .ilrvjyvi {
 	> .children {
-		padding: 16px;
+		margin: 16px;
+
+		&:empty {
+			display: none;
+		}
+	}
+
+	> .add {
+		margin: 16px auto;
 	}
 }
 </style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.switch.vue b/packages/client/src/pages/page-editor/els/page-editor.el.switch.vue
deleted file mode 100644
index 96d35c3966..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.switch.vue
+++ /dev/null
@@ -1,34 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ti ti-bolt"></i> {{ $ts._pages.blocks.switch }}</template>
-
-	<section class="kjuadyyj">
-		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._switch.name }}</template></MkInput>
-		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._switch.text }}</template></MkInput>
-		<MkSwitch v-model="value.default"><span>{{ $ts._pages.blocks._switch.default }}</span></MkSwitch>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkSwitch from '@/components/form/switch.vue';
-import MkInput from '@/components/form/input.vue';
-
-withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		name: '',
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.kjuadyyj {
-	padding: 0 16px 16px 16px;
-}
-</style>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.text-input.vue b/packages/client/src/pages/page-editor/els/page-editor.el.text-input.vue
deleted file mode 100644
index 8cbae1caf5..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.text-input.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ti ti-bolt"></i> {{ $ts._pages.blocks.textInput }}</template>
-
-	<section style="padding: 0 16px 0 16px;">
-		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._textInput.name }}</template></MkInput>
-		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._textInput.text }}</template></MkInput>
-		<MkInput v-model="value.default" type="text"><template #label>{{ $ts._pages.blocks._textInput.default }}</template></MkInput>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkInput from '@/components/form/input.vue';
-
-withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		name: '',
-	},
-});
-</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.text.vue b/packages/client/src/pages/page-editor/els/page-editor.el.text.vue
index 27274253b4..6f11e2a08b 100644
--- a/packages/client/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/client/src/pages/page-editor/els/page-editor.el.text.vue
@@ -4,22 +4,31 @@
 	<template #header><i class="fas fa-align-left"></i> {{ $ts._pages.blocks.text }}</template>
 
 	<section class="vckmsadr">
-		<textarea v-model="value.text"></textarea>
+		<textarea v-model="text"></textarea>
 	</section>
 </XContainer>
 </template>
 
 <script lang="ts" setup>
 /* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
+import { watch } from 'vue';
 import XContainer from '../page-editor.container.vue';
 
-withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		text: '',
-	},
+const props = defineProps<{
+	modelValue: any
+}>();
+
+const emit = defineEmits<{
+	(ev: 'update:modelValue', value: any): void;
+}>();
+
+const text = $ref(props.modelValue.text ?? '');
+
+watch($$(text), () => {
+	emit('update:modelValue', {
+		...props.modelValue,
+		text,
+	});
 });
 </script>
 
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.textarea-input.vue b/packages/client/src/pages/page-editor/els/page-editor.el.textarea-input.vue
deleted file mode 100644
index 973f32242b..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.textarea-input.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="ti ti-bolt"></i> {{ $ts._pages.blocks.textareaInput }}</template>
-
-	<section style="padding: 0 16px 16px 16px;">
-		<MkInput v-model="value.name"><template #prefix><i class="fas fa-magic"></i></template><template #label>{{ $ts._pages.blocks._textareaInput.name }}</template></MkInput>
-		<MkInput v-model="value.text"><template #label>{{ $ts._pages.blocks._textareaInput.text }}</template></MkInput>
-		<MkTextarea v-model="value.default"><template #label>{{ $ts._pages.blocks._textareaInput.default }}</template></MkTextarea>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
-import XContainer from '../page-editor.container.vue';
-import MkTextarea from '@/components/form/textarea.vue';
-import MkInput from '@/components/form/input.vue';
-
-withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		name: '',
-	},
-});
-</script>
diff --git a/packages/client/src/pages/page-editor/els/page-editor.el.textarea.vue b/packages/client/src/pages/page-editor/els/page-editor.el.textarea.vue
deleted file mode 100644
index e0c05df911..0000000000
--- a/packages/client/src/pages/page-editor/els/page-editor.el.textarea.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :draggable="true" @remove="() => $emit('remove')">
-	<template #header><i class="fas fa-align-left"></i> {{ $ts._pages.blocks.textarea }}</template>
-
-	<section class="ihymsbbe">
-		<textarea v-model="value.text"></textarea>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts" setup>
-/* eslint-disable vue/no-mutating-props */
-import { } from 'vue';
-import XContainer from '../page-editor.container.vue';
-
-withDefaults(defineProps<{
-	value: any
-}>(), {
-	value: {
-		text: '',
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.ihymsbbe {
-	> textarea {
-		display: block;
-		-webkit-appearance: none;
-		-moz-appearance: none;
-		appearance: none;
-		width: 100%;
-		min-width: 100%;
-		min-height: 150px;
-		border: none;
-		box-shadow: none;
-		padding: 16px;
-		background: transparent;
-		color: var(--fg);
-		font-size: 14px;
-		box-sizing: border-box;
-	}
-}
-</style>
diff --git a/packages/client/src/pages/page-editor/page-editor.blocks.vue b/packages/client/src/pages/page-editor/page-editor.blocks.vue
index bc983cd2c8..f99fcb202f 100644
--- a/packages/client/src/pages/page-editor/page-editor.blocks.vue
+++ b/packages/client/src/pages/page-editor/page-editor.blocks.vue
@@ -1,7 +1,10 @@
 <template>
-<Sortable :list="blocks" tag="div" item-key="id" :options="{ handle: '.drag-handle', group: { name: 'blocks' }, animation: 150, swapThreshold: 0.5 }">
+<Sortable :model-value="modelValue" tag="div" item-key="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swap-threshold="0.5" @update:model-value="v => $emit('update:modelValue', v)">
 	<template #item="{element}">
-		<component :is="'x-' + element.type" :value="element" :hpml="hpml" @update:value="updateItem" @remove="() => removeItem(element)"/>
+		<div :class="$style.item">
+			<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
+			<component :is="'x-' + element.type" :model-value="element" @update:model-value="updateItem" @remove="() => removeItem(element)"/>
+		</div>
 	</template>
 </Sortable>
 </template>
@@ -10,25 +13,15 @@
 import { defineComponent, defineAsyncComponent } from 'vue';
 import XSection from './els/page-editor.el.section.vue';
 import XText from './els/page-editor.el.text.vue';
-import XTextarea from './els/page-editor.el.textarea.vue';
 import XImage from './els/page-editor.el.image.vue';
-import XButton from './els/page-editor.el.button.vue';
-import XTextInput from './els/page-editor.el.text-input.vue';
-import XTextareaInput from './els/page-editor.el.textarea-input.vue';
-import XNumberInput from './els/page-editor.el.number-input.vue';
-import XSwitch from './els/page-editor.el.switch.vue';
-import XIf from './els/page-editor.el.if.vue';
-import XPost from './els/page-editor.el.post.vue';
-import XCounter from './els/page-editor.el.counter.vue';
-import XRadioButton from './els/page-editor.el.radio-button.vue';
-import XCanvas from './els/page-editor.el.canvas.vue';
 import XNote from './els/page-editor.el.note.vue';
 import * as os from '@/os';
+import { deepClone } from '@/scripts/clone';
 
 export default defineComponent({
 	components: {
 		Sortable: defineAsyncComponent(() => import('vuedraggable').then(x => x.default)),
-		XSection, XText, XImage, XButton, XTextarea, XTextInput, XTextareaInput, XNumberInput, XSwitch, XIf, XPost, XCounter, XRadioButton, XCanvas, XNote,
+		XSection, XText, XImage, XNote,
 	},
 
 	props: {
@@ -36,43 +29,37 @@ export default defineComponent({
 			type: Array,
 			required: true,
 		},
-		hpml: {
-			required: true,
-		},
 	},
 
 	emits: ['update:modelValue'],
 
-	computed: {
-		blocks: {
-			get() {
-				return this.modelValue;
-			},
-			set(value) {
-				this.$emit('update:modelValue', value);
-			},
-		},
-	},
-
 	methods: {
 		updateItem(v) {
-			const i = this.blocks.findIndex(x => x.id === v.id);
+			const i = this.modelValue.findIndex(x => x.id === v.id);
 			const newValue = [
-				...this.blocks.slice(0, i),
+				...this.modelValue.slice(0, i),
 				v,
-				...this.blocks.slice(i + 1),
+				...this.modelValue.slice(i + 1),
 			];
 			this.$emit('update:modelValue', newValue);
 		},
 
 		removeItem(el) {
-			const i = this.blocks.findIndex(x => x.id === el.id);
+			const i = this.modelValue.findIndex(x => x.id === el.id);
 			const newValue = [
-				...this.blocks.slice(0, i),
-				...this.blocks.slice(i + 1),
+				...this.modelValue.slice(0, i),
+				...this.modelValue.slice(i + 1),
 			];
 			this.$emit('update:modelValue', newValue);
 		},
 	},
 });
 </script>
+
+<style lang="scss" module>
+.item {
+	& + .item {
+		margin-top: 16px;
+	}
+}
+</style>
diff --git a/packages/client/src/pages/page-editor/page-editor.container.vue b/packages/client/src/pages/page-editor/page-editor.container.vue
index 03228b8e01..15cdda5efb 100644
--- a/packages/client/src/pages/page-editor/page-editor.container.vue
+++ b/packages/client/src/pages/page-editor/page-editor.container.vue
@@ -74,7 +74,7 @@ export default defineComponent({
 	overflow: hidden;
 	background: var(--panel);
 	border: solid 2px var(--X12);
-	border-radius: 6px;
+	border-radius: 8px;
 
 	&:hover {
 		border: solid 2px var(--X13);
@@ -88,10 +88,6 @@ export default defineComponent({
 		border: solid 2px #f00;
 	}
 
-	& + .cpjygsrt {
-		margin-top: 16px;
-	}
-
 	> header {
 		> .title {
 			z-index: 1;
diff --git a/packages/client/src/pages/page-editor/page-editor.script-block.vue b/packages/client/src/pages/page-editor/page-editor.script-block.vue
deleted file mode 100644
index 1f621e185f..0000000000
--- a/packages/client/src/pages/page-editor/page-editor.script-block.vue
+++ /dev/null
@@ -1,279 +0,0 @@
-<template>
-<!-- eslint-disable vue/no-mutating-props -->
-<XContainer :removable="removable" :error="error" :warn="warn" :draggable="draggable" @remove="() => $emit('remove')">
-	<template #header><i v-if="icon" :class="icon"></i> <template v-if="title">{{ title }} <span v-if="typeText" class="turmquns">({{ typeText }})</span></template><template v-else-if="typeText">{{ typeText }}</template></template>
-	<template #func>
-		<button class="_button" @click="changeType()">
-			<i class="ti ti-pencil"></i>
-		</button>
-	</template>
-
-	<section v-if="modelValue.type === null" class="pbglfege" @click="changeType()">
-		{{ $ts._pages.script.emptySlot }}
-	</section>
-	<section v-else-if="modelValue.type === 'text'" class="tbwccoaw">
-		<input v-model="modelValue.value"/>
-	</section>
-	<section v-else-if="modelValue.type === 'multiLineText'" class="tbwccoaw">
-		<textarea v-model="modelValue.value"></textarea>
-	</section>
-	<section v-else-if="modelValue.type === 'textList'" class="tbwccoaw">
-		<textarea v-model="modelValue.value" :placeholder="$ts._pages.script.blocks._textList.info"></textarea>
-	</section>
-	<section v-else-if="modelValue.type === 'number'" class="tbwccoaw">
-		<input v-model="modelValue.value" type="number"/>
-	</section>
-	<section v-else-if="modelValue.type === 'ref'" class="hpdwcrvs">
-		<select v-model="modelValue.value">
-			<option v-for="v in hpml.getVarsByType(getExpectedType ? getExpectedType() : null).filter(x => x.name !== name)" :value="v.name">{{ v.name }}</option>
-			<optgroup :label="$ts._pages.script.argVariables">
-				<option v-for="v in fnSlots" :value="v.name">{{ v.name }}</option>
-			</optgroup>
-			<optgroup :label="$ts._pages.script.pageVariables">
-				<option v-for="v in hpml.getPageVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
-			</optgroup>
-			<optgroup :label="$ts._pages.script.enviromentVariables">
-				<option v-for="v in hpml.getEnvVarsByType(getExpectedType ? getExpectedType() : null)" :value="v">{{ v }}</option>
-			</optgroup>
-		</select>
-	</section>
-	<section v-else-if="modelValue.type === 'aiScriptVar'" class="tbwccoaw">
-		<input v-model="modelValue.value"/>
-	</section>
-	<section v-else-if="modelValue.type === 'fn'" class="" style="padding:0 16px 16px 16px;">
-		<MkTextarea v-model="slots">
-			<template #label>{{ $ts._pages.script.blocks._fn.slots }}</template>
-			<template #caption>{{ $t('_pages.script.blocks._fn.slots-info') }}</template>
-		</MkTextarea>
-		<XV v-if="modelValue.value.expression" v-model="modelValue.value.expression" :title="$t(`_pages.script.blocks._fn.arg1`)" :get-expected-type="() => null" :hpml="hpml" :fn-slots="modelValue.value.slots" :name="name"/>
-	</section>
-	<section v-else-if="modelValue.type.startsWith('fn:')" class="" style="padding:16px;">
-		<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="hpml.getVarByName(modelValue.type.split(':')[1]).value.slots[i].name" :get-expected-type="() => null" :hpml="hpml" :name="name"/>
-	</section>
-	<section v-else class="" style="padding:16px;">
-		<XV v-for="(x, i) in modelValue.args" :key="i" v-model="modelValue.args[i]" :title="$t(`_pages.script.blocks._${modelValue.type}.arg${i + 1}`)" :get-expected-type="() => _getExpectedType(i)" :hpml="hpml" :name="name" :fn-slots="fnSlots"/>
-	</section>
-</XContainer>
-</template>
-
-<script lang="ts">
-/* eslint-disable vue/no-mutating-props */
-import { defineAsyncComponent, defineComponent } from 'vue';
-import { v4 as uuid } from 'uuid';
-import XContainer from './page-editor.container.vue';
-import MkTextarea from '@/components/form/textarea.vue';
-import { blockDefs } from '@/scripts/hpml/index';
-import * as os from '@/os';
-import { isLiteralValue } from '@/scripts/hpml/expr';
-import { funcDefs } from '@/scripts/hpml/lib';
-
-export default defineComponent({
-	components: {
-		XContainer, MkTextarea,
-		XV: defineAsyncComponent(() => import('./page-editor.script-block.vue')),
-	},
-
-	inject: ['getScriptBlockList'],
-
-	props: {
-		getExpectedType: {
-			required: false,
-			default: null,
-		},
-		modelValue: {
-			required: true,
-		},
-		title: {
-			required: false,
-		},
-		removable: {
-			required: false,
-			default: false,
-		},
-		hpml: {
-			required: true,
-		},
-		name: {
-			required: true,
-		},
-		fnSlots: {
-			required: false,
-		},
-		draggable: {
-			required: false,
-			default: false,
-		},
-	},
-
-	data() {
-		return {
-			error: null,
-			warn: null,
-			slots: '',
-		};
-	},
-
-	computed: {
-		icon(): any {
-			if (this.modelValue.type === null) return null;
-			if (this.modelValue.type.startsWith('fn:')) return 'ti ti-plug';
-			return blockDefs.find(x => x.type === this.modelValue.type).icon;
-		},
-		typeText(): any {
-			if (this.modelValue.type === null) return null;
-			if (this.modelValue.type.startsWith('fn:')) return this.modelValue.type.split(':')[1];
-			return this.$t(`_pages.script.blocks.${this.modelValue.type}`);
-		},
-	},
-
-	watch: {
-		slots: {
-			handler() {
-				this.modelValue.value.slots = this.slots.split('\n').map(x => ({
-					name: x,
-					type: null,
-				}));
-			},
-			deep: true,
-		},
-	},
-
-	created() {
-		if (this.modelValue.value == null) this.modelValue.value = null;
-
-		if (this.modelValue.value && this.modelValue.value.slots) this.slots = this.modelValue.value.slots.map(x => x.name).join('\n');
-
-		this.$watch(() => this.modelValue.type, (t) => {
-			this.warn = null;
-
-			if (this.modelValue.type === 'fn') {
-				const id = uuid();
-				this.modelValue.value = {
-					slots: [],
-					expression: { id, type: null },
-				};
-				return;
-			}
-
-			if (this.modelValue.type && this.modelValue.type.startsWith('fn:')) {
-				const fnName = this.modelValue.type.split(':')[1];
-				const fn = this.hpml.getVarByName(fnName);
-
-				const empties = [];
-				for (let i = 0; i < fn.value.slots.length; i++) {
-					const id = uuid();
-					empties.push({ id, type: null });
-				}
-				this.modelValue.args = empties;
-				return;
-			}
-
-			if (isLiteralValue(this.modelValue)) return;
-
-			const empties = [];
-			for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
-				const id = uuid();
-				empties.push({ id, type: null });
-			}
-			this.modelValue.args = empties;
-
-			for (let i = 0; i < funcDefs[this.modelValue.type].in.length; i++) {
-				const inType = funcDefs[this.modelValue.type].in[i];
-				if (typeof inType !== 'number') {
-					if (inType === 'number') this.modelValue.args[i].type = 'number';
-					if (inType === 'string') this.modelValue.args[i].type = 'text';
-				}
-			}
-		});
-
-		this.$watch(() => this.modelValue.args, (args) => {
-			if (args == null) {
-				this.warn = null;
-				return;
-			}
-			const emptySlotIndex = args.findIndex(x => x.type === null);
-			if (emptySlotIndex !== -1 && emptySlotIndex < args.length) {
-				this.warn = {
-					slot: emptySlotIndex,
-				};
-			} else {
-				this.warn = null;
-			}
-		}, {
-			deep: true,
-		});
-
-		this.$watch(() => this.hpml.variables, () => {
-			if (this.type != null && this.modelValue) {
-				this.error = this.hpml.typeCheck(this.modelValue);
-			}
-		}, {
-			deep: true,
-		});
-	},
-
-	methods: {
-		async changeType() {
-			const { canceled, result: type } = await os.select({
-				title: this.$ts._pages.selectType,
-				groupedItems: this.getScriptBlockList(this.getExpectedType ? this.getExpectedType() : null),
-			});
-			if (canceled) return;
-			this.modelValue.type = type;
-		},
-
-		_getExpectedType(slot: number) {
-			return this.hpml.getExpectedType(this.modelValue, slot);
-		},
-	},
-});
-</script>
-
-<style lang="scss" scoped>
-.turmquns {
-	opacity: 0.7;
-}
-
-.pbglfege {
-	opacity: 0.5;
-	padding: 16px;
-	text-align: center;
-	cursor: pointer;
-	color: var(--fg);
-}
-
-.tbwccoaw {
-	> input,
-	> textarea {
-		display: block;
-		-webkit-appearance: none;
-		-moz-appearance: none;
-		appearance: none;
-		width: 100%;
-		max-width: 100%;
-		min-width: 100%;
-		border: none;
-		box-shadow: none;
-		padding: 16px;
-		font-size: 16px;
-		background: transparent;
-		color: var(--fg);
-		box-sizing: border-box;
-	}
-
-	> textarea {
-		min-height: 100px;
-	}
-}
-
-.hpdwcrvs {
-	padding: 16px;
-
-	> select {
-		display: block;
-		padding: 4px;
-		font-size: 16px;
-		width: 100%;
-	}
-}
-</style>
diff --git a/packages/client/src/pages/page-editor/page-editor.vue b/packages/client/src/pages/page-editor/page-editor.vue
index f094de0cbc..f7740545d1 100644
--- a/packages/client/src/pages/page-editor/page-editor.vue
+++ b/packages/client/src/pages/page-editor/page-editor.vue
@@ -45,36 +45,10 @@
 		</div>
 
 		<div v-else-if="tab === 'contents'">
-			<div>
-				<XBlocks v-model="content" class="content" :hpml="hpml"/>
+			<div :class="$style.contents">
+				<XBlocks v-model="content" class="content"/>
 
-				<MkButton v-if="!readonly" @click="add()"><i class="ti ti-plus"></i></MkButton>
-			</div>
-		</div>
-
-		<div v-else-if="tab === 'variables'">
-			<div class="qmuvgica">
-				<Sortable v-show="variables.length > 0" v-model="variables" tag="div" class="variables" item-key="name" handle=".drag-handle" :group="{ name: 'variables' }" animation="150" swap-threshold="0.5">
-					<template #item="{element}">
-						<XVariable
-							:model-value="element"
-							:removable="true"
-							:hpml="hpml"
-							:name="element.name"
-							:title="element.name"
-							:draggable="true"
-							@remove="() => removeVariable(element)"
-						/>
-					</template>
-				</Sortable>
-
-				<MkButton v-if="!readonly" class="add" @click="addVariable()"><i class="ti ti-plus"></i></MkButton>
-			</div>
-		</div>
-
-		<div v-else-if="tab === 'script'">
-			<div>
-				<MkTextarea v-model="script" class="_code"/>
+				<MkButton v-if="!readonly" rounded class="add" @click="add()"><i class="ti ti-plus"></i></MkButton>
 			</div>
 		</div>
 	</MkSpacer>
@@ -83,31 +57,20 @@
 
 <script lang="ts" setup>
 import { defineAsyncComponent, computed, provide, watch } from 'vue';
-import 'prismjs';
-import { highlight, languages } from 'prismjs/components/prism-core';
-import 'prismjs/components/prism-clike';
-import 'prismjs/components/prism-javascript';
-import 'prismjs/themes/prism-okaidia.css';
-import 'vue-prism-editor/dist/prismeditor.min.css';
 import { v4 as uuid } from 'uuid';
-import XVariable from './page-editor.script-block.vue';
 import XBlocks from './page-editor.blocks.vue';
 import MkTextarea from '@/components/form/textarea.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkSelect from '@/components/form/select.vue';
 import MkSwitch from '@/components/form/switch.vue';
 import MkInput from '@/components/form/input.vue';
-import { blockDefs } from '@/scripts/hpml/index';
-import { HpmlTypeChecker } from '@/scripts/hpml/type-checker';
 import { url } from '@/config';
-import { collectPageVars } from '@/scripts/collect-page-vars';
 import * as os from '@/os';
 import { selectFile } from '@/scripts/select-file';
 import { mainRouter } from '@/router';
 import { i18n } from '@/i18n';
 import { definePageMetadata } from '@/scripts/page-metadata';
 import { $i } from '@/account';
-const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
 const props = defineProps<{
 	initPageId?: string;
@@ -130,12 +93,8 @@ let font = $ref('sans-serif');
 let content = $ref([]);
 let alignCenter = $ref(false);
 let hideTitleWhenPinned = $ref(false);
-let variables = $ref([]);
-let hpml = $ref(null);
-let script = $ref('');
 
 provide('readonly', readonly);
-provide('getScriptBlockList', getScriptBlockList);
 provide('getPageBlockList', getPageBlockList);
 
 watch($$(eyeCatchingImageId), async () => {
@@ -154,11 +113,11 @@ function getSaveOptions() {
 		name: name.trim(),
 		summary: summary,
 		font: font,
-		script: script,
+		script: '',
 		hideTitleWhenPinned: hideTitleWhenPinned,
 		alignCenter: alignCenter,
 		content: content,
-		variables: variables,
+		variables: [],
 		eyeCatchingImageId: eyeCatchingImageId,
 	};
 }
@@ -243,7 +202,7 @@ async function add() {
 	const { canceled, result: type } = await os.select({
 		type: null,
 		title: i18n.ts._pages.chooseBlock,
-		groupedItems: getPageBlockList(),
+		items: getPageBlockList(),
 	});
 	if (canceled) return;
 
@@ -251,97 +210,13 @@ async function add() {
 	content.push({ id, type });
 }
 
-async function addVariable() {
-	let { canceled, result: name } = await os.inputText({
-		title: i18n.ts._pages.enterVariableName,
-	});
-	if (canceled) return;
-
-	name = name.trim();
-
-	if (hpml.isUsedName(name)) {
-		os.alert({
-			type: 'error',
-			text: i18n.ts._pages.variableNameIsAlreadyUsed,
-		});
-		return;
-	}
-
-	const id = uuid();
-	variables.push({ id, name, type: null });
-}
-
-function removeVariable(v) {
-	variables = variables.filter(x => x.name !== v.name);
-}
-
 function getPageBlockList() {
-	return [{
-		label: i18n.ts._pages.contentBlocks,
-		items: [
-			{ value: 'section', text: i18n.ts._pages.blocks.section },
-			{ value: 'text', text: i18n.ts._pages.blocks.text },
-			{ value: 'image', text: i18n.ts._pages.blocks.image },
-			{ value: 'textarea', text: i18n.ts._pages.blocks.textarea },
-			{ value: 'note', text: i18n.ts._pages.blocks.note },
-			{ value: 'canvas', text: i18n.ts._pages.blocks.canvas },
-		],
-	}, {
-		label: i18n.ts._pages.inputBlocks,
-		items: [
-			{ value: 'button', text: i18n.ts._pages.blocks.button },
-			{ value: 'radioButton', text: i18n.ts._pages.blocks.radioButton },
-			{ value: 'textInput', text: i18n.ts._pages.blocks.textInput },
-			{ value: 'textareaInput', text: i18n.ts._pages.blocks.textareaInput },
-			{ value: 'numberInput', text: i18n.ts._pages.blocks.numberInput },
-			{ value: 'switch', text: i18n.ts._pages.blocks.switch },
-			{ value: 'counter', text: i18n.ts._pages.blocks.counter },
-		],
-	}, {
-		label: i18n.ts._pages.specialBlocks,
-		items: [
-			{ value: 'if', text: i18n.ts._pages.blocks.if },
-			{ value: 'post', text: i18n.ts._pages.blocks.post },
-		],
-	}];
-}
-
-function getScriptBlockList(type: string = null) {
-	const list = [];
-
-	const blocks = blockDefs.filter(block => type == null || block.out == null || block.out === type || typeof block.out === 'number');
-
-	for (const block of blocks) {
-		const category = list.find(x => x.category === block.category);
-		if (category) {
-			category.items.push({
-				value: block.type,
-				text: i18n.t(`_pages.script.blocks.${block.type}`),
-			});
-		} else {
-			list.push({
-				category: block.category,
-				label: i18n.t(`_pages.script.categories.${block.category}`),
-				items: [{
-					value: block.type,
-					text: i18n.t(`_pages.script.blocks.${block.type}`),
-				}],
-			});
-		}
-	}
-
-	const userFns = variables.filter(x => x.type === 'fn');
-	if (userFns.length > 0) {
-		list.unshift({
-			label: i18n.t('_pages.script.categories.fn'),
-			items: userFns.map(v => ({
-				value: 'fn:' + v.name,
-				text: v.name,
-			})),
-		});
-	}
-
-	return list;
+	return [
+		{ value: 'section', text: i18n.ts._pages.blocks.section },
+		{ value: 'text', text: i18n.ts._pages.blocks.text },
+		{ value: 'image', text: i18n.ts._pages.blocks.image },
+		{ value: 'note', text: i18n.ts._pages.blocks.note },
+	];
 }
 
 function setEyeCatchingImage(img) {
@@ -354,21 +229,7 @@ function removeEyeCatchingImage() {
 	eyeCatchingImageId = null;
 }
 
-function highlighter(code) {
-	return highlight(code, languages.js, 'javascript');
-}
-
 async function init() {
-	hpml = new HpmlTypeChecker();
-
-	watch($$(variables), () => {
-		hpml.variables = variables;
-	}, { deep: true });
-
-	watch($$(content), () => {
-		hpml.pageVars = collectPageVars(content);
-	}, { deep: true });
-
 	if (props.initPageId) {
 		page = await os.api('pages/show', {
 			pageId: props.initPageId,
@@ -389,11 +250,9 @@ async function init() {
 		currentName = page.name;
 		summary = page.summary;
 		font = page.font;
-		script = page.script;
 		hideTitleWhenPinned = page.hideTitleWhenPinned;
 		alignCenter = page.alignCenter;
 		content = page.content;
-		variables = page.variables;
 		eyeCatchingImageId = page.eyeCatchingImageId;
 	} else {
 		const id = uuid();
@@ -417,14 +276,6 @@ const headerTabs = $computed(() => [{
 	key: 'contents',
 	title: i18n.ts._pages.contents,
 	icon: 'fas fa-sticky-note',
-}, {
-	key: 'variables',
-	title: i18n.ts._pages.variables,
-	icon: 'fas fa-magic',
-}, {
-	key: 'script',
-	title: i18n.ts.script,
-	icon: 'ti ti-code',
 }]);
 
 definePageMetadata(computed(() => {
@@ -442,8 +293,20 @@ definePageMetadata(computed(() => {
 }));
 </script>
 
+<style lang="scss" module>
+.contents {
+	&:global {
+		> .add {
+			margin: 16px auto 0 auto;
+		}
+	}
+}
+</style>
+
 <style lang="scss" scoped>
 .jqqmcavi {
+	margin-bottom: 16px;
+
 	> .button {
 		& + .button {
 			margin-left: 8px;
diff --git a/packages/client/src/pages/page.vue b/packages/client/src/pages/page.vue
index 09d92d72ab..7072b8ef03 100644
--- a/packages/client/src/pages/page.vue
+++ b/packages/client/src/pages/page.vue
@@ -175,6 +175,7 @@ definePageMetadata(computed(() => page ? {
 
 .xcukqgmh {
 	> .main {
+		padding: 32px;
 
 		> .header {
 			padding: 16px;