Day One Importer
Day One Journals importer for WordPress plugin.
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/automattic/day-one-to-wp/archive/refs/heads/main.zipDay One Importer is a WordPress admin importer for Day One JSON exports. It creates one private WordPress post per Day One entry and attempts to preserve dates, journal categories, tags, text, supported photos, supported videos, supported audios, supported PDF attachments, per-entry location metadata, and per-entry weather metadata.
Try in WordPress Playground
You can launch a temporary WordPress site with Day One Importer installed and activated:
Playground runs in your browser and is useful for checking the importer screens with fictional or disposable exports. Avoid uploading private Day One exports unless you are comfortable testing them in that browser session. Playground storage is temporary and browser-backed; large ZIP uploads, media processing, and generated attachment URLs may behave differently from a normal WordPress host.
Requirements
- WordPress 6.4 or newer
- PHP 7.4 or newer
- A user account with permissions to import, upload files, and edit posts
- PHP
ZipArchiveextension is required so ZIP exports can be inspected with safety budgets before extraction
Install and activate
- Confirm PHP has the
ZipArchiveextension enabled. The importer requires it to inspect ZIP exports with safety budgets before extraction. - Install the plugin ZIP through the WordPress Plugins screen, or copy this plugin directory into your site's configured plugins directory.
- In WordPress admin, go to Plugins → Installed Plugins.
- Activate Day One Importer.
- Go to Tools → Import and confirm Day One is listed.
Export from Day One
- Open Day One and export your journal as JSON.
- Keep the original ZIP export intact. Do not manually edit the JSON before importing.
- The importer expects a ZIP containing one or more journal JSON files with an
entriesarray and, when photos, videos, audios, or PDFs are present, aphotos/,videos/,audios/, and/orpdfs/directory.
A typical export follows this shape: a journal JSON file plus media files in photos/ (and videos/ / audios/ / pdfs/ when the export contains video, audio, or PDF moments). Do not commit real Day One exports, photos, videos, audios, or PDFs to this repository; they can contain private journal content.
Import into WordPress
- In WordPress admin, go to Tools → Import.
- Choose Day One.
- Upload the Day One JSON export ZIP.
- Click Import Day One export. The upload is queued quickly, then the browser advances the import in short resumable requests with a WP-Cron fallback.
- Keep the importer page open when possible and watch the job panel for phase, progress, counters, warnings, and errors. You may refresh the page or use Retry / Continue after a network interruption.
- When the job is complete, review the summary counts and any warnings.
- Spot-check the imported private posts before deleting or closing a Day One account.
The results/status screen is designed to be privacy-safe: it reports counts, UUIDs, dates, filenames, and generic warning text rather than full journal entry content.
What is imported
-
One WordPress
postper Day One entry. -
Posts are created with
privatestatus by default. -
Day One
creationDateis used for the WordPress post date when valid. -
Day One tags are assigned as WordPress post tags.
-
Each Day One journal is assigned as a WordPress post category, using the journal JSON filename when the export does not provide an explicit journal name.
-
Day One text is imported conservatively as safe HTML. Raw HTML is escaped and shortcode-like text such as
[gallery]is neutralized so it remains visible text rather than executing. -
When a Day One entry ships a
richTextpayload (newer exports), the importer reads it and emits one block per text run. Plain paragraph runs become paragraph blocks, and runs beginning with#,-, or*still become the corresponding markdown-derived heading or list block when no explicit line attribute is set — matching the way the same content rendered for legacytext-only entries. Supported run-level inline formatting is honored:bold,italic,strikethrough,inlineCode,linkURL(http(s) only),autolink(when paired with alinkURL), andhighlightedColor(Day One's0xRRGGBBform) are wrapped in<strong>,<em>,<s>,<code>,<a href="…">, and<mark style="background-color:#RRGGBB">respectively. Non-http(s) links and malformed highlight colors are dropped with a privacy-safe warning (the run's text still renders). Legacy entries that ship only the plaintextfield continue to import exactly as before. -
Day One richText
attributes.lineblock-level structures are mapped to native Gutenberg blocks:headervalues 1–6 becomecore/headingblocks at the matching level (intra-text newlines render as<br />). Adjacent headings stay as separate blocks.headervalues outside 1–6 fall through to the paragraph path.listStyle: bulletedandlistStyle: numberedbecomecore/listblocks (withcore/list-itemchildren). Numbered lists honor the first item'slistIndexas thestartattribute when it is an integer (or numeric string)>= 2. Nested levels (indentLevel) render as Gutenberg-native parent-child nestedcore/listblocks inside the parent list item.listStyle: checkboxbecomes acore/listwith thetask-listclassName. Each item is prefixed with a Unicode ballot-box glyph —☑(☑, U+2611) whenchecked: true,☐(☐, U+2610) otherwise — followed by a single space. The checkbox is purely visual: Day One captures the checked state at export time andcore/list-itemdoes not accept<input>content inside the editor, so the glyph approach survives bothwp_kses_postand Gutenberg block validation while keeping the recognizable rendering.codeBlock: trueruns collapse into onecore/codeblock, joined with single\nseparators. Inline-formatting wrappers are intentionally not applied inside code (no<strong>/<em>/<a>inside<code>), so anylinkURLorhighlightedColorattribute on a code item is ignored without recording a warning.quote: trueruns collapse into onecore/quoteblock with one child paragraph per item. QuoteindentLevelis intentionally ignored (Day One uses it as typographic spacing); all quote items render as flat siblings inside the same<blockquote>. Inline formatting wrappers still apply inside headings, list items, and quote paragraphs.
-
Supported photos are imported into the Media Library, attached to the imported post, and placed in the post content. When a Day One entry ships a
richTextpayload that lists photos viaembeddedObjects, each photo block renders at the position its embed appears in the text stream — a single photo becomes acore/imageblock between the surrounding paragraphs, and consecutive embeds collapse into a singlecore/galleryblock. Legacytext-only entries continue to append photos after the entry text in Day One entry order. If arichTextentry has imported photos but no inlineembeddedObjectsreferences for them, the photos still attach to the post and remain visible in the Media Library, but they are not rendered in the body and a privacy-safe warning is recorded. -
Every imported photo carries a
_day_one_photo_formatattachment meta marker holding the lowercased Day Onephoto.typevalue (for examplegif,jpeg,png,heic). The marker is uniform across formats and is the signal the content layer uses to decide how to render a given photo block. Animated GIF photos are preserved: thecore/imageblock emitted for a GIF photo references the original full-size file URL returned bywp_get_attachment_url()and usessizeSlug: "full"with awp-block-image size-fullfigure class, so the GIF stays animated in the post body instead of being replaced by a flattened first-framelargederivative. Non-GIF photos keep the existinglargederivative ladder unchanged, so JPEG and PNG posts render identically to prior releases. Mixed galleries (for example a GIF alongside a JPEG in the same entry) emit per-image correctsizeSlugvalues automatically because the gallery serializer delegates to the per-image block builder. The_day_one_photo_formatmarker is written only for newly imported attachments; previously imported GIFs do not have the marker and continue to render through thelargederivative path until they are re-imported. Hosts running image-optimization plugins that transcode GIFs post-upload (Jetpack Image CDN, EWWW Image Optimizer, similar) may still flatten animation after the file lands inwp-content/uploads/; that path is outside this plugin's control. -
Day One
entry.videos[]are imported alongside photos. ZIP preflight discovers any top-levelvideos/directory next to the existingphotos/discovery, so a Day One export that ships both is recognized in a single pass. Each video is sideloaded into the same protectedday-one-importer-privateuploads subfolder used for photos, served through the same nonce- and permission-checked WordPress media endpoint, and attached to the imported post. Importer markers (_day_one_uuid,_day_one_media_identifier,_day_one_media_md5,_day_one_source = day-one-export,_day_one_media_kind = video, plus video-specific_day_one_video_duration,_day_one_width,_day_one_height,_day_one_media_date, and_day_one_original_filenamewhen present) are written to the attachment so reruns deduplicate by UUID + identifier (or md5) without creating duplicate attachments. When arichTextpayload lists a video viaembeddedObjects[].type === "video", the video renders inline at the position the embed appears in the text stream as acore/videoblock (<figure class="wp-block-video"><video controls src="…"></video></figure>). Interleaved photo + video sequences emit blocks in scan order — consecutive photos still collapse into a singlecore/imageorcore/gallery, and each video produces onecore/videoblock in between, splitting a run of photos when a video is interposed. -
The importer's MIME allowlist gate is extended to accept
video/*(in addition to the existingimage/*path). A video is sideloaded only when bothwp_check_filetype_and_ext()reports avideo/*MIME and that MIME appears in the site'sget_allowed_mime_types()list. On a default WordPress install Administrator-role users havevideo/quicktimeandvideo/mp4enabled, so the typical.movand.mp4files from a Day One export are accepted. If a site explicitly removes those MIMEs (via theupload_mimesfilter or multisite Add to mime types settings), the affected embed is dropped with a privacy-safe warning ("Skipping embedded video in Day One entry: referenced media file is unsupported or missing."). No attachment is created, nocore/filefallback is emitted, and the warning contains no identifier, UUID, filename, md5, or path. To support additional video MIME types on your site, extend the uploader allowlist via the standard WordPressupload_mimesfilter (or your multisite Add to mime types settings). -
Day One
entry.audios[]are imported alongside photos and videos. ZIP preflight discovers any top-levelaudios/directory next to the existingphotos/andvideos/discovery, so a Day One export that ships audio is recognized in the same preflight pass. Each audio file is sideloaded into the same protectedday-one-importer-privateuploads subfolder used for photos and videos, served through the same nonce- and permission-checked WordPress media endpoint, and attached to the imported post. Importer markers (_day_one_uuid,_day_one_media_identifier,_day_one_media_md5,_day_one_source = day-one-export,_day_one_media_kind = audio, plus audio-specific_day_one_audio_duration(when the source provides a positive numeric value),_day_one_audio_title(when the Day One record carries a non-empty title),_day_one_media_date, and_day_one_original_filenamewhen present) are written to the attachment so reruns deduplicate by UUID + identifier (or md5) without creating duplicate attachments. Width and height are intentionally not persisted on audio attachments (Day One ships zeros for both). When arichTextpayload lists an audio viaembeddedObjects[].type === "audio", the audio renders inline at the position the embed appears in the text stream as acore/audioblock (<figure class="wp-block-audio"><audio controls src="…"></audio></figure>). When the Day One record'stitleis non-empty, the block also renders a<figcaption class="wp-element-caption">with that title underneath the player. Interleaved photo + video + audio sequences emit blocks in scan order: consecutive photos still collapse into a singlecore/imageorcore/gallery, and each video and each audio produces onecore/videoorcore/audioblock in between, splitting a run of photos when a video or audio is interposed. -
The MIME allowlist gate (
Day_One_Importer_Media::validate_media_file()) now also acceptsaudio/*(in addition to the existingimage/*andvideo/*paths). An audio is sideloaded only when bothwp_check_filetype_and_ext()reports anaudio/*MIME and that MIME appears in the site'sget_allowed_mime_types()list. On a default WordPress install, Administrator-role users haveaudio/mpeg,audio/mp4, andaudio/aacenabled, so typical.mp3,.m4a, and.aacfiles from a Day One export are accepted. Day One records shipped as.lpcm(linear PCM —audio/L16/audio/L24) are reliably not in WordPress's default allowlist and are dropped with a privacy-safe warning ("Skipping embedded audio in Day One entry: referenced media file is unsupported or missing."). Any audio whose MIME the site refuses follows the same drop path. No attachment is created, nocore/filefallback is emitted, and the warning contains no identifier, UUID, filename, md5, or path. To support additional audio MIME types on your site, extend the uploader allowlist via the standard WordPressupload_mimesfilter (or your multisite Add to mime types settings). -
Day One
entry.pdfAttachments[]are imported alongside photos, videos, and audios. ZIP preflight discovers any top-levelpdfs/directory next to the existingphotos/,videos/, andaudios/discovery, so a Day One export that ships PDFs is recognized in the same preflight pass. Each PDF file is sideloaded into the same protectedday-one-importer-privateuploads subfolder used for photos, videos, and audios, served through the same nonce- and permission-checked WordPress media endpoint, and attached to the imported post. Importer markers (_day_one_uuid,_day_one_media_identifier,_day_one_media_md5,_day_one_source = day-one-export,_day_one_media_kind = pdf, plus the pdf-specific_day_one_pdf_namewhen the Day One record carries a non-emptypdfName) are written to the attachment so reruns deduplicate by UUID + identifier (or md5) without creating duplicate attachments. Width, height, duration, and date are intentionally not persisted on PDF attachments (Day One ships zeros for the first three and omits the date). When arichTextpayload lists a PDF viaembeddedObjects[].type === "pdfAttachment", the PDF renders inline at the position the embed appears in the text stream as acore/fileblock (<div class="wp-block-file"><a href="…">…</a><a … class="wp-block-file__button …" download>Download</a></div>) withshowDownloadButton: true. The Day One record'spdfNameis used as the block link text when present; absent names fall back to the attachment basename without extension, and finally to a literal[PDF]floor. Interleaved photo + video + audio + PDF sequences emit blocks in scan order: consecutive photos still collapse into a singlecore/imageorcore/gallery, and each video, audio, and PDF produces onecore/video,core/audio, orcore/fileblock in between, splitting a run of photos when any of them is interposed. -
The MIME allowlist gate (
Day_One_Importer_Media::validate_media_file()) now also acceptsapplication/pdf(in addition to the existingimage/*,video/*, andaudio/*paths). A PDF is sideloaded only when bothwp_check_filetype_and_ext()reportsapplication/pdfand that MIME appears in the site'sget_allowed_mime_types()list. On a default WordPress install, Administrator-role users haveapplication/pdfenabled, so the typical PDFs from a Day One export are accepted. Sites whose admins have explicitly strippedapplication/pdffromupload_mimeswill see the affected PDFs dropped with a privacy-safe warning ("Skipping embedded PDF in Day One entry: referenced media file is unsupported or missing."). No attachment is created, nocore/filefallback block is emitted, and the warning contains no identifier, UUID, filename, md5,pdfName, or path. To support additional PDF behavior on your site, extend the uploader allowlist via the standard WordPressupload_mimesfilter (or your multisite Add to mime types settings). PDF preview / thumbnail rendering is intentionally out of scope: the emittedcore/fileblock is link + Download button only. -
Day One
entry.locationis preserved as sanitized post meta on the imported post. The streaming parser extracts the location subtree when present and the runner writes up to eight typed_day_one_location_*meta keys duringfinalize_imported_entry(), plus one raw JSON snapshot key. Each typed key is written only when its source field is present after sanitization, exceptlatitude/longitudewhich are gated onisset()so the equator (0.0) and prime meridian (0.0) are preserved rather than dropped as falsy. The full key set:_day_one_location_latitude—(string)(float)of Day Onelatitude._day_one_location_longitude—(string)(float)of Day Onelongitude._day_one_location_place_name— sanitized Day OneplaceName._day_one_location_locality— sanitized Day OnelocalityName._day_one_location_administrative_area— sanitized Day OneadministrativeArea._day_one_location_country— sanitized Day Onecountry._day_one_location_timezone— sanitized Day OnetimeZoneName._day_one_location_raw—wp_json_encode()of the originallocationsubtree (withJSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE). This is where Day One'sregioncenter/identifier/radius subtree round-trips; it is intentionally not split into separate meta fields. Entries whose source JSON omitslocation(or ships an empty object) write zero_day_one_location_*rows. A non-arraylocationvalue emits one privacy-safe warning naming the entry UUID and the entry continues importing without location meta. All writes useupdate_post_metaso reruns are idempotent and produce identical rows and values. No block-level rendering, map embed, or geocoding is added in this release; the data is meta-only and is meant for downstream code (theme code, custom block, REST consumer) that wants to render or query against it.
-
The location meta map is exposed through a
day_one_importer_location_metafilter before any rows are written, so downstream code can mutate the eight typed values, add related keys (the prefix is not enforced), or skip writes entirely:/** * Filter the location meta key/value map before writing to the post. * * @param array<string,string> $meta Meta key => value map, ready to write. * @param array<string,mixed> $location Normalized location array (latitude, longitude, placeName, localityName, administrativeArea, country, timeZoneName, raw). * @param int $post_id Imported post ID. * @param array<string,mixed> $entry Full normalized entry. */ apply_filters( 'day_one_importer_location_meta', $meta, $location, $post_id, $entry );Filter return contract:
- Returning an
arraywrites itskey => valuepairs viaupdate_post_meta(callbacks may add, remove, or replace keys; unprefixed keys are allowed). - Returning
nullorfalseskips ALL writes for this entry (no rows touched). - Returning any other value (scalar truthy, object, resource) is treated as a no-op and emits one privacy-safe warning naming the filter; no rows are written.
The filter fires only on entries that have a normalized location. Entries without
locationdo not invoke the filter at all. - Returning an
-
Day One
entry.weatheris preserved as sanitized post meta on the imported post. The streaming parser extracts the weather subtree when present and the runner writes up to twelve typed_day_one_weather_*meta keys duringfinalize_imported_entry(), plus one raw JSON snapshot key. Each typed key is gated onisset()in the source array (after sanitization) rather than truthiness, so numeric0/0.0values are preserved:relativeHumidity: 0(matching one of Day One's sample payloads),windBearing: 0(due north), andmoonPhase: 0(new moon) all round-trip as the string"0"instead of being dropped as falsy. The full key set:_day_one_weather_temperature_celsius—(string)(float)of Day OnetemperatureCelsius._day_one_weather_humidity—(string)(float)of Day OnerelativeHumidity._day_one_weather_pressure_mb—(string)(float)of Day OnepressureMB._day_one_weather_wind_kph—(string)(float)of Day OnewindSpeedKPH._day_one_weather_wind_bearing—(string)(int)of Day OnewindBearing._day_one_weather_visibility_km—(string)(float)of Day OnevisibilityKM._day_one_weather_moon_phase—(string)(float)of Day OnemoonPhase._day_one_weather_moon_phase_code— sanitized Day OnemoonPhaseCode._day_one_weather_code— sanitized Day OneweatherCode._day_one_weather_conditions— sanitized Day OneconditionsDescription._day_one_weather_service— sanitized Day OneweatherServiceName._day_one_weather_raw—wp_json_encode()of the originalweathersubtree (withJSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE). This is where any additional fields Day One ships underweather(sunrise/sunset timestamps, icon hints, future additions) round-trip; they are intentionally not split into separate meta fields. Entries whose source JSON omitsweather(or ships an empty object) write zero_day_one_weather_*rows. A non-arrayweathervalue emits one privacy-safe warning naming the entry UUID and the entry continues importing without weather meta. All writes useupdate_post_metaso reruns are idempotent and produce identical rows and values. No block-level rendering, icon mapping, unit conversion (Celsius → Fahrenheit, KPH → MPH), or external weather re-fetch is added in this release; the data is meta-only and is meant for downstream code (theme code, custom block, REST consumer) that wants to render or query against it. Floats are stored verbatim from source — note that Day One ships full IEEE-754 double precision (29.909999847412109) and the(string)(float)cast keeps that precision intact.
-
The weather meta map is exposed through a
day_one_importer_weather_metafilter before any rows are written, so downstream code can mutate the twelve typed values, add related keys (the prefix is not enforced), or skip writes entirely:/** * Filter the weather meta key/value map before writing to the post. * * @param array<string,string> $meta Meta key => value map, ready to write. * @param array<string,mixed> $weather Normalized weather array (temperatureCelsius, relativeHumidity, pressureMB, windSpeedKPH, windBearing, visibilityKM, moonPhase, moonPhaseCode, weatherCode, conditionsDescription, weatherServiceName, raw). * @param int $post_id Imported post ID. * @param array<string,mixed> $entry Full normalized entry. */ apply_filters( 'day_one_importer_weather_meta', $meta, $weather, $post_id, $entry );Filter return contract:
- Returning an
arraywrites itskey => valuepairs viaupdate_post_meta(callbacks may add, remove, or replace keys; unprefixed keys are allowed). - Returning
nullorfalseskips ALL writes for this entry (no rows touched). - Returning any other value (scalar truthy, object, resource) is treated as a no-op and emits one privacy-safe warning naming the filter; no rows are written.
The filter fires only on entries that have a normalized weather array. Entries without
weatherdo not invoke the filter at all. - Returning an
Batched jobs, idempotency, and resume behavior
Large exports are processed as persisted import jobs instead of one long admin request. Each processing request handles a bounded amount of ZIP preflight/extraction, JSON indexing, entry creation, or media import work and returns status before typical proxy/PHP timeout limits are reached. Browser polling normally advances the job; WP-Cron can also continue queued work as a fallback. The importer screen shows the current phase, counts, warnings/errors, and an estimated progress bar so a paused, canceled, retried, or re-opened job is visible instead of leaving a blank page. The percentage on the progress bar is dominated by entries imported during the importing phase, so it tracks the "Imported X of Y entries" detail line rather than jumping ahead while pre-import phases finish. A resumed job paints at the percentage matching its already-imported entries on first render, and a failed or canceled mid-import job keeps the computed percentage rather than snapping to 100%.
The importer stores Day One UUID metadata on posts and media. Re-importing the same export, refreshing the browser, retrying a failed batch, or continuing after a temporary interruption skips entries that were already imported and marked complete. If an earlier import created a post but did not finish, the importer resumes that post instead of creating a duplicate.
If importer behavior changes in a way that requires existing imported posts to be refreshed, rerunning the same export reprocesses older importer-schema versions in place instead of skipping them. This lets import-time fixes, such as cleaner Day One text/title conversion, apply by rerunning the import rather than manually editing posts.
If you move imported posts to Trash and rerun the import, the trashed imported copy is permanently removed and a fresh private post is created for that Day One UUID. This is useful when cleaning up a failed test import before retrying.
This means you can normally click Retry / Continue or rerun the same export after an interruption without creating duplicate posts or attachments for the same Day One UUIDs/media items. Use Cancel import only when you want to abandon the queued job and remove its temporary files.
Media behavior and privacy
Supported initial image types are JPEG/JPG, PNG, GIF, and other image formats that the target WordPress site accepts safely. Animated GIF photos are preserved end-to-end: the original file bytes are written to disk untouched by the sideload step, and the emitted core/image block points at the original file URL (sizeSlug: "full") instead of WordPress's large derivative, so the saved attachment file md5 matches the export's photo.md5 and the GIF stays animated in the post body. WebP, AVIF, and HEIC files pass through the importer unmodified — there is no GIF-style original-URL emission for those formats yet; if the site's MIME allowlist accepts them they import as ordinary core/image blocks against the large derivative, otherwise the embed is dropped with a privacy-safe warning the same way unsupported video, audio, and PDF embeds are. Day One videos (.mov, .mp4), Day One audios (.mp3, .m4a, .aac), and Day One PDFs (application/pdf) are imported when the site's MIME allowlist accepts them (the default on most WordPress sites for Administrator-role users); embeds whose MIME the site refuses (most notably .lpcm audio on default WordPress allowlists, or PDFs on sites that have explicitly stripped application/pdf from upload_mimes) are dropped with a privacy-safe warning rather than failing the entry. HEIC and other attachment types are not guaranteed to import unless WordPress accepts and processes them in that environment.
Photos, videos, audios, and PDFs are attached to the corresponding private post and importer metadata is stored on the attachment (including the _day_one_media_kind marker that distinguishes photo, video, audio, and pdf attachments) to support reuse on reruns.
New Day One media is stored in a protected day-one-importer-private subfolder of the WordPress uploads directory. The importer uses a nonce- and permission-checked WordPress media endpoint instead of raw upload URLs for imported attachments. The endpoint only serves a Day One media file to a logged-in user who can read the associated private post or attachment.
To reduce timeout risk during large imports, the importer uses resumable batches, asks WordPress for the admin memory limit where the host allows it, and skips generated image sub-sizes during Day One media sideloads. Imported posts use the original uploaded image file. If you need WordPress thumbnail sizes for imported media later, regenerate thumbnails after the import using your preferred trusted maintenance tool.
Important media caveat: The private media directory is a protected subfolder of WordPress uploads. The importer writes .htaccess and web.config files as defense in depth, but privacy ultimately depends on the host honoring those protection files. Advanced operators can customize the directory with the day_one_importer_private_media_dir filter.
Temporary files and external services
- The plugin does not send journal content or media to external services.
- ZIP files, extracted content, and job manifests are processed in a protected temporary location and cleaned up after the import completes or is canceled when possible.
- Failed/interrupted jobs retain enough temporary state to retry until they are canceled or expire as stale.
- Uploaded ZIPs are not intentionally left in the plugin directory.
Local private data
Local Day One exports, extracted photos, and prompt/reference images must not be committed. This repository ignores sample/ and prompt-images/ for local-only private data.
For tests and examples, the repository includes tests/fixtures/day-one-fictional.zip, a wholly fictional Day One-style export that is safe to publish and is used by default in automated smoke tests.
License
Day One Importer is licensed under GPL-2.0-or-later. See LICENSE for details.
Limitations
- Day One rich text fidelity is not guaranteed. The importer reads the
richTextpayload when present and maps run-level inline formatting (bold, italic, strikethrough, inline code, http(s) links, highlight color) and block-level line attributes (headings 1–6, bulleted/numbered/checkbox lists with nesting, code blocks, blockquotes) to the matching Gutenberg blocks. The highlight<mark style="background-color:#RRGGBB">wrapper depends on the site'swp_kses_postallowlist accepting<mark>with that style attribute; if a host or filter tightens the allowlist, the text still survives and only the highlight color is dropped. Imported checkbox list items are visual-only (Unicode ballot-box glyphs) and cannot be re-toggled in the editor without re-importing. Nested<blockquote>rendering for Day One quoteindentLevelis not emitted (quote items render as flat siblings). - Inline-positioned photos inside
richTextembeddedObjectsrender at their original position in the text stream (single embed →core/image, consecutive embeds →core/gallery). Legacytext-only entries still append photos after the entry text. - Animated GIF preservation applies only to newly imported attachments. The
core/imageblock emitted for a GIF photo now references the original full-size file URL (wp_get_attachment_url()) withsizeSlug: "full"instead of WordPress's flattened first-framelargederivative, and every photo attachment carries a_day_one_photo_formatmeta marker the renderer keys off. Previously imported GIFs (from 0.2.12 or earlier) do not have the marker and continue to render through thelargederivative path until they are re-imported — there is no automatic backfill. Hosts running image-optimization plugins that transcode GIFs post-upload (Jetpack Image CDN, EWWW Image Optimizer, similar) may still flatten animation after the file lands inwp-content/uploads/; that path is outside this plugin's control. WebP, AVIF, and HEIC photos pass through unmodified: they receive the_day_one_photo_formatmarker (webp,avif,heic) but the renderer does not apply GIF-style original-URL emission to them, so they import as ordinarycore/imageblocks against thelargederivative when the site's MIME allowlist accepts them, or are dropped with a privacy-safe warning when it does not. GIF-to-MP4 conversion andloading="lazy"toggling for GIFs are intentionally out of scope. - Inline-positioned videos inside
richTextembeddedObjectsare sideloaded and rendered ascore/videoblocks at their original position. Video sideload depends on the site's MIME allowlist accepting the file'svideo/*MIME; embeds whose MIME the site rejects are dropped with a privacy-safe warning rather than emitted as acore/filefallback, and the<video>element relies on Gutenberg's defaults (noposterthumbnail, no transcoding, nowidth/heightblock attributes — the HTML element auto-detects dimensions from the source). Videos play only for users who can read the parent private post, since thesrcis served through the same nonce-checked private endpoint as photos. The "imported photos but no inline embed" privacy-safe warning is photo-specific by design and does not fire for videos (extending it to videos may land as a follow-up). - Inline-positioned audios inside
richTextembeddedObjectsare sideloaded and rendered ascore/audioblocks at their original position. Audio sideload depends on the site's MIME allowlist accepting the file'saudio/*MIME; embeds whose MIME the site rejects (most notably.lpcmlinear-PCM payloads on default WordPress allowlists) are dropped with a privacy-safe warning rather than emitted as acore/filefallback, and the<audio>element relies on Gutenberg's defaults (no transcoding, no waveform thumbnail, nowidth/heightblock attributes — Day One ships zeros for audio width/height and the importer does not persist them). When the Day One record'stitleis non-empty, the block renders that title as a<figcaption class="wp-element-caption">caption under the player; an empty title produces a player without a caption. Audios play only for users who can read the parent private post, since thesrcis served through the same nonce-checked private endpoint as photos and videos. The "imported photos but no inline embed" privacy-safe warning is photo-specific by design and does not fire for audios (same rationale as videos). - Inline-positioned PDFs inside
richTextembeddedObjectsare sideloaded and rendered ascore/fileblocks at their original position. PDF sideload depends on the site's MIME allowlist acceptingapplication/pdf; embeds whose MIME the site rejects are dropped with a privacy-safe warning rather than emitted as acore/filefallback against a missing attachment. The emitted block is link + Download button only: PDF preview / thumbnail rendering is intentionally out of scope (Gutenberg has no native PDF preview block, and emitting an<embed>/ `
This README is longer than the copy stored here. Read the rest on GitHub →