diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..dea5525
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "includes/twitter-cards-php"]
+ path = includes/twitter-cards-php
+ url = https://github.com/niallkennedy/twitter-cards-php.git
diff --git a/class-twitter-card-wp.php b/class-twitter-card-wp.php
new file mode 100644
index 0000000..0f904de
--- /dev/null
+++ b/class-twitter-card-wp.php
@@ -0,0 +1,110 @@
+ element from a name and value
+ *
+ * @param string $name name attribute value
+ * @param string|int $value value attribute value
+ * @param bool $xml include a trailing slash for XML. encode attributes for XHTML in PHP 5.4+
+ * @return meta element or empty string if name or value not valid
+ */
+ public static function build_meta_element( $name, $value, $xml = false ) {
+ if ( ! ( is_string( $name ) && $name && ( is_string( $value ) || ( is_int( $value ) && $value > 0 ) ) ) )
+ return '';
+ return '' : '>' ) . "\n";
+ }
+
+ /**
+ * Pass all URLs through esc_url_raw. Unset the property if URL rejected
+ *
+ * @since 1.0
+ * @uses esc_url_raw()
+ */
+ private function filter_urls() {
+ if ( isset( $this->url ) ) {
+ $this->url = esc_url_raw( $this->url );
+ if ( ! $this->url )
+ unset( $this->url );
+ }
+
+ if ( isset( $this->image ) && isset( $this->image->url ) ) {
+ $this->image->url = esc_url_raw( $this->image->url );
+ if ( ! $this->image->url )
+ unset( $this->image );
+ }
+
+ if ( isset( $this->video ) && isset( $this->video->url ) ) {
+ $this->video->url = esc_url_raw( $this->video->url );
+ if ( $this->video->url ) {
+ if ( isset( $this->video->stream ) && isset( $this->video->stream->url ) )
+ $this->video->stream->url = esc_url_raw( $this->video->stream->url );
+ if ( ! $this->video->stream->url )
+ unset( $this->video->stream );
+ } else {
+ unset( $this->video );
+ }
+ }
+ }
+
+ /**
+ * Build a string of elements representing the object
+ * Pass URLs through esc_url_raw to preserve site preferences
+ *
+ * @since 1.0
+ * @param string $style markup style. "xml" adds a trailing slash to the meta void element
+ * @return string elements or empty string if minimum requirements not met
+ */
+ private function generate_markup( $style = 'html' ) {
+ $xml = false;
+ if ( $style === 'xml' )
+ $xml = true;
+ $this->filter_urls();
+ $t = apply_filters( 'twitter_cards_properties', $this->toArray() );
+ if ( ! is_array( $t ) || empty( $t ) )
+ return '';
+ $s = '';
+ foreach ( $t as $name => $value ) {
+ $s .= self::build_meta_element( $name, $value, $xml );
+ }
+ return $s;
+ }
+
+ /**
+ * Output object properties as HTML meta elements with name and value attributes
+ *
+ * @return string HTML elements or empty string if minimum requirements not met for card type
+ */
+ public function asHTML() {
+ return $this->generate_markup();
+ }
+
+ /**
+ * Output object properties as XML meta elements with name and value attributes
+ *
+ * @since 1.0
+ * @return string XML elements or empty string if minimum requirements not met for card type
+ */
+ public function asXML() {
+ return $this->generate_markup( 'xml' );
+ }
+}
+?>
\ No newline at end of file
diff --git a/includes/twitter-cards-php b/includes/twitter-cards-php
new file mode 160000
index 0000000..0a2f64e
--- /dev/null
+++ b/includes/twitter-cards-php
@@ -0,0 +1 @@
+Subproject commit 0a2f64e8e536799f314ceab356882a64b745e340
diff --git a/twitter-cards.php b/twitter-cards.php
new file mode 100644
index 0000000..bb27be2
--- /dev/null
+++ b/twitter-cards.php
@@ -0,0 +1,79 @@
+
+ *
+ * @since 1.0
+ * @version 1.0
+ */
+class Twitter_Cards {
+ /**
+ * Attach Twitter cards markup to wp_head if single post view
+ *
+ * @since 1.0
+ */
+ public static function init() {
+ if ( is_single() )
+ add_action( 'wp_head', 'Twitter_Cards::markup' );
+ }
+
+ /**
+ * Build a Twitter Card object. Possibly output markup
+ *
+ * @since 1.0
+ */
+ public static function markup() {
+ global $post;
+
+ if ( ! class_exists( 'Twitter_Card_WP' ) )
+ require_once( dirname(__FILE__) . '/class-twitter-card-wp.php' );
+
+ $card = new Twitter_Card_WP();
+ $card->setURL( apply_filters( 'rel_canonical', get_permalink() ) );
+ $post_type = get_post_type();
+ if ( post_type_supports( $post_type, 'title' ) )
+ $card->setTitle( get_the_title() );
+ if ( post_type_supports( $post_type, 'excerpt' ) ) {
+ // one line, no HTML
+ $card->setDescription( self::clean_description( apply_filters( 'the_excerpt', get_the_excerpt() ) ) );
+ }
+ // does current post type and the current theme support post thumbnails?
+ if ( post_type_supports( $post_type, 'thumbnail' ) && function_exists( 'has_post_thumbnail' ) && has_post_thumbnail() ) {
+ list( $post_thumbnail_url, $post_thumbnail_width, $post_thumbnail_height ) = wp_get_attachment_image_src( get_post_thumbnail_id( $post->ID ), 'full' );
+ $card->setImage( $post_thumbnail_url, $post_thumbnail_width, $post_thumbnail_height );
+ }
+
+ if ( apply_filters( 'twitter_cards_htmlxml', 'html' ) === 'xml' )
+ echo $card->asXML();
+ else
+ echo $card->asHTML();
+ }
+
+ public static function clean_description( $description ) {
+ if ( ! ( is_string( $description ) && $description ) )
+ return '';
+
+ $description = wp_strip_all_tags( strip_shortcodes( $description ) );
+ $description = trim( str_replace( array( "\r\n", "\r", "\n" ), ' ', $description ) );
+ $excerpt_more = trim( wp_strip_all_tags( apply_filters('excerpt_more', '[...]') ) );
+ if ( $excerpt_more ) {
+ $excerpt_more_length = strlen( $excerpt_more );
+ if ( strlen( $description ) > $excerpt_more_length && substr_compare( $description, $excerpt_more, $excerpt_more_length * -1, $excerpt_more_length ) === 0 ) {
+ $description = trim( substr( $description, 0, $excerpt_more_length * -1 ) );
+ }
+ }
+ return $description;
+ }
+}
+add_action( 'wp', 'Twitter_Cards::init' );
+endif;
+?>
\ No newline at end of file