diff --git a/content/.yaml b/content/.yaml
new file mode 100644
index 0000000..604c82e
--- /dev/null
+++ b/content/.yaml
@@ -0,0 +1,8 @@
+meta:
+    title: Typemill
+    description: 'Typemill is a user-friendly and lightweight open source CMS for publishing text-works like prose, lyrics, manuals, documentations, studies and more. Just download and start.'
+    author: 'Sebastian Schürmanns'
+    created: '2020-04-27'
+    time: 14-20-18
+    navtitle: null
+    modified: '2020-04-27'
diff --git a/media/files/neue-datei.tx6t b/media/files/neue-datei.tx6t
new file mode 100644
index 0000000..01dba91
--- /dev/null
+++ b/media/files/neue-datei.tx6t
@@ -0,0 +1,13 @@
+Hallo Frau Zarth, 
+
+vielleicht sagt Ihnen als Leiterin Com & Tech die Fachseite CMSstash.de etwas? Wir behandeln ausschließlich CMS und bieten seit letzter Woche auch ein Dienstleisterverzeichnis an. Ist das interessant für Nexum als Tech-Partner von Magnolia, Typo3 und Co? 
+
+Herzliche Grüße
+
+
+Hallo Herr Timm,
+
+ich kenne unitb vor allem als Spezialist für AEM, Magnolia oder Drupal. Vielleicht ist für Ihre Agentur die wachsende Zielgruppe von CMSstash interessant?  Wir bieten seit letzter Woche ein Dienstleisterverzeichnis an, in dem sich CMS-Experten präsentieren können.
+
+Herzliche Grüße
+octet-stream
\ No newline at end of file
diff --git a/system/Controllers/ArticleApiController.php b/system/Controllers/ArticleApiController.php
index 034260a..1031ab7 100644
--- a/system/Controllers/ArticleApiController.php
+++ b/system/Controllers/ArticleApiController.php
@@ -84,9 +84,9 @@ class ArticleApiController extends ContentController
 
 			# dispatch event
 			$page = ['content' => $this->content, 'meta' => $meta, 'item' => $this->item];
-			$this->c->dispatcher->dispatch('onPagePublished', new OnPagePublished($page));
+			$page = $this->c->dispatcher->dispatch('onPagePublished', new OnPagePublished($page))->getData();
 
-			return $response->withJson(['success' => true, 'meta' => $meta], 200);
+			return $response->withJson(['success' => true, 'meta' => $page['meta']], 200);
 		}
 		else
 		{
diff --git a/system/Controllers/MediaApiController.php b/system/Controllers/MediaApiController.php
index 5b0ec9b..0cccfde 100644
--- a/system/Controllers/MediaApiController.php
+++ b/system/Controllers/MediaApiController.php
@@ -108,6 +108,11 @@ class MediaApiController extends ContentController
 
 		if($imageProcessor->createImage($this->params['image'], $this->params['name'], $this->settings['images']))
 		{
+			# publish image directly, used for example by image field for meta-tabs
+			if($this->params['publish'])
+			{
+				$imageProcessor->publishImage();
+			}
 			return $response->withJson(['name' => 'media/live/' . $imageProcessor->getFullName(),'errors' => false]);	
 		}
 
diff --git a/system/Controllers/MetaApiController.php b/system/Controllers/MetaApiController.php
index 30c6df9..b05dffc 100644
--- a/system/Controllers/MetaApiController.php
+++ b/system/Controllers/MetaApiController.php
@@ -272,10 +272,10 @@ class MetaApiController extends ContentController
 		}
 
 		# add the new/edited metadata
-		$meta[$tab] = $metaInput;
+		$metaPage[$tab] = $metaInput;
 
 		# store the metadata
-		$writeMeta->updateYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml', $meta);
+		$writeMeta->updateYaml($this->settings['contentFolder'], $this->item->pathWithoutType . '.yaml', $metaPage);
 
 		if($structure)
 		{
diff --git a/system/Models/ProcessImage.php b/system/Models/ProcessImage.php
index e6a1d18..1f70938 100644
--- a/system/Models/ProcessImage.php
+++ b/system/Models/ProcessImage.php
@@ -333,6 +333,8 @@ class ProcessImage extends ProcessAssets
 		# generate images from live folder to 'tmthumbs'
 		$liveImages 	= scandir($this->liveFolder);
 
+		$result = false;
+
 		foreach ($liveImages as $key => $name)
 		{
 			if (!in_array($name, array(".","..")))
diff --git a/system/Models/Validation.php b/system/Models/Validation.php
index d5ed19a..be68110 100644
--- a/system/Models/Validation.php
+++ b/system/Models/Validation.php
@@ -342,7 +342,7 @@ class Validation
 	*/
 	
 	public function objectField($fieldName, $fieldValue, $objectName, $fieldDefinitions, $skiprequired = NULL)
-	{	
+	{
 		$v = new Validator(array($fieldName => $fieldValue));
 		
 		if(isset($fieldDefinitions['required']) && !$skiprequired)
@@ -412,8 +412,16 @@ class Validation
 #				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-\.\?\!\/\:]*$/u');
 				break;
 			case "textarea":
-				$v->rule('noHTML', $fieldName);
-				$v->rule('lengthMax', $fieldName, 1000);
+				# it understands array, json, yaml
+				if(is_array($fieldValue))
+				{
+					$v = $this->checkArray($fieldValue, $v);
+				}
+				else
+				{
+					$v->rule('noHTML', $fieldName);
+					$v->rule('lengthMax', $fieldName, 1000);
+				}
 				break;
 			case "paragraph":
 				$v->rule('noHTML', $fieldName);
@@ -422,9 +430,13 @@ class Validation
 			case "password":
 				$v->rule('lengthMax', $fieldName, 100);
 				break;
+			case "image":
+				$v->rule('noHTML', $fieldName);
+				$v->rule('lengthMax', $fieldName, 1000);
+				break;
 			default:
 				$v->rule('lengthMax', $fieldName, 1000);
-				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-]*$/u');		
+				$v->rule('regex', $fieldName, '/^[\pL0-9_ \-]*$/u');
 		}
 		return $this->validationResult($v, $objectName);
 	}
@@ -435,6 +447,20 @@ class Validation
 	* @param obj $v the validation object.
 	* @return bool
 	*/
+
+	public function checkArray($arrayvalues, $v)
+	{		
+		foreach($arrayvalues as $key => $value)
+		{
+			if(is_array($value))
+			{
+				$this->checkArray($value, $v);
+			}
+			$v->rule('noHTML', $value);
+			$v->rule('lengthMax', $value, 1000);
+		}
+		return $v;
+	}
 	
 	public function validationResult($v, $name = false)
 	{
diff --git a/system/author/css/style.css b/system/author/css/style.css
index b4f6c00..a4f02ff 100644
--- a/system/author/css/style.css
+++ b/system/author/css/style.css
@@ -141,7 +141,7 @@ a.tm-download::before{
 	width: 30px;
 	height: 30px;
 	line-height: 30px;
-	font-family: "Comic Sans MS",cursive,sans-serif;
+	font-family: Calibri, "Segoe UI", Roboto, Courier, Helvetica, -apple-system, BlinkMacSystemFont, sans-serif, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 	font-size: 1.3em;
 	font-weight: 900;	
 	border: 2px solid #e0474c;
diff --git a/system/author/js/vue-blox.js b/system/author/js/vue-blox.js
index e7b4139..d174419 100644
--- a/system/author/js/vue-blox.js
+++ b/system/author/js/vue-blox.js
@@ -2073,7 +2073,7 @@ const medialib = Vue.component('medialib', {
 
 				this.$parent.showmedialib = false;
 
-				this.$parent.updatemarkdown(imgmarkdown);
+				this.$parent.updatemarkdown(imgmarkdown, image.src_live);
 			}
 			if(this.parentcomponent == 'files')
 			{
@@ -2084,7 +2084,7 @@ const medialib = Vue.component('medialib', {
 
 				this.$parent.showmedialib = false;
 
-				this.$parent.updatemarkdown(filemarkdown);
+				this.$parent.updatemarkdown(filemarkdown, image.src_live);
 			}
 		},
 		selectFile: function(file)
@@ -2106,7 +2106,7 @@ const medialib = Vue.component('medialib', {
 
 				this.$parent.showmedialib = false;
 
-				this.$parent.updatemarkdown(imgmarkdown);				
+				this.$parent.updatemarkdown(imgmarkdown, file.url);
 			}
 			if(this.parentcomponent == 'files')
 			{
@@ -2117,7 +2117,7 @@ const medialib = Vue.component('medialib', {
 				this.$parent.filemeta = true;
 				this.$parent.filetitle = file.info.filename + ' (' + file.info.extension.toUpperCase() + ')';
 
-				this.$parent.updatemarkdown(filemarkdown);
+				this.$parent.updatemarkdown(filemarkdown, file.url);
 			}
 			this.showFiles();
 		},		
diff --git a/system/author/js/vue-meta.js b/system/author/js/vue-meta.js
index e544997..35c5f6b 100644
--- a/system/author/js/vue-meta.js
+++ b/system/author/js/vue-meta.js
@@ -2,12 +2,12 @@ const FormBus = new Vue();
 
 Vue.filter('translate', function (value) {
   if (!value) return ''
-  value = value.replace(/[ ]/g,"_").replace(/[.]/g, "_").replace(/[-]/g, "_").replace(/[,]/g,"_").replace(/[(]/g,"_").replace(/[)]/g,"_").toUpperCase()
-  translated_string = labels[value]
+  transvalue = value.replace(/[ ]/g,"_").replace(/[.]/g, "_").replace(/[-]/g, "_").replace(/[,]/g,"_").replace(/[(]/g,"_").replace(/[)]/g,"_").toUpperCase()
+  translated_string = labels[transvalue]
   if(!translated_string || translated_string.length === 0){
-    return value + '?'
+    return value
   } else {
-    return labels[value]
+    return labels[transvalue]
   }
 })
 
@@ -37,18 +37,42 @@ Vue.component('component-text', {
 	},
 })
 
+Vue.component('component-hidden', {
+	props: ['class', 'id', 'maxlength', 'required', 'disabled', 'name', 'type', 'value', 'errors'],
+	template: '<div class="hidden">' +
+				'<input type="hidden"' + 
+					' :id="id"' +
+					' :maxlength="maxlength"' +
+					' :name="name"' +
+					' :value="value"' +
+					'@input="update($event, name)">' +
+			  '</div>',
+	methods: {
+		update: function($event, name)
+		{
+			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
+		},
+	},
+})
+
 Vue.component('component-textarea', {
 	props: ['class', 'id', 'description', 'maxlength', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	data: function () {
+		return {
+			textareaclass: ''
+		 }
+	},
 	template: '<div class="large">' +
 				'<label>{{ label|translate }}</label>' +
-				'<textarea ' +
+				'<textarea rows="8" ' +
 					' :id="id"' +
+					' :class="textareaclass"' +
 					' :readonly="readonly"' +
 					' :required="required"' +  
 					' :disabled="disabled"' +  
 					' :name="name"' +
 					' :placeholder="placeholder"' +
-					' :value="value"' +
+					' :value="formatValue(value)"' +
 					' @input="update($event, name)"></textarea>' +
 			  	'<span v-if="errors[name]" class="error">{{ errors[name] }}</span>' +
 			  	'<span v-else class="fielddescription"><small>{{ description|translate }}</small></span>' +
@@ -58,6 +82,15 @@ Vue.component('component-textarea', {
 		{
 			FormBus.$emit('forminput', {'name': name, 'value' : $event.target.value});
 		},
+		formatValue: function(value)
+		{
+			if(value !== null && typeof value === 'object')
+			{
+				this.textareaclass = 'codearea';
+				return JSON.stringify(value, undefined, 4);
+			}
+			return value;
+		},
 	},
 })
 
diff --git a/system/author/js/vue-shared.js b/system/author/js/vue-shared.js
new file mode 100644
index 0000000..7d09441
--- /dev/null
+++ b/system/author/js/vue-shared.js
@@ -0,0 +1,136 @@
+Vue.component('component-image', {
+	props: ['class', 'id', 'description', 'maxlength', 'hidden', 'readonly', 'required', 'disabled', 'placeholder', 'label', 'name', 'type', 'value', 'errors'],
+	template: '<div class="large">' +
+				'<label>{{ label|translate }}</label>' +
+				'<div class="flex flex-wrap item-start">' +
+					'<div class="w-50">' +
+						'<div class="w6 h6 bg-black-40 dtc v-mid bg-chess">' +
+							'<img :src="imgpreview" class="mw6 max-h6 dt center">' +
+						'</div>' +
+					'</div>' +
+					'<div class="w-50 ph3 lh-copy f6 relative">' +
+						'<div class="relative dib w-100">' +
+							'<input class="absolute o-0 w-100 top-0 z-1 pointer" type="file" name="image" accept="image/*" @change="onFileChange( $event )" /> ' +
+							'<p class="relative w-100 bn br1 bg-tm-green white pa3 ma0 tc"><svg class="icon icon-upload baseline"><use xlink:href="#icon-upload"></use></svg> upload an image</p>'+
+						'</div>' +
+						'<div class="dib w-100 mt3">' +
+							'<button class="w-100 pointer bn br1 bg-tm-green white pa3 ma0 tc" @click.prevent="openmedialib()"><svg class="icon icon-image baseline"><use xlink:href="#icon-image"></use></svg> select from medialib</button>' +
+						'</div>' +
+						'<div class="dib w-100 mt3">' +
+							'<label>Image URL (read only)</label>' +
+							'<div class="flex">' +
+								'<button @click.prevent="deleteImage()" class="w-10 bg-tm-gray bn hover-bg-tm-red hover-white">x</button>' +
+								'<input class="w-90" type="text"' + 
+									' :id="id"' +
+									' :maxlength="maxlength"' +
+									' readonly="readonly"' +
+									' :hidden="hidden"' +
+									' :required="required"' +
+									' :disabled="disabled"' +
+									' :name="name"' +
+									' :placeholder="placeholder"' +
+									' :value="value"' +
+									'@input="update($event, name)">' +
+							'</div>' +
+						'</div>' +
+					  	'<div v-if="description" class="w-100 dib"><p>{{ description|translate }}</p></div>' +
+					  	'<div v-if="errors[name]" class="error">{{ errors[name] }}</div>' +
+					'</div>' +
+				'</div>' +
+				'<transition name="fade-editor">' +
+					'<div v-if="showmedialib" class="modalWindow">' +
+						'<medialib parentcomponent="images"></medialib>' + 
+					'</div>' +
+				'</transition>' +
+			  '</div>',
+	data: function(){
+		return {
+			maxsize: 5, // megabyte
+			imgpreview: false,
+			showmedialib: false,
+			load: false,
+		}
+	},
+	mounted: function(){
+		this.imgpreview = this.value;
+	},
+	methods: {
+		update: function(value)
+		{
+			FormBus.$emit('forminput', {'name' : this.name, 'value' : value});
+		},
+		updatemarkdown: function(markdown, url)
+		{
+			/* is called from child component medialib */
+			this.update(url);
+		},
+		deleteImage: function()
+		{
+			this.imgpreview = false;
+			this.update('');
+		},
+		openmedialib: function()
+		{
+			this.showmedialib = true;
+		},
+		onFileChange: function( e )
+		{
+			if(e.target.files.length > 0)
+			{
+				let imageFile = e.target.files[0];
+				let size = imageFile.size / 1024 / 1024;
+				
+				if (!imageFile.type.match('image.*'))
+				{
+					publishController.errors.message = "Only images are allowed.";
+				} 
+				else if (size > this.maxsize)
+				{
+					publishController.errors.message = "The maximal size of images is " + this.maxsize + " MB";
+				}
+				else
+				{
+					sharedself = this;
+					
+					let reader = new FileReader();
+					reader.readAsDataURL(imageFile);
+					reader.onload = function(e) 
+					{
+						sharedself.imgpreview = e.target.result;
+						
+						/* load image to server */
+						var url = sharedself.$root.$data.root + '/api/v1/image';
+						
+						var params = {
+							'url':				document.getElementById("path").value,
+							'image':			e.target.result,
+							'name': 			imageFile.name,
+							'publish':  		true,
+							'csrf_name': 		document.getElementById("csrf_name").value,
+							'csrf_value':		document.getElementById("csrf_value").value,
+						};
+
+						var method 	= 'POST';
+
+						sendJson(function(response, httpStatus)
+						{
+							if(response)
+							{
+								var result = JSON.parse(response);
+
+								if(result.errors)
+								{
+									publishController.errors.message = result.errors;
+								}
+								else
+								{
+									sharedself.update(result.name);
+								}
+							}
+						}, method, url, params);
+					}
+				}
+			}
+		}
+	},
+})
\ No newline at end of file
diff --git a/system/author/layouts/layoutBlox.twig b/system/author/layouts/layoutBlox.twig
index 7bc2e0e..177fb41 100644
--- a/system/author/layouts/layoutBlox.twig
+++ b/system/author/layouts/layoutBlox.twig
@@ -216,6 +216,7 @@
 		<script src="{{ base_url }}/system/author/js/sortable.min.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/vuedraggable.umd.min.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/vue-navi.js?20200420"></script>
+		<script src="{{ base_url }}/system/author/js/vue-shared.js?20200420"></script>
 		<script src="{{ base_url }}/system/author/js/vue-meta.js?20200420"></script>
 
 		{{ assets.renderJS() }}
diff --git a/system/author/metatabs.yaml b/system/author/metatabs.yaml
index 1cc5f37..391ef6a 100644
--- a/system/author/metatabs.yaml
+++ b/system/author/metatabs.yaml
@@ -11,6 +11,13 @@ meta:
       size: 160
       class: large
       description: If not filled, the description is extracted from content.
+    heroimage:
+      type: image
+      label: Hero Image
+      description: Maximum size for an image is 5 MB. Hero images are not supported by all themes.
+    heroimagealt:
+      type: text
+      label: Alternative Text for the hero image
     author:
       type: text
       label: author
diff --git a/system/author/partials/fields.twig b/system/author/partials/fields.twig
index 41b229b..3dc7227 100644
--- a/system/author/partials/fields.twig
+++ b/system/author/partials/fields.twig
@@ -9,6 +9,11 @@
 
 		<textarea id="{{ itemName }}[{{ field.name }}]" name="{{ itemName }}[{{ field.name }}]"{{field.getAttributeValues() }}{{ field.getAttributes() }}>{{ field.getContent() }}</textarea>
 
+	{% elseif field.type == 'image' %}
+		<div class="imageupload dropbox">
+			<input id="{{itemName}}[{{ field.name }}]" class="input-file" name="{{itemName}}[{{ field.name }}]" type="file" accept="image/*"{{ field.getAttributeValues() }}{{ field.getAttributes() }}>
+			<p><svg class="icon icon-upload baseline"><use xlink:href="#icon-upload"></use></svg> upload an image</p>
+		</div>
 	{% elseif field.type == 'paragraph' %}
 		
 		{{ markdown(field.getContent()) }}	
diff --git a/themes/typemill/css/style.css b/themes/typemill/css/style.css
index 0a603a9..3db2d27 100644
--- a/themes/typemill/css/style.css
+++ b/themes/typemill/css/style.css
@@ -605,7 +605,7 @@ a.tm-download::before{
 	width: 30px;
 	height: 30px;
 	line-height: 30px;
-	font-family: "Comic Sans MS",cursive,sans-serif;
+	font-family: Calibri, "Segoe UI", Roboto, Courier, Helvetica, -apple-system, BlinkMacSystemFont, sans-serif, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
 	font-size: 1.3em;
 	font-weight: 900;	
 	border: 2px solid #e0474c;
diff --git a/themes/typemill/typemill.yaml b/themes/typemill/typemill.yaml
index 9a1cd56..15db73a 100644
--- a/themes/typemill/typemill.yaml
+++ b/themes/typemill/typemill.yaml
@@ -27,6 +27,10 @@ forms:
       label: Different Design for Startpage
       checkboxlabel: Activate Special Startpage-Design
 
+    test:
+      type: image
+      label: Image test
+
     coverlogo:
       type: checkbox
       label: Logo on startpage