diff --git a/lib/blocks.php b/lib/blocks.php
index 80522bda95367..aa37d373bba5a 100644
--- a/lib/blocks.php
+++ b/lib/blocks.php
@@ -85,6 +85,7 @@ function gutenberg_reregister_core_block_types() {
'query.php' => 'core/query',
'query-loop.php' => 'core/query-loop',
'query-pagination.php' => 'core/query-pagination',
+ 'site-logo.php' => 'core/site-logo',
'site-title.php' => 'core/site-title',
'template-part.php' => 'core/template-part',
)
diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss
index 8f232cb0a35e6..491295a4af55a 100644
--- a/packages/block-library/src/editor.scss
+++ b/packages/block-library/src/editor.scss
@@ -37,6 +37,7 @@
@import "./search/editor.scss";
@import "./separator/editor.scss";
@import "./shortcode/editor.scss";
+@import "./site-logo/editor.scss";
@import "./social-link/editor.scss";
@import "./social-links/editor.scss";
@import "./spacer/editor.scss";
diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js
index cf54d74dab512..e4e553926eb14 100644
--- a/packages/block-library/src/index.js
+++ b/packages/block-library/src/index.js
@@ -64,6 +64,7 @@ import * as socialLink from './social-link';
import * as widgetArea from './widget-area';
// Full Site Editing Blocks
+import * as siteLogo from './site-logo';
import * as siteTitle from './site-title';
import * as templatePart from './template-part';
import * as query from './query';
@@ -198,6 +199,7 @@ export const __experimentalRegisterExperimentalCoreBlocks =
...( __experimentalEnableFullSiteEditing
? [
siteTitle,
+ siteLogo,
templatePart,
query,
queryLoop,
diff --git a/packages/block-library/src/site-logo/block.json b/packages/block-library/src/site-logo/block.json
new file mode 100644
index 0000000000000..f86b4a22ef363
--- /dev/null
+++ b/packages/block-library/src/site-logo/block.json
@@ -0,0 +1,16 @@
+{
+ "name": "core/site-logo",
+ "category": "layout",
+ "attributes": {
+ "align": {
+ "type": "string"
+ },
+ "width": {
+ "type": "number"
+ }
+ },
+ "supports": {
+ "html": false,
+ "lightBlockWrapper": true
+ }
+}
diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js
new file mode 100644
index 0000000000000..911189f311573
--- /dev/null
+++ b/packages/block-library/src/site-logo/edit.js
@@ -0,0 +1,373 @@
+/**
+ * External dependencies
+ */
+import classnames from 'classnames';
+import { includes, pick } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { isBlobURL } from '@wordpress/blob';
+import { useState, useRef } from '@wordpress/element';
+import { __ } from '@wordpress/i18n';
+import {
+ Notice,
+ PanelBody,
+ RangeControl,
+ ResizableBox,
+ Spinner,
+ ToolbarButton,
+ ToolbarGroup,
+} from '@wordpress/components';
+import { useViewportMatch } from '@wordpress/compose';
+import {
+ BlockControls,
+ BlockIcon,
+ InspectorControls,
+ MediaPlaceholder,
+ MediaReplaceFlow,
+ __experimentalBlock as Block,
+} from '@wordpress/block-editor';
+import { useSelect, useDispatch } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import icon from './icon';
+import useClientWidth from '../image/use-client-width';
+
+/**
+ * Module constants
+ */
+import { MIN_SIZE } from '../image/constants';
+
+const ALLOWED_MEDIA_TYPES = [ 'image' ];
+const ACCEPT_MEDIA_STRING = 'image/*';
+
+const SiteLogo = ( {
+ alt,
+ attributes: { align, width, height },
+ containerRef,
+ isSelected,
+ setAttributes,
+ logoUrl,
+ siteUrl,
+} ) => {
+ const clientWidth = useClientWidth( containerRef, [ align ] );
+ const isLargeViewport = useViewportMatch( 'medium' );
+ const isWideAligned = includes( [ 'wide', 'full' ], align );
+ const isResizable = ! isWideAligned && isLargeViewport;
+ const [ { naturalWidth, naturalHeight }, setNaturalSize ] = useState( {} );
+ const { toggleSelection } = useDispatch( 'core/block-editor' );
+ const classes = classnames( {
+ 'is-transient': isBlobURL( logoUrl ),
+ } );
+ const { maxWidth, isRTL, title } = useSelect( ( select ) => {
+ const { getSettings } = select( 'core/block-editor' );
+ const siteEntities = select( 'core' ).getEditedEntityRecord(
+ 'root',
+ 'site'
+ );
+ return {
+ title: siteEntities.title,
+ ...pick( getSettings(), [ 'imageSizes', 'isRTL', 'maxWidth' ] ),
+ };
+ } );
+
+ function onResizeStart() {
+ toggleSelection( false );
+ }
+
+ function onResizeStop() {
+ toggleSelection( true );
+ }
+
+ const img = (
+ // Disable reason: Image itself is not meant to be interactive, but
+ // should direct focus to block.
+ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
+ event.preventDefault() }
+ >
+
+ {
+ setNaturalSize(
+ pick( event.target, [
+ 'naturalWidth',
+ 'naturalHeight',
+ ] )
+ );
+ } }
+ />
+
+
+ /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */
+ );
+
+ let imageWidthWithinContainer;
+
+ if ( clientWidth && naturalWidth && naturalHeight ) {
+ const exceedMaxWidth = naturalWidth > clientWidth;
+ imageWidthWithinContainer = exceedMaxWidth ? clientWidth : naturalWidth;
+ }
+
+ if ( ! isResizable || ! imageWidthWithinContainer ) {
+ return
{ img }
;
+ }
+
+ const currentWidth = width || imageWidthWithinContainer;
+ const ratio = naturalWidth / naturalHeight;
+ const currentHeight = currentWidth / ratio;
+ const minWidth = naturalWidth < naturalHeight ? MIN_SIZE : MIN_SIZE * ratio;
+ const minHeight =
+ naturalHeight < naturalWidth ? MIN_SIZE : MIN_SIZE / ratio;
+
+ // With the current implementation of ResizableBox, an image needs an
+ // explicit pixel value for the max-width. In absence of being able to
+ // set the content-width, this max-width is currently dictated by the
+ // vanilla editor style. The following variable adds a buffer to this
+ // vanilla style, so 3rd party themes have some wiggleroom. This does,
+ // in most cases, allow you to scale the image beyond the width of the
+ // main column, though not infinitely.
+ // @todo It would be good to revisit this once a content-width variable
+ // becomes available.
+ const maxWidthBuffer = maxWidth * 2.5;
+
+ let showRightHandle = false;
+ let showLeftHandle = false;
+
+ /* eslint-disable no-lonely-if */
+ // See https://github.com/WordPress/gutenberg/issues/7584.
+ if ( align === 'center' ) {
+ // When the image is centered, show both handles.
+ showRightHandle = true;
+ showLeftHandle = true;
+ } else if ( isRTL ) {
+ // In RTL mode the image is on the right by default.
+ // Show the right handle and hide the left handle only when it is
+ // aligned left. Otherwise always show the left handle.
+ if ( align === 'left' ) {
+ showRightHandle = true;
+ } else {
+ showLeftHandle = true;
+ }
+ } else {
+ // Show the left handle and hide the right handle only when the
+ // image is aligned right. Otherwise always show the right handle.
+ if ( align === 'right' ) {
+ showLeftHandle = true;
+ } else {
+ showRightHandle = true;
+ }
+ }
+ /* eslint-enable no-lonely-if */
+
+ return (
+ <>
+
+
+
+ setAttributes( { width: newWidth } )
+ }
+ min={ minWidth }
+ max={ maxWidthBuffer }
+ initialPosition={ Math.min(
+ naturalWidth,
+ maxWidthBuffer
+ ) }
+ value={ width || '' }
+ disabled={ ! isResizable }
+ />
+
+
+ {
+ onResizeStop();
+ setAttributes( {
+ width: parseInt( currentWidth + delta.width, 10 ),
+ height: parseInt( currentHeight + delta.height, 10 ),
+ } );
+ } }
+ >
+ { img }
+
+ >
+ );
+};
+
+export default function LogoEdit( {
+ attributes,
+ className,
+ setAttributes,
+ isSelected,
+} ) {
+ const { width } = attributes;
+ const [ logoUrl, setLogoUrl ] = useState();
+ const [ error, setError ] = useState();
+ const ref = useRef();
+ const { mediaItemData, sitelogo, url } = useSelect( ( select ) => {
+ const siteSettings = select( 'core' ).getEditedEntityRecord(
+ 'root',
+ 'site'
+ );
+ const mediaItem = select( 'core' ).getEntityRecord(
+ 'root',
+ 'media',
+ siteSettings.sitelogo
+ );
+ return {
+ mediaItemData: mediaItem && {
+ url: mediaItem.source_url,
+ alt: mediaItem.alt_text,
+ },
+ sitelogo: siteSettings.sitelogo,
+ url: siteSettings.url,
+ };
+ }, [] );
+
+ const { editEntityRecord } = useDispatch( 'core' );
+ const setLogo = ( newValue ) =>
+ editEntityRecord( 'root', 'site', undefined, {
+ sitelogo: newValue,
+ } );
+
+ let alt = null;
+ if ( mediaItemData ) {
+ alt = mediaItemData.alt;
+ if ( logoUrl !== mediaItemData.url ) {
+ setLogoUrl( mediaItemData.url );
+ }
+ }
+
+ const onSelectLogo = ( media ) => {
+ if ( ! media ) {
+ return;
+ }
+
+ if ( ! media.id && media.url ) {
+ // This is a temporary blob image
+ setLogo( '' );
+ setError();
+ setLogoUrl( media.url );
+ return;
+ }
+
+ setLogo( media.id.toString() );
+ };
+
+ const deleteLogo = () => {
+ setLogo( '' );
+ setLogoUrl( '' );
+ };
+
+ const onUploadError = ( message ) => {
+ setError( message[ 2 ] ? message[ 2 ] : null );
+ };
+
+ const controls = (
+
+
+ { logoUrl && (
+
+ ) }
+ { !! logoUrl && (
+ deleteLogo() }
+ label={ __( 'Delete Site Logo' ) }
+ />
+ ) }
+
+
+ );
+
+ const label = __( 'Site Logo' );
+ let logoImage;
+ if ( sitelogo === undefined ) {
+ logoImage = ;
+ }
+
+ if ( !! logoUrl ) {
+ logoImage = (
+
+ );
+ }
+
+ const mediaPlaceholder = (
+ }
+ labels={ {
+ title: label,
+ instructions: __(
+ 'Upload an image, or pick one from your media library, to be your site logo'
+ ),
+ } }
+ onSelect={ onSelectLogo }
+ accept={ ACCEPT_MEDIA_STRING }
+ allowedTypes={ ALLOWED_MEDIA_TYPES }
+ mediaPreview={ logoImage }
+ notices={
+ error && (
+
+ { error }
+
+ )
+ }
+ onError={ onUploadError }
+ />
+ );
+
+ const classes = classnames( className, {
+ 'is-resized': !! width,
+ 'is-focused': isSelected,
+ } );
+
+ const key = !! logoUrl;
+
+ return (
+
+ { controls }
+ { logoUrl && logoImage }
+ { ! logoUrl && mediaPlaceholder }
+
+ );
+}
diff --git a/packages/block-library/src/site-logo/editor.scss b/packages/block-library/src/site-logo/editor.scss
new file mode 100644
index 0000000000000..8cb4592480eca
--- /dev/null
+++ b/packages/block-library/src/site-logo/editor.scss
@@ -0,0 +1,28 @@
+.wp-block[data-align="center"] > .wp-block-site-logo {
+ margin-left: auto;
+ margin-right: auto;
+ text-align: center;
+}
+
+.wp-block-site-logo {
+ &.is-resized {
+ display: table;
+ }
+
+ .custom-logo-link {
+ cursor: inherit;
+
+ &:focus {
+ box-shadow: none;
+ }
+
+ &.is-transient img {
+ opacity: 0.3;
+ }
+ }
+
+ img {
+ display: block;
+ max-width: 100%;
+ }
+}
diff --git a/packages/block-library/src/site-logo/icon.js b/packages/block-library/src/site-logo/icon.js
new file mode 100644
index 0000000000000..a7118a72de038
--- /dev/null
+++ b/packages/block-library/src/site-logo/icon.js
@@ -0,0 +1,24 @@
+/**
+ * WordPress dependencies
+ */
+import { SVG, Path } from '@wordpress/components';
+
+export default (
+
+);
diff --git a/packages/block-library/src/site-logo/index.js b/packages/block-library/src/site-logo/index.js
new file mode 100644
index 0000000000000..b021f2b08bbec
--- /dev/null
+++ b/packages/block-library/src/site-logo/index.js
@@ -0,0 +1,25 @@
+/**
+ * WordPress dependencies
+ */
+import { __ } from '@wordpress/i18n';
+
+/**
+ * Internal dependencies
+ */
+import metadata from './block.json';
+import icon from './icon';
+import edit from './edit';
+
+const { name } = metadata;
+export { metadata, name };
+
+export const settings = {
+ title: __( 'Site Logo' ),
+ description: __( 'Show a site logo' ),
+ icon,
+ supports: {
+ align: true,
+ alignWide: false,
+ },
+ edit,
+};
diff --git a/packages/block-library/src/site-logo/index.php b/packages/block-library/src/site-logo/index.php
new file mode 100644
index 0000000000000..66b3a84023af2
--- /dev/null
+++ b/packages/block-library/src/site-logo/index.php
@@ -0,0 +1,101 @@
+%s', $class_name, $custom_logo );
+ remove_filter( 'wp_get_attachment_image_src', $adjust_width_height_filter );
+ return $html;
+}
+
+
+/**
+ * Registers the `core/site-logo` block on the server.
+ */
+function register_block_core_site_logo() {
+ if ( gutenberg_is_experiment_enabled( 'gutenberg-full-site-editing' ) ) {
+ register_block_type(
+ 'core/site-logo',
+ array(
+ 'render_callback' => 'render_block_core_site_logo',
+ )
+ );
+ add_filter( 'pre_set_theme_mod_custom_logo', 'sync_site_logo_to_theme_mod' );
+ add_filter( 'theme_mod_custom_logo', 'override_custom_logo_theme_mod' );
+ }
+}
+add_action( 'init', 'register_block_core_site_logo' );
+
+/**
+ * Overrides the custom logo with a site logo, if the option is set.
+ *
+ * @param string $custom_logo The custom logo set by a theme.
+ *
+ * @return string The site logo if set.
+ */
+function override_custom_logo_theme_mod( $custom_logo ) {
+ $sitelogo = get_option( 'sitelogo' );
+ return false === $sitelogo ? $custom_logo : $sitelogo;
+}
+
+/**
+ * Syncs the site logo with the theme modified logo.
+ *
+ * @param string $custom_logo The custom logo set by a theme.
+ *
+ * @return string The custom logo.
+ */
+function sync_site_logo_to_theme_mod( $custom_logo ) {
+ if ( $custom_logo ) {
+ update_option( 'sitelogo', $custom_logo );
+ }
+ return $custom_logo;
+}
+
+/**
+ * Register a core site setting for a site logo
+ */
+function register_block_core_site_logo_setting() {
+ register_setting(
+ 'general',
+ 'sitelogo',
+ array(
+ 'show_in_rest' => array(
+ 'name' => 'sitelogo',
+ ),
+ 'type' => 'string',
+ 'description' => __( 'Site logo.' ),
+ )
+ );
+}
+
+add_action( 'rest_api_init', 'register_block_core_site_logo_setting', 10 );
diff --git a/packages/block-library/src/site-logo/style.scss b/packages/block-library/src/site-logo/style.scss
new file mode 100644
index 0000000000000..6e186d32296df
--- /dev/null
+++ b/packages/block-library/src/site-logo/style.scss
@@ -0,0 +1,5 @@
+.wp-block-custom-logo {
+ .aligncenter {
+ display: table;
+ }
+}
diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss
index 2ef1a87bba417..31abfed6e3eb7 100644
--- a/packages/block-library/src/style.scss
+++ b/packages/block-library/src/style.scss
@@ -26,6 +26,7 @@
@import "./rss/style.scss";
@import "./search/style.scss";
@import "./separator/style.scss";
+@import "./site-logo/style.scss";
@import "./social-links/style.scss";
@import "./spacer/style.scss";
@import "./subhead/style.scss";
diff --git a/packages/e2e-tests/fixtures/blocks/core__site-logo.html b/packages/e2e-tests/fixtures/blocks/core__site-logo.html
new file mode 100644
index 0000000000000..cfd4cd6a3b8d6
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__site-logo.html
@@ -0,0 +1 @@
+
diff --git a/packages/e2e-tests/fixtures/blocks/core__site-logo.json b/packages/e2e-tests/fixtures/blocks/core__site-logo.json
new file mode 100644
index 0000000000000..a84c7c57737e3
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__site-logo.json
@@ -0,0 +1,10 @@
+[
+ {
+ "clientId": "_clientId_0",
+ "name": "core/site-logo",
+ "isValid": true,
+ "attributes": {},
+ "innerBlocks": [],
+ "originalContent": ""
+ }
+]
diff --git a/packages/e2e-tests/fixtures/blocks/core__site-logo.parsed.json b/packages/e2e-tests/fixtures/blocks/core__site-logo.parsed.json
new file mode 100644
index 0000000000000..3db3836e10d1a
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__site-logo.parsed.json
@@ -0,0 +1,18 @@
+[
+ {
+ "blockName": "core/site-logo",
+ "attrs": {},
+ "innerBlocks": [],
+ "innerHTML": "",
+ "innerContent": []
+ },
+ {
+ "blockName": null,
+ "attrs": {},
+ "innerBlocks": [],
+ "innerHTML": "\n",
+ "innerContent": [
+ "\n"
+ ]
+ }
+]
diff --git a/packages/e2e-tests/fixtures/blocks/core__site-logo.serialized.html b/packages/e2e-tests/fixtures/blocks/core__site-logo.serialized.html
new file mode 100644
index 0000000000000..cfd4cd6a3b8d6
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__site-logo.serialized.html
@@ -0,0 +1 @@
+