I built a CMS recently that was designed from the get-go to be "cache-friendly". Although the load to build each page was not high enough to warrant implementing server-side caching, I did include HTTP headers intended to strongly suggest to the browser that it cache my pages (Mittineaque is exactly right, best you can do is suggest that the browser cache your pages, it's entirely up to the browser whether it will or not).
Since every "page" in my CMS included a column in the database to hold the last-modified date, and I was using a single file as the template, sending a last-modified header was easy:
I also used the ETag header, although in practice it's just a failsafe in case a browser doesn't acknowledge Last-Modified:
//date of the last modification of the content or the template
header("Last-Modified: ".gmdate("r", max($cms['lastmodified'], filemtime($template))));
Then every request made to the server that includes either a If-Modified-Since or If-None-Match has those values compared with the ones computed above; if they match, I send an HTTP 304 Not Modified response and terminate execution (as per spec, a 304 response MUST NOT include any entity-body - the whole point behind the introduction of this header was to reduce bandwidth by not sending the same data over and over). This improved performance for me by about 3 fold since there were several very complex SQL statements involved in generating a page; for cache misses (that is, the browser needs a fresh page either because its cache is stale or this is its first request) this adds overhead of about 1 millisecond to a page that takes almost 10 milliseconds to parse - I saved on average 85% of my load time!
//if the content or the template change, the page has changed