diff --git a/.changeset/automated-releases.md b/.changeset/automated-releases.md new file mode 100644 index 000000000..9086e6ff5 --- /dev/null +++ b/.changeset/automated-releases.md @@ -0,0 +1,11 @@ +--- +'@headstartwp/core': patch +'@headstartwp/next': patch +--- + +Publish from the new automated Release workflow with npm provenance. + +Packages are now published by a single GitHub Actions workflow (`.github/workflows/release.yml`) using npm +trusted publishing, so every release carries a signed provenance attestation linking it to the commit and +workflow run that built it. `package.json` now declares `repository` (required for provenance). No runtime +changes. diff --git a/.changeset/fix-cross-domain-redirect-skip.md b/.changeset/fix-cross-domain-redirect-skip.md deleted file mode 100644 index 84eca3685..000000000 --- a/.changeset/fix-cross-domain-redirect-skip.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@headstartwp/core": patch ---- - -Fix: shouldSkipRedirect incorrectly skipping cross-domain redirects when pathnames match. Fixes #941 diff --git a/.changeset/fix-gutenberg-dollar-sign.md b/.changeset/fix-gutenberg-dollar-sign.md deleted file mode 100644 index d5e069e73..000000000 --- a/.changeset/fix-gutenberg-dollar-sign.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@headstartwp/headstartwp": patch ---- - -Fix Gutenberg block attribute rendering when JSON attribute values contain dollar signs (for example, `$50 million`), preventing incorrect replacement escaping. - diff --git a/.changeset/fix-undefined-context-key.md b/.changeset/fix-undefined-context-key.md deleted file mode 100644 index f8bc49475..000000000 --- a/.changeset/fix-undefined-context-key.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@headstartwp/headstartwp": patch ---- - -Fix: Add null coalescing check for context parameter in extend_post_content to prevent PHP 8+ "Undefined array key" warning. Fixes #940 diff --git a/.github/workflows/deploy_wp_plugin_develop.yml b/.github/workflows/deploy_wp_plugin_develop.yml deleted file mode 100644 index 0d77da987..000000000 --- a/.github/workflows/deploy_wp_plugin_develop.yml +++ /dev/null @@ -1,33 +0,0 @@ -# This is a basic workflow to help you get started with Actions -# this is disabled for now to avoid breaking projects using the older method of installation - -name: Release develop WP Plugin - -permissions: - contents: write - -on: - push: - branches: - - develop2 - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2.4.0 - - name: Set PHP version - uses: shivammathur/setup-php@v2 - - name: Pushes to another repository - id: push_directory - uses: cpina/github-action-push-to-another-repository@main - env: - API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_TOKEN }} - with: - source-directory: wp/headless-wp/ - destination-github-username: '10up' - destination-repository-name: 'tenup-headless-wp-plugin' - user-email: bot@10up.com - commit-message: See ORIGIN_COMMIT from $GITHUB_REF - target-branch: develop \ No newline at end of file diff --git a/.github/workflows/deploy_wp_plugin_stable.yml b/.github/workflows/deploy_wp_plugin_stable.yml deleted file mode 100644 index abd84a648..000000000 --- a/.github/workflows/deploy_wp_plugin_stable.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: Release Stable WP Plugin - -### This has been merged into release-latest-version action - -permissions: - contents: write - -on: - push: - branches: - - trunk2 - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Set PHP version - uses: shivammathur/setup-php@v2 - - name: Publish to the publishing repository - id: push_directory - uses: cpina/github-action-push-to-another-repository@main - env: - API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_TOKEN }} - with: - source-directory: wp/headless-wp/ - destination-github-username: '10up' - destination-repository-name: 'tenup-headless-wp-plugin' - user-email: bot@10up.com - commit-message: See ORIGIN_COMMIT from $GITHUB_REF - target-branch: trunk - - name: Checkout the publishing repository - uses: actions/checkout@v3 - with: - repository: 10up/tenup-headless-wp-plugin - path: 'repo' - ref: trunk - token: ${{ secrets.API_GITHUB_TOKEN }} - - name: Create Github Tag - run: 'echo "VERSION=$(grep -Po ''\"version\": \"[0-9\\.]+\"'' repo/package.json | grep -Po ''[0-9\\.]+'')" >> $GITHUB_ENV' - - name: Tag - run: | - cd repo - echo "Releasing version $VERSION ..." - git config user.name github-actions - git config user.email github-actions@github.com - git checkout -b "release-$VERSION" - git tag "$VERSION" - git push --tags \ No newline at end of file diff --git a/.github/workflows/npm-release-next-version.yml b/.github/workflows/npm-release-next-version.yml deleted file mode 100644 index 61e3d9ed9..000000000 --- a/.github/workflows/npm-release-next-version.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Release @next - -permissions: - contents: write - pull-requests: write - -on: - push: - branches: - - develop - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -jobs: - packages: - name: Release - runs-on: ubuntu-latest - steps: - - name: Checkout Repo - uses: actions/checkout@v2 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: 20.x - - - name: Install Dependencies - run: npm ci - - name: Enter pre next - run: | - [[ ! -f .changeset/pre.json ]] && npx changeset pre enter next || echo 'already in pre mode' - - name: Create Release Pull Request or Publish to npm - id: changesets - uses: changesets/action@v1 - with: - # This expects you to have a script called release which does a build for your packages and calls changeset publish - publish: npm run publish - version: npm run version - title: "Release" - commit: "chore: version packages" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - plugin-release: - runs-on: ubuntu-latest - needs: [packages] - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Publish to the publishing repository - id: push_directory - uses: cpina/github-action-push-to-another-repository@main - env: - API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_TOKEN }} - with: - source-directory: wp/headless-wp/ - destination-github-username: '10up' - destination-repository-name: 'headstartwp-plugin' - user-email: bot@10up.com - commit-message: See ORIGIN_COMMIT from $GITHUB_REF - target-branch: next - - name: Checkout the publishing repository - uses: actions/checkout@v3 - with: - repository: 10up/headstartwp-plugin - path: 'repo' - ref: next - token: ${{ secrets.API_GITHUB_TOKEN }} - - name: Create Github Tag - run: echo "VERSION=$(cat repo/package.json | jq -r '.version')" >> $GITHUB_ENV - - name: Tag - run: | - cd repo - git fetch --all --tags - - echo "Releasing version $VERSION ..." - if [ $(git tag -l "$VERSION") ]; then - echo "Plugin version already published" - else - git config user.name github-actions - git config user.email github-actions@github.com - git checkout -b "$VERSION" - git tag "$VERSION" - git push --tags - git push --all origin - fi - \ No newline at end of file diff --git a/.github/workflows/release-check.yml b/.github/workflows/release-check.yml new file mode 100644 index 000000000..9728e47e4 --- /dev/null +++ b/.github/workflows/release-check.yml @@ -0,0 +1,64 @@ +name: Release check + +# Dry-runs the release on every PR, so a broken changeset, version script or +# plugin version sync is caught before it reaches trunk/develop instead of in +# the Release workflow. + +on: + pull_request: + branches: + - trunk + - develop + +concurrency: + group: release-check-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + dry-run: + name: Release dry run + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Changeset status + # Informational: lists what this PR would release. Missing changesets are + # flagged by the changeset bot, so this does not fail the build. + run: npx changeset status --verbose --since="origin/${{ github.base_ref }}" || true + + - name: Version packages (dry run) + run: | + npm run version + echo "### Release dry run" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + git diff --stat >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + + - name: Plugin version is in sync + run: | + VERSION=$(jq -r .version wp/headless-wp/package.json) + grep -q "^ \* Version: ${VERSION}$" wp/headless-wp/plugin.php + grep -q "HEADLESS_WP_PLUGIN_VERSION', '${VERSION}'" wp/headless-wp/plugin.php + echo "Plugin version ${VERSION} is consistent." + + - name: Build and pack packages + run: | + npm run build:packages + for dir in packages/*/; do + (cd "$dir" && npm pack --dry-run > /dev/null) || exit 1 + done diff --git a/.github/workflows/release-latest-version.yml b/.github/workflows/release-latest-version.yml deleted file mode 100644 index c5aa9c9af..000000000 --- a/.github/workflows/release-latest-version.yml +++ /dev/null @@ -1,93 +0,0 @@ -name: Release @latest - -permissions: - contents: write - pull-requests: write - -on: - push: - branches: - - trunk - workflow_dispatch: - -concurrency: ${{ github.workflow }}-${{ github.ref }} - -jobs: - packages: - name: Release - runs-on: ubuntu-latest - # Only run if on trunk branch - if: github.ref == 'refs/heads/trunk' - outputs: - hasChangesets: ${{ steps.changesets.outputs.hasChangesets }} - steps: - - name: Checkout Repo - uses: actions/checkout@v2 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: 20.x - - - name: Install Dependencies - run: npm ci - - name: Promote from @next - run: | - [[ -f .changeset/pre.json ]] && { git config user.email "github-actions[bot]@users.noreply.github.com"; git config user.name "github-actions[bot]"; npx changeset pre exit; npx changeset version; git add -u; git commit -m "chore: promote from @next" --no-verify; git push --follow-tags; } || echo 'not in pre mode, no need to exit' - - name: Create Release Pull Request or Publish to npm - id: changesets - uses: changesets/action@v1 - with: - # This expects you to have a script called release which does a build for your packages and calls changeset publish - publish: npm run publish - version: npm run version - commit: "chore: version packages" - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - plugin-release: - runs-on: ubuntu-latest - needs: [packages] - # only run this when there are no changesets to be published AND on trunk branch - if: needs.packages.outputs.hasChangesets == 'false' && github.ref == 'refs/heads/trunk' - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Publish to the publishing repository - id: push_directory - uses: cpina/github-action-push-to-another-repository@main - env: - API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_TOKEN }} - with: - source-directory: wp/headless-wp/ - destination-github-username: '10up' - destination-repository-name: 'headstartwp-plugin' - user-email: bot@10up.com - commit-message: See ORIGIN_COMMIT from $GITHUB_REF - target-branch: trunk - - name: Checkout the publishing repository - uses: actions/checkout@v3 - with: - repository: 10up/headstartwp-plugin - path: 'repo' - ref: trunk - token: ${{ secrets.API_GITHUB_TOKEN }} - - name: Create Github Tag - run: echo "VERSION=$(cat repo/package.json | jq -r '.version')" >> $GITHUB_ENV - - name: Tag - run: | - cd repo - git fetch --all --tags - echo "Releasing version $VERSION ..." - if [ $(git tag -l "$VERSION") ]; then - echo "Plugin version already published" - else - git config user.name github-actions - git config user.email github-actions@github.com - git checkout -b "release-$VERSION" - git tag "$VERSION" - git push --tags - fi - - - \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..72990004f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,257 @@ +name: Release + +# One workflow owns every release, so npm trusted publishing needs exactly one +# configuration per package (repository 10up/headstartwp, workflow release.yml). +# +# develop -> prerelease (`next` dist-tag) + plugin repo `next` branch +# trunk -> stable (`latest` dist-tag) + plugin repo `trunk` branch +# +# On every push, changesets/action either opens/updates the "Release" PR (when +# unreleased changesets exist) or, once that PR is merged, publishes to npm, +# pushes git tags and creates GitHub Releases. The plugin is released after the +# npm step whenever its version has not been tagged in 10up/headstartwp-plugin. + +on: + push: + branches: + - trunk + - develop + workflow_dispatch: + +concurrency: + group: release-${{ github.ref }} + # Never cancel a release half-way through publishing. + cancel-in-progress: false + +permissions: {} + +jobs: + packages: + name: npm packages (${{ github.ref_name }}) + runs-on: ubuntu-latest + if: >- + github.repository == '10up/headstartwp' && + (github.ref == 'refs/heads/trunk' || github.ref == 'refs/heads/develop') + permissions: + contents: write # version commits, tags, GitHub Releases + pull-requests: write # the Release PR + id-token: write # npm trusted publishing (OIDC) + provenance + outputs: + hasChangesets: ${{ steps.changesets.outputs.hasChangesets }} + published: ${{ steps.changesets.outputs.published }} + publishedPackages: ${{ steps.changesets.outputs.publishedPackages }} + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + # Trusted publishing needs Node >= 22.14 and npm >= 11.5.1. + # Do NOT set registry-url: it writes a token-based .npmrc, and npm + # prefers a token over the OIDC exchange. + node-version: 24 + cache: npm + + - name: Use an npm that supports trusted publishing + run: npm install --global "npm@^11.5.1" + + - name: Install dependencies + run: npm ci + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Enter prerelease mode (develop) + if: github.ref == 'refs/heads/develop' + run: | + if [ -f .changeset/pre.json ]; then + echo "Already in prerelease mode" + else + npx changeset pre enter next + fi + + - name: Promote from @next (trunk) + # develop is merged into trunk while still in prerelease mode. Exit it and + # commit, so the Release PR computes stable versions. + if: github.ref == 'refs/heads/trunk' + run: | + if [ ! -f .changeset/pre.json ]; then + echo "Not in prerelease mode, nothing to promote" + exit 0 + fi + npx changeset pre exit + npm run version + git add -A + git commit -m "chore: promote from @next" --no-verify + git push --follow-tags + + - name: Create Release PR or publish to npm + id: changesets + uses: changesets/action@v1.9.0 + with: + version: npm run version + publish: npm run publish + title: ${{ github.ref == 'refs/heads/trunk' && 'Release' || 'Release (next)' }} + commit: 'chore: version packages' + createGithubReleases: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Optional fallback while trusted publishing is being set up on + # npmjs.com. When this secret is absent or empty, changesets/action + # uses OIDC. Delete the secret once trusted publishing works. + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + NPM_CONFIG_PROVENANCE: 'true' + + - name: Summary + if: always() + env: + HAS_CHANGESETS: ${{ steps.changesets.outputs.hasChangesets }} + PUBLISHED: ${{ steps.changesets.outputs.published }} + PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} + run: | + { + echo "### npm release (${GITHUB_REF_NAME})" + if [ "$PUBLISHED" = "true" ]; then + echo "Published:" + echo "$PACKAGES" | jq -r '.[] | "- `\(.name)@\(.version)`"' + elif [ "$HAS_CHANGESETS" = "true" ]; then + echo "Unreleased changesets found: the Release PR was opened or updated." + else + echo "Nothing to release." + fi + } >> "$GITHUB_STEP_SUMMARY" + + plugin: + name: WordPress plugin (${{ github.ref_name }}) + runs-on: ubuntu-latest + needs: packages + # trunk: only once the Release PR has been merged (no pending changesets). + # develop: every run, since prerelease mode keeps consumed changesets on disk. + # The tag check below makes both idempotent. + if: >- + needs.packages.result == 'success' && + (github.ref == 'refs/heads/develop' || needs.packages.outputs.hasChangesets == 'false') + permissions: + contents: write # GitHub Release for the plugin in this repo + env: + PLUGIN_REPO: 10up/headstartwp-plugin + TARGET_BRANCH: ${{ github.ref == 'refs/heads/trunk' && 'trunk' || 'next' }} + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Check whether this plugin version is already released + id: plugin + env: + API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_TOKEN }} + run: | + VERSION=$(jq -r .version wp/headless-wp/package.json) + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + if git ls-remote --exit-code --tags \ + "https://x-access-token:${API_TOKEN_GITHUB}@github.com/${PLUGIN_REPO}.git" \ + "refs/tags/${VERSION}" > /dev/null; then + echo "Plugin ${VERSION} is already tagged in ${PLUGIN_REPO}; skipping." + echo "release=false" >> "$GITHUB_OUTPUT" + else + echo "Releasing plugin ${VERSION} to ${PLUGIN_REPO}@${TARGET_BRANCH}." + echo "release=true" >> "$GITHUB_OUTPUT" + fi + + - name: Push plugin to the publishing repository + if: steps.plugin.outputs.release == 'true' + uses: cpina/github-action-push-to-another-repository@v1.7.3 + env: + API_TOKEN_GITHUB: ${{ secrets.API_GITHUB_TOKEN }} + with: + source-directory: wp/headless-wp/ + destination-github-username: '10up' + destination-repository-name: 'headstartwp-plugin' + user-email: bot@10up.com + commit-message: See ORIGIN_COMMIT from $GITHUB_REF + target-branch: ${{ env.TARGET_BRANCH }} + + - name: Checkout the publishing repository + if: steps.plugin.outputs.release == 'true' + uses: actions/checkout@v7 + with: + repository: ${{ env.PLUGIN_REPO }} + path: plugin-repo + ref: ${{ env.TARGET_BRANCH }} + token: ${{ secrets.API_GITHUB_TOKEN }} + + - name: Tag the plugin release + if: steps.plugin.outputs.release == 'true' + working-directory: plugin-repo + env: + VERSION: ${{ steps.plugin.outputs.version }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # Composer consumers install from these tags; the release-* branch is + # kept for parity with the previous release process. + git tag "$VERSION" + git push origin "refs/tags/$VERSION" + if [ "$TARGET_BRANCH" = "trunk" ]; then + git push origin "HEAD:refs/heads/release-$VERSION" + else + git push origin "HEAD:refs/heads/$VERSION" + fi + + - name: Create a GitHub Release for the plugin + if: steps.plugin.outputs.release == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.plugin.outputs.version }} + run: | + TAG="@headstartwp/headstartwp@${VERSION}" + # The section of the plugin CHANGELOG for this version. + awk -v v="## ${VERSION}" '$0==v{f=1;next} /^## /{if(f)exit} f' \ + wp/headless-wp/CHANGELOG.md > plugin-notes.md + [ -s plugin-notes.md ] || echo "See wp/headless-wp/CHANGELOG.md" > plugin-notes.md + PRERELEASE="" + [ "$TARGET_BRANCH" = "next" ] && PRERELEASE="--prerelease" + gh release create "$TAG" --target "$GITHUB_SHA" --title "$TAG" \ + --notes-file plugin-notes.md $PRERELEASE + + back-merge: + name: Open trunk -> develop back-merge PR + runs-on: ubuntu-latest + needs: packages + if: github.ref == 'refs/heads/trunk' && needs.packages.outputs.published == 'true' + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Open PR if develop is behind trunk + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PACKAGES: ${{ needs.packages.outputs.publishedPackages }} + run: | + if ! git ls-remote --exit-code --heads origin develop > /dev/null; then + echo "No develop branch; nothing to back-merge." + exit 0 + fi + git fetch origin develop trunk + AHEAD=$(git rev-list --count origin/develop..origin/trunk) + if [ "$AHEAD" = "0" ]; then + echo "develop already contains trunk." + exit 0 + fi + if [ -n "$(gh pr list --base develop --head trunk --state open --json number --jq '.[].number')" ]; then + echo "A back-merge PR is already open." + exit 0 + fi + RELEASED=$(echo "$PACKAGES" | jq -r '.[] | "- `\(.name)@\(.version)`"') + gh pr create --base develop --head trunk \ + --title "chore: merge trunk back into develop" \ + --body "$(printf 'Automated back-merge after the stable release of:\n\n%s\n\nMerge with a **merge commit** (not squash) so develop keeps trunk'"'"'s release history.\n\n_Opened by the Release workflow. PRs opened with GITHUB_TOKEN do not trigger CI; push an empty commit or re-run checks if required._' "$RELEASED")" diff --git a/README.md b/README.md index bae6a45e6..69d27e526 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ ## Documentation -See our [Getting Started](https://headstartwp.10up.com/docs/learn/getting-started/quick-setup/) guide. +See our [Getting Started](https://headstartwp.fueled.com/docs/learn/getting-started/quick-setup/) guide. -Visit [headstartwp.10up.com/docs](https://headstartwp.10up.com/docs) for the full documentation. +Visit [headstartwp.fueled.com/docs](https://headstartwp.fueled.com/docs) for the full documentation. ### Running docs site locally @@ -40,4 +40,4 @@ This repository is a monorepo, under the `packages` there are all the tools that ## Like what you see? -10up +[![Work with the 10up WordPress Practice at Fueled](https://github.com/10up/.github/blob/trunk/profile/10up-github-banner.jpg)](http://10up.com/contact/) diff --git a/RELEASING.md b/RELEASING.md index 6abb2ef40..a31b09f3e 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,63 +1,98 @@ # Release Instructions +Releases are automated with [changesets](https://github.com/changesets/changesets) and a single GitHub +Actions workflow, [`.github/workflows/release.yml`](.github/workflows/release.yml). You never bump versions, +edit the plugin header, tag, publish to npm, or write GitHub Releases by hand. + ## QA Before any major or minor release, we will run through our [QA plan](https://docs.google.com/spreadsheets/d/1Ep0FwOnjaXCcZOvrZok9AuEEt1MbPzhk9CLU9KjdG00/edit#gid=0). -## Process +## 1. Add a changeset to your PR + +Any PR that should ship in a release needs a changeset: + +1. Run `npx changeset add`. +2. Select the packages to bump (space to select, enter to continue) and choose major/minor/patch. Include + `@headstartwp/headstartwp` when the WordPress plugin changes. +3. Write the changelog entry. You can edit it later in `.changeset/.md`. +4. Commit the file with your PR. -We use [changesets](https://github.com/changesets/changesets) to manage changelogs and releases. +The changeset bot comments on PRs without one. The **Release check** workflow dry-runs the version step on +every PR and shows which packages would be released in the run summary. -When creating a PR that should trigger a release you should include a changeset within your PR. +## 2. What happens on merge -To include a changeset follow these steps: +| Branch | npm dist-tag | Plugin repo branch | Release PR title | +| --- | --- | --- | --- | +| `develop` | `next` (prerelease, e.g. `1.7.0-next.0`) | `next` | `Release (next)` | +| `trunk` | `latest` | `trunk` | `Release` | -1. Run `npx changeset add` -2. Follow the prompts, the CLI will ask you to select which packages should have a major, minor and patch bump. Use *space* to select the packages and hit *enter* to go to the next step. To skip a prompt (e.g if you changes do not require a major bump) just hit *enter* without selecting a package. -3. Enter a short message describing the changes. You can always change the changelog entry by editing the newly created file in `.changesets/[name].md` -4. Add the changesets to your PR (`git add`), commit and push. +On every push to `develop` or `trunk`, the Release workflow: -A GitHub bot will check if you PR include a changeset file. If it doesn't you will be warned in the PR. +1. **Opens or updates a Release PR** when there are unreleased changesets. The PR contains the version bumps, + CHANGELOG entries, the synced plugin version (`plugin.php` header and `HEADLESS_WP_PLUGIN_VERSION`, via + `scripts/version-plugin.sh`) and a refreshed `package-lock.json`. Merge more PRs and it updates itself. +2. **Publishes** once you merge the Release PR: + - npm packages via trusted publishing, with provenance attestations + - git tags (`@headstartwp/core@x.y.z`, …) and a GitHub Release per package with its changelog +3. **Releases the WordPress plugin** if `wp/headless-wp/package.json`'s version isn't tagged yet in + [10up/headstartwp-plugin](https://github.com/10up/headstartwp-plugin): pushes the plugin to that repo, + tags it (Composer installs from these tags), and creates a `@headstartwp/headstartwp@x.y.z` GitHub + Release here. +4. **On `trunk` only, opens a `trunk → develop` back-merge PR** after a stable release. Merge it with a + merge commit. -*NOTE*: You should also do this for the WordPress plugin. +### Prereleases → stable -### @next releases +1. Merge feature PRs (with changesets) into `develop`. The workflow puts `develop` into prerelease mode + (`.changeset/pre.json`) and maintains the `Release (next)` PR. +2. Merge `Release (next)` to publish `@next`. Test it. +3. Open a PR from `develop` into `trunk` and merge it. The workflow exits prerelease mode ("promote from + @next") and opens the `Release` PR with stable versions. +4. Merge `Release` to publish `@latest`, the plugin, and the GitHub Releases. +5. Merge the automated back-merge PR into `develop`. -Whenever a PR is merged to the `develop` branch, if it contains a changeset a new PR will be opened automatically against `develop` to bump versions and push to `npm` under the `next` tag. Merging this PR opened by `changeset` will trigger the release flow. +Hotfixes can go straight to `trunk` with a changeset. The same flow applies from step 4. -You do not need to release a new version to NPM on every PR that is merged, you can batch as many PRs as you want. For stable releases though, typically we'd only merge `develop` into `trunk` once we're ready for a new stable release. +## One-time setup (maintainers) -Here's a summary of the process: +### npm trusted publishing -1. Merge a PR with changesets files into `develop` -2. Wait for `changeset` to open a new PR called `Release (next)`. -3. Optionally merge more PRs into `develop` if you want to include other changes in the same release. Doing so will update the `Release (next)` PR automatically. -4. Merge the PR opened by `changeset` into `develop`. -5. A new release under the `next` tag will be pushed to npm. -6. A new Github Release with the changelog will be created automatically. +For each published package (`@headstartwp/core`, `@headstartwp/next`, `@headstartwp/block-primitives`, +`@headstartwp/epio-search`, `@10up/next-redis-cache-provider`), on npmjs.com open **Settings → Trusted +publishing** and add a GitHub Actions publisher: -### Stable releases +- Organization or user: `10up` +- Repository: `headstartwp` +- Workflow filename: `release.yml` +- Environment: leave empty -Whenever a PR is merged to the `trunk` branch, if it contains a changeset a new PR will be opened automatically against `trunk` to bump versions and push to `npm` under the `latest` tag. Merging this PR opened by `changeset` will trigger the release flow. +Once every package is configured and a release has published successfully, delete the `NPM_TOKEN` +repository secret. While it exists, the workflow uses it instead of trusted publishing (provenance is still +attached), which makes it a fallback during the switch-over. -To promote a next release to a stable release, first make sure to release the `@next` version by merging the `Release (@next)` PR opened by changeset. Then open a PR from `develop` against `trunk` and merge the `Release` PR into `trunk`. +### Repository secrets -After a new stable version has been released, merge `trunk` back into `develop`. +- `API_GITHUB_TOKEN`: token with write access to `10up/headstartwp-plugin` (plugin push and tags). +- `NPM_TOKEN`: optional, temporary (see above). -Here's a summary of the process -1. Follow the process to create a `next` release and test that the release is good to go. -2. Merge `develop` into `trunk`. -2. Wait for `changeset` to open a new PR called `Release`. -4. Merge the PR opened by `changeset` into `trunk`. -5. A new release under the `latest` tag will be pushed to npm. -6. Merge `trunk` back into `develop`. -7. A new Github Release with the changelog will be created automatically. +`GITHUB_TOKEN` needs **Settings → Actions → General → Workflow permissions → Allow GitHub Actions to create +and approve pull requests** enabled, so the workflow can open the Release and back-merge PRs. -### Plugin Releases +### Branch protection -Before merging any release PR to `trunk` make sure to manually update the `wp/headless-wp/plugin.php` file. You can commit the changes to the PR generated by changeset or by pushing to develop directly. Update the following things: +If `trunk` or `develop` requires status checks, PRs opened by `GITHUB_TOKEN` (the Release and back-merge +PRs) don't trigger workflows. Re-run checks by closing and reopening the PR, or pushing an empty commit to +its branch. The "promote from @next" step pushes directly to `trunk`, so `github-actions[bot]` must be +allowed to bypass protection there if direct pushes are restricted. -1. The `Version: x.x.x` phpdoc line. -2. The `HEADLESS_WP_PLUGIN_VERSION` constant. +## Troubleshooting -to the version of the plugin being released. This should match the version changeset is showing in the release PR. If changeset is not picking up the plugin then there's nothing to release to the plugin and you don't need to update the version. \ No newline at end of file +- **`E404`/`ENEEDAUTH` when publishing:** the trusted publisher on npmjs.com doesn't match (check the + workflow filename `release.yml` and repository), or a package is missing from the list above. +- **`E422` provenance error:** the package's `repository.url` must point at `github.com/10up/headstartwp`. +- **Plugin didn't release:** its version was already tagged in `10up/headstartwp-plugin`. Add a changeset + for `@headstartwp/headstartwp`. +- **Re-run a release:** use **Actions → Release → Run workflow** on the branch. Every step is idempotent: + already-published versions and existing plugin tags are skipped. diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 9896e10ed..bdc3e3757 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -6,11 +6,13 @@ import { themes as prismThemes } from 'prism-react-renderer'; /** @type {import('@docusaurus/types').Config} */ const config = { - title: 'HeadstartWP Docs - Next.js Framework for WordPress', + title: 'HeadstartWP Docs - Next.js Framework for Headless WordPress', tagline: '', - url: 'https://headstartwp.10up.com', + url: 'https://headstartwp.fueled.com', baseUrl: '/docs', - onBrokenLinks: 'throw', + // Links into the generated /api section are unavoidably broken when + // SKIP_TYPEDOC skips generation — downgrade so local builds still pass. + onBrokenLinks: process.env.SKIP_TYPEDOC ? 'warn' : 'throw', onBrokenMarkdownLinks: 'warn', favicon: 'img/favicon.ico', organizationName: '10up', // Usually your GitHub org/user name. @@ -32,9 +34,19 @@ const config = { ({ docs: false, blog: false, - googleTagManager: { - containerId: 'GTM-TKCGKK2', - }, + // Same Google tag as the main site (Site Kit on + // headstartwp.fueled.com) — one GA4 property covers the full + // site→docs journey. Replaces the legacy 10up GTM container. + // Production-only: in dev the gtag script never loads, so the + // plugin's route-change handler would throw on every navigation. + ...(process.env.NODE_ENV === 'production' + ? { + gtag: { + trackingID: 'GT-WRDG786', + anonymizeIP: true, + }, + } + : {}), theme: { customCss: './src/css/custom.css', }, @@ -43,21 +55,29 @@ const config = { ], plugins: [ - [ - 'docusaurus-plugin-typedoc', - { - name: 'HeadstartWP', - out: './docs', - entryPoints: ['../packages/core', '../packages/next'], - entryPointStrategy: 'packages', - packageOptions: { - entryPoints: ['src/docs-entry-point.ts'], - }, - categorizeByGroup: false, - excludeInternal: true, - readme: 'none', - }, - ], + // SKIP_TYPEDOC=1 skips API-reference generation for faster local + // theme/content work (it needs the monorepo packages installed and + // built). CI always runs it. Local builds still get working search, + // minus API-reference entries (see the search plugin config below). + ...(process.env.SKIP_TYPEDOC + ? [] + : [ + [ + 'docusaurus-plugin-typedoc', + { + name: 'HeadstartWP', + out: './docs', + entryPoints: ['../packages/core', '../packages/next'], + entryPointStrategy: 'packages', + packageOptions: { + entryPoints: ['src/docs-entry-point.ts'], + }, + categorizeByGroup: false, + excludeInternal: true, + readme: 'none', + }, + ], + ]), [ '@docusaurus/plugin-content-docs', { @@ -97,8 +117,11 @@ const config = { '@easyops-cn/docusaurus-search-local', { indexDocs: true, - docsRouteBasePath: ['learn', 'api'], - docsDir: ['documentation', 'docs'], + // Search must only reference contexts that emit an index: with + // SKIP_TYPEDOC there is no /api content, and a missing context + // index leaves the search loader hanging forever. + docsRouteBasePath: process.env.SKIP_TYPEDOC ? ['learn'] : ['learn', 'api'], + docsDir: process.env.SKIP_TYPEDOC ? ['documentation'] : ['documentation', 'docs'], hashed: true, }, ], @@ -121,7 +144,7 @@ const config = { type: 'doc', docId: 'index', position: 'right', - label: 'Docs', + label: 'Developer Guide', }, { type: 'doc', @@ -135,6 +158,12 @@ const config = { position: 'left', dropdownActiveClassDisabled: true, }, + { + href: 'https://headstartwp.fueled.com/', + label: 'About HeadstartWP', + position: 'right', + className: 'navbar-site-cta', + }, ], }, announcementBar: { @@ -149,24 +178,53 @@ const config = { style: 'light', links: [ { - title: 'Docs', + title: 'Docs & Community', items: [ { - label: 'Documentation', + label: 'Developer Guide', to: '/learn', }, { label: 'API Reference', to: '/api', }, + { + label: 'GitHub Discussions', + href: 'https://github.com/10up/headstartwp/discussions/', + }, ], }, { - title: 'Community', + title: 'HeadstartWP', items: [ { - label: 'GitHub Discussions', - href: 'https://github.com/10up/headstartwp/discussions/', + label: 'Main site', + href: 'https://headstartwp.fueled.com/', + }, + { + label: 'News & Updates', + href: 'https://headstartwp.fueled.com/news/', + }, + { + label: 'Privacy Policy', + href: 'https://headstartwp.fueled.com/privacy-policy/', + }, + ], + }, + { + title: 'Fueled (formerly 10up)', + items: [ + { + label: 'WordPress', + href: 'https://fueled.com/wordpress/?utm_source=referral&utm_medium=Website%20Referral&utm_campaign=headstartwp.fueled.com&utm_content=docs-footer', + }, + { + label: 'Hire us', + href: 'https://fueled.com/contact/?utm_source=referral&utm_medium=Website%20Referral&utm_campaign=headstartwp.fueled.com&utm_content=docs-footer-hire', + }, + { + label: 'Careers', + href: 'https://fueled.com/careers/?utm_source=referral&utm_medium=Website%20Referral&utm_campaign=headstartwp.fueled.com&utm_content=docs-footer-careers', }, ], }, diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index f0a1b18a9..347ee3135 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -4,7 +4,7 @@ * work well for content-centric websites. */ -@import url("https://fonts.googleapis.com/css2?family=Cabin:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=IBM+Plex+Sans+Condensed:wght@600;700&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap"); @font-face { font-family: "Virgil"; diff --git a/docs/src/css/global-footer.css b/docs/src/css/global-footer.css index fe2f081b8..9712db87e 100644 --- a/docs/src/css/global-footer.css +++ b/docs/src/css/global-footer.css @@ -3,97 +3,108 @@ border-top: 2px solid var(--ifm-color-secondary-lightest); } -.footer-about { - background-color: var(--ifm-color-secondary-lightest); - padding: 3rem var(--ifm-spacing-horizontal); -} - -.footer-about-inner { - margin: 0 auto; - max-width: var(--wide-width); +/* --- Bottom band: mirrors the headstartwp.fueled.com site footer --- */ + +.footer-fueled { + background-color: #323F60; + color: #fff; + font-size: 0.8rem; + font-weight: 300; + padding: 3.5rem var(--ifm-spacing-horizontal); text-align: center; } -.cta-careers { - vertical-align: top; +.footer-fueled a { + color: #fff; + text-decoration: underline; } -.license { - margin-top: 20px; +.footer-fueled a:hover { + color: #ddd; } -.license p { +.footer-fueled p { margin: 0; } -.license .copyright { - margin-right: 4px; +.footer-fueled .footer-links { + list-style: none; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 4px 18px; + margin: 6px 0 0; + padding: 0; } -@media (min-width: 900px) { - .cta-careers { - display: inline-block; - vertical-align: top; - text-align: left; - width: 48%; - } - - .license { - display: inline-block; - margin-left: 2%; - text-align: right; - width: 48%; - margin-top: 0; - } +.footer-fueled .footer-links li { + opacity: 0.75; } -.cta-careers .button { - margin-top: .5em; - background-color: var(--ifm-color-primary); - text-decoration: none; +.footer-fueled .footer-links li:hover { + opacity: 1; } -.cta-careers .button:hover { - background-color: var(--ifm-color-primary-dark); +.footer-fueled .footer-links a { + color: inherit; } - -.footer-10up { - padding: 3rem var(--ifm-spacing-horizontal); +.footer-fueled .logo img { + width: 170px; + height: auto; + display: block; + margin: 30px auto; } -.footer-10up .wrap { - margin: 0 auto; - max-width: var(--wide-width); - text-align: center; +.footer-fueled .social ul { + list-style: none; + display: flex; + justify-content: center; + gap: 5px; + margin: 0; + padding: 0; } -.footer-10up .social-media svg { - width: 25px; - margin-right: 15px; +.footer-fueled .social svg { + width: 24px; + display: block; } -.footer-10up .social-media path { - fill: var(--ifm-color-gray-600); +.footer-fueled .social path { + fill: #fff; } -.footer-10up .social-media svg:hover path { - fill: var(--ifm-color-black); +.footer-fueled .social a:hover path { + fill: #ddd; } -.footer-10up p { - margin: 20px 0; +.footer-fueled .sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); } @media (min-width: 900px) { - .footer-10up .wrap { - display: flex; - text-align: left; + .footer-fueled .wrap { + display: grid; + grid-template-columns: 1fr 170px 1fr; align-items: center; - justify-content: space-between; + max-width: var(--wide-width); + margin: 0 auto; + text-align: left; + } + + .footer-fueled .footer-links { + justify-content: flex-start; + } + + .footer-fueled .logo img { + margin: 0 auto; } - .footer-10up p { - margin: 0; + .footer-fueled .social ul { + justify-content: flex-end; } -} \ No newline at end of file +} diff --git a/docs/src/css/global-header.css b/docs/src/css/global-header.css index 24019fc91..5be752860 100644 --- a/docs/src/css/global-header.css +++ b/docs/src/css/global-header.css @@ -8,4 +8,18 @@ html:not(.docs-wrapper) .navbar { box-shadow: none; -} \ No newline at end of file +} +/* CTA back to the main site (mirrors the site header's Get Started chip). */ +.navbar-site-cta { + background: #1C1F37; + color: #fff !important; + border-radius: 2px; + padding: 8px 16px; + margin-left: 8px; + font-weight: 600; + text-decoration: none; +} + +.navbar-site-cta:hover { + background: var(--ifm-color-primary); +} diff --git a/docs/src/css/variables.css b/docs/src/css/variables.css index 0c35aaafa..36dc3d32f 100644 --- a/docs/src/css/variables.css +++ b/docs/src/css/variables.css @@ -8,23 +8,26 @@ --g2-color-slate: #40464d; --g2-color-black: #1e1e1e; + /* HeadstartWP site palette (headstartwp.fueled.com): teal links on + navy ink, light-blue accents. */ --ifm-footer-link-color: var(--ifm-color-black); - --ifm-color-primary: #df2b26; - --ifm-color-primary-dark: #B5130E; - --ifm-color-primary-darker: #213dd3; - --ifm-color-primary-darkest: #8A0400; - --ifm-color-primary-light: #FE4641; - --ifm-color-primary-lighter: #FF6662; - --ifm-color-primary-lightest: #FF918E; + --ifm-color-primary: #33647E; + --ifm-color-primary-dark: #2B5468; + --ifm-color-primary-darker: #284E61; + --ifm-color-primary-darkest: #1C1F37; + --ifm-color-primary-light: #3B7492; + --ifm-color-primary-lighter: #3F7C9C; + --ifm-color-primary-lightest: #4BA0C4; --ifm-footer-background-color: #fff; --ifm-link-decoration: underline; --ifm-link-hover-color: var(--ifm-color-primary-dark); --ifm-code-font-size: 1rem; --ifm-container-width-xl: 130ch; --ifm-container-width: 90ch; - --ifm-font-family-base: -apple-system, BlinkMacSystemFont, "Segoe UI", - Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"; - --ifm-heading-font-family: var(--ifm-font-family-base); + --ifm-font-family-base: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, + "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji"; + --ifm-heading-font-family: "IBM Plex Sans Condensed", var(--ifm-font-family-base); --ifm-h1-font-size: 2.625rem; --ifm-h2-font-size: 2rem; --ifm-heading-font-weight: var(--ifm-font-weight-semibold); @@ -32,9 +35,6 @@ --ifm-leading-desktop: 1.5; --ifm-menu-color-background-active: white; --ifm-navbar-height: 4.375rem; - --c-10up-secondary: #E3FAF8; - --c-10up-secondary-dark: #7ED5D4; - --wide-width: min(calc(100vw - 2.5rem), calc(var(--ifm-container-width-xl) + 300px)); --docusaurus-highlighted-code-line-bg: #e0e0e0; diff --git a/docs/src/pages/index.js b/docs/src/pages/index.js index 96c306cf8..7b3c0c5af 100644 --- a/docs/src/pages/index.js +++ b/docs/src/pages/index.js @@ -18,7 +18,7 @@ export default function Home() {

HeadstartWP

-

Next.js Framework for WordPress

+

Next.js Framework for Headless WordPress

@@ -31,7 +31,7 @@ export default function Home() { height={237} /> -

Documentation

+

Developer Guide

If you are unsure how to do something with the framework, this is where you should start. diff --git a/docs/src/pages/index.module.css b/docs/src/pages/index.module.css index 16cf451f8..854907858 100644 --- a/docs/src/pages/index.module.css +++ b/docs/src/pages/index.module.css @@ -7,11 +7,61 @@ padding: 6rem 0; text-align: center; position: relative; - background-color: var(--c-10up-secondary); + /* No overflow:hidden here - the search dropdown must be able to + extend past the hero. The lattice stays contained because the + ::after box is inset:0 with border-radius:inherit. */ + /* Mirrors the main site's hero: soft lavender wash with the purple + radial from the top right, and the lattice graphic ghosted on the + right (CSS background keeps it inert - no click capture, no + gradient-id collisions). */ + background: + radial-gradient(153.4% 328.78% at 88.61% -33.98%, rgba(163, 107, 163, 0.24) 0%, rgba(163, 107, 163, 0) 53.12%), + #faf8fb; border-radius: 2rem; margin-inline: 1rem; } +.heroBanner::after { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + background: url("../../static/img/hero-lattice.svg") no-repeat right -8rem center; + background-size: auto 200%; + opacity: 0.5; + pointer-events: none; +} + +.heroBanner > * { + position: relative; + z-index: 1; +} + +/* The oversized lattice reads great wide, but scales down busy — step the + crop and strength down with the viewport. */ +@media (max-width: 1200px) { + .heroBanner::after { + background-position: right -5rem center; + background-size: auto 160%; + opacity: 0.4; + } +} + +@media (max-width: 996px) { + .heroBanner::after { + background-position: right -4rem center; + background-size: auto 130%; + opacity: 0.25; + } +} + +@media (max-width: 600px) { + .heroBanner::after { + background-size: auto 110%; + opacity: 0.18; + } +} + .heroBanner p { font-size: 1.25rem; } diff --git a/docs/src/pages/philosophy.md b/docs/src/pages/philosophy.md index c06fd74c6..026656b86 100644 --- a/docs/src/pages/philosophy.md +++ b/docs/src/pages/philosophy.md @@ -1,16 +1,16 @@ # Framework Principles -These are the guiding principles for 10up's Headless Framework. +These are the guiding principles for HeadstartWP, Fueled's framework for headless WordPress. ## Solid Foundation -We aren't trying to reinvent the wheel nor do we want to spend a massive amount of resources building a new foundation for our framework. Therefore we decided to pick an existing and solid foundation to power 10up's Headless Framework: [Next.js](https://nextjs.org/). +We aren't trying to reinvent the wheel nor do we want to spend a massive amount of resources building a new foundation for our framework. Therefore we decided to pick an existing and solid foundation to power HeadstartWP: [Next.js](https://nextjs.org/). Next.js is by far the most used Full-Stack React Framework and we believe using Next.js will give us a solid foundation for our framework and let us focus on what matters: solving headless WordPress sites. ## Reduce the complexity of building headless sites -The 10up headless framework aims at making creating headless sites as easy as creating traditional WordPress sites. We want to reduce the complexity that developers need to face when building headless WordPress sites from scratch. +HeadstartWP aims at making creating headless sites as easy as creating traditional WordPress sites. We want to reduce the complexity that developers need to face when building headless WordPress sites from scratch. We aim to let engineers focus on the important aspects of the site instead of spending time figuring out how to "wire up" the Next.js application with WordPress. @@ -20,7 +20,7 @@ We want to boost creativity and let engineers explore new ways of building and s ## Low cost of maintenance -The 10up headless framework is a thin layer built on top of a solid foundation. It focuses on interacting with WordPress. At the end of the day, it's a Next.js application. +HeadstartWP is a thin layer built on top of a solid foundation. It focuses on interacting with WordPress. At the end of the day, it's a Next.js application. This means the maintenance cost is low as the lowest-level and most complex parts are provided by Next.js which is maintained by Vercel and have been driving a lot of innovations alongside partners like Google. @@ -30,7 +30,7 @@ We also aim at maintaining a simple stack. ### REST API over WPGraphQL -The 10up's Headless Framework at the moment does not work with WPGraphQL. +HeadstartWP at the moment does not work with WPGraphQL. GraphQL is great and when used on the right project adds tons of value in the long run. However, for most headless sites, there isn’t much value added by GraphQL. The additional complexity and engineering time required by adopting GraphQL/WPGraphQL isn’t worth the cost most of the time (caching, persisted queries, cache-bursting, etc). diff --git a/docs/src/theme/Footer/index.js b/docs/src/theme/Footer/index.js index 03619e62c..d317c84c9 100644 --- a/docs/src/theme/Footer/index.js +++ b/docs/src/theme/Footer/index.js @@ -4,90 +4,91 @@ import React from 'react'; import Footer from '@theme-original/Footer'; import useBaseUrl from '@docusaurus/useBaseUrl'; +/** + * Bottom band mirrors the footer on headstartwp.fueled.com: greyish-blue + * ground, "Finely crafted by Fueled (formerly 10up)" with a links row, + * centered white Fueled lockup, GitHub + LinkedIn on the right. + */ export default function FooterWrapper(props) { + const year = new Date().getFullYear(); + return ( <>