|
A functionally invisible frame...
I use this term because the script takes the original html file and rewrites
it. If you look at a site that uses Framed! The correct title and URL are displayed
in your browser, and the source includes the full text of the page in the <noframes>
tag. When you click on a link in a Framed! site, the page title and URL change,
just as if there were no frameset. Viewing the page from a non-frames browser
shows the full page as it was originally encoded, with an optional header message
in place of the frame. This is what I mean by functionally invisible - the frame
appears at the top of the page, visible to the user, but without changing any
of the dynamics of the way the site works. Why do this?
Good question. It started because I wanted to make it very clear that my
portfolio cache was just that, a cache and not the live site. Many people
don't look at the URL of pages they're visiting and so it's easy to confuse
my (most likely outdated) cached sites (eg: http://lorriechurch.cache.ravis.org)
with an actual live site.
Sounds Simple...
It does, doesn't it? Read on...
I started by adding a PHP script that ran with every page
load, using the php_value auto_prepend_file setting in my apache.conf
file. That PHP file then pre-pended a JavaScript popup script to the start of
each page, this popup explaining that the site was a cache. But there are drawbacks
to this method - many people either disable popups or JavaScript altogether.
The newest version of Norton Antivirus actually tries and claim many popups
are viri, which is odd.
The obvious solution to these problems was to have a notice
plastered on each page. There are several ways to do this, including the GeoCities
method of placing a JavaScript layer on the page. I personally don't like this
because it covers up content, as well as again requiring JavaScript to be on.
It is also not acceptable to use with frames because you can never tell which
frame to place the layer in. The only acceptable method, from a design and maximum
compatibility point of view, was to go with a frame at the top.
This presents it's own problems though. Frames are disliked
by many people (including myself) because, which they often look nice, they
destroy many of the useful features of a browser. Bookmarks become almost unusable,
since bookmarking a page bookmarks the frameset, not the frame you're looking
at. Search engines will link to pages within your frameset, without loading
the frameset, and so you get those pages that say "Click on a menu item
in the frame to the left" when there is no frame to the left. Page titles
get all out of whack because again, you're viewing the title of the frameset
and not the frame you're looking at. Lastly there are compatibility issues for
browsers that don't support frames (before you scoff, think mobile phones and
PDAs). Fortunately I have, in the past, done a great deal of work with frames,
and have developed various ways around the shortcomings. One of my more recent
sites used many of these ideas to circumvent the problems.
Frameset Problem #1: Bookmarks
Solution: All links open in the _top window space - that is, in the top of the
browser. This allows the page URL to refresh. It also requires redrawing the
frameset on every page load, but there's really no other way around it. It's
a small sacrifice in order to circumvent the problems.
Frameset Problem #2: Search Engines
Solution: Automatically loading the frameset at the top of each page. This solution
goes hand in hand with Problem #1, in that each page loads at the _top of the
window. But instead of having a separate page for the frameset and another for
the frame contents, we have only the frame contents. When the page is requested,
our code dynamically creates the frameset, sends it to the browser, which then
requests the frame contents. We must, however, exercise caution here in order
to prevent infinitely recursive framesets - we only want one frameset and one
content frame.
Frameset Problem #3: Page Titles
Solution: This problem can be solved by parsing the requested page and retrieving
the title from within the <title> tag. We can then take that title and
apply it to the frameset, which will in turn be displayed in the browser.
Frameset Problem #4: Compatibility & Search Engines
Solution: By automatically parsing the page and including much of the code in
<noframes> tags, we are able to maintain backward compatibility. Since
I hate maintaining two copies of any one thing (in this case the frames content
and the noframes content, which should be identical) we need to parse and load
this content automatically, so that we only have one version to maintain. Also,
since search engines will read the noframes content, they'll be more likely
to link to our frameset page.
This works great for single pages! Every link opens in _top,
which it would do anyway if there was no frameset. We see the title we should
see, and backward compatibility is maintained! And it worked really well for
the site I developed for my client. You can see a cached copy here
(note the red "this site is a cache" header - which is what this
page is all about...
Frames Within Frames
However, we've introduced a whole slew of other problems now. The root of which
is this: What happens when we want to add our automatically generated frame
to a page that already contains frames? If we follow only the steps above each
frameset will contain it's own subframeset. In the case of my cache, each frame
will contain a red header saying "this is a cache". Obviously we don't
want this - not only is it ugly, but it will no doubt disturb the layout of
the page (frames are often made to fit the content they display - if we add
more content they won't fit anymore).
Solution! Rewrite any framesets in the page to include a variable
that we check for. If that variable exists then we don't load the automatic
frameset. If that variable doesn't exist then it means the user has requested
this page as a _top page and we are free to add the frame. This is fairly simple
to do, in that we look for any <frame> (or <iframe>) tags, find
the src="<URL>" attribute within them, and add a variable to
the end of the URL So if a page contains a frameset:
<frame src="hello/there.html">
we need to rewrite it to say:
<frame src="hello/there.html?frameset=false">
Then when we get any page that has a "frameset=false" in the URL,
we don't write a new frameset, and instead simply dump the page out to the browser.
(Note that I'm using .html here instead of PHP - that's because my cached pages
are all .htm and .html - I've configured Apache to run them as PHP scripts instead
of changing all the hundreds of filenames.)
Works great! Until you start clicking around in a frames site
that is. And this is where it starts getting complex...
Links, Targets, and Frames
<a href="hello/there/link.html">
What happens when you click on this link when it's in a frameset? If that link
opens in _top, we're fine - the above code works like a charm. What if we click
on a link that points to another frame? or to itself (_self)? Our code looks
at the URL, says "well, there's no 'frameset=false' in there, so let's
draw a frameset." Oops. Now we have the frameset within a frameset problem
again. Actually there are several problems here, and they all revolve around
targets...
Targets specify for the browser where the link that you just
clicked should be opened. By default, links are opened in the frame that the
link is in. For pages without framesets this works well because you normally
want to open links in the same window. For framesets (say with a menu on the
side) this doesn't work so well because what you really want to be doing is
opening the link in the main frame - where your content is displayed - and not
in the menu frame. To do this you specify a target.
There are three possible ways to determine a target. First
the browser looks at the link itself. If the link contains a target then this
is used when the user clicks that link. eg:
<a href="hello/there.html"
target="main">
If there is no target specified in the link, then the browser looks to the page's
<base> tag. The base tag defines a default target for all links on the
page (that don't have their own targets) and is contained in the page header
and looks something like:
<base target="main">
Failing to find a base tag, the browser will open the link in the same frame
as the link itself.
Why am I explaining all this? Because we need to understand
how targets work if we're going to get our automatic frames working properly
- we must teach the script that's generating the frames when to draw a new frameset
and when to leave the page alone.
To teach the script when to draw framesets and when not to
we therefore need to determine the default target and then go through the page
and change links.
To determine the default target we can simply look at the
<base target=""> tag for the page, right? Sort of. This works
great if there is a base tag (and if it contains a target reference). Many pages
don't, especially pages where the link is supposed to open in it's own frame
(_self) and pages that simply don't contain frames (_top). First we need to
figure out what layer we're on. In other words, if we weren't drawing an automatic
frameset around our content would we be on the _top? or are we on a subframe?
We can accomplish this by maintaining a depth counter. Instead of adding the
"frameset=false" to each frame src (as above) we add "depth=x"
where x is incremented from the received value (if any).
What happens is the first page you load (the automatic frameset)
creates a frameset with a link to the page content using our depth counter +
1. Since this is the first page we've loaded, our depth counter is missing (and
therefore 0). We add one and our frame tag then looks like:
<frame src="hello/there.html?depth=1">
The browser receives this frameset and loads the specified page (namely hello/there.html?depth=1).
Our script looks at the URL, finds our depth counter, and rewrites any subframesets
by incrementing that counter. So sub-frames would look like:
<frame src="hello/there/subframe.html?depth=2">
and so on until all the frames are loaded. This provides us with a simple way
of determining the default target tag (which, if you recall, is what we're after).
If the depth is 1, then our default target would be _top (without our automatic
frame). If it's greater than 1, then our default target is _self (because it
would be a subframe, even without our automatic frameset).
Now we have the default target we can start looking for links
to change. Basically, any link that doesn't have a target tag of it's own will
get assigned the default target tag. From then on it's simply a matter of determining
which targets need to be changed and which need to be left alone.
There are four special target tags in html. We've already
seen two of them: _top and _self. The two others are _blank (opens the content
in a new window) and _parent (opens content in the frame just above the current
one). Then there are the other targets - user defined targets. Let's address
each of these in turn.
_top
_top targets should never be rewritten because we always want to reload the
frameset so that we can refresh our title and URL So anytime we see a link with
a target of _top (or a link without a target and a page default target of _top)
we simply leave it alone.
_blank
_blank targets never need to be rewritten because the whole idea of opening
a link in _blank is to open it in a new window - so we want a frameset at the
top of that window. Easy.
_parent
_parent links are a little more difficult. If we're on a depth of 2 or less
then _parent actually refers to _top and we actually do want a frameset redrawn,
so we don't want to manipulate the link - leave it as is and the script will
reload the frameset if the user clicks on it. If, however, we're on a sub-subframe
(ie: with a depth of 3 or more) than we want to rewrite the link to include
a depth tag, and therefore not redraw our frameset (which would end up being
within our frameset, etc). So we change these links to include a depth counter
(just like our frames) that will fool our frames script into thinking that this
page is being loaded as part of a frameset (which, in fact, it is).
_self
_self targets are also a bit tricky. Like parent targets they can have two different
meanings depending on what _self refers to. If we're in a subframe (ie: depth
is 2 or more) then self truly does refer to self and we need to rewrite the
link so that we don't redraw our frameset. If, however, depth is 1 then _self
really refers to _top. In this case we want a frameset redrawn and so we leave
the link alone.
user defined targets
User defined targets always get rewritten because they are always opening in
sub-frames. The exception to this is when a user defined target is used to open
a new window. This complicates the situation tremendously and therefore we'll
just ignore it. :-) There's only so much we can do and this practice is fairly
rare anyway (most sites these days use JavaScript to open new windows).
Done! Wasn't that easy?
Now we have a script that automatically adds a frameset to
every page loaded, avoids many of the problems inherent in framesets, avoids
infinite recursion, and avoids loading automatic framesets within framesets.
Turns out to be a lot more complex than it first looks. But damn, was that fun
or what?
Now that your brain's fried, go watch a good movie
and recuperate...
|