Build a Theme Using Custom HTML

  1. Background
  2. Getting Started
  3. Base Template
  4. Index Page
  5. Video Playback Page
  6. Category Landing Page
  7. Search Results Page
  8. Custom Page Template
  9. Footer
  10. Embeddable iFrame Widget

Background

Vidcaster's theme system uses the Django templating language. For a basic understanding of how Django works, see the Django documentation. The Vidcaster theme API is a combination of built-in Django functionality and custom Django tags and variables.

Vidcaster's prebuilt themes are coded in Django as part of the code base of the Vidcaster application. Each prebuilt theme consists of several template files. The code within these files is different for each theme, although the template names are always the same.

In order to code your own custom Vidcaster theme, we make make these template files available to you via the Custom HTML feature, which is core to Vidcaster plans Gold and above.

The basic purpose of the Custom HTML feature is to provide access to the template files for each theme, allowing you to customize a brand new theme based on the code base of any prebuilt theme you choose.

The sky's the limit on what you can do with your own theme. The entire Vidcaster.com public-facing site is built using a Vidcaster custom theme!

Getting Started

  1. Familiarize yourself with Django.
  2. Review Vidcaster prebuilt themes, the theme outline documentation, and Photoshop template examples.
  3. Select a prebuilt theme from Vidcaster Admin > Site > Design to use as your starting point.
  4. Design a mockup in HTML or Photoshop.
  5. Code your design within the Django templates inside Vidcaster Admin > Site > Custom HTML.

In order to start customizing your theme, navigate to the Custom HTML area in Vidcaster Admin > Site > Custom HTML. You will see a list of all the available template files you can customize. See our theme content outline for an overview of the main page templates. The files that are loaded into the code editor are the original files of the prebuilt theme you have currently selected in Site > Design.

Before you have made edits to your Custom HTML files, you can toggle between prebuilt themes to get new template files in the code editor. HOWEVER, once you start editing files in the code editor, those edited files will override their respective equivalents within the prebuilt theme. This will result in a very broken looking theme. It's important to stick with one prebuilt theme as the code base, once you start editing it. If you edit every single file within Custom HTML (rather than just a few), then it of course doesn't matter which prebuilt theme you have chosen, since the entire theme will be overwritten with your custom theme. It's a good idea to have all your templates customized when you're done with your custom theme, to make sure your theme is completely protected and takes precedence over any other prebuilt theme.

Note: At the current time, Vidcaster does not support static asset uploads, so you will have to host JavaScript, CSS, and image files on your own server or hosting service.

A great way to get started is to review the templates on the theme content outline and start designing in Photoshop or directly with HTML locally or your own server. You can then integrate your code with Django in the Custom HTML area when you're ready. Another helpful thing to do is go through the prebuilt themes and look at the code within the Custom HTML idea to familiarize yourself with the basic structure and Django templates.

Download a zip file of the four main templates as psd examples. These are helpful in understanding the main elements of a Vidcaster theme and giving you some layout ideas. Don't be afraid to go crazy though, there are no limits in terms of what is possible with your design and layout!

The Base Template

The base template is the template that will contain the header and footer area of your theme pages, as well as some other global information and container wraps. All other templates are extensions of the base template.

One basic concept to understand about Django is blocks. If you declare a block of code in the base template - indicated by code inside {% block some-name %} code inside here {% endblock some-name %} it will automatically carry through to the other templates, assuming you place this code at the top of the page: {% extends "base.HTML" %}.

Tip: See Django's documentation for further reading on Django inheritance and template blocks

If you place a block on a template that inherits base.HTML, but leave the block empty or add code inside it that varies from what is on the base template, your new template will override the code from the base template for that particular block. In short, the sub-templates automatically inherit blocks of code from the base template unless they are explicitly overwritten.

On the flip side, you will see some examples of empty blocks inside the base template. A block must first be declared on the base template in order to be used on a sub-template. If you see an empty block on the base template, that may be because there is a corresponding block on a sub-template that actually does contain content. In other cases the block may not be in use at all, and could be removed from your theme.

Important content in the base template

  • {% if user.is_authenticated %} – checks if the user is logged in to Vidcaster. This is used to display the login bar, which is included as {% include "include/userstate.HTML" %}
  • block globalstatus – the code inside this block is for global errors and messages. You can use CSS to style it.
  • block endbodycontent – used on the custom page template to add scripts to the end of the body tag.
  • block headcontent – used on the custom page template to add scripts or CSS to the head tag.
  • twitter_screen_name and facebook_page_id – these account names can be set in the Vidcaster Add-ons settings, and will automatically populate.
  • current_site – your current site object. This has some attributes like name, description, domain, current_site.siteprofile.logo, current_site.siteprofile.favicon.url. You can use these for meta information.
  • /feeds/recent/ – the URI of the RSS feed for your site.
  • /custom.CSS – a stylesheet containing Django variables that control how the CSS affects the link color, text color, background color, and background image pickers within the Vidcaster Admin, Site > Design settings. Any CSS added to Custom CSS (also in Site > Design settings) is also appended to this file.
  • {% load page_extras %}, {% load item_extras %}, {% load sites_extras %} – these are files that contain custom functions. They can be loaded within any template page to enable the needed functionality.
  • media_path - this variable dynamically creates a path to the prebuilt theme's media folder (/media/themes/<theme-name>/). You do not have direct access the theme's media files, but rather you will be including your own media files (CSS, js, images) instead. One thing to note about this:

Note: Transcription is a special feature within Vidcaster that requires a jQuery function and accompanying CSS. Since you will be including your own js and CSS for your custom theme, you must copy and paste the transcription function and CSS into your media files if you would like transcription to be enabled for your theme. The jQuery function should not be touched, but the CSS can be adjusted slightly if needed. Here is the code you need:


//	initiate transcription function in ready function to call on load
$(function() {
	TRANSCRIPT.init();
});

// transcription function

var TRANSCRIPT = function () {

	var clickedSpan,
		currentPhrase = null,
		currentPhraseYPos,
		currentTime,
		expandCollapseButton,
		loadingSpinner,
		previousPhrase,
		transcriptHovered = false,
		transcriptHoveredTO,
		transcriptLineHeight,
		transcriptSpans,
		transcriptText,
		transcriptTextP,
		transcriptWrap,
		videoPlayer;

	return {

		//	Adds behavior for the caption box, for details pages of videos with transcripts provided.
		init: function () {
			//	Set up the first selectors we'll need
			transcriptWrap = document.getElementById('transcript-wrap');
			videoPlayer = document.getElementById('video-player');

			//	Check for the existance of the video player and one of the transcript divs 
			//	before continuing
			if (transcriptWrap && videoPlayer) {

				//	Continue setting up now that we know everything is there
				expandCollapseButton = document.getElementById('transcript-expand-button');
				transcriptText = document.getElementById('transcript-text');
				transcriptSpans = transcriptText.getElementsByTagName('span');
				transcriptTextP = transcriptText.getElementsByTagName('p')[0];

				//	Get the line-height of the text in the transcript area,
				//	to be used for some height calculations in a minute
				transcriptLineHeight = $(transcriptTextP).CSS('line-height');
				//	The value returned from CSS() includes "px" on the end,
				//	so we'll also trim off the last two characters
				transcriptLineHeight = transcriptLineHeight.substr(0, transcriptLineHeight.length - 2);

				//	When a phrase in the transcript is clicked
				$(transcriptSpans).click(function () {
					
					//	If left alone, VideoJS will show the loading spinner after clicking on a phrase,
					//	and it will stay visible until the next time it needs to hide (like hitting pause and play)
					//	So we'll locate the spinner here and hide it. I would prefer to only temporarily disable it
					//	but I couldn't find a way to do that in time.
					if (videoPlayer.contentWindow) {
						loadingSpinner = videoPlayer.contentWindow.document.getElementById('vjs-loading-spinner');
						if (loadingSpinner) {
							loadingSpinner.style.visibility = "hidden";
						}
					}
					
					//	We'll want to set this span to be the current phrase
					//	First we'll want to find what position it has in the spans array
					//	Store the clicked span to use it for comparisons
					clickedSpan = this;
					//	Then go through all of the transcript spans
					$(transcriptSpans).each(function(i) {
						//	And check each one to see if we can find a match
						if (transcriptSpans[i] == clickedSpan) {
							//	If a match is found, remove the highlight class from the current phrase
							$(transcriptSpans[currentPhrase]).removeClass();
							//	Set the current position to be the new current Phrase
							currentPhrase = i;
							//	apply the highlight class to the new current phrase
							$(transcriptSpans[currentPhrase]).addClass('current-phrase');
							//	We're done here, break the each() loop
							return false;
						}
					});

					//	Find the video player iframe
					videoPlayer.contentWindow
						//	Reference the VideoJS object within the iframe
						.VideoJS('vid')
						//	Set the current time of the video to the ID of this span,
						//	converted from milliseconds to seconds
						.currentTime(($(this).attr('id') * 0.001).toFixed(3));
				});

				//	When the expand/collapse button is clicked
				$(expandCollapseButton).click(function (e) {
					//	Check for the current class on the transcript wrapper and then 
					//	toggle between the expanded or collapsed state
					if ($(transcriptWrap).hasClass('expanded')) {
						$(transcriptWrap).removeClass().removeAttr('style');
					} else {
						//	The transcript wrapper needs an explicit height for the animated
						//	transition to run smoothly, so that's why $(transcript).height() is here
						$(transcriptWrap).addClass('expanded').CSS('height', $(transcriptText).outerHeight());
					}
					//	Prevent default behavior for clicking the link
					e.preventDefault();
				});

				//	Disable auto-scrolling when hovering with the mouse over the transcript area
				$(transcriptWrap)
					.mouseenter(function () {
						transcriptHovered = true;
						//	Clearing the timeout prevents any issues from happening if the user 
						//	mouseenters again within two seconds of mouseleave
						clearTimeout(transcriptHoveredTO);
						//	This stops any scrolling animations currently in progress
						TRANSCRIPT.stopScrolling();
					})
					.mouseleave(function () {
						//	When mousing out, wait two seconds, then re-enable auto-scroll
						transcriptHoveredTO = setTimeout(function () { transcriptHovered = false; }, 2000);
					});
			}
		},
		
		stopScrolling: function () {
			//	This is its own function since the embed iframe also needs access to it
			$(transcriptWrap).stop();
		},
		
		update: function (currentTime) {
			//	If there isn't a current phrase stored yet
			if (currentPhrase == null) {
				//	Store the first phrase as the current phrase
				currentPhrase = 0;
				//	Highlight the very first phrase
				$(transcriptSpans).eq(currentPhrase).addClass('current-phrase');
			} else {
				//	this means there is a current phrase stored
				//	if the current timecode is lower than the current phrase timecode
				if (currentTime < $(transcriptSpans[currentPhrase]).attr('id')) {
					//	first check if there is a phrase below the current phrase
					if (transcriptSpans[currentPhrase - 1]) {
						//	store the current phrase so it can be referenced later
						previousPhrase = currentPhrase;
						//	while currentTime is lower than the timecode of the phrase below the current phrase:
						while (currentTime <= $(transcriptSpans[currentPhrase - 1]).attr('id')) {
							//	decrement the current phrase by 1
							currentPhrase = currentPhrase - 1;
						}
						//	remove the highlight class from the current phrase
						$(transcriptSpans[previousPhrase]).removeClass();
						//	apply the highlight class to what is now the current phrase
						$(transcriptSpans[currentPhrase]).addClass('current-phrase');
					}
				} else if (currentTime > $(transcriptSpans[currentPhrase]).attr('id')) {
					//	else if the currentTime is higher than the current phrase timecode
					//	first check if there is a phrase above the current phrase
					if (transcriptSpans[currentPhrase + 1]) {
						//	store the current phrase so it can be referenced later
						previousPhrase = currentPhrase;
						//	while currentTime is higher than the timecode of the phrase above the current phrase:
						while (currentTime >= $(transcriptSpans[currentPhrase + 1]).attr('id')) {
							//	increment the current phrase by 1
							currentPhrase = currentPhrase + 1;
						}
						//	remove the highlight class from the current phrase
						$(transcriptSpans[previousPhrase]).removeClass();
						//	apply the highlight class to what is now the current phrase
						$(transcriptSpans[currentPhrase]).addClass('current-phrase');
					}
				}	//	else (currentTime matches the currentPhrase ID) do nothing, the current phrase is correct

				//	Now that we have the current phrase sorted out, it's time to autoscroll
				//	Only proceed with autoscroll if the transcript isn't being moused over
				if (transcriptHovered == false) {
					//	Get the y position of the current phrase (relative to #transcript)
					if ($(transcriptSpans[currentPhrase]).position()) {
						currentPhraseYPos = $(transcriptSpans[currentPhrase]).position().top;
						//	In order to tell when to scroll down, we add the current scroll position, plus the height of 
						//	the scrollable container, plus the height of two lines of text (to scroll once the current 
						//	line is two from the bottom). If the currentPhraseYPos is greater than that, we need to scroll down.
						if (currentPhraseYPos >= ($(transcriptWrap).scrollTop() + $(transcriptWrap).outerHeight()) - (transcriptLineHeight * 2)) {
							//	Do an animated scrolling transition to scroll to where the current phrase is. Note that this
							//	actually results in being one line above where the current phrase is, because of the padding 
							//	at the top of the transcript wrap area. This transition lasts 1000 milliseconds.
							//	Using not(':animated') prevents multiple animations from queueing up while the first is going.
							$(transcriptWrap).not(':animated').animate({scrollTop: currentPhraseYPos - transcriptLineHeight - (currentPhraseYPos % transcriptLineHeight)}, 1000);
						} else if (currentPhraseYPos < $(transcriptWrap).scrollTop()) {
							//	Same thing here, possibly more efficient as its own condition here though,
							//	rather than putting both in an or statement and evaluating both each time.
							$(transcriptWrap).not(':animated').animate({scrollTop: currentPhraseYPos - transcriptLineHeight - (currentPhraseYPos % transcriptLineHeight)}, 1000);
						}
					}
				} else {
					//	If the transcript is currently hovered over this ensures that scrolling stops right away.
					TRANSCRIPT.stopScrolling();
				}
			}
		}
	};
}();
		


/*** transcript box *****/

#transcript {
	/*	These needed to be adjusted a bit for this theme */
	margin: 20px 0 0 0;
	position: relative;
}
#transcript-wrap {
	/*	These needed to be adjusted a bit for this theme */
	background: #fdfdfd url(/media/img/caption-box-bg.png) repeat-x 0 100%;
	-webkit-border-radius: 5px;
	-moz-border-radius: 5px;
	border-radius: 5px;
	-webkit-box-shadow: 0 0 1px rgba(0,0,0,0.25), 1px 1px 2px rgba(0,0,0,0.2);
	-moz-box-shadow: 0 0 1px rgba(0,0,0,0.25), 1px 1px 2px rgba(0,0,0,0.2);
	box-shadow: 0 0 1px rgba(0,0,0,0.25), 1px 1px 2px rgba(0,0,0,0.2);
	height: 200px;
	min-height: 80px;
	overflow-y: scroll;
	-webkit-transition: all 0.5s;
	-moz-transition: all 0.5s;
	transition: all 0.5s;
}
#transcript-text {
	padding: 20px 80px 20px 20px;
	position: relative;
}
#transcript-text p {
	color: #555;
	/*	Needed to specify a font-family for this theme */
	font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
	font-size: 14px;
	line-height: 20px;
}
#transcript-text span {
	height: 20px;
}
#transcript-text .current-phrase,
#transcript-text span:hover {
	background: #ccc;
	color: #111;
	cursor: pointer;
}
#transcript-text span:hover {
	background: #c2ddff;
}
#transcript-expand-button {
	position: absolute;
	right: 28px;
	top: 18px;
	z-index: 2;	
}
#transcript-expand-button a {
	background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(255,255,255,0.25)), to(rgba(0,0,0,0.25)));
	background-image: -moz-linear-gradient(top, rgba(255,255,255,0.25), rgba(0,0,0,0.25));
	border: 1px solid #999;
	border-top-color: #bbb;
	border-left-color: #bbb;
	-webkit-border-radius: 3px;
	-moz-border-radius: 3px;
	border-radius: 3px;
	-webkit-box-shadow: 1px 1px 2px rgba(0,0,0,0.25);
	-moz-box-shadow: 1px 1px 2px rgba(0,0,0,0.25);
	box-shadow: 1px 1px 2px rgba(0,0,0,0.25);
	display: block;
	height: 24px;
	opacity: 0.5;
	text-indent: -9999px;
	-webkit-transition: opacity 0.2s;
	-moz-transition: opacity 0.2s;
	transition: opacity 0.2s;	
	width: 24px;
}
#transcript-expand-button a:hover {
	opacity: 1;
}
#transcript-expand-button span {
	background: url(/media/img/icons/caption-box-expand-button.png) no-repeat 50% 50%;
	display: block;
	height: 16px;
	left: 50%;
	margin: -8px 0 0 -4px;
	position: absolute;
	top: 50%;
	width: 8px;
}
#transcript-expand-button a:active {
	position: relative;
	top: 1px;
}

The Index Template (home page)

The index page (home page) for your theme can contain whatever you want. The most common format for the home page is to show the most recent 4 videos (or featured playlist) as a large feature or slider, along with the most recent 10 videos as a list below it. Other possible elements could include a list of popular videos, a list of recent comments, or a list of videos by category.

One of the most important objects on a Vidcaster video site is the 'item'. An item is simply a video. Throughout a theme, you can show different lists of items and compose the HTML however you want by using item attributes such as summary, headline, pub_date, get_absolute_url, fetch_thumbnail_url, group etc. in this format:


	<ul>
	{% for item in above_fold_list %}
	
							<li>
	
								<a class="thumbnail" href="{{ item.get_absolute_url }}" title="watch {{ item.headline }}"><img src="{% fetch_thumbnail_url item 245 175 %}" alt="video thumbnail for {{ item.headline }}" /><span></span></a>
	
								<h3 class="title"><a href="{{ item.get_absolute_url }}">{{ item.headline }}</a></h3>
	
							</li>
	</ul>
							{% endfor %}

The code above is a loop that goes through each item in a list called the 'above_fold_list'. This is a list of your 4 more recent videos, excluding the most recent video, which is called the lead_item. For each item, the thumbnail image will show inside a link that goes directly to the video playback page. Below that is an h3 with a link to the video playback page and the video headline as text.

To get this same information for the most recent video you could do something like this:



	{% if lead_item %}
	
							<div>
	
								<a class="thumbnail" href="{{ lead_item.get_absolute_url }}" title="watch {{ lead_item.headline }}"><img src="{% fetch_thumbnail_url lead_item 245 175 %}" alt="video thumbnail for {{ lead_item.headline }}" /><span></span></a>
	
								<h3 class="title"><a href="{{ lead_item.get_absolute_url }}">{{ lead_item.headline }}</a></h3>
	
							</div>

							{% endif %}

Common elements on the index page

  • lead_item – the most recent video.
  • above_fold_list – the most recent 4 videos, excluding the lead_item.
  • below_fold_list – the most recent 10 videos, excluding the lead_item and above_fold_list.
  • latest – a list of the most recent 10 videos.
  • top_viewed_items – a list of the most popular videos.
  • total_items – the number of total videos. This is used to navigate to the first page of the video archive if there are more than 10 videos <a href="/videos/archive/?page=2">View {{ total_items|add:"-10" }} more video{{ total_items|add:"-10"|pluralize }}<a>
  • group – a category that videos can belong to, (i.e. a grouping of items).
  • group_list – a list of all categories that have videos (you must fetch the list {% get_groups as group_list %} before going through the loop).
  • Within the group list, you can also loop through items within each category, for example: {% for item in group.item_set.public_on_site|dictsortreversed:"pub_date"|slice:"6" %}
  • recent_comments_list – a list of recent comments. See example below.

{% if current_site.siteprofile.allow_comments %}
					{% if recent_comments_list %}
					<div id="recent-comments" class="block">
						<div class="block-header">
							<h3>Recent Comments</h3>
						</div>
						<ol>
						{% for comment in recent_comments_list %}
							<li>
								<div class="text">
									<p>{{ comment.comment|truncatewords:17 }}</p>
								</div>
								<div class="meta">
									<img src="{{ comment.get_poster_gravatar_url }}" alt="" />
									<p><span class="author">{{ comment.get_poster_HTML_tag|safe }}</span> said about <span class="video"><a href="{{ comment.item.get_absolute_url }}">{{ comment.item.headline|truncatewords:5 }}</a></span></p>
									<span class="point"></span>
								</div>
							</li>
						{% endfor %}
						</ol>
					</div>
					{% endif %}
					{% endif %}

The above code is says – if the current site has comments enabled and there are comments, show a list of recent comments. Each comment has few attributes you can optionally display, such as the gravatar, headline, absolute_url.

Tip: It is good practice to wrap code in if statements to check for the existence of videos, groups, and lists. This will prevent null errors.

Tip: If you're building a theme on a new Vidcaster site, make sure to watch some of your videos in order to populate lists such as most popular and related videos.

The Video Playback Page

Also known as the details page, this template is a landing page for playback of a video. The get_absolute_url attribute of the item object is the URL for the video playback page of that item. The video playback page contains the actual video player that plays the video. In addition, this template usually contains a title and description of the video and other meta information, a comment form and comments, and a list of related videos.

The Vidcaster player is contained within an iframe. There's logic wrapped around this code to conditionally show the YouTube player or the Vidcaster player. It's best to copy the code exactly as it is shown in the example below. The only things that are safe to change are the height and width of the player.


{% if video_status  == "Youtube" %}
					<iframe width="640" height="360" src="http://www.youtube.com/embed/{{ foreign_video_embed_id }}?modestbranding=1{% if autoplay%}&autoplay=1{%endif%}&rel=0&wmode=transparent" frameborder="0" allowfullscreen></iframe>
					<p id="youtube-player-notice"><strong>Note:</strong> This video is still being imported from YouTube, but once it is finished processing it will be shown in the Vidcaster player.</p>
				{% else %}
					{% if item.valid_youtube %}
					<iframe width="640" height="360" src="http://www.youtube.com/embed/{{ item.valid_youtube }}?modestbranding=1{% if autoplay%}&autoplay=1{%endif%}&rel=0&wmode=transparent" frameborder="0" allowfullscreen></iframe>
					{% else %}	
                                        <iframe id="video-player" title="{{ current_site.name }} Video Player" width="640" height="360" src="http://{{ current_site.domain }}/player/{{ item.code }}/native/autoplay/" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
					<div class="current-video-thumbnail">
						<img src="{% fetch_thumbnail_url item 120 68 %}" alt="" />
					</div>
					{% endif %}
				{% endif %}

Transcription is a paid feature within Vidcaster. In order to display transcribed content for a video, the transcription widget must be included directly below the video player, like so:


{% if item|transcript_as_HTML %}
				<div id="transcript">
					<div id="transcript-wrap">
						<div id="transcript-expand-button"><a href="#">Expand/collapse<span></span></a></div>
						<div id="transcript-text">
							{{ item|transcript_as_HTML|safe }}
						</div>
					</div>
				</div>
				{% endif %}

Tip: Also make sure to include the transcription jQuery function and CSS to make this functionality work.

Other content on the video playback page

  • {% load tagging_tags %}, {% load comments_tags %}, {% load item_extras %}, {% load distro_extras %}, {% load subtitle_extras %} – load these files at the top of the template to enable functions that affect tagging, comments, transcription and more.
  • {% block meta %} – this block contains facebook open graph metadata ( og:type, og:video, etc. ).
  • item.geo_long and item.geo_lat – geolocation feature for the video. This is a rarely used feature and can be safely eliminated from your theme if desired.
  • related_items – list of related videos.

The other main pieces of content on the video playback page are: video meta info, comments widget, and tags. Below is an example of each.

Tags


{% tags_for_object item as tags %}
			{% if tags %}
				<div id="tags" class="block">
					<div class="block-header">
						<h3>Tags</h3>
					</div>
					<p>{% for tag in tags %}{% if not forloop.first %}, {% endif %}<a href="/videos/tagged/{{ tag|urlencode }}/" rel="tag">{{ tag }}</a>{% endfor %}</p>
				</div>
				{% endif %}		

Video Meta

This example contains item headline, item group (category), pub_date, featured video/edit video meta, summary, and description (long form text).


<h1>{{ item.headline }}</h1>

<p class="meta">{% if item.group %}<span class="group {{ item.group.short_name }}">{{ item.group.get_HTML_tag|safe }}</span>{% endif %}

<span class="date">{{ item.pub_date|date:"F j, Y" }}</span>

{% if edit_state = 'can_edit' %}<span class="edit"><a href="{{ item.get_absolute_url }}edit/">edit this video</a></span>{% endif %}{% if edit_state = 'can_feature' %}<span class="edit"><a href="{{ item.get_absolute_url }}?feature_video">feature this video</a></span>{% endif %}{% if edit_state = 'is_featured' %}<span class="edit"><a href="/Admin/videos/playlists/">This video is featured on your home page</a></span>{% endif %}</p>


{% if item.summary %}
	<div class="summary">
		<p>{{ item.summary }}</p>
	</div>
	{% endif %}
	{% if item.desc %}
	<div class="description">
		{% if HTML_desc %}
		{{ item.desc|safe|urlize|linebreaks }}
		{% else %}
		{{ item.desc|urlize|linebreaks }}
		{% endif %}
	</div>
{% endif %}

Comments Widget

If comments are enabled for a site (which they are by default), you can include Vidcaster's default commenting system. If you like, you can also eliminate all this and replace it with any third party commenting widget of your choice. We have a how-to article specifically about third-party commenting widget integration.

Below is sample code for the default comment form and list of comments. Make sure to see the different views of the comment form – {% comment_form_for_object item %} – depending on whether the user is authenticated or not. Go to a video playback page and view it while logged in, and while logged out in order to see this.


	{% if current_site.siteprofile.allow_comments and preview = "False" %}
				{% comments_for_object item as comments %}
				<div id="comments">
					{% if comments %}
 					<div class="block comment-list">
						<div class="block-header">
							<h3>Comments</h3>
						</div>
						<ol>
							{% for comment in comments %}
							<li id="comment{{ comment.id }}" class="{% if comment.poster_is_registered %}registered{% else %}unregistered{% endif %}">
								<div class="avatar">
									<img src="{{ comment.get_poster_gravatar_url }}" alt="" />
								</div>
								<div class="text">
									{{ comment.comment|urlize|linebreaks }}
								</div>
								<div class="meta">
									<p><strong>{{ comment.get_poster_HTML_tag|safe }}</strong>, {{ comment.date|timesince }} ago.</p>
								</div>
							</li>
							{% endfor %}
						</ol>
					</div>
					{% endif %}
					<div class="block simple-form{% if user.is_authenticated %} authenticated{% else %} unauthenticated{% endif %}">
						<div class="block-header">
							<h3>Post a Comment</h3>
						</div>
						{% comment_form_for_object item %}
					</div>
				</div>
				{% endif %}

The Category Landing Page

Besides item, 'group' is another important object of a Vidcaster site. A group is essentially a category, and we will refer to groups as categories interchangeably. Each item can belong to one group. A group can have many items.

You can get a list all the groups you have on your site that have videos. This is commonly done as a drop-down menu for site navigation. Each group has it's own landing page, with a list of all the videos within that group. You can also include an item's group as part of its meta information.

When you are not on the category landing page template, you typically refer to group simply by group (as in item.group.display_name). When you are within the category landing page template, you refer to the group by selected_group – {{ selected_group.display_name }}.

The category landing page is a pretty simple page. It can contain attributes about the selected_group such as:

  • display_name
  • short_name – slug
  • description
  • masthead – group header image
  • masthead.url – url path for the header image
  • get_podcast_absolute_url – podcast url for the group
  • get_audio_podcast_absolute_url – audio only podcast url
  • /feeds/{{ selected_group.short_name }}/ – RSS feed URI for the group
  • secondary_content_block – an optional block of content for the group. User can put third party widgets or HTML in here.

Other common elements of the category landing page are a list of the videos within the group (selected_items) and a list of recent comments (recent_comments_list).

The Search Results Page

The search results page (also known as the list template) is a simple, high-utility template. It is basically just a list of videos. This template is used for the search results page, and the 'more videos' views of the category and index page if there are more than 10 videos. The list template includes pagination, serving as a video archive. It will paginate with 10 videos per page.

The main building blocks of the search results page are: the header, the list, and the pagination.

  • item_list – list of videos. Use your own item attributes and HTML to make the list look how you want.
  • paginator – pagination that will show up if there are more than 10 videos on the page. {% if is_paginated %}{% paginator %}{% endif %} Use the existing HTML that it generates and customize with CSS.
  • header, query – dynamically displays relevant information depending on whether the pages shows as a search results or an archive page. See example code.
  • hits, first_on_page, last_on_page – displays relevant information on total videos within the search results. (1-10 of 241 videos) See example code.


<h1>{% if header %}{{ header }}{% else %}Videos matching “{{ query }}”{% endif %}</h1>



<p class="displaying">Displaying {{ first_on_page }}-{{ last_on_page }} of {{ hits }} video{{ hits|pluralize }}.</p>

The Custom Page Template

The custom page template (or default template) controls the look of Vidcaster custom pages. Vidcaster sites have the ability to create custom pages within the Vidcaster Admin (Site > Pages). These pages can contain any HTML content you want. In Django language, these are 'flat pages.' Vidcaster flat pages have a few fields, some of which are customized from generic Django flat page structure. The Vidcaster flat page fields are:

  • Title
  • URL – this is the pathname of your custom page. You can add multiple directories into this URL, even if they don't actually exist.
  • Visibility – whether or not you want this page to show up in your navigation. If your base template uses {% get_public_pages as page_list %} as part of the navigation block, you must pay attention to this. If you hardcode your navigation however, this is of course irrelevant.
  • Content – the main area for the HTML content of the page.
  • Head Content – optional content inserted into the head tag.
  • End of Body Content – optional content inserted right before the end of the body tag.

The custom page template itself is pretty simple. It uses the elements detailed above, along with some wrapper divs to contain the content area. In order to create a super flexible custom page template, you can leave out wrapping divs. You could even get the custom page template to show a blank page by default, so you could create a custom page that looks completely different from the rest of your site (if you really wanted to).

Keep this in mind when designing your base template – include as much content as possible within blocks, so that you can eliminate unwanted content blocks from other templates such as the custom page template. Remember, unless content is contained within a block on the base template, you will automatically have to inherit it on all your sub-templates.

Below is an example of a full custom page template. The title and url are used for meta information. The content is the main content area. The headcontent and endbodycontent blocks are included to display scripts and CSS that a user may have included in a specific custom page.


{% extends "base.HTML" %}

{% block title %}{{ flatpage.title }} - {{ current_site.name }}{% endblock title %}
{% block bodyid %} id="{{ flatpage.url|cut:"/" }}"{% endblock bodyid %}
{% block bodyclass %} class="page"{% endblock bodyclass %}
{% block canonical %}<link rel="canonical" href="http://{{ current_site.domain }}/{{ flatpage.url|cut:'/' }}/" />{% endblock canonical %}
{% block headcontent %} {% if flatpage.flatpageextras.head_content %}{{ flatpage.flatpageextras.head_content|safe }}{% endif %} {% endblock %}
{% block content %}

	<div class="section">
		<div class="main-column">
			{{ flatpage.content }}
		</div>
	</div>
{% endblock content %}

{% block endbodycontent %} {% if flatpage.flatpageextras.endbody_content %}{{ flatpage.flatpageextras.endbody_content|safe }}{% endif %} {% endblock %}

This is a tiny and simple template, pretty self explanatory. It is just a bit of HTML that contains a 'powered by Vidcaster' link, as well as a login link that will show if the user is not authenticated. You can add any HTML you want to this template.

Embeddable iFrame Widget

This is used to create an embeddable widget for a group of videos. This template is rarely used and can in most cases just be ignored.