class Akismet_REST_API { /** * Register the REST API routes. */ public static function init() { if ( ! function_exists( 'register_rest_route' ) ) { // The REST API wasn't integrated into core until 4.4, and we support 4.0+ (for now). return false; } register_rest_route( 'akismet/v1', '/key', array( array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_key' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'set_key' ), 'args' => array( 'key' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ), 'description' => __( 'A 12-character Akismet API key. Available at akismet.com/account', 'akismet' ), ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'delete_key' ), ), ) ); register_rest_route( 'akismet/v1', '/settings/', array( array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_settings' ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'set_boolean_settings' ), 'args' => array( 'akismet_strictness' => array( 'required' => false, 'type' => 'boolean', 'description' => __( 'If true, Akismet will automatically discard the worst spam automatically rather than putting it in the spam folder.', 'akismet' ), ), 'akismet_show_user_comments_approved' => array( 'required' => false, 'type' => 'boolean', 'description' => __( 'If true, show the number of approved comments beside each comment author in the comments list page.', 'akismet' ), ), ), ), ) ); register_rest_route( 'akismet/v1', '/stats', array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_stats' ), 'args' => array( 'interval' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_interval' ), 'description' => __( 'The time period for which to retrieve stats. Options: 60-days, 6-months, all', 'akismet' ), 'default' => 'all', ), ), ) ); register_rest_route( 'akismet/v1', '/stats/(?P[\w+])', array( 'args' => array( 'interval' => array( 'description' => __( 'The time period for which to retrieve stats. Options: 60-days, 6-months, all', 'akismet' ), 'type' => 'string', ), ), array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'privileged_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_stats' ), ), ) ); register_rest_route( 'akismet/v1', '/alert', array( array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'get_alert' ), 'args' => array( 'key' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ), 'description' => __( 'A 12-character Akismet API key. Available at akismet.com/account', 'akismet' ), ), ), ), array( 'methods' => WP_REST_Server::EDITABLE, 'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'set_alert' ), 'args' => array( 'key' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ), 'description' => __( 'A 12-character Akismet API key. Available at akismet.com/account', 'akismet' ), ), ), ), array( 'methods' => WP_REST_Server::DELETABLE, 'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ), 'callback' => array( 'Akismet_REST_API', 'delete_alert' ), 'args' => array( 'key' => array( 'required' => false, 'type' => 'string', 'sanitize_callback' => array( 'Akismet_REST_API', 'sanitize_key' ), 'description' => __( 'A 12-character Akismet API key. Available at akismet.com/account', 'akismet' ), ), ), ), ) ); register_rest_route( 'akismet/v1', '/webhook', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( 'Akismet_REST_API', 'receive_webhook' ), 'permission_callback' => array( 'Akismet_REST_API', 'remote_call_permission_callback' ), ) ); } /** * Get the current Akismet API key. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function get_key( $request = null ) { return rest_ensure_response( Akismet::get_api_key() ); } /** * Set the API key, if possible. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function set_key( $request ) { if ( defined( 'WPCOM_API_KEY' ) ) { return rest_ensure_response( new WP_Error( 'hardcoded_key', __( 'This site\'s API key is hardcoded and cannot be changed via the API.', 'akismet' ), array( 'status' => 409 ) ) ); } $new_api_key = $request->get_param( 'key' ); if ( ! self::key_is_valid( $new_api_key ) ) { return rest_ensure_response( new WP_Error( 'invalid_key', __( 'The value provided is not a valid and registered API key.', 'akismet' ), array( 'status' => 400 ) ) ); } update_option( 'wordpress_api_key', $new_api_key ); return self::get_key(); } /** * Unset the API key, if possible. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function delete_key( $request ) { if ( defined( 'WPCOM_API_KEY' ) ) { return rest_ensure_response( new WP_Error( 'hardcoded_key', __( 'This site\'s API key is hardcoded and cannot be deleted.', 'akismet' ), array( 'status' => 409 ) ) ); } delete_option( 'wordpress_api_key' ); return rest_ensure_response( true ); } /** * Get the Akismet settings. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function get_settings( $request = null ) { return rest_ensure_response( array( 'akismet_strictness' => ( get_option( 'akismet_strictness', '1' ) === '1' ), 'akismet_show_user_comments_approved' => ( get_option( 'akismet_show_user_comments_approved', '1' ) === '1' ), ) ); } /** * Update the Akismet settings. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function set_boolean_settings( $request ) { foreach ( array( 'akismet_strictness', 'akismet_show_user_comments_approved', ) as $setting_key ) { $setting_value = $request->get_param( $setting_key ); if ( is_null( $setting_value ) ) { // This setting was not specified. continue; } // From 4.7+, WP core will ensure that these are always boolean // values because they are registered with 'type' => 'boolean', // but we need to do this ourselves for prior versions. $setting_value = self::parse_boolean( $setting_value ); update_option( $setting_key, $setting_value ? '1' : '0' ); } return self::get_settings(); } /** * Parse a numeric or string boolean value into a boolean. * * @param mixed $value The value to convert into a boolean. * @return bool The converted value. */ public static function parse_boolean( $value ) { switch ( $value ) { case true: case 'true': case '1': case 1: return true; case false: case 'false': case '0': case 0: return false; default: return (bool) $value; } } /** * Get the Akismet stats for a given time period. * * Possible `interval` values: * - all * - 60-days * - 6-months * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function get_stats( $request ) { $api_key = Akismet::get_api_key(); $interval = $request->get_param( 'interval' ); $stat_totals = array(); $request_args = array( 'blog' => get_option( 'home' ), 'key' => $api_key, 'from' => $interval, ); $request_args = apply_filters( 'akismet_request_args', $request_args, 'get-stats' ); $response = Akismet::http_post( Akismet::build_query( $request_args ), 'get-stats' ); if ( ! empty( $response[1] ) ) { $stat_totals[ $interval ] = json_decode( $response[1] ); } return rest_ensure_response( $stat_totals ); } /** * Get the current alert code and message. Alert codes are used to notify the site owner * if there's a problem, like a connection issue between their site and the Akismet API, * invalid requests being sent, etc. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function get_alert( $request ) { return rest_ensure_response( array( 'code' => get_option( 'akismet_alert_code' ), 'message' => get_option( 'akismet_alert_msg' ), ) ); } /** * Update the current alert code and message by triggering a call to the Akismet server. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function set_alert( $request ) { delete_option( 'akismet_alert_code' ); delete_option( 'akismet_alert_msg' ); // Make a request so the most recent alert code and message are retrieved. Akismet::verify_key( Akismet::get_api_key() ); return self::get_alert( $request ); } /** * Clear the current alert code and message. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function delete_alert( $request ) { delete_option( 'akismet_alert_code' ); delete_option( 'akismet_alert_msg' ); return self::get_alert( $request ); } private static function key_is_valid( $key ) { $request_args = array( 'key' => $key, 'blog' => get_option( 'home' ), ); $request_args = apply_filters( 'akismet_request_args', $request_args, 'verify-key' ); $response = Akismet::http_post( Akismet::build_query( $request_args ), 'verify-key' ); if ( $response[1] == 'valid' ) { return true; } return false; } public static function privileged_permission_callback() { return current_user_can( 'manage_options' ); } /** * For calls that Akismet.com makes to the site to clear outdated alert codes, use the API key for authorization. */ public static function remote_call_permission_callback( $request ) { $local_key = Akismet::get_api_key(); return $local_key && ( strtolower( $request->get_param( 'key' ) ?? '' ) === strtolower( $local_key ) ); } public static function sanitize_interval( $interval, $request, $param ) { $interval = trim( $interval ); $valid_intervals = array( '60-days', '6-months', 'all' ); if ( ! in_array( $interval, $valid_intervals ) ) { $interval = 'all'; } return $interval; } public static function sanitize_key( $key, $request, $param ) { return trim( $key ); } /** * Process a webhook request from the Akismet servers. * * @param WP_REST_Request $request * @return WP_Error|WP_REST_Response */ public static function receive_webhook( $request ) { Akismet::log( array( 'Webhook request received', $request->get_body() ) ); /** * The request body should look like this: * array( * 'key' => '1234567890abcd', * 'endpoint' => '[comment-check|submit-ham|submit-spam]', * 'comments' => array( * array( * 'guid' => '[...]', * 'result' => '[true|false]', * 'comment_author' => '[...]', * [...] * ), * array( * 'guid' => '[...]', * [...], * ), * [...] * ) * ) * * Multiple comments can be included in each request, and the only truly required * field for each is the guid, although it would be friendly to include also * comment_post_ID, comment_parent, and comment_author_email, if possible to make * searching easier. */ // The response will include statuses for the result of each comment that was supplied. $response = array( 'comments' => array(), ); $endpoint = $request->get_param( 'endpoint' ); switch ( $endpoint ) { case 'comment-check': $webhook_comments = $request->get_param( 'comments' ); if ( ! is_array( $webhook_comments ) ) { return rest_ensure_response( new WP_Error( 'malformed_request', __( 'The \'comments\' parameter must be an array.', 'akismet' ), array( 'status' => 400 ) ) ); } foreach ( $webhook_comments as $webhook_comment ) { $guid = $webhook_comment['guid']; if ( ! $guid ) { // Without the GUID, we can't be sure that we're matching the right comment. // We'll make it a rule that any comment without a GUID is ignored intentionally. continue; } // Search on the fields that are indexed in the comments table, plus the GUID. // The GUID is the only thing we really need to search on, but comment_meta // is not indexed in a useful way if there are many many comments. This // should help narrow it down first. $queryable_fields = array( 'comment_post_ID' => 'post_id', 'comment_parent' => 'parent', 'comment_author_email' => 'author_email', ); $query_args = array(); $query_args['status'] = 'any'; $query_args['meta_key'] = 'akismet_guid'; $query_args['meta_value'] = $guid; foreach ( $queryable_fields as $queryable_field => $wp_comment_query_field ) { if ( isset( $webhook_comment[ $queryable_field ] ) ) { $query_args[ $wp_comment_query_field ] = $webhook_comment[ $queryable_field ]; } } $comments_query = new WP_Comment_Query( $query_args ); $comments = $comments_query->comments; if ( ! $comments ) { // Unexpected, although the comment could have been deleted since being submitted. Akismet::log( 'Webhook failed: no matching comment found.' ); $response['comments'][ $guid ] = array( 'status' => 'error', 'message' => __( 'Could not find matching comment.', 'akismet' ), ); continue; } if ( count( $comments ) > 1 ) { // Two comments shouldn't be able to match the same GUID. Akismet::log( 'Webhook failed: multiple matching comments found.', $comments ); $response['comments'][ $guid ] = array( 'status' => 'error', 'message' => __( 'Multiple comments matched request.', 'akismet' ), ); continue; } else { // We have one single match, as hoped for. Akismet::log( 'Found matching comment.', $comments ); $comment = $comments[0]; $current_status = wp_get_comment_status( $comment ); $result = $webhook_comment['result']; if ( 'true' == $result ) { Akismet::log( 'Comment should be spam' ); // The comment should be classified as spam. if ( 'spam' != $current_status ) { // The comment is not classified as spam. If Akismet was the one to act on it, move it to spam. if ( Akismet::last_comment_status_change_came_from_akismet( $comment->comment_ID ) ) { Akismet::log( 'Comment is not spam; marking as spam.' ); wp_spam_comment( $comment ); Akismet::update_comment_history( $comment->comment_ID, '', 'webhook-spam' ); } else { Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' ); Akismet::update_comment_history( $comment->comment_ID, '', 'webhook-spam-noaction' ); } } } elseif ( 'false' == $result ) { Akismet::log( 'Comment should be ham' ); // The comment should be classified as ham. if ( 'spam' == $current_status ) { Akismet::log( 'Comment is spam.' ); // The comment is classified as spam. If Akismet was the one to label it as spam, unspam it. if ( Akismet::last_comment_status_change_came_from_akismet( $comment->comment_ID ) ) { Akismet::log( 'Akismet marked it as spam; unspamming.' ); wp_unspam_comment( $comment ); akismet::update_comment_history( $comment->comment_ID, '', 'webhook-ham' ); } else { Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' ); Akismet::update_comment_history( $comment->comment_ID, '', 'webhook-ham-noaction' ); } } else if ( 'unapproved' == $current_status ) { Akismet::log( 'Comment is pending.' ); // The comment is in Pending. If Akismet was the one to put it there, approve it (but only if the site // settings dictate that). if ( Akismet::last_comment_status_change_came_from_akismet( $comment->comment_ID ) ) { Akismet::log( 'Akismet marked it as Pending; approving.' ); if ( check_comment( $comment->comment_author, $comment->comment_author_email, $comment->comment_author_url, $comment->comment_content, $comment->comment_author_IP, $comment->comment_agent, $comment->comment_type ) ) { wp_set_comment_status( $comment->comment_ID, 1 ); } akismet::update_comment_history( $comment->comment_ID, '', 'webhook-ham' ); } else { Akismet::log( 'Comment is not spam, but it has already been manually handled by some other process.' ); Akismet::update_comment_history( $comment->comment_ID, '', 'webhook-ham-noaction' ); } } $moderation_email_was_delayed = get_comment_meta( $comment->comment_ID, 'akismet_delayed_moderation_email', true ); if ( $moderation_email_was_delayed ) { Akismet::log( 'Moderation email was delayed for comment #' . $comment->comment_ID . '; sending now.' ); delete_comment_meta( $comment->comment_ID, 'akismet_delayed_moderation_email' ); wp_new_comment_notify_moderator( $comment->comment_ID ); wp_new_comment_notify_postauthor( $comment->comment_ID ); } delete_comment_meta( $comment->comment_ID, 'akismet_delay_moderation_email' ); } $response['comments'][ $guid ] = array( 'status' => 'success' ); } } break; case 'submit-ham': case 'submit-spam': // Nothing to do for submit-ham or submit-spam. break; default: // Unsupported endpoint. break; } /** * Allow plugins to do things with a successfully processed webhook request, like logging. * * @since 5.3.2 * * @param WP_REST_Request $request The REST request object. */ do_action( 'akismet_webhook_received', $request ); Akismet::log( 'Done processing webhook.' ); return rest_ensure_response( $response ); } } /** * Astra Theme Extension * * @package Astra Addon */ if ( ! function_exists( 'astra_get_theme_name' ) ) { /** * Get theme name. * * @return string Theme Name. */ function astra_get_theme_name() { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedFunctionFound $theme_name = __( 'Astra', 'astra-addon' ); return apply_filters( 'astra_theme_name', $theme_name ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound } } /*! elementor-pro - v3.31.0 - 05-08-2025 */ *[data-editable-elementor-document] { position: relative; } *[data-editable-elementor-document]:before, *[data-editable-elementor-document]:after { content: ""; display: table; } *[data-editable-elementor-document] .elementor-document-handle { position: absolute; z-index: 2147483620; cursor: pointer; inset: 0; display: none; border: 2px solid var(--e-a-color-primary, #F3BAFD); } *[data-editable-elementor-document] .elementor-document-handle:before { content: ""; position: absolute; background: var(--e-a-color-primary, #F3BAFD); opacity: 0.3; inset: 0; } *[data-editable-elementor-document] .elementor-document-handle__inner { display: none; align-items: center; position: absolute; top: 0; left: 50%; transform: translateX(-50%); background: var(--e-a-color-primary, #F3BAFD); padding: 8px 16px; font-family: Roboto, Arial, Helvetica, sans-serif; font-size: 12px; line-height: 14px; color: var(--e-p-border-global-invert, #0C0D0E); gap: 8px; border-radius: 0 0 3px 3px; } *[data-editable-elementor-document] .elementor-document-handle__inner:before, *[data-editable-elementor-document] .elementor-document-handle__inner:after { content: ""; position: absolute; border: solid transparent; } *[data-editable-elementor-document] .elementor-document-handle__inner:before { right: calc(100% - 1px); border-inline-end-color: var(--e-a-color-primary, #F3BAFD); border-width: 0 14px 30px 0; } *[data-editable-elementor-document] .elementor-document-handle__inner:after { left: calc(100% - 1px); border-inline-start-color: var(--e-a-color-primary, #F3BAFD); border-width: 0 0 30px 14px; } *[data-editable-elementor-document].e-embedded-document-active .elementor-document-handle:not(.elementor-document-save-back-handle), *[data-editable-elementor-document].elementor-widget-container .elementor-document-handle:not(.elementor-document-save-back-handle) { bottom: unset; border-block-end: unset; } *[data-editable-elementor-document].e-embedded-document-active .elementor-document-handle:not(.elementor-document-save-back-handle)::before, *[data-editable-elementor-document].elementor-widget-container .elementor-document-handle:not(.elementor-document-save-back-handle)::before { display: none; bottom: unset; } .elementor-editor-active *[data-editable-elementor-document]:not(.elementor-edit-mode):hover .elementor-document-handle:not(.elementor-document-save-back-handle) { display: block; } .elementor-editor-active *[data-editable-elementor-document]:not(.elementor-edit-mode):hover .elementor-document-handle:not(.elementor-document-save-back-handle)::before { display: block; } .elementor-editor-active *[data-editable-elementor-document]:not(.elementor-edit-mode):hover .elementor-document-handle:not(.elementor-document-save-back-handle) .elementor-document-handle__inner { display: flex; } .elementor-editor-active *[data-editable-elementor-document].loading { opacity: 0.5; } .elementor-editor-active *[data-editable-elementor-document][data-elementor-type=single] > .elementor-document-handle { transform: translateX(-50%) translateY(-100%); border-radius: 3px 3px 0 0; } .elementor-editor-active *[data-editable-elementor-document][data-elementor-type=single] > .elementor-document-handle:before { border-width: 30px 14px 0 0; } .elementor-editor-active *[data-editable-elementor-document][data-elementor-type=single] > .elementor-document-handle:after { border-width: 30px 0 0 14px; } [data-elementor-post-type=elementor_library] > .elementor-document-handle { border: 2px solid var(--e-p-border-global, #5EEAD4); } [data-elementor-post-type=elementor_library] > .elementor-document-handle:before { background-color: var(--e-p-border-global, #5EEAD4); } [data-elementor-post-type=elementor_library] > .elementor-document-handle .elementor-document-handle__inner { background-color: var(--e-p-border-global, #5EEAD4); } [data-elementor-post-type=elementor_library] > .elementor-document-handle .elementor-document-handle__inner:before { border-inline-end-color: var(--e-p-border-global, #5EEAD4); } [data-elementor-post-type=elementor_library] > .elementor-document-handle .elementor-document-handle__inner:after { border-inline-start-color: var(--e-p-border-global, #5EEAD4); } .elementor-widget.elementor-sticky--effects .elementor-editor-widget-settings { right: -14px; } .elementor-editor-active .elementor.elementor-edit-mode .elementor-widget.elementor-global-widget:hover { outline: 1px solid var(--e-p-border-global); } .elementor-editor-active .elementor.elementor-edit-mode .elementor-widget.elementor-global-widget.elementor-element-editable, .elementor-editor-active .elementor.elementor-edit-mode .elementor-widget.elementor-global-widget.elementor-element-editable:hover { box-shadow: 0 0 0 2px var(--e-p-border-global); } .elementor-editor-active .elementor.elementor-edit-mode .elementor-global-widget .elementor-editor-widget-settings { background-color: var(--e-p-border-global); } .elementor-editor-active .elementor.elementor-edit-mode .elementor-global-widget .elementor-editor-widget-settings .elementor-editor-element-setting { background-color: var(--e-p-border-global); color: var(--e-p-border-global-invert); } .elementor-editor-active .elementor.elementor-edit-mode .elementor-global-widget .elementor-editor-widget-settings .elementor-editor-element-setting:hover { background-color: var(--e-p-border-global-hover); } .elementor-editor-active .elementor.elementor-edit-mode .elementor-global-widget .elementor-editor-widget-settings .elementor-editor-element-setting.elementor-editor-element-save { display: none; } .elementor-embedded-editor.elementor-location-header .elementor-section-wrap:not(:empty) + #elementor-add-new-section { display: none; } .elementor-editor-preview .elementor-edit-area-active[data-elementor-type=loop-item] .elementor-element-overlay, .elementor-editor-preview .elementor-edit-area-active[data-elementor-type=loop-item] .elementor-empty, .elementor-editor-preview .elementor-edit-area-active[data-elementor-type=loop-item] .elementor-add-section, .elementor-editor-preview .elementor-edit-area-active[data-elementor-type=loop-item] .elementor-add-section-inline, .elementor-editor-preview .elementor-edit-area-active[data-elementor-type=loop-item] .elementor-empty-view, .elementor-editor-preview .elementor-edit-area-active[data-elementor-type=loop-item] .elementor-widget-empty { display: initial; } .elementor.elementor-edit-area-active .elementor-document-handle.elementor-document-save-back-handle { display: block; bottom: unset; border: unset; } .elementor.elementor-edit-area-active .elementor-document-handle.elementor-document-save-back-handle:before { display: none; } .elementor.elementor-edit-area-active .elementor-document-handle.elementor-document-save-back-handle > .elementor-document-handle__inner { display: flex; opacity: 1; } .elementor-document-handle.elementor-document-save-back-handle .elementor-document-handle__inner { transform: translateX(-50%) translateY(-100%); border-radius: 3px 3px 0 0; } .elementor-document-handle.elementor-document-save-back-handle .elementor-document-handle__inner:before { border-width: 30px 14px 0 0; } .elementor-document-handle.elementor-document-save-back-handle .elementor-document-handle__inner:after { border-width: 30px 0 0 14px; } .elementor-document-handle.elementor-document-save-back-handle .eicon-arrow-left, .elementor-document-handle.elementor-document-save-back-handle .eicon-arrow-right { margin-inline-end: 5px; } .elementor-loop-container > div.elementor-edit-area-active:first-of-type { border: 2px solid #5EEAD4; } div[class*=elementor-widget-loop] .elementor-edit-area-active[data-editable-elementor-document], div[class*=elementor-widget-loop] .elementor-edit-area-active.swiper-slide-active, div[class*=elementor-widget-loop] .elementor-edit-area-active.e-loop-first-edit:first-of-type { border: 2px solid #5EEAD4; } div[class*=elementor-widget-loop] .elementor-edit-area-active .elementor-document-save-back-handle { display: flex; line-height: initial; white-space: nowrap; } div[class*=elementor-widget-loop] #elementor-add-new-section { margin: 30px auto; } div[class*=elementor-widget-loop] .elementor-add-section-inner { padding: 15px 0; } .elementor-edit-area-active[data-elementor-type=loop-item] .elementor-section-wrap { /* Overwrite the 25px min-height from the default .elementor-section-wrap:empty, as this causes unnecessary white space between the "Drag widget here" box and the document handles for in-place editing, making it look like this area is not vertically aligned on a new empty state. */ } .elementor-edit-area-active[data-elementor-type=loop-item] .elementor-section-wrap:not(:empty) + #elementor-add-new-section { display: none; } .elementor-edit-area-active[data-elementor-type=loop-item] .elementor-section-wrap:empty { min-height: 0; } .elementor-editor-active [class*=elementor-widget-loop] .elementor:not([data-editable-elementor-document]):not(.e-loop-alternate-template):hover { box-shadow: initial; } .elementor-editor-active [class*=elementor-widget-loop]:hover .elementor-document-handle:not(.elementor-document-save-back-handle) { display: flex; line-height: initial; white-space: nowrap; } .elementor-editor-active [class*=elementor-widget-loop]:hover .elementor-document-handle:not(.elementor-document-save-back-handle)::before { display: block; } .elementor-editor-active [class*=elementor-widget-loop]:hover .elementor-document-handle:not(.elementor-document-save-back-handle) .elementor-document-handle__inner { display: flex; } .elementor-editor-active [class*=elementor-widget-loop]:hover .e-loop-alternate-template { box-shadow: 0 0 0 1px #9DA5AE; } .e-loop-template-canvas { display: flex; align-items: center; justify-content: center; min-height: 100vh; } .e-loop-template-canvas [data-elementor-type=loop-item].e-loop-item { max-width: var(--preview-width, 410px); width: var(--preview-width, 410px); } .e-loop-template-canvas [data-elementor-type=loop-item].e-loop-item #elementor-add-new-section { width: var(--preview-width, 410px); } /*# sourceMappingURL=preview-rtl.css.map */ if ( ! function_exists( 'ipq_get_theme_image' ) ) { /** * Get image HTML for a specific context in a theme, specifying the exact sizes * for the image. The first image size is always used as the `src` and the other * sizes are used in the `srcset` if they're the same aspect ratio as the original * image. If any of the image sizes don't currently exist, they are queued for * creation by a background process. Example: * * echo ipq_get_theme_image( 1353, array( * array( 600, 400, false ), * array( 1280, 720, false ), * array( 1600, 1067, false ), * ), * array( * 'class' => 'header-banner' * ) * ); * * @param int $post_id Image attachment ID. * @param array $sizes Array of arrays of sizes in the format array(width,height,crop). * @param string $attr Optional. Attributes for the image markup. Default empty. * * @return string HTML img element or empty string on failure. */ function ipq_get_theme_image( $post_id, $sizes, $attr = '' ) { return \Image_Processing_Queue\Queue::instance()->get_image( $post_id, $sizes, $attr ); } } if ( ! function_exists( 'ipq_get_theme_image_url' ) ) { /** * Get image URL for a specific context in a theme, specifying the exact size * for the image. If the image size does not currently exist, it is queued for * creation by a background process. Example: * * echo ipq_get_theme_image_url( 1353, array( 600, 400, false ) ); * * @param int $post_id * @param array $size * * @return string Img URL */ function ipq_get_theme_image_url( $post_id, $size ) { return \Image_Processing_Queue\Queue::instance()->get_image_url( $post_id, $size ); } } __( 'Notes', 'elementor-pro' ); Linkedin | Career Change Announcement | AI Writing Assistant – dirprompt.com

Linkedin | Career Change Announcement | AI Writing Assistant

What This Form Does

You just accepted a new role and need to announce it on LinkedIn without sounding like you’re bragging or burning bridges.

This form creates an optimized prompt that helps you write career transition announcements balancing genuine excitement with diplomatic respect for your previous role–ensuring you thank the right people, express enthusiasm appropriately, and reconnect with your network naturally.

Perfect for professionals making career transitions–new jobs, promotions, company changes, or career pivots who need to announce diplomatically.

You’ll receive a ready-to-use prompt to generate your career announcement post.

Want Better Output? Start Here

⚡ Quick Start: The Most Important Fields

Getting a great career announcement is about giving the right context, not just facts. Three fields make the biggest difference in quality.

Transition Type

Be honest about what kind of change this is. “Internal promotion” needs a different tone than “leaving for competitor.”

[tip]The transition type adjusts your announcement’s diplomatic level automatically. Don’t sugarcoat it–accuracy helps the output match your actual situation. “Career pivot” triggers explanation language so people understand the switch before asking in comments.[/tip]

Tone Selection

Most people default to “humble and grateful,” but if you’re genuinely pumped about this move, choose “excited and confident.” Authenticity wins over playing it safe.

[tip]Simple test: If you told your best friend about this job over coffee, what energy would you have? That’s your tone. Your network can tell when you’re holding back. Choose the tone that matches how you actually feel, not what seems safest.[/tip]

Options and when to use them:

  • Excited & Confident: You’re genuinely pumped and not worried about appearing too enthusiastic
  • Humble & Grateful: Big milestone, but you’re focused on thanking people who helped
  • Balanced & Professional: Significant move, measured emotion, letting the facts speak
  • Reflective & Forward-Looking: Career pivot or major change requiring some explanation

Who to Thank

Keep this genuine and specific. “My manager Sarah who believed in me when I doubted myself” beats “my amazing team.”

[tip]Write this like you’re sending a thank you text, not giving a speech. Be specific about roles and contributions–generic thanks to “the team” feels performative. Example: “Huge thanks to my manager John who took a chance on someone with zero experience, and to the product team who taught me everything about shipping software.”[/tip]

The prompt uses your actual words here, so write like you’re talking to a friend, not accepting an award.


đŸŽ¯ Strategy & Best Practices: Career Change Announcements

[takeaway]Career announcements are about maintaining relationships while moving forward. Your former colleagues need to know you valued the experience. Your new company’s employees might be reading this. Your broader network needs a reason to stay connected. The announcement that works handles all three audiences simultaneously without feeling like you’re talking to everyone and no one.[/takeaway]

This section covers strategic considerations unique to career transition announcements. Understanding these principles will help you make better decisions when filling out the form and creating your content.

The Multiple Audience Challenge

Career transitions are simultaneously exciting and terrifying to announce. You’re proud of the new opportunity but worried about how your previous employer will react. You want to celebrate without looking like you’re bragging. You need to thank people without forgetting someone important.

[tip]Your announcement reaches three distinct audiences: (1) Former colleagues who need to know you valued the experience, (2) Your new company’s employees who might be reading this, (3) Your broader network who needs a reason to stay connected. The best announcements acknowledge all three without feeling scattered.[/tip]

Tone Matters More Than You Think

Too excited and you seem immature or like you hated your old job. Too humble and people wonder if you’re actually enthusiastic about the move. The sweet spot is “genuinely excited professional who remembers where they came from.”

[mistake]Don’t fake enthusiasm for a job you hated–people can smell that a mile away. If you had a good experience, say so warmly. If it was just “fine,” go neutral. Authenticity beats diplomatic fakeness every time.[/mistake]

That’s harder to nail than it sounds, which is why most career announcements feel slightly off. The tone selector helps you calibrate based on your actual emotional state, not what you think you should feel.

Timing Your Announcement

Post within your first week of starting. Announcing before you begin feels presumptuous (what if something changes?). Waiting more than a week means people already heard through the grapevine.

[tip]First week hits the sweet spot of timely and established. Post early morning (7-8am) on Tuesday-Thursday for maximum LinkedIn visibility. Monday is noisy, Friday sees less engagement.[/tip]

Bridge Preservation is Everything

Even if you hated your last job, neutral language serves you better. You’ll run into those people again. The professional world is smaller than you think.

Career transitions happen fast. You accept an offer, start in two weeks, and suddenly realize you need to tell your entire professional network. Most people draft something at 11pm the night before their first day, overthink every word, and post something that doesn’t quite capture how they actually feel.


âš ī¸ Common Mistakes to Avoid

The Humble-Brag Hybrid

“I’m so humbled and honored to announce…” No you’re not. You’re proud. That’s okay. Just say you’re proud.

[mistake]Stop using “humbled” when you mean “proud.” It’s become corporate speak for false modesty. Be genuinely humble by acknowledging who helped you, not by pretending you’re not excited about your achievement.[/mistake]

The Previous Employer Shade

Any hint of “finally leaving” or “new opportunity to actually grow” reads as bitter, even if subtle. Go neutral or skip mentioning them entirely if you left on bad terms.

[tip]Use the “Skip Reference” option if you left on bad terms. The prompt writes around it naturally, focusing 100% on the exciting new opportunity without any awkward previous job mentions.[/tip]

The Exhaustive Thank You List

Thanking 12 people by name makes everyone skim past it. Pick 2-3 people who truly made a difference and be specific about how.

[mistake]Don’t create an Oscar speech credits list. “Thanks to our engineering team, our early customers, and our advisors” works better than listing 15 names. If it feels like you’re thanking everyone so you don’t offend anyone, you’re doing it wrong.[/mistake]

The Vague Enthusiasm

“So excited for this new chapter!” tells people nothing. What specifically excites you about the role? That’s the interesting part.

Generic excitement is forgettable. Specific excitement creates conversation. “Excited to finally work on climate tech after a decade wanting to make this pivot” beats “excited for this journey” every time.

The Over-Explanation

If you’re making a career pivot, one good paragraph of rationale is enough. Don’t write an essay defending your decision before anyone questions it.

[tip]For career pivots, keep explanation to 2-3 sentences maximum. Focus on what attracted you to the new field, not what pushed you away from the old one. “I’ve always been fascinated by how technology solves healthcare problems” beats a long explanation of why you left your previous industry.[/tip]


đŸ’ŧ LinkedIn Best Practices & Tips

Front-Load The Announcement

Lead with the new role, then acknowledge the past. Don’t make people wait three paragraphs to find out what you’re announcing.

[tip]Opening structure that works: “After 4 incredible years at StartupX, I’m excited to share that I’m joining [New Company] as [Role].” This flows naturally–acknowledgment then announcement. Don’t bury the lead in paragraph three.[/tip]

Use Conversational Transitions

“After 4 incredible years at StartupX, I’m excited to share…” flows better than formal corporate speak. Write like you talk.

Avoid: “I am pleased to announce that I have accepted a position…”
Better: “I’m joining [Company] as [Role] and couldn’t be more excited.”

Create Curiosity About Your New Company

If you’re joining a lesser-known company, one compelling sentence about what they do helps people understand why you joined.

[tip]Example: “I’m joining Acme Corp–they’re building the infrastructure that lets hospitals share patient data securely across systems, solving one of healthcare’s biggest bottlenecks.” This context makes your decision make sense to readers.[/tip]

End With A Specific Invitation

“I’d love to hear from anyone working in FinTech–particularly around regulatory compliance” is more engaging than “Let’s stay connected!”

Questions that invite story-sharing or practical advice generate deeper engagement:

  • “What’s your best advice for someone starting a leadership role?”
  • “What surprised you most in your first 90 days of a similar transition?”
  • “For those in [industry], what should be on my radar?”

Tagging Strategy

Tag your new company. Consider tagging your previous company if you left on genuinely good terms. Skip tagging individuals unless they’re expecting it.

[mistake]Don’t tag everyone you mention. It clutters the post and feels like forced engagement farming. Tag the new company, maybe 1-2 key people who are expecting it, and that’s it.[/mistake]

Respond to Comments Promptly

The first few hours matter for LinkedIn’s algorithm. Thank people, answer questions, keep the conversation going. Career announcements are networking opportunities disguised as updates.


📋 Field-by-Field Guide

New Role & Company

Don’t just write “Senior Manager at TechCorp.” Your announcement reads better with context.

[tip]Try: “Senior Product Manager at TechCorp, leading the enterprise AI platform” or “Marketing Director at StartupX, building our brand in fintech.” Specificity makes your move real and interesting. “AI platform” or “fintech” gives people a conversation starter.[/tip]

❌ Generic: “Marketing Director at StartupX”
✅ Better: “Marketing Director at StartupX, building our brand in fintech”

Previous Role & Company

Provide similar context for where you’re coming from. This helps people understand the transition arc.

If you left on bad terms, you can skip detailed mention–the form handles this with the “Skip Reference” option in Transition Type.

Transition Type Selection

This field does heavy lifting behind the scenes:

  • Internal Promotion: Triggers gratitude language toward your current company
  • New Company – Same Industry: Balances excitement with diplomacy
  • New Company – Different Industry: Adds brief industry context
  • Career Pivot: Includes explanation language so people understand the switch
  • Returning After Break: Acknowledges the gap naturally
  • Entrepreneurship/Freelance: Frames independence positively

Select accurately–this calibrates the entire announcement’s approach.

Tone Calibration

Choose based on how you actually feel, not what seems safest:

  • Excited & Confident: You’re genuinely pumped, not worried about appearing too enthusiastic
  • Humble & Grateful: Big milestone, focused on thanking people who helped
  • Balanced & Professional: Significant move, measured emotion, letting facts speak
  • Reflective & Forward-Looking: Career pivot or major change requiring explanation

[tip]Most people choose “Humble & Grateful” because it feels safest. But if you’re actually excited, that’s more authentic and engaging. Match your actual emotional state, not what you think LinkedIn expects.[/tip]

What You’re Most Excited About

Be specific. What about this role excites you?

❌ “The opportunity to grow and learn”
✅ “Finally getting to work on climate tech after years of wanting to make this pivot”
✅ “Leading a team for the first time after 5 years of individual contribution”
✅ “Joining a company whose mission I’ve followed for years”

Specific excitement creates connection. Generic excitement is forgettable.

Gratitude & Acknowledgments

Write this like you’re sending a thank you text, not giving a speech. Be specific about roles and contributions.

❌ Stiff: “I would like to express my gratitude to my manager, John Smith, and my colleagues for their support”
✅ Natural: “Huge thanks to my manager John who took a chance on someone with zero experience, and to the product team who taught me everything about shipping software”

[tip]Focus on 2-3 people or groups maximum. Specific gratitude feels genuine. Generic thank-yous sound obligatory. Only mention people you genuinely want to acknowledge, not everyone to avoid hurt feelings.[/tip]

Previous Employer Reference

Choose how to handle your previous company:

  • Positive Mention: Left on good terms, want to acknowledge the experience
  • Neutral/Brief: It was fine, but not emphasizing it
  • Skip Reference: Left on bad terms, focusing 100% on new opportunity

If you choose “Skip Reference,” the announcement focuses entirely on your exciting new role without any awkward mentions of where you came from.

Career Pivot Explanation

If you’re making a significant industry or role change, briefly explain the “why” (2-3 sentences).

[tip]Focus on what attracted you to the new field, not what pushed you away from the old one. “I’ve always been fascinated by how technology solves healthcare problems, and this role finally lets me combine my clinical experience with product development” works perfectly.[/tip]

Don’t over-explain or defend. Just provide enough context so people understand the transition logic.

Personal Story or Context

Optional field for adding authenticity through a brief personal moment or realization that led to this change.

Example: “Six months ago, I realized I was spending more time thinking about product strategy than legal briefs. That’s when I knew it was time to make the leap.”

Only include if you have something authentic to share. Don’t force it.

Future Goals or Vision

Brief statement about what you hope to accomplish in this new role or where you’re headed.

Keep it concise: “Looking forward to helping mid-market companies finally get access to enterprise-grade security tools” or “Excited to build a team that ships products customers actually love.”


đŸ’Ŧ Frequently Asked Questions

Should I wait until my first day to post, or announce right after accepting?

Post within your first week of starting. Announcing before you begin feels presumptuous (what if something changes?). Waiting more than a week means people already heard through the grapevine. First week hits the sweet spot of timely and established.

What if I left my previous job on bad terms–can I just skip mentioning it?

Absolutely. Use “Skip Reference” in the previous employer dropdown. The announcement focuses 100% on the exciting new opportunity without any awkward previous job mentions. You can frame it entirely around what you’re moving toward, not what you’re leaving.

How much detail should I share about my new role?

Share what you’re excited about and what you’ll be working on generally. Skip anything that might be confidential–specific metrics, team size, reporting structure, or anything you wouldn’t say in a coffee shop conversation with an acquaintance.

[tip]Good examples: “Leading product strategy for our enterprise platform” or “Building our marketing function from the ground up.” Avoid: Specific revenue targets, organizational charts, or confidential product details.[/tip]

I’m making a huge career pivot–should I explain why I’m leaving my field?

Yes, but keep it to 2-3 sentences maximum. Focus on what attracted you to the new field, not what pushed you away from the old one.

Good example: “I’ve always been fascinated by how technology solves healthcare problems, and this role finally lets me combine my clinical experience with product development.”

Should I tag my previous company in the post?

Only if you left on genuinely positive terms and want to maintain that public relationship. If there’s any doubt, skip it. You can thank the company in your text without tagging them directly.

What if I’m nervous about my new company seeing my announcement?

That’s actually a feature, not a bug. Your new colleagues will likely see your post–that’s networking gold. Make sure your announcement shows genuine excitement about joining them. They want to see you’re proud to be there.

How do I avoid sounding like I’m bragging?

Acknowledge the journey and thank people who helped. “After 6 months of searching and some amazing mentorship from colleagues, I’m thrilled to share…” feels earned, not boastful. Gratitude is the antidote to bragging.

[takeaway]Posts that acknowledge team contributions and the path you took get significantly more positive engagement than solo celebration posts. People want to see you recognize the humans behind your success.[/takeaway]

I have three different audiences (old colleagues, new company, broader network)–how do I write for all of them?

This is exactly what the form handles. The prompt crafts language that thanks your old colleagues, shows enthusiasm for your new company, and invites your broader network to stay connected–all without feeling like you’re talking to everyone and no one.

The key is layering: gratitude for the past, excitement for the present, invitation for the future.


đŸŽ¯ Ready to create your career announcement?

Fill out the form below and get a perfectly calibrated prompt that captures your unique situation. The prompt handles the diplomatic complexity while keeping your authentic voice front and center.

[takeaway]Career announcements work when they balance genuine excitement with diplomatic respect, acknowledge the people who helped you get here, and invite your network to stay connected as you move forward. Let the form guide you through these elements–you’ll get an announcement that feels authentically you.[/takeaway]

How Was Your Experience?

Your feedback helps us create better templates.

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Need Help?

For tips on how to get the best results from this form, see more information here.

Form Designer

This form was created and designed by Eyal Doron.

Scroll to Top