Useful improvements to the user interface

using the image file name for Alt makes only sense if you do not handle SEO for images. SEO for images is another thing entirely. your example could become something like this (assuming a travel blog) “The Bay of Kotor at Sunset, Travel Suggestion, Marvel, Travel Advice, Trip to Kotor, Summer vacation, Kotor Resort, Vacation in Kotor, Hotels in Kotor, Where to eat in Kotor” or similar. Google finds the image and all the keywords associated with it and ranks the image and the page for them. very often the image can contain “long tail keywords” that are composed of long sentences defining the thing that are a less used paraphrase of the concept you want to rank for.
That means each image is uploaded with the intent of ranking for certain keywords and those need to be filled out with intention for each image where applicable. That is why filenames do not really matter when uploading images (and it is unnecessary to change them). Because what really matters most is that Title, Alt and Description get filled in with SEO keywords that have a purpose in relation to the content of the page and the search intent.

2 Likes

Maybe you are right. I usually have 50+ images on my pages. So it will be variety of keywords.

I don’t push any of the features I’m using. I just share ideas that may be useful for someone.

1 Like

if on your pages there are more than 50 images, It would make sense to rank them for long tail keywords, the more elements on the page you can rank for the better (from a SEO perspective) - I can understand that it is a bit overwhelming with all those images but you can surely use an automation like chat GPT (by inputting the search intent you want to rank for, the topic you are writing about etc…) and it can output a list of Alt/Description/Title for each image to rank for a series of different terms for each. You can then upload them with each of them and be done. There are also plugins that do this inside the dashboard (usually they let you use your free monthly quota for the AI generation part, and if you exceed you only pay the tokens you use)

1 Like

I have now included some code for that in the PR. Could you test it, please, to see if it works for you?

Could you test the PR (while removing your own code) to see if it works for you?

1 Like

Hello.

setTimeout 500 will not be enough for long complex posts. As I mentioned before, my solution is really dirty and not perfect. Not for the core. Maybe I will write proper JS later and post it here.

Surely the length of the post won’t make any difference? The code I’ve written won’t even load until after the page has loaded, and then it waits 500 milliseconds after that to check sessionStorage.

1 Like

I haven’t electricity, so can’t test it now. But as I remember “DOMContentLoaded” fired after page loaded. But TinyMCE also loads on this event. So page height can drastically change.

I have a notebook with an i7-7700HQ processor. And for long articles, TinyMCE may render text longer than 1 sec.

I will try to write some solution when will be my turn to use electricity. Maybe, tomorrow.

Good point about TinyMCE loading then. But I think you’ll see that Tiny still does its own saving, irrespective of this code.

If you’re without electricity because of the war, please be safe!

1 Like

@ElisabettaCarrara Yes, this is exactly what I was trying to say. The filename off an image has very little bearing to SEO but the Alt and Title attributes do.

@Soller By naming the filename the same as the other attributes all you’re doing is repeating the same keywords over again. Google can clearly see that and will likely assume you are keyword stuffing, because you are repeating this practice consistently throughout the site.

Google will see this as suspicious and flag your entire site. Certainly you can rename the image filenames, but repeating the same terms in alt and title tags is not a wise idea. Use alt for SEO and title at a minimum.

Again as Tim also mentioned, this can be incredibly annoying for clients with screen readers.

Most importantly stay safe! :ukraine:

1 Like

Yes @Soller I do understand what you’re doing, not necessarily how you are achieving at code level.

Please see my comment below to you and Elisabetta.

You would be much better off using an SEO plugin for your travel site. For ClassicPress there is Classic SEO (love this plugin) and WP3 SEO.

This would give you far more benefit than image names .

3 Likes

Checked my code. I never output the title on the front end. So no repetition. It will be used only in the ClassicPress media library.

All filenames and alt are useful. I just describe what is in the photo. So screenreader users must be happy. But I never used it.

Names of images may not be useful for SEO. But it helps me find a specific one if needed.

I checked the blogs of some of my colleges with good rankings on Google. All using the same human-readable alts and named images. No one staffing keywords in alt for SEO.

Nevermind. As you wish, your site, do as you please.

1 Like

Just a fun thing. I installed it when moved to ClassicPress. But it does not support Polylang (false breadcrumbs), has a lot on not so useful features, and has too complex interface for my taste.

I tried to modify Classic SEO. But it is written too complex. Simple features may be spread over 10+ files. Just like Yoast or any other SEO plugin.

So today I started my simple SEO plugin:
image

We are talking about the same thing. But doing it in different ways. If I understood you correctly, only one difference - on your site names are like 0109-rotated.jpg, and on my: some-text.jpg
Then we both don’t output the title. And use alt to describe images. Maybe your descriptions are longer. Can you show me one of your sites (a good optimized page), so I can check how you do it?

Tried to write better code for the scroll position. It’s too complex for my skill. Can’t find TinyMCE event, which fire after total loading (including gallery and embedded media).

'init’ invent fires when the editor loads and applyes editor-styles.css. But before galleries are rendered.

So, I think, your code is simple and good enough.

Hello. 5 hours of work here:

to class-wp-editor.php (in same place as yours code):

const setScrollPosition = function() {
 if ( sessionStorage.getItem( 'scrollPosition' ) ) {
  window.scrollTo( 0, parseInt( sessionStorage.getItem( 'scrollPosition' ) ) );
  document.removeEventListener("May_run_scroll", setScrollPosition);
 }
};
document.addEventListener( 'DOMContentLoaded', function() {
 window.addEventListener( 'beforeunload', function() {
  var scrollPosition = document.documentElement.scrollTop;
  sessionStorage.setItem( 'scrollPosition', scrollPosition );
  if (document.getElementById('wp-content-wrap').classList.contains('html-active')) {
   const htmlEditorArea = document.querySelector('.wp-editor-area');
   const start = htmlEditorArea.selectionStart;
   const end = htmlEditorArea.selectionEnd;
   const selectionData = { start, end };
   sessionStorage.setItem('textareaSelection', JSON.stringify(selectionData));
  } else {
   /*
   //This will allow to restore cursor position and selection, if we find a way to save it be
sessions.
   const editor = tinymce.get('content');
   if (editor) {
    range = editor.selection.getRng();
    //Maybe Someone know how to save it?
   }
   */
  }
 });
 document.addEventListener("May_run_scroll", setScrollPosition);
 if (document.getElementById('wp-content-wrap').classList.contains('html-active')) {
  const htmlEditorArea = document.querySelector('.wp-editor-area');
  const resizeObserver = new ResizeObserver(() => {
   setTimeout( function () {
    const savedSelection = JSON.parse(sessionStorage.getItem('textareaSelection'));
    if (savedSelection) {
     htmlEditorArea.focus();
     htmlEditorArea.setSelectionRange(savedSelection.start, savedSelection.end);
    }
    document.dispatchEvent(new Event("May_run_scroll"));
   }, 50); //don't work without it. Maybe someone know how to fix?
  });
  resizeObserver.observe(htmlEditorArea);
 } else {
  /*
  range = need to somehow restore previously saved variable
  const editor = tinymce.get('content');
  if (editor) {
   editor.selection.setRng(range);
   var selectedNode = selection.startContainer;
   selectedNode.parentNode.scrollIntoView({
     behavior: 'smooth',
     block: 'center'
   });
  }*/
 }
});

and to js/mce-view.js on line 346:

document.dispatchEvent(new Event("May_run_scroll"));

Like this:

if ( content ) {
    this.setContent( content, function( editor, node ) {
        $( node ).data( 'rendered', true );
        this.bindNode.call( this, editor, node );
    }, force ? null : false );
    document.dispatchEvent(new Event("May_run_scroll"));
} else {
    this.setLoader();
}

What the difference:

  1. My code runs only after all rendering is done. So even if TinyMCE has a lot of galleries, code runs just after all of them are rendered and we know the final scroll height for window.
  2. My code preserve selection for textarea. And if someone help me to find a way for saving editor.selection.getRng(); it can save TinyMCE selection also.

In real life my code will restore position of scroll little bit more fast and precise. Not a big dial. But why not?

Of course, it’s also not perfect. So someone may improve it.

Honestly, as you’ve discovered with the parts you can’t complete, this isn’t going to be possible to include in core. Before I wrote any code, I took a look at what WP’s core devs had tried to do years ago, and they kept finding all sorts of new cases where their code wouldn’t work. So they abandoned the idea.

The problem is that version 4 of TinyMCE just wasn’t built with customization in mind. You can write something that will work for you, but it won’t work for someone else.

In your case, it seems that the real issue you are trying to resolve is not that TinyMCE might load after the script I’ve written loads at DOMContentLoaded, but that the images might load later. In that case, using an observer is a good call. But it potentially creates a nightmare experience for some users, where the window apparently settles on one location, only to move suddenly when an image is loaded (and then potentially another image, and another).

So, as I say, I can’t see how we can put this in core, though it’s great if it works for you (and for anyone else who likes it).

Yes. And If the images / galleries will be above the current scroll position - this will cause the text “jump”:

  1. DomContent loaded
  2. Your script sets scroll position to almost end of the text
  3. Than TinyMCE renders 2-4 galleries with 10s of images above selected scroll position.
  4. Scroll going to unpredictable direction.

So I chose 1500 ms in my initial code to avoid such situations. In some cases 500ms was not enough. I used similar code to the one in your PR, so I know some of it’s flaws.

In updated code this is avoided. Scroll will be set right after all images loaded in TinyMCE. Not a big deal. But a little bit better.

In html view you can see only textarea with text. No images. But the height of this textarea is calculated later, than Domcontentloaded fires. So I set the scroll position after another JS (jquery) sets a final height. If you have a plan to rewrite that JS, maybe fire some event when the final height is set? For this case just delay code for 500 ms will be also good enough. I never encountered situations when all rendering in text view mode was running more than 100-200ms.

Commented parts - more advanced approach where the code store not scroll position, but selection or cursor position. This will be more useful and allow to set scroll more precisely. Because, for example, Classic SEO metabox always loads closed. If it was opened on page save what all the seo checks also opened, scroll, that script set, will be wrong.

This approach better, because usually user want to return to place where he edited content. But to click on save button sometimes you need to scroll up (if save button not visible).

Only one really useful feature in my code is change in mce-view.js on line 346. Ability to use event “TinyMCE and all galleries/media loaded” can’t make any harm. So this line may go to core, just change event name.

Honestly, I think that preserving scroll position shouldn’t be in the core at all. It’s not a feature, that will be useful for all. Or, at least, someone should have an ability to disable it.

About TinyMCE you are 100% right. I have a big plugin for the link and prices management and rendering the shortcodes. And all the TinyMCE integrations was really hard to do.

I made it!

Code:

document.addEventListener( 'DOMContentLoaded', function() {
	//SAVING SCROLL POSITION
  window.addEventListener( 'beforeunload', function() {
		if (document.getElementById('wp-content-wrap').classList.contains('html-active')) {
			const htmlEditorArea = document.querySelector('.wp-editor-area');
			const start = htmlEditorArea.selectionStart;
			const end = htmlEditorArea.selectionEnd;
			const selectionData = {start, end};
			sessionStorage.setItem('textareaSelection', JSON.stringify(selectionData));
		} else if (document.getElementById('wp-content-wrap').classList.contains('tmce-active')) {
			const editor = tinymce.get('content');
      const selectionRange = editor.selection.getRng();
      const selectionNode = editor.selection.getNode();
      
      const getMatchingNodes = (container, selection) => {
          const tag = container.tagName;
          const allMatchingNodes = Array.from(editor.getBody().querySelectorAll(tag));
          return { tag, index: allMatchingNodes.indexOf(container) };
      };
      const start = getMatchingNodes(selectionNode, selectionRange.startContainer);
      const end = getMatchingNodes(selectionNode, selectionRange.endContainer);
      function containsShortcode(str) {
          // Regular expression to match any shortcode pattern. Maybe needs improvements
          const shortcodePattern = /\[[a-zA-Z0-9_-]+[^\]]*\]/;
          
          return shortcodePattern.test(str);
      }
      var selectionStartOffset = selectionRange.startOffset;
      var selectionEndOffset = selectionRange.endOffset;
      //FIX offset if shortcode
      if (containsShortcode(editor.selection.getContent())) {
        selectionStartOffset = 1;
        selectionEndOffset = 2;  
      }
      const selectionData = {
          startContainerTag: start.tag,
          startOffset: selectionStartOffset,
          startIndex: start.index,
          endContainerTag: end.tag,
          endOffset: selectionEndOffset,
          endIndex: end.index,
      };
      sessionStorage.setItem('TinyMCEselectionRange', JSON.stringify(selectionData));
		}
	});
	
  //RESTORE SCROLL POSITION
	if (document.getElementById('wp-content-wrap').classList.contains('html-active')) {
		const htmlEditorArea = document.querySelector('.wp-editor-area');
		const resizeObserver = new ResizeObserver((entries, observer) => {
			setTimeout( function () {
				const savedSelection = JSON.parse(sessionStorage.getItem('textareaSelection'));
				if (savedSelection) {
					htmlEditorArea.focus();
				    htmlEditorArea.setSelectionRange(savedSelection.start, savedSelection.end);
				}
        const lineHeight = parseInt(getComputedStyle(htmlEditorArea).lineHeight, 10);
        const linesBeforeSelection = htmlEditorArea.value.substr(0, savedSelection.start).split('\n').length;
        const targetScrollTop = (linesBeforeSelection - 1) * lineHeight;
        window.scrollTo({
            top: targetScrollTop,
            behavior: 'smooth'
        });
			}, 50); //don't work without it. Maybe someone know how to improve
      observer.disconnect();
		});
		resizeObserver.observe(htmlEditorArea);
	} else if (document.getElementById('wp-content-wrap').classList.contains('tmce-active')) {
    document.addEventListener("May_run_scroll", setTinyMCEScrollPosition);
	}
});
const setTinyMCEScrollPosition = function() {
    const savedSelectionData = JSON.parse(sessionStorage.getItem('TinyMCEselectionRange'));
    if (!savedSelectionData || !sessionStorage.getItem('scrollPosition')) return;
    setTimeout(() => {
        const editor = tinymce.get('content');
        editor.focus();
        // Helper function to find the matching element for start or end position
        const findMatchingElement = (tag, index) => {
            const paragraphs = editor.getBody().querySelectorAll(tag);
            return Array.from(paragraphs)[index] || null;
        };
        const startTargetElement = findMatchingElement(savedSelectionData.startContainerTag, savedSelectionData.
startIndex);
        const endTargetElement = findMatchingElement(savedSelectionData.endContainerTag, savedSelectionData.endIndex);
        if (startTargetElement && endTargetElement) {
            const startContainer = startTargetElement.firstChild;
            const endContainer = endTargetElement.firstChild;
            const range = editor.dom.createRng();
            range.setStart(startContainer, savedSelectionData.startOffset);
            range.setEnd(endContainer, savedSelectionData.endOffset);
            editor.selection.setRng(range);
            const selectionRange = editor.selection.getRng();
            const selectedNode = selectionRange.startContainer;
            selectedNode.parentNode.scrollIntoView({
                behavior: 'smooth',
                block: 'center'
            });
        }
        document.removeEventListener("May_run_scroll", setTinyMCEScrollPosition);
    }, 100); // Delay to ensure long posts are handled properly
};

And this needs previously mentioned event on js/mce-view.js on line 346

I still still think that this code shouldn’t be in core. But if you can help me to get that event in js/mce-view.js, I can build separete plugin. As for now my code works much better than initial version.

It can save cursor position, selection, or selected shortcode.