Commit realizado el 12:13:52 08-04-2024

This commit is contained in:
Pagina Web Monito
2024-04-08 12:13:55 -04:00
commit 0c33094de9
7815 changed files with 1365694 additions and 0 deletions

View File

@@ -0,0 +1,65 @@
<?php
/**
* The Video Sitemap Metabox.
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Sitemap;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use MyThemeShop\Helpers\WordPress;
use RankMath\Sitemap\Cache_Watcher;
defined( 'ABSPATH' ) || exit;
/**
* Video_Metabox class.
*/
class Video_Metabox {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
$this->action( 'save_post', 'save_post' );
}
/**
* Check for relevant post type before invalidation.
*
* @param int $post_id Post ID to possibly invalidate for.
*/
public function save_post( $post_id ) {
if (
! $this->can_add_tab( get_post_type( $post_id ) ) ||
false === Helper::is_post_indexable( $post_id ) ||
wp_is_post_revision( $post_id )
) {
return false;
}
Cache_Watcher::invalidate( 'video' );
}
/**
* Show field check callback.
*
* @param string $post_type Post type.
*
* @return boolean
*/
private function can_add_tab( $post_type ) {
return in_array(
$post_type,
(array) Helper::get_settings( 'sitemap.video_sitemap_post_type' ),
true
);
}
}

View File

@@ -0,0 +1,240 @@
<?php
/**
* The Video Sitemap Provider
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Sitemap;
use RankMath\Helper;
use RankMath\Sitemap\Router;
use RankMath\Sitemap\Sitemap;
use RankMath\Sitemap\Providers\Post_Type;
defined( 'ABSPATH' ) || exit;
/**
* Video_Provider class.
*/
class Video_Provider extends Post_Type {
/**
* Check if provider supports given item type.
*
* @param string $type Type string to check for.
* @return boolean
*/
public function handles_type( $type ) {
return 'video' === $type;
}
/**
* Get set of sitemaps index link data.
*
* @param int $max_entries Entries per sitemap.
* @return array
*/
public function get_index_links( $max_entries ) {
$post_types = (array) Helper::get_settings( 'sitemap.video_sitemap_post_type', [] );
if ( empty( $post_types ) ) {
return [];
}
global $wpdb;
$sql = "SELECT p.ID, p.post_modified_gmt FROM {$wpdb->postmeta} as pm
INNER JOIN {$wpdb->posts} as p ON pm.post_id = p.ID
WHERE pm.meta_key = 'rank_math_schema_VideoObject'
AND post_type IN ( '" . join( "', '", esc_sql( $post_types ) ) . "' )
AND post_status IN ( 'publish', 'inherit' )
GROUP BY p.ID
ORDER BY p.post_modified_gmt DESC";
$posts = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore
$total_count = count( $posts );
if ( 0 === $total_count ) {
return [];
}
$max_pages = 1;
if ( $total_count > $max_entries ) {
$max_pages = (int) ceil( $total_count / $max_entries );
}
$all_dates = array_chunk( $posts, $max_entries );
$index = [];
for ( $page_counter = 0; $page_counter < $max_pages; $page_counter++ ) {
$current_page = ( $max_pages > 1 ) ? ( $page_counter + 1 ) : '';
$video = $all_dates[ $page_counter ][0];
$item = $this->do_filter(
'sitemap/index/entry',
[
'loc' => Router::get_base_url( 'video-sitemap' . $current_page . '.xml' ),
'lastmod' => $video['post_modified_gmt'],
],
'video',
$video,
);
if ( ! $item ) {
continue;
}
$index[] = $item;
}
return $index;
}
/**
* Get set of sitemap link data.
*
* @param string $type Sitemap type.
* @param int $max_entries Entries per sitemap.
* @param int $current_page Current page of the sitemap.
* @return array
*/
public function get_sitemap_links( $type, $max_entries, $current_page ) {
rank_math()->variables->setup();
$post_types = (array) Helper::get_settings( 'sitemap.video_sitemap_post_type', [] );
$links = [];
$steps = min( 100, $max_entries );
$offset = ( $current_page > 1 ) ? ( ( $current_page - 1 ) * $max_entries ) : 0;
$total = ( $offset + $max_entries );
$typecount = 0;
$stacked_urls = [];
while ( $total > $offset ) {
$posts = $this->get_posts( $post_types, $steps, $offset );
$offset += $steps;
if ( empty( $posts ) ) {
continue;
}
foreach ( $posts as $post ) {
if ( ! Helper::is_post_indexable( $post->ID ) ) {
continue;
}
$url = $this->get_url( $post );
if ( ! isset( $url['loc'] ) ) {
continue;
}
/**
* Filter URL entry before it gets added to the sitemap.
*
* @param array $url Array of URL parts.
* @param string $type URL type.
* @param object $user Data object for the URL.
*/
$url = $this->do_filter( 'sitemap/entry', $url, 'post', $post );
if ( empty( $url ) ) {
continue;
}
$stacked_urls[] = $url['loc'];
$links[] = $url;
}
unset( $post, $url );
}
return $links;
}
/**
* Produce array of URL parts for given post object.
*
* @param object $post Post object to get URL parts for.
* @return array|boolean
*/
protected function get_url( $post ) {
$url = [];
/**
* Filter the URL Rank Math SEO uses in the XML sitemap.
*
* Note that only absolute local URLs are allowed as the check after this removes external URLs.
*
* @param string $url URL to use in the XML sitemap
* @param object $post Post object for the URL.
*/
$url['loc'] = $this->do_filter( 'sitemap/xml_post_url', get_permalink( $post ), $post );
/**
* Do not include external URLs.
*
* @see https://wordpress.org/plugins/page-links-to/ can rewrite permalinks to external URLs.
*/
if ( 'external' === $this->get_classifier()->classify( $url['loc'] ) ) {
return false;
}
$canonical = Helper::get_post_meta( 'canonical', $post->ID );
if ( '' !== $canonical && $canonical !== $url['loc'] ) {
/*
* Let's assume that if a canonical is set for this page and it's different from
* the URL of this post, that page is either already in the XML sitemap OR is on
* an external site, either way, we shouldn't include it here.
*/
return false;
}
unset( $canonical );
$schemas = get_post_meta( $post->ID, 'rank_math_schema_VideoObject' );
if ( empty( $schemas ) ) {
return false;
}
$url['author'] = $post->post_author;
$url['videos'] = [];
foreach ( $schemas as $schema ) {
$url['videos'][] = [
'title' => ! empty( $schema['name'] ) ? Helper::replace_vars( $schema['name'], $post ) : '',
'thumbnail_loc' => ! empty( $schema['thumbnailUrl'] ) ? Helper::replace_vars( $schema['thumbnailUrl'], $post ) : '',
'description' => ! empty( $schema['description'] ) ? Helper::replace_vars( $schema['description'], $post ) : '',
'publication_date' => ! empty( $schema['uploadDate'] ) ? Helper::replace_vars( $schema['uploadDate'], $post ) : '',
'content_loc' => ! empty( $schema['contentUrl'] ) ? Helper::replace_vars( $schema['contentUrl'], $post ) : '',
'player_loc' => ! empty( $schema['embedUrl'] ) ? Helper::replace_vars( $schema['embedUrl'], $post ) : '',
'duration' => ! empty( $schema['duration'] ) ? Helper::duration_to_seconds( Helper::replace_vars( $schema['duration'], $post ) ) : '',
'tags' => ! empty( $schema['metadata']['tags'] ) ? Helper::replace_vars( $schema['metadata']['tags'], $post ) : '',
'family_friendly' => ! empty( $schema['isFamilyFriendly'] ) ? 'yes' : 'no',
'rating' => ! empty( $schema['metadata']['rating'] ) ? $schema['metadata']['rating'] : '',
];
}
return $url;
}
/**
* Retrieve set of posts with optimized query routine.
*
* @param array $post_types Post type to retrieve.
* @param int $count Count of posts to retrieve.
* @param int $offset Starting offset.
*
* @return object[]
*/
protected function get_posts( $post_types, $count, $offset ) { // phpcs:ignore
global $wpdb;
$sql = "SELECT p.* FROM {$wpdb->postmeta} as pm
INNER JOIN {$wpdb->posts} as p ON pm.post_id = p.ID
WHERE pm.meta_key = 'rank_math_schema_VideoObject'
AND post_type IN ( '" . join( "', '", esc_sql( $post_types ) ) . "' )
AND post_status IN ( 'publish', 'inherit' )
AND post_password = ''
GROUP BY p.ID
ORDER BY p.post_modified DESC
LIMIT %d OFFSET %d";
return $wpdb->get_results( $wpdb->prepare( $sql, $count, $offset ) ); // phpcs:ignore
}
}

View File

@@ -0,0 +1,226 @@
<?php
/**
* The Video Sitemap Module
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
namespace RankMathPro\Sitemap;
use RankMath\KB;
use RankMath\Helper;
use RankMath\Traits\Hooker;
use RankMath\Sitemap\Router;
use RankMath\Sitemap\Cache_Watcher;
defined( 'ABSPATH' ) || exit;
/**
* Video_Sitemap class.
*/
class Video_Sitemap {
use Hooker;
/**
* The Constructor.
*/
public function __construct() {
if ( is_admin() ) {
$this->filter( 'rank_math/settings/sitemap', 'add_settings', 11 );
new Video_Metabox();
}
if ( ! $this->can_add_sitemap() ) {
return;
}
$this->filter( 'rank_math/sitemap/providers', 'add_provider' );
$this->filter( 'rank_math/sitemap/video_urlset', 'xml_urlset' );
$this->filter( 'rank_math/sitemap/xsl_video', 'sitemap_xsl' );
$this->filter( 'rank_math/sitemap/video_stylesheet_url', 'stylesheet_url' );
$this->filter( 'rank_math/sitemap/video_sitemap_url', 'sitemap_url', 10, 2 );
$this->action( 'transition_post_status', 'status_transition', 10, 3 );
}
/**
* Add module settings into general optional panel.
*
* @param array $tabs Array of option panel tabs.
*
* @return array
*/
public function add_settings( $tabs ) {
$sitemap_url = Router::get_base_url( 'video-sitemap.xml' );
$tabs['video-sitemap'] = [
'icon' => 'rm-icon rm-icon-video',
'title' => esc_html__( 'Video Sitemap', 'rank-math-pro' ),
'desc' => wp_kses_post( sprintf( __( 'Video Sitemaps give search engines information about video content on your site. More information: <a href="%s" target="_blank">Video Sitemaps</a>', 'rank-math-pro' ), KB::get( 'video-sitemap', 'Options Panel Sitemap Video' ) ) ),
'file' => dirname( __FILE__ ) . '/settings-video.php',
/* translators: Video Sitemap Url */
'after_row' => '<div class="notice notice-alt notice-info info inline rank-math-notice"><p>' . sprintf( esc_html__( 'Your Video Sitemap index can be found here: %s', 'rank-math-pro' ), '<a href="' . $sitemap_url . '" target="_blank">' . $sitemap_url . '</a>' ) . '</p></div>',
];
return $tabs;
}
/**
* Add video sitemap provider.
*
* @param array $providers Sitemap provider registry.
*/
public function add_provider( $providers ) {
$providers[] = new \RankMathPro\Sitemap\Video_Provider();
return $providers;
}
/**
* Produce XML output for video urlset.
*
* @return string
*/
public function xml_urlset() {
return '<urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
. 'xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd '
. 'http://www.google.com/schemas/sitemap-video/1.1 http://www.google.com/schemas/sitemap-video/1.1/sitemap-video.xsd" '
. 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" '
. 'xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">' . "\n";
}
/**
* Stylesheet Url for video.
*
* @return string
*/
public function stylesheet_url() {
$stylesheet_url = preg_replace( '/(^http[s]?:)/', '', Router::get_base_url( 'video-sitemap.xsl' ) );
return '<?xml-stylesheet type="text/xsl" href="' . $stylesheet_url . '"?>';
}
/**
* Stylesheet for Video Sitemap.
*
* @param string $title Title for stylesheet.
*/
public function sitemap_xsl( $title ) {
require_once 'sitemap-xsl.php';
}
/**
* Build the `<url>` tag for a given URL.
*
* @param array $url Array of parts that make up this entry.
* @param Renderer $renderer Sitemap renderer class object.
* @return string
*/
public function sitemap_url( $url, $renderer ) {
$output = $renderer->newline( '<url>', 1 );
$output .= $renderer->newline( '<loc>' . $renderer->encode_url_rfc3986( htmlspecialchars( $url['loc'] ) ) . '</loc>', 2 );
if ( ! empty( $url['videos'] ) ) {
foreach ( $url['videos'] as $video ) {
$date = null;
if ( ! empty( $video['publication_date'] ) ) {
// Create a DateTime object date in the correct timezone.
$date = $renderer->timezone->format_date( $video['publication_date'] );
}
$output .= $renderer->newline( '<video:video>', 2 );
$output .= $renderer->add_cdata( $video['title'], 'video:title', 3 );
$output .= empty( $date ) ? '' : $renderer->newline( '<video:publication_date>' . htmlspecialchars( $date ) . '</video:publication_date>', 3 );
$output .= $renderer->add_cdata( $video['description'], 'video:description', 3 );
if ( ! empty( $video['player_loc'] ) ) {
$output .= $renderer->newline( '<video:player_loc>' . esc_url( $video['player_loc'] ) . '</video:player_loc>', 3 );
}
foreach ( [ 'thumbnail_loc', 'content_loc' ] as $prop ) {
if ( empty( $video[ $prop ] ) ) {
continue;
}
/**
* Filter the video content and thumbnail location:
* - rank_math/sitemap/video/thumbnail_loc
* - rank_math/sitemap/video/content_loc
*/
$value = $this->do_filter( "sitemap/video/{$prop}", $video[ $prop ] );
$output .= $renderer->newline( "<video:{$prop}>" . esc_url( $value ) . "</video:{$prop}>", 3 );
}
if ( ! empty( $video['tags'] ) ) {
$tags = explode( ', ', $video['tags'] );
foreach ( $tags as $tag ) {
$output .= $renderer->add_cdata( $tag, 'video:tag', 3 );
}
}
if ( ! empty( $video['rating'] ) ) {
$output .= $renderer->newline( '<video:rating>' . absint( $video['rating'] ) . '</video:rating>', 3 );
}
if ( ! empty( $video['duration'] ) ) {
$output .= $renderer->newline( '<video:duration>' . esc_html( $video['duration'] ) . '</video:duration>', 3 );
}
$output .= $renderer->newline( '<video:family_friendly>' . $video['family_friendly'] . '</video:family_friendly>', 3 );
$output .= $renderer->newline( '<video:uploader info="' . get_author_posts_url( $url['author'] ) . '">' . ent2ncr( esc_html( get_the_author_meta( 'display_name', $url['author'] ) ) ) . '</video:uploader>', 3 );
$output .= $renderer->newline( '</video:video>', 2 );
}
}
$output .= $renderer->newline( '</url>', 1 );
/**
* Filters the output for the sitemap url tag.
*
* @param string $output The output for the sitemap url tag.
* @param array $url The sitemap url array on which the output is based.
*/
return $this->do_filter( 'sitemap_url', $output, $url );
}
/**
* Whether to add Video Sitemap.
*
* @return booleans
*/
private function can_add_sitemap() {
if ( ! Helper::get_settings( 'sitemap.hide_video_sitemap' ) || current_user_can( 'manage_options' ) ) {
return true;
}
return isset( $_SERVER['HTTP_USER_AGENT'] ) && preg_match( '/bot|crawl|slurp|spider|mediapartners/i', $_SERVER['HTTP_USER_AGENT'] );
}
/**
* Invalidate News Sitemap cache when a scheduled post is published.
*
* @param string $new_status New Status.
* @param string $old_status Old Status.
* @param object $post Post Object.
*/
public function status_transition( $new_status, $old_status, $post ) {
if ( $old_status === $new_status || 'publish' !== $new_status ) {
return;
}
$post_types = (array) Helper::get_settings( 'sitemap.video_sitemap_post_type', [] );
if ( ! in_array( $post->post_type, $post_types, true ) ) {
return;
}
if ( false === Helper::is_post_indexable( $post->ID ) ) {
return;
}
Cache_Watcher::invalidate( 'video' );
}
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* Sitemap - Video
*
* @since 1.0.0
* @package RankMath
* @subpackage RankMathPro
* @author MyThemeShop <admin@mythemeshop.com>
*/
use RankMath\Helper;
defined( 'ABSPATH' ) || exit;
$cmb->add_field(
[
'id' => 'hide_video_sitemap',
'type' => 'toggle',
'name' => esc_html__( 'Hide Sitemap', 'rank-math-pro' ),
'desc' => esc_html__( 'Hide the sitemap from normal visitors?', 'rank-math-pro' ),
]
);
$post_types = Helper::choices_post_types();
if ( isset( $post_types['attachment'] ) && Helper::get_settings( 'general.attachment_redirect_urls', true ) ) {
unset( $post_types['attachment'] );
}
$cmb->add_field(
[
'id' => 'video_sitemap_post_type',
'type' => 'multicheck_inline',
'name' => esc_html__( 'Video Post Type', 'rank-math-pro' ),
'desc' => esc_html__( 'Select the post type where you use videos and want them to be shown in the Video search.', 'rank-math-pro' ),
'options' => $post_types,
'default' => array_keys( $post_types ),
]
);
$cmb->add_field(
[
'id' => 'video_sitemap_custom_fields',
'type' => 'textarea_small',
'name' => esc_html__( 'Custom Fields', 'rank-math-pro' ),
'desc' => esc_html__( 'List of custom fields name to check for video content. Add one per line.', 'rank-math-pro' ),
'default' => '',
'classes' => 'rank-math-advanced-option',
]
);

View File

@@ -0,0 +1,222 @@
<?php
/**
* Sitemap Video stylesheet.
*
* @package RankMath
* @subpackage RankMath\Sitemap
*/
use RankMath\Sitemap\Router;
use RankMath\Sitemap\Sitemap;
defined( 'ABSPATH' ) || exit;
// Echo so opening tag doesn't get confused for PHP.
echo '<?xml version="1.0" encoding="UTF-8"?>';
?>
<xsl:stylesheet version="2.0"
xmlns:html="http://www.w3.org/TR/REC-html40"
xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<html>
<head>
<title><?php echo esc_html( $title ); ?></title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
body {
font-size: 14px;
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif;
margin: 0;
color: #545353;
}
a {
color: #05809e;
text-decoration: none;
}
h1 {
font-size: 24px;
font-family: Verdana,Geneva,sans-serif;
font-weight: normal;
margin: 0;
}
#description {
background-color: #4275f4;
padding: 20px 40px;
color: #fff;
padding: 30px 30px 20px;
}
#description h1,
#description p,
#description a {
color: #fff;
margin: 0;
font-size: 1.1em;
}
#description h1 {
font-size: 2em;
margin-bottom: 1em;
}
#description p {
margin-top: 5px;
}
#content {
padding: 20px 30px;
background: #fff;
max-width: 75%;
margin: 0 auto;
}
table {
border: none;
border-collapse: collapse;
font-size: .9em;
width: 100%;
}
th {
background-color: #4275f4;
color: #fff;
text-align: left;
padding: 15px;
font-size: 14px;
cursor: pointer;
}
td {
padding: 10px;
border-bottom: 1px solid #ddd;
}
tbody tr:nth-child(even) {
background-color: #f7f7f7;
}
table td a {
display: block;
}
</style>
</head>
<body>
<div id="description">
<h1><?php esc_html_e( 'Video Sitemap', 'rank-math-pro' ); ?></h1>
<?php if ( false === $this->do_filter( 'sitemap/remove_credit', false ) ) : ?>
<p>
<?php
printf(
wp_kses_post(
/* translators: link to rankmath.com */
__( 'This Video Sitemap is generated by <a href="%s" target="_blank">Rank Math WordPress SEO Plugin</a>. It is what search engines like Google use to find and understand the video content on your website.', 'rank-math-pro' )
),
\RankMath\KB::get( 'seo-suite' )
);
?>
</p>
<?php endif; ?>
<p>
<?php
printf(
wp_kses_post(
/* translators: link to rankmath.com */
__( 'Learn more about <a href="%s" target="_blank">Video Sitemap</a>.', 'rank-math-pro' )
),
'https://developers.google.com/search/docs/advanced/sitemaps/video-sitemaps'
);
?>
</p>
</div>
<div id="content">
<p>
<?php
printf(
/* translators: xsl value count */
__( 'This XML Sitemap contains <strong>%s</strong> URLs.', 'rank-math-pro' ),
'<xsl:value-of select="count(sitemap:urlset/sitemap:url/video:video)"/>'
);
?>
</p>
<p class="expl">
<?php
printf(
/* translators: Sitemap index link. */
__( '<a href="%s">&#8592; Sitemap Index</a>', 'rank-math-pro' ),
esc_url( Router::get_base_url( Sitemap::get_sitemap_index_slug() . '.xml' ) )
);
?>
</p>
<table id="sitemap" cellpadding="3">
<thead>
<tr>
<th width="15%"><?php echo esc_html__( 'Video', 'rank-math-pro' ); ?></th>
<th width="20%"><?php echo esc_html__( 'Title', 'rank-math-pro' ); ?></th>
<th width="20%"><?php echo esc_html__( 'Description', 'rank-math-pro' ); ?></th>
<th width="15%"><?php echo esc_html__( 'Tags', 'rank-math-pro' ); ?></th>
<th width="15%"><?php echo esc_html__( 'Last Mod.', 'rank-math-pro' ); ?></th>
</tr>
</thead>
<tbody>
<xsl:for-each select="sitemap:urlset/sitemap:url">
<xsl:for-each select="video:video">
<tr>
<td>
<xsl:variable name="thumbURL">
<xsl:value-of select="video:thumbnail_loc"/>
</xsl:variable>
<xsl:variable name="flvURL">
<xsl:value-of select="video:player_loc"/>
</xsl:variable>
<a href="{$flvURL}">
<xsl:choose>
<xsl:when test="$thumbURL != ''">
<img src="{$thumbURL}" width="80" height="60" />
</xsl:when>
<xsl:otherwise>
-
</xsl:otherwise>
</xsl:choose>
</a>
</td>
<td>
<xsl:variable name="itemURL">
<xsl:value-of select="../sitemap:loc"/>
</xsl:variable>
<a href="{$itemURL}">
<xsl:value-of select="video:title"/>
</a>
</td>
<td>
<xsl:variable name="desc">
<xsl:value-of select="video:description"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="string-length($desc) &lt; 200">
<xsl:value-of select="$desc"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat(substring($desc,1,200),' ...')"/>
</xsl:otherwise>
</xsl:choose>
</td>
<td>
<xsl:for-each select="video:tag">
<xsl:value-of select="."/>,
</xsl:for-each>
</td>
<td>
<xsl:value-of select="concat(substring(video:publication_date,0,11),concat(' ', substring(video:publication_date,12,5)),concat(' ', substring(video:publication_date,20,6)))"/>
</td>
</tr>
</xsl:for-each>
</xsl:for-each>
</tbody>
</table>
</div>
</body>
</html>
</xsl:template>
</xsl:stylesheet>