Accepting Crypto Payments in Classic Commerce App

E-commerce storefronts have been slow to offer crypto payment methods to their customers. Crypto payment plug-ins or payment gateway integrations aren’t generally available, or they rely on third-party custodians to collect, exchange, and distribute money. Considering the growing ownership rate and experimentation ratio of cryptocurrencies, a “pay with crypto” button could greatly drive sales.

This article demonstrates how you can integrate a custom, secure crypto payment method into any online store without relying on a third-party service. Coding and maintaining smart contracts needs quite some heavy lifting under the hood, a job that we’re handing over to Truffle suite, a commonly used toolchain for blockchain builders. To provide access to blockchain nodes during development and for the application backend, we rely on Infura nodes that offer access to the Ethereum network at a generous free tier. Using these tools together will make the development process much easier.

Scenario: The Amethon Bookstore

The goal is to build a storefront for downloadable eBooks that accepts the Ethereum blockchain’s native currency (“Ether”) and ERC20 stablecoins (payment tokens pegged in USD) as a payment method. Let’s refer to it as “Amethon” from here on. Full implementation can be found on the accompanying GitHub monorepo. All code is written in Typescript and can be compiled using the package’syarn build oryarn devcommands.

We’ll walk you through the process step by step, but familiarity with smart contracts, Ethereum, and minimal knowledge of the Solidity programming language might be helpful to read along. We recommend you to read some fundamentals first to become familiar with the ecosystem’s basic concepts.

Application Structure

The store backend is built as a CRUD API that is not connected to any blockchain itself. Its front end triggers payment requests on that API, which customers fulfill using their crypto wallets.

Amethon is designed as a “traditional” ecommerce application that takes care of the business logic and doesn’t rely on any on-chain data besides the payment itself. During checkout, the backend issues PaymentRequest objects that carry a unique identifier (such as an “invoice number”) that users attach to their payment transactions.

A background daemon listens to the respective contract events and updates the store’s database when it detects a payment.

Payment settlements on Amethon

The PaymentReceiver Contract

At the center of Amethon, thePaymentReceiversmart contract accepts and escrows payments on behalf of the storefront owner.

Each time a user sends funds to thePaymentReceiver contract, aPaymentReceivedevent is emitted containing information about the payment’s origin (the customer’s Ethereum account), its total value, the ERC20 token contract address utilized, and thepaymentIdthat refers to the backend’s database entry.

 event PaymentReceived(
    address indexed buyer,
    uint256 value,
    address token,
    bytes32 paymentId
  );

Ethereum contracts act similarly to user-based (aka “externally owned” / EOA) accounts and get their own account address upon deployment. Receiving the native Ether currency requires implementing thereceiveandfallback functions which are invoked when someone transfers Ether funds to the contract, and no other function signature matches the call:

 receive() external payable {
    emit PaymentReceived(msg.sender, msg.value, ETH_ADDRESS, bytes32(0));
  }

  fallback() external payable {
    emit PaymentReceived(
      msg.sender, msg.value, ETH_ADDRESS, bytes32(msg.data));
  }

The official Solidity docs point out the subtle difference between these functions:receiveis invoked when the incoming transaction doesn’t contain additional data, otherwise fallback is called. The native currency of Ethereum itself is not an ERC20 token and has no utility besides being a counting unit. However, it has an identifiable address (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE) that we use to signal an Ether payment in ourPaymentReceivedevents.

Ether transfers, however, have a major shortcoming: the amount of allowed computation upon reception is extremely low. The gas sent along by customers merely allows us to emit an event but not to redirect funds to the store owner’s original address. Therefore, the receiver contract keeps all incoming Ethers and allows the store owner to release them to their own account at any time:

function getBalance() public view returns (uint256) {
  return address(this).balance;
}

function release() external onlyOwner {
  (bool ok, ) = _owner.call{value: getBalance()}("");
  require(ok, "Failed to release Eth");
}

Accepting ERC20 tokens as payment is slightly more difficult for historical reasons. In 2015, the authors of the initial specification couldn’t predict the upcoming requirements and kept the ERC20 standard’s interface as simple as possible. Most notably, ERC20 contracts aren’t guaranteed to notify recipients about transfers, so there’s no way for ourPaymentReceiver to execute code when ERC20 tokens are transferred to it.

The ERC20 ecosystem has evolved and now includes additional specs. For example, the EIP 1363 standard addresses this very problem. Unfortunately, you cannot rely on major stablecoin platforms to have implemented it.

So Amethon must accept ERC20 token payments in the “classic” way. Instead of “dropping” tokens on it unwittingly, the contract takes care of the transfer on behalf of the customer. This requires users to first allow the contract to handle a certain amount of their funds. This inconveniently requires users to first transmit anApproval transaction to the ERC20 token contract before interacting with the real payment method. EIP-2612 might improve this situation, however, we have to play by the old rules for the time being.

 function payWithErc20(
    IERC20 erc20,
    uint256 amount,
    uint256 paymentId
  ) external {
    erc20.transferFrom(msg.sender, _owner, amount);
    emit PaymentReceived(
      msg.sender,
      amount,
      address(erc20),
      bytes32(paymentId)
    );
  }

Compiling, Deploying, and Variable Safety

Several toolchains allow developers to compile, deploy, and interact with Ethereum smart contracts, but one of the most advanced ones is the Truffle Suite. It comes with a built-in development blockchain based on Ganache and a migration concept that allows you to automate and safely run contract deployments.

Deploying contracts on “real” blockchain infrastructure, such as Ethereum testnets, requires two things: an Ethereum provider that’s connected to a blockchain node and either the private keys/wallet mnemonics of an account or a wallet connection that can sign transactions on behalf of an account. The account also needs to have some (testnet) Ethers on it to pay for gas fees during deployment.

MetaMask does that job. Create a new account that you’re not using for anything else but deployment (it will become the “owner” of the contract) and fund it with some Ethers using your preferred testnet’s faucet (we recommend Paradigm). Usually, you would now export that account’s private key (“Account Details” > “Export Private Key”) and wire it up with your development environment but to circumvent all security issues implied by that workflow, Truffle comes with a dedicated dashboard network and web application that can be used to sign transactions like contract deployments using Metamask inside a browser. To start it up, execute truffle dashboard in a fresh terminal window and visit http://localhost:24012/ using a browser with an active Metamask extension.

Using truffle’s dashboard to sign transactions without exposing private keys

The Amethon project also relies on various secret settings. Note that due to the way dotenv-flowworks,.envfiles contain samples or publicly visible settings, which are overridden by gitignored.env.localfiles. Copy all .env files in the packages’ subdirectories to.env.locals and override their values.

To connect your local environment to an Ethereum network, access a synced blockchain node. While you certainly could download one of the many clients and wait for it to sync on your machine, it is far more convenient to connect your applications to Ethereum nodes that are offered as a service, the most well-known being Infura. Their free tier provides you with three different access keys and 100k RPC requests per month supporting a wide range of Ethereum networks.

After signup, take note of your Infura key and put it in your contracts .env.localas INFURA_KEY.

If you’d like to interact with contracts, e.g. on the Kovan network, simply add the respective truffle configuration and an--network kovan option to all your truffle commands. You can even start an interactive console:yarn truffle console --network kovan. There isn’t any special setup process needed to test contracts locally. To make our lives simple we’re using the providers and signers injected by Metamask through the truffle dashboard provider instead.

Change to thecontracts folder and runyarn truffle develop. This will start a local blockchain with pre-funded accounts and open a connected console on it. To connect your Metamask wallet to the development network, create a new network using http://localhost:9545 as its RPC endpoint. Take note of the accounts listed when the chain starts: you can import their private keys into your Metamask wallet to send transactions on their behalf on your local blockchain.

Typecompileto compile all contracts at once and deploy them to the local chain with migrate.You can interact with contracts by requesting their currently deployed instance and call its functions like so:

pr = await PaymentReceiver.deployed()
balance = await pr.getBalance()

Once you’re satisfied with your results, you can then deploy them on a public testnet (or mainnet), as well:

yarn truffle migrate --interactive --network dashboard

The Backend

The Store API / CRUD

Our backend provides a JSON API to interact with payment entities on a high level. We’ve decided to use TypeORM and a local SQLite database to support entities for Books and PaymentRequests. Books represent our shop’s main entity and have a retail price, denoted in USD cents. To initially seed the database with books, you can use the accompanyingseed.ts file. After compiling the file, you can execute it by invokingnode build/seed.js.

//backend/src/entities/Book.ts
import { Entity, Column, PrimaryColumn, OneToMany } from "typeorm";
import { PaymentRequest } from "./PaymentRequest";

@Entity()
export class Book {
  @PrimaryColumn()
  ISBN: string;

  @Column()
  title: string;

  @Column()
  retailUSDCent: number;

  @OneToMany(
    () => PaymentRequest,
    (paymentRequest: PaymentRequest) => paymentRequest.book
  )
  payments: PaymentRequest[];
}

Heads up: storing monetary values as float values are strongly discouraged on any computer system because operating on float values will certainly introduce precision errors. This is also why all crypto tokens operate with 18 decimal digits and Solidity doesn’t even have a float data type. 1 Ether actually represents “1000000000000000000” Wei, the smallest Ether unit.

For users who intend to buy a book from Amethon, create an individualPaymentRequestfor their item first by calling the/books/:isbn/orderroute. This creates a new unique identifier that must be sent along with each request.

We’re using plain integers here, however, for real-world use cases, you’ll use something more sophisticated. The only restriction is the id’s binary length which must fit into 32 bytes (uint256). EachPaymentRequest inherits the book’s retail value in USD cents and bears the customer’s address, fulfilledHash and paidUSDCentwill be determined during the buying process.

//backend/src/entities/PaymentRequest.ts
@Entity()
export class PaymentRequest {
  @PrimaryGeneratedColumn()
  id: number;

  @Column("varchar", { nullable: true })
  fulfilledHash: string | null;

  @Column()
  address: string;

  @Column()
  priceInUSDCent: number;

  @Column("mediumint", { nullable: true })
  paidUSDCent: number;

  @ManyToOne(() => Book, (book) => book.payments)
  book: Book;
}

An initial order request that creates a PaymentRequestentity looks like this:

POST http://localhost:3001/books/978-0060850524/order
Content-Type: application/json

{
  "address": "0xceeca1AFA5FfF2Fe43ebE1F5b82ca9Deb6DE3E42"
}
--->
{
  "paymentRequest": {
    "book": {
      "ISBN": "978-0060850524",
      "title": "Brave New World",
      "retailUSDCent": 1034
    },
    "address": "0xceeca1AFA5FfF2Fe43ebE1F5b82ca9Deb6DE3E42",
    "priceInUSDCent": 1034,
    "fulfilledHash": null,
    "paidUSDCent": null,
    "id": 6
  },
  "receiver": "0x7A08b6002bec4B52907B4Ac26f321Dfe279B63E9"
}

The Blockchain Listener Background Service

Querying a blockchain’s state tree doesn’t cost clients any gas but nodes still need to compute. When those operations become too computation-heavy, they can time out. For real-time interactions, it is highly recommended to not poll chain state but rather listen to events emitted by transactions. This requires the use of WebSocket-enabled providers, so make sure to use the Infura endpoints that start with wss://as URL scheme for your backend’s PROVIDER_RPC environment variable. Then you can start the backend’s daemon.tsscript and listen for PaymentReceivedevents on any chain:

//backend/src/daemon.ts
  const web3 = new Web3(process.env.PROVIDER_RPC as string);
  const paymentReceiver = new web3.eth.Contract(
    paymentReceiverAbi as AbiItem[],
    process.env.PAYMENT_RECEIVER_CONTRACT as string
  );

  const emitter = paymentReceiver.events.PaymentReceived({
    fromBlock: "0",
  });

  emitter.on("data", handlePaymentEvent);
})();

Take note of how we’re instantiating the Contract instance with an Application Binary Interface. The Solidity compiler generates the ABI and contains information for RPC clients on how to encode transactions to invoke and decode functions, events, or parameters on a smart contract.

Once instantiated, you can hook a listener on the contract’s PaymentReceived logs (starting at block 0) and handle them once received.

Since Amethon supports Ether and stablecoin (“USD”) payments, the daemon’s handlePaymentEventmethod first checks which token has been used in the user’s payment and computes its dollar value, if needed:

//backend/src/daemon.ts
const ETH_USD_CENT = 2_200 * 100;
const ACCEPTED_USD_TOKENS = (process.env.STABLECOINS as string).split(",");
const NATIVE_ETH = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";

const handlePaymentEvent = async (event: PaymentReceivedEvent) => {
  const args = event.returnValues;
  const paymentId = web3.utils.hexToNumber(args.paymentId);
  const decimalValue = web3.utils.fromWei(args.value);
  const payment = await paymentRepo.findOne({ where: { id: paymentId } });
  let valInUSDCents;
  if (args.token === NATIVE_ETH) {
    valInUSDCents = parseFloat(decimalValue) * ETH_USD_CENT;
  } else {
    if (!ACCEPTED_USD_TOKENS.includes(args.token)) {
      return console.error("payments of that token are not supported");
    }
    valInUSDCents = parseFloat(decimalValue) * 100;
  }

  if (valInUSDCents < payment.priceInUSDCent) {
    return console.error(`payment [${paymentId}] not sufficient`);
  }

  payment.paidUSDCent = valInUSDCents;
  payment.fulfilledHash = event.transactionHash;
  await paymentRepo.save(payment);
};

The Frontend

Our bookstore’s frontend is built on the official Create React App template with Typescript support and uses Tailwind for basic styles. It supports all known CRA scripts so you can start it locally by yarn start after you created your own .env.local file containing the payment receiver and stablecoin contract addresses you created before.

Heads up: CRA5 bumped their webpack dependency to a version that no longer supports node polyfills in browsers. This breaks the builds of nearly all Ethereum-related projects today. A common workaround that avoids ejecting is to hook into the CRA build process. We’re using react-app-rewired but you could simply stay at CRA4 until the community comes up with a better solution.

Connecting a WEB3 Wallet

The crucial part of any Dapp is connecting to a user’s wallet. You could try to manually wire that process following the official MetaMask docs but we strongly recommend using an appropriate React library. We found Noah Zinsmeister’s web3-react to be the best. Detecting and connecting a web3 client boils down to this code (ConnectButton.tsx):

//frontend/src/components/ConnectButton.ts
import { useWeb3React } from "@web3-react/core";
import { InjectedConnector } from "@web3-react/injected-connector";
import React from "react";
import Web3 from "web3";

export const injectedConnector = new InjectedConnector({
  supportedChainIds: [42, 1337, 31337], //Kovan, Truffle, Hardhat
});

export const ConnectButton = () => {
  const { activate, account, active } = useWeb3React<Web3>();

  const connect = () => {
    activate(injectedConnector, console.error);
  };

  return active ? (
    <div className="text-sm">connected as: {account}</div>
  ) : (
    <button className="btn-primary" onClick={connect}>
      Connect
    </button>
  );
};

By wrapping your App‘s code in an <Web3ReactProvider getLibrary={getWeb3Library}> context you can access the web3 provider, account, and connected state using theuseWeb3Reacthook from any component. Since Web3React is agnostic to the web3 library being used (Web3.js or ethers.js), you must provide a callback that yields a connected “library”:

//frontend/src/App.tsx
import Web3 from "web3";
function getWeb3Library(provider: any) {
  return new Web3(provider);
}

Payment Flows

After loading the available books from the Amethon backend, the <BookView>component first checks whether payments for this user have already been processed and then displays all supported payment options bundled inside the <PaymentOptions> component.

Paying With ETH

The <PayButton> is responsible for initiating direct Ether transfers to the PaymentReceivercontract. Since these calls are not interacting with the contract’s interface directly, we don’t even need to initialize a contract instance:

//frontend/src/components/PayButton.tsx
const weiPrice = usdInEth(paymentRequest.priceInUSDCent);

const tx = web3.eth.sendTransaction({
  from: account, //the current user
  to: paymentRequest.receiver.options.address, //the PaymentReceiver contract address
  value: weiPrice, //the eth price in wei (10**18)
  data: paymentRequest.idUint256, //the paymentRequest's id, converted to a uint256 hex string
});
const receipt = await tx;
onConfirmed(receipt);

As explained earlier, since the new transaction carries a msg.data field, Solidity’s convention triggers the PaymentReceiver's fallback() external payable function that emits a PaymentReceivedevent with Ether’s token address. This is picked up by the daemonized chain listener that updates the backend’s database state accordingly.

A static helper function is responsible for converting the current dollar price to an Ether value. In a real-world scenario, query the exchange rates from a trustworthy third party like Coingecko or from a DEX like Uniswap. Doing so allows you to extend Amethon to accept arbitrary tokens as payments.

//frontend/src/modules/index.ts
const ETH_USD_CENT = 2_200 * 100;
export const usdInEth = (usdCent: number) => {
  const eth = (usdCent / ETH_USD_CENT).toString();
  const wei = Web3.utils.toWei(eth, "ether");
  return wei;
};

Paying With ERC20 Stablecoins

For reasons mentioned earlier, payments in ERC20 tokens are slightly more complex from a user’s perspective since one cannot simply drop tokens on a contract. Like nearly anyone with a comparable use case, we must first ask the user to give their permission for our PaymentReceivercontract to transfer their funds and call the actual payWithEerc20 method that transfers the requested funds on behalf of the user.

Here’s the PayWithStableButton‘s code for giving permission on a selected ERC20 token:

//frontend/src/components/PayWithStableButton.tsx
const contract = new web3.eth.Contract(
  IERC20ABI as AbiItem[],
  process.env.REACT_APP_STABLECOINS
);

const appr = await coin.methods
  .approve(
    paymentRequest.receiver.options.address, //receiver contract's address
    price // USD value in wei precision (1$ = 10^18wei)
  )
  .send({
    from: account,
  });

Note that the ABI needed to set up a Contract instance of the ERC20 token receives a general IERC20 ABI. We’re using the generated ABI from OpenZeppelin’s official library but any other generated ABI would do the job. After approving the transfer we can initiate the payment:

//frontend/src/components/PayWithStableButton.tsx
const contract = new web3.eth.Contract(
  PaymentReceiverAbi as AbiItem[],
  paymentRequest.receiver.options.address
);
const tx = await contract.methods
  .payWithErc20(
    process.env.REACT_APP_STABLECOINS, //identifies the ERC20 contract
    weiPrice, //price in USD (it's a stablecoin)
    paymentRequest.idUint256 //the paymentRequest's id as uint256
  )
  .send({
    from: account,
  });

Signing Download Requests

Finally, our customer can download their eBook. But there’s an issue: Since we don’t have a “logged in” user, how do we ensure that only users who actually paid for content can invoke our download route? The answer is a cryptographic signature. Before redirecting users to our backend, the <DownloadButton> component allows users to sign a unique message that is submitted as proof of account control:

//frontend/src/components/DownloadButton.tsx
const download = async () => {
  const url = `${process.env.REACT_APP_BOOK_SERVER}/books/${book.ISBN}/download`;

  const nonce = Web3.utils.randomHex(32);
  const dataToSign = Web3.utils.keccak256(`${account}${book.ISBN}${nonce}`);

  const signature = await web3.eth.personal.sign(dataToSign, account, "");

  const resp = await (
    await axios.post(
      url,
      {
        address: account,
        nonce,
        signature,
      },
      { responseType: "arraybuffer" }
    )
  ).data;
  // present that buffer as download to the user...
};

The backend’s download route can recover the signer’s address by assembling the message in the same way the user did before and calling the crypto suite’s ecrecover method using the message and the provided signature. If the recovered address matches a fulfilled PaymentRequest on our database, we know that we can permit access to the requested eBook resource:

//backend/src/server.ts
app.post(
  "/books/:isbn/download",
  async (req: DownloadBookRequest, res: Response) => {
    const { signature, address, nonce } = req.body;

    //rebuild the message the user created on their frontend
    const signedMessage = Web3.utils.keccak256(
      `${address}${req.params.isbn}${nonce}`
    );

    //recover the signer's account from message & signature
    const signingAccount = await web3.eth.accounts.recover(
      signedMessage,
      signature,
      false
    );

    if (signingAccount !== address) {
      return res.status(401).json({ error: "not signed by address" });
    }

    //deliver the binary content...
  }
);

The proof of account ownership presented here is still not infallible. Anyone who knows a valid signature for a purchased item can successfully call the download route. The final fix would be to create the random message on the backend first and have the customer sign and approve it. Since users cannot make any sense of the garbled hex code they’re supposed to sign, they won’t know if we’re going to trick them into signing another valid transaction that might compromise their accounts.

Although we’ve avoided this attack vector by making use of web3’s eth.personal.signmethod it is better to display the message to be signed in a human-friendly way. That’s what EIP-712 achieves—a standard already supported by MetaMask.

Conclusion and Next Steps

Accepting payments on e-commerce websites has never been an easy task for developers. While the web3 ecosystem allows storefronts to accept digital currencies, the availability of service-independent plugin solutions falls short. This article demonstrated a safe, simple, and custom way to request and receive crypto payments.

There’s room to take the approach a step or two further. Gas costs for ERC20 transfers on the Ethereum mainnet are exceeding our book prices by far. Crypto payments for low-priced items would make sense in gas-friendly environments like Gnosis Chain (their “native” Ether currency is DAI, so you wouldn’t even have to worry about stablecoin transfers here) or Arbitrum. You could also extend the backend with cart checkouts or use DEXes to swap any incoming ERC20 tokens into your preferred currency.

After all, the promise of web3 is to allow direct monetary transactions without middlemen and to add significant value to online stores that want to engage their crypto-savvy customers.

Credit: Source link

How to Add Old Post Notification on Your WordPress Blog

Do you want to add an old post notification in WordPress?

If you have been producing content for some time now, then there is a chance that some of your content may be outdated.

In this article, we’ll show you how to easily add old post notification to your WordPress blog.

Adding old post notice to WordPress

Why Add Old Post Notification to WordPress Blog Posts

Content decay (outdated blog posts) can be a bit of a problem for a growing WordPress blogs.

Depending on your niche, sometimes your content may become irrelevant, incorrect, or inappropriate over a period of time. This may cause a bad user experience, a higher bounce rate, and lower search rankings.

Ideally, you would want to edit those articles and update them with more useful, accurate, and up-to-date information.

But that’s not always possible because your site may have too many old articles, and you may not have enough resources to update them. In that case, adding an old post notification may be helpful for your users.

It will let them know that the content is a bit older, and they should keep this in mind when using the information presented on that page.

Another solution that many blogs use is by simply adding the ‘Last updated date’ instead of the publishing date.

An older article with last updated date

That being said, let’s take a look at how you can add the old post notification in WordPress, and how to display the last updated date on your articles.

Method 1. Display Old Post Notification Using Plugin

This method is easier and recommended for all users that want to display an old post notification.

First, you need to install and activate the DX Out of Date plugin. For more details, see our step-by-step guide on how to install a WordPress plugin.

Upon activation, you need to visit the Settings » Out of Date page to configure plugin settings.

Out of date settings

Here, you need to choose the period and duration. This is the time after which a post will be considered old by the plugin.

Below that you can provide a custom message to display on older posts and enable the notification to be displayed for all old posts. Don’t worry, you’ll be able to hide it for specific posts by editing them.

On the settings page, you can also choose post types, colors for the notification box, and add custom CSS if needed.

Advanced settings for outdated posts

Don’t forget to click on the Save Changes button to store your settings.

You can now visit an old post on your website to see the plugin in action.

Old post notification displayed on an article

Hiding Old Post Notification on Individual Posts

Now let’s say you have an article that is older, but it is still accurate, up-to-date, and has great search rankings. You may want to hide the old post notification there.

Similarly, what if you have updated an old post with new information. The plugin will keep showing old post notification because it uses the post’s published date to determine its age.

To fix this, you can edit the post and scroll down to the ‘Out of Date Notification’ tab under the Post panel of the block editor. From here, simply uncheck the notification option and save your changes.

Hide old post notification

The plugin will now stop showing old post notification on this particular article.

Method 2. Display Last Modified Date for Your Posts

A lot of WordPress websites display the last modified date for their blog posts. Some replace the publish date with the last modified date as well.

The advantage of this method is that it shows users when a post was last updated without showing an old post message.

First, you need to install and activate the WP Last Modified Info plugin. For more details, see our step by step guide on how to install a WordPress plugin.

Upon activation, head over to Settings » WP Last Modified Info page to configure plugin settings.

Last modified settings

On the settings page, you need to turn on the Global display of the last modified info toggle. After that, you can choose how you want to display the modified date.

You can replace the published date, show it before or after the content, or manually insert it into a post.

Below that, you’ll find a bunch of options. If you are unsure, then you can leave them to default.

Don’t forget to click on the Save Settings button to store your changes.

You can now visit your website to see the last updated information for all your blog posts.

Last updated notice

This problem with this method is that it will show the last updated date for all posts including the newer posts.

You can set a time gap under plugin settings. But this gap is only limited to 30 days.

Time gap between published and updated posts

The plugin also provides three blocks that you can manually insert into a post or page to display last modified info.

Add last modified information using blocks

You also have the option to use custom CSS to style your last updated date notice. We used the following custom CSS in the screenshots above.

p.post-modified-info {
    background: #fbffd8;
    padding: 10px;
    border: 1px solid orange;
    font-size: small;
    font-weight: bold;
}

Method 3. Add Old Post Notification Using Code

This method requires you to manually add code to your WordPress theme files. If you haven’t done this before, then take a look at our guide on how to add custom code snippets in WordPress.

Simply copy and paste the following code in your theme’s single.php template.

// Define old post duration to one year
$time_defined_as_old = 60*60*24*365; 

// Check to see if a post is older than a year
if((date('U')-get_the_time('U')) > $time_defined_as_old) {

$lastmodified = get_the_modified_time('U');
$posted = get_the_time('U');

//check if the post was updated after being published
 if ($lastmodified > $posted) {
 
// Display last updated notice
      echo '<p class="old-article-notice">This article was last updated ' . human_time_diff($lastmodified,current_time('U')) . ' ago</p>';   

  } else { 
// Display last published notice 
echo '<p class="old-article-notice">This article was published ' . human_time_diff($posted,current_time( 'U' )). 'ago</p>';

}
}

This code defines old posts to be any articles published at least one year ago.

After that, it checks if a post is older than a year. If it is, then it checks if the post was updated after publication. Then it displays a notice based on those checks.

Here is how it looked on our demo website for post that is old and was never updated.

Last updated information for old post

Here is how it looked for a post that is old, but it was updated after being published.

An old post that is never updated

We customized the old post notification with the following custom CSS.

p.old-article-notice {
    background: #fbffd8;
    padding: 10px;
    border: 1px solid orange;
    font-size: small;
    font-weight: bold;
}

We hope this article helped you learn how to easily display old post notification on your WordPress blog. You may also want to see our WordPress SEO guide or see our pick of the best popular posts plugins for WordPress.

If you liked this article, then please subscribe to our YouTube Channel for WordPress video tutorials. You can also find us on Twitter and Facebook.


Credit: Source link

Tips to Create a Proper Go Project Layout

matched_content]
Credit: Source link