A new custom post type has a working single view and an archive that 404s, and it starts working the moment somebody opens Settings → Permalinks. The rewrite rules are cached in an option; registering a post type adds rules to the in-memory set but never writes them. The fix is one flush, in the right place, and the right place is not obvious.
function catalogue_register_types() {
register_post_type( 'datasheet', array(
'public' => true,
'has_archive' => true,
'rewrite' => array( 'slug' => 'datasheets' ),
) );
}
add_action( 'init', 'catalogue_register_types' );
register_activation_hook( __FILE__, function () {
catalogue_register_types(); // init has already fired; call it by hand
flush_rewrite_rules();
} );
register_deactivation_hook( __FILE__, 'flush_rewrite_rules' );
That extra call is the part everyone leaves out, and it is why so many plugins flush on activation and still 404. Activation happens on an admin request where init ran before your plugin file was loaded, so your init callback has not executed — flushing at that point writes a rule set that does not contain the post type. Calling the registration function directly first is the whole fix. The deactivation hook cleans up so the URLs stop resolving when the plugin is off. What this does not cover is changing the slug in a later release, which is not an activation: that needs a flush gated on a stored version number, run once per deploy. Never on plain init — the rebuild walks every registered rule and writes an option, on every request.