A reader clicks Login halfway through an article, authenticates, and lands on the dashboard instead of the paragraph they cared about. That tiny bit of friction is surprisingly irritating. WordPress already knows how to carry a return URL through its login form—we only need to generate and render it without confusing login state with account registration.
The compact template solution
<?php
if ( ! is_user_logged_in() && ! is_page( "register" ) ) {
$return_url = is_singular() ? get_permalink() : home_url( "/" );
$login_url = wp_login_url( $return_url );
?>
<p class="account-prompt">
<?php esc_html_e( "Already have an account?", "lynxbee" ); ?>
<a href="<?php echo esc_url( $login_url ); ?>">
<?php esc_html_e( "Log in", "lynxbee" ); ?>
</a>
</p>
<?php
}Every helper has a narrow job
is_user_logged_in()evaluates the current authentication cookie and user state.is_page("register")suppresses the prompt on the page whose slug isregister; a numeric page ID is safer if editors may change slugs.is_singular()ensuresget_permalink()refers to the viewed post, page, or custom post rather than guessing on archives and search pages.wp_login_url($return_url)adds the encodedredirect_tovalue to WordPress’s configured login URL.esc_url()escapes the URL at HTML output time.esc_html_e()translates and escapes visible text using the theme or plugin text domain.
A reusable shortcode instead of a header edit
<?php
/* Plugin Name: Site Account Link */
function lynxbee_account_link_shortcode(): string {
if ( is_user_logged_in() || is_page( "register" ) ) {
return "";
}
$return_url = is_singular() ? get_permalink() : home_url( "/" );
$login_url = wp_login_url( $return_url );
return sprintf(
'<p class="account-prompt">%1$s <a href="%2$s">%3$s</a></p>',
esc_html__( "Already have an account?", "lynxbee" ),
esc_url( $login_url ),
esc_html__( "Log in", "lynxbee" )
);
}
add_shortcode( "account_login_link", "lynxbee_account_link_shortcode" );The shortcode is portable and returns markup
Place
[account_login_link]in content or a compatible block/widget area after activating the plugin.A shortcode callback returns its output; echoing can move markup to an unexpected location.
The return type makes the empty logged-in path explicit.
sprintfkeeps the HTML structure fixed while each dynamic value is escaped for its exact context.Prefix names to avoid collisions with themes and other plugins; use namespaces for a larger plugin.
A shortcode is still evaluated server-side, so page-cache variation remains relevant.
Offer a logout link to authenticated users
<?php
$return_url = is_singular() ? get_permalink() : home_url( "/" );
if ( is_user_logged_in() ) : ?>
<a href="<?php echo esc_url( wp_logout_url( $return_url ) ); ?>">
<?php esc_html_e( "Log out", "lynxbee" ); ?>
</a>
<?php else : ?>
<a href="<?php echo esc_url( wp_login_url( $return_url ) ); ?>">
<?php esc_html_e( "Log in", "lynxbee" ); ?>
</a>
<?php endif; ?>Let WordPress create the logout nonce
wp_logout_url()generates the logout action URL and its nonce; do not hard-code/wp-login.php?action=logout.Passing the same local return URL restores the current content after logout.
Escape both generated URLs when placing them in
href.Do not display usernames or profile values without context-appropriate escaping.
Protect a page instead of merely showing a prompt
A conditional link is presentation, not access control. If the content itself requires authentication, perform the decision before template output and use WordPress’s authentication flow. A private plugin can hook template_redirect, check the target route, call auth_redirect(), and return immediately. Do not rely on hiding HTML that the server already sent.
function lynxbee_require_login_for_member_page(): void {
if ( is_page( "members" ) && ! is_user_logged_in() ) {
auth_redirect();
exit;
}
}
add_action( "template_redirect", "lynxbee_require_login_for_member_page" );Redirect before headers are sent
template_redirectruns after WordPress knows the queried page and before the template renders.auth_redirect()sends unauthenticated visitors through the core login flow and handles the requested destination.exitprevents protected template code from continuing after a redirect response.Use capability checks such as
current_user_can()when access depends on a role or permission rather than any account.Do not protect REST, AJAX, feeds, or downloads accidentally; define every route and response type in scope.
Avoid an open redirect
Pass a server-derived local permalink or home URL to
wp_login_url().Do not copy an arbitrary
redirect_toquery parameter into a custom redirect without validation.When implementing your own post-login redirect, validate the destination with
wp_validate_redirect()or perform it withwp_safe_redirect().Call
exitafterwp_safe_redirect()because the function does not terminate PHP execution.Only allow another host through
allowed_redirect_hostswhen the cross-domain flow is intentional and reviewed.
Page caching can show the wrong link
A full-page cache may store the anonymous HTML and serve it to a logged-in visitor, or cache a personalized page and expose it to someone else if cookie variation is misconfigured. WordPress normally sends logged-in cookies, but the CDN or cache plugin must actually bypass or vary on them.
Test while logged out and logged in through the same CDN path, not only by bypassing cache locally.
Configure the cache to bypass authenticated WordPress cookies and all login/account pages.
Purge affected pages after changing the template or shortcode.
For heavily cached public pages, render only generic account navigation or use a carefully designed client-side fragment backed by a non-cacheable identity endpoint.
Never embed private user data in a response that a shared cache may store.
Verify the complete flow
Open a normal post in a private window and confirm the login link appears.
Inspect the link and confirm its
redirect_toresolves to the same site and intended post.Log in with a test account and confirm WordPress returns to that post.
Refresh and confirm the logged-out prompt disappears.
Log out and confirm the nonce-backed flow returns to the post.
Visit the excluded registration page and confirm no circular or redundant prompt appears.
Test the homepage, archive, search result, 404, preview, and multilingual/domain-mapped variants.
Repeat through the production cache/CDN and on mobile.
Common mistakes and fixes
Hard-coded login domain: use
wp_login_url()so staging, HTTPS, subdirectories, and multisite configuration are respected.Raw URL printed into HTML: escape at output with
esc_url().“Registered user” inferred from login state: describe the visitor as logged in or logged out instead.
Prompt hidden but content still visible: enforce authorization before rendering.
Redirect returns to the wrong item: call
get_permalink()in a valid singular query context and choose an explicit fallback.Prompt appears on the login/register page: exclude by stable page ID, template, or route to prevent a confusing loop.
Logged-in users see the login prompt: audit shared-cache cookie variation and purge stale HTML.
Theme update removes the code: move it to a child theme or site plugin.
Official WordPress references
is_user_logged_in() documents the current-session check.
wp_login_url() generates the core login URL and optional redirect destination.
wp_logout_url() creates a nonce-protected logout URL.
get_permalink() retrieves a post’s permalink.
wp_validate_redirect() validates redirect hosts and schemes.
wp_safe_redirect() performs a local-safe redirect but still requires an explicit exit.
WordPress output security explains late, context-aware escaping.
Comments and corrections