No, you did nothing wrong.
It’s a known issue and in WordPress v6.1.3 and up to the current stable release as of writing (v6.2.2), it’s happening because of the following lines in get_the_block_template_html() which returns the markup for the current (block-based) template:
$content = do_shortcode( $content );
$content = do_blocks( $content );
So as you could see, shortcodes in the post content (post_content) are being run before the blocks are parsed, which means that by the time your shortcode runs, the Query Loop block has not yet run, hence get_the_ID(), get_post()->ID, etc. would return the ID of the first post or whatever is the current post in the current loop.
In the past, it used to be that do_blocks() is being called before do_shortcode(), then in WordPress v6.1.2, the do_shortcode() call was removed and yet, added back in v6.1.3 (see changesets 55771 and 55830 in Trac), so I don’t know if it will ever be back to any of that behaviors.
Nonetheless, there are 2 possible workarounds you can try:
-
In your shortcode function, return a special tag like
%xyz_shortcode%, and use a filter hook likerender_blockorrender_block_<name>to replace that tag with the “real” content.See example here.
-
Create or turn your shortcode into a dynamic block, i.e. a block type with a
render_callback.See examples here.
Note that I’m consuming a block context named
postIdwhich gives us the correct ID of the current post in the Query Loop. Seexyz-block-csr/index.jsxandxyz-block-ssr/index.jsxto see how I used that context.