Create a free digital business card, then share it by link, QR code, or NFC tap.

Digital Business Card Template for WordPress: The Hard Part

A finished looking contact card page on a laptop screen, with the small contact file that makes it work sitting just outside the frame

Don't Get Left Behind!

Create a free Zapped Digital Business Card with a shareable profile, QR code, and modern contact page.

Make networking and exchanging contact information a breeze.

What a digital business card template for WordPress actually gives you

A digital business card template for WordPress gives you the visible card, and you can assemble that yourself out of core blocks without buying anything. What it does not give you is the file behind the save to contacts button, because WordPress does not accept that file into the media library by default.

The visible half is the easy half.

That one gap reorganizes the whole decision. Whichever route you pick, a page of blocks, a pattern, a custom template, a plugin, or a whole theme, the card design part is largely solved and the delivery part is not. The routes differ mainly in where their code lives once you change themes.

If what you want is the general job rather than the WordPress route, start with how to make a digital business card and come back when the platform is decided.

Building the card page out of core blocks

Four core blocks and you have the card. A Group block holds it, an Image block carries the photo, a Buttons block carries call and email, and a Social Icons block carries the profiles.

The core File block is described in the block reference as adding a link to a downloadable file. That is exactly what it does, and exactly where its responsibility ends.

Build the card out of blocks rather than one Custom HTML block. The Custom HTML block takes raw markup, and raw markup on the page depends on the unfiltered_html capability, which on Multisite only Super Admins hold. A colleague who lacks it and edits the page saves that markup away, for every visitor, with no warning.

The Block Editor Handbook records that block content is serialized into the post as HTML annotated with comment delimiters, so the card's content lives with the page rather than with the theme. The content is the most portable thing you are about to build. The button that makes it work is the least.

The markup semantics underneath are the same on any platform: the contact element, the link forms, the alt text. None of that is a WordPress question.

The save to contacts file WordPress will not let you upload

WordPress developer reference page listing the default file extensions mapped to media types, with the text group visible and no vCard entry among them

Source: WordPress developer reference, wp_get_mime_types, captured 2026-08-06.

The default state is checkable in one page of the reference. The map returned by wp_get_mime_types() has no vcf entry, no vcard entry, no text/vcard, and no text/x-vcard. The text group runs nine entries: txt|asc|c|cc|h|srt, csv, tsv, ics, rtx, css, htm|html, vtt, and dfxp. One of them, ics, maps to text/calendar. Calendar files are on that list. Contact files are not.

Permitting the extension, and why that may not finish the job

The documented way to permit a type for upload is the upload_mimes filter, which WordPress describes as filtering the list of allowed mime types and file extensions, and which core applies inside get_allowed_mime_types(). Its reference page shows the array going both ways, adding entries and unsetting one.

The note about scope sits on a different page. wp_get_mime_types() says of its own mime_types filter: "This filter should be used to add, not remove, mime types. To remove mime types, use the 'upload_mimes' filter." That note sends removals to upload_mimes, and the filter's own examples add as readily as they remove.

add_filter( 'upload_mimes', 'mrdn_allow_vcard' );

function mrdn_allow_vcard( $mimes ) {
    $mimes['vcf'] = 'text/vcard';

    return $mimes;
}

That is the whole documented step, and its smallness is the point. The array is keyed by the file extension regex, you add one entry, you return the filtered value. Miss the return and the media library silently keeps the unmodified list.

Permitting the extension does not automatically finish the job. WordPress then runs wp_check_filetype_and_ext(), which attempts to determine the real type from the file's contents using PHP's finfo_file() where that is available and falls back to the extension when it is not.

Whether a given contact file clears that step depends on the fileinfo database on your own server, so confirm your upload actually succeeds instead of assuming it. The wp_check_filetype_and_ext filter receives the sniffed type, and that is where you would intervene if it does not.

The allowed set already varies by who is uploading. WordPress removes htm|html and js from the list for any user without the unfiltered_html capability, so "it uploads for me" is not a statement about your site.

One constant bypasses type checking altogether, and WordPress documents it in terms worth quoting rather than paraphrasing. The unfiltered_upload capability "is not available to any role by default (including Super Admins)", and it exists only once ALLOW_UNFILTERED_UPLOADS is defined in the configuration file.

On Multisite, only Super Admins can be given it at all. That is a caveat to know about, not a step to take.

Generating the file instead of uploading it

Generate the file from the same fields the page already holds. The upload problem disappears, and so does the second one, which is keeping an uploaded file in step with a card you keep editing.

WordPress documents every part of that route separately. add_rewrite_rule() adds a rule that transforms a URL structure into a set of query vars, it has to run on init, and a custom query variable is only readable through get_query_var() once it is registered on the query_vars filter.

add_action( 'init', 'mrdn_register_card_route' );

function mrdn_register_card_route() {
    add_rewrite_rule(
        '^card/([^/]+)\.vcf$',
        'index.php?mrdn_card=$matches[1]',
        'top'
    );
}

add_filter( 'query_vars', function ( $vars ) {
    $vars[] = 'mrdn_card';
    return $vars;
} );

Two things break this in practice. Register the rule outside init and it never enters the rules array. Skip the query_vars filter and the rule matches while your handler reads an empty variable, which looks identical to a routing failure and is not one.

Rewrite rules also have to be regenerated before a new rule answers anything. WordPress's own instruction is to open the permalinks screen and save it without changing anything, and the reference calls flush_rewrite_rules() an expensive operation.

Contributors on that reference page suggest calling it from the activation hook rather than on init. Others there recommend deleting the rewrite rules option instead. Either way it is contributed guidance rather than a documented requirement.

add_rewrite_endpoint() is the other documented shape, adding an endpoint "like /trackback/" through an endpoint mask and exposing whatever follows the endpoint name as a query variable. Its reference names the hook to test that variable on, which is template_redirect.

add_action( 'template_redirect', 'mrdn_serve_card' );

function mrdn_serve_card() {
    $slug = get_query_var( 'mrdn_card' );

    if ( ! $slug ) {
        return;
    }

    $body = mrdn_build_vcard( $slug );

    if ( headers_sent() ) {
        return;
    }

    header( 'Content-Type: text/vcard; charset=utf-8' );
    header(
        'Content-Disposition: attachment; filename="' . sanitize_file_name( $slug ) . '.vcf"'
    );

    echo $body;
    exit;
}

template_redirect fires before WordPress determines which template to load, which is early enough that nothing has been printed yet. That timing is the entire reason the two headers in the handler arrive at all.

Note what the handler does not contain. mrdn_build_vcard() is called and never written here, because what goes inside a contact file is a format question rather than a WordPress question.

Escape values as late as possible, which is WordPress's own guidance, and pick the function that matches where the value lands. The filename in that header is not HTML, so it goes through sanitize_file_name(), which WordPress describes as removing characters that are illegal in filenames on some operating systems.

Do not gate the download behind a nonce. WordPress documents a nonce lifetime as variable between 12 and 24 hours, and a printed QR code lasts considerably longer than that.

The REST API is the documented alternative if you would rather not touch rewrite rules.

add_action( 'rest_api_init', function () {
    register_rest_route(
        'meridian/v1',
        '/card/(?P<slug>[a-z0-9-]+)',
        array(
            'methods'             => 'GET',
            'callback'            => 'mrdn_rest_serve_card',
            'permission_callback' => '__return_true',
        )
    );
} );

The namespace is not decoration. The REST handbook is blunt that failing to namespace is analogous to skipping a vendor prefix in a theme or plugin. Since WordPress 5.5 a route without a permission_callback raises a notice, and __return_true is the documented value for a route that really is public, which a business card endpoint is.

None of this is one recipe in the WordPress documentation. Each piece is documented on its own page, and the assembly is ours. Worth knowing before you go hunting for an official page that says otherwise.

Diagram of a request reaching a custom WordPress route, showing the point where response headers can still be set and the later point, after output begins, where a header call has no effect

If a contact file is not the shape you want at all, the alternative is putting the details into the code itself, which a vCard QR code does and which has its own tradeoffs. For the reader who wants the QR built rather than the file served, generating a vCard QR code covers that path end to end.

The header that never arrives

When the headers never arrive, the download opens as a wall of text. Something printed output before your headers did, and WordPress core treats that state as unrecoverable rather than as something to work around: nocache_headers() opens by checking headers_sent() and returns without sending anything at all.

A stray blank line after a closing PHP tag is enough. So is one plugin echoing a notice early in the request. Nothing on the page looks broken when this happens, which is what makes it expensive to find.

send_headers is the documented place to add headers, described as firing once the requested HTTP headers for caching and content type have been sent. A contributed note on that page adds that since WordPress 6.1 it runs later in core load, after pre_get_posts, so conditional tags work inside it. That is a contributor's account, not part of the hook's own description.

If you took the media library route instead, the type your server sends is not a WordPress setting at all. It comes from the web server's own extension mapping, which on Apache is the mime.types file named by TypesConfig and is overridable per extension with AddType.

That is why there is no universal answer to what your host serves for a contact file, and why the only reliable move is to read your own response.

curl -sI https://yoursite.example/card/mara-okonjo.vcf

Run it against your own URL and read the two headers that come back. If the type is wrong, you now know whether to fix a route, a server configuration, or a plugin printing early.

Five routes, and what each one costs you later

A page assembled from core blocks. A registered block pattern. A custom template in a child theme. A plugin. A dedicated theme. Each of the five is documented by WordPress, and each is judged here on the same four things.

What you have to know to build it. What breaks when something updates. Where its code lives. Whether it can serve the file on its own.

There is no winner in the table, because the four columns do not resolve to one. Read the row that matches the site you already have.

Route What you have to know What breaks on an update Where its code lives Serves the file alone?
A page of core blocks The block editor Least. Core blocks are core Content sits with the post as serialized block HTML. Styling comes from the theme No. The File block adds a link and settles nothing about the response
A registered block pattern A file with a header in the theme's /patterns folder, or register_block_pattern() on init The pattern registration, when the theme changes The pattern belongs to the theme. Content already inserted sits with the post No
A custom template in a child theme The block theme Page hierarchy and a child theme with a matching Template header Little. Child themes exist so parent updates do not destroy modifications A /templates folder inside the theme, child before parent No
A plugin Plugin conventions, prefixing, and the route above Your own code, or somebody else's maintenance decisions Its own plugin directory, outside the theme Yes. This is the only route that owns delivery
A dedicated theme All of the above plus theme.json and a full template set Most surfaces at once The theme, entirely Only by carrying functionality that theme review puts elsewhere

Two details in that table are worth spelling out. In a block theme the Page hierarchy resolves {custom-template}.html, then page-{post_name}.html, then page-{post_id}.html, then page.html, then index.html, and templates in a child theme's /templates folder are consulted before the parent's. index.html is the only template a block theme is required to ship. Classic themes use a PHP templating system instead.

theme.json sits at the root of the theme directory and is where a block theme's settings, styles, custom templates, and template parts are configured. Its current schema is version 3, introduced in WordPress 6.6.

Diagram showing which parts of a WordPress contact card live with the post, which live inside the theme, and which live in a plugin

One architectural recommendation, then: presentation in the theme, the download route in a plugin. The boundary is WordPress's own.

The theme review requirements list functionality unrelated to design and presentation as plugin territory, naming analytics, SEO options, contact forms, resource caching, and share buttons among the examples. A route that serves a file is functionality by that definition, and a plugin is not a theme.

If you write that plugin yourself, prefix everything globally accessible with a unique identifier. WordPress's plugin guidance is direct about why: prefixes prevent conflicts with other plugins, and a function named serve_card will meet another one eventually.

What the plugin directory actually offers

Start with the install counts, which answer this better than any adjective. Each figure comes from the plugin's own directory page.

The plugin with the most installs in this space, Enable virtual card upload, sits above 7,000 active installations and does exactly one thing: it makes the file type uploadable.

The second, Enable vCard Upload, has passed 2,000 active installations and has gone eight years without an update. It is tested only up to WordPress 5.0.25, carries the directory's own warning that it has not been tested with the latest three major releases, and adds text/x-vcard to the allowed list. IANA lists that type as deprecated in favour of text/vcard.

Screenshot of a WordPress plugin directory listing showing its active installation count, a last updated value of eight years ago, and a tested up to version of 5.0.25

Source: WordPress.org plugin directory, Enable vCard Upload, captured 2026-08-06.

The plugins that actually build a card are much smaller. Smart Contact Card is the most current one that surfaced, above 100 active installations, tested up to WordPress 7.0.2, offering a downloadable contact file, a shortcode, and an Elementor widget. It has no reviews submitted, and its QR codes are generated by an outside service, QuickChart.io.

Business Card Template sits above 60 installations. Its listing advertises a contact file download, and separately a display shortcode, with nothing said about how the two relate. ECard, above 300 installations, describes contact details, social links, and galleries, and its listing does not mention a file download at all, which is a fact about the listing rather than about the code.

Those are the most installed plugins that surfaced, not a census of the directory. The pattern across them is still clear enough to act on: the shim that unlocks an upload has more installations than every plugin that builds a card put together, and the shim is the piece you least need if you generate the file instead.

Adding structured data next to the SEO plugin you already run

You are not adding structured data to a blank page. That is the whole WordPress difference here, and it is why advice written for a hand built page misfires.

wp_head is the documented hook for printing into the head of a front end page, and it runs only when the theme calls wp_head(). Every block theme does.

add_action( 'wp_head', 'mrdn_print_card_schema' );

function mrdn_print_card_schema() {
    if ( ! is_page( 'card' ) ) {
        return;
    }
    $data = array(
        '@context' => 'https://schema.org',
        '@id'      => home_url( '/card/#person' ),
    );
    echo '<script type="application/ld+json">' . wp_json_encode( $data ) . '</script>';
}

What that block demonstrates is the hook and the identifier, not the vocabulary. The properties describing a person belong to the format question rather than to WordPress. What matters here is the @id, because it is how a separate item connects to what is already on the page instead of competing with it.

Yoast does not emit isolated snippets. Its documentation describes one JSON-LD script containing one or more @graph objects, with a distinct top level piece for each entity, linked by @id, and its base output on every page includes Organization or Person, WebSite, and WebPage.

Add a hand written block beside that and you have two descriptions of the same person on one page. Yoast names that failure itself, which is stronger than anyone else asserting it.

Its functional specification describes plugins, themes, or systems from outside pulling the graph apart, producing duplicate or conflated properties and shared ID spaces. The recommendation that follows, adopt Yoast's framework and use its APIs, is the vendor's. The failure description is the useful part.

Both major plugins publish the way in. Yoast exposes wpseo_schema_graph_pieces for adding or removing pieces and wpseo_schema_needs_<class-name> for toggling one, with wpseo_json_ld_output switching the whole thing off. Rank Math exposes rank_math/json_ld for modifying all of its JSON-LD output and rank_math/snippet/rich_snippet_{$schema_type} for controlling whether a given type is emitted.

Google's policies set the constraint that decides how much of this is worth doing. Marked up content has to be visible to readers of the page, and structured data has to be a true representation of the page content.

Google also recommends @id for linking separate related items, which is the same mechanism your SEO plugin is already using. A profile page, in Google's terms, has to focus on a single person or organization affiliated with the site. A card page is exactly that.

One more reason a hand added block goes missing: a script tag pasted into a Custom HTML block runs into unfiltered_html again. The filter route has no such condition.

Accessibility checks worth running, and what the badges do not mean

Three WordPress facts here, and each one narrows what a standard or a badge actually covers.

WordPress's accessibility coding standard is WCAG 2.2 at level AA, and it states its own scope: WordPress core, WordPress.org websites, and official plugins. The theme you installed from a marketplace and the plugin you added last week are not inside that boundary.

WordPress's public accessibility statement is scoped just as carefully. It aims to make the WordPress Admin and bundled themes fully compliant with WCAG 2.2 AA where possible, acknowledges features in development that may not yet comply, and states plainly that WordPress is not currently conforming with ATAG 2.0.

ATAG is the standard for authoring tools, meaning the tool that helps you produce accessible content. That sentence is WordPress being honest about its own editor, and it is the sourced answer to whether the block editor will catch your mistakes for you.

Then the badge. A theme tagged Accessibility Ready has met the theme review team's minimum standards, and the handbook says outright that this does not require WCAG AA level compliance. That is a review threshold, not a compliance certificate.

The accessibility ready requirement set is still a useful preflight list by title. It names a skip to content link, meaningful landmark roles and names, keyboard navigation support, controls with accessible names, roles and states, labeled form fields, and meaningful heading structure.

It goes on: underlined links in text, no ambiguous link text, sufficient colour contrast of text and controls, alternative text, support for reflow, resize and text spacing changes, no unexpected changes of context, no links opening new windows without warning, accessible hover and focus content, an accessibility statement, and support for the .screen-reader-text class.

Four checks are worth running on the card itself before you share it:

  • Tab through the entire card and confirm the focus indicator is visible on every control, including the social icons.
  • Check the social icons row at a real tap size on a phone, not at desktop cursor precision.
  • Check the card's text against the theme's own palette rather than against the colours you designed with.
  • Narrow the browser to phone width and confirm nothing needs sideways scrolling.

How fast a card page can be on WordPress

Findable and slow is the wrong shape for a page someone opens once, on cellular, standing in a conference hall. That is the specific risk, and the measurements are public.

The HTTP Archive's Web Almanac put WordPress sites at a 45 percent pass rate for all three Core Web Vitals on mobile in its 2025 CMS chapter, with a median mobile Lighthouse performance score of 41 against an SEO score of 92. Those two numbers next to each other are the whole problem in miniature.

The trend is genuinely improving, which the single year figure hides. The 2024 chapter recorded 40 percent, up from 28 percent the year before.

WordPress attributes the cost where you can act on it. Its optimization documentation points at plugins, caching layers, and static asset delivery, and says directly that the number of plugins and their performance will have a huge impact on a site's performance.

Which brings the route comparison back around. A card page is one of the very few WordPress pages that can legitimately load almost nothing, so every plugin the card route drags in is a real cost paid on a slow connection.

Once the page URL becomes a digital business card QR code printed on something, that cost is fixed for the life of the print run.

What it costs to keep running

The build above is finite. The maintenance is not, and it has five parts.

Three things update on their own schedules: WordPress core, the theme, and every plugin in the card's path. The route in the middle of this article is code you now own, on top of those three.

If the contact file is uploaded rather than generated, the card and the file are two copies of the same details, and nothing warns you when they drift.

The download is the fragile part, and it fails quietly. One byte of early output and the headers never arrive. The page still looks perfect.

A theme change moves the boundary. Templates and patterns live in the theme, the page's content does not, and knowing which artifact sits where before you switch is the entire point of the route table above.

And the plugin you picked may simply stop. One of the two most installed vCard plugins has not been updated in eight years and still registers a deprecated media type, and it did not announce that in advance.

Zapped takes the other side of that trade, where nothing needs updating after launch. The card stays live after it is shared, so a printed QR code and every link already handed out keep working once you edit the card's details. Per card analytics come with the product rather than with another plugin in the page's path.

On the Free plan that means one card with five content blocks, sixty days of analytics history, and a badge on the card that a Professional plan removes.

If you are weighing that against building it yourself, how electronic business cards work covers what the hosted side of the category actually does. And once the page exists, getting it opened is a separate job, which sharing a digital business card covers channel by channel.

Build it in WordPress for the reason that holds up: the card sits on a domain and a site you already own and control, next to everything else you publish. Put the download route where it survives the next redesign, which means outside the theme.

Sources

WordPress developer documentation, standards registries, plugin directory pages, and independent measurement, all checked August 6, 2026.

Visit Zapped.to to create your digital business card, QR code, and shareable contact profile.

Signup at Zapped.to for your free digital business cards.

Are you tired of using boring, outdated business cards to market your business? Are you looking for a modern, effective way to showcase your brand and make a lasting impression on potential clients and partners? Look no further than Zapped Digital Business Cards!

With Zapped, you can easily create and customize stunning digital business cards that showcase your brand, contact information, and social media profiles. These cards are not only more convenient and eco-friendly than traditional business cards, but they also allow you to add multimedia elements like videos and animations to make your card stand out.

Plus, Zapped's platform makes it easy to share your digital business card with anyone, anywhere. Simply send a link or scan a QR code to share your card with anyone with a smartphone or computer. No more fumbling around with physical cards that can get lost or damaged.

Upgrade your marketing game with Zapped Digital Business Cards. Try it out today and see the difference it can make for your business!

Visit Zapped.to for your free digital business card now.