# bludit-github-sync Export Bludit articles to GitHub, and import an individual article back through an explicit preview and confirmation step. Saving an article in Bludit can automatically export it. Changes committed to GitHub **do not automatically publish to Bludit**: use the import controls when you are ready to apply them. ## Repository layout ```text my-article/ content/ index.md img/ screenshot.png files/ deployment-log.txt ``` `index.md` contains metadata followed by the HTML or Markdown body: ```markdown --- title: "My Article" date: 2026-11-01 slug: my-article status: published tags: - homelab - network description: "An optional meta description." category: network --- Article content with ![Screenshot](../img/screenshot.png) and a [deployment log](../files/deployment-log.txt). ``` HTML `src` and `href` references use the same relative paths. External HTTP(S) links remain unchanged; imports never download them. Literal fenced code blocks and inline/HTML code examples are not treated as asset links. ## Install or upgrade 1. Copy **both `plugin.php` and `importer.php`**, plus `metadata.json` and `languages/`, into the existing plugin directory under `bl-plugins/`. Keep the installed directory name unchanged when upgrading, so Bludit keeps the plugin's settings and workspace. 2. Enable **GitHub Sync** under **Admin → Plugins** and open its settings. The old display name was **GitHub Export**. 3. Configure the repository owner, name, branch, optional path prefix and token. Existing settings are retained on upgrade. The plugin configuration screen uses Bludit's normal administrator authorization and POST/CSRF protection. There is no public import endpoint or webhook. ## Settings | Field | Description | |---|---| | Personal Access Token | A classic token with `public_repo` for public repositories or `repo` for private repositories; alternatively a fine-grained token scoped to this repository with Contents read/write for exports. Import also needs read access to repository commits, trees and blobs. | | Repository Owner | GitHub user or organization. | | Repository Name | Target repository, such as `blog`. | | Branch | Branch used for both exports and import previews; default `main`. Branch names containing slashes are supported. | | Path Prefix | Optional subfolder above the article folders. Leave empty for the repository root. | | Auto-export draft articles on save | Default off. | | Auto-export published articles on save | Default on. | Use **Save** after configuring the repository. Import preview also saves the displayed settings before reading GitHub. Changing repository settings after preview requires a fresh preview. ## Import one article For example, to import the article stored as `load-balancing-my-homelab/content/index.md`: 1. Push the article and its referenced assets to the configured GitHub branch. 2. Open the plugin settings and enter `load-balancing-my-homelab` under **Import one article from GitHub**. With a path prefix, still enter only the article folder name. 3. Choose **Article author** before previewing. Select an existing Bludit account to override the author, or leave the default to use repository `author`, otherwise preserve the existing page author (new pages: logged-in account). To repair an earlier import attributed to `admin`, select the intended account and reimport the same article folder. 4. Choose the status behavior **before previewing**: - **Import as draft** is checked by default, including for an existing published article. Confirming with this option changes the page to a draft. - Uncheck it to use a nonempty repository `status`. A blank or omitted status preserves an existing page's type; a new page remains a draft. Future published dates are scheduled by Bludit. 5. Click **Preview import**. Review create versus replace, commit, metadata, resulting status, incoming source text and the assets to download. Existing content can be expanded for comparison. The incoming HTML is escaped in the preview, not executed. 6. Click **Confirm this import**. The plugin downloads and verifies the assets, saves a protected backup when replacing a page, then uses Bludit's `createPage()` or `editPage()` functions to save the article and rebuild category/tag indexes. 7. Check the imported draft or live page in Bludit, including its images and attachment links. The preview is pinned to the Git commit it displayed; newer GitHub commits require another preview. It belongs to the current administrator session, expires after 30 minutes and can only be confirmed once. **Cancel preview** discards it without editing an article. Confirmation checks the persisted local page metadata and body again, including after downloads. An intervening local edit requires a new preview. Imports are serialized with each other, but do not lock out Bludit's normal editor: avoid editing the same page during confirmation. Bludit's page storage is not a transactional database. ### Metadata and content - Supported fields: `title`, `date`, `slug`, `author`, `status`, `tags`, `description`, `category`, `cover`. - A title and nonempty body are required. If present, `slug` must match the selected folder. Only a single top-level page key is supported, not nested pages or renames; Bludit slug normalization/collisions are refused. - Dates use `YYYY-MM-DD`. An unchanged date preserves the existing time; a changed date uses midnight in the site's timezone. - Supported statuses: `draft`, `published`, `static`, `sticky`, `scheduled`, or blank for legacy exports. Normal Bludit scheduling applies. - Tags use `tags: []` or an indented ` - tag` list. Omitted metadata is preserved on update, apart from the explicit draft option and required title. Blank fields clear their corresponding values. - A nonempty category key must already exist in Bludit; create it in the administration interface first. Tags are rebuilt through Bludit after save. - `author` is a Bludit username, not a display nickname. It must refer to an existing account. Explicit selection in the import form takes priority over repository metadata. Without either, an existing author is preserved and a new page belongs to the logged-in administrator. The preview shows the resulting author; no account is created or impersonated. New pages needing a different author are created through `createPage()` and then adjusted through `editPage()`, with this plugin's export hooks suppressed for both operations. - Existing UUID and unrelated metadata are retained. - Front matter supports the small scalar/list format emitted by this plugin, not general YAML. Nested objects, aliases, tags, multiline scalars, unknown fields and duplicate fields are rejected. Quote strings containing YAML punctuation with JSON-style double quotes. - A `cover` may be a repository `../img/…` reference or an HTTP(S) URL. A repository cover image is imported with the other assets. - The body is stored as supplied, with asset destinations rewritten to Bludit uploads. The site's usual Markdown configuration controls Markdown rendering. Confirmed HTML is trusted author content, just like content saved in Bludit's editor; previewing it does not sanitize the eventual article. ### Assets Referenced files under the selected article's `img/` and `files/` directories are fetched through GitHub's blob API and checked against their Git SHA-1 object IDs. Unreferenced files are ignored. This also allows an unused SVG source diagram to remain in Git while its PNG rendition is imported. Supported extensions: PNG, JPEG, GIF, WebP, AVIF, ICO, TXT, PDF, CSV, JSON and ZIP. Images must pass PHP's image-type validation; support for some formats depends on the PHP runtime. Executable files, HTML and SVG are refused. Absolute external URLs, symlinks, traversal, percent-encoded paths and paths containing whitespace are not fetched. Use simple filenames and inline Markdown links or quoted HTML attributes; reference-style Markdown links and `srcset` are not imported. Limits: 1 MiB for the article document; at most 100 referenced assets, 10 MiB per asset and 32 MiB total. The GitHub recursive tree must be complete; truncated trees are rejected. Server memory and request timeouts must also accommodate the import. Git LFS pointer files are not hydrated. Assets receive generated filenames under the page's UUID uploads directory, preserving existing uploads. A protected per-page mapping makes later exports restore the original repository `img/` and `files/` paths. Old uploads are not pruned automatically. ## Backups and failures Before updating an existing article, the plugin writes `backup-*.php` in its Bludit workspace, containing the prior page database row, raw content and imported asset mapping. The file begins with a PHP access-denial guard and is created with restrictive permissions. Keep the site's normal full backups too; these per-page snapshots are not a backup of the entire site or all uploads. Failed downloads leave page content unchanged. If saving or a later page hook fails, the plugin attempts to restore the previous metadata and content, or remove a partially created page. External side effects from other plugins cannot be rolled back. An unsuccessful automatic restoration is reported explicitly with the backup filename. Backups and asset mapping files remain in the workspace across plugin upgrades. Do not delete mappings while using import/export round trips. Backups are retained until an administrator removes them. To recover manually, an administrator can read the JSON after the guard line on the server and restore the recorded body and metadata through Bludit; this release does not have a restore button. ## Export - Saving a page exports it when its auto-export setting is enabled. - **Export everything** exports drafts and published articles; **Export published only** exports the published selection. - Bludit upload URLs become relative `../img/` or `../files/` references. Imported asset mappings retain their original paths. - Exports include the page's actual `author` username so later imports can preserve attribution. - Exported `status` comes from Bludit's actual page `type`. Earlier plugin versions exported an empty `status` field; imports accept that legacy format. - Contents API reads use the configured branch, so export SHA lookups do not accidentally use the repository's default branch. - Deleting a Bludit page retains the existing automatic GitHub deletion behavior: its document and direct files in `img/` and `files/` are removed. This is unrelated to importing: missing repository pages never trigger local deletion. Nested repository asset directories are not recursively deleted. - The plugin's own export hooks are suppressed during import. Other Bludit plugins still receive normal page hooks. Do not save an older local copy over a newer GitHub article before importing: ordinary exports still replace the repository's current file. This is a manual two-way workflow, not automatic conflict merging or continuous deployment. ## Requirements and checks - Bludit 3.20+; integration tests run against **Bludit 3.22.0**. - PHP 7.4+ with cURL; the site's usual Bludit PHP extensions, including mbstring. - Writable page storage, uploads and plugin workspace, plus access to `api.github.com` over HTTPS. No Composer dependencies or YAML extension are required. To run the tests against a separate, unmodified Bludit checkout: ```bash php -l plugin.php php -l importer.php BLUDIT_ROOT=/path/to/bludit-3.22.0 php tests/discovery.php BLUDIT_ROOT=/path/to/bludit-3.22.0 php tests/import.php ``` The discovery regression test runs Bludit's real `buildPlugins()` with its real `Plugin` base class. Helper classes are loaded from `init()`, after class discovery, so Bludit does not mistake them for plugins. Tests use temporary local storage and a fake GitHub transport. They exercise real Bludit page creation/editing, asset rewriting and export round trips, rejected paths, escaped previews, stale metadata/configuration, failed downloads and rollback after a failing page hook. They do not modify a live site or contact GitHub. Optionally exercise a real exported article as a fixture too: ```bash BLUDIT_ROOT=/path/to/bludit-3.22.0 \ BLOG_ARTICLE_DIR=/path/to/blog/load-balancing-my-homelab \ php tests/import.php ```